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
package/dist/exec.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* command. And nothing here rejects: a failed command is a value. Discovery
|
|
7
7
|
* walks repos it has never seen, and one broken repo must not end the scan.
|
|
8
8
|
*/
|
|
9
|
+
import { type FenceGrants } from "./agent-fence.js";
|
|
9
10
|
import { type ProcessTreeObserver } from "./process-tree.js";
|
|
10
11
|
import { type ContainmentBackendId } from "./containment.js";
|
|
11
12
|
import type { Store } from "./store.js";
|
|
@@ -43,8 +44,28 @@ export type RunOptions = ProcessTreeObserver & {
|
|
|
43
44
|
* inside a sandbox that denies these paths. Applied here, at the one spawn
|
|
44
45
|
* road, so an injected test runner still sees the provider's own argv. */
|
|
45
46
|
fence?: readonly string[];
|
|
47
|
+
/** What the fence gives back beside those paths (agent-fence.ts FenceGrants). */
|
|
48
|
+
fenceGrants?: FenceGrants;
|
|
49
|
+
/** `required`: this spawn runs code a build controls, so it runs inside the OS fence or not at all (never unwrapped
|
|
50
|
+
* because no backend applies here). The caller passes the fence it must run in. */
|
|
51
|
+
confinement?: "required";
|
|
46
52
|
/** UTF-8 input for JSONL or buffered process-group transports; closed after delivery. */
|
|
47
53
|
stdin?: string;
|
|
54
|
+
/** One ACP conversation (runAcpSession only): the brief, the sealed model, and the agent the attestation names. */
|
|
55
|
+
acp?: AcpRun;
|
|
56
|
+
/** The conformance check's own admission (the gateway only; set by the check job and nothing else): this run is the
|
|
57
|
+
* request's current step, admitted by the request instead of an attestation. */
|
|
58
|
+
acpCheck?: {
|
|
59
|
+
requestId: number;
|
|
60
|
+
step: 1 | 2;
|
|
61
|
+
/** Step 1: the plane's isolation probes — a canary file in the fenced state, and a canary value the launching
|
|
62
|
+
* environment carries under a key every ACP launch sheds. */
|
|
63
|
+
isolation?: {
|
|
64
|
+
file: string;
|
|
65
|
+
fileCanary: string;
|
|
66
|
+
envValue: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
48
69
|
/**
|
|
49
70
|
* Absolute wall-clock ceiling. Keep this for bounded commands and repair
|
|
50
71
|
* turns. Long-running provider sessions use idleTimeoutMs instead: useful
|
|
@@ -161,6 +182,19 @@ export type RunOptions = ProcessTreeObserver & {
|
|
|
161
182
|
export declare class ContainmentRefusal extends Error {
|
|
162
183
|
readonly name = "ContainmentRefusal";
|
|
163
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The command as it runs inside the agent fence (sandbox-exec or bubblewrap wrapping it), or as it is when it has no
|
|
187
|
+
* fence. A spawn whose confinement is required and that no backend here can wrap is refused (ContainmentRefusal):
|
|
188
|
+
* nothing runs, and nothing falls back to running it unwrapped.
|
|
189
|
+
*/
|
|
190
|
+
export declare function confined(file: string, args: readonly string[], bag: {
|
|
191
|
+
fence?: readonly string[] | undefined;
|
|
192
|
+
fenceGrants?: FenceGrants | undefined;
|
|
193
|
+
confinement?: "required" | undefined;
|
|
194
|
+
}): {
|
|
195
|
+
file: string;
|
|
196
|
+
args: string[];
|
|
197
|
+
};
|
|
164
198
|
/** Run IDs are local to a database. Connections to the same file share
|
|
165
199
|
* custody; separate files and separate in-memory databases never do. */
|
|
166
200
|
export declare function runOwnerTag(store: Store, runId: number): string;
|
|
@@ -246,6 +280,53 @@ export declare function runStreamJsonl(file: string, args: readonly string[], op
|
|
|
246
280
|
* semantics match `runStreamJsonl`.
|
|
247
281
|
*/
|
|
248
282
|
export declare function runGeminiStreamJsonl(file: string, args: readonly string[], options?: RunOptions): Promise<ExecResult>;
|
|
283
|
+
/** What runAcpSession needs beyond a process: the conversation's terms (acp-client.ts). */
|
|
284
|
+
export type AcpRun = {
|
|
285
|
+
prompt: string;
|
|
286
|
+
model: string;
|
|
287
|
+
/** The agent's name and version as its attestation recorded them; the handshake must say the same. */
|
|
288
|
+
expect: {
|
|
289
|
+
name: string;
|
|
290
|
+
version: string;
|
|
291
|
+
} | null;
|
|
292
|
+
/** Handed the conversation's controls once it starts (the conformance probe cancels with it). */
|
|
293
|
+
onControl?: (control: AcpControl) => void;
|
|
294
|
+
/** Each handshake step's deadline (ACP_HANDSHAKE_MS unless a test shortens it). */
|
|
295
|
+
handshakeMs?: number;
|
|
296
|
+
};
|
|
297
|
+
/** What a caller (the conformance check) may do with a running ACP conversation. */
|
|
298
|
+
export type AcpControl = {
|
|
299
|
+
/** Ask the agent to stop the prompt in flight; unanswered within `deadlineMs`, the agent is stopped and the
|
|
300
|
+
* conversation fails (a check's cancel must be answered in time). */
|
|
301
|
+
cancel: (deadlineMs?: number) => void;
|
|
302
|
+
/** A fresh, plane-side reading, now, of what the agent itself started and is still running: the live (never zombie)
|
|
303
|
+
* processes beneath the agent's own process — below any wrapper the fence put around it — with their process
|
|
304
|
+
* groups. Null when it can't be read here (Windows). */
|
|
305
|
+
liveDescendants: () => {
|
|
306
|
+
pid: number;
|
|
307
|
+
group: number;
|
|
308
|
+
}[] | null;
|
|
309
|
+
};
|
|
310
|
+
/** How long each shutdown step waits: session/close's answer, then exit after stdin closes, then exit after SIGTERM. */
|
|
311
|
+
export declare const ACP_SHUTDOWN_MS: {
|
|
312
|
+
readonly close: 2000;
|
|
313
|
+
readonly eof: 5000;
|
|
314
|
+
readonly term: 5000;
|
|
315
|
+
};
|
|
316
|
+
/** How long each handshake step (initialize, session/new, choosing the model) may take before the agent is stopped. */
|
|
317
|
+
export declare const ACP_HANDSHAKE_MS = 30000;
|
|
318
|
+
/**
|
|
319
|
+
* One ACP build conversation over the agent's stdio (docs/plans/acp-adapter.md § Gateway, runner, custody). The
|
|
320
|
+
* conversation itself is acp-client.ts; this owns the process: the same contained spawn, ownership, descendant
|
|
321
|
+
* observation, container settlement and watchdog every other runner uses, with both clocks at once.
|
|
322
|
+
*
|
|
323
|
+
* After the prompt is answered it shuts the agent down in bounded steps — session/close (when offered), stdin EOF,
|
|
324
|
+
* SIGTERM to the group, then the forced kill — and returns only when the process has settled, so the gateway's
|
|
325
|
+
* witness records the real exit. A protocol failure kills at once. A stop, from the gateway's stop watch, kills at
|
|
326
|
+
* any step. The result's stdout is the conversation's retained record, never the agent's stream; its code is 0 only
|
|
327
|
+
* when the prompt was answered end_turn and the agent then exited cleanly or on our own signal.
|
|
328
|
+
*/
|
|
329
|
+
export declare function runAcpSession(file: string, args: readonly string[], options?: RunOptions): Promise<ExecResult>;
|
|
249
330
|
/**
|
|
250
331
|
* A live session's account so far (one main result per turn, checked on Claude Code 2.1.290): the latest result's
|
|
251
332
|
* session, ending, words and cost — `total_cost_usd` is already the session's running total — with the usage token
|
package/dist/exec.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* command. And nothing here rejects: a failed command is a value. Discovery
|
|
7
7
|
* walks repos it has never seen, and one broken repo must not end the scan.
|
|
8
8
|
*/
|
|
9
|
+
import { ACP_LINE_CAP, createAcpConversation } from "./acp-client.js";
|
|
9
10
|
import { linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced } from "./agent-fence.js";
|
|
10
|
-
import { execFile, spawn } from "node:child_process";
|
|
11
|
-
import { realpathSync } from "node:fs";
|
|
11
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
12
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
12
14
|
import { observeProcessTree, stopProcessTree } from "./process-tree.js";
|
|
13
15
|
import { jsonlDiscriminants } from "./jsonl-discriminants.js";
|
|
14
16
|
import { createContainer, currentContainment } from "./containment.js";
|
|
@@ -45,6 +47,24 @@ const containers = new WeakMap();
|
|
|
45
47
|
export class ContainmentRefusal extends Error {
|
|
46
48
|
name = "ContainmentRefusal";
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The command as it runs inside the agent fence (sandbox-exec or bubblewrap wrapping it), or as it is when it has no
|
|
52
|
+
* fence. A spawn whose confinement is required and that no backend here can wrap is refused (ContainmentRefusal):
|
|
53
|
+
* nothing runs, and nothing falls back to running it unwrapped.
|
|
54
|
+
*/
|
|
55
|
+
export function confined(file, args, bag) {
|
|
56
|
+
const fence = bag.fence ?? [];
|
|
57
|
+
if (fence.length > 0 && macosFenceAvailable())
|
|
58
|
+
return macosFenced(file, args, fence, bag.fenceGrants);
|
|
59
|
+
if (fence.length > 0 && linuxFenceAvailable())
|
|
60
|
+
return linuxFenced(file, args, fence, bag.fenceGrants);
|
|
61
|
+
if (bag.confinement === "required") {
|
|
62
|
+
throw new ContainmentRefusal(fence.length === 0
|
|
63
|
+
? "This process must run inside the agent fence, and it was given none."
|
|
64
|
+
: "This machine can't confine a build (no macOS sandbox, no working bubblewrap), so nothing was started.");
|
|
65
|
+
}
|
|
66
|
+
return { file, args: [...args] };
|
|
67
|
+
}
|
|
48
68
|
/** A spawn callback refused custody AFTER the child existed: the caller
|
|
49
69
|
* must reap exactly this child before answering. */
|
|
50
70
|
class SpawnCustodyFailure extends Error {
|
|
@@ -72,10 +92,7 @@ function rejectedChild(error, child) {
|
|
|
72
92
|
*/
|
|
73
93
|
function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
74
94
|
// The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
|
|
75
|
-
|
|
76
|
-
({ file, args } = macosFenced(file, args, bag.fence));
|
|
77
|
-
else if (bag.fence !== undefined && bag.fence.length > 0 && linuxFenceAvailable())
|
|
78
|
-
({ file, args } = linuxFenced(file, args, bag.fence));
|
|
95
|
+
({ file, args } = confined(file, args, bag));
|
|
79
96
|
// The policy refusal comes first: nothing is reserved, recorded or
|
|
80
97
|
// spawned for a spawn that cannot be contained as required.
|
|
81
98
|
const effective = contain ? currentContainment() : null;
|
|
@@ -224,7 +241,7 @@ function containmentOf(container, empty, attachFailure) {
|
|
|
224
241
|
}
|
|
225
242
|
/** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
|
|
226
243
|
function spawnStream(file, args, options, stdio, childEnv) {
|
|
227
|
-
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
|
|
244
|
+
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence, fenceGrants: options.fenceGrants, confinement: options.confinement }, options.processGroup === true);
|
|
228
245
|
}
|
|
229
246
|
/** The refusal-before-spawn shape shared by every containable transport. */
|
|
230
247
|
function refusedResult(error) {
|
|
@@ -521,6 +538,8 @@ export function run(file, args, options = {}) {
|
|
|
521
538
|
...(options.onStdout === undefined ? {} : { onStdout: options.onStdout }),
|
|
522
539
|
...(options.onStderr === undefined ? {} : { onStderr: options.onStderr }),
|
|
523
540
|
...(options.fence === undefined ? {} : { fence: options.fence }),
|
|
541
|
+
...(options.fenceGrants === undefined ? {} : { fenceGrants: options.fenceGrants }),
|
|
542
|
+
...(options.confinement === undefined ? {} : { confinement: options.confinement }),
|
|
524
543
|
...(options.onDescendant === undefined ? {} : { onDescendant: options.onDescendant }),
|
|
525
544
|
...(options.onDescendantWriteFailure === undefined ? {} : { onDescendantWriteFailure: options.onDescendantWriteFailure }),
|
|
526
545
|
...(options.onDescendantExit === undefined ? {} : { onDescendantExit: options.onDescendantExit }),
|
|
@@ -530,8 +549,19 @@ export function run(file, args, options = {}) {
|
|
|
530
549
|
...(options.onContainerEmpty === undefined ? {} : { onContainerEmpty: options.onContainerEmpty }),
|
|
531
550
|
}));
|
|
532
551
|
}
|
|
552
|
+
// The plain road wraps exactly as the process-group road does: a fence given here is never dropped, and a spawn
|
|
553
|
+
// whose confinement is required and can't be wrapped refuses before anything runs.
|
|
554
|
+
let wrapped;
|
|
555
|
+
try {
|
|
556
|
+
wrapped = confined(file, args, options);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
if (!(error instanceof ContainmentRefusal))
|
|
560
|
+
throw error;
|
|
561
|
+
return Promise.resolve({ code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: error.message, timedOut: false, notFound: false, containment: { refused: error.message } });
|
|
562
|
+
}
|
|
533
563
|
return retryTransientSpawn(() => new Promise(resolve => {
|
|
534
|
-
execFile(file,
|
|
564
|
+
execFile(wrapped.file, wrapped.args, {
|
|
535
565
|
cwd,
|
|
536
566
|
timeout: timeoutMs,
|
|
537
567
|
maxBuffer,
|
|
@@ -563,7 +593,7 @@ function runBufferedGroup(file, args, bag) {
|
|
|
563
593
|
let container = null;
|
|
564
594
|
let attached;
|
|
565
595
|
try {
|
|
566
|
-
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
|
|
596
|
+
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence, fenceGrants: bag.fenceGrants, confinement: bag.confinement });
|
|
567
597
|
child = spawned.child;
|
|
568
598
|
container = spawned.container;
|
|
569
599
|
attached = spawned.attached;
|
|
@@ -1402,6 +1432,335 @@ export function runGeminiStreamJsonl(file, args, options = {}) {
|
|
|
1402
1432
|
child.on("close", code => finish(code));
|
|
1403
1433
|
});
|
|
1404
1434
|
}
|
|
1435
|
+
/** One process listing: ids, group, state and command name only (never arguments — they can carry a brief). */
|
|
1436
|
+
function processRows() {
|
|
1437
|
+
try {
|
|
1438
|
+
return execFileSync("/bin/ps", ["-axo", "pid=,ppid=,pgid=,stat=,comm="], { encoding: "utf8", timeout: 2_000, maxBuffer: 8 * 1024 * 1024 })
|
|
1439
|
+
.split("\n").flatMap(line => {
|
|
1440
|
+
const parts = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
1441
|
+
return parts === null ? [] : [{ pid: Number(parts[1]), parent: Number(parts[2]), group: Number(parts[3]), state: parts[4], name: parts[5].trim() }];
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
catch {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* What the agent started and is still running: from `root` (the spawned process — the agent itself, or a fence
|
|
1450
|
+
* wrapper around it), down to the agent's own process (the first whose command is the agent's launch, `names`), then
|
|
1451
|
+
* every live, non-zombie process beneath it, with its group. The agent itself and any wrapper are never counted.
|
|
1452
|
+
*/
|
|
1453
|
+
function liveUnderAgent(root, names) {
|
|
1454
|
+
const rows = processRows();
|
|
1455
|
+
if (rows === null)
|
|
1456
|
+
return null;
|
|
1457
|
+
const children = new Map();
|
|
1458
|
+
for (const row of rows)
|
|
1459
|
+
children.set(row.parent, [...(children.get(row.parent) ?? []), row]);
|
|
1460
|
+
const self = rows.find(row => row.pid === root);
|
|
1461
|
+
const named = (row) => names.some(name => row.name === name || row.name.endsWith(`/${name}`));
|
|
1462
|
+
// The agent: the root when it is the agent (macOS sandbox-exec execs in place), else the first named descendant.
|
|
1463
|
+
let agent = self !== undefined && named(self) ? root : null;
|
|
1464
|
+
const pending = [...(children.get(root) ?? [])];
|
|
1465
|
+
while (agent === null && pending.length > 0) {
|
|
1466
|
+
const row = pending.shift();
|
|
1467
|
+
if (named(row))
|
|
1468
|
+
agent = row.pid;
|
|
1469
|
+
else
|
|
1470
|
+
pending.push(...(children.get(row.pid) ?? []));
|
|
1471
|
+
}
|
|
1472
|
+
if (agent === null)
|
|
1473
|
+
return [];
|
|
1474
|
+
const found = [];
|
|
1475
|
+
const below = [...(children.get(agent) ?? [])];
|
|
1476
|
+
while (below.length > 0) {
|
|
1477
|
+
const row = below.pop();
|
|
1478
|
+
if (!row.state.startsWith("Z"))
|
|
1479
|
+
found.push({ pid: row.pid, group: row.group });
|
|
1480
|
+
below.push(...(children.get(row.pid) ?? []));
|
|
1481
|
+
}
|
|
1482
|
+
return found;
|
|
1483
|
+
}
|
|
1484
|
+
/** How long each shutdown step waits: session/close's answer, then exit after stdin closes, then exit after SIGTERM. */
|
|
1485
|
+
export const ACP_SHUTDOWN_MS = { close: 2_000, eof: 5_000, term: 5_000 };
|
|
1486
|
+
/** How long each handshake step (initialize, session/new, choosing the model) may take before the agent is stopped. */
|
|
1487
|
+
export const ACP_HANDSHAKE_MS = 30_000;
|
|
1488
|
+
/** After the agent itself has exited, how long its pipes may stay open (a descendant holding them) before they are closed. */
|
|
1489
|
+
const ACP_PIPES_AFTER_EXIT_MS = 5_000;
|
|
1490
|
+
/** SIGTERM the child's process group (POSIX) or the child alone; a container's own stop is the forced one, later. */
|
|
1491
|
+
function termGroup(child) {
|
|
1492
|
+
const pid = child.pid;
|
|
1493
|
+
try {
|
|
1494
|
+
if (pid !== undefined && process.platform !== "win32")
|
|
1495
|
+
process.kill(-pid, "SIGTERM");
|
|
1496
|
+
else
|
|
1497
|
+
child.kill("SIGTERM");
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// Already gone.
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* One ACP build conversation over the agent's stdio (docs/plans/acp-adapter.md § Gateway, runner, custody). The
|
|
1505
|
+
* conversation itself is acp-client.ts; this owns the process: the same contained spawn, ownership, descendant
|
|
1506
|
+
* observation, container settlement and watchdog every other runner uses, with both clocks at once.
|
|
1507
|
+
*
|
|
1508
|
+
* After the prompt is answered it shuts the agent down in bounded steps — session/close (when offered), stdin EOF,
|
|
1509
|
+
* SIGTERM to the group, then the forced kill — and returns only when the process has settled, so the gateway's
|
|
1510
|
+
* witness records the real exit. A protocol failure kills at once. A stop, from the gateway's stop watch, kills at
|
|
1511
|
+
* any step. The result's stdout is the conversation's retained record, never the agent's stream; its code is 0 only
|
|
1512
|
+
* when the prompt was answered end_turn and the agent then exited cleanly or on our own signal.
|
|
1513
|
+
*/
|
|
1514
|
+
export function runAcpSession(file, args, options = {}) {
|
|
1515
|
+
const acp = options.acp;
|
|
1516
|
+
if (acp === undefined)
|
|
1517
|
+
return Promise.resolve({ code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: "an ACP run needs its conversation terms", timedOut: false, notFound: false });
|
|
1518
|
+
const childEnv = resolveChildEnv(options);
|
|
1519
|
+
return new Promise(resolve => {
|
|
1520
|
+
let child;
|
|
1521
|
+
let container = null;
|
|
1522
|
+
let attachFailure = null;
|
|
1523
|
+
try {
|
|
1524
|
+
const spawned = spawnStream(file, args, options, ["pipe", "pipe", "pipe"], childEnv);
|
|
1525
|
+
child = spawned.child;
|
|
1526
|
+
container = spawned.container;
|
|
1527
|
+
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
1528
|
+
attachFailure = outcome.detail; });
|
|
1529
|
+
}
|
|
1530
|
+
catch (error) {
|
|
1531
|
+
void reapRejectedSpawn(rejectedChild(error, child), options.processGroup === true).then(() => resolve(refusedResult(error)));
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (options.processGroup === true)
|
|
1535
|
+
liveProviders.add(child);
|
|
1536
|
+
registerOwned(options.owner, child);
|
|
1537
|
+
if (options.processGroup === true && container === null)
|
|
1538
|
+
observeProcessTree(child, options);
|
|
1539
|
+
const conversation = createAcpConversation({ cwd: options.cwd ?? process.cwd(), prompt: acp.prompt, model: acp.model, expect: acp.expect });
|
|
1540
|
+
// The agent's own process, as a listing names it: the launched file, or the script a pinned interpreter runs.
|
|
1541
|
+
const agentNames = [...new Set([file, ...args.slice(0, 2)].filter(one => one.startsWith("/")).map(one => one.split("/").pop()).filter(one => one !== ""))];
|
|
1542
|
+
let stderr = "";
|
|
1543
|
+
let timedOut = false;
|
|
1544
|
+
let notFound = false;
|
|
1545
|
+
let settled = false;
|
|
1546
|
+
let shuttingDown = false;
|
|
1547
|
+
/** The signals the plane itself sent after the prompt was answered, by its own shutdown ladder: an exit by one of
|
|
1548
|
+
* those is clean. A stop or a timeout kills through other roads and never sets these. */
|
|
1549
|
+
let termAfterTerminal = false;
|
|
1550
|
+
let killAfterTerminal = false;
|
|
1551
|
+
/** How the process came to end, for the record: on its own (before or after stdin closed), or by our signal. */
|
|
1552
|
+
let endedBy = "self";
|
|
1553
|
+
let announced = null;
|
|
1554
|
+
const timers = [];
|
|
1555
|
+
const later = (ms, fn) => { const timer = setTimeout(fn, ms); timer.unref?.(); timers.push(timer); };
|
|
1556
|
+
const write = (lines) => {
|
|
1557
|
+
for (const one of lines) {
|
|
1558
|
+
if (child.stdin === null || child.stdin.destroyed || child.stdin.writableEnded)
|
|
1559
|
+
return;
|
|
1560
|
+
try {
|
|
1561
|
+
child.stdin.write(`${one}\n`);
|
|
1562
|
+
}
|
|
1563
|
+
catch { /* the agent closed its stdin: its exit decides */ }
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
const forceKill = () => {
|
|
1567
|
+
endedBy = "kill";
|
|
1568
|
+
if (options.processGroup === true)
|
|
1569
|
+
killGroup(child);
|
|
1570
|
+
else
|
|
1571
|
+
child.kill("SIGKILL");
|
|
1572
|
+
};
|
|
1573
|
+
/** Each handshake step has its own deadline, beside the run's clocks. */
|
|
1574
|
+
let handshakePhase = null;
|
|
1575
|
+
const armHandshake = () => {
|
|
1576
|
+
const phase = conversation.phase();
|
|
1577
|
+
if (phase === handshakePhase || (phase !== "initialize" && phase !== "session" && phase !== "model"))
|
|
1578
|
+
return;
|
|
1579
|
+
handshakePhase = phase;
|
|
1580
|
+
const limit = acp.handshakeMs ?? ACP_HANDSHAKE_MS;
|
|
1581
|
+
later(limit, () => {
|
|
1582
|
+
if (settled || conversation.phase() !== phase)
|
|
1583
|
+
return;
|
|
1584
|
+
conversation.refuse(`the agent took more than ${limit / 1000} s to answer ${phase === "initialize" ? "initialize" : phase === "session" ? "session/new" : "the model choice"}`);
|
|
1585
|
+
forceKill();
|
|
1586
|
+
});
|
|
1587
|
+
};
|
|
1588
|
+
const exited = () => child.exitCode !== null || child.signalCode !== null;
|
|
1589
|
+
const shutdown = () => {
|
|
1590
|
+
if (shuttingDown)
|
|
1591
|
+
return;
|
|
1592
|
+
shuttingDown = true;
|
|
1593
|
+
if (conversation.phase() === "failed") {
|
|
1594
|
+
forceKill();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const endInput = () => {
|
|
1598
|
+
if (exited())
|
|
1599
|
+
return;
|
|
1600
|
+
try {
|
|
1601
|
+
child.stdin?.end();
|
|
1602
|
+
}
|
|
1603
|
+
catch { /* gone */ }
|
|
1604
|
+
endedBy = "eof";
|
|
1605
|
+
later(ACP_SHUTDOWN_MS.eof, () => {
|
|
1606
|
+
if (exited())
|
|
1607
|
+
return;
|
|
1608
|
+
termAfterTerminal = conversation.phase() === "ended";
|
|
1609
|
+
endedBy = "term";
|
|
1610
|
+
termGroup(child);
|
|
1611
|
+
later(ACP_SHUTDOWN_MS.term, () => {
|
|
1612
|
+
if (exited())
|
|
1613
|
+
return;
|
|
1614
|
+
killAfterTerminal = conversation.phase() === "ended";
|
|
1615
|
+
forceKill();
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
};
|
|
1619
|
+
const close = conversation.close();
|
|
1620
|
+
if (close.length === 0) {
|
|
1621
|
+
endInput();
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
write(close);
|
|
1625
|
+
const started = Date.now();
|
|
1626
|
+
const waitClose = () => {
|
|
1627
|
+
if (conversation.closeAnswered() || Date.now() - started >= ACP_SHUTDOWN_MS.close || exited())
|
|
1628
|
+
endInput();
|
|
1629
|
+
else
|
|
1630
|
+
later(50, waitClose);
|
|
1631
|
+
};
|
|
1632
|
+
waitClose();
|
|
1633
|
+
};
|
|
1634
|
+
const onLine = (framed) => {
|
|
1635
|
+
if (settled)
|
|
1636
|
+
return;
|
|
1637
|
+
armHandshake();
|
|
1638
|
+
const reaction = framed.overflowed
|
|
1639
|
+
? (conversation.refuse("the agent wrote a line too long to read"), { send: [], live: [] })
|
|
1640
|
+
: conversation.receive(framed.prefix.toString("utf8"));
|
|
1641
|
+
write(reaction.send);
|
|
1642
|
+
const id = conversation.sessionId();
|
|
1643
|
+
if (id !== null && announced === null) {
|
|
1644
|
+
announced = id;
|
|
1645
|
+
try {
|
|
1646
|
+
options.onSessionId?.(id);
|
|
1647
|
+
}
|
|
1648
|
+
catch { /* a registry that cannot be written must not kill the turn */ }
|
|
1649
|
+
}
|
|
1650
|
+
for (const update of reaction.live) {
|
|
1651
|
+
try {
|
|
1652
|
+
options.onStreamEvent?.({ acp: update });
|
|
1653
|
+
}
|
|
1654
|
+
catch { /* observational only */ }
|
|
1655
|
+
}
|
|
1656
|
+
const phase = conversation.phase();
|
|
1657
|
+
if (phase === "failed" || phase === "ended")
|
|
1658
|
+
shutdown();
|
|
1659
|
+
else
|
|
1660
|
+
armHandshake();
|
|
1661
|
+
};
|
|
1662
|
+
const framing = boundedJsonlFramer(ACP_LINE_CAP, onLine);
|
|
1663
|
+
const watchdog = streamWatchdog(options, () => {
|
|
1664
|
+
timedOut = true;
|
|
1665
|
+
forceKill();
|
|
1666
|
+
});
|
|
1667
|
+
child.stdout?.on("data", (chunk) => {
|
|
1668
|
+
watchdog.touch();
|
|
1669
|
+
framing.push(chunk);
|
|
1670
|
+
});
|
|
1671
|
+
child.stderr?.setEncoding("utf8");
|
|
1672
|
+
child.stderr?.on("data", (chunk) => {
|
|
1673
|
+
watchdog.touch();
|
|
1674
|
+
if (stderr.length < JSONL_STDERR_CAP)
|
|
1675
|
+
stderr += chunk.slice(0, JSONL_STDERR_CAP - stderr.length);
|
|
1676
|
+
});
|
|
1677
|
+
child.stdin?.on("error", () => { });
|
|
1678
|
+
write(conversation.start());
|
|
1679
|
+
armHandshake();
|
|
1680
|
+
try {
|
|
1681
|
+
acp.onControl?.({
|
|
1682
|
+
cancel: (deadlineMs) => {
|
|
1683
|
+
write(conversation.cancel());
|
|
1684
|
+
if (deadlineMs !== undefined)
|
|
1685
|
+
later(deadlineMs, () => {
|
|
1686
|
+
if (settled || conversation.terminal() !== null)
|
|
1687
|
+
return;
|
|
1688
|
+
conversation.refuse(`the agent didn't answer the cancel within ${Math.round(deadlineMs / 1000)} s`);
|
|
1689
|
+
forceKill();
|
|
1690
|
+
});
|
|
1691
|
+
},
|
|
1692
|
+
liveDescendants: () => process.platform === "win32" ? null : exited() || child.pid === undefined ? [] : liveUnderAgent(child.pid, agentNames),
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
catch { /* the caller's own failure */ }
|
|
1696
|
+
// The moment the agent itself exits, what it left in its own group ends with it — not at "close", which a
|
|
1697
|
+
// descendant holding the agent's pipes can postpone forever; and if those pipes are still open shortly after, they
|
|
1698
|
+
// are closed, so settlement never waits on a stray. Escaped groups are the custody observer's, as for every runner.
|
|
1699
|
+
let leftover = false;
|
|
1700
|
+
child.on("exit", () => {
|
|
1701
|
+
if (options.processGroup === true && container === null && child.pid !== undefined && process.platform !== "win32") {
|
|
1702
|
+
try {
|
|
1703
|
+
process.kill(-child.pid, 0);
|
|
1704
|
+
leftover = true;
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
leftover = false;
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1711
|
+
}
|
|
1712
|
+
catch { /* none left */ }
|
|
1713
|
+
}
|
|
1714
|
+
later(ACP_PIPES_AFTER_EXIT_MS, () => {
|
|
1715
|
+
if (settled)
|
|
1716
|
+
return;
|
|
1717
|
+
for (const pipe of [child.stdout, child.stderr, child.stdin]) {
|
|
1718
|
+
try {
|
|
1719
|
+
pipe?.destroy();
|
|
1720
|
+
}
|
|
1721
|
+
catch { /* gone */ }
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
const finish = (code, signal) => {
|
|
1726
|
+
if (settled)
|
|
1727
|
+
return;
|
|
1728
|
+
// The last record, even one with no newline after it, is read before anything is decided.
|
|
1729
|
+
framing.finish();
|
|
1730
|
+
settled = true;
|
|
1731
|
+
for (const timer of timers)
|
|
1732
|
+
clearTimeout(timer);
|
|
1733
|
+
watchdog.stop();
|
|
1734
|
+
liveProviders.delete(child);
|
|
1735
|
+
// Whatever the agent started in its own group and left running ends with it (a group id can't be reused while any
|
|
1736
|
+
// member lives, so this reaches only what it left). Escaped groups are the custody observer's, as for every runner.
|
|
1737
|
+
const terminal = conversation.terminal();
|
|
1738
|
+
const answered = terminal !== null && "stopReason" in terminal && terminal.stopReason === "end_turn" && conversation.failure() === null;
|
|
1739
|
+
// A clean exit after the answer, or an exit by the plane's own post-answer signal (its shutdown ladder's SIGTERM,
|
|
1740
|
+
// then SIGKILL), is success; anything else — a stop, a timeout, another signal — is its code.
|
|
1741
|
+
const ownSignal = (termAfterTerminal && (signal === "SIGTERM" || code === 143)) || (killAfterTerminal && (signal === "SIGKILL" || code === 137));
|
|
1742
|
+
const exitCode = answered && !timedOut && (code === 0 || ownSignal) ? 0 : (code ?? 1);
|
|
1743
|
+
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
1744
|
+
void settlement.then(empty => resolve({
|
|
1745
|
+
code: notFound ? NOT_FOUND_CODE : timedOut ? TIMEOUT_CODE : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : exitCode,
|
|
1746
|
+
// The runner's own account of the ending rides last: a type no agent can emit.
|
|
1747
|
+
stdout: `${conversation.retained()}${JSON.stringify({ type: "acp.exit", how: endedBy, leftover: leftover || empty === false, timedOut })}\n`,
|
|
1748
|
+
stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
|
|
1749
|
+
timedOut,
|
|
1750
|
+
notFound,
|
|
1751
|
+
...containmentOf(container, empty, attachFailure),
|
|
1752
|
+
}));
|
|
1753
|
+
};
|
|
1754
|
+
child.on("error", error => {
|
|
1755
|
+
endedBy = "error";
|
|
1756
|
+
notFound = error.code === "ENOENT";
|
|
1757
|
+
if (stderr === "")
|
|
1758
|
+
stderr = String(error);
|
|
1759
|
+
finish(null, null);
|
|
1760
|
+
});
|
|
1761
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1405
1764
|
const TRUNCATION_MARKER = "…[truncated]";
|
|
1406
1765
|
/**
|
|
1407
1766
|
* One serialized synthetic line, guaranteed ≤ JSONL_LINE_CAP in UTF-8
|
package/dist/export-ui.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Settings → Data (v105): an instance operator downloads everything Standing
|
|
3
3
|
* Orders knows as one .zip. One form: the password, typed again, and a button.
|
|
4
4
|
*/
|
|
5
|
-
import { html, postForm } from "./html.js";
|
|
5
|
+
import { html, postForm, reauth } from "./html.js";
|
|
6
6
|
export const EXPORT_CSS = `.data-export{max-width:720px;min-width:0}.data-export label{display:grid;gap:4px;margin:14px 0 0;font-size:.875rem}` +
|
|
7
7
|
`.data-export input[type=password]{width:100%;max-width:360px;box-sizing:border-box}.data-export button[type=submit]{margin-top:14px;min-height:44px;white-space:nowrap}`;
|
|
8
8
|
export function dataExportHtml(notice) {
|
|
9
9
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : "";
|
|
10
|
-
return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label
|
|
10
|
+
return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Download export</button>`)}<p class="meta">The same export from the command line: <code>toolroll export --out <path> [--zip]</code></p></section>`;
|
|
11
11
|
}
|
package/dist/flow-code.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Runner } from "./backend.js";
|
|
2
2
|
import type { FlowScriptRow } from "./store.js";
|
|
3
|
+
import { projectFile } from "./project-file.js";
|
|
4
|
+
export { projectFile };
|
|
3
5
|
export { LANGUAGE_WORDS, SCRIPT_LANGUAGES, type ScriptLanguage } from "./flows.js";
|
|
4
6
|
/** A step's result on the card: what the script printed, up to what a step passes on (the end, when longer; the whole
|
|
5
7
|
* is in the step's log). */
|
|
6
8
|
export declare const CODE_OUTPUT_CHARS: 12000;
|
|
7
|
-
/** A project file a script names, if it is one: inside the project, and there. */
|
|
8
|
-
export declare function projectFile(root: string, path: string): string | null;
|
|
9
9
|
export type CodeRun = {
|
|
10
10
|
script: FlowScriptRow;
|
|
11
11
|
/** Where it runs, and the project folder a file script is found in. */
|
package/dist/flow-code.js
CHANGED
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
* keys are out of reach), under a time limit, with the setup's plain
|
|
16
16
|
* environment rather than this process's.
|
|
17
17
|
*/
|
|
18
|
-
import { chmodSync, existsSync, mkdirSync,
|
|
18
|
+
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { randomBytes } from "node:crypto";
|
|
20
|
-
import {
|
|
20
|
+
import { join } from "node:path";
|
|
21
21
|
import { SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
|
|
22
22
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
23
23
|
import { scrubSecrets } from "./flow-secrets.js";
|
|
24
24
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
25
|
+
import { projectFile } from "./project-file.js";
|
|
26
|
+
export { projectFile };
|
|
25
27
|
import { LANGUAGE_WORDS } from "./flows.js";
|
|
26
28
|
export { LANGUAGE_WORDS, SCRIPT_LANGUAGES } from "./flows.js";
|
|
27
29
|
const EXTENSION = { shell: "sh", python: "py", node: "mjs" };
|
|
@@ -33,19 +35,6 @@ const LOG_CHARS = 64_000;
|
|
|
33
35
|
function runtimeOf(language, file) {
|
|
34
36
|
return language === "python" ? { file: "python3", args: [file] } : language === "node" ? { file: process.execPath, args: [file] } : { file: "/bin/sh", args: [file] };
|
|
35
37
|
}
|
|
36
|
-
/** A project file a script names, if it is one: inside the project, and there. */
|
|
37
|
-
export function projectFile(root, path) {
|
|
38
|
-
if (path === "" || isAbsolute(path) || path.split(/[\\/]/).includes(".."))
|
|
39
|
-
return null;
|
|
40
|
-
try {
|
|
41
|
-
const base = realpathSync(root), full = realpathSync(join(root, path));
|
|
42
|
-
const inside = relative(base, full);
|
|
43
|
-
return inside !== "" && !inside.startsWith("..") && !isAbsolute(inside) && statSync(full).isFile() ? full : null;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
38
|
/** What it printed, without key-shaped lines or saved secrets, and the goto line taken off the end. */
|
|
50
39
|
export function codeOutput(stdout, secrets) {
|
|
51
40
|
const clean = scrubSecrets(stdout.replace(/\r\n?/g, "\n"), secrets);
|