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
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
|
-
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { dirname, join, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -82,16 +82,37 @@ function run(file, args, options = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Build isolation sentinels
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
85
|
+
* Build isolation sentinels. Each fires on its own, with no help from the
|
|
86
|
+
* model, and appends "<launch> <sentinel>" to fired.log, where <launch> is
|
|
87
|
+
* the CANARY_LAUNCH of the process tree that ran it:
|
|
88
|
+
*
|
|
89
|
+
* - project-hook and local-hook: hooks in the repository's own and local
|
|
90
|
+
* Claude settings; repository-mcp: a server in its .mcp.json (the local
|
|
91
|
+
* settings enable every project server, so nothing waits on approval).
|
|
92
|
+
* - plugin: a Claude plugin named in CLAUDE_CODE_PLUGIN_DIRS, which the
|
|
93
|
+
* canary puts in every launch's environment; its hooks run whatever a
|
|
94
|
+
* launch's setting sources are.
|
|
95
|
+
* - adjacent-folder: the project's opted-in server reads a file in a
|
|
96
|
+
* repository beside the project's own as it starts. The agent fence keeps
|
|
97
|
+
* a build's tools inside its task boundary.
|
|
98
|
+
* - named-tool: that opted-in server started at all.
|
|
99
|
+
*
|
|
100
|
+
* The control launches the provider CLI unisolated (the repository's own
|
|
101
|
+
* setup loaded, the tool started directly, no fence), fresh and resumed:
|
|
102
|
+
* every sentinel must fire. The isolated launches are the real build and a
|
|
103
|
+
* resumed turn through the same gateway: only named-tool may fire. Claude
|
|
104
|
+
* only; other providers keep the repository checks without a control.
|
|
90
105
|
*/
|
|
91
|
-
const
|
|
92
|
-
|
|
106
|
+
const LOG = (log, kind) => `node -e "require('fs').appendFileSync(process.argv[1],(process.env.CANARY_LAUNCH||'unknown')+' '+process.argv[2]+'\\n')" ${JSON.stringify(log)} ${kind}`;
|
|
107
|
+
/** The opted-in tool's server. A brokered server gets only an allowlisted environment (tool-broker.ts), so it reads
|
|
108
|
+
* which launch it serves from the launch file the canary keeps beside the log; the control passes CANARY_LAUNCH. */
|
|
109
|
+
export const OPT_IN_SERVER = `import { appendFileSync, readFileSync } from "node:fs";
|
|
93
110
|
import { createInterface } from "node:readline";
|
|
94
|
-
|
|
111
|
+
const [log, adjacent, launchFile] = process.argv.slice(2);
|
|
112
|
+
const launch = () => { try { return readFileSync(launchFile, "utf8").trim(); } catch { return ""; } };
|
|
113
|
+
const fired = kind => appendFileSync(log, (process.env.CANARY_LAUNCH || launch() || "unknown") + " " + kind + "\\n");
|
|
114
|
+
fired("named-tool");
|
|
115
|
+
try { readFileSync(adjacent); fired("adjacent-folder"); } catch { /* fenced */ }
|
|
95
116
|
const send = message => process.stdout.write(JSON.stringify(message) + "\\n");
|
|
96
117
|
createInterface({ input: process.stdin }).on("line", line => {
|
|
97
118
|
const message = JSON.parse(line);
|
|
@@ -100,35 +121,132 @@ createInterface({ input: process.stdin }).on("line", line => {
|
|
|
100
121
|
else if (message.id !== undefined) send({ jsonrpc: "2.0", id: message.id, result: {} });
|
|
101
122
|
});
|
|
102
123
|
`;
|
|
124
|
+
const SENTINELS = ["project-hook", "local-hook", "repository-mcp", "plugin", "adjacent-folder", "named-tool"];
|
|
103
125
|
|
|
104
|
-
async function plantIsolationSentinels(repo, sentinels) {
|
|
105
|
-
const
|
|
126
|
+
async function plantIsolationSentinels(base, repo, sentinels) {
|
|
127
|
+
const log = join(sentinels, "fired.log");
|
|
128
|
+
const hook = kind => ({ hooks: [{ type: "command", command: LOG(log, kind) }] });
|
|
106
129
|
await mkdir(join(repo, ".claude"), { recursive: true });
|
|
107
|
-
await writeFile(join(repo, ".claude", "settings.json"), `${JSON.stringify({ hooks: { SessionStart: [hook(
|
|
108
|
-
await writeFile(join(repo, ".claude", "settings.local.json"), `${JSON.stringify({ hooks: { UserPromptSubmit: [hook(
|
|
109
|
-
await writeFile(join(repo, ".mcp.json"), `${JSON.stringify({ mcpServers: { "repo-sentinel": { type: "stdio", command: "node", args: ["-e", `require('fs').
|
|
130
|
+
await writeFile(join(repo, ".claude", "settings.json"), `${JSON.stringify({ hooks: { SessionStart: [hook("project-hook")], UserPromptSubmit: [hook("project-hook")] } }, null, 2)}\n`);
|
|
131
|
+
await writeFile(join(repo, ".claude", "settings.local.json"), `${JSON.stringify({ enableAllProjectMcpServers: true, hooks: { UserPromptSubmit: [hook("local-hook")] } }, null, 2)}\n`);
|
|
132
|
+
await writeFile(join(repo, ".mcp.json"), `${JSON.stringify({ mcpServers: { "repo-sentinel": { type: "stdio", command: "node", args: ["-e", `require('fs').appendFileSync(${JSON.stringify(log)},(process.env.CANARY_LAUNCH||'unknown')+' repository-mcp\\n')`] } } }, null, 2)}\n`);
|
|
133
|
+
await mkdir(join(sentinels, "plugin", ".claude-plugin"), { recursive: true });
|
|
134
|
+
await mkdir(join(sentinels, "plugin", "hooks"), { recursive: true });
|
|
135
|
+
await writeFile(join(sentinels, "plugin", ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "toolroll-canary-sentinel", version: "1.0.0", description: "Toolroll provider canary" })}\n`);
|
|
136
|
+
await writeFile(join(sentinels, "plugin", "hooks", "hooks.json"), `${JSON.stringify({ hooks: { SessionStart: [hook("plugin")], UserPromptSubmit: [hook("plugin")] } })}\n`);
|
|
110
137
|
await writeFile(join(sentinels, "opt-in-server.mjs"), OPT_IN_SERVER);
|
|
138
|
+
// A repository beside the project's own, inside the build's task boundary.
|
|
139
|
+
await mkdir(join(base, "adjacent", ".git"), { recursive: true });
|
|
140
|
+
await writeFile(join(base, "adjacent", "secret.txt"), "adjacent\n");
|
|
141
|
+
await writeFile(launchFile(sentinels), "");
|
|
142
|
+
return { log, plugin: join(sentinels, "plugin"), server: [join(sentinels, "opt-in-server.mjs"), log, join(base, "adjacent", "secret.txt"), launchFile(sentinels)] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Where the canary says which launch is running, for the brokered tool server that can't see CANARY_LAUNCH. */
|
|
146
|
+
const launchFile = sentinels => join(sentinels, "launch");
|
|
147
|
+
/** Name the launch about to run (null: none), in the environment its own processes inherit and in the launch file. */
|
|
148
|
+
async function tagLaunch(sentinels, launch) {
|
|
149
|
+
if (launch === null) delete process.env.CANARY_LAUNCH;
|
|
150
|
+
else process.env.CANARY_LAUNCH = launch;
|
|
151
|
+
await writeFile(launchFile(sentinels), launch ?? "");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The task the canary files: no quote marks, which a model may copy into the file. */
|
|
155
|
+
export function canaryGoal(provider, marker) {
|
|
156
|
+
return marker === null
|
|
157
|
+
? `Add canary.txt: exactly standing-orders ${provider} canary passed plus newline. Add canary.test.js using node:test. Only these files; no questions; acceptance evidence: check and changed-path only.`
|
|
158
|
+
: `Add canary.txt: line 1 exactly standing-orders ${provider} canary passed; line 2 the TOOLROLL_MEMORY_CANARY marker memory gave you (never search), else NO_MEMORY. Add canary.test.js (node:test).`;
|
|
111
159
|
}
|
|
112
160
|
|
|
113
161
|
/** The project's one named tool, added the way the console adds it, before approval seals the task's tools. */
|
|
114
|
-
async function optInNamedTool(db, repo,
|
|
162
|
+
async function optInNamedTool(db, repo, server) {
|
|
115
163
|
const { openStore } = await import(join(root, "dist", "store.js"));
|
|
116
164
|
const { addToolTo } = await import(join(root, "dist", "project-tools.js"));
|
|
117
165
|
const store = openStore(db);
|
|
118
166
|
try {
|
|
119
|
-
const added = addToolTo(store, repo, { name: "canary-notes", transport: "stdio", command: process.execPath, args:
|
|
167
|
+
const added = addToolTo(store, repo, { name: "canary-notes", transport: "stdio", command: process.execPath, args: server, url: null, secrets: [], bearer: null, headerSecrets: {}, about: "The canary's opt-in server" }, "provider canary", "canary", new Date());
|
|
120
168
|
if (!added.ok) throw new Error(`the canary's named tool was refused: ${added.message}`);
|
|
121
169
|
} finally {
|
|
122
170
|
store.close();
|
|
123
171
|
}
|
|
124
172
|
}
|
|
125
173
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
174
|
+
/** Which sentinels each launch fired. */
|
|
175
|
+
export async function firedBy(log) {
|
|
176
|
+
const fired = {};
|
|
177
|
+
const text = existsSync(log) ? await readFile(log, "utf8") : "";
|
|
178
|
+
for (const line of text.split("\n").filter(Boolean)) {
|
|
179
|
+
const [launch, kind] = line.split(" ");
|
|
180
|
+
(fired[launch] ??= new Set()).add(kind);
|
|
181
|
+
}
|
|
182
|
+
return Object.fromEntries(Object.entries(fired).map(([launch, kinds]) => [launch, [...kinds].sort()]));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Every control launch fired every sentinel; every isolated launch fired
|
|
187
|
+
* only its named tool (Claude and Codex start it; the others have none).
|
|
188
|
+
*/
|
|
189
|
+
export function judgeIsolation(provider, fired, controls, isolated, fence = null) {
|
|
190
|
+
const problems = [];
|
|
191
|
+
for (const launch of controls) {
|
|
192
|
+
const missing = SENTINELS.filter(kind => !(fired[launch] ?? []).includes(kind));
|
|
193
|
+
if (missing.length > 0) problems.push(`the unisolated ${launch} launch did not fire ${missing.join(", ")}, so the canary can't tell isolation from a sentinel that never runs`);
|
|
194
|
+
}
|
|
195
|
+
for (const launch of isolated) {
|
|
196
|
+
const leaked = (fired[launch] ?? []).filter(kind => kind !== "named-tool");
|
|
197
|
+
if (leaked.length > 0) problems.push(`the isolated ${launch} launch fired ${leaked.join(", ")}${fence !== null && leaked.includes("adjacent-folder") ? ` (its fence: ${fence})` : ""}`);
|
|
198
|
+
if ((provider === "claude" || provider === "codex") && !(fired[launch] ?? []).includes("named-tool")) problems.push(`the project's named tool did not start for the ${launch} launch`);
|
|
199
|
+
}
|
|
200
|
+
const unknown = Object.keys(fired).filter(launch => !controls.includes(launch) && !isolated.includes(launch));
|
|
201
|
+
if (unknown.length > 0) problems.push(`sentinels fired from an unexpected launch: ${unknown.map(launch => `${launch} (${fired[launch].join(", ")})`).join("; ")}`);
|
|
202
|
+
if (problems.length > 0) throw new Error(`Build isolation: ${problems.join("; ")}.`);
|
|
203
|
+
return Object.fromEntries([...controls, ...isolated].map(launch => [launch, fired[launch] ?? []]));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The unisolated control: the CLI as a build ran before isolation, in the repository, fresh and then resumed. */
|
|
207
|
+
async function claudeControl(repo, model, sentinels, server) {
|
|
208
|
+
const config = join(sentinels, "control-mcp.json");
|
|
209
|
+
await writeFile(config, JSON.stringify({ mcpServers: { "canary-notes": { type: "stdio", command: process.execPath, args: server } } }));
|
|
210
|
+
const turn = async (launch, resume) => {
|
|
211
|
+
const answer = await run("claude", ["-p", "Reply with exactly: control", "--model", model, "--max-turns", "1", "--output-format", "json",
|
|
212
|
+
"--setting-sources", "project,local", "--mcp-config", config, ...(resume === null ? [] : ["--resume", resume])], { cwd: repo, env: { ...process.env, CANARY_LAUNCH: launch } });
|
|
213
|
+
const envelope = parseEnvelope(answer, `the unisolated control (${launch})`);
|
|
214
|
+
if (typeof envelope.session_id !== "string") throw new Error(`the unisolated control (${launch}) named no session`);
|
|
215
|
+
return envelope.session_id;
|
|
216
|
+
};
|
|
217
|
+
const session = await turn("control-fresh", null);
|
|
218
|
+
await turn("control-resumed", session);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A resumed turn of the finished build through the real gateway: the same fence, broker and isolation a repair gets. */
|
|
222
|
+
async function resumeThroughGateway({ db, repo, provider, model, runnerToken, buildRunId, sentinels }) {
|
|
223
|
+
const { openStore } = await import(join(root, "dist", "store.js"));
|
|
224
|
+
const { acquire } = await import(join(root, "dist", "claim.js"));
|
|
225
|
+
const { invokeAgent } = await import(join(root, "dist", "invoke.js"));
|
|
226
|
+
const store = openStore(db);
|
|
227
|
+
try {
|
|
228
|
+
const build = store.getRun(buildRunId);
|
|
229
|
+
if (build?.sessionId == null || build.worktree == null) throw new Error("the build run kept no session or worktree to resume");
|
|
230
|
+
// Claude finds a session by the folder it ran in; a released worktree's path is made again, empty.
|
|
231
|
+
await mkdir(build.worktree, { recursive: true });
|
|
232
|
+
const now = new Date();
|
|
233
|
+
store.createTask({ id: `${TASK_ID}-resume`, title: "Resume the canary build once" }, now);
|
|
234
|
+
const ref = store.refFor("built-in", `${TASK_ID}-resume`).id;
|
|
235
|
+
store.placeTask(ref, repo);
|
|
236
|
+
const leaseId = `canary-resume-${randomUUID()}`;
|
|
237
|
+
const took = acquire(store, ref, "canary-worker", { now, token: runnerToken, newLeaseId: () => leaseId, ttlMs: 30 * 60_000 });
|
|
238
|
+
if (!took.ok) throw new Error(`the resumed turn could not claim its task: ${took.reason}`);
|
|
239
|
+
const id = store.startRun({ taskRef: ref, leaseId, runner: "canary-worker", branch: build.branch ?? "canary", worktree: build.worktree, route: { routeDigest: "legacy", phase: "build", provider, model, chosen: "legacy" }, now });
|
|
240
|
+
store.stampRun(id, { sessionId: build.sessionId });
|
|
241
|
+
await tagLaunch(sentinels, "isolated-resumed");
|
|
242
|
+
const result = await invokeAgent(store, id, { provider, model }, { phase: "repair", brief: "Reply with exactly: resumed. Do not change any file.", maxTurns: 2, permissionMode: "plan", skipPermissions: false, resumeSession: build.sessionId },
|
|
243
|
+
{ cwd: build.worktree, env: { CANARY_LAUNCH: "isolated-resumed" }, timeoutMs: 600_000 }).finally(() => tagLaunch(sentinels, null));
|
|
244
|
+
store.finishRun(id, { outcome: result.kind === "ran" && result.outcome.code === 0 ? "built" : "failed", now: new Date() });
|
|
245
|
+
if (result.kind !== "ran" || result.outcome.code !== 0) throw new Error(`the resumed turn did not run: ${result.kind === "ran" ? `exit ${result.outcome.code}` : `${result.reason}: ${result.diagnostic ?? ""}`}`);
|
|
246
|
+
return { run: id, fence: store.runFence(id) };
|
|
247
|
+
} finally {
|
|
248
|
+
store.close();
|
|
249
|
+
}
|
|
132
250
|
}
|
|
133
251
|
|
|
134
252
|
/** Own only this disposable repository's memory directory. Never overwrite a
|
|
@@ -191,6 +309,45 @@ async function runtimeIdentity() {
|
|
|
191
309
|
trackedChanges: changes.code === 0 ? changes.stdout.trim() !== "" : null };
|
|
192
310
|
}
|
|
193
311
|
|
|
312
|
+
/** Verify Git from the produced checkout through the provider's generated policy, without a model turn. */
|
|
313
|
+
export async function checkCheckoutGit({ provider, worktree, databaseFile, repository, home = homedir() }, execute = run) {
|
|
314
|
+
if (!["claude", "codex", "gemini"].includes(provider)) return null;
|
|
315
|
+
if (typeof worktree !== "string" || worktree === "") throw new Error("the builder did not retain its worktree for the Git fence check");
|
|
316
|
+
const { agentFence, taskBoundary, trustedWorktreeBinding, checkoutGitEnv, HARDENED_GIT_ARGS, fenceGrants, codexFenceArgv, macosFenceAvailable, macosFenced, linuxFenceAvailable, linuxFenced } = await import(join(root, "dist", "agent-fence.js"));
|
|
317
|
+
const binding = trustedWorktreeBinding(repository, worktree);
|
|
318
|
+
const boundary = taskBoundary({ worktree, provider, home, binding });
|
|
319
|
+
const fence = agentFence({ databaseFile, worktree, home, boundary, binding });
|
|
320
|
+
const grants = fenceGrants({ databaseFile, worktree, boundary });
|
|
321
|
+
const codexHome = provider === "codex" ? await mkdtemp(join(tmpdir(), "so-codex-canary-")) : null;
|
|
322
|
+
try {
|
|
323
|
+
let head = null;
|
|
324
|
+
let skipped = false;
|
|
325
|
+
const env = checkoutGitEnv({ ...process.env, ...(codexHome === null ? {} : { CODEX_HOME: codexHome }) });
|
|
326
|
+
for (const args of [["rev-parse", "HEAD"], ["status", "--porcelain"]]) {
|
|
327
|
+
const gitArgs = [...HARDENED_GIT_ARGS, ...args];
|
|
328
|
+
const control = await execute("git", gitArgs, { cwd: worktree, env });
|
|
329
|
+
if (control.code !== 0) throw new Error(`unfenced git ${args.join(" ")} failed: ${control.stderr}`);
|
|
330
|
+
const wrapped = provider === "codex"
|
|
331
|
+
? { file: "codex", args: ["sandbox", ...codexFenceArgv(fence, false, grants), "-P", "standing-orders", "--", "git", ...gitArgs] }
|
|
332
|
+
: macosFenceAvailable() ? macosFenced("git", gitArgs, fence, grants)
|
|
333
|
+
: linuxFenceAvailable() ? linuxFenced("git", gitArgs, fence, grants) : null;
|
|
334
|
+
if (wrapped === null || fence.length === 0) { skipped = true; if (args[0] === "rev-parse") head = control.stdout.trim(); continue; }
|
|
335
|
+
const checked = await execute(wrapped.file, wrapped.args, { cwd: worktree,
|
|
336
|
+
env });
|
|
337
|
+
if (checked.code !== 0) {
|
|
338
|
+
const trivial = provider === "codex"
|
|
339
|
+
? { file: "codex", args: ["sandbox", ...codexFenceArgv(fence, false, grants), "-P", "standing-orders", "--", "true"] }
|
|
340
|
+
: macosFenceAvailable() ? macosFenced("true", [], fence, grants) : linuxFenced("true", [], fence, grants);
|
|
341
|
+
const sandbox = await execute(trivial.file, trivial.args, { cwd: worktree, env });
|
|
342
|
+
throw new Error(sandbox.code === 0 ? `The build fence blocks this checkout's own Git data. git ${args.join(" ")}: ${checked.stderr}` : `The provider sandbox could not run its control: ${sandbox.stderr}`);
|
|
343
|
+
}
|
|
344
|
+
if (checked.stdout !== control.stdout) throw new Error(`fenced git ${args.join(" ")} disagrees with its control`);
|
|
345
|
+
if (args[0] === "rev-parse") head = checked.stdout.trim();
|
|
346
|
+
}
|
|
347
|
+
return { head, revParse: skipped ? "unfenced-only" : "passed", status: skipped ? "unfenced-only" : "passed", provider };
|
|
348
|
+
} finally { if (codexHome !== null) await rm(codexHome, { recursive: true, force: true }); }
|
|
349
|
+
}
|
|
350
|
+
|
|
194
351
|
async function main() {
|
|
195
352
|
let options;
|
|
196
353
|
try {
|
|
@@ -253,10 +410,13 @@ async function main() {
|
|
|
253
410
|
`${JSON.stringify({ name: "standing-orders-provider-canary", private: true, type: "module", scripts: { test: "node --test" } }, null, 2)}\n`,
|
|
254
411
|
);
|
|
255
412
|
await writeFile(join(repo, "README.md"), "# Toolroll provider canary\n\nThis repository is disposable.\n");
|
|
256
|
-
await plantIsolationSentinels(repo, sentinels);
|
|
413
|
+
const planted = await plantIsolationSentinels(base, repo, sentinels);
|
|
414
|
+
// Every launch from here on (the CLI's planner and builder included) inherits the plugin sentinel.
|
|
415
|
+
process.env.CLAUDE_CODE_PLUGIN_DIRS = planted.plugin;
|
|
257
416
|
await git("add", ".");
|
|
258
417
|
await git("commit", "-qm", "seed provider canary");
|
|
259
418
|
const baseSha = await git("rev-parse", "HEAD");
|
|
419
|
+
await symlink(repo, join(base, "repo-alias"), "dir");
|
|
260
420
|
|
|
261
421
|
const checkBuild = async memoryMarker => {
|
|
262
422
|
|
|
@@ -284,13 +444,9 @@ async function main() {
|
|
|
284
444
|
"--as", "canary", "--token", password,
|
|
285
445
|
]);
|
|
286
446
|
|
|
287
|
-
await optInNamedTool(db, repo,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
...(memoryMarker === null ? [] : ["On its second line, report the TOOLROLL_MEMORY_CANARY marker only if automatic memory already supplied one; otherwise write NO_MEMORY. Do not read home files or search for memory. End that line with a newline."]),
|
|
291
|
-
"Add canary.test.js using node:test.",
|
|
292
|
-
"Only these files; no questions; acceptance evidence: check and changed-path only.",
|
|
293
|
-
].join(" ");
|
|
447
|
+
await optInNamedTool(db, repo, planted.server);
|
|
448
|
+
// A title is at most 200 characters, so the memory check says only what it must.
|
|
449
|
+
const title = canaryGoal(options.provider, memoryMarker);
|
|
294
450
|
await cli("file the task", ["task", "add", title, "--id", TASK_ID, "--repo", repo]);
|
|
295
451
|
let filedDigest = null;
|
|
296
452
|
if (options.autoApprove) {
|
|
@@ -313,6 +469,7 @@ async function main() {
|
|
|
313
469
|
"--as", "canary", "--token", password,
|
|
314
470
|
]);
|
|
315
471
|
if (options.autoApprove && requested.autoPlan !== true) throw new Error("the exact filed plan was not pre-authorized");
|
|
472
|
+
await tagLaunch(sentinels, "isolated-plan");
|
|
316
473
|
const planned = await cli("run the planning pass", [
|
|
317
474
|
"tick", "--runner", "canary-worker", "--token", runnerToken,
|
|
318
475
|
"--repo", repo, "--pool", pool,
|
|
@@ -336,10 +493,13 @@ async function main() {
|
|
|
336
493
|
"--as", "canary", "--token", password,
|
|
337
494
|
]);
|
|
338
495
|
|
|
496
|
+
await tagLaunch(sentinels, "isolated-fresh");
|
|
339
497
|
const built = await cli("run the build and evidence pass", [
|
|
340
498
|
"tick", "--runner", "canary-worker", "--token", runnerToken,
|
|
341
499
|
"--repo", repo, "--pool", pool,
|
|
342
500
|
]);
|
|
501
|
+
// Anything launched after the build is no isolated launch of this canary's: it would fire as "unknown".
|
|
502
|
+
await tagLaunch(sentinels, null);
|
|
343
503
|
if (!Array.isArray(built.dispatched) || !built.dispatched.some(item => item?.outcome === "built")) {
|
|
344
504
|
throw new Error(`builder did not complete the task: ${JSON.stringify(built)}`);
|
|
345
505
|
}
|
|
@@ -352,7 +512,9 @@ async function main() {
|
|
|
352
512
|
if (buildRun === null || buildRun === undefined || typeof buildRun.branch !== "string") {
|
|
353
513
|
throw new Error("no completed builder run named its branch");
|
|
354
514
|
}
|
|
515
|
+
const checkoutGit = await checkCheckoutGit({ provider: options.provider, worktree: buildRun.worktree, databaseFile: db, repository: repo });
|
|
355
516
|
const branchSha = await git("rev-parse", buildRun.branch);
|
|
517
|
+
if (checkoutGit !== null && checkoutGit.head !== branchSha) throw new Error("the fenced checkout is not the completed build candidate");
|
|
356
518
|
if (branchSha === baseSha) throw new Error("the build branch did not advance");
|
|
357
519
|
const canaryText = await git("show", `${buildRun.branch}:canary.txt`);
|
|
358
520
|
const expectedText = `standing-orders ${options.provider} canary passed${memoryMarker === null ? "" : "\nNO_MEMORY"}`;
|
|
@@ -376,7 +538,23 @@ async function main() {
|
|
|
376
538
|
if (await git("rev-parse", "main") !== baseSha) throw new Error("the default branch changed");
|
|
377
539
|
if (Number(await git("rev-list", "--count", `${baseSha}..${branchSha}`)) !== 1) throw new Error("expected exactly one work commit");
|
|
378
540
|
await assertRuntime();
|
|
379
|
-
const
|
|
541
|
+
const controls = [];
|
|
542
|
+
const isolatedLaunches = ["isolated-plan", "isolated-fresh"];
|
|
543
|
+
let resumed = null;
|
|
544
|
+
if (options.provider === "claude") {
|
|
545
|
+
resumed = await resumeThroughGateway({ db, repo, provider: options.provider, model: options.model, runnerToken, buildRunId: buildRun.id, sentinels });
|
|
546
|
+
isolatedLaunches.push("isolated-resumed");
|
|
547
|
+
// The control names its launch in its own environment; the launch file stays empty.
|
|
548
|
+
await tagLaunch(sentinels, null);
|
|
549
|
+
await claudeControl(repo, options.model, sentinels, planted.server);
|
|
550
|
+
controls.push("control-fresh", "control-resumed");
|
|
551
|
+
}
|
|
552
|
+
const fence = await (async () => {
|
|
553
|
+
const { openStore } = await import(join(root, "dist", "store.js"));
|
|
554
|
+
const store = openStore(db);
|
|
555
|
+
try { return store.runFence(buildRun.id)?.method ?? null; } finally { store.close(); }
|
|
556
|
+
})();
|
|
557
|
+
const isolation = { fence, launches: judgeIsolation(options.provider, await firedBy(planted.log), controls, isolatedLaunches, fence), resumedRun: resumed };
|
|
380
558
|
if (memoryMarker !== null) {
|
|
381
559
|
if (JSON.stringify(final).includes(memoryMarker)) throw new Error("Claude supplied the disposable automatic-memory marker to the task.");
|
|
382
560
|
isolation.autoMemory = "NO_MEMORY";
|
|
@@ -403,6 +581,7 @@ async function main() {
|
|
|
403
581
|
humanActionsAfterPlanningStarted: options.autoApprove ? 0 : 1,
|
|
404
582
|
filedScopeDigest: filedDigest,
|
|
405
583
|
worktreeBuild: "passed",
|
|
584
|
+
checkoutGit,
|
|
406
585
|
commit: branchSha,
|
|
407
586
|
verificationCommand: "node --test",
|
|
408
587
|
proofVerdict: final.proofVerdict,
|