Skip to content

feat(sessions): Sessions module + per-runner hashed pairing & targeted dispatch (M3)#25

Open
hoangsnowy wants to merge 8 commits into
mainfrom
feat/m3-sessions-runners
Open

feat(sessions): Sessions module + per-runner hashed pairing & targeted dispatch (M3)#25
hoangsnowy wants to merge 8 commits into
mainfrom
feat/m3-sessions-runners

Conversation

@hoangsnowy
Copy link
Copy Markdown
Owner

Change description

M3 (model + hub rewire) of the remote repo-execution spine. Adds a new AgentOs.Modules.Sessions module and rewires the RemoteAgent path so dispatch is authenticated per-runner and routed to exactly one machine.

New module AgentOs.Modules.Sessions (schema sessions, Api host only):

  • RemoteSession (member x workspace unit of work) + Runner (a member's standing paired dev machine) aggregates, a tenant-filtered SessionsDbContext, an Initial EF migration, EF repositories with no-op fallbacks for DB-less boot, and /sessions + /runners REST endpoints (Member policy).
  • A runner's pairing token is generated server-side, persisted only as a salted SHA-256 hash, and the plaintext is returned exactly once from POST /runners — never stored, never in any list/get DTO.

Trust-boundary rewire of the RemoteAgent path (closes two known spine bugs):

  • Per-runner hashed pairing replaces the single shared static RemoteAgent:PairingToken. The hub resolves a connecting runner by id (tenant-unfiltered, since the connection has no tenant yet), verifies the presented token against the stored hash in constant time (CryptographicOperations.FixedTimeEquals), and rejects unknown/revoked/bad-token connects.
  • Targeted dispatch replaces the Clients.All broadcast. The broker resolves a request to one runner connection scoped by (tenant, member); a tenant-A request can never reach a tenant-B runner. Transport now uses Clients.Client(connectionId).
  • The standalone runner pairs with REMOTE_AGENT_ID + REMOTE_AGENT_TOKEN.

Deferred to follow-ups: EvidenceEntry.SessionId + durable EF evidence (M3c); flipping Pending→Paired / LastSeen + force-disconnect on revoke (M4); AppHost wiring of a seed runner.

Type of change

  • New feature (new agent / endpoint / capability)
  • Bugfix
  • Refactor (no behavior change)
  • Documentation / README
  • Infrastructure / CI configuration

Related issues

Part of the H1 remote repo-execution spine (M3). Follows M2 Workspaces.

Checklist

  • dotnet build passes locally in Release mode (0 warnings, TreatWarningsAsErrors on)
  • dotnet test passes — 318 passed / 5 skipped (live-LLM smoke) / 0 failed
  • No secrets committed (pairing tokens are hashed at rest; plaintext returned once, never persisted)
  • README / docs updated if public behavior changed
  • Measurable results provided when related to agent output quality (n/a — infra; test counts above)

Test plan

  • New RunnerPairingServiceTests (6): issue/verify round-trip, wrong token, salt uniqueness + cross-verify rejection, malformed-hash never-throws, empty-token rejection.
  • Updated RemoteAgentTests (11): targeted dispatch hits the matching member's connection, HasRunnerFor tenant+member matching, cross-tenant resolution returns false (leak closed), operator-mode empty-member matches any runner in tenant, timeout/disconnect/complete lifecycle, client zero-cost/no-runner/failure paths.

🤖 Generated with Claude Code

hoangsnowy and others added 8 commits May 30, 2026 23:49
…geted dispatch (M3)

Adds AgentOs.Modules.Sessions (schema `sessions`): RemoteSession (member x workspace) and
Runner (a member's standing paired dev machine) aggregates, a tenant-filtered DbContext, an
EF migration, repositories with no-op fallbacks, and /sessions + /runners REST endpoints.

Rewires the RemoteAgent remote-execution path to close two trust-boundary gaps:
- Pairing is now per-runner. The single shared static RemoteAgent:PairingToken is replaced by
  a high-entropy token per runner, stored only as a salted SHA-256 hash and verified in
  constant time (RunnerPairingService, Domain). The hub authenticates a connecting runner by
  id + token before trusting its tenant/owner.
- Dispatch is now targeted, not broadcast. The broker resolves a request to exactly one runner
  connection scoped by (tenant, member); a tenant-A request can never reach a tenant-B runner.
  Replaces hub Clients.All with Clients.Client(connectionId).

The standalone runner now pairs with REMOTE_AGENT_ID + REMOTE_AGENT_TOKEN. Sessions + RemoteAgent
are wired into the Api host only (not Web). Full suite 318 passed / 5 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ers/Sessions app (M3 UI)

Wires the Workspaces + Sessions modules into the Web host and adds a "Spine" desktop app
(AppCatalog + WindowHost) with three panes: connected Workspaces (list), Runners (register
-> one-time pairing token, list, revoke), and Sessions (create, list, close).

A Blazor Server circuit has no HttpContext, so ITenantContext is blank in a component; the app
reads tenant + user from AuthenticationState and calls new tenant-explicit repository overloads
(ListForTenantAsync / AddForTenantAsync / *ForTenantAsync) that bypass the ambient query filter
and scope by the passed tenant id.

Also adds a CLAUDE.md "Verification" convention: a user-facing change is not done until it is
wired into the desktop and exercised in the running app, not unit-tests-only.

Builds on the M3 model + hub rewire in this PR. Full suite 318 passed / 5 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds SpineAppTests (open the app -> three panes render; register a runner -> the one-time
REMOTE_AGENT_ID/TOKEN pairing panel appears) and a "Spine" row to the desktop-icon theory.
Same gate as the other UI E2E: skipped unless RUN_AGENTOS_E2E=true with the Web at AGENTOS_URL.
The register flow needs no DB -- the token is minted by the real IRunnerPairingService.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ifiable M3 UI)

The AgentOS desktop is globally [Authorize], so `dotnet run --project src/AgentOs.Web` previously
500'd without a live Keycloak -- the app's own Web could not run on its own. Adds a Development-only
auto-login (DevAutoAuthHandler) that signs in a fixed developer/tenant=default principal so the
desktop boots with no Keycloak/Postgres (modules already fall back to no-op repos). Defaults ON in
Development; hard-throws if ever enabled outside Development; the AppHost injects
Auth__DevAutoLogin=false so the full stack keeps using real OIDC.

Also makes WindowManagerService + ToastService scoped (per-circuit) -- as singletons on Blazor
Server they bleed windows/toasts across users and broke E2E isolation.

This makes the UI E2E runnable headless: SpineAppTests passes 2/2 against the standalone Web (open
Spine -> three panes; register a runner -> one-time REMOTE_AGENT_ID/TOKEN panel). Updates the
CLAUDE.md verification rule: every host must run standalone with one command, no external services.
Full suite 318 passed / 5 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds AgentOsRealAuthFixture (real OIDC login as the realm-seeded operator/operator against the
AppHost Web on https://localhost:5180, IgnoreHTTPSErrors) + SpineAppRealAuthTests:
- Login_RealAuth_RendersDesktop -- the real Keycloak round-trip lands on the authenticated desktop.
- Spine_RealAuth_RegisterRunner_AppearsInList -- registers a runner through the real stack and
  asserts it persists (real Postgres + tenant scoping).
Gated by RUN_AGENTOS_E2E_REAL=true (separate from the dev-auth RUN_AGENTOS_E2E suite). The login
smoke is verified green against the live stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed connector)

Extracts the connect-a-workspace flow into IWorkspaceConnector (validate via provider -> store the
PAT encrypted -> persist), shared by the HTTP endpoint and the desktop. Adds the connect form to the
Spine Workspaces tab so a repo can be connected without the API. Tenant-explicit throughout (a Blazor
circuit has no HttpContext): new IAppConfigStore.SetForTenantAsync/DeleteForTenantAsync and
IWorkspaceRepository.AddForTenantAsync -- EfAppConfigStore otherwise resolves the tenant from a fresh
scope that falls back to the default tenant. WorkspaceEndpoints.ConnectAsync now delegates to the
connector (one implementation, no duplication).

Tests: WorkspaceConnectorTests (valid -> persists + stores token; validation-fail -> nothing
persisted; missing fields; unknown provider) + a dev-auth E2E asserting the connect form renders.
Unit suite 322 passed / 5 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… desktop app (M1/M3c)

Lands the deferred durable tool-invocation evidence: EfToolInvocationLog persists every governed
tool call (success, error, policy denial) to a new `tools` schema (ToolsDbContext + Initial
migration); ToolsModule wires it when a DB is configured, else the in-memory ring buffer (dev/CI).
Adds SessionId to ToolInvocationRequest + ToolInvocationEvidence (M3c), threaded through
DefaultToolGateway, so evidence ties to a remote session once M4 wires session context.

UI: a new admin-only Evidence desktop app surfaces the recent tool-invocation trail per tenant
(reads the already-tenant-explicit ListRecentAsync). Tools module wired into the Web host.

Tests: EvidenceTests (gateway threads SessionId; EfToolInvocationLog appends + lists per tenant +
respects limit + round-trips SessionId, via EF in-memory) + an Evidence-opens E2E row. Unit suite
325 passed / 5 skipped / 0 failed; dev-auth UI E2E 9/9 green (Evidence opens, connect form renders).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds BrandingTests: the desktop title + TopBar are "AgentOS" and the retired "Agent Studio" name
does not surface in the desktop UI. Reworks LoginOverlay_ShowsOnFirstVisit (now
Desktop_DevAuth_LoadsWithoutLoginWall): the standalone dev Web uses Auth:DevAutoLogin, so "/" renders
the authenticated desktop directly with no login overlay/wall -- the old localStorage overlay was
retired by dev-auto-login. Backfill new tests all green (14 UI E2E pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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