Skip to content

feat(accounts): unify native logins and provider accounts under one namespace (RUSH-2527) - #2563

Open
muqsitnawaz wants to merge 20 commits into
mainfrom
agents/rush-2527-account-core
Open

feat(accounts): unify native logins and provider accounts under one namespace (RUSH-2527)#2563
muqsitnawaz wants to merge 20 commits into
mainfrom
agents/rush-2527-account-core

Conversation

@muqsitnawaz

@muqsitnawaz muqsitnawaz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What + type: feature — unify native harness logins and provider credential accounts under one account model, wired end-to-end through run/routine/profile/view/fleet (RUSH-2527).

Model

An account is one authorization identity, in two kinds sharing one namespace (meta.accounts) and one renderer:

  • Provider credential accounts (accounts add) — a durable key/token stored as a policy-never secrets bundle; portable, sync copies it.
  • Native account records (accounts name <agent@version> <name>) — a durable name for a harness's own login, metadata only in meta.accounts.native (stable id + identity key + scope); the OAuth/session credential is never copied. A native lookup reads only meta — never the provider bundle store or keychain.

Bindings live in meta.accounts.bindings (target → stable id). resolveAccountSelection order: explicit --account → exact agent@version binding → device-scoped agent binding → per-harness default.

Capability truth (account-capabilities.ts)

Version-scoped strong: Claude/Codex/Grok only. Muse: version, email-conditional. Cursor: blocked. Antigravity/Kimi/Droid/OpenCode: recorded device-scoped opaque but unsupported — a NativeAccount has no device-id discriminator, so an opaque/singleton identity can't be proven unique across synced metadata (truthful over false support).

Runtime wiring (not cosmetic)

  • exec.ts — binding order via a target key (a custom harness keys on its profile name). Provider → env injected (fail-closed on missing credential). Native → no secret/env; a pinned version is validated live, an unpinned run is pinned to the install actually signed in as that identity (resolveAccountVersion, not the global default); fails closed for remote/cloud and cross-harness. A native account is rejected on a provider-backed harness (e.g. run deepseek --account work) so provider env is never injected under a native claim.
  • runner.ts — routine account: classifies native vs provider; native translates name→identity key then pins the matching install (never forwarded/injected), refuses cross-harness. Native accounts are rejected before any host/cloud dispatch (assertRoutineAccountLocalForPlacement at the top of both the foreground and detached placement blocks, which run before resolveRoutineLaunch).
  • accounts.tsattach validates the target exists (installed agent@version, harness id, or existing profile; typos rejected) before mutating, and refuses a native account on a provider-backed profile.
  • Displayagents view + fleet/harness inventory render work · email.

Run evidence (fleet-local asset, path not embed)

Captured agents accounts --help + the real temp-home unit run: yosemite-s0:/home/muqsit/src/github.com/muqsitnawaz/agents-cli/.agents/worktrees/rush-2527-account-core/apps/cli/../../.agents/artifacts/2026-08-10/rush-2527-accounts-run.log

Tests (real paths / injected deps, no mocks): spawn-selection classification + binding order + profile-name binding + cross-harness + native-on-provider rejection + native-lookup-no-provider-read regression (account-registry.test.ts); pinned capability semantics (account-capabilities.test.ts); attach target/profile validation (accounts.test.ts); routine pin-by-identity + cross-harness + host/cloud placement rejection (runner.test.ts); signed-out \0 sentinel collision (harness-inventory.test.ts). Affected-surface sweep green; build.sh --skip-tests green; command-index + docs verify. Merged current origin/main.

Ticket: RUSH-2527

muqsitnawaz and others added 4 commits August 10, 2026 08:14
…amespace (RUSH-2527)

Native harness logins get durable, fingerprint-keyed aliases (metadata only —
no tokens or raw emails), sharing one account namespace and one renderer with
provider credential accounts. Adds positional grammar (name/view/attach/detach,
positional sync), safe remove/reference checks, and recovers retired version-bound
labels into aliases by their preserved fingerprint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source: bun run gen:index — adds name/view/attach/detach to docs/command-index.{md,json}
and command-reference.html (RUSH-2527).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-2527)

