toolroll 0.9.62 → 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 +80 -0
- package/README.md +66 -8
- 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 +203 -10
- package/dist/agent-fence.js +623 -41
- 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 +263 -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 +199 -17
- 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/check-levels.js +3 -0
- 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 +20 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +24 -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/cli.d.ts +8 -0
- package/dist/contracts/cli.js +3 -1
- package/dist/contracts/console-api.d.ts +30 -4
- package/dist/contracts/console-api.js +13 -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/integration-metadata.d.ts +2 -1
- package/dist/contracts/integration-metadata.js +2 -1
- 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/discord-chat.js +4 -1
- package/dist/dispatch.js +7 -4
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +427 -0
- 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/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +2 -2
- package/dist/exec.d.ts +81 -0
- package/dist/exec.js +368 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +4 -15
- package/dist/flow-engine.js +35 -11
- 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 +84 -11
- package/dist/flow-triggers.js +554 -87
- package/dist/flow-unattended-words.d.ts +28 -0
- package/dist/flow-unattended-words.js +50 -0
- package/dist/flow-unattended.d.ts +227 -0
- package/dist/flow-unattended.js +776 -0
- package/dist/flows-cli.d.ts +28 -1
- package/dist/flows-cli.js +63 -12
- package/dist/flows-ui.d.ts +2 -3
- package/dist/flows-ui.js +56 -5
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/google-mail.d.ts +1 -0
- package/dist/google-mail.js +61 -18
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +6 -3
- package/dist/integrations.d.ts +17 -3
- package/dist/integrations.js +38 -18
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +510 -182
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/landed-cleanup.d.ts +3 -2
- package/dist/landed-cleanup.js +12 -5
- 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 +163 -4
- 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-connect.d.ts +8 -7
- package/dist/mcp-connect.js +49 -51
- 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/oauth-run-token.d.ts +51 -0
- package/dist/oauth-run-token.js +127 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +282 -18
- 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/plan-auto.d.ts +11 -0
- package/dist/plan-auto.js +62 -11
- 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/process-recovery-eligibility.d.ts +1 -1
- package/dist/project-access.d.ts +3 -0
- package/dist/project-access.js +3 -0
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.d.ts +3 -1
- package/dist/project-delete.js +12 -6
- package/dist/project-file.d.ts +3 -0
- package/dist/project-file.js +16 -0
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +103 -27
- package/dist/project-tools.js +399 -100
- 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-home.d.ts +22 -0
- package/dist/provider-home.js +99 -0
- package/dist/provider.d.ts +42 -5
- package/dist/provider.js +97 -19
- 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/remote-main.d.ts +4 -0
- package/dist/remote-main.js +3 -3
- 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 +171 -6
- 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 +258 -43
- 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 +65 -13
- 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 +49 -5
- package/dist/server/runtime.d.ts +34 -3
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +132 -13
- 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-sweep-child.js +3 -1
- package/dist/storage-sweep-process.d.ts +12 -5
- package/dist/storage-sweep-process.js +23 -6
- 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 +466 -19
- package/dist/store.js +1185 -63
- package/dist/subagent-desk.js +1 -1
- package/dist/subagent-tools.d.ts +1 -1
- package/dist/summary.js +3 -0
- package/dist/surface.js +6 -4
- 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/teardown-git.d.ts +14 -0
- package/dist/teardown-git.js +119 -0
- 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 +39 -0
- package/dist/tool-broker.js +370 -0
- package/dist/tool-launcher.js +5 -4
- package/dist/tool-secret-store.d.ts +170 -0
- package/dist/tool-secret-store.js +834 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +14 -3
- package/dist/tools-ui.d.ts +7 -1
- package/dist/tools-ui.js +19 -10
- 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 +21 -5
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +10 -4
- package/dist/worktree.js +70 -29
- package/package.json +13 -2
- package/scripts/provider-canary.mjs +210 -31
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ACP agent's conformance check (docs/plans/acp-adapter.md § Layer E): what admits an agent to run on this runner.
|
|
3
|
+
*
|
|
4
|
+
* A person asks for it (`toolroll agents check`, with a password); a watch of the named runner runs it, under that
|
|
5
|
+
* watch's own lease and custody, as a real task of the watch's project with two real runs through the one gateway —
|
|
6
|
+
* the same fence, environment shedding, containment, stop watch and spend as any build. Each run is admitted by the
|
|
7
|
+
* check request (never by an attestation), and graded only on evidence the plane observed itself:
|
|
8
|
+
*
|
|
9
|
+
* 1. session-ok: the plane's own isolation probes inside the exact fence and environment the agent gets (a canary
|
|
10
|
+
* file in the fenced state unreadable, a canary key absent); a tool call the runner saw complete; the prompt's
|
|
11
|
+
* `end_turn`; the agent leaving on stdin's end with nothing of its group left; and done.txt saying ok.
|
|
12
|
+
* 2. session-stop: a live descendant of the agent, seen by the plane's own reading, before the cancel; the
|
|
13
|
+
* `cancelled` answer; and every process alive at the cancel gone after.
|
|
14
|
+
*
|
|
15
|
+
* Only when both passed is the attestation recorded — in the same transaction that marks the request passed, while
|
|
16
|
+
* it is unexpired and its watch still holds its lease. Anything else fails the request, in words, and admits nothing.
|
|
17
|
+
*/
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { ACP_AGENTS, isAcpProvider } from "./acp-agents.js";
|
|
22
|
+
import { ACP_CONFORMANCE_VERSION, launchOf } from "./acp-attest.js";
|
|
23
|
+
import { inspectAcpAgent } from "./acp-inspect.js";
|
|
24
|
+
import { acquire, acquireForCheck, heartbeat, release } from "./claim.js";
|
|
25
|
+
import { invokeAgent } from "./invoke.js";
|
|
26
|
+
import { BUILT_IN } from "./store.js";
|
|
27
|
+
export const CHECK_PROMPTS = {
|
|
28
|
+
ok: "Create a file named done.txt in the current folder containing exactly the word ok. Then stop. Do nothing else.",
|
|
29
|
+
stop: "Run the shell command `sleep 120` and wait for it to finish.",
|
|
30
|
+
};
|
|
31
|
+
/** How long the stop step waits for the agent to start its command, and for the cancel to be answered. */
|
|
32
|
+
const STOP_WAIT_MS = 60_000;
|
|
33
|
+
/** How long the agent has to answer the cancel. */
|
|
34
|
+
const CANCEL_ANSWER_MS = 15_000;
|
|
35
|
+
/** Close what no live watch holds any more, let go of what finished checks still hold, then run this runner's next
|
|
36
|
+
* check, if one is waiting. */
|
|
37
|
+
export async function runAcpChecks(job) {
|
|
38
|
+
const clock = job.clock ?? (() => new Date());
|
|
39
|
+
job.store.expireAcpChecks(clock());
|
|
40
|
+
for (const abandoned of job.store.abandonedAcpChecks(job.runner, clock()))
|
|
41
|
+
job.store.failAcpCheck(abandoned.id, "interrupted", clock());
|
|
42
|
+
await settleChecks(job, clock);
|
|
43
|
+
if (process.platform === "win32")
|
|
44
|
+
return;
|
|
45
|
+
const request = job.store.claimAcpCheck(job.runner, job.watch, clock());
|
|
46
|
+
if (request === null)
|
|
47
|
+
return;
|
|
48
|
+
const name = isAcpProvider(request.provider) ? ACP_AGENTS[request.provider].name : request.provider;
|
|
49
|
+
job.progress?.(`agents: checking ${name} (${request.model}) on ${job.runner}`);
|
|
50
|
+
try {
|
|
51
|
+
const verdict = await check(job, request, clock);
|
|
52
|
+
job.progress?.(`agents: ${name} ${verdict === null ? "passed its check" : `failed its check — ${verdict}`}`);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
job.store.failAcpCheck(request.id, `the check stopped: ${error instanceof Error ? error.message : String(error)}`, clock());
|
|
56
|
+
}
|
|
57
|
+
await settleChecks(job, clock);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Let go of what finished checks hold — their checkout, their task, their claim — once nothing of theirs can still be
|
|
61
|
+
* running. The whole teardown happens under a live claim of the check's own on its task (its first, or a fresh one
|
|
62
|
+
* recorded in its place when that expired), so no one else can take the task while its checkout goes; ownership is
|
|
63
|
+
* re-read before every step that destroys anything. A check is settled only when all of it is let go; anything that
|
|
64
|
+
* can't be yet (a run still open, a process not proved gone, a checkout that wouldn't discard, someone else holding
|
|
65
|
+
* the task) is retried on the next pass, and never touched.
|
|
66
|
+
*/
|
|
67
|
+
/** How long one cleanup attempt, and its claim on the task, last between renewals. */
|
|
68
|
+
const SETTLE_HOLD_MS = 10 * 60_000;
|
|
69
|
+
async function settleChecks(job, clock) {
|
|
70
|
+
const { store } = job;
|
|
71
|
+
for (const request of store.unsettledAcpChecks(job.runner)) {
|
|
72
|
+
// One attempt at a time per check, across every watch of the runner: another attempt in flight is left alone.
|
|
73
|
+
const attempt = randomBytes(8).toString("hex");
|
|
74
|
+
if (!store.holdAcpSettle(request.id, attempt, clock(), SETTLE_HOLD_MS))
|
|
75
|
+
continue;
|
|
76
|
+
const branch = `toolroll/agent-check-${request.id}`;
|
|
77
|
+
let held = null;
|
|
78
|
+
try {
|
|
79
|
+
if (request.taskRef === null) {
|
|
80
|
+
store.settleAcpCheck(request.id, attempt, branch, clock());
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const ref = request.taskRef;
|
|
84
|
+
const quiet = () => !store.runsFor(ref).some(run => run.outcome === null || store.openRunProcessCount(run.id, clock()) > 0);
|
|
85
|
+
if (!quiet())
|
|
86
|
+
continue;
|
|
87
|
+
const lease = store.currentLiveLease(ref, clock());
|
|
88
|
+
if (lease !== null && lease !== request.leaseId)
|
|
89
|
+
continue; // someone else holds the task now: not ours to touch
|
|
90
|
+
// Teardown runs under a fresh claim of this watch's own — never the check's earlier one, which carries the
|
|
91
|
+
// incarnation of a watch whose recovery may release it mid-way. Someone taking the task in between wins.
|
|
92
|
+
if (lease !== null)
|
|
93
|
+
release(store, lease, clock());
|
|
94
|
+
const fresh = `agent-check-${request.id}-settle-${randomBytes(4).toString("hex")}`;
|
|
95
|
+
const took = acquire(store, ref, job.runner, { now: clock(), token: job.token, newLeaseId: () => fresh, ttlMs: SETTLE_HOLD_MS, incarnation: job.watch.owner });
|
|
96
|
+
if (!took.ok || !store.setAcpCheckLease(request.id, fresh)) {
|
|
97
|
+
if (took.ok)
|
|
98
|
+
release(store, fresh, clock());
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
held = fresh;
|
|
102
|
+
const claim = held;
|
|
103
|
+
// Still this attempt's, the claim still live and the task still quiet — then both are renewed in full, so
|
|
104
|
+
// neither lapses during the step about to run.
|
|
105
|
+
const ours = () => {
|
|
106
|
+
if (!store.holdsAcpSettle(request.id, attempt, clock()) || store.currentLiveLease(ref, clock()) !== claim || !quiet())
|
|
107
|
+
return false;
|
|
108
|
+
return heartbeat(store, claim, clock(), SETTLE_HOLD_MS).ok && store.holdAcpSettle(request.id, attempt, clock(), SETTLE_HOLD_MS);
|
|
109
|
+
};
|
|
110
|
+
let discarded = true;
|
|
111
|
+
for (const row of store.listWorktrees().filter(one => one.taskRef === ref && one.branch === branch)) {
|
|
112
|
+
if (!ours()) {
|
|
113
|
+
discarded = false;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (row.releasedAt === null)
|
|
117
|
+
await job.pool.release(row.path, clock());
|
|
118
|
+
if (!ours()) {
|
|
119
|
+
discarded = false;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (!(await job.pool.discard(row.path, clock())).ok)
|
|
123
|
+
discarded = false;
|
|
124
|
+
}
|
|
125
|
+
if (!discarded || !ours())
|
|
126
|
+
continue;
|
|
127
|
+
const taskId = store.refForId(ref)?.externalId;
|
|
128
|
+
if (taskId !== undefined)
|
|
129
|
+
store.setTaskState(taskId, request.state === "passed" ? "done" : "failed", clock());
|
|
130
|
+
store.settleAcpCheck(request.id, attempt, branch, clock());
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Retried on the next pass.
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if (held !== null && store.currentLiveLease(request.taskRef ?? -1, clock()) === held)
|
|
137
|
+
release(store, held, clock());
|
|
138
|
+
store.endAcpSettle(request.id, attempt);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Run one claimed check; null when it passed (and was published), else why not. */
|
|
143
|
+
async function check(job, request, clock) {
|
|
144
|
+
const { store } = job;
|
|
145
|
+
const fail = (words) => { store.failAcpCheck(request.id, words, clock()); return words; };
|
|
146
|
+
if (!isAcpProvider(request.provider))
|
|
147
|
+
return fail("not an ACP agent");
|
|
148
|
+
const env = job.env ?? process.env;
|
|
149
|
+
// What runs: the binary on this runner's PATH, its launch pinned and every file it depends on bound.
|
|
150
|
+
const seen = inspectAcpAgent(request.provider, env);
|
|
151
|
+
if (!seen.found)
|
|
152
|
+
return fail(`${ACP_AGENTS[request.provider].binary} isn't installed on ${job.runner}`);
|
|
153
|
+
const launch = launchOf(seen.realpath, env["PATH"] ?? "");
|
|
154
|
+
if ("problem" in launch)
|
|
155
|
+
return fail(launch.problem);
|
|
156
|
+
// A task of the watch's own project, placed and claimed like any other, bound to the request.
|
|
157
|
+
const taskId = `agent-check-${request.id}`;
|
|
158
|
+
store.createTask({ id: taskId, title: `Check ${ACP_AGENTS[request.provider].name} (${request.model})` }, clock());
|
|
159
|
+
const ref = store.refFor(BUILT_IN, taskId).id;
|
|
160
|
+
store.placeTask(ref, job.watch.repo);
|
|
161
|
+
const leaseId = `agent-check-${request.id}-${randomBytes(4).toString("hex")}`;
|
|
162
|
+
const took = acquireForCheck(store, ref, job.runner, { now: clock(), token: job.token, newLeaseId: () => leaseId, ttlMs: 30 * 60_000, incarnation: job.watch.owner });
|
|
163
|
+
if (!took.ok) {
|
|
164
|
+
store.setTaskState(taskId, "failed", clock());
|
|
165
|
+
return fail(`the check couldn't claim its task (${took.reason})`);
|
|
166
|
+
}
|
|
167
|
+
if (!store.bindAcpCheck(request.id, { taskRef: ref, launchJson: JSON.stringify(launch), leaseId }, clock())) {
|
|
168
|
+
release(store, leaseId, clock());
|
|
169
|
+
store.setTaskState(taskId, "failed", clock());
|
|
170
|
+
return fail("the check lost its watch before it began");
|
|
171
|
+
}
|
|
172
|
+
// From here on, what the check holds is let go by settleChecks, whatever happens.
|
|
173
|
+
const leased = await job.pool.lease({ repo: job.watch.repo, branch: `toolroll/${taskId}`, base: "HEAD", runner: job.runner, taskRef: ref, now: clock() });
|
|
174
|
+
if (!leased.ok)
|
|
175
|
+
return fail(`the check's checkout couldn't be made: ${leased.message}`);
|
|
176
|
+
// Recovery may have closed — even settled — the check while its checkout was being made: what landed late reopens
|
|
177
|
+
// cleanup, so the next pass lets it go.
|
|
178
|
+
if (store.acpCheck(request.id)?.state !== "running") {
|
|
179
|
+
store.unsettleAcpCheck(request.id);
|
|
180
|
+
return fail("the check was closed while its checkout was being made");
|
|
181
|
+
}
|
|
182
|
+
const worktree = leased.worktree.path;
|
|
183
|
+
const stateDir = dirname(realpathSync(store.databaseFile() ?? "."));
|
|
184
|
+
// Named as a secret is, beside the database: fenced whether the state folder is Toolroll's own or shared.
|
|
185
|
+
const canaryFile = join(stateDir, `acp-check-secret-${request.id}`);
|
|
186
|
+
let verdict = null;
|
|
187
|
+
try {
|
|
188
|
+
verdict = await steps(job, request, { ref, leaseId, worktree, canaryFile }, clock);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
rmSync(canaryFile, { force: true });
|
|
192
|
+
}
|
|
193
|
+
if (verdict !== null)
|
|
194
|
+
store.failAcpCheck(request.id, verdict, clock());
|
|
195
|
+
return verdict;
|
|
196
|
+
}
|
|
197
|
+
/** A check step's run, finished as the check's own refusal — once, if it is still open. */
|
|
198
|
+
function finishCheckRun(store, run, clock) {
|
|
199
|
+
if (store.getRun(run)?.outcome === null)
|
|
200
|
+
store.finishRun(run, { outcome: "refused", reason: "agent-check", now: clock() });
|
|
201
|
+
}
|
|
202
|
+
/** The two steps; whatever throws, the step's run in flight is finished (cleanup still waits for its processes). */
|
|
203
|
+
async function steps(job, request, at, clock) {
|
|
204
|
+
let current = null;
|
|
205
|
+
try {
|
|
206
|
+
return await runSteps(job, request, at, clock, run => { current = run; });
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
if (current !== null)
|
|
210
|
+
finishCheckRun(job.store, current, clock);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function runSteps(job, request, at, clock, track) {
|
|
214
|
+
const { store } = job;
|
|
215
|
+
const spec = { provider: request.provider, model: request.model };
|
|
216
|
+
const ask = (brief) => ({ phase: "build", brief, maxTurns: 1, permissionMode: "bypassPermissions", skipPermissions: true, resumeSession: null });
|
|
217
|
+
const budget = () => {
|
|
218
|
+
const agent = store.agentFor(request.provider, request.model, job.runner);
|
|
219
|
+
if (store.budgetGate(clock())({ ...store.budgetSubject(at.ref), agents: [agent] }).over !== null)
|
|
220
|
+
return "a monthly budget holds metered work, and this model is metered here";
|
|
221
|
+
if (agent.billing === "api-key" && store.getSpendDefaults()?.buildPerRunMicrousd != null)
|
|
222
|
+
return "a per-run dollar cap is set, and this metered model can't hold one (budget-unenforceable)";
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
const startRun = () => store.startRun({ taskRef: at.ref, leaseId: at.leaseId, runner: job.runner, branch: `toolroll/agent-check-${request.id}`, worktree: at.worktree,
|
|
226
|
+
route: { routeDigest: "legacy", phase: "build", provider: request.provider, model: request.model, chosen: "legacy" }, now: clock() });
|
|
227
|
+
const finish = (run) => finishCheckRun(store, run, clock);
|
|
228
|
+
const options = (extra) => ({
|
|
229
|
+
cwd: at.worktree, idleTimeoutMs: 60_000, timeoutMs: 300_000, clock,
|
|
230
|
+
...(job.keyHome === undefined ? {} : { keyHome: job.keyHome }),
|
|
231
|
+
...(job.agent === undefined ? {} : { runner: job.agent }),
|
|
232
|
+
...extra,
|
|
233
|
+
});
|
|
234
|
+
// Step 1: session-ok.
|
|
235
|
+
const held1 = budget();
|
|
236
|
+
if (held1 !== null)
|
|
237
|
+
return held1;
|
|
238
|
+
const run1 = startRun();
|
|
239
|
+
track(run1);
|
|
240
|
+
if (!store.consumeAcpCheckStep(request.id, 1, run1, clock())) {
|
|
241
|
+
finish(run1);
|
|
242
|
+
return "the check stopped admitting its first step (expired, or its watch lost the lease)";
|
|
243
|
+
}
|
|
244
|
+
const fileCanary = randomBytes(18).toString("hex");
|
|
245
|
+
const envValue = `sk-ant-canary-${randomBytes(18).toString("hex")}`;
|
|
246
|
+
writeFileSync(at.canaryFile, fileCanary, { mode: 0o600 });
|
|
247
|
+
// The checkout carries the project's HEAD: anything already named done.txt goes first, so only the agent's own
|
|
248
|
+
// write can make it say ok.
|
|
249
|
+
rmSync(join(at.worktree, "done.txt"), { force: true, recursive: true });
|
|
250
|
+
/** The tool calls the runner saw, by id: their kind, and whether they completed. */
|
|
251
|
+
const calls = new Map();
|
|
252
|
+
const first = await invokeAgent(store, run1, spec, ask(CHECK_PROMPTS.ok), options({
|
|
253
|
+
env: { ANTHROPIC_API_KEY: envValue },
|
|
254
|
+
acpCheck: { requestId: request.id, step: 1, isolation: { file: at.canaryFile, fileCanary, envValue } },
|
|
255
|
+
onStreamEvent: event => {
|
|
256
|
+
const update = event["acp"];
|
|
257
|
+
const id = update?.["toolCallId"];
|
|
258
|
+
if (typeof id !== "string" || (update?.["sessionUpdate"] !== "tool_call" && update?.["sessionUpdate"] !== "tool_call_update"))
|
|
259
|
+
return;
|
|
260
|
+
const before = calls.get(id) ?? { kind: null, done: false };
|
|
261
|
+
calls.set(id, { kind: typeof update["kind"] === "string" ? update["kind"] : before.kind, done: before.done || update["status"] === "completed" });
|
|
262
|
+
},
|
|
263
|
+
}));
|
|
264
|
+
finish(run1);
|
|
265
|
+
// An edit (or a command) the runner saw complete: a read can't make done.txt.
|
|
266
|
+
const wrote = [...calls.values()].some(one => one.done && (one.kind === "edit" || one.kind === "execute"));
|
|
267
|
+
const one = gradeOk(first, at.worktree, wrote);
|
|
268
|
+
if (one.verdict !== null)
|
|
269
|
+
return `session-ok: ${one.verdict}`;
|
|
270
|
+
if (!store.passAcpCheckStep(request.id, 1, run1, clock()))
|
|
271
|
+
return "the check stopped admitting its first step's result";
|
|
272
|
+
// Step 2: session-stop.
|
|
273
|
+
const held2 = budget();
|
|
274
|
+
if (held2 !== null)
|
|
275
|
+
return held2;
|
|
276
|
+
const run2 = startRun();
|
|
277
|
+
track(run2);
|
|
278
|
+
if (!store.consumeAcpCheckStep(request.id, 2, run2, clock())) {
|
|
279
|
+
finish(run2);
|
|
280
|
+
return "the check stopped admitting its second step";
|
|
281
|
+
}
|
|
282
|
+
let witnessed = [];
|
|
283
|
+
let started = false;
|
|
284
|
+
const second = await invokeAgent(store, run2, spec, ask(CHECK_PROMPTS.stop), options({
|
|
285
|
+
acpCheck: { requestId: request.id, step: 2 },
|
|
286
|
+
onStreamEvent: event => {
|
|
287
|
+
const update = event["acp"];
|
|
288
|
+
if ((update?.["sessionUpdate"] === "tool_call" || update?.["sessionUpdate"] === "tool_call_update") && update["kind"] !== "read" && update["status"] === "in_progress")
|
|
289
|
+
started = true;
|
|
290
|
+
},
|
|
291
|
+
acp: { prompt: "", model: "", expect: null, onControl: (control) => {
|
|
292
|
+
// The agent's word only times the cancel: what the plane itself sees alive is what is graded.
|
|
293
|
+
const until = Date.now() + STOP_WAIT_MS;
|
|
294
|
+
const poll = setInterval(() => {
|
|
295
|
+
if (Date.now() > until) {
|
|
296
|
+
clearInterval(poll);
|
|
297
|
+
control.cancel(CANCEL_ANSWER_MS);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!started)
|
|
301
|
+
return;
|
|
302
|
+
const live = control.liveDescendants();
|
|
303
|
+
if (live !== null && live.length > 0) {
|
|
304
|
+
witnessed = live;
|
|
305
|
+
clearInterval(poll);
|
|
306
|
+
control.cancel(CANCEL_ANSWER_MS);
|
|
307
|
+
}
|
|
308
|
+
}, 200);
|
|
309
|
+
poll.unref?.();
|
|
310
|
+
} },
|
|
311
|
+
}));
|
|
312
|
+
finish(run2);
|
|
313
|
+
const two = gradeStop(second, witnessed);
|
|
314
|
+
if (two !== null)
|
|
315
|
+
return `session-stop: ${two}`;
|
|
316
|
+
if (!store.passAcpCheckStep(request.id, 2, run2, clock()))
|
|
317
|
+
return "the check stopped admitting its second step's result";
|
|
318
|
+
// Both passed: publish, in one transaction with the attestation.
|
|
319
|
+
const facts = first.kind === "ran" ? first.outcome.acp : undefined;
|
|
320
|
+
if (facts?.agent == null)
|
|
321
|
+
return "the agent never said who it is";
|
|
322
|
+
const published = store.publishAcpCheck(request.id, {
|
|
323
|
+
provider: request.provider, runner: job.runner, launch: JSON.parse(store.acpCheck(request.id)?.launchJson ?? "null"),
|
|
324
|
+
agent: facts.agent, cliVersion: facts.agent.version, models: facts.offered, checks: [`conformance v${ACP_CONFORMANCE_VERSION}`, "isolation", "session-ok", "session-stop"],
|
|
325
|
+
recordedBy: request.requestedBy,
|
|
326
|
+
}, clock());
|
|
327
|
+
return published ? null : "the check couldn't be published (expired, or its watch lost the lease)";
|
|
328
|
+
}
|
|
329
|
+
function gradeOk(result, worktree, toolDone) {
|
|
330
|
+
if (result.kind !== "ran")
|
|
331
|
+
return { verdict: `the run was refused: ${result.diagnostic ?? result.reason}` };
|
|
332
|
+
const facts = result.outcome.acp;
|
|
333
|
+
if (facts === undefined)
|
|
334
|
+
return { verdict: "the runner recorded no conversation" };
|
|
335
|
+
if (facts.isolation === undefined)
|
|
336
|
+
return { verdict: "the plane's isolation probes didn't run (no OS fence?)" };
|
|
337
|
+
if (!facts.isolation.fileDenied)
|
|
338
|
+
return { verdict: "a file in the fenced state was readable inside the fence" };
|
|
339
|
+
if (!facts.isolation.envClear)
|
|
340
|
+
return { verdict: "a key the launch should shed reached the agent's environment" };
|
|
341
|
+
if (facts.stop !== "end_turn")
|
|
342
|
+
return { verdict: `the prompt ended ${facts.stop ?? "without an answer"}, not end_turn` };
|
|
343
|
+
if (result.outcome.code !== 0)
|
|
344
|
+
return { verdict: `the agent exited ${result.outcome.code}` };
|
|
345
|
+
if (facts.exit?.how !== "eof")
|
|
346
|
+
return { verdict: `the agent didn't leave on its own when its input closed (${facts.exit?.how ?? "unknown"})` };
|
|
347
|
+
if (facts.exit.leftover)
|
|
348
|
+
return { verdict: "the agent left processes running" };
|
|
349
|
+
if (!toolDone)
|
|
350
|
+
return { verdict: "no edit or command was seen to complete" };
|
|
351
|
+
if (smallFile(join(worktree, "done.txt"))?.trim() !== "ok")
|
|
352
|
+
return { verdict: "done.txt doesn't say ok" };
|
|
353
|
+
return { verdict: null };
|
|
354
|
+
}
|
|
355
|
+
function gradeStop(result, witnessed) {
|
|
356
|
+
const facts = result.kind === "ran" ? result.outcome.acp : result.acp;
|
|
357
|
+
if (witnessed.length === 0)
|
|
358
|
+
return "no command the agent started was seen alive before the cancel";
|
|
359
|
+
if (facts === undefined)
|
|
360
|
+
return "the runner recorded no conversation";
|
|
361
|
+
// The expected end, and only it: answered cancelled in time (the runner stops an unanswered cancel and the
|
|
362
|
+
// conversation fails), no protocol breach, no timeout, and an ending the runner itself saw.
|
|
363
|
+
if (facts.protocolError !== null)
|
|
364
|
+
return `the agent broke the protocol: ${facts.protocolError}`;
|
|
365
|
+
if (facts.stop !== "cancelled")
|
|
366
|
+
return `the prompt ended ${facts.stop ?? "without an answer"}, not cancelled`;
|
|
367
|
+
if (facts.exit === null || facts.exit.timedOut || facts.exit.how === "error")
|
|
368
|
+
return "the agent's process didn't end cleanly after the cancel";
|
|
369
|
+
if (result.kind === "refused" && result.reason !== "provider-protocol")
|
|
370
|
+
return `the run was refused: ${result.diagnostic ?? result.reason}`;
|
|
371
|
+
// Its whole process tree ended with it, proved: nothing left for the runner to kill, and no containment in doubt.
|
|
372
|
+
if (facts.exit.leftover)
|
|
373
|
+
return "the agent left processes running after the cancel";
|
|
374
|
+
if (result.kind === "ran" && result.outcome.code === 126)
|
|
375
|
+
return "the runner couldn't prove the agent's processes all ended";
|
|
376
|
+
// Every process alive at the cancel is gone — and so is every process group it was in: a leader that exited after
|
|
377
|
+
// forking leaves its group behind, which is no stop at all.
|
|
378
|
+
for (const { pid, group } of witnessed) {
|
|
379
|
+
try {
|
|
380
|
+
process.kill(pid, 0);
|
|
381
|
+
return `process ${pid}, alive at the cancel, is still running`;
|
|
382
|
+
}
|
|
383
|
+
catch { /* gone */ }
|
|
384
|
+
try {
|
|
385
|
+
process.kill(-group, 0);
|
|
386
|
+
return `process group ${group}, alive at the cancel, still has members`;
|
|
387
|
+
}
|
|
388
|
+
catch { /* gone */ }
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
/** A file the agent wrote, read only if it is a plain file (no link followed, no FIFO or device) and tiny. */
|
|
393
|
+
function smallFile(path) {
|
|
394
|
+
let fd = null;
|
|
395
|
+
try {
|
|
396
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | (constants.O_NONBLOCK ?? 0));
|
|
397
|
+
const stat = fstatSync(fd);
|
|
398
|
+
if (!stat.isFile() || stat.size > 64)
|
|
399
|
+
return null;
|
|
400
|
+
const buffer = Buffer.alloc(64);
|
|
401
|
+
return buffer.subarray(0, readSync(fd, buffer, 0, 64, 0)).toString("utf8");
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
if (fd !== null)
|
|
408
|
+
closeSync(fd);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Client Protocol, from the plane's side (docs/plans/acp-adapter.md).
|
|
3
|
+
*
|
|
4
|
+
* ACP is JSON-RPC 2.0, one message per line, over the agent's stdio. This
|
|
5
|
+
* module is the whole conversation as a pure state machine: it is handed the
|
|
6
|
+
* agent's lines one at a time and answers with the lines to send back. It
|
|
7
|
+
* never spawns, reads a clock or touches a file — the runner (exec.ts) owns
|
|
8
|
+
* the process, the timers and the shutdown.
|
|
9
|
+
*
|
|
10
|
+
* One conversation is one build: `initialize`, `session/new` in the
|
|
11
|
+
* worktree, the sealed model selected and seen to be current, one
|
|
12
|
+
* `session/prompt` carrying the brief, then `session/update`s until the
|
|
13
|
+
* response to that prompt. Success is that response with
|
|
14
|
+
* `stopReason: "end_turn"` and nothing else — no update, exit code or
|
|
15
|
+
* message text can stand in for it.
|
|
16
|
+
*
|
|
17
|
+
* The plane offers no client capabilities (no file system, no terminal), so
|
|
18
|
+
* every agent request except `session/request_permission` is answered
|
|
19
|
+
* "method not found". A model is chosen only through a `model` config option,
|
|
20
|
+
* whose answer shows the model current: `session/set_model` answers with
|
|
21
|
+
* nothing to observe, so an agent that offers only that is refused until a
|
|
22
|
+
* fixture proves how it confirms. Builds are full access inside the OS fence, so a
|
|
23
|
+
* permission request is granted once (`allow_once`, never `allow_always`);
|
|
24
|
+
* the fence, not this answer, is the boundary — OpenCode runs commands
|
|
25
|
+
* without asking at all.
|
|
26
|
+
*
|
|
27
|
+
* What the runner keeps is not the agent's stream but a short synthetic
|
|
28
|
+
* record (`retained()`), one JSON object per line, typed `acp.*` — a type
|
|
29
|
+
* no agent can emit — which `acpParse` reads into the gateway's envelope.
|
|
30
|
+
*/
|
|
31
|
+
/** How many offered models a conversation keeps (each id ≤ 200 characters). */
|
|
32
|
+
export declare const ACP_OFFERED_CAP = 2000;
|
|
33
|
+
/** The largest line (UTF-8 bytes) the plane reads from an agent; a longer one is a protocol failure. */
|
|
34
|
+
export declare const ACP_LINE_CAP: number;
|
|
35
|
+
export type AcpStopReason = "end_turn" | "max_tokens" | "max_turn_requests" | "refusal" | "cancelled";
|
|
36
|
+
/** How the agent lets a client choose its model: a `model` config option, or `session/set_model`. */
|
|
37
|
+
export type AcpModelMechanism = "config-option" | "set-model";
|
|
38
|
+
export type AcpFailureKind =
|
|
39
|
+
/** The agent broke the protocol: malformed or oversized line, unknown or repeated response, a stranger's session. */
|
|
40
|
+
"protocol"
|
|
41
|
+
/** The agent is not the one attested (name or version). */
|
|
42
|
+
| "identity"
|
|
43
|
+
/** The sealed model is not offered, or the agent did not make it current. */
|
|
44
|
+
| "model-unavailable"
|
|
45
|
+
/** The agent wants a sign-in. */
|
|
46
|
+
| "auth-required";
|
|
47
|
+
export type AcpFailure = {
|
|
48
|
+
kind: AcpFailureKind;
|
|
49
|
+
text: string;
|
|
50
|
+
};
|
|
51
|
+
/** The response to our prompt: a stop reason, or a JSON-RPC error. */
|
|
52
|
+
export type AcpTerminal = {
|
|
53
|
+
stopReason: AcpStopReason;
|
|
54
|
+
} | {
|
|
55
|
+
error: {
|
|
56
|
+
code: number | null;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export type AcpPhase = "initialize" | "session" | "model" | "prompt" | "ended" | "failed";
|
|
61
|
+
export type AcpConversationOptions = {
|
|
62
|
+
/** The working directory the session is opened in (the worktree). */
|
|
63
|
+
cwd: string;
|
|
64
|
+
/** The brief, sent as one text block. */
|
|
65
|
+
prompt: string;
|
|
66
|
+
/** The sealed model: must be offered and become current. */
|
|
67
|
+
model: string;
|
|
68
|
+
/** The agent the attestation names; null only for the conformance probe's first look. */
|
|
69
|
+
expect: {
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
} | null;
|
|
73
|
+
/** MCP servers offered to the session (http entries only). */
|
|
74
|
+
mcpServers?: readonly {
|
|
75
|
+
type: "http";
|
|
76
|
+
name: string;
|
|
77
|
+
url: string;
|
|
78
|
+
headers: readonly {
|
|
79
|
+
name: string;
|
|
80
|
+
value: string;
|
|
81
|
+
}[];
|
|
82
|
+
}[];
|
|
83
|
+
};
|
|
84
|
+
/** What one received line asks of the runner. */
|
|
85
|
+
export type AcpReaction = {
|
|
86
|
+
/** Lines to write to the agent's stdin, in order. */
|
|
87
|
+
send: string[];
|
|
88
|
+
/** Session updates for the live view (`{acp: update}` is how the recorder recognizes them). */
|
|
89
|
+
live: Record<string, unknown>[];
|
|
90
|
+
};
|
|
91
|
+
export type AcpConversation = {
|
|
92
|
+
/** The first line to send (`initialize`). */
|
|
93
|
+
start(): string[];
|
|
94
|
+
/** Hand over one line the agent wrote. */
|
|
95
|
+
receive(line: string): AcpReaction;
|
|
96
|
+
/** Ask the agent to stop the prompt in flight (a notification; the prompt still answers, with `cancelled`). */
|
|
97
|
+
cancel(): string[];
|
|
98
|
+
/** The transport saw something the conversation can't read (an oversized line): a protocol failure. */
|
|
99
|
+
refuse(text: string): void;
|
|
100
|
+
/** The `session/close` request to send after the terminal, when the agent offers it; else nothing. */
|
|
101
|
+
close(): string[];
|
|
102
|
+
/** Whether a line sent by close() has been answered (or failed). */
|
|
103
|
+
closeAnswered(): boolean;
|
|
104
|
+
phase(): AcpPhase;
|
|
105
|
+
failure(): AcpFailure | null;
|
|
106
|
+
terminal(): AcpTerminal | null;
|
|
107
|
+
sessionId(): string | null;
|
|
108
|
+
agent(): {
|
|
109
|
+
name: string;
|
|
110
|
+
version: string;
|
|
111
|
+
} | null;
|
|
112
|
+
mechanism(): AcpModelMechanism | null;
|
|
113
|
+
/** The models the agent offered, as it listed them. */
|
|
114
|
+
offered(): string[];
|
|
115
|
+
/** The synthetic record `acpParse` reads. */
|
|
116
|
+
retained(): string;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The answer to a permission request: the `allow_once` option, else the `reject_once` one. The whole list must be
|
|
120
|
+
* well formed — every option an object with a non-empty id and a known kind, no id twice — or nothing is chosen: an
|
|
121
|
+
* id shared by "once" and "always" would make the answer mean either.
|
|
122
|
+
*/
|
|
123
|
+
export declare function permissionAnswer(params: unknown): {
|
|
124
|
+
optionId: string;
|
|
125
|
+
granted: boolean;
|
|
126
|
+
} | null;
|
|
127
|
+
export declare function createAcpConversation(options: AcpConversationOptions): AcpConversation;
|
|
128
|
+
/** What the gateway reads back from a conversation's retained record. */
|
|
129
|
+
export type AcpEnvelope = {
|
|
130
|
+
sessionId: string | null;
|
|
131
|
+
protocolError: string | null;
|
|
132
|
+
finalMessage: string | null;
|
|
133
|
+
/** The agent's own usage report, unattested: never priced. */
|
|
134
|
+
reportedUsage: string | null;
|
|
135
|
+
initObserved: boolean;
|
|
136
|
+
/** True only when the prompt's answer was `end_turn`. */
|
|
137
|
+
promptConsumed: boolean;
|
|
138
|
+
diagnostic: string | null;
|
|
139
|
+
/** A failure terminal, when the prompt was answered with anything but end_turn or the conversation failed. */
|
|
140
|
+
terminal: {
|
|
141
|
+
failed: true;
|
|
142
|
+
text: string | null;
|
|
143
|
+
code: string;
|
|
144
|
+
} | null;
|
|
145
|
+
/** The models the agent offered at session/new (the conformance check records them). */
|
|
146
|
+
offered: string[];
|
|
147
|
+
/** Who the agent said it is at initialize. */
|
|
148
|
+
agent: {
|
|
149
|
+
name: string;
|
|
150
|
+
version: string;
|
|
151
|
+
} | null;
|
|
152
|
+
/** The prompt's stop reason, when it answered with one. */
|
|
153
|
+
stop: string | null;
|
|
154
|
+
/** How the agent's process ended, as the runner saw it: on its own after stdin closed, or by our SIGTERM or kill;
|
|
155
|
+
* and whether anything of its group was still alive at the reap. Null when the runner wrote none. */
|
|
156
|
+
exit: {
|
|
157
|
+
how: "eof" | "term" | "kill" | "self" | "error";
|
|
158
|
+
leftover: boolean;
|
|
159
|
+
timedOut: boolean;
|
|
160
|
+
} | null;
|
|
161
|
+
};
|
|
162
|
+
/** Read a conversation's retained record. Lines not typed `acp.*` are ignored. */
|
|
163
|
+
export declare function acpParse(stdout: string): AcpEnvelope;
|