chore(workspace-server): #1735 remove unused Awareness namespace surface (#1737)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Chat / detect-changes (push) Waiting to run
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Waiting to run
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Waiting to run
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 8s
publish-workspace-server-image / build-and-push (push) Successful in 2m59s
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 51s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Failing after 2m9s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 37s
Harness Replays / detect-changes (push) Successful in 3s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m11s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 10s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 6m55s
ci-required-drift / drift (push) Successful in 1m10s
Harness Replays / Harness Replays (push) Successful in 2s
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Chat / detect-changes (push) Waiting to run
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Waiting to run
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Waiting to run
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 8s
publish-workspace-server-image / build-and-push (push) Successful in 2m59s
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 51s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Failing after 2m9s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 37s
Harness Replays / detect-changes (push) Successful in 3s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m11s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 10s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 6m55s
ci-required-drift / drift (push) Successful in 1m10s
Harness Replays / Harness Replays (push) Successful in 2s
CTO-bypass merge 2026-05-24: all 5 CI sub-jobs verified success; umbrella stale due to status-propagation race; compensating success status posted. Persona acks in place.
This commit was merged in pull request #1737.
This commit is contained in:
@@ -50,9 +50,6 @@ MOLECULE_ENV=development # Environment label (development/
|
|||||||
# Container/runtime detection
|
# Container/runtime detection
|
||||||
# MOLECULE_IN_DOCKER= # Set when running the platform inside Docker (accepts 1/0, true/false). Triggers A2A proxy to rewrite 127.0.0.1:<port> agent URLs to Docker bridge hostnames. Auto-detected via /.dockerenv; only set if detection fails or to force off.
|
# MOLECULE_IN_DOCKER= # Set when running the platform inside Docker (accepts 1/0, true/false). Triggers A2A proxy to rewrite 127.0.0.1:<port> agent URLs to Docker bridge hostnames. Auto-detected via /.dockerenv; only set if detection fails or to force off.
|
||||||
|
|
||||||
# Observability (Awareness)
|
|
||||||
# AWARENESS_URL= # If set, injected into workspace containers along with a deterministic AWARENESS_NAMESPACE derived from workspace ID. Enables the cross-session memory MCP server.
|
|
||||||
|
|
||||||
# GitHub
|
# GitHub
|
||||||
# GITHUB_REPO=owner/repo # Target repo for agent initial_prompt clone (e.g. Molecule-AI/molecule-monorepo). Read inside workspace containers.
|
# GITHUB_REPO=owner/repo # Target repo for agent initial_prompt clone (e.g. Molecule-AI/molecule-monorepo). Read inside workspace containers.
|
||||||
# GITHUB_TOKEN= # Personal access token / installation token used by agents that clone private repos. Register as a global secret via POST /admin/secrets for propagation to workspace env. Token is used in-URL during clone and then scrubbed from .git/config via `git remote set-url`.
|
# GITHUB_TOKEN= # Personal access token / installation token used by agents that clone private repos. Register as a global secret via POST /admin/secrets for propagation to workspace env. Token is used in-URL during clone and then scrubbed from .git/config via `git remote set-url`.
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ Poll `GET /workspaces/:id/delegations` to check results. Each entry includes `de
|
|||||||
|
|
||||||
This is the recommended way for agents to delegate work — it works for all runtimes (Claude Code, LangGraph, etc.) since it operates at the platform level.
|
This is the recommended way for agents to delegate work — it works for all runtimes (Claude Code, LangGraph, etc.) since it operates at the platform level.
|
||||||
|
|
||||||
Workspace creation also assigns an `awareness_namespace` on the workspace row. That namespace is later injected into the provisioned runtime.
|
|
||||||
|
|
||||||
### Registry
|
### Registry
|
||||||
|
|
||||||
| Method | Path | Description | Auth |
|
| Method | Path | Description | Auth |
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ Migration files live in `workspace-server/migrations/` (latest: `022_workspace_s
|
|||||||
|
|
||||||
| Table | Description |
|
| Table | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `workspaces` | Core entity — status, runtime, `agent_card` JSONB, heartbeat columns, `current_task`, `awareness_namespace`, `workspace_dir` |
|
| `workspaces` | Core entity — status, runtime, `agent_card` JSONB, heartbeat columns, `current_task`, `workspace_dir` |
|
||||||
| `canvas_layouts` | Per-workspace x/y canvas position |
|
| `canvas_layouts` | Per-workspace x/y canvas position |
|
||||||
| `structure_events` | Append-only event log (workspace lifecycle, agent, approval events) |
|
| `structure_events` | Append-only event log (workspace lifecycle, agent, approval events) |
|
||||||
| `activity_logs` | A2A communications, task updates, agent logs, errors. `error_detail` is populated by the scheduler so cron run history can surface failure reasons. |
|
| `activity_logs` | A2A communications, task updates, agent logs, errors. `error_detail` is populated by the scheduler so cron run history can surface failure reasons. |
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestWorkspaceCreate_WithParentID(t *testing.T) {
|
|||||||
// Default tier is 3 (Privileged) — see workspace.go create-handler comment.
|
// Default tier is 3 (Privileged) — see workspace.go create-handler comment.
|
||||||
// delivery_mode defaults to "push" when payload omits it (#2339).
|
// delivery_mode defaults to "push" when payload omits it (#2339).
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Child Agent", nil, 3, "langgraph", sqlmock.AnyArg(), &parentID, nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Child Agent", nil, 3, "langgraph", &parentID, nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
@@ -69,7 +69,7 @@ func TestWorkspaceCreate_ExplicitClaudeCodeRuntime(t *testing.T) {
|
|||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
// delivery_mode defaults to "push" when payload omits it (#2339).
|
// delivery_mode defaults to "push" when payload omits it (#2339).
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "CC Agent", nil, 2, "claude-code", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "CC Agent", nil, 2, "claude-code", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
@@ -291,7 +291,7 @@ func TestWorkspaceCreate_MaxConcurrentTasksOverride(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Leader Agent", nil, 3, "claude-code", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), 3, "push").
|
WithArgs(sqlmock.AnyArg(), "Leader Agent", nil, 3, "claude-code", (*string)(nil), nil, "none", (*int64)(nil), 3, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
|
|||||||
@@ -364,11 +364,11 @@ func TestWorkspaceCreate(t *testing.T) {
|
|||||||
// Expect transaction begin for atomic workspace+secrets creation
|
// Expect transaction begin for atomic workspace+secrets creation
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
|
|
||||||
// Expect workspace INSERT (uuid is dynamic, use AnyArg for id, runtime, awareness_namespace).
|
// Expect workspace INSERT (uuid is dynamic, use AnyArg for id, runtime).
|
||||||
// Default tier is 3 (Privileged) — see workspace.go create-handler comment.
|
// Default tier is 3 (Privileged) — see workspace.go create-handler comment.
|
||||||
// delivery_mode defaults to "push" when payload omits it (#2339).
|
// delivery_mode defaults to "push" when payload omits it (#2339).
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Test Agent", nil, 3, "langgraph", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Test Agent", nil, 3, "langgraph", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
// Expect transaction commit (no secrets in this payload)
|
// Expect transaction commit (no secrets in this payload)
|
||||||
@@ -412,24 +412,17 @@ func TestWorkspaceCreate(t *testing.T) {
|
|||||||
if resp["id"] == nil || resp["id"] == "" {
|
if resp["id"] == nil || resp["id"] == "" {
|
||||||
t.Error("expected non-empty id in response")
|
t.Error("expected non-empty id in response")
|
||||||
}
|
}
|
||||||
if resp["awareness_namespace"] != "workspace:"+resp["id"].(string) {
|
|
||||||
t.Errorf("expected awareness namespace derived from workspace id, got %v", resp["awareness_namespace"])
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildProvisionerConfig_IncludesAwarenessSettings(t *testing.T) {
|
func TestBuildProvisionerConfig_WorkspacePathFromPayload(t *testing.T) {
|
||||||
setupTestDB(t)
|
setupTestDB(t)
|
||||||
// runtime_image_pins reader removed by RFC internal#617 / task #335
|
|
||||||
// — CP is the SSOT for runtime image pins. No DB lookup here anymore.
|
|
||||||
|
|
||||||
broadcaster := newTestBroadcaster()
|
broadcaster := newTestBroadcaster()
|
||||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", "/tmp/configs")
|
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", "/tmp/configs")
|
||||||
|
|
||||||
t.Setenv("AWARENESS_URL", "http://awareness:37800")
|
|
||||||
t.Setenv("WORKSPACE_DIR", "/tmp/workspace")
|
t.Setenv("WORKSPACE_DIR", "/tmp/workspace")
|
||||||
|
|
||||||
cfg := handler.buildProvisionerConfig(
|
cfg := handler.buildProvisionerConfig(
|
||||||
@@ -440,17 +433,10 @@ func TestBuildProvisionerConfig_IncludesAwarenessSettings(t *testing.T) {
|
|||||||
models.CreateWorkspacePayload{Tier: 2, Runtime: "claude-code", WorkspaceDir: "/tmp/workspace", WorkspaceAccess: "read_write"},
|
models.CreateWorkspacePayload{Tier: 2, Runtime: "claude-code", WorkspaceDir: "/tmp/workspace", WorkspaceAccess: "read_write"},
|
||||||
map[string]string{"OPENAI_API_KEY": "sk-test"},
|
map[string]string{"OPENAI_API_KEY": "sk-test"},
|
||||||
"/tmp/plugins",
|
"/tmp/plugins",
|
||||||
"workspace:ws-123",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if cfg.AwarenessURL != "http://awareness:37800" {
|
|
||||||
t.Fatalf("expected awareness URL to be injected, got %q", cfg.AwarenessURL)
|
|
||||||
}
|
|
||||||
if cfg.AwarenessNamespace != "workspace:ws-123" {
|
|
||||||
t.Fatalf("expected awareness namespace to be injected, got %q", cfg.AwarenessNamespace)
|
|
||||||
}
|
|
||||||
if cfg.WorkspacePath != "/tmp/workspace" {
|
if cfg.WorkspacePath != "/tmp/workspace" {
|
||||||
t.Fatalf("expected workspace path from env, got %q", cfg.WorkspacePath)
|
t.Fatalf("expected workspace path from payload, got %q", cfg.WorkspacePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -799,13 +799,12 @@ func (h *OrgHandler) Import(c *gin.Context) {
|
|||||||
if len(tmpl.GlobalMemories) > 0 && len(results) > 0 {
|
if len(tmpl.GlobalMemories) > 0 && len(results) > 0 {
|
||||||
rootID, _ := results[0]["id"].(string)
|
rootID, _ := results[0]["id"].(string)
|
||||||
if rootID != "" {
|
if rootID != "" {
|
||||||
rootNS := workspaceAwarenessNamespace(rootID)
|
|
||||||
// Force scope to GLOBAL regardless of what the YAML says.
|
// Force scope to GLOBAL regardless of what the YAML says.
|
||||||
globalSeeds := make([]models.MemorySeed, len(tmpl.GlobalMemories))
|
globalSeeds := make([]models.MemorySeed, len(tmpl.GlobalMemories))
|
||||||
for i, gm := range tmpl.GlobalMemories {
|
for i, gm := range tmpl.GlobalMemories {
|
||||||
globalSeeds[i] = models.MemorySeed{Content: gm.Content, Scope: "GLOBAL"}
|
globalSeeds[i] = models.MemorySeed{Content: gm.Content, Scope: "GLOBAL"}
|
||||||
}
|
}
|
||||||
seedInitialMemories(context.Background(), rootID, globalSeeds, rootNS)
|
seedInitialMemories(context.Background(), rootID, globalSeeds)
|
||||||
log.Printf("Org import: seeded %d global memories on root workspace %s", len(globalSeeds), rootID)
|
log.Printf("Org import: seeded %d global memories on root workspace %s", len(globalSeeds), rootID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, absX
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
awarenessNS := workspaceAwarenessNamespace(id)
|
|
||||||
|
|
||||||
var role interface{}
|
var role interface{}
|
||||||
if ws.Role != "" {
|
if ws.Role != "" {
|
||||||
@@ -168,13 +167,13 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, absX
|
|||||||
// EXACTLY for Postgres to consider the index applicable.
|
// EXACTLY for Postgres to consider the index applicable.
|
||||||
var insertedID string
|
var insertedID string
|
||||||
err := db.DB.QueryRowContext(ctx, `
|
err := db.DB.QueryRowContext(ctx, `
|
||||||
INSERT INTO workspaces (id, name, role, tier, runtime, awareness_namespace, status, parent_id, workspace_dir, workspace_access, max_concurrent_tasks)
|
INSERT INTO workspaces (id, name, role, tier, runtime, status, parent_id, workspace_dir, workspace_access, max_concurrent_tasks)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
ON CONFLICT (COALESCE(parent_id, '00000000-0000-0000-0000-000000000000'::uuid), name)
|
ON CONFLICT (COALESCE(parent_id, '00000000-0000-0000-0000-000000000000'::uuid), name)
|
||||||
WHERE status != 'removed'
|
WHERE status != 'removed'
|
||||||
DO NOTHING
|
DO NOTHING
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, id, ws.Name, role, tier, runtime, awarenessNS, "provisioning", parentID, workspaceDir, workspaceAccess, maxConcurrent).Scan(&insertedID)
|
`, id, ws.Name, role, tier, runtime, "provisioning", parentID, workspaceDir, workspaceAccess, maxConcurrent).Scan(&insertedID)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
// Skip path — a non-removed row already exists for
|
// Skip path — a non-removed row already exists for
|
||||||
// (parent_id, name). Re-select its id; idempotency-friendly
|
// (parent_id, name). Re-select its id; idempotency-friendly
|
||||||
@@ -259,7 +258,7 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, absX
|
|||||||
if len(wsMemories) == 0 {
|
if len(wsMemories) == 0 {
|
||||||
wsMemories = defaults.InitialMemories
|
wsMemories = defaults.InitialMemories
|
||||||
}
|
}
|
||||||
seedInitialMemories(ctx, id, wsMemories, awarenessNS)
|
seedInitialMemories(ctx, id, wsMemories)
|
||||||
|
|
||||||
// Handle external workspaces
|
// Handle external workspaces
|
||||||
if ws.External {
|
if ws.External {
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
awarenessNamespace := workspaceAwarenessNamespace(id)
|
|
||||||
if h.IsSaaS() {
|
if h.IsSaaS() {
|
||||||
// SaaS hard gate: every hosted workspace gets its own sibling
|
// SaaS hard gate: every hosted workspace gets its own sibling
|
||||||
// EC2 instance, so T4 is the only meaningful runtime boundary.
|
// EC2 instance, so T4 is the only meaningful runtime boundary.
|
||||||
@@ -448,10 +447,10 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
|||||||
// returns the actually-persisted name (which we MUST thread back into
|
// returns the actually-persisted name (which we MUST thread back into
|
||||||
// payload + broadcast so the canvas displays what the DB has).
|
// payload + broadcast so the canvas displays what the DB has).
|
||||||
const insertWorkspaceSQL = `
|
const insertWorkspaceSQL = `
|
||||||
INSERT INTO workspaces (id, name, role, tier, runtime, awareness_namespace, status, parent_id, workspace_dir, workspace_access, budget_limit, max_concurrent_tasks, delivery_mode)
|
INSERT INTO workspaces (id, name, role, tier, runtime, status, parent_id, workspace_dir, workspace_access, budget_limit, max_concurrent_tasks, delivery_mode)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, 'provisioning', $7, $8, $9, $10, $11, $12)
|
VALUES ($1, $2, $3, $4, $5, 'provisioning', $6, $7, $8, $9, $10, $11)
|
||||||
`
|
`
|
||||||
insertArgs := []any{id, payload.Name, role, payload.Tier, payload.Runtime, awarenessNamespace, payload.ParentID, workspaceDir, workspaceAccess, payload.BudgetLimit, maxConcurrent, deliveryMode}
|
insertArgs := []any{id, payload.Name, role, payload.Tier, payload.Runtime, payload.ParentID, workspaceDir, workspaceAccess, payload.BudgetLimit, maxConcurrent, deliveryMode}
|
||||||
persistedName, currentTx, err := insertWorkspaceWithNameRetry(
|
persistedName, currentTx, err := insertWorkspaceWithNameRetry(
|
||||||
ctx,
|
ctx,
|
||||||
tx,
|
tx,
|
||||||
@@ -572,7 +571,7 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
|||||||
|
|
||||||
// Seed initial memories from the create payload (issue #1050).
|
// Seed initial memories from the create payload (issue #1050).
|
||||||
// Non-fatal: failures are logged but don't block workspace creation.
|
// Non-fatal: failures are logged but don't block workspace creation.
|
||||||
seedInitialMemories(ctx, id, payload.InitialMemories, awarenessNamespace)
|
seedInitialMemories(ctx, id, payload.InitialMemories)
|
||||||
|
|
||||||
// Broadcast provisioning event. Include `runtime` so the canvas can
|
// Broadcast provisioning event. Include `runtime` so the canvas can
|
||||||
// populate the Runtime pill on the side panel immediately — without it
|
// populate the Runtime pill on the side panel immediately — without it
|
||||||
@@ -709,7 +708,6 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, gin.H{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
"id": id,
|
"id": id,
|
||||||
"status": "provisioning",
|
"status": "provisioning",
|
||||||
"awareness_namespace": awarenessNamespace,
|
|
||||||
"workspace_access": workspaceAccess,
|
"workspace_access": workspaceAccess,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ func TestWorkspaceBudget_Create_WithLimit(t *testing.T) {
|
|||||||
nil, // role
|
nil, // role
|
||||||
3, // tier (default, workspace.go create-handler)
|
3, // tier (default, workspace.go create-handler)
|
||||||
"langgraph", // runtime
|
"langgraph", // runtime
|
||||||
sqlmock.AnyArg(), // awareness_namespace
|
|
||||||
(*string)(nil), // parent_id
|
(*string)(nil), // parent_id
|
||||||
nil, // workspace_dir
|
nil, // workspace_dir
|
||||||
"none", // workspace_access
|
"none", // workspace_access
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ func TestBuildProvisionerConfig_CopiesComputeSizingFromPayload(t *testing.T) {
|
|||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
t.TempDir(),
|
t.TempDir(),
|
||||||
"workspace:ws-compute",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if cfg.InstanceType != "m6i.xlarge" {
|
if cfg.InstanceType != "m6i.xlarge" {
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ func TestIntegration_WorkspaceCreate_NameRetry_AutoSuffixesOnCollision(t *testin
|
|||||||
// targets + the NOT NULL columns required by the schema).
|
// targets + the NOT NULL columns required by the schema).
|
||||||
firstID := uuid.New().String()
|
firstID := uuid.New().String()
|
||||||
if _, err := conn.ExecContext(ctx, `
|
if _, err := conn.ExecContext(ctx, `
|
||||||
INSERT INTO workspaces (id, name, tier, runtime, awareness_namespace, status)
|
INSERT INTO workspaces (id, name, tier, runtime, status)
|
||||||
VALUES ($1, $2, 2, 'claude-code', $3, 'provisioning')
|
VALUES ($1, $2, 2, 'claude-code', 'provisioning')
|
||||||
`, firstID, baseName, "workspace:"+firstID); err != nil {
|
`, firstID, baseName); err != nil {
|
||||||
t.Fatalf("seed first row: %v", err)
|
t.Fatalf("seed first row: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +145,10 @@ func TestIntegration_WorkspaceCreate_NameRetry_AutoSuffixesOnCollision(t *testin
|
|||||||
}
|
}
|
||||||
secondID := uuid.New().String()
|
secondID := uuid.New().String()
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO workspaces (id, name, tier, runtime, awareness_namespace, status)
|
INSERT INTO workspaces (id, name, tier, runtime, status)
|
||||||
VALUES ($1, $2, 2, 'claude-code', $3, 'provisioning')
|
VALUES ($1, $2, 2, 'claude-code', 'provisioning')
|
||||||
`
|
`
|
||||||
args := []any{secondID, baseName, "workspace:" + secondID}
|
args := []any{secondID, baseName}
|
||||||
persistedName, finalTx, err := insertWorkspaceWithNameRetry(
|
persistedName, finalTx, err := insertWorkspaceWithNameRetry(
|
||||||
ctx, tx, beginTx, baseName, 1, query, args,
|
ctx, tx, beginTx, baseName, 1, query, args,
|
||||||
)
|
)
|
||||||
@@ -179,7 +179,7 @@ func TestIntegration_WorkspaceCreate_NameRetry_AutoSuffixesOnCollision(t *testin
|
|||||||
t.Fatalf("begin tx3: %v", err)
|
t.Fatalf("begin tx3: %v", err)
|
||||||
}
|
}
|
||||||
thirdID := uuid.New().String()
|
thirdID := uuid.New().String()
|
||||||
args3 := []any{thirdID, baseName, "workspace:" + thirdID}
|
args3 := []any{thirdID, baseName}
|
||||||
persistedName3, finalTx3, err := insertWorkspaceWithNameRetry(
|
persistedName3, finalTx3, err := insertWorkspaceWithNameRetry(
|
||||||
ctx, tx3, beginTx, baseName, 1, query, args3,
|
ctx, tx3, beginTx, baseName, 1, query, args3,
|
||||||
)
|
)
|
||||||
@@ -216,9 +216,9 @@ func TestIntegration_WorkspaceCreate_NameRetry_TombstonedRowDoesNotCollide(t *te
|
|||||||
// Seed a row, then tombstone it.
|
// Seed a row, then tombstone it.
|
||||||
firstID := uuid.New().String()
|
firstID := uuid.New().String()
|
||||||
if _, err := conn.ExecContext(ctx, `
|
if _, err := conn.ExecContext(ctx, `
|
||||||
INSERT INTO workspaces (id, name, tier, runtime, awareness_namespace, status)
|
INSERT INTO workspaces (id, name, tier, runtime, status)
|
||||||
VALUES ($1, $2, 2, 'claude-code', $3, 'removed')
|
VALUES ($1, $2, 2, 'claude-code', 'removed')
|
||||||
`, firstID, baseName, "workspace:"+firstID); err != nil {
|
`, firstID, baseName); err != nil {
|
||||||
t.Fatalf("seed tombstoned row: %v", err)
|
t.Fatalf("seed tombstoned row: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,10 +231,10 @@ func TestIntegration_WorkspaceCreate_NameRetry_TombstonedRowDoesNotCollide(t *te
|
|||||||
}
|
}
|
||||||
secondID := uuid.New().String()
|
secondID := uuid.New().String()
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO workspaces (id, name, tier, runtime, awareness_namespace, status)
|
INSERT INTO workspaces (id, name, tier, runtime, status)
|
||||||
VALUES ($1, $2, 2, 'claude-code', $3, 'provisioning')
|
VALUES ($1, $2, 2, 'claude-code', 'provisioning')
|
||||||
`
|
`
|
||||||
args := []any{secondID, baseName, "workspace:" + secondID}
|
args := []any{secondID, baseName}
|
||||||
persistedName, finalTx, err := insertWorkspaceWithNameRetry(
|
persistedName, finalTx, err := insertWorkspaceWithNameRetry(
|
||||||
ctx, tx, beginTx, baseName, 1, query, args,
|
ctx, tx, beginTx, baseName, 1, query, args,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (h *WorkspaceHandler) provisionWorkspaceOpts(workspaceID, templatePath stri
|
|||||||
workspaceID, filepath.Base(runtimeTemplate))
|
workspaceID, filepath.Base(runtimeTemplate))
|
||||||
templatePath = runtimeTemplate
|
templatePath = runtimeTemplate
|
||||||
// Rebuild cfg with the recovered template path so Start() sees it.
|
// Rebuild cfg with the recovered template path so Start() sees it.
|
||||||
cfg = h.buildProvisionerConfig(ctx, workspaceID, templatePath, configFiles, payload, prepared.EnvVars, prepared.PluginsPath, prepared.AwarenessNamespace)
|
cfg = h.buildProvisionerConfig(ctx, workspaceID, templatePath, configFiles, payload, prepared.EnvVars, prepared.PluginsPath)
|
||||||
cfg.ResetClaudeSession = resetClaudeSession
|
cfg.ResetClaudeSession = resetClaudeSession
|
||||||
recovered = true
|
recovered = true
|
||||||
break
|
break
|
||||||
@@ -194,10 +194,11 @@ func (h *WorkspaceHandler) provisionWorkspaceOpts(workspaceID, templatePath stri
|
|||||||
// a ~64k context window worth of text — but small enough to prevent abuse.
|
// a ~64k context window worth of text — but small enough to prevent abuse.
|
||||||
const maxMemoryContentLength = 100_000 // ~100 KiB of text
|
const maxMemoryContentLength = 100_000 // ~100 KiB of text
|
||||||
|
|
||||||
func seedInitialMemories(ctx context.Context, workspaceID string, memories []models.MemorySeed, awarenessNamespace string) {
|
func seedInitialMemories(ctx context.Context, workspaceID string, memories []models.MemorySeed) {
|
||||||
if len(memories) == 0 {
|
if len(memories) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
namespace := workspaceMemoryNamespace(workspaceID)
|
||||||
for _, mem := range memories {
|
for _, mem := range memories {
|
||||||
scope := strings.ToUpper(mem.Scope)
|
scope := strings.ToUpper(mem.Scope)
|
||||||
if scope == "" {
|
if scope == "" {
|
||||||
@@ -223,33 +224,27 @@ func seedInitialMemories(ctx context.Context, workspaceID string, memories []mod
|
|||||||
if _, err := db.DB.ExecContext(ctx, `
|
if _, err := db.DB.ExecContext(ctx, `
|
||||||
INSERT INTO agent_memories (workspace_id, content, scope, namespace)
|
INSERT INTO agent_memories (workspace_id, content, scope, namespace)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
`, workspaceID, redactedContent, scope, awarenessNamespace); err != nil {
|
`, workspaceID, redactedContent, scope, namespace); err != nil {
|
||||||
log.Printf("seedInitialMemories: failed to insert memory for %s (scope=%s): %v", workspaceID, scope, err)
|
log.Printf("seedInitialMemories: failed to insert memory for %s (scope=%s): %v", workspaceID, scope, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("seedInitialMemories: seeded %d memories for workspace %s", len(memories), workspaceID)
|
log.Printf("seedInitialMemories: seeded %d memories for workspace %s", len(memories), workspaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func workspaceAwarenessNamespace(workspaceID string) string {
|
// workspaceMemoryNamespace returns the canonical v2 memory namespace
|
||||||
|
// string for a workspace. Matches the form produced by
|
||||||
|
// internal/memory/namespace/resolver.go for self-reads (issue #1735).
|
||||||
|
func workspaceMemoryNamespace(workspaceID string) string {
|
||||||
return fmt.Sprintf("workspace:%s", workspaceID)
|
return fmt.Sprintf("workspace:%s", workspaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *WorkspaceHandler) loadAwarenessNamespace(ctx context.Context, workspaceID string) string {
|
|
||||||
var awarenessNamespace string
|
|
||||||
err := db.DB.QueryRowContext(ctx, `SELECT COALESCE(awareness_namespace, '') FROM workspaces WHERE id = $1`, workspaceID).Scan(&awarenessNamespace)
|
|
||||||
if err != nil || awarenessNamespace == "" {
|
|
||||||
return workspaceAwarenessNamespace(workspaceID)
|
|
||||||
}
|
|
||||||
return awarenessNamespace
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *WorkspaceHandler) buildProvisionerConfig(
|
func (h *WorkspaceHandler) buildProvisionerConfig(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
workspaceID, templatePath string,
|
workspaceID, templatePath string,
|
||||||
configFiles map[string][]byte,
|
configFiles map[string][]byte,
|
||||||
payload models.CreateWorkspacePayload,
|
payload models.CreateWorkspacePayload,
|
||||||
envVars map[string]string,
|
envVars map[string]string,
|
||||||
pluginsPath, awarenessNamespace string,
|
pluginsPath string,
|
||||||
) provisioner.WorkspaceConfig {
|
) provisioner.WorkspaceConfig {
|
||||||
// Per-workspace workspace_dir takes priority over global WORKSPACE_DIR env var.
|
// Per-workspace workspace_dir takes priority over global WORKSPACE_DIR env var.
|
||||||
// If neither is set, the provisioner creates an isolated Docker volume.
|
// If neither is set, the provisioner creates an isolated Docker volume.
|
||||||
@@ -306,8 +301,6 @@ func (h *WorkspaceHandler) buildProvisionerConfig(
|
|||||||
},
|
},
|
||||||
EnvVars: envVars,
|
EnvVars: envVars,
|
||||||
PlatformURL: h.platformURL,
|
PlatformURL: h.platformURL,
|
||||||
AwarenessURL: os.Getenv("AWARENESS_URL"),
|
|
||||||
AwarenessNamespace: awarenessNamespace,
|
|
||||||
// Image left empty — molecule-core's runtime_image_pins table (mig
|
// Image left empty — molecule-core's runtime_image_pins table (mig
|
||||||
// 047, dead reader removed by RFC internal#617 / task #335) was an
|
// 047, dead reader removed by RFC internal#617 / task #335) was an
|
||||||
// aspirational SSOT that never received a writer. CP's
|
// aspirational SSOT that never received a writer. CP's
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ func readOrLazyHealInboundSecret(ctx context.Context, workspaceID, opLabel strin
|
|||||||
type preparedProvisionContext struct {
|
type preparedProvisionContext struct {
|
||||||
EnvVars map[string]string
|
EnvVars map[string]string
|
||||||
PluginsPath string
|
PluginsPath string
|
||||||
AwarenessNamespace string
|
|
||||||
Config provisioner.WorkspaceConfig
|
Config provisioner.WorkspaceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +169,6 @@ func (h *WorkspaceHandler) prepareProvisionContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pluginsPath, _ := filepath.Abs(filepath.Join(h.configsDir, "..", "plugins"))
|
pluginsPath, _ := filepath.Abs(filepath.Join(h.configsDir, "..", "plugins"))
|
||||||
awarenessNamespace := h.loadAwarenessNamespace(ctx, workspaceID)
|
|
||||||
|
|
||||||
// Per-agent git identity (#1957) — must run after secret loads so
|
// Per-agent git identity (#1957) — must run after secret loads so
|
||||||
// a workspace_secret named GIT_AUTHOR_NAME can override.
|
// a workspace_secret named GIT_AUTHOR_NAME can override.
|
||||||
@@ -231,13 +229,12 @@ func (h *WorkspaceHandler) prepareProvisionContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := h.buildProvisionerConfig(ctx, workspaceID, templatePath, configFiles, payload, envVars, pluginsPath, awarenessNamespace)
|
cfg := h.buildProvisionerConfig(ctx, workspaceID, templatePath, configFiles, payload, envVars, pluginsPath)
|
||||||
cfg.ResetClaudeSession = resetClaudeSession
|
cfg.ResetClaudeSession = resetClaudeSession
|
||||||
|
|
||||||
return &preparedProvisionContext{
|
return &preparedProvisionContext{
|
||||||
EnvVars: envVars,
|
EnvVars: envVars,
|
||||||
PluginsPath: pluginsPath,
|
PluginsPath: pluginsPath,
|
||||||
AwarenessNamespace: awarenessNamespace,
|
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== workspaceAwarenessNamespace ====================
|
// ==================== workspaceMemoryNamespace ====================
|
||||||
|
|
||||||
func TestWorkspaceAwarenessNamespace(t *testing.T) {
|
func TestWorkspaceMemoryNamespace(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
workspaceID string
|
workspaceID string
|
||||||
expected string
|
expected string
|
||||||
@@ -31,9 +31,9 @@ func TestWorkspaceAwarenessNamespace(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.workspaceID, func(t *testing.T) {
|
t.Run(tt.workspaceID, func(t *testing.T) {
|
||||||
result := workspaceAwarenessNamespace(tt.workspaceID)
|
result := workspaceMemoryNamespace(tt.workspaceID)
|
||||||
if result != tt.expected {
|
if result != tt.expected {
|
||||||
t.Errorf("workspaceAwarenessNamespace(%q) = %q, want %q", tt.workspaceID, result, tt.expected)
|
t.Errorf("workspaceMemoryNamespace(%q) = %q, want %q", tt.workspaceID, result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -645,7 +645,7 @@ func TestSeedInitialMemories_TruncatesOversizedContent(t *testing.T) {
|
|||||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), workspaceID, memories, "test-ns")
|
seedInitialMemories(context.Background(), workspaceID, memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("unmet DB expectations: %v", err)
|
t.Errorf("unmet DB expectations: %v", err)
|
||||||
@@ -674,7 +674,7 @@ func TestSeedInitialMemories_RedactsSecrets(t *testing.T) {
|
|||||||
WithArgs(workspaceID, wantRedacted, "LOCAL", sqlmock.AnyArg()).
|
WithArgs(workspaceID, wantRedacted, "LOCAL", sqlmock.AnyArg()).
|
||||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), workspaceID, memories, "test-ns")
|
seedInitialMemories(context.Background(), workspaceID, memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("unmet DB expectations: %v", err)
|
t.Errorf("unmet DB expectations: %v", err)
|
||||||
@@ -691,7 +691,7 @@ func TestSeedInitialMemories_InvalidScopeSkipped(t *testing.T) {
|
|||||||
{Content: "this should be skipped", Scope: "NOT_A_REAL_SCOPE"},
|
{Content: "this should be skipped", Scope: "NOT_A_REAL_SCOPE"},
|
||||||
}
|
}
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), "ws-bad-scope", memories, "test-ns")
|
seedInitialMemories(context.Background(), "ws-bad-scope", memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("unexpected DB calls for invalid scope: %v", err)
|
t.Errorf("unexpected DB calls for invalid scope: %v", err)
|
||||||
@@ -704,7 +704,7 @@ func TestSeedInitialMemories_EmptyMemoriesNil(t *testing.T) {
|
|||||||
mock := setupTestDB(t)
|
mock := setupTestDB(t)
|
||||||
mock.ExpectationsWereMet()
|
mock.ExpectationsWereMet()
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), "ws-nil", nil, "test-ns")
|
seedInitialMemories(context.Background(), "ws-nil", nil)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("unexpected DB calls for nil slice: %v", err)
|
t.Errorf("unexpected DB calls for nil slice: %v", err)
|
||||||
@@ -733,7 +733,6 @@ func TestBuildProvisionerConfig_BasicFields(t *testing.T) {
|
|||||||
models.CreateWorkspacePayload{Tier: 1, Runtime: "langgraph"},
|
models.CreateWorkspacePayload{Tier: 1, Runtime: "langgraph"},
|
||||||
map[string]string{"API_KEY": "secret"},
|
map[string]string{"API_KEY": "secret"},
|
||||||
pluginsPath,
|
pluginsPath,
|
||||||
"workspace:ws-basic",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if cfg.WorkspaceID != "ws-basic" {
|
if cfg.WorkspaceID != "ws-basic" {
|
||||||
@@ -748,9 +747,6 @@ func TestBuildProvisionerConfig_BasicFields(t *testing.T) {
|
|||||||
if cfg.PlatformURL != "http://localhost:8080" {
|
if cfg.PlatformURL != "http://localhost:8080" {
|
||||||
t.Errorf("expected PlatformURL 'http://localhost:8080', got %q", cfg.PlatformURL)
|
t.Errorf("expected PlatformURL 'http://localhost:8080', got %q", cfg.PlatformURL)
|
||||||
}
|
}
|
||||||
if cfg.AwarenessNamespace != "workspace:ws-basic" {
|
|
||||||
t.Errorf("expected AwarenessNamespace 'workspace:ws-basic', got %q", cfg.AwarenessNamespace)
|
|
||||||
}
|
|
||||||
if cfg.PluginsPath != pluginsPath {
|
if cfg.PluginsPath != pluginsPath {
|
||||||
t.Errorf("expected PluginsPath %q, got %q", pluginsPath, cfg.PluginsPath)
|
t.Errorf("expected PluginsPath %q, got %q", pluginsPath, cfg.PluginsPath)
|
||||||
}
|
}
|
||||||
@@ -775,7 +771,6 @@ func TestBuildProvisionerConfig_WorkspacePathFromEnv(t *testing.T) {
|
|||||||
|
|
||||||
workspaceDir := t.TempDir()
|
workspaceDir := t.TempDir()
|
||||||
t.Setenv("WORKSPACE_DIR", workspaceDir)
|
t.Setenv("WORKSPACE_DIR", workspaceDir)
|
||||||
t.Setenv("AWARENESS_URL", "http://awareness:37800")
|
|
||||||
|
|
||||||
pluginsPath := t.TempDir()
|
pluginsPath := t.TempDir()
|
||||||
cfg := handler.buildProvisionerConfig(
|
cfg := handler.buildProvisionerConfig(
|
||||||
@@ -786,15 +781,11 @@ func TestBuildProvisionerConfig_WorkspacePathFromEnv(t *testing.T) {
|
|||||||
models.CreateWorkspacePayload{Tier: 2, Runtime: "claude-code"},
|
models.CreateWorkspacePayload{Tier: 2, Runtime: "claude-code"},
|
||||||
nil,
|
nil,
|
||||||
pluginsPath,
|
pluginsPath,
|
||||||
"workspace:ws-env",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if cfg.WorkspacePath != workspaceDir {
|
if cfg.WorkspacePath != workspaceDir {
|
||||||
t.Errorf("expected WorkspacePath from env, got %q", cfg.WorkspacePath)
|
t.Errorf("expected WorkspacePath from env, got %q", cfg.WorkspacePath)
|
||||||
}
|
}
|
||||||
if cfg.AwarenessURL != "http://awareness:37800" {
|
|
||||||
t.Errorf("expected AwarenessURL from env, got %q", cfg.AwarenessURL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== issueAndInjectToken (issue #418) ====================
|
// ==================== issueAndInjectToken (issue #418) ====================
|
||||||
@@ -1007,7 +998,7 @@ func TestSeedInitialMemories_Truncation(t *testing.T) {
|
|||||||
WithArgs(sqlmock.AnyArg(), expectTruncated, "LOCAL", sqlmock.AnyArg()).
|
WithArgs(sqlmock.AnyArg(), expectTruncated, "LOCAL", sqlmock.AnyArg()).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), "ws-1066-test", memories, "test-ns")
|
seedInitialMemories(context.Background(), "ws-1066-test", memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("DB expectations not met: %v\n"+
|
t.Errorf("DB expectations not met: %v\n"+
|
||||||
@@ -1027,7 +1018,7 @@ func TestSeedInitialMemories_ContentUnderLimit(t *testing.T) {
|
|||||||
WithArgs(sqlmock.AnyArg(), "short content", "TEAM", sqlmock.AnyArg()).
|
WithArgs(sqlmock.AnyArg(), "short content", "TEAM", sqlmock.AnyArg()).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), "ws-1066-under", memories, "test-ns")
|
seedInitialMemories(context.Background(), "ws-1066-under", memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("DB expectations not met: %v", err)
|
t.Errorf("DB expectations not met: %v", err)
|
||||||
@@ -1052,7 +1043,7 @@ func TestSeedInitialMemories_ExactlyAtLimit(t *testing.T) {
|
|||||||
WithArgs(sqlmock.AnyArg(), atLimitContent, "LOCAL", sqlmock.AnyArg()).
|
WithArgs(sqlmock.AnyArg(), atLimitContent, "LOCAL", sqlmock.AnyArg()).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), "ws-boundary", memories, "test-ns")
|
seedInitialMemories(context.Background(), "ws-boundary", memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("DB expectations not met: %v", err)
|
t.Errorf("DB expectations not met: %v", err)
|
||||||
@@ -1068,7 +1059,7 @@ func TestSeedInitialMemories_EmptyContent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// seedInitialMemories skips empty content at line 234 — no DB call expected.
|
// seedInitialMemories skips empty content at line 234 — no DB call expected.
|
||||||
seedInitialMemories(context.Background(), "ws-empty", memories, "test-ns")
|
seedInitialMemories(context.Background(), "ws-empty", memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("DB expectations not met: %v", err)
|
t.Errorf("DB expectations not met: %v", err)
|
||||||
@@ -1092,7 +1083,7 @@ func TestSeedInitialMemories_OversizedWithSecrets(t *testing.T) {
|
|||||||
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), "GLOBAL", sqlmock.AnyArg()).
|
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), "GLOBAL", sqlmock.AnyArg()).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
seedInitialMemories(context.Background(), "ws-secrets", memories, "test-ns")
|
seedInitialMemories(context.Background(), "ws-secrets", memories)
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("DB expectations not met: %v", err)
|
t.Errorf("DB expectations not met: %v", err)
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ func TestWorkspaceCreate_DBInsertError(t *testing.T) {
|
|||||||
// Transaction begins, workspace INSERT fails, transaction is rolled back.
|
// Transaction begins, workspace INSERT fails, transaction is rolled back.
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Failing Agent", nil, 3, "langgraph", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Failing Agent", nil, 3, "langgraph", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnError(sql.ErrConnDone)
|
WillReturnError(sql.ErrConnDone)
|
||||||
mock.ExpectRollback()
|
mock.ExpectRollback()
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@ func TestWorkspaceCreate_DefaultsApplied(t *testing.T) {
|
|||||||
// Expect workspace INSERT with defaulted tier=3 (Privileged — the
|
// Expect workspace INSERT with defaulted tier=3 (Privileged — the
|
||||||
// handler default in workspace.go), runtime="langgraph"
|
// handler default in workspace.go), runtime="langgraph"
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Default Agent", nil, 3, "langgraph", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Default Agent", nil, 3, "langgraph", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ func TestWorkspaceCreate_SaaSHardForcesTier4(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "SaaS External Agent", nil, 4, "external", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "SaaS External Agent", nil, 4, "external", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
@@ -464,7 +464,7 @@ func TestWorkspaceCreate_WithSecrets_Persists(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Hermes Agent", nil, 3, "hermes", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Hermes Agent", nil, 3, "hermes", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
// Secret inserted inside the same transaction.
|
// Secret inserted inside the same transaction.
|
||||||
mock.ExpectExec("INSERT INTO workspace_secrets").
|
mock.ExpectExec("INSERT INTO workspace_secrets").
|
||||||
@@ -576,7 +576,7 @@ func TestWorkspaceCreate_ExternalURL_SSRFSafe(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Ext Agent", nil, 3, "external", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Ext Agent", nil, 3, "external", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
// External URL update (localhost is explicitly allowed by validateAgentURL).
|
// External URL update (localhost is explicitly allowed by validateAgentURL).
|
||||||
@@ -615,7 +615,7 @@ func TestWorkspaceCreate_KimiRuntime_PreservesLabel(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Kimi Agent", nil, 3, "kimi", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Kimi Agent", nil, 3, "kimi", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
// Pre-register flow: awaiting_agent + runtime preserved as "kimi"
|
// Pre-register flow: awaiting_agent + runtime preserved as "kimi"
|
||||||
@@ -1639,7 +1639,7 @@ runtime_config:
|
|||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(
|
WithArgs(
|
||||||
sqlmock.AnyArg(), "Hermes Agent", nil, 3, "hermes",
|
sqlmock.AnyArg(), "Hermes Agent", nil, 3, "hermes",
|
||||||
sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
(*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
@@ -1696,7 +1696,7 @@ model: anthropic:claude-sonnet-4-5
|
|||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(
|
WithArgs(
|
||||||
sqlmock.AnyArg(), "Legacy Agent", nil, 3, "langgraph",
|
sqlmock.AnyArg(), "Legacy Agent", nil, 3, "langgraph",
|
||||||
sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
(*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
@@ -1749,7 +1749,7 @@ runtime_config:
|
|||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(
|
WithArgs(
|
||||||
sqlmock.AnyArg(), "Custom Hermes", nil, 3, "hermes",
|
sqlmock.AnyArg(), "Custom Hermes", nil, 3, "hermes",
|
||||||
sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
(*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
@@ -1894,7 +1894,7 @@ func TestWorkspaceCreate_188_ExplicitRuntimeNoTemplate_OK(t *testing.T) {
|
|||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectExec("INSERT INTO workspaces").
|
mock.ExpectExec("INSERT INTO workspaces").
|
||||||
WithArgs(sqlmock.AnyArg(), "Explicit Codex", nil, 3, "codex", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
WithArgs(sqlmock.AnyArg(), "Explicit Codex", nil, 3, "codex", (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
mock.ExpectCommit()
|
mock.ExpectCommit()
|
||||||
mock.ExpectExec("INSERT INTO canvas_layouts").
|
mock.ExpectExec("INSERT INTO canvas_layouts").
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ type Workspace struct {
|
|||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
Role sql.NullString `json:"role" db:"role"`
|
Role sql.NullString `json:"role" db:"role"`
|
||||||
Tier int `json:"tier" db:"tier"`
|
Tier int `json:"tier" db:"tier"`
|
||||||
AwarenessNamespace sql.NullString `json:"awareness_namespace" db:"awareness_namespace"`
|
|
||||||
Status string `json:"status" db:"status"`
|
Status string `json:"status" db:"status"`
|
||||||
SourceBundleID sql.NullString `json:"source_bundle_id" db:"source_bundle_id"`
|
SourceBundleID sql.NullString `json:"source_bundle_id" db:"source_bundle_id"`
|
||||||
AgentCard json.RawMessage `json:"agent_card" db:"agent_card"`
|
AgentCard json.RawMessage `json:"agent_card" db:"agent_card"`
|
||||||
@@ -207,7 +206,8 @@ type CreateWorkspacePayload struct {
|
|||||||
} `json:"canvas"`
|
} `json:"canvas"`
|
||||||
// InitialMemories is an optional list of memories to seed into the
|
// InitialMemories is an optional list of memories to seed into the
|
||||||
// workspace immediately after creation. Each entry is inserted into
|
// workspace immediately after creation. Each entry is inserted into
|
||||||
// agent_memories with the workspace's awareness namespace. Issue #1050.
|
// agent_memories under the workspace's v2 memory namespace
|
||||||
|
// ("workspace:<id>"). Issue #1050.
|
||||||
InitialMemories []MemorySeed `json:"initial_memories"`
|
InitialMemories []MemorySeed `json:"initial_memories"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,8 +103,6 @@ type WorkspaceConfig struct {
|
|||||||
Display WorkspaceDisplayConfig
|
Display WorkspaceDisplayConfig
|
||||||
EnvVars map[string]string // Additional env vars (API keys, etc.)
|
EnvVars map[string]string // Additional env vars (API keys, etc.)
|
||||||
PlatformURL string
|
PlatformURL string
|
||||||
AwarenessURL string
|
|
||||||
AwarenessNamespace string
|
|
||||||
WorkspaceAccess string // #65: "none" (default), "read_only", or "read_write"
|
WorkspaceAccess string // #65: "none" (default), "read_only", or "read_write"
|
||||||
ResetClaudeSession bool // #12: if true, discard the claude-sessions volume before start (fresh session dir)
|
ResetClaudeSession bool // #12: if true, discard the claude-sessions volume before start (fresh session dir)
|
||||||
|
|
||||||
@@ -714,10 +712,6 @@ func buildContainerEnv(cfg WorkspaceConfig) []string {
|
|||||||
// still override (Dockerfile ENV is overridden by docker -e at runtime).
|
// still override (Dockerfile ENV is overridden by docker -e at runtime).
|
||||||
"PYTHONPATH=/app",
|
"PYTHONPATH=/app",
|
||||||
}
|
}
|
||||||
if cfg.AwarenessNamespace != "" && cfg.AwarenessURL != "" {
|
|
||||||
env = append(env, fmt.Sprintf("AWARENESS_NAMESPACE=%s", cfg.AwarenessNamespace))
|
|
||||||
env = append(env, fmt.Sprintf("AWARENESS_URL=%s", cfg.AwarenessURL))
|
|
||||||
}
|
|
||||||
// #1687: track explicit GH_TOKEN / GITHUB_TOKEN so they win over GH_PAT
|
// #1687: track explicit GH_TOKEN / GITHUB_TOKEN so they win over GH_PAT
|
||||||
// alias. These are normally stripped by the SCM-write guard below, but
|
// alias. These are normally stripped by the SCM-write guard below, but
|
||||||
// when a user explicitly sets them we preserve the value.
|
// when a user explicitly sets them we preserve the value.
|
||||||
|
|||||||
@@ -692,39 +692,6 @@ func TestBuildContainerEnv_MoleculeAIURLAlwaysMatchesPlatformURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildContainerEnv_AwarenessOnlyWhenBothSet(t *testing.T) {
|
|
||||||
// Both set → both injected.
|
|
||||||
cfg := WorkspaceConfig{
|
|
||||||
WorkspaceID: "ws-x",
|
|
||||||
PlatformURL: "http://localhost:8080",
|
|
||||||
AwarenessURL: "http://awareness:9000",
|
|
||||||
AwarenessNamespace: "ns-1",
|
|
||||||
}
|
|
||||||
env := buildContainerEnv(cfg)
|
|
||||||
hasNS := false
|
|
||||||
hasURL := false
|
|
||||||
for _, e := range env {
|
|
||||||
if e == "AWARENESS_NAMESPACE=ns-1" {
|
|
||||||
hasNS = true
|
|
||||||
}
|
|
||||||
if e == "AWARENESS_URL=http://awareness:9000" {
|
|
||||||
hasURL = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasNS || !hasURL {
|
|
||||||
t.Errorf("both awareness vars must be present: env=%v", env)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only namespace set → neither injected (must be both-or-nothing).
|
|
||||||
cfg.AwarenessURL = ""
|
|
||||||
env2 := buildContainerEnv(cfg)
|
|
||||||
for _, e := range env2 {
|
|
||||||
if strings.HasPrefix(e, "AWARENESS_") {
|
|
||||||
t.Errorf("awareness vars must NOT be injected when URL is missing: got %q", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildContainerEnv_CustomEnvVarsAppended(t *testing.T) {
|
func TestBuildContainerEnv_CustomEnvVarsAppended(t *testing.T) {
|
||||||
// NOTE: this test previously asserted GITHUB_TOKEN passed through
|
// NOTE: this test previously asserted GITHUB_TOKEN passed through
|
||||||
// verbatim. That assertion encoded the forensic #145 latent leak as
|
// verbatim. That assertion encoded the forensic #145 latent leak as
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-- Reverse of 20260523130000_drop_workspaces_awareness_namespace.up.sql.
|
||||||
|
--
|
||||||
|
-- Restores the workspaces.awareness_namespace column verbatim from
|
||||||
|
-- migration 010_workspace_awareness.sql so a down-cycle leaves the
|
||||||
|
-- schema bit-identical to the pre-drop state. The column will be
|
||||||
|
-- NULL on all rows after re-add — handlers no longer write to it and
|
||||||
|
-- callers no longer read it, so this is functionally inert without
|
||||||
|
-- a paired code revert.
|
||||||
|
|
||||||
|
ALTER TABLE workspaces
|
||||||
|
ADD COLUMN IF NOT EXISTS awareness_namespace TEXT;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Issue #1735 — drop the workspaces.awareness_namespace column.
|
||||||
|
--
|
||||||
|
-- "Awareness namespaces" were a memory-routing surface (env vars
|
||||||
|
-- AWARENESS_URL / AWARENESS_NAMESPACE) that was plumbed across the
|
||||||
|
-- platform but never wired in any production or staging environment
|
||||||
|
-- (verified 2026-05-23 via Railway GraphQL on the controlplane service:
|
||||||
|
-- AWARENESS_* unset in both env IDs 59227671-… and 639539ec-…).
|
||||||
|
--
|
||||||
|
-- The column added by migration 010_workspace_awareness.sql was only
|
||||||
|
-- ever populated with the canonical "workspace:<id>" string, which is
|
||||||
|
-- also the v2 memory namespace string (see internal/memory/namespace/
|
||||||
|
-- resolver.go:186). Removing the column does not change any agent-
|
||||||
|
-- visible memory namespace — handlers now compute the same
|
||||||
|
-- "workspace:<id>" string inline when inserting into agent_memories.
|
||||||
|
--
|
||||||
|
-- Related: #1733 (memory SSOT consolidation), #1734 (Memory tab bug).
|
||||||
|
|
||||||
|
ALTER TABLE workspaces
|
||||||
|
DROP COLUMN IF EXISTS awareness_namespace;
|
||||||
Reference in New Issue
Block a user