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
17 changes: 17 additions & 0 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,23 @@ public permission chain. This is the V1 permission ceiling; the portable proof
that every provider exposes no ambient tool is tracked separately and does not
widen it. `xmd run` keeps its caller-selected Agent permission behavior.

What a turn was allowed to do is retained beside what it said, and the two are
recorded by different things. A permission policy decides every request inside
its scope without deferring outward, so nothing installed inside one — and
nothing installed inside the turn, below a policy a host put around the execution
— can see the decision it makes. The Agent middleware installed with the
Agent itself, outside every policy installed after it, owns both sides of this.
Each `Agent.prompt()` call is given a private ledger and the cold stream it
returns is wrapped, so every event that call produces is read with that ledger
in place and a request raised while it is read inherits that ledger and no
other. A component installs nothing to be audited. The observer reserves a
place, delegates once, and copies the outcome that comes back into that place, so
concurrent turns keep their own audits and two overlapping requests keep the
order they were asked in. A place nobody completed — a policy that raised, a turn
torn down while a request waited — is published as nothing rather than as a
denial somebody invented. The ledger names a destination and confers no authority
to answer anything.

The only thing a workflow Agent receives is the rendered content of an authored
`<Prompt>`. It proposes an information request, or a change, by returning XMD. The
trusted workflow host passes that exact response to the constrained
Expand Down
1 change: 1 addition & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"exports": "./src/deno.ts",
"imports": {
"@bomb.sh/tty": "npm:@bomb.sh/tty@0.9.0",
"@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.8.3",
"@standard-schema/spec": "npm:@standard-schema/spec@^1.0.0",
"zod": "npm:zod@^4.3.6"
}
Expand Down
33 changes: 25 additions & 8 deletions packages/cli/src/agent-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,20 @@ export function hostAcpDependencies(stack: PlanWriterStack): AcpxProviderDepende
}

/**
* Install the agent stack a document runs under: the registration, the
* components with the resolved root provider, the permission mode, and the
* terminal this command has to give away.
* The half of the stack that answers *who* an agent is and *how* a document
* reaches one: the registered provider, and the components over it.
*
* Separated because two commands need exactly this much and disagree about the
* rest. `xmd run` owns a terminal and a readline, so it adds the permission
* mode and the foreground launcher below. The REPL owns neither — it presents
* requests in its own surface and has no terminal to give away — so it installs
* this and its own private policy instead of inheriting one that would ask
* through a readline nobody is looking at.
*
* Nothing starts an agent — the provider validates availability on first use,
* and an embedded adapter reaches the disk at that same point. A document that
* asks for no agent installs no adapter.
* Nothing here starts an agent. The provider validates availability on first
* use, and an embedded adapter reaches the disk at that same point.
*/
export function* installRunAgentStack(stack: AgentStack): Operation<void> {
export function* installAgentProviderStack(stack: AgentStack): Operation<void> {
const acpx = createAcpxProvider(hostAcpDependencies(stack));
yield* registerAgentProvider("acpx", acpx);

Expand All @@ -174,7 +179,19 @@ export function* installRunAgentStack(stack: AgentStack): Operation<void> {
permissionMode,
rootProvider: { factory, options: { defaultAgent, permissionMode } },
});
yield* installPermissionMode(permissionMode);
}

/**
* Install the agent stack a document runs under: the registration, the
* components with the resolved root provider, the permission mode, and the
* terminal this command has to give away.
*
* A document that asks for no agent still installs no adapter, for the reason
* above: nothing here starts one.
*/
export function* installRunAgentStack(stack: AgentStack): Operation<void> {
yield* installAgentProviderStack(stack);
yield* installPermissionMode(stack.permissionMode);
// `xmd run` is the one command that has a terminal to give away. Help,
// document inspection and `xmd test` install no launcher, so a document that
// reaches <Session.Launch> under any of them refuses instead of spawning.
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/src/repl/admission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* What admits a provisional session: work that went beyond the retained prefix.
*
* A session reconstructing from history is provisional until something proves
* the run reached new work — a queued Agent turn, a question, a fresh record.
* The announcement that proves it is sent on a `Signal`, and a Signal delivers
* only to subscriptions that are already active: whatever it sends before one
* exists is dropped, not buffered.
*
* `spawn()` returns before its child body has run, so a consumer that
* subscribes as its first act subscribes a turn too late. The subscription
* therefore belongs to the caller, which creates it before starting the work
* that can announce; this consumer only iterates what it was handed. Values
* sent after `yield* stream` returns are queued for that active subscription
* even while the consumer has not begun reading, which is exactly the window
* this closes.
*/

import type { Operation, Subscription } from "effection";

/**
* Drain an already-active subscription, admitting once per value it carries.
*
* Generic on purpose: what counts as admitting work is decided by filtering
* the stream before it is subscribed, which is the caller's business and
* happens in the caller's scope. This only reads what it was handed.
*
* Takes the subscription rather than the stream, so there is no way to write
* the consumer that creates its own: the race this exists to prevent cannot be
* reintroduced without changing the signature.
*/
export function consumeAdmissions<T>(
subscription: Subscription<T, never>,
admit: () => void,
): () => Operation<void> {
return function* (): Operation<void> {
let next = yield* subscription.next();
while (!next.done) {
admit();
next = yield* subscription.next();
}
};
}
Loading
Loading