Replace the standalone fingerprint-alias file with the unified model: native
account records live in meta.accounts.native (metadata only, no OAuth copy),
bindings in meta.accounts.bindings (target -> stable id), and account-capabilities
derives version- vs device-scope. accounts name/attach/detach/view now support
native attach to an installation or device-scoped harness; resolveAccountSelection
resolves explicit -> exact-target -> device-scoped -> per-harness default. Aligns
account-core with the reference implementation and the fleet-auth/transport tracks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…527)

findUnifiedAccount defaulted its doc arg to readAccountRegistry(), which evaluated
before the native check — so a native view/attach/run triggered a bundle read,
legacy-accounts.yaml migration, and a keychain decrypt (a Touch ID prompt, or a
hard crash on an undecryptable legacy item). Make the arg optional and read the
registry lazily only for the provider fallback. Regression test uses a poisoned
registry that throws on any access and asserts a native lookup never reaches it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@muqsitnawaz
muqsitnawaz marked this pull request as draft August 10, 2026 15:34
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Status update — account-core landed; EXEC-ACCOUNT-3 runtime wiring is the remaining seam

Converted to draft: per review, attach bindings must be wired into spawn selection before merge — this PR should not land while attachments are cosmetic.

Landed in this PR (account-core: model + command + resolution contract)

  • Unified model: meta.accounts.native (metadata-only native records — stable id + identity key + scope; no OAuth copy) + meta.accounts.bindings (target → stable id). account-capabilities.ts classifies each harness version- vs device-scoped. src/lib/account-registry.ts, src/lib/account-capabilities.ts, src/lib/types.ts.
  • Positional command surface: name <source> <name>, attach <account> <target> / detach, unified view (alias inspect), sync <account> <device>; merged native+provider renderer. src/commands/accounts.ts.
  • Resolution contract the runtime consumes: resolveAccountSelection(explicit, agent, meta, { target }) → explicit → exact-target binding → device-scoped binding → per-harness default (account-registry.ts).
  • No-Touch-ID fix (was a real crash): findUnifiedAccount no longer default-evaluates readAccountRegistry(); native lookups never touch the provider bundle store, legacy migration, or keychain. Regression test uses a poisoned registry that throws on any access (account-registry.test.ts).
  • Tests: registry (18, incl. the regression), capabilities, catalog, accounts — green; adjacent harness/profiles — 47 passed; ./scripts/build.sh --skip-tests — green. Run evidence asset: yosemite-s0:.agents/artifacts/2026-08-10/rush-2527-accounts-run.log.

Remaining before merge (EXEC-ACCOUNT-3 — runtime wiring; hand-off to root / runtime-fleet-auth track)

