feat(workspace): honour a workspace pinned by the IDE extension

Merged
SA
2mo ago
+885-2

Server half of "pin altimate-code serve to the datamate selected in the IDE". The extension half is AltimateAI/vscode-altimate-mcp-server#471 — this one lands first.

Why

serve is launched by the VS Code / Cursor extension, which already knows which datamate the user picked in its panel. Nothing carried that across, so skills and memory followed whatever binding the project had on the backend rather than the selection on screen.

The change is one hook

skill-sync.ts:663 and memory-sync.ts:143 both funnel through resolveBindingOutcome. Honouring the pin there is all that is needed — neither module changes.

  • pin.ts (new) — parses ALTIMATE_PINNED_WORKSPACE_{ID,NAME,ROOT} into a tagged absent | invalid | valid. A present-but-broken pin is invalid and fails closed, because falling through to normal resolution can legitimately return a different workspace, and silently working against a workspace the user did not pick is the one outcome this must never produce.
  • state.ts — the pin outranks the cached binding and server auto-adoption; is validated against listDatamates() (memoized on REVALIDATE_MS, since this runs per turn and per memory write); is scoped to the launch root; and is never persisted. pinned is stripped from anything read off disk so a hand-edited cache cannot impersonate extension authorization.

Why a separate env namespace

ALTIMATE_RESOLVED_WORKSPACE_* looks like the obvious home, but launch-resolve.ts:69 sets only ..._ID for the TUI's --workspace flag. Sharing the namespace would make every such TUI session look like a half-populated pin and the fail-closed rule would break --workspace outright. readPin also returns absent unless ALTIMATE_CODE_SERVE is set, so the two mechanisms can never both be live.

Offline

Splits on whether this process ever validated the pin: never-validated is unknown (nothing was established, and the env name is a presentation fallback, never an authorization one); previously-validated keeps serving within the TTL — which is what this module already does for a cached binding rather than "tear a working setup down over a network blip". Verified non-membership always fails closed.

Verification

23 new tests (575 workspace tests total, 0 failures) covering the parser, precedence, fail-closed paths, root scoping, the offline split and memoization.

End-to-end against a real serve with the extension branch, OPENCODE_PRINT_LOGS=1:

workspace gate enabled=true serve=1 pinId=237
syncSkills entered canon=/home/coder/project enabled=true
resolved the workspace pinned by the IDE extension datamateId=237 datamateName=activity_test
workspace has no custom skills; removed the local snapshot

datamateName came back from the server, not the env, so the listDatamates() validation path is exercised.

Then, with a custom skill added to that workspace and memory enabled on it:

workspace skills synced datamateId=237 skills=1
  • Skills — the workspace's skill landed at .altimate-code/skill/_workspace/<id>/SKILL.md with a manifest pinned to datamateId: 237, and its contents matched the skill added in the workspace UI.
  • Memory — the agent's altimate_memory_write produced a local block, and it appears in that workspace's "What's been remembered" list in the product UI.

Both are the pinned datamate's, not the project's backend binding — which is the whole point of the change.

Debugging note: Log.create().info() is gated on printEnabled(), which defaults to OFF. Without OPENCODE_PRINT_LOGS=1 this subsystem is completely silent and looks dead. That cost me a long detour; worth knowing before debugging it.

Not in this PR

  • memory-sync has no adopted checks at all, so ongoing mirroring already fires for server-adopted bindings despite the contract documented at state.ts:349. Pre-existing, not introduced here, and fixing it changes behaviour for current users — deserves its own PR and release note.
  • No backfill trigger for a pin. backfillOnBind's only caller is recordApprovedBinding (the explicit-link path), and the pin is deliberately not persisted, so pre-existing local memory is not pushed. New saves mirror normally through mirrorBlock.
  • No purge-before-fetch in skill-sync when the pin changes to a different workspace.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added IDE workspace pinning to associate sessions with a validated workspace.
    • Workspace pins are checked for account access and directory scope before use.
    • Temporary API outages can preserve previously validated workspace access during a limited grace period.
    • Invalid, inaccessible, or unverified pins fail safely without binding the session.
  • Bug Fixes

    • Prevented nested commands from unintentionally inheriting workspace pin settings.
    • Improved protection against symlink-based paths escaping the pinned workspace.