docs(environments): update AGENTS.md and ARCHITECTURE.md for per-assistant data layout#25504
Conversation
| // TODO: extraction target is hardcoded to homedir(); multi-instance entries | ||
| // whose instanceDir differs from homedir will extract to the wrong | ||
| // location. Tracked separately from the collision-check regression. | ||
| await exec("tar", ["xzf", archivePath, "-C", homedir()]); |
There was a problem hiding this comment.
🔴 recover command extracts archive to wrong location for all new multi-instance entries
After this PR, allocateLocalResources always places new instances under $XDG_DATA_HOME/vellum/assistants/<name>/ (the first-instance-gets-homedir special case was removed at cli/src/lib/assistant-config.ts:419-424). However, the recover command at line 74 still hardcodes homedir() as the tar extraction target. When retire-local.ts:111-112 archives a multi-instance entry, the archive root is the staging directory basename (e.g., alice.tar.gz.staging/). Extracting with -C homedir() places the data under ~/alice.tar.gz.staging/ instead of the entry's resources.instanceDir (e.g., ~/.local/share/vellum/assistants/alice/). The daemon then starts with paths pointing at the empty instanceDir, meaning all workspace data, DB, credentials, and config are missing. Before this PR only second+ instances were affected; now ALL new instances are affected since no instance gets homedir anymore.
Prompt for agents
The `recover` command's tar extraction target at line 74 is hardcoded to `homedir()`, but the archive was created by `retire-local.ts` relative to the staging directory under `~/.local/share/vellum/retired/`. For multi-instance entries whose `instanceDir` differs from homedir, the extracted files land at the wrong location.
To fix this, the extraction needs to account for the original `dirToArchive` path that was used during retire. The archive contains the renamed staging directory, so extraction needs to reconstruct the original path. One approach:
1. In `retire-local.ts`, record the original `dirToArchive` path in the metadata JSON alongside the entry.
2. In `recover.ts`, read that path from metadata, ensure the parent directory exists, and extract to the correct parent so the directory structure is restored at the right location.
Alternatively, change the tar creation in `retire-local.ts` to archive relative to the instanceDir's parent, so extraction with `-C <parent of instanceDir>` restores correctly. Both `retire-local.ts` (archive creation) and `recover.ts` (extraction) need coordinated changes.
Relevant files: `cli/src/commands/recover.ts:74`, `cli/src/lib/retire-local.ts:111-114`.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // Kept in sync with `cli/src/lib/environments/seeds.ts`. The daemon does not | ||
| // import from the CLI package, so the list is duplicated here. If a new | ||
| // environment is added to the seed table, add it here too. | ||
| const KNOWN_ENVIRONMENTS: ReadonlySet<string> = new Set([ | ||
| "production", | ||
| "staging", | ||
| "test", | ||
| "dev", | ||
| "local", | ||
| ]); |
There was a problem hiding this comment.
🚩 KNOWN_ENVIRONMENTS duplication between daemon and CLI
The KNOWN_ENVIRONMENTS set in assistant/src/util/platform.ts:160-166 is manually kept in sync with cli/src/lib/environments/seeds.ts per an inline comment. This is a potential drift hazard — if a new environment is added to the CLI seeds but not to the daemon's set, that environment's daemon would fall back to production paths. The comment at line 157-159 documents this, but there's no compile-time or test-time enforcement. A shared constant or a guard test that verifies both lists match would prevent drift.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3d1e5a014
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const signingKeyPaths = [ | ||
| join(homedir(), ".vellum", "protected", "actor-token-signing-key"), | ||
| join(getProtectedDir(), "actor-token-signing-key"), | ||
| join(getDeprecatedDir(), "actor-token-signing-key"), |
There was a problem hiding this comment.
Preserve legacy signing-key path in risk classification
This change drops the explicit ~/.vellum/protected/actor-token-signing-key check and only matches getProtectedDir(). In multi-instance mode (BASE_DATA_DIR set), reads of the legacy key path are no longer classified as high-risk, even though upgraded machines can still have that legacy file on disk. That creates a policy gap where one signing key location is protected and the other is not; the checker should continue to include both paths (as shell.ts now does for protected dirs).
Useful? React with 👍 / 👎.
| const lockfilePath = getLockfilePath(getCurrentEnvironment()); | ||
| mkdirSync(dirname(lockfilePath), { recursive: true }); |
There was a problem hiding this comment.
Keep lockfile path compatible with native host discovery
Routing lockfile writes through getLockfilePath(getCurrentEnvironment()) moves non-production environments to $XDG_CONFIG_HOME/vellum-<env>/lockfile.json, but the Chrome native host still discovers assistants only from ~/.vellum.lock.json / ~/.vellum.lockfile.json (clients/chrome-extension/native-host/src/lockfile.ts). In dev/staging/test/local builds this makes list_assistants and assistant-scoped token resolution fail because the native host no longer sees any lockfile entries.
Useful? React with 👍 / 👎.
…stant data layout
d3d1e5a to
1b63827
Compare
…stant data layout (#25504)
…stant data layout (#25504)
…stant data layout (#25504)
* feat(environments): daemon vellumRoot() honors BASE_DATA_DIR per-instance override (#25455) * feat(environments): Swift-side VellumPaths env-aware helpers (#25457) * feat(environments): route CLI lockfile R/W and allocator through environment helpers; delete getDataDir (#25458) * feat(environments): route CLI platform token, guardian token, and device-id paths through getConfigDir(env) (#25456) * feat(environments): route CLI platform token, guardian token, and device-id paths through getConfigDir(env) * fix(cli): use spyOn for platform-client and guardian-token in teleport tests `mock.module()` in bun:test replaces a module globally in the process and provides no way to unmock. `teleport.test.ts` was using it to stub both `../lib/platform-client.js` and `../lib/guardian-token.js`, so those mocks leaked into `platform-client.test.ts` and `guardian-token.test.ts` when they ran in the same bun test process — every call to `readPlatformToken()` in the platform-client tests returned the literal string "platform-token" from the stale teleport mock, and `loadGuardianToken()` in the guardian-token tests returned a minimal fake object missing the `guardianPrincipalId` field the tests assert on. Mirror the existing `assistant-config` pattern (already using `spyOn` for the same reason per its inline comment) for `platform-client` and `guardian-token`. `spyOn()` mutates the imported module namespace object only, and `mockRestore()` in `afterAll` fully reverts the stubs so other test files see the real implementations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): delete unused LOCKFILE_NAMES export (#25488) * fix(environments): route daemon protected/ callers through platform helpers (#25493) * fix(environments): orphan-detection and recover find daemons across all lockfile entries (#25481) * fix(environments): orphan-detection and recover find daemons across all lockfile entries * fix(recover): scope collision check to recovering entry's own target path Addresses Codex P1 and Devin P1 on #25481: the iterate-all-entries loop blocked recovery whenever any unrelated local assistant was still installed. * refactor(environments): route Swift client path sites through VellumPaths (#25483) * refactor(environments): route Swift client path sites through VellumPaths * fix(environments): remove dead xdgDataHome field + reject relative XDG paths Addresses Devin and Codex P2 findings on PR #25457: - xdgDataHome was stored but never read; remove the field and its resolver helper - resolveXdgConfigHome() no longer rewrites relative XDG_CONFIG_HOME values against cwd — relative values are rejected for parity with the TypeScript env package * fix(environments): make daemon XDG platform-token and device-id env-aware (#25497) * refactor(device-id): inline base-dir helper into migration 003 Removes the stale getDeviceIdBaseDir() export from device-id.ts — getDeviceId() no longer uses it, so it was a maintenance trap whose return value diverged from where device.json actually resolves in non-production envs. Its sole remaining caller was workspace migration 003, so inline the 2-line containerized-vs-homedir branch there. Brings migration 003 closer to the self-containment rule in assistant/src/workspace/migrations/AGENTS.md (no external imports beyond types/logger). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(environments): update AGENTS.md and ARCHITECTURE.md for per-assistant data layout (#25504) * fix(environments): CLI falls back to production on unknown VELLUM_ENVIRONMENT (parity with daemon and Swift) (#25541) * fix(permissions): restore legacy signing-key path in risk classification (#25542) * fix(config-watcher): use || for GATEWAY_SECURITY_DIR fallback to match sibling convention (#25543) * fix(cli): read platformBaseUrl from lockfile instead of legacy workspace config path (#25544) * fix(chrome-ext): native host reads env-aware lockfile path (#25547) * fix(environments): VellumPaths accepts relative XDG_CONFIG_HOME to match TS/daemon (#25550) * refactor(recover): drop unreachable legacy fallback in collision check (#25575) * fix(cli): sync platformBaseUrl to lockfile on vellum use / vellum wake (#25578) * fix(environments): env-seed fallback for getPlatformUrl, revert H1 sync-on-switch (#25595) Under our invariant "each env has its own lockfile, and all assistants in that lockfile share a platform URL", the H1 workspace-config→lockfile sync on `vellum use`/`vellum wake` was load-bearing for nothing: switching the active assistant within a single env cannot change the platform URL. Revert that sync. When no lockfile is seeded yet, fall back to the current environment's seed URL instead of the hardcoded production default so `VELLUM_ENVIRONMENT=dev vellum …` targets `dev-platform.vellum.ai` out of the box. - Revert H1: `vellum use` no longer calls `syncActiveAssistantConfigToLockfile`, `vellum wake` no longer re-runs `syncConfigToLockfile`, and the helper itself is deleted (the hatch-time sync is still done by `syncConfigToLockfile`, unchanged). - `getPlatformUrl()` fallback: prefer `getCurrentEnvironment().platformUrl` over the hardcoded prod URL so non-prod CLI users get the right tenant before any assistant is registered. - Tests: drop the H1 sync-on-switch suite, add a dev-env seed fallback test, keep the existing prod fallback test. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(environments): drift-guard for KNOWN_ENVIRONMENTS across TS sites (#25617) The set of recognized environment names is duplicated in three TS locations: `cli/src/lib/environments/seeds.ts` (SEEDS), the daemon's `assistant/src/util/platform.ts` (KNOWN_ENVIRONMENTS), and the Chrome native host's `native-host/src/lockfile.ts` (NON_PRODUCTION_ENVIRONMENTS). Cross-package imports don't work today — assistant's tsconfig restricts `include` to its own src tree, and the native host is a standalone TS project with `rootDir: ./src`. Add a drift-guard test in cli that parses the literal Set bodies from both external files and asserts they agree with CLI's SEEDS (minus `production` for the native host set). Catches any future addition to the seed table that fails to propagate to the other two sites. Also refresh the comments on all three declarations to point at the drift-guard test and the fast-follow plan: hoist the shared name list into a `packages/environments` package (mirroring `packages/ces-contracts` etc.) so this check becomes a compile-time import instead of a runtime regex. That refactor is planned alongside CLI-driven context support. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(environments): forward VELLUM_ENVIRONMENT across desktop→CLI→daemon handoffs (#25633) Two call sites were stripping VELLUM_ENVIRONMENT from spawn whitelists, breaking environment isolation for the main desktop launch path: 1. macOS `VellumCli.makeBaseEnvironment()` — `forwardedEnvKeys` did not include `VELLUM_ENVIRONMENT`, so every bundled-CLI command launched from the app (hatch, wake, sleep, retire, …) ran as production even when the app itself was built for a non-production environment. The app's Info.plist sets `VELLUM_ENVIRONMENT` at build time (`build.sh:1054`), so forwarding it is sufficient. 2. `cli/src/lib/local.ts` compiled-daemon spawn — the `daemonEnv` whitelist used when `bun run` is unavailable (packaged desktop builds) also omitted `VELLUM_ENVIRONMENT`. Even when the CLI process itself had the variable set, the spawned daemon fell back to production path/env behavior, so assistant-side env-scoped state (device ID, XDG-backed tokens and config reads) bled into prod. Note: the source/watch daemon spawn path in `local.ts:281` is unaffected — it uses `{...process.env}` and inherits everything. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * default to dev environment * remove duplicate env var --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(environments): daemon vellumRoot() honors BASE_DATA_DIR per-instance override (#25455) * feat(environments): Swift-side VellumPaths env-aware helpers (#25457) * feat(environments): route CLI lockfile R/W and allocator through environment helpers; delete getDataDir (#25458) * feat(environments): route CLI platform token, guardian token, and device-id paths through getConfigDir(env) (#25456) * feat(environments): route CLI platform token, guardian token, and device-id paths through getConfigDir(env) * fix(cli): use spyOn for platform-client and guardian-token in teleport tests `mock.module()` in bun:test replaces a module globally in the process and provides no way to unmock. `teleport.test.ts` was using it to stub both `../lib/platform-client.js` and `../lib/guardian-token.js`, so those mocks leaked into `platform-client.test.ts` and `guardian-token.test.ts` when they ran in the same bun test process — every call to `readPlatformToken()` in the platform-client tests returned the literal string "platform-token" from the stale teleport mock, and `loadGuardianToken()` in the guardian-token tests returned a minimal fake object missing the `guardianPrincipalId` field the tests assert on. Mirror the existing `assistant-config` pattern (already using `spyOn` for the same reason per its inline comment) for `platform-client` and `guardian-token`. `spyOn()` mutates the imported module namespace object only, and `mockRestore()` in `afterAll` fully reverts the stubs so other test files see the real implementations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): delete unused LOCKFILE_NAMES export (#25488) * fix(environments): route daemon protected/ callers through platform helpers (#25493) * fix(environments): orphan-detection and recover find daemons across all lockfile entries (#25481) * fix(environments): orphan-detection and recover find daemons across all lockfile entries * fix(recover): scope collision check to recovering entry's own target path Addresses Codex P1 and Devin P1 on #25481: the iterate-all-entries loop blocked recovery whenever any unrelated local assistant was still installed. * refactor(environments): route Swift client path sites through VellumPaths (#25483) * refactor(environments): route Swift client path sites through VellumPaths * fix(environments): remove dead xdgDataHome field + reject relative XDG paths Addresses Devin and Codex P2 findings on PR #25457: - xdgDataHome was stored but never read; remove the field and its resolver helper - resolveXdgConfigHome() no longer rewrites relative XDG_CONFIG_HOME values against cwd — relative values are rejected for parity with the TypeScript env package * fix(environments): make daemon XDG platform-token and device-id env-aware (#25497) * refactor(device-id): inline base-dir helper into migration 003 Removes the stale getDeviceIdBaseDir() export from device-id.ts — getDeviceId() no longer uses it, so it was a maintenance trap whose return value diverged from where device.json actually resolves in non-production envs. Its sole remaining caller was workspace migration 003, so inline the 2-line containerized-vs-homedir branch there. Brings migration 003 closer to the self-containment rule in assistant/src/workspace/migrations/AGENTS.md (no external imports beyond types/logger). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(environments): update AGENTS.md and ARCHITECTURE.md for per-assistant data layout (#25504) * fix(environments): CLI falls back to production on unknown VELLUM_ENVIRONMENT (parity with daemon and Swift) (#25541) * fix(permissions): restore legacy signing-key path in risk classification (#25542) * fix(config-watcher): use || for GATEWAY_SECURITY_DIR fallback to match sibling convention (#25543) * fix(cli): read platformBaseUrl from lockfile instead of legacy workspace config path (#25544) * fix(chrome-ext): native host reads env-aware lockfile path (#25547) * fix(environments): VellumPaths accepts relative XDG_CONFIG_HOME to match TS/daemon (#25550) * refactor(recover): drop unreachable legacy fallback in collision check (#25575) * fix(cli): sync platformBaseUrl to lockfile on vellum use / vellum wake (#25578) * fix(environments): env-seed fallback for getPlatformUrl, revert H1 sync-on-switch (#25595) Under our invariant "each env has its own lockfile, and all assistants in that lockfile share a platform URL", the H1 workspace-config→lockfile sync on `vellum use`/`vellum wake` was load-bearing for nothing: switching the active assistant within a single env cannot change the platform URL. Revert that sync. When no lockfile is seeded yet, fall back to the current environment's seed URL instead of the hardcoded production default so `VELLUM_ENVIRONMENT=dev vellum …` targets `dev-platform.vellum.ai` out of the box. - Revert H1: `vellum use` no longer calls `syncActiveAssistantConfigToLockfile`, `vellum wake` no longer re-runs `syncConfigToLockfile`, and the helper itself is deleted (the hatch-time sync is still done by `syncConfigToLockfile`, unchanged). - `getPlatformUrl()` fallback: prefer `getCurrentEnvironment().platformUrl` over the hardcoded prod URL so non-prod CLI users get the right tenant before any assistant is registered. - Tests: drop the H1 sync-on-switch suite, add a dev-env seed fallback test, keep the existing prod fallback test. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(environments): drift-guard for KNOWN_ENVIRONMENTS across TS sites (#25617) The set of recognized environment names is duplicated in three TS locations: `cli/src/lib/environments/seeds.ts` (SEEDS), the daemon's `assistant/src/util/platform.ts` (KNOWN_ENVIRONMENTS), and the Chrome native host's `native-host/src/lockfile.ts` (NON_PRODUCTION_ENVIRONMENTS). Cross-package imports don't work today — assistant's tsconfig restricts `include` to its own src tree, and the native host is a standalone TS project with `rootDir: ./src`. Add a drift-guard test in cli that parses the literal Set bodies from both external files and asserts they agree with CLI's SEEDS (minus `production` for the native host set). Catches any future addition to the seed table that fails to propagate to the other two sites. Also refresh the comments on all three declarations to point at the drift-guard test and the fast-follow plan: hoist the shared name list into a `packages/environments` package (mirroring `packages/ces-contracts` etc.) so this check becomes a compile-time import instead of a runtime regex. That refactor is planned alongside CLI-driven context support. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(environments): forward VELLUM_ENVIRONMENT across desktop→CLI→daemon handoffs (#25633) Two call sites were stripping VELLUM_ENVIRONMENT from spawn whitelists, breaking environment isolation for the main desktop launch path: 1. macOS `VellumCli.makeBaseEnvironment()` — `forwardedEnvKeys` did not include `VELLUM_ENVIRONMENT`, so every bundled-CLI command launched from the app (hatch, wake, sleep, retire, …) ran as production even when the app itself was built for a non-production environment. The app's Info.plist sets `VELLUM_ENVIRONMENT` at build time (`build.sh:1054`), so forwarding it is sufficient. 2. `cli/src/lib/local.ts` compiled-daemon spawn — the `daemonEnv` whitelist used when `bun run` is unavailable (packaged desktop builds) also omitted `VELLUM_ENVIRONMENT`. Even when the CLI process itself had the variable set, the spawned daemon fell back to production path/env behavior, so assistant-side env-scoped state (device ID, XDG-backed tokens and config reads) bled into prod. Note: the source/watch daemon spawn path in `local.ts:281` is unaffected — it uses `{...process.env}` and inherits everything. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * default to dev environment * remove duplicate env var --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…25977) * fix(gmail): make retry sleeps signal-aware in Gmail client The abort controller from sender-digest can now interrupt retry sleep delays, preventing Promise.allSettled from hanging past the deadline when batchGetMessages enters exponential backoff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(macos): gate thinking-anchor reset to toolRunning only (#25968) Resetting the thinking anchor on .streamingCode can erase valid post-tool thinking intervals when a late code preview fires after tools complete. Restrict the reset to .toolRunning so the thinking duration is accurate. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(gmail): safe default for has_prior_reply, time-budget enrichment (#25967) - Default has_prior_reply to true on API errors (safe direction per SKILL.md) - Skip reply checks when already rate-limited to avoid wasting quota - Add time budget to enrichment step using remaining TIME_BUDGET_MS - Over-fetch sender candidates before capping to max_senders Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(gmail): restrict archive fallback to expired scans, sanitize query (#25966) * fix(gmail): restrict archive fallback to expired scans, sanitize query - Only fall back to query-based archiving when scan is truly expired (null), not when sender IDs don't match (empty array). - Quote emails in fallback query to prevent Gmail query injection. - Update SKILL.md to reflect new fallback behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(gmail): clarify SKILL.md scan expiration fallback behavior Document the distinction between expired scan (null, falls back to query) vs sender ID mismatch (empty array, returns error). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(oauth): add gmail.settings.basic scope to Google OAuth defaults (#25970) The Gmail settings scope is required for filter creation/deletion, label management, and other settings-level operations. Without it, the gmail_filters tool fails with 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT. Existing tokens will be flagged by the credential health service's scope drift detection, prompting users to re-authenticate. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add contacts + contact_channels tables to gateway SQLite (#25951) * feat: add contacts + contact_channels tables to gateway SQLite Gateway cutover step 1: declare contacts and contact_channels tables in the gateway DB schema. This is the foundation for moving contact auth/authz ownership from the assistant daemon to the gateway. - contacts: mirrors assistant's contacts table (auth/authz fields only) - contact_channels: mirrors assistant's contact_channels table with same indexes (type+external_user_id, type+external_chat_id) - m0002-seed-contacts: one-time data migration that seeds both tables from assistant.db on first startup (INSERT OR IGNORE, transactional) - ContactStore: read-only store with prepared-statement queries (getContact, listContacts, getContactByChannel, getChannelsForContact) - IPC handlers: list_contacts, get_contact, get_contact_by_channel, get_channels_for_contact — wired into the gateway IPC server - Tests: ContactStore unit tests + IPC round-trip tests * review: address Vargas feedback on PR #25951 - Strip contacts table to auth/authz-only: remove notes, user_file, contact_type columns (not needed for actor validation) - Remove m0002-seed-contacts data migration — hold off until endpoints have cutover and we're dual-writing - Move test imports to top level (no more inline await import()) - Use fake channel IDs in tests instead of real ones - Clean test state between runs (DELETE before seed) - Update ARCHITECTURE.md + gateway/ARCHITECTURE.md to document the contacts ownership migration direction - Add Drizzle migration + test preload env var cleanup tasks to workstream Up Next --------- Co-authored-by: root <root@assistant-89f9b42a-2563-4bbe-96b7-b2840c145b37-0.assistant-89f9b42a-2563-4bbe-96b7-b2840c145b37.warm-pool.svc.cluster.local> * fix(runtime): wake adapter drains queue, persists with metadata, broadcasts to all clients (#25972) * meet-join: SKILL.md guidance for voice participation (#25973) * fix(gmail): persist blocklist only after archive succeeds (#25971) Move addToBlocklist() call from before the batch archive operation to after it succeeds. Prevents corrupted cleanup state when archiving fails. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(environments): env-aware data and config path layout (#25499) * feat(environments): daemon vellumRoot() honors BASE_DATA_DIR per-instance override (#25455) * feat(environments): Swift-side VellumPaths env-aware helpers (#25457) * feat(environments): route CLI lockfile R/W and allocator through environment helpers; delete getDataDir (#25458) * feat(environments): route CLI platform token, guardian token, and device-id paths through getConfigDir(env) (#25456) * feat(environments): route CLI platform token, guardian token, and device-id paths through getConfigDir(env) * fix(cli): use spyOn for platform-client and guardian-token in teleport tests `mock.module()` in bun:test replaces a module globally in the process and provides no way to unmock. `teleport.test.ts` was using it to stub both `../lib/platform-client.js` and `../lib/guardian-token.js`, so those mocks leaked into `platform-client.test.ts` and `guardian-token.test.ts` when they ran in the same bun test process — every call to `readPlatformToken()` in the platform-client tests returned the literal string "platform-token" from the stale teleport mock, and `loadGuardianToken()` in the guardian-token tests returned a minimal fake object missing the `guardianPrincipalId` field the tests assert on. Mirror the existing `assistant-config` pattern (already using `spyOn` for the same reason per its inline comment) for `platform-client` and `guardian-token`. `spyOn()` mutates the imported module namespace object only, and `mockRestore()` in `afterAll` fully reverts the stubs so other test files see the real implementations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): delete unused LOCKFILE_NAMES export (#25488) * fix(environments): route daemon protected/ callers through platform helpers (#25493) * fix(environments): orphan-detection and recover find daemons across all lockfile entries (#25481) * fix(environments): orphan-detection and recover find daemons across all lockfile entries * fix(recover): scope collision check to recovering entry's own target path Addresses Codex P1 and Devin P1 on #25481: the iterate-all-entries loop blocked recovery whenever any unrelated local assistant was still installed. * refactor(environments): route Swift client path sites through VellumPaths (#25483) * refactor(environments): route Swift client path sites through VellumPaths * fix(environments): remove dead xdgDataHome field + reject relative XDG paths Addresses Devin and Codex P2 findings on PR #25457: - xdgDataHome was stored but never read; remove the field and its resolver helper - resolveXdgConfigHome() no longer rewrites relative XDG_CONFIG_HOME values against cwd — relative values are rejected for parity with the TypeScript env package * fix(environments): make daemon XDG platform-token and device-id env-aware (#25497) * refactor(device-id): inline base-dir helper into migration 003 Removes the stale getDeviceIdBaseDir() export from device-id.ts — getDeviceId() no longer uses it, so it was a maintenance trap whose return value diverged from where device.json actually resolves in non-production envs. Its sole remaining caller was workspace migration 003, so inline the 2-line containerized-vs-homedir branch there. Brings migration 003 closer to the self-containment rule in assistant/src/workspace/migrations/AGENTS.md (no external imports beyond types/logger). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(environments): update AGENTS.md and ARCHITECTURE.md for per-assistant data layout (#25504) * fix(environments): CLI falls back to production on unknown VELLUM_ENVIRONMENT (parity with daemon and Swift) (#25541) * fix(permissions): restore legacy signing-key path in risk classification (#25542) * fix(config-watcher): use || for GATEWAY_SECURITY_DIR fallback to match sibling convention (#25543) * fix(cli): read platformBaseUrl from lockfile instead of legacy workspace config path (#25544) * fix(chrome-ext): native host reads env-aware lockfile path (#25547) * fix(environments): VellumPaths accepts relative XDG_CONFIG_HOME to match TS/daemon (#25550) * refactor(recover): drop unreachable legacy fallback in collision check (#25575) * fix(cli): sync platformBaseUrl to lockfile on vellum use / vellum wake (#25578) * fix(environments): env-seed fallback for getPlatformUrl, revert H1 sync-on-switch (#25595) Under our invariant "each env has its own lockfile, and all assistants in that lockfile share a platform URL", the H1 workspace-config→lockfile sync on `vellum use`/`vellum wake` was load-bearing for nothing: switching the active assistant within a single env cannot change the platform URL. Revert that sync. When no lockfile is seeded yet, fall back to the current environment's seed URL instead of the hardcoded production default so `VELLUM_ENVIRONMENT=dev vellum …` targets `dev-platform.vellum.ai` out of the box. - Revert H1: `vellum use` no longer calls `syncActiveAssistantConfigToLockfile`, `vellum wake` no longer re-runs `syncConfigToLockfile`, and the helper itself is deleted (the hatch-time sync is still done by `syncConfigToLockfile`, unchanged). - `getPlatformUrl()` fallback: prefer `getCurrentEnvironment().platformUrl` over the hardcoded prod URL so non-prod CLI users get the right tenant before any assistant is registered. - Tests: drop the H1 sync-on-switch suite, add a dev-env seed fallback test, keep the existing prod fallback test. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(environments): drift-guard for KNOWN_ENVIRONMENTS across TS sites (#25617) The set of recognized environment names is duplicated in three TS locations: `cli/src/lib/environments/seeds.ts` (SEEDS), the daemon's `assistant/src/util/platform.ts` (KNOWN_ENVIRONMENTS), and the Chrome native host's `native-host/src/lockfile.ts` (NON_PRODUCTION_ENVIRONMENTS). Cross-package imports don't work today — assistant's tsconfig restricts `include` to its own src tree, and the native host is a standalone TS project with `rootDir: ./src`. Add a drift-guard test in cli that parses the literal Set bodies from both external files and asserts they agree with CLI's SEEDS (minus `production` for the native host set). Catches any future addition to the seed table that fails to propagate to the other two sites. Also refresh the comments on all three declarations to point at the drift-guard test and the fast-follow plan: hoist the shared name list into a `packages/environments` package (mirroring `packages/ces-contracts` etc.) so this check becomes a compile-time import instead of a runtime regex. That refactor is planned alongside CLI-driven context support. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(environments): forward VELLUM_ENVIRONMENT across desktop→CLI→daemon handoffs (#25633) Two call sites were stripping VELLUM_ENVIRONMENT from spawn whitelists, breaking environment isolation for the main desktop launch path: 1. macOS `VellumCli.makeBaseEnvironment()` — `forwardedEnvKeys` did not include `VELLUM_ENVIRONMENT`, so every bundled-CLI command launched from the app (hatch, wake, sleep, retire, …) ran as production even when the app itself was built for a non-production environment. The app's Info.plist sets `VELLUM_ENVIRONMENT` at build time (`build.sh:1054`), so forwarding it is sufficient. 2. `cli/src/lib/local.ts` compiled-daemon spawn — the `daemonEnv` whitelist used when `bun run` is unavailable (packaged desktop builds) also omitted `VELLUM_ENVIRONMENT`. Even when the CLI process itself had the variable set, the spawned daemon fell back to production path/env behavior, so assistant-side env-scoped state (device ID, XDG-backed tokens and config reads) bled into prod. Note: the source/watch daemon spawn path in `local.ts:281` is unaffected — it uses `{...process.env}` and inherits everything. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * default to dev environment * remove duplicate env var --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * meet-bot: implement POST /play_audio streaming endpoint (#25974) * fix(macos): reset thinking anchor on streamingCode when tools active Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: vellum-apollo-bot[bot] <242025090+vellum-apollo-bot[bot]@users.noreply.github.com> Co-authored-by: root <root@assistant-89f9b42a-2563-4bbe-96b7-b2840c145b37-0.assistant-89f9b42a-2563-4bbe-96b7-b2840c145b37.warm-pool.svc.cluster.local> Co-authored-by: siddseethepalli <siddseethepalli@gmail.com> Co-authored-by: clopen-set <33433326+clopen-set@users.noreply.github.com>
Summary
vellumRoot()per-instance behavior (readsBASE_DATA_DIR, falls back tohomedir()), note env-aware XDG config paths viagetXdgVellumConfigDirName(), and clarify docker-mode non-interaction. Removed outdated "legacy / slated for removal" language forBASE_DATA_DIR.ARCHITECTURE.mdcovering environments-as-namespaces, per-assistant data dirs under$XDG_DATA_HOME, lockfile/config-dir rules per environment, backwards compat via the read path, and indexed it in the Architecture Docs table.ARCHITECTURE.mdto show the new XDG-based instance layout (~/.local/share/vellum/assistants/<name>/.vellum/), with a note on legacyinstanceDir = ~entries.cli/README.mdretire description to reflect that local retire removesresources.instanceDir, not a hardcoded~/.vellum, and noted the env-scoped lockfile path.clients/macos/README.mdmulti-instance note to point atLockfileAssistantandVellumPathsfor resolution, and reflect the new allocation path.Part of plan: env-data-layout.md (PR 7 of 7)