These touch the execution chokepoint and sibling-track files (exec.ts, runner.ts, view.ts, devices/*-inventory.ts, hosts/*); I've stopped short of editing them to avoid a half-wired change to the spawn path and a collision with the fleet-auth track (which already edits exec.ts/routines.ts). Precise seams:

  1. Local run selectionsrc/commands/exec.ts:2590-2600: pass target (${agent}@${concreteVersion}) into resolveAccountSelection; when the selection resolves to a native account, validate the target install's live identity fingerprint and inject no secret/env (native login used as-is), instead of routing through resolveCredentialAccount (which only knows provider accounts). Note the concrete version is only final after resume/isolation resolution — the binding-key must be built there, not at 2591.
  2. Routinessrc/lib/runner.ts: a routine account: naming a native identity must pin the installed version home that owns it and fail before spawn if unavailable; never rotate/forward it through the provider path (EXEC-ACCOUNT-3, spec docs/specifications.md:1824).
  3. Profiles/custom harness — consume a per-target binding for custom harnesses.
  4. Displaysrc/commands/view.ts: render the attached account name per installation.
  5. Fleet/harness inventory labelssrc/lib/devices/fleet-inventory.ts (+8 in ref 9cb33177a) + harness-inventory.ts.
  6. Remote fail-closedsrc/lib/hosts/*: reject native-account forwarding to remote/cloud/lease without identity presence on the target.
  7. Spawn-selection tests (not only CRUD): exercise exec selecting a bound native vs provider account.

Reference for the seams: 9cb33177a (unified-accounts worktree). Coordinate transport with the cred-transport track; do not duplicate it.

muqsitnawaz and others added 4 commits August 10, 2026 08:44
…2527, EXEC-ACCOUNT-3)

resolveSpawnAccount classifies the selected account and exec.ts acts on it:
- binding order is honored via a target key — a custom harness binds by its raw
  profile name, a native/global run by exact agent@version.
- provider account: env resolved and injected (fail-closed on missing credential).
- native account: no secret/env injected; a pinned version is validated to hold
  the named identity, and an unpinned run is pinned to the installed version
  actually signed in as that identity (resolveAccountVersion, not the global
  default). A native account fails closed for a remote target (no identity
  forwarding) and for a mismatched harness.
Tests cover provider vs native classification, binding order, profile-name
binding, and cross-harness rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (RUSH-2527)

- view.ts / fleet-inventory / harness-inventory: prefix the durable native
  account name (work · email) wherever a signed-in identity is shown.
- runner.ts: routine account selection classifies native vs provider — a native
  routine account translates its durable name to the identity key before
  resolveAccountVersion pins the matching install, refuses a cross-harness login,
  and never forwards/injects; a native default/binding no longer crashes the
  provider credential path. Tests cover pin-by-identity and cross-harness failure.
- Strip a stray NUL byte from harness-inventory.ts (it read as binary) and force a
  text diff via .gitattributes so the change is reviewable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… escape (RUSH-2527)

The signed-out group key is a NUL-prefixed sentinel so a real account label named
literally "signed-out" cannot merge into the signed-out bucket. Express it as the
TypeScript escape '\0signed-out' (text, no raw NUL byte in the source) rather than
an embedded NUL, and drop the .gitattributes workaround. Adds a groupByAccount test
with a real 'signed-out' label proving it stays separate from the null bucket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@muqsitnawaz
muqsitnawaz marked this pull request as ready for review August 10, 2026 15:57
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Runtime wiring complete — EXEC-ACCOUNT-3 seams landed

The bindings are no longer cosmetic; ready for review.

Spawn selection (exec.ts)resolveSpawnAccount follows the binding order (explicit → exact agent@version → device-scoped agent → default) via a target key that a custom harness keys on its profile name. Provider → env injected (fail-closed on missing credential); native → no secret/env, live-fingerprint validated; an unpinned native run is pinned to the installed version actually signed in as that identity (resolveAccountVersion, not the global default); fails closed for a remote/cloud target or a cross-harness login.

Routines (runner.ts) — all three selection sites classify native vs provider: a native routine account: translates its durable name → identity key before resolveAccountVersion pins the matching install (never forwarded/injected), refuses a cross-harness login, and a native default/binding no longer crashes the provider path.

Displayagents view + fleet-inventory + harness-inventory render work · email for a named identity.

Sentinel fix — the signed-out group key is kept collision-proof as a TS \0signed-out escape (text, no raw NUL); .gitattributes workaround removed; a groupByAccount test proves a real signed-out label never merges into the null bucket.

Tests (real paths / injected deps, no mocks): spawn-selection classification + binding order + profile-name binding + cross-harness rejection (account-registry.test.ts); routine pin-by-identity + cross-harness failure (runner.test.ts); the signed-out collision (harness-inventory.test.ts); the native-lookup no-provider-read regression (poisoned registry). Affected-surface sweep: 462 green; build.sh --skip-tests green; command-index + docs verify.

@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Non-author review: REQUEST CHANGES. Blocking before merge: (1) routine host/cloud placement bypasses native-account rejection because placement occurs before resolveRoutineLaunch; reject before dispatch and test foreground/detached paths. (2) Reject native accounts on provider-backed custom profiles; otherwise profile provider env remains active while the run claims a native identity. (3) Correct the capability registry: strong version identity is currently safe only for Claude/Codex/Grok; Cursor is blocked pending token-bound identity; Muse requires email; Antigravity/Kimi/Droid/OpenCode are opaque/singleton device identities, not strong. Pin classifications with tests. (4) Validate attach targets as an installed agent@version, a supported device-scoped native agent, or an existing custom profile; reject typo targets before mutation. The positional name <source> <name> grammar is intentional because the user explicitly rejected --from/--to flags.

muqsitnawaz and others added 3 commits August 10, 2026 09:12
…+ routine placement (RUSH-2527)

- Capability truth: version-strong is only Claude/Codex/Grok; Muse is email-
  conditional; Cursor is blocked; Antigravity/Kimi/Droid/OpenCode are recorded as
  device-scoped opaque but UNSUPPORTED — a NativeAccount has no device-id
  discriminator, so an opaque/singleton identity can't be proven unique across
  synced metadata (Droid has no account key; Antigravity/OpenCode can alias). A
  truthful "unsupported" over false support. nativeAccountNameable gates naming.
- attach validates the target BEFORE mutating: it must be an installed
  agent@version, a device harness id, or an existing custom profile (typos
  rejected). A native account is refused on a provider-backed custom profile
  (which would inject provider env under a native identity claim).
- Routines: reject a native account before ANY host/cloud dispatch —
  assertRoutineAccountLocalForPlacement runs at the top of both the foreground
  and detached placement blocks (placement resolves before resolveRoutineLaunch).
Tests: pinned capability semantics; attach target/profile validation; routine
placement rejection for host+cloud with a provider allowed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t spawn (RUSH-2527)

`agents run deepseek --account work` bypasses `attach`, so the exec path must also
refuse it: resolveSpawnAccount now throws when a native account is selected while a
provider is in play (exec passes the profile's provider), since the harness would
still inject its provider credentials under a native identity claim. A bare native
run (no provider) is unaffected. Adds a direct run-resolution test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unt-core

# Conflicts:
#	apps/cli/docs/command-index.json
#	apps/cli/docs/command-index.md
#	apps/cli/docs/command-reference.html
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Review blockers addressed (commits 513af03, 97e2ced) + merged current main

  1. Routine host/cloud before resolveRoutineLaunchassertRoutineAccountLocalForPlacement now runs at the top of BOTH the foreground (executeJobPlaced) and detached (executeJobDetachedClaimed) placement blocks, before any host/cloud dispatch. Native rejected; provider allowed. Tests for host + cloud modes.
  2. Native on provider-backed profile → rejected at attach (profile target with a provider) AND at spawn: resolveSpawnAccount throws when a native account is selected while a provider is in play, so run deepseek --account work (bypassing attach) fails before env merge. Direct run-resolution test added.
  3. Capability truth → version-strong = Claude/Codex/Grok only; Muse email-conditional; Cursor blocked; Antigravity/Kimi/Droid/OpenCode recorded device-opaque but unsupported (no device-id discriminator in NativeAccount → can't prove identity uniqueness across synced metadata; Droid has no account key, Antigravity/OpenCode can alias). Pinned-semantics tests.
  4. attach target validationclassifyAttachTarget rejects a typo before mutation; the target must be an installed agent@version, a harness id, or an existing custom profile.

Also: kept the signed-out sentinel collision-proof as a TS \0signed-out escape (no raw NUL) with a groupByAccount collision test; the --from/--to note is not applicable (positional name <source> <name> is the approved surface). Merged origin/main (docs regenerated). Affected sweep green; build + docs verify.

@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Non-author re-review: prior blockers are resolved; two blockers remain. (1) Provider routine accounts are accepted for host/cloud but config.account is not forwarded. Host dispatch already supports an account name, so forward it for remote-local resolution without copying a secret; cloud must support a secure provider account or fail before dispatch. Never accept-and-ignore. (2) docs/credential-management.md still teaches Cursor/OpenCode/Antigravity/Kimi/Droid as attachable device-native accounts, contradicting the conservative capability table; update the matrix to match supported/conditional/discovery/unsupported truth. REQUEST CHANGES until both are fixed and tested.

…n cloud; doc capability truth (RUSH-2527)

Review round 2:
- A provider routine account is no longer silently dropped on placement: the host
  dispatch forwards it by NAME (dispatchPromptToHost account; the remote resolves
  its own local bundle, no secret copied), and cloud placement fails loud before
  dispatch (no secure provider-account injection there yet). Native stays rejected
  for both. Guard tests updated for provider-host allow / provider-cloud reject.
- credential-management.md: replace the stale "Cursor/OpenCode/Antigravity/Kimi/
  Droid attach as device native accounts" claim with the conservative table —
  only Claude/Codex/Grok (version) + Muse (conditional) are nameable; the opaque
  device harnesses and Cursor are unsupported. attach row corrected: native binds
  only to a supported agent@version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Review round 2 addressed (commit 00b8802)

  1. Provider routine account no longer dropped on placement. The host dispatch now forwards it by NAME (dispatchPromptToHost({ account })) — the remote resolves its own local bundle, so no secret crosses the wire. Cloud placement fails loud before dispatch (assertRoutineAccountLocalForPlacement: provider+cloud → clear error) since there's no secure provider-account injection there yet. Native stays rejected for both host and cloud. Guard tests updated (provider-host allowed, provider-cloud rejected).
  2. Doc capability truth. credential-management.md no longer claims Cursor/OpenCode/Antigravity/Kimi/Droid attach as device native accounts. It now carries the conservative table (Claude/Codex/Grok supported version-scoped; Muse conditional; the opaque device harnesses + Cursor unsupported) and the corrected attach row (native binds only to a supported agent@version).

Build + affected sweep green. --from/--to remains N/A (positional name <source> <name> is the approved surface).

…unt-core

# Conflicts:
#	apps/cli/docs/command-index.json
#	apps/cli/docs/command-index.md
#	apps/cli/docs/command-reference.html
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Non-author re-review: runtime behavior and capability docs are correct. One test blocker remains: the new test calls only assertRoutineAccountLocalForPlacement; it does not exercise executeJobOnHost and therefore would still pass if config.account were dropped again. Add an execution-boundary test asserting a provider host routine emits account: prov, and that provider cloud placement produces zero cloud dispatch calls. REQUEST CHANGES until that real path is covered.

… (RUSH-2527)

- Extract buildHostDispatchOptions (used by executeJobOnHost) so the host dispatch
  boundary is unit-testable; a test asserts the provider account rides along as
  account:'prov' (catches the "executeJobOnHost drops config.account" regression a
  guard-only test would miss).
- runner-cloud-account.test.ts drives the real executeJobPlaced with a cloud
  placement + a provider account and asserts it fails loud at the placement guard
  with ZERO cloud dispatch calls (resolveProvider never invoked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Review round 3 addressed (commit beab26b)

Execution-boundary tests, not guard-only:

  • Host forwards the account: executeJobOnHost now builds its dispatch options via the exported buildHostDispatchOptions; a test asserts the provider account rides along as account: 'prov' — this fails if the line is ever dropped again (the exact regression cited).
  • Cloud makes zero dispatch calls: runner-cloud-account.test.ts drives the real executeJobPlaced with a cloud placement + provider account and asserts it throws at the placement guard with resolveProvider never called (mocked spy).

Build + affected sweep (94) green.

@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Final review fix pushed at e52530186.

Evidence:

  • bun vitest run src/lib/runner.test.ts — 1 file passed, 40 tests passed.
  • ./scripts/build.sh --skip-tests — TypeScript compiled; 1,432 files built.
  • Host provider account is asserted at the dispatch-options boundary.
  • Cloud provider account uses an injected dispatcher counter and proves zero dispatch calls before the fail-loud error.
  • Removed the mocked second runner test file.

@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Non-author re-review: APPROVE at e52530186.

Verified:

  • runner.ts builds the canonical host dispatch payload with account: config.account; runner.test.ts asserts account === "prov".
  • Provider cloud placement executes the account guard before either dispatcher; the injected cloud-dispatch counter remains zero.
  • Tests use injected dependencies in runner.test.ts, with no module mocks or second test file.
  • credential-management.md matches account-capabilities.ts: Claude/Codex/Grok supported, Muse conditional, opaque/Cursor unsupported, Gemini discovery-only.
  • git diff --check clean.

Verdict: APPROVE.

@muqsitnawaz

Copy link
Copy Markdown
Contributor Author

Independent non-author re-review at 114b008: APPROVE. Verified provider host forwarding preserves account='prov'; provider cloud refusal occurs before the supplied dispatcher with zero calls; runner tests use injected production boundaries without module mocks or test-only exports; credential capability docs match the canonical capability table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant