Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ agent-shim usage [--identity <name>] [--provider <name>] [--since <duration>] [-
agent-shim session list [--identity <name>] [--all] [--json] # sessions that spawned agents or ran workflows, from the transcripts on disk
agent-shim session show <session> [--identity <name>] [--json] # one session's subagents, forks, workflow agents, tasks and workflow runs
agent-shim account show [<identity>] [--refresh] [--json]
agent-shim mcp [--allow-writes] [--allow-remote-control] # serve the door's typed API as MCP tools on stdio; read-only without the flags
agent-shim mcp [--allow-writes] [--allow-remote-control] [--allow-history] # serve the door's typed API as MCP tools on stdio; read-only without the flags
agent-shim mcp permissions show [--json] # what the stored file allows
agent-shim mcp permissions allow|deny <writes|remote-control> # change it; a running server follows on its next request, with no restart
agent-shim mcp permissions allow|deny <writes|remote-control|history> # change it; a running server follows on its next request, with no restart
agent-shim update [--check] [--restart-door] [--mode <off|notify|auto>] [--json]
agent-shim completion <bash|zsh|fish>
agent-shim shim enable [--dir <path>] [--force]
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ src/
resets.ts # every recorded quota window with when it resets, the pure reading `usage --resets`, the library and the door's `usage.resets` share
account.ts, commands.ts # the plan and tier read from an identity's stored login, and the `usage` and `account show` commands
signIn.ts # whether an identity can authenticate as itself (a Keychain item or credentials file, or its own credential block), decided from facts the caller gathers so `account show` and `doctor` share one rule
mcp/ # `agent-shim mcp`: an MCP server on stdio over the serving door's typed API. `tools.ts` derives one tool per non-streaming operation of the door's OpenAPI document (name from the operation id, input schema from its Zod-derived request body or query parameters, writes only where an opt-in allows their group), `server.ts` serves them and answers a call from the procedure's own output or as an error result carrying the oRPC code, and `command.ts` wires the pinned TLS link and re-reads the control material per request so a door restart is followed. `permissions.ts` keeps what the person has allowed (`agent-shim mcp permissions`) in an owner-only file in the state root that the server re-reads on every request and watches to announce `tools/list_changed`, so a grant or revocation needs no restart; no door procedure writes it and no tool is derived for it, which is what keeps an agent from raising its own permissions, and the startup flags are a floor the file only raises. The MCP SDK is CLI-only: the library build fails if it is imported there
mcp/ # `agent-shim mcp`: an MCP server on stdio over the serving door's typed API. `tools.ts` derives one tool per non-streaming operation of the door's OpenAPI document (name from the operation id, input schema from its Zod-derived request body or query parameters, writes, and the reads of session history, only where an opt-in allows their group), `server.ts` serves them and answers a call from the procedure's own output or as an error result carrying the oRPC code, and `command.ts` wires the pinned TLS link and re-reads the control material per request so a door restart is followed. `permissions.ts` keeps what the person has allowed (`agent-shim mcp permissions`) in an owner-only file in the state root that the server re-reads on every request and watches to announce `tools/list_changed`, so a grant or revocation needs no restart; no door procedure writes it and no tool is derived for it, which is what keeps an agent from raising its own permissions, and the startup flags are a floor the file only raises. The MCP SDK is CLI-only: the library build fails if it is imported there
frontdoor/ # the front-door daemon: one agent-shim listener routing every session agent-shim routes (a provider's, or headroom's)
route.ts # the URL space (/providers/<name>), the identity step that strips the launcher-injected session headers, and the route interface a destination implements (including whether a headroom hop may sit in front)
pipeline.ts # the ordered pipeline (identify, admit, response middleware at each response head, headroom hop then route) and the typed hook point where the usage-tracking middleware registers
Expand Down Expand Up @@ -105,6 +105,7 @@ src/
quotaExpiringEvents.ts # the expiring-quota publisher: an identity's unspent window entering the final fraction of its own span, checked on the supervisor's tick against the snapshots the usage source's own events keep fresh (seeded once from the snapshot directory), one event per window instance so a scheduler can spend what would otherwise be lost
eventsApi.ts # the door-wide events surface of the typed API (`events.subscribe`, its source filter validated against the live publisher registry), the router `controlApi.ts` mounts beside the Remote Control and control-plane ones under the one prefix and token gate
launchApi.ts, launchSchemas.ts # the door's launch resolution as the same typed API (`launch.resolve`), read-only: the launcher's own decision for one absolute directory with its effects left out, behind the same per-generation control token, mounted beside the control plane on the one oRPC prefix; the answer is a closed shape of names, flags, paths and credential summaries, so a token has no field to appear in and a value outside the shape fails output validation, and nothing is spawned
agentTreesApi.ts # the agent tree reader as the same typed API (`agentTrees.list` and `agentTrees.show`), read-only: the data `session list` and `session show` print, so an MCP client or the library asks the door for it
supervisor.ts, ensure.ts, state.ts # the three-listener lifecycle (sticky ports, session registry, an idle shutdown that stops the listeners and then the Remote Control stream hub, whose close persists every live attachment's sequence cursor before the process exits; a startup failure takes the same hub close; a door whose state file comes to name another live supervisor is superseded, takes no new sessions, never writes the state file again and exits through the same listener and hub close once the sessions it served have ended; each tick's registry listing and prune results also feed the launch lifecycle publisher on the event backbone), the launcher's lock-and-poll bring-up that authenticates the listener before registering, and the coordination state; the session registry holds each launch's capability token behind owner-only modes and has its own list, prune and remove (never headroom's token-less ones)
probe.ts # the launcher's synchronous TLS probe of the provider listener, run on a worker thread the launcher blocks on
trust.ts # the NODE_EXTRA_CA_CERTS bundle a routed child gets: the CA, combined with any bundle the parent environment already named
Expand Down
4 changes: 2 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ agent-shim usage [--identity <name>] [--provider <name>] [--since <duration>] [-
agent-shim account show [<identity>] [--refresh] [--json]
agent-shim session list [--identity <name>] [--all] [--json] # sessions that spawned agents or ran workflows, newest transcript first
agent-shim session show <session> [--identity <name>] [--json] # the session id or an unambiguous prefix
agent-shim mcp [--allow-writes] [--allow-remote-control] # serve the door's typed API as MCP tools on stdio; read-only without the flags
agent-shim mcp [--allow-writes] [--allow-remote-control] [--allow-history] # serve the door's typed API as MCP tools on stdio; read-only without the flags
agent-shim mcp permissions show [--json] # what the stored file allows
agent-shim mcp permissions allow|deny <writes|remote-control> # change it; a running server follows on its next request, with no restart
agent-shim mcp permissions allow|deny <writes|remote-control|history> # change it; a running server follows on its next request, with no restart
agent-shim update [--check] [--restart-door] [--mode <off|notify|auto>] [--json]
agent-shim completion <bash|zsh|fish>
agent-shim shim enable [--dir <path>] [--force]
Expand Down
2 changes: 1 addition & 1 deletion docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
| Codex account and rate limits | `readCodexAccount`, `readCodexRateLimits` with `realCodexAppServerPort`, the `CodexAccountReadSchema` and `CodexRateLimitsReadSchema` answers, `CODEX_APP_SERVER_GENERATED_AGAINST`, and the `CodexAppServerUnavailableError`, `CodexAppServerRequestError` and `CodexAppServerProtocolError` they raise | Ask Codex itself what a Codex home holds, so no consumer opens `auth.json` or reads Codex's private rollout files: each call starts a short-lived `codex app-server` under `CODEX_HOME` over stdio, asks `account/read` (the account type and, for a ChatGPT sign-in, its email and plan) or `account/rateLimits/read` (the windows with their lengths and reset instants, and the banked reset credits), validates the answer and ends the server. It never starts a login and never sends a prompt. A home with no login answers `account: null` to the first and is refused by the second as `CodexAppServerRequestError` carrying the server's own code. The shapes are the stable surface of the app-server's generated schema as of `CODEX_APP_SERVER_GENERATED_AGAINST`; an answer that does not match raises `CodexAppServerProtocolError` naming the installed Codex version beside that one. `bin` names the executable (`codex` on `PATH` by default), and `timeoutMs` and `signal` bound the read. The port is injected, so a consumer or test can serve the protocol itself. |
| Door event backbone | `createDoorEventHub`, `rcFanoutOnDoorHub` (with `DOOR_EVENT_BUFFER_EVENTS` and the `DoorEventHub`/`DoorEventPublisher`/`DoorEventStream` shapes), `DoorEventSchema`, `DoorEventSourceQuerySchema`, `LaunchLifecycleEventSchema`, `DoorHealthEventSchema`, `QuotaExpiringEventSchema` and the `UsageChangeEvent` payload type with the `DOOR_EVENT_SOURCE_RC`/`LAUNCH_EVENT_SOURCE`/`USAGE_EVENT_SOURCE`/`DOOR_HEALTH_EVENT_SOURCE`/`QUOTA_EXPIRING_EVENT_SOURCE` tags, `createLaunchEventPublisher`, `createDoorHealthPublisher`, `createQuotaExpiringPublisher`, and the `createEventsApiRouter` the whole-door mount serves beside the others | Run the door's generalised event fan-out in process: register sources, publish source-tagged events stamped with a per-source sequence, and subscribe synchronously or through the bounded drop-oldest pull bridge whose gaps that sequence make visible; wrap the Remote Control fan-out on as the backbone's first publisher, turn the session registry's register, prune and end moments into events, publish every usage snapshot the store writes (the usage source), the supervisor's own state transitions (the door source), and each unspent window entering its final span (the quota-expiring source, computed from the same snapshots the pool ranking reads); `events.subscribe` serves it on the whole-door mount |
| Usage | `readUsageSnapshot`, `listUsageSnapshots`, `UsageSnapshotSchema`, `effectiveWindow`, `collectResets`, `ResetScheduleSchema` | Read the per-identity usage snapshot a statusline shows, and read one of its windows as agent-shim does (a window past its `resetsAt` counts as empty, so a consumer keeps no copy of that rule). `collectResets` lists every recorded window of every snapshot with when it resets, soonest first, as `usage --resets --json` prints it (also published as `schema/ResetSchedule.schema.json`) |
| Agent tree | `listSessions`, `readSessionAgentTree` with the `AgentReadFs` filesystem port, and the `SessionAgentTreeSchema`, `AgentNodeSchema`, `AgentMetaSchema`, `AgentKindSchema`, `TaskItemSchema` and `WorkflowRunSchema` they return and read | Read a session's subagents, forks and workflow agents with their parents, its task list and its workflow runs from the transcripts on disk, over an injected filesystem. A file that cannot be used is reported in the tree's `problems`, never dropped; the layout is in [agent-transcripts.md](agent-transcripts.md) |
| Agent tree | `listSessions`, `summariseSession`, `readSessionAgentTree` with the `AgentReadFs` filesystem port; `collectSessionList`, `locateSession` and `showSession` (identities that share one history store scanned once, over `SessionCommandFs` with `realSessionFs`); the `SessionAgentTreeSchema`, `SessionSummarySchema`, `AgentNodeSchema`, `AgentMetaSchema`, `AgentKindSchema`, `TaskItemSchema` and `WorkflowRunSchema` they return and read; and on the door's typed API `createAgentTreesApiRouter` with its input and output schemas | Read a session's subagents, forks and workflow agents with their parents, its task list and its workflow runs from the transcripts on disk, over an injected filesystem. A file that cannot be used is reported in the tree's `problems`, never dropped; the layout is in [agent-transcripts.md](agent-transcripts.md) |
| Pool usage refresh | `refreshPoolUsage`, `refreshLaunchPoolUsage`, `poolSelectedByLaunch`, `refreshStalePoolMembers`, `AnthropicUsageRefresher` | A pool ranks on the usage recorded when requests pass the front door, so an idle member ranks on an old observation and one never routed ranks as unknown. `refreshPoolUsage({ cwd, poolNames })` asks each stale member of the named pools (nested pools included) for its plan windows through Claude Code's usage report, which makes no model request, and records them; use it before a quota display or before a launch you plan yourself. `refreshLaunchPoolUsage(options)` does it for the pool a launch with the same arguments as `prepareClaudeLaunch` would select (`poolSelectedByLaunch` names it), and `runClaudeLaunch` calls it before it picks. A failed fetch is logged through `log` and the member keeps its record. `refreshStalePoolMembers` is the same step over an injected refresher. It needs the Agent SDK package, a dependency of this one, loaded only when a pool is named. The command line bundle carries the SDK inside it and gives it a stand-in for `import.meta.url` (the SDK, an ES module, hands it to `createRequire`, which a CommonJS bundle leaves undefined); the library files leave the SDK external, so a consumer that bundles them into CommonJS output of its own has to supply the same stand-in |
| Pool ranking | `rankPool`, `planOf`, `collectPoolPick`, `poolIdentities`, `PoolGraphError`, `PoolPickReportSchema`, `PROMPT_CACHE_TTL_MS`, with the types `RankPoolInput`, `PoolMember`, `NestedContribution`, `PoolRanking`, `Candidate`, `StickyPick`, `PlanClass`, `PoolPickReport` | Choose an identity from a pool the way `agent-shim pool pick` and a pool launch do. `rankPool` is pure over its input: each member's usage snapshot (`UsageSnapshotSchema`), account metadata and recent log records, the clock, and the directory's last pick; a member may instead stand for a nested `pool:<name>` entry, contributing that pool's pick (with the composition in its reasons) or its refusal; it returns every member in pick order with its class, score and reasons. `collectPoolPick` reads this machine and returns the report `pool pick --json` prints, validated by `PoolPickReportSchema` (also published as `schema/PoolPickReport.schema.json`). It reads the unified five-hour and seven-day windows only `poolIdentities` lists the concrete identities a pool can launch as, nested pools expanded in place, without reading any usage. It throws `PoolGraphError` for a pool, or a pool it nests, that is not defined and for a cycle between pools. |
| Conditions | `evaluateWhen`, `matchBranch`, `ConditionContext`, `WhenEvaluation` | Evaluate a rule's or entry's `when` (branch, environment and, when the context carries an entry's facts, its age and size) with the cascade's own semantics, for example over rules from `listDirectoryRules`. A `when` is either the object form or a trilean predicate tree over the same facts, and the evaluation is three-valued: definite, or indeterminate naming what was missing. The context also carries the pool facts a member entry's policy condition reads (`quota.*`, `session.*`) and the request facts a provider route's condition reads (`request.*`, `provider.<name>.*`), so one evaluator serves the cascade, the pool ranking and request routing |
Expand Down
4 changes: 3 additions & 1 deletion src/agents/commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, it } from "vitest";

import { createFakeFarmFs } from "../test-helpers";
import { collectSessionList, formatSessionList, formatSessionTree, locateSession, SessionLookupError, type SessionCommandFs } from "./commands";
import { formatSessionList, formatSessionTree } from "./commands";
import { readSessionAgentTree } from "./read";
import type { SessionCommandFs } from "./realFs";
import { collectSessionList, locateSession, SessionLookupError } from "./sessions";

const IDENTITIES = "/state/identities";
const WORK = `${IDENTITIES}/work`;
Expand Down
Loading
Loading