toolroll 0.9.63 → 0.9.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +15 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +50 -2
- package/dist/agent-fence.js +157 -10
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +36 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +12 -4
- package/dist/contracts/console-api.js +12 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +217 -21
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +275 -17
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +170 -5
- package/dist/scope.js +251 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +247 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +4 -1
- package/dist/server/render-pages.js +5 -4
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +110 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +46 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +121 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +320 -74
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +430 -15
- package/dist/store.js +1041 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +2 -0
- package/dist/tool-broker.js +1 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/package.json +11 -2
package/dist/invoke.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { adapterFor, auditOf, claudePluginEnv, isolationUnsupported, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
20
20
|
import { readProviderKey, readAuthModeStrict, PROVIDER_KEY_ENV, OWN_KEY_ENV } from "./keys.js";
|
|
21
|
+
import { ACP_AGENTS, isAcpProvider, isKeyedProvider } from "./acp-agents.js";
|
|
22
|
+
import { launchFromJson, verifyLaunch } from "./acp-attest.js";
|
|
21
23
|
import { classifyTerminal } from "./exhaustion.js";
|
|
22
24
|
import { liftAuthPause } from "./provider-auth.js";
|
|
23
25
|
import { attestProvider } from "./attest.js";
|
|
@@ -27,11 +29,14 @@ import { witnessedRunner } from "./process-custody.js";
|
|
|
27
29
|
import { underStopWatch } from "./task-control.js";
|
|
28
30
|
import { childDatabaseEnv, isolatedChildDatabase, removeChildDatabase as removeAgentDatabase } from "./child-database.js";
|
|
29
31
|
import { RunSignInError, noToolsArgs, prepareRunTools } from "./project-tools.js";
|
|
30
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
32
|
+
import { mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
31
33
|
import { homedir } from "node:os";
|
|
32
34
|
import { dirname, join } from "node:path";
|
|
33
35
|
import { poolRootOf } from "./worktree.js";
|
|
36
|
+
import { BRIDGE_DIR } from "./agent-fence.js";
|
|
34
37
|
import { agentFence, CheckoutBindingError, trustedWorktreeBinding, checkoutGitEnv, HARDENED_GIT_ARGS, codexFenceArgv, fenceGrants, fencedGitEnv, linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced, NO_GRANTS, runFolders, taskBoundary } from "./agent-fence.js";
|
|
38
|
+
import { proxyEnv, providerOnlyNetwork, startEgressProxy } from "./egress.js";
|
|
39
|
+
import { bridgeFits, startEgressBridge } from "./egress-bridge.js";
|
|
35
40
|
import { claudeConfigDir, claudeDefaults, codexDefaults, codexHomeDir, isolatedCodexHome } from "./provider-home.js";
|
|
36
41
|
import { billingOf, claudeBillingFrom, seenBilling } from "./spend.js";
|
|
37
42
|
/** The claude binary name — kept for the legacy quota rows and tests that
|
|
@@ -100,13 +105,6 @@ function transportSessionId(id) {
|
|
|
100
105
|
const normalized = id.trim();
|
|
101
106
|
return normalized === "" ? null : normalized;
|
|
102
107
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Spend money, on the record. Throws — never refuses quietly — when the run
|
|
105
|
-
* is missing, already finished, or recorded against a different provider:
|
|
106
|
-
* a caller that reaches for a harness without a matching open run is a
|
|
107
|
-
* bug, not a case. Attestation and protocol refusals are VALUES (above),
|
|
108
|
-
* not throws — those are cases, and they must dispose cleanly.
|
|
109
|
-
*/
|
|
110
108
|
export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
111
109
|
const clock = options.clock ?? (() => new Date());
|
|
112
110
|
if (options.accumulateUsage && (invocation.phase !== "review" || invocation.resumeSession === null)) {
|
|
@@ -175,7 +173,8 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
175
173
|
// Tier-2 attestation (A2/B3): the authoritative check, immediately
|
|
176
174
|
// before the spawn it authorizes. Tier-1 providers return null here and
|
|
177
175
|
// keep their road byte-identical.
|
|
178
|
-
|
|
176
|
+
// An ACP agent is admitted by its own record (acp-attest.ts), read below; the version ranges are for the others.
|
|
177
|
+
const attested = isAcpProvider(spec.provider) ? null : await attestProvider(spec.provider, adapter.binary, options.versionProbe);
|
|
179
178
|
if (attested !== null && !attested.ok) {
|
|
180
179
|
if (attested.version !== null)
|
|
181
180
|
store.stampProviderVersion(runId, attested.version);
|
|
@@ -186,13 +185,61 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
186
185
|
diagnostic: attested.problem,
|
|
187
186
|
};
|
|
188
187
|
}
|
|
188
|
+
// AN ACP AGENT'S ADMISSION (docs/plans/acp-adapter.md § Admission): a passed conformance check of the exact binary
|
|
189
|
+
// on this runner, or nothing runs — absence is denial. It builds and repairs only, on a model it offered when it was
|
|
190
|
+
// checked, and no dollar cap rides it (it can't hold one; the scheduler refuses capped work first, this is the belt).
|
|
191
|
+
// An ACP spawn is admitted by its agent's attestation on this runner — or, for the conformance check alone, by the
|
|
192
|
+
// check request that will create it (acpCheck: set only by the check job). The two never stand in for each other.
|
|
193
|
+
const acpCheckAsk = options.acpCheck ?? null;
|
|
194
|
+
const acpAttestation = isAcpProvider(spec.provider) && acpCheckAsk === null ? store.acpAttestationFor(spec.provider, run.runner) : null;
|
|
195
|
+
const acpCheckRequest = isAcpProvider(spec.provider) && acpCheckAsk !== null ? store.acpCheckAdmitting(acpCheckAsk.requestId, acpCheckAsk.step, runId, clock()) : null;
|
|
196
|
+
const acpCheckLaunch = acpCheckRequest?.launchJson == null ? null : launchFromJson(acpCheckRequest.launchJson);
|
|
197
|
+
/** The plane's own isolation reading for a check's first step (below, before spawn); null when none was asked. */
|
|
198
|
+
let acpIsolation = null;
|
|
199
|
+
/** What starts the agent, and who the handshake must say it is (unknown, for a check: the check learns it). */
|
|
200
|
+
const acpLaunch = acpAttestation !== null ? acpAttestation.launch : acpCheckLaunch;
|
|
201
|
+
if (!isAcpProvider(spec.provider) && acpCheckAsk !== null)
|
|
202
|
+
throw new Error("only an ACP agent is checked");
|
|
203
|
+
if (isAcpProvider(spec.provider)) {
|
|
204
|
+
const name = ACP_AGENTS[spec.provider].name;
|
|
205
|
+
const refuse = (diagnostic) => ({ kind: "refused", reason: "provider-unattested", providerVersion: acpAttestation?.agent.version ?? null, diagnostic });
|
|
206
|
+
if (acpCheckAsk !== null) {
|
|
207
|
+
if (acpCheckRequest === null || acpCheckLaunch === null)
|
|
208
|
+
return refuse(`this check isn't running, has expired, lost its watch, or isn't this step's run`);
|
|
209
|
+
if (acpCheckRequest.provider !== spec.provider || acpCheckRequest.runner !== run.runner || acpCheckRequest.model !== spec.model || acpCheckRequest.taskRef !== run.taskRef) {
|
|
210
|
+
return refuse("this run isn't the check's: another agent, runner, model or task");
|
|
211
|
+
}
|
|
212
|
+
if (invocation.phase !== "build")
|
|
213
|
+
return refuse(`${name}'s check runs as a build`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
if (acpAttestation === null)
|
|
217
|
+
return refuse(`${name} isn't checked on ${run.runner} — run \`toolroll agents check ${spec.provider}\` there`);
|
|
218
|
+
if (invocation.phase !== "build" && invocation.phase !== "repair")
|
|
219
|
+
return refuse(`${name} builds and repairs only`);
|
|
220
|
+
if (spec.model === null || !acpAttestation.models.includes(spec.model))
|
|
221
|
+
return refuse(`${name} didn't offer ${spec.model ?? "a model"} when it was checked — check it again, or choose one it offered`);
|
|
222
|
+
}
|
|
223
|
+
if (invocation.maxBudgetUsd !== undefined)
|
|
224
|
+
return refuse(`${name} can't hold a dollar cap, so it doesn't run capped work (budget-unenforceable)`);
|
|
225
|
+
// The billing the run was admitted under must be for the model about to be chosen.
|
|
226
|
+
const stamp = store.acpBillingOfRun(runId);
|
|
227
|
+
if (stamp !== null && stamp.model !== spec.model)
|
|
228
|
+
return refuse(`this run was admitted to bill ${stamp.model}, not ${spec.model}`);
|
|
229
|
+
// And the money moment holds it to the hard monthly budgets as stamped — immutable since admission, so a
|
|
230
|
+
// declaration changed between a gate and this spawn changes nothing: metered work a budget holds never starts.
|
|
231
|
+
const billed = stamp?.kind === "plan" ? "subscription" : "api-key";
|
|
232
|
+
const held = store.budgetGate(clock())({ ...store.budgetSubject(run.taskRef), agents: [{ provider: spec.provider, billing: billed }] });
|
|
233
|
+
if (held.over !== null)
|
|
234
|
+
return refuse(`a monthly budget holds metered work, and ${name} on ${spec.model ?? "this model"} is metered here (budget)`);
|
|
235
|
+
}
|
|
189
236
|
// Auth mode decides how the credential reaches the process (the operator
|
|
190
237
|
// keeps BOTH and prefers the subscription): in "api-key" mode the
|
|
191
238
|
// managed file wins, the ambient env is a real fallback (explicit
|
|
192
239
|
// re-supply survives resolveChildEnv's chat-key strip); in
|
|
193
240
|
// "subscription" mode the key is STRIPPED from the child so the CLI's
|
|
194
241
|
// own login is used — its key is retained, just not handed over.
|
|
195
|
-
const { runner, clock: _clock, versionProbe: _probe, keyHome, timeoutMs: _hardTimeout, idleTimeoutMs: _idleTimeout, stdin: _callerStdin, accumulateUsage: _accumulateUsage, liveInput: _liveInput, ...runOptions } = options;
|
|
242
|
+
const { runner, clock: _clock, versionProbe: _probe, keyHome, timeoutMs: _hardTimeout, idleTimeoutMs: _idleTimeout, stdin: _callerStdin, accumulateUsage: _accumulateUsage, liveInput: _liveInput, acpCheck: _acpCheck, network: launchNetwork, ...runOptions } = options;
|
|
196
243
|
// The live setting is read STRICTLY (atomic authority closure): a
|
|
197
244
|
// present mode file that says neither word is a value-shaped refusal
|
|
198
245
|
// before any stamp or process — never the subscription default a
|
|
@@ -213,9 +260,12 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
213
260
|
if (problem !== null)
|
|
214
261
|
return { kind: "refused", reason: "auth-mode", providerVersion: attested?.version ?? null, diagnostic: problem };
|
|
215
262
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
263
|
+
// An ACP agent has no key Toolroll holds or injects: its own and everyone else's are shed (adapter.extraOmitEnv).
|
|
264
|
+
const keyed = isKeyedProvider(spec.provider) ? spec.provider : null;
|
|
265
|
+
const ownKeyEnv = keyed === null ? [] : OWN_KEY_ENV[keyed];
|
|
266
|
+
const keyEnvName = keyed === null ? null : PROVIDER_KEY_ENV[keyed];
|
|
267
|
+
const managedKey = authMode === "api-key" && keyed !== null && keyEnvName !== null
|
|
268
|
+
? readProviderKey(keyed, keyHome) ?? (process.env[keyEnvName] || null)
|
|
219
269
|
: null;
|
|
220
270
|
// Resume XOR mint, enforced at the GATEWAY (Codex gemini verify round 2,
|
|
221
271
|
// finding 3): a resume and a minted start id are mutually exclusive —
|
|
@@ -283,7 +333,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
283
333
|
// tool servers outside its command sandbox, so its own launch folder is
|
|
284
334
|
// fenced too; Claude reads its config from there, inside the sandbox.
|
|
285
335
|
const ownTools = tools?.privateDir === undefined ? null : realpathSync(tools.privateDir);
|
|
286
|
-
const fenceOf = invocation.phase === "review" ? { fence: [], fileTools: [], grants: NO_GRANTS, problem: undefined } : runFence(store, runId, spec.provider, options.keyHome, codexHarness ? null : ownTools);
|
|
336
|
+
const fenceOf = invocation.phase === "review" ? { fence: [], fileTools: [], grants: NO_GRANTS, gitPaths: [], problem: undefined } : runFence(store, runId, spec.provider, options.keyHome, codexHarness ? null : ownTools);
|
|
287
337
|
if (fenceOf.problem !== undefined) {
|
|
288
338
|
tools?.cleanup();
|
|
289
339
|
codexHome?.cleanup();
|
|
@@ -291,11 +341,26 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
291
341
|
return { kind: "refused", reason: "containment", providerVersion: attested?.version ?? null, diagnostic: fenceOf.problem };
|
|
292
342
|
}
|
|
293
343
|
const fence = [...fenceOf.fence, ...(fenceOf.fence.length > 0 && codexHarness ? [ownTools, codexHome === null ? null : realpathSync(codexHome.dir)].filter((path) => path !== null) : [])];
|
|
294
|
-
const grants = fenceOf.grants;
|
|
344
|
+
const grants = { ...fenceOf.grants };
|
|
345
|
+
// The run's network (egress.ts): sealed by its approval, or the provider's hosts alone, or the caller's own proxy.
|
|
346
|
+
const network = launchNetwork ?? { policy: providerOnlyNetwork(spec.provider) };
|
|
347
|
+
// Codex's command sandbox has no proxy (its argv is fixed just below): its commands are online only for a sealed
|
|
348
|
+
// open network, and offline otherwise.
|
|
349
|
+
if (fence.length > 0 && codexHarness) {
|
|
350
|
+
grants.network = "policy" in network && network.policy.mode === "open" ? { mode: "open" }
|
|
351
|
+
: { mode: "allowlist", proxyPort: 0, brokerPorts: [], loopback: "proxy-only", socketRoots: [] };
|
|
352
|
+
}
|
|
295
353
|
const inherited = spec.model === null && invocation.phase !== "review" ? inheritedDefaults(spec.provider, { ...process.env, ...(runOptions.env ?? {}) }, keyHome ?? homedir()) : undefined;
|
|
296
354
|
const buildInvocation = { ...resolvedInvocation, ...(tools === null ? {} : { toolArgv: tools.argv, fence, fenceGrants: grants, fileToolFence: fenceOf.fileTools }), ...(workspace === null ? {} : { workspace }), ...(inherited === undefined ? {} : { inherited }) };
|
|
297
355
|
const launchArgv = tools === null ? argv : adapter.argv(buildInvocation);
|
|
298
356
|
const fenced = fenceLaunch(spec.provider, fence);
|
|
357
|
+
// An ACP agent runs only inside the OS fence: nothing else holds it (it may run commands without asking).
|
|
358
|
+
if (isAcpProvider(spec.provider) && fenced.method !== "macos-sandbox" && fenced.method !== "linux-bubblewrap") {
|
|
359
|
+
tools?.cleanup();
|
|
360
|
+
codexHome?.cleanup();
|
|
361
|
+
removeAgentDatabase(isolatedDb.dir);
|
|
362
|
+
return { kind: "refused", reason: "containment", providerVersion: acpAttestation?.agent.version ?? null, diagnostic: `${ACP_AGENTS[spec.provider].name} runs only inside the OS fence (the macOS sandbox or bubblewrap), and this launch has neither (fence-unavailable)` };
|
|
363
|
+
}
|
|
299
364
|
// The tools were chosen for the fence decided above: a launch that got any other starts nothing.
|
|
300
365
|
// Only a prepared project-tool broker depends on the earlier decision. Empty Claude configs also have a privateDir.
|
|
301
366
|
if (tools?.serverPids !== undefined && fenced.method !== containment.fence) {
|
|
@@ -307,10 +372,69 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
307
372
|
// A tool server the broker starts is fenced by what this platform supports, whatever the provider: Codex's own
|
|
308
373
|
// profile fences only its agent's commands, and the broker starts servers outside it.
|
|
309
374
|
if (fence.length > 0) {
|
|
375
|
+
// A tool server keeps its own reach (it holds its tool's secrets, not the agent's code): never the agent's network.
|
|
376
|
+
const serverGrants = () => { const { network: _agentOnly, ...rest } = grants; return rest; };
|
|
310
377
|
if (macosFenceAvailable())
|
|
311
|
-
serverFence = (file, args) => macosFenced(file, args, fence,
|
|
378
|
+
serverFence = (file, args) => macosFenced(file, args, fence, serverGrants());
|
|
312
379
|
else if (linuxFenceAvailable())
|
|
313
|
-
serverFence = (file, args) => linuxFenced(file, args, fence,
|
|
380
|
+
serverFence = (file, args) => linuxFenced(file, args, fence, serverGrants());
|
|
381
|
+
}
|
|
382
|
+
// No silent fallback (arc 1): a build this machine can't confine at the OS — Claude reduced to its own file-tool
|
|
383
|
+
// rules, Gemini to nothing — doesn't start unless the instance operator allowed unconfined agents here. An injected
|
|
384
|
+
// runner (tests) starts no real agent.
|
|
385
|
+
const unconfined = fence.length > 0 && (fenced.method === "claude-rules" || fenced.method === "none");
|
|
386
|
+
if (unconfined && runner === undefined && !store.unfencedAgentsAllowed()) {
|
|
387
|
+
tools?.cleanup();
|
|
388
|
+
codexHome?.cleanup();
|
|
389
|
+
removeAgentDatabase(isolatedDb.dir);
|
|
390
|
+
return { kind: "refused", reason: "containment", providerVersion: attested?.version ?? null,
|
|
391
|
+
diagnostic: "This machine can't confine an agent (no macOS sandbox, no working bubblewrap), so nothing was started. Install bubblewrap, or an instance operator allows unconfined agents with `toolroll config unconfined-agents allow`." };
|
|
392
|
+
}
|
|
393
|
+
let egress = null;
|
|
394
|
+
let bridge = null;
|
|
395
|
+
let runTemp = null;
|
|
396
|
+
const confinedOs = fenced.method === "macos-sandbox" || fenced.method === "linux-bubblewrap";
|
|
397
|
+
if (fence.length > 0 && !codexHarness && confinedOs) {
|
|
398
|
+
const brokerPorts = tools?.brokerPort === undefined ? [] : [tools.brokerPort];
|
|
399
|
+
// Unix sockets only under this run's own folders: its checkout, and a temp folder of its own inside its private
|
|
400
|
+
// launch folder (the system temp folder holds other programs' sockets — editors, agents, password managers).
|
|
401
|
+
if (ownTools !== null) {
|
|
402
|
+
runTemp = join(ownTools, "tmp");
|
|
403
|
+
mkdirSync(runTemp, { recursive: true, mode: 0o700 });
|
|
404
|
+
}
|
|
405
|
+
const socketRoots = [...(runTemp === null ? [] : [runTemp]), ...(workspace === null ? [] : [realpathSync(workspace)])];
|
|
406
|
+
try {
|
|
407
|
+
if ("via" in network) {
|
|
408
|
+
const [proxyPort, ...more] = network.via;
|
|
409
|
+
grants.network = { mode: "allowlist", proxyPort: proxyPort ?? 0, brokerPorts: [...more, ...brokerPorts], loopback: "proxy-only", socketRoots };
|
|
410
|
+
}
|
|
411
|
+
else if (network.policy.mode === "open") {
|
|
412
|
+
grants.network = { mode: "open" };
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
egress = await startEgressProxy({ hosts: network.policy.hosts });
|
|
416
|
+
grants.network = { mode: "allowlist", proxyPort: egress.port, brokerPorts, loopback: network.policy.loopback, socketRoots };
|
|
417
|
+
}
|
|
418
|
+
if (fenced.method === "linux-bubblewrap" && grants.network.mode === "allowlist") {
|
|
419
|
+
const database = store.databaseFile();
|
|
420
|
+
// The plane's own fenced folder, unless a socket there would be too long a path (a unix socket's path is capped
|
|
421
|
+
// near 100 bytes): then the system temp folder, which a confined Linux build doesn't see (its /tmp is its own).
|
|
422
|
+
const fenced = database === null || database === "" || database === ":memory:" ? undefined : join(dirname(realpathSync(database)), BRIDGE_DIR);
|
|
423
|
+
bridge = await startEgressBridge([grants.network.proxyPort, ...grants.network.brokerPorts], fenced !== undefined && bridgeFits(fenced) ? fenced : undefined);
|
|
424
|
+
grants.network.bridge = { folder: realpathSync(bridge.folder), forwarder: realpathSync(bridge.forwarder), node: process.execPath };
|
|
425
|
+
// Its private /tmp gives back only its own folders: its checkout and its launch folder (its temp is inside).
|
|
426
|
+
grants.network.keep = [...(workspace === null ? [] : [realpathSync(workspace)]), ...(ownTools === null ? [] : [ownTools]), ...(fenceOf.gitPaths ?? [])];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
await egress?.close();
|
|
431
|
+
await bridge?.close();
|
|
432
|
+
tools?.cleanup();
|
|
433
|
+
codexHome?.cleanup();
|
|
434
|
+
removeAgentDatabase(isolatedDb.dir);
|
|
435
|
+
return { kind: "refused", reason: "containment", providerVersion: attested?.version ?? null,
|
|
436
|
+
diagnostic: `The build's network could not be set up, so nothing was started: ${error instanceof Error ? error.message : String(error)}` };
|
|
437
|
+
}
|
|
314
438
|
}
|
|
315
439
|
if (invocation.phase !== "review")
|
|
316
440
|
store.recordRunFence(runId, { method: fenced.method, paths: fence.length }, clock());
|
|
@@ -327,7 +451,12 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
327
451
|
// A fenced build's git never expires a reflog, so gc never rewrites another task's (agent-fence.ts).
|
|
328
452
|
...(fence.length > 0 ? fencedGitEnv({ ...process.env, ...(runOptions.env ?? {}) }) : {}),
|
|
329
453
|
...(codexHome === null ? {} : { CODEX_HOME: codexHome.dir }),
|
|
330
|
-
...(managedKey === null ? {} : { [
|
|
454
|
+
...(managedKey === null || keyEnvName === null ? {} : { [keyEnvName]: managedKey }),
|
|
455
|
+
// The run's own proxy, in every spelling a program reads (egress.ts): after every other setting, so no caller's
|
|
456
|
+
// environment points the build around it. A program that ignores them is refused by the OS fence instead.
|
|
457
|
+
...(egress === null || !("policy" in network) ? {} : proxyEnv(egress, network.policy.loopback)),
|
|
458
|
+
// Its own temp folder, where its tools' sockets may live (the only one its fence admits besides its checkout).
|
|
459
|
+
...(runTemp === null ? {} : { TMPDIR: runTemp, TMP: runTemp, TEMP: runTemp }),
|
|
331
460
|
// This key is deliberately last: no caller may point an agent back at
|
|
332
461
|
// the live control database through a generic RunOptions override.
|
|
333
462
|
...childDatabaseEnv(isolatedDb.file),
|
|
@@ -344,7 +473,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
344
473
|
// must not override the ONE canonical key this mode injected.
|
|
345
474
|
...(authMode === "subscription"
|
|
346
475
|
? ownKeyEnv
|
|
347
|
-
: ownKeyEnv.filter(name => name !==
|
|
476
|
+
: ownKeyEnv.filter(name => name !== keyEnvName)),
|
|
348
477
|
];
|
|
349
478
|
const launchOptions = {
|
|
350
479
|
...(fenced.wrap.length === 0 ? {} : { fence: fenced.wrap, fenceGrants: grants }),
|
|
@@ -375,6 +504,8 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
375
504
|
},
|
|
376
505
|
...(stdin === undefined ? {} : { stdin }),
|
|
377
506
|
...(liveInput === null ? {} : { liveInput }),
|
|
507
|
+
// One ACP conversation: the brief, the sealed model, and who the check says the agent is.
|
|
508
|
+
...(acpLaunch === null || spec.model === null ? {} : { acp: { prompt: buildInvocation.brief, model: spec.model, expect: acpAttestation?.agent ?? null, ...(runOptions.acp?.onControl === undefined ? {} : { onControl: runOptions.acp.onControl }), ...(runOptions.acp?.handshakeMs === undefined ? {} : { handshakeMs: runOptions.acp.handshakeMs }) } }),
|
|
378
509
|
...(hardTimeoutMs === undefined ? {} : { timeoutMs: hardTimeoutMs }),
|
|
379
510
|
...(idleTimeoutMs === undefined ? {} : { idleTimeoutMs }),
|
|
380
511
|
...(defaultTimeoutMs === undefined ? {} : { timeoutMs: defaultTimeoutMs }),
|
|
@@ -426,6 +557,19 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
426
557
|
}
|
|
427
558
|
}
|
|
428
559
|
}
|
|
560
|
+
// THE CHECK'S ISOLATION, PROVEN BY THE PLANE (docs/plans/acp-adapter.md § Layer E): inside the exact fence and
|
|
561
|
+
// child environment the agent is about to get, a canary file in the fenced state must be unreadable and a canary
|
|
562
|
+
// key in the launching environment must be absent. No agent takes part, so no agent can fake it.
|
|
563
|
+
if (acpCheckAsk?.isolation !== undefined && launchOptions.fence !== undefined && workspace !== null) {
|
|
564
|
+
// processGroup: run() applies the fence only on the process-group road (a plain execFile carries none).
|
|
565
|
+
const probe = { cwd: workspace, env: childEnv, omitEnv: childOmitEnv, fence: launchOptions.fence, ...(launchOptions.fenceGrants === undefined ? {} : { fenceGrants: launchOptions.fenceGrants }), timeoutMs: 15_000, processGroup: true };
|
|
566
|
+
const read = await runCommand("/bin/cat", [acpCheckAsk.isolation.file], probe);
|
|
567
|
+
const env = await runCommand("/usr/bin/env", [], probe);
|
|
568
|
+
acpIsolation = {
|
|
569
|
+
fileDenied: read.code !== 0 && !read.stdout.includes(acpCheckAsk.isolation.fileCanary),
|
|
570
|
+
envClear: env.code === 0 && !env.stdout.includes(acpCheckAsk.isolation.envValue),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
429
573
|
// The stamp precedes the spawn, so a crash between the two leaves a run
|
|
430
574
|
// that claims spend which never happened — the honest direction. A spawn
|
|
431
575
|
// before the stamp would leave spend no record claims, which is the lie
|
|
@@ -487,8 +631,32 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
487
631
|
diagnostic: containment.refusal,
|
|
488
632
|
};
|
|
489
633
|
}
|
|
634
|
+
// The ACP launch re-proved HERE, after every await: each bound file is still the one that passed its check.
|
|
635
|
+
if (acpCheckAsk !== null && acpCheckRequest !== null && acpCheckLaunch !== null) {
|
|
636
|
+
// The check request is read again here, after every await: still running, unexpired, owned, this step's run.
|
|
637
|
+
const still = store.acpCheckAdmitting(acpCheckAsk.requestId, acpCheckAsk.step, runId, clock());
|
|
638
|
+
if (still === null || still.launchJson !== acpCheckRequest.launchJson) {
|
|
639
|
+
return { kind: "refused", reason: "provider-unattested", providerVersion: null, diagnostic: "the check stopped admitting this run while it was being set up — nothing was started" };
|
|
640
|
+
}
|
|
641
|
+
const verified = verifyLaunch(acpCheckLaunch);
|
|
642
|
+
if (!verified.ok)
|
|
643
|
+
return { kind: "refused", reason: "provider-unattested", providerVersion: null, diagnostic: verified.problem };
|
|
644
|
+
}
|
|
645
|
+
if (acpAttestation !== null) {
|
|
646
|
+
// The record is read again here, after every await: a check revoked or replaced during setup starts nothing.
|
|
647
|
+
const live = store.acpAttestationFor(spec.provider, run.runner);
|
|
648
|
+
if (live === null || live.id !== acpAttestation.id) {
|
|
649
|
+
return { kind: "refused", reason: "provider-unattested", providerVersion: acpAttestation.agent.version, diagnostic: `${ACP_AGENTS[spec.provider].name}'s check was revoked or replaced while this run was being set up — nothing was started` };
|
|
650
|
+
}
|
|
651
|
+
const verified = verifyLaunch(live.launch);
|
|
652
|
+
if (!verified.ok) {
|
|
653
|
+
return { kind: "refused", reason: "provider-unattested", providerVersion: acpAttestation.agent.version, diagnostic: verified.problem };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
490
656
|
if (attested !== null)
|
|
491
657
|
store.stampProviderStart(runId, clock(), attested.version);
|
|
658
|
+
else if (acpAttestation !== null)
|
|
659
|
+
store.stampProviderStart(runId, clock(), acpAttestation.agent.version);
|
|
492
660
|
else
|
|
493
661
|
store.stampProviderStart(runId, clock());
|
|
494
662
|
// The brief is queued only once the checkout has passed the fence check.
|
|
@@ -498,12 +666,26 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
498
666
|
// provider runs, and a stop kills THIS run's process group through the
|
|
499
667
|
// handle registered under its owner tag — the exact-attempt road,
|
|
500
668
|
// never the global sweep. Settlement then reads the same row.
|
|
501
|
-
|
|
669
|
+
// An ACP agent starts through its attested launch (the pinned interpreter for a script), never a PATH lookup.
|
|
670
|
+
result = await underStopWatch(store, runId, () => acpLaunch !== null
|
|
671
|
+
? spawn(acpLaunch.file, [...acpLaunch.prefix, ...launchArgv], launchOptions)
|
|
672
|
+
: spawn(attested !== null ? attested.executable : adapter.binary, launchArgv, launchOptions));
|
|
502
673
|
}
|
|
503
674
|
finally {
|
|
504
675
|
removeAgentDatabase(isolatedDb.dir);
|
|
505
676
|
tools?.cleanup();
|
|
506
677
|
codexHome?.cleanup();
|
|
678
|
+
// What the run's proxy saw (shown on its result): connections it made and refused. A connection the OS refused
|
|
679
|
+
// outright never reached it, and isn't counted.
|
|
680
|
+
if (invocation.phase !== "review" && grants.network !== undefined) {
|
|
681
|
+
store.recordRunNetwork(runId, {
|
|
682
|
+
mode: grants.network.mode, phase: "agent",
|
|
683
|
+
...("via" in network ? { via: true } : { hosts: network.policy.hosts.length, loopback: network.policy.loopback }),
|
|
684
|
+
...(egress === null ? {} : { counts: egress.counts() }),
|
|
685
|
+
}, clock());
|
|
686
|
+
}
|
|
687
|
+
await egress?.close();
|
|
688
|
+
await bridge?.close();
|
|
507
689
|
}
|
|
508
690
|
// v105: the run's billing, fixed from evidence before its usage is priced: a key it was given is a key; otherwise
|
|
509
691
|
// what the CLI said (Claude's stream) or was last seen doing (Codex's account), and the setting only when neither.
|
|
@@ -515,8 +697,11 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
515
697
|
if (spec.provider === "claude" && managedKey === null && authMode === "subscription" && billingSeen.answered && evidence !== null) {
|
|
516
698
|
store.recordProviderBilling("claude", evidence, new Date());
|
|
517
699
|
}
|
|
518
|
-
|
|
700
|
+
// An ACP run bills as its admission stamp says (store.acpBillingOfRun): its agent's own plan, or metered.
|
|
701
|
+
const acpBilling = isAcpProvider(spec.provider) ? (store.acpBillingOfRun(runId)?.kind === "plan" ? "subscription" : "api-key") : null;
|
|
702
|
+
store.fixRunBilling(runId, acpBilling ?? (authMode === "api-key" ? "api-key" : evidence ?? billingOf(spec.provider, store.handle)), new Date());
|
|
519
703
|
const envelope = adapter.parse(result.stdout);
|
|
704
|
+
store.stampAgentReportedUsage(runId, envelope.agentReportedUsage ?? null);
|
|
520
705
|
// An installed CLI too old for a build's own setup refuses its flags
|
|
521
706
|
// before it starts: nothing ran, and the run says what to update.
|
|
522
707
|
const unsupported = result.code !== 0 && envelope.initObserved !== true && invocation.phase !== "review" ? isolationUnsupported(spec.provider, result.stderr) : null;
|
|
@@ -562,6 +747,8 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
562
747
|
...(envelope.tokensOut === null ? {} : { tokensOut: envelope.tokensOut + (options.accumulateUsage ? run.tokensOut ?? 0 : 0) }),
|
|
563
748
|
...(costUsd === null ? {} : { costUsd: costUsd + (options.accumulateUsage ? run.costUsd ?? 0 : 0) }),
|
|
564
749
|
...(usageJson === null ? {} : { usageJson }),
|
|
750
|
+
// How many agent turns it took, where the provider reports it (docs/plans/run-efficiency.md); unknown stays NULL.
|
|
751
|
+
...(envelope.ending?.turns == null ? {} : { agentTurns: envelope.ending.turns + (options.accumulateUsage ? run.agentTurns ?? 0 : 0) }),
|
|
565
752
|
});
|
|
566
753
|
// How the attempt ended, classified HERE where the structural terminal
|
|
567
754
|
// still exists; a sign-in that no longer works pauses its provider.
|
|
@@ -589,6 +776,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
589
776
|
providerVersion: attested === null ? null : attested.version,
|
|
590
777
|
diagnostic: envelope.protocolError,
|
|
591
778
|
finalMessage: envelope.finalMessage,
|
|
779
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
592
780
|
};
|
|
593
781
|
}
|
|
594
782
|
// The minted-identity proof (A5): once the harness initialized, the id
|
|
@@ -606,6 +794,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
606
794
|
? "the harness initialized without announcing its session id"
|
|
607
795
|
: "the harness announced a session id different from the one it was started under",
|
|
608
796
|
finalMessage: envelope.finalMessage,
|
|
797
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
609
798
|
};
|
|
610
799
|
}
|
|
611
800
|
// An early transport announcement, a pre-existing resume identity, and
|
|
@@ -619,6 +808,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
619
808
|
providerVersion: attested === null ? null : attested.version,
|
|
620
809
|
diagnostic: sessionIdentityProblem,
|
|
621
810
|
finalMessage: envelope.finalMessage,
|
|
811
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
622
812
|
};
|
|
623
813
|
}
|
|
624
814
|
// A provider's explicit failure terminal always wins over its process exit
|
|
@@ -631,6 +821,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
631
821
|
providerVersion: attested === null ? null : attested.version,
|
|
632
822
|
diagnostic: "the provider reported a failed terminal despite exiting with code 0",
|
|
633
823
|
finalMessage: envelope.finalMessage,
|
|
824
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
634
825
|
};
|
|
635
826
|
}
|
|
636
827
|
// The terminal contract (A4): when the audit requires it, a zero exit is
|
|
@@ -650,6 +841,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
650
841
|
? "the harness exited 0 without its success terminal — the stream ended mid-protocol"
|
|
651
842
|
: "the harness exited 0 without ever initializing"),
|
|
652
843
|
finalMessage: envelope.finalMessage,
|
|
844
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
653
845
|
};
|
|
654
846
|
}
|
|
655
847
|
// A run that worked proves its provider's sign-in works: a sign-in pause
|
|
@@ -669,7 +861,9 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
669
861
|
notFound: result.notFound,
|
|
670
862
|
sessionId: envelope.sessionId,
|
|
671
863
|
finalMessage: envelope.finalMessage,
|
|
864
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
672
865
|
structuredOutput: envelope.structuredOutput ?? null,
|
|
866
|
+
...(envelope.acp === undefined ? {} : { acp: { ...envelope.acp, ...(acpIsolation === null ? {} : { isolation: acpIsolation }) } }),
|
|
673
867
|
ending: envelope.ending ?? null,
|
|
674
868
|
usage: {
|
|
675
869
|
tokensIn: envelope.tokensIn,
|
|
@@ -735,6 +929,8 @@ function runFence(store, runId, provider, keyHome, ownTools) {
|
|
|
735
929
|
// checkout was recorded in (a --pool elsewhere), so another project's worktree, present at launch, is out of reach. Native wrappers also cover later creations.
|
|
736
930
|
grants: fenceGrants({ databaseFile, worktree, boundary, runFolders: folders, linked: linkedFor(store, worktree), binding, ...home,
|
|
737
931
|
poolRoots }),
|
|
932
|
+
// The checkout's own Git data, which a private /tmp gives back beside the checkout (agent-fence.ts linuxFenced).
|
|
933
|
+
gitPaths: binding === null ? [] : [binding.gitdir, join(binding.main, ".git")],
|
|
738
934
|
};
|
|
739
935
|
}
|
|
740
936
|
catch (error) {
|
package/dist/keys.d.ts
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* being deliberate, wins).
|
|
15
15
|
*/
|
|
16
16
|
import type { ProviderId } from "./provider.js";
|
|
17
|
+
import { type KeyedProviderId } from "./acp-agents.js";
|
|
17
18
|
/** The ONE env name each provider's own harness reads its key from —
|
|
18
19
|
* where a managed key is INJECTED. */
|
|
19
|
-
export declare const PROVIDER_KEY_ENV: Record<
|
|
20
|
+
export declare const PROVIDER_KEY_ENV: Record<KeyedProviderId, string>;
|
|
20
21
|
/** EVERY env name a provider reads its OWN key from — what subscription
|
|
21
22
|
* mode STRIPS so an ambient key cannot override the login. gemini reads
|
|
22
23
|
* two; the rest read one. */
|
|
23
|
-
export declare const OWN_KEY_ENV: Record<
|
|
24
|
+
export declare const OWN_KEY_ENV: Record<KeyedProviderId, readonly string[]>;
|
|
24
25
|
export declare function keysDir(home?: string): string;
|
|
25
26
|
/**
|
|
26
27
|
* How a provider authenticates (the operator's ruling 2026-08-29: keep
|
|
@@ -28,15 +29,18 @@ export declare function keysDir(home?: string): string;
|
|
|
28
29
|
* — a Claude/ChatGPT plan, a Google cached login — and the API key is
|
|
29
30
|
* kept but NOT handed to the process, so its own auth wins. "api-key"
|
|
30
31
|
* hands the stored (or ambient) key to the process. openrouter has no
|
|
31
|
-
* login and is always api-key.
|
|
32
|
+
* login and is always api-key. An ACP agent (acp-agents.ts) has neither:
|
|
33
|
+
* it signs in its own way and Toolroll never holds a key for it —
|
|
34
|
+
* "agent-login", which says nothing about how it is billed (that is the
|
|
35
|
+
* agent's billing declaration, per model).
|
|
32
36
|
*/
|
|
33
|
-
export type AuthMode = "subscription" | "api-key";
|
|
37
|
+
export type AuthMode = "subscription" | "api-key" | "agent-login";
|
|
34
38
|
/** Providers that CAN authenticate without an API key (a subscription or
|
|
35
39
|
* cached login). openrouter cannot — it is api-key only. */
|
|
36
|
-
export declare const SUBSCRIPTION_CAPABLE: Record<
|
|
40
|
+
export declare const SUBSCRIPTION_CAPABLE: Record<KeyedProviderId, boolean>;
|
|
37
41
|
/** The default when nothing is set: subscription where it exists (prefer
|
|
38
42
|
* the plan the operator already pays for), api-key otherwise. */
|
|
39
|
-
export declare const DEFAULT_AUTH_MODE: Record<
|
|
43
|
+
export declare const DEFAULT_AUTH_MODE: Record<KeyedProviderId, AuthMode>;
|
|
40
44
|
export declare function readAuthMode(provider: ProviderId, home?: string): AuthMode;
|
|
41
45
|
/**
|
|
42
46
|
* The auth mode as an AUTHORITY reads it (raw authority repair): an absent
|
|
@@ -52,24 +56,24 @@ export declare function readAuthModeStrict(provider: ProviderId, home?: string):
|
|
|
52
56
|
ok: false;
|
|
53
57
|
problem: string;
|
|
54
58
|
};
|
|
55
|
-
export declare function setAuthMode(provider:
|
|
59
|
+
export declare function setAuthMode(provider: KeyedProviderId, mode: "subscription" | "api-key", home?: string): {
|
|
56
60
|
ok: true;
|
|
57
61
|
} | {
|
|
58
62
|
ok: false;
|
|
59
63
|
reason: "no-subscription";
|
|
60
64
|
};
|
|
61
|
-
export declare function keyFileFor(provider:
|
|
65
|
+
export declare function keyFileFor(provider: KeyedProviderId, home?: string): string;
|
|
62
66
|
/** Shape check only — never a validity claim. A key is printable ASCII
|
|
63
67
|
* without whitespace, bounded; anything else is a paste accident. */
|
|
64
68
|
export declare function plausibleKey(value: string): boolean;
|
|
65
|
-
export declare function saveProviderKey(provider:
|
|
69
|
+
export declare function saveProviderKey(provider: KeyedProviderId, value: string, home?: string): {
|
|
66
70
|
ok: true;
|
|
67
71
|
} | {
|
|
68
72
|
ok: false;
|
|
69
73
|
reason: "implausible";
|
|
70
74
|
};
|
|
71
|
-
export declare function readProviderKey(provider:
|
|
72
|
-
export declare function clearProviderKey(provider:
|
|
75
|
+
export declare function readProviderKey(provider: KeyedProviderId, home?: string): string | null;
|
|
76
|
+
export declare function clearProviderKey(provider: KeyedProviderId, home?: string): boolean;
|
|
73
77
|
/**
|
|
74
78
|
* A live credential check that spends NO tokens: each provider exposes a
|
|
75
79
|
* cheap authenticated GET (its model list, or a key-info route) whose only
|
|
@@ -93,12 +97,12 @@ export type KeyVerdict = {
|
|
|
93
97
|
reason: "unexpected";
|
|
94
98
|
status: number;
|
|
95
99
|
};
|
|
96
|
-
export declare function verifyProviderKey(provider:
|
|
100
|
+
export declare function verifyProviderKey(provider: KeyedProviderId, value: string, fetcher?: typeof fetch, timeoutMs?: number): Promise<KeyVerdict>;
|
|
97
101
|
/** Words for a verdict, safe to render on any surface (never the key). */
|
|
98
|
-
export declare function verdictWords(provider:
|
|
102
|
+
export declare function verdictWords(provider: KeyedProviderId, verdict: KeyVerdict): string;
|
|
99
103
|
/** What a surface may say about a key: that it exists and when it last
|
|
100
104
|
* changed — NEVER the value, never a prefix, never a length. */
|
|
101
|
-
export declare function keyStatus(provider:
|
|
105
|
+
export declare function keyStatus(provider: KeyedProviderId, home?: string): {
|
|
102
106
|
set: boolean;
|
|
103
107
|
updatedAt: string | null;
|
|
104
108
|
};
|
package/dist/keys.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
19
20
|
import { namedPath } from "./names.js";
|
|
20
21
|
/** The ONE env name each provider's own harness reads its key from —
|
|
21
22
|
* where a managed key is INJECTED. */
|
|
@@ -62,6 +63,8 @@ function authModeFileFor(provider, home) {
|
|
|
62
63
|
return join(keysDir(home), `${provider}.auth`);
|
|
63
64
|
}
|
|
64
65
|
export function readAuthMode(provider, home = homedir()) {
|
|
66
|
+
if (isAcpProvider(provider))
|
|
67
|
+
return "agent-login";
|
|
65
68
|
if (!SUBSCRIPTION_CAPABLE[provider])
|
|
66
69
|
return "api-key";
|
|
67
70
|
try {
|
|
@@ -80,6 +83,8 @@ export function readAuthMode(provider, home = homedir()) {
|
|
|
80
83
|
* no subscription login is the key, whatever the file says.
|
|
81
84
|
*/
|
|
82
85
|
export function readAuthModeStrict(provider, home = homedir()) {
|
|
86
|
+
if (isAcpProvider(provider))
|
|
87
|
+
return { ok: true, mode: "agent-login" };
|
|
83
88
|
if (!SUBSCRIPTION_CAPABLE[provider])
|
|
84
89
|
return { ok: true, mode: "api-key" };
|
|
85
90
|
let raw;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* takes the owner there. Everything here reads what is already recorded — runner readiness reports, sign-in pauses,
|
|
6
6
|
* plan-limit readings, tool tests, skill tests, integration checks — and never probes anything that spends.
|
|
7
7
|
*/
|
|
8
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
8
9
|
import { projectBatchChecks } from "./batch-policy.js";
|
|
9
10
|
import { resolveRouteCandidates } from "./agentconfig.js";
|
|
10
11
|
import { authPauseOf, messageCommand, providerName } from "./provider-auth.js";
|
|
@@ -60,7 +61,7 @@ function agentsOf(store, repo, now, authMode) {
|
|
|
60
61
|
// Signed out only on a real sign-in failure: a sign-in pause from a run, the worker's login check saying not logged
|
|
61
62
|
// in, or the key a key-only provider needs missing. A provider on an API key gets the key step, never the sign-in.
|
|
62
63
|
const pause = authPauseOf(store, pair.provider);
|
|
63
|
-
const mode = pause?.authMode ?? (SUBSCRIPTION_CAPABLE[pair.provider] ? authMode(pair.provider) : "api-key");
|
|
64
|
+
const mode = pause?.authMode ?? (isAcpProvider(pair.provider) ? "agent-login" : SUBSCRIPTION_CAPABLE[pair.provider] ? authMode(pair.provider) : "api-key");
|
|
64
65
|
const key = mode === "api-key";
|
|
65
66
|
const outWords = key ? "Needs a working API key" : "Signed out";
|
|
66
67
|
const outNext = (detail) => key ? `Save a working API key: run \`${messageCommand({ provider: pair.provider, authMode: "api-key" })}\` on this computer${detail}.` : `Sign in: run \`${messageCommand({ provider: pair.provider, authMode: "subscription" })}\` on this computer${detail}.`;
|
|
@@ -198,7 +199,7 @@ export function capabilitiesOf(store, input) {
|
|
|
198
199
|
* report on a task, attempt or check does (crew work must run), and a timed one does when it is reported on a chat app. */
|
|
199
200
|
const FLOW_WORK = new Set(["create", "edit", "kit", "starter", "add_trigger", "resume_trigger", "add_card"]);
|
|
200
201
|
export function needsCapabilities(name, args, channel) {
|
|
201
|
-
if (name === "propose_task")
|
|
202
|
+
if (name === "propose_task" || name === "propose_sweep")
|
|
202
203
|
return true;
|
|
203
204
|
if (name === "propose_flow")
|
|
204
205
|
return FLOW_WORK.has(String(args["operation"]));
|
package/dist/lead-contract.js
CHANGED
|
@@ -14,6 +14,7 @@ export const LEAD_CONTRACT = [
|
|
|
14
14
|
"Before a recommendation or any propose_* on a project with recorded decisions, call search_project_memory for it this turn, in an earlier step than the proposal (the plane refuses the proposal otherwise), and cite the decision ids you rely on.",
|
|
15
15
|
"Before promising work that depends on an agent, a worker, a tool, a skill or an integration, call get_capabilities this turn, in an earlier step than the promise (the plane refuses propose_task, a propose_flow that sets up or starts work, and commit_to otherwise). If what it needs can't work, say so plainly with its next step and open its link with show_control; never promise what it cannot do yet, and never pretend it works. Drafting a flow or confirmation the operator asked for is not a promise (see Flows).",
|
|
16
16
|
"Investigate before reporting a limitation: when a tool refuses or something looks unsupported, read get_capabilities and the relevant skill (get_skills, then that skill's instructions) before telling the owner it can't be done. Then say what you checked, what is missing, and the one next step.",
|
|
17
|
+
"You start each reply with the everyday tools. Flows, subagents, sweeps, agents and models, project tools and integrations, settings pages, holds and the queue, task actions (retry, stop, merge), images and evidence are found with find_tools: search by what you want to do before saying something can't be done, then call what it finds from your next step.",
|
|
17
18
|
"Ask rarely: only when the answer changes the work and you cannot settle it from tools or project memory. Then use ask_owner: one question, 2-4 short options (Something else is added), shown as buttons after your reply; the tapped option comes back as the owner's next message. Keep your reply to one short line leading into it, and do not repeat the options in your text.",
|
|
18
19
|
"propose_* drafts, never completes. Confirmation and execution approvals still apply. Never request credentials in chat. Read get_task currentExecution before acting; revisions retain task identity and old cards retain exact targets.",
|
|
19
20
|
"The operator talks plainly and names work by what it is about ('the login page', 'that payout fix'), rarely by id. Use the current task when there is one; otherwise find it with list_tasks search, and if several fit, ask which by title. When they want work moved forward, read get_task and draft its next step: changes to a result still waiting on them → propose_review revise; guidance for work in progress → propose_steer; stopped → resume and failed → retry with propose_task_action; waiting for approval, or 'approve X' → offer_approval for exactly that plan or flow card, which sends its own Approve buttons to the owner's chat (their tap approves it; you never do), and only when it says why not, say that in one line and show_control approval; new work, or more on something already completed → propose_task in the same project, naming the earlier task in its goal.",
|
|
@@ -24,6 +25,7 @@ export const LEAD_CONTRACT = [
|
|
|
24
25
|
"For a task's models/risk, read get_agents and answer in its words; for defaults, CLI versions and new models, read get_models; never an agent that is not listed. Changes require renewed approval. Before propose_answer read get_decision in an earlier step, including every option and consequence. Irreversible answers require explicit confirmation; cards retain consequences and recommendations.",
|
|
25
26
|
"For intake, a plain-language outcome is enough to draft a task. Infer title, narrow goal, safe non-goals and testable criteria; leave touches empty for discovery. Do not ask the operator for a title, paths, implementation details, acceptance wording, model, budget or safely inferable fields. Ask at most three questions with defaults (one at a time with ask_owner) for material ambiguity, conflicting goals or unresolved irreversible/public/security/data-loss/migration choices. With 'use your judgment', use reversible defaults; real blockers may still arise.",
|
|
26
27
|
"Set propose_task planning to 'required' for broad, risky or plan-first work, 'skip' for explicitly small direct builds, otherwise 'auto'. report:true investigates without a branch; follow-ups require confirmed proposals.",
|
|
28
|
+
"Several independent items under one rule set (a bug sweep, a batch of small fixes) are one propose_sweep: the shared rules go in brief, each item is only its own report, one approval starts them all and its table collects their verdicts. Not for a single task, and not for steps that depend on each other.",
|
|
27
29
|
"Tools are MCP servers a project's builds get, and builds get nothing else: read get_project_tools. A service in connectBySigningIn (Stripe, Notion, Linear, Sentry, Jira, Intercom, Attio and more) connects in one click: give the operator its connect link, where they sign in on the service's own page; never tool_add it and never ask for its key. To add another, prefer its commonTools entry (propose_action tool_add with catalog); otherwise use exactly the command or address the operator gives, never a package you are unsure exists. A tool reaches work approved after it was added. Never ask for, accept or repeat a secret's value: name it and open the Tools page with show_control tools.",
|
|
28
30
|
"Your subagents (get_subagents) are named helpers in a project, each with its own personality, instructions and rules (do on its own, ask first, never). When the operator says 'ask Rosa to …' or writes to one by name ('@rosa, …'), delegate with propose_subagent ask in their words; never do its work yourself or speak for it. A flow step can call one by name too.",
|
|
29
31
|
"Flows are a project's process: zones that cards move through, started by triggers and worked by people and your subagents. Read get_flows before answering about them and draft changes with propose_flow (subagents: get_subagents, propose_subagent). Those tools' descriptions say how flows, steps, triggers, cards, subagents, starter flows and kits work. When asked for a flow or a confirmation step, call propose_flow and draft it even if an integration it uses (a chat app, email) isn't set up yet; then say in one line what's missing for it to run and the next step. Say a draft or any tool call failed only when a tool result this turn said so, quoting its words.",
|
package/dist/lead-doors.d.ts
CHANGED
package/dist/lead-doors.js
CHANGED
|
@@ -169,6 +169,13 @@ export function confirmLeadProposal(store, who, proposalId, now, options) {
|
|
|
169
169
|
store.casLeadProposal(proposalId, "confirming", "pending", null, null, now);
|
|
170
170
|
return outcome;
|
|
171
171
|
}
|
|
172
|
+
// A first yes on protected work is kept, and the card stays pending for the second approver.
|
|
173
|
+
if (outcome.ok && outcome.awaiting === true) {
|
|
174
|
+
store.casLeadProposal(proposalId, "confirming", "pending", null, null, now);
|
|
175
|
+
if (shared)
|
|
176
|
+
store.handle.prepare('INSERT INTO team_event(lead,conversation,kind,actor,created_at) VALUES(?,?,?,?,?)').run(shared['lead'], shared['id'], 'proposal-changed', who.name, now.toISOString());
|
|
177
|
+
return outcome;
|
|
178
|
+
}
|
|
172
179
|
// The recorded outcome names the surface that confirmed — the audit a
|
|
173
180
|
// card shows on every other surface afterwards.
|
|
174
181
|
store.casLeadProposal(proposalId, "confirming", outcome.ok ? "confirmed" : "refused", who.name, { ...outcome, via: options.via }, now);
|
package/dist/lead-follow.d.ts
CHANGED
|
@@ -22,3 +22,5 @@ export type LeadFollowInput = {
|
|
|
22
22
|
fetcher?: typeof fetch;
|
|
23
23
|
};
|
|
24
24
|
export declare function runLeadFollowPass(input: LeadFollowInput): Promise<void>;
|
|
25
|
+
/** How long a burst of one sweep's updates is held so it lands on one page (docs/plans/lead-fan-out.md). */
|
|
26
|
+
export declare const SWEEP_SETTLE_MS: number;
|