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,124 @@
|
|
|
1
|
+
import { type Socket } from "node:net";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export declare const CONSOLE_INSTANCE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS console_instance (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n secret TEXT NOT NULL,\n pid INTEGER NOT NULL,\n port INTEGER NOT NULL,\n build TEXT NOT NULL,\n socket TEXT NOT NULL,\n started_at TEXT NOT NULL\n);";
|
|
4
|
+
/**
|
|
5
|
+
* The socket's own owner-only directory beside the database's REAL file (a symlinked database resolves to the one
|
|
6
|
+
* folder every spelling shares). Each console start binds a socket of its own, under a fresh random name recorded
|
|
7
|
+
* with its secret: no console ever needs to remove — or can remove — another console's socket.
|
|
8
|
+
*/
|
|
9
|
+
/** Toolroll's own name, so it never collides with a project or worktree that happens to sit beside the database. */
|
|
10
|
+
export declare const INSTANCE_DIR = ".toolroll-run";
|
|
11
|
+
export declare function instanceRunDir(databaseFile: string): string;
|
|
12
|
+
/** The console's own answer to a challenge over HTTP (an edge route, no sign-in: it reveals nothing). */
|
|
13
|
+
export declare const INSTANCE_PATH = "/__instance";
|
|
14
|
+
export type InstanceRecord = {
|
|
15
|
+
secret: string;
|
|
16
|
+
pid: number;
|
|
17
|
+
port: number;
|
|
18
|
+
build: string;
|
|
19
|
+
socket: string;
|
|
20
|
+
startedAt: string;
|
|
21
|
+
};
|
|
22
|
+
export type HelloFacts = {
|
|
23
|
+
pid: number;
|
|
24
|
+
build: string;
|
|
25
|
+
database: string;
|
|
26
|
+
port: number;
|
|
27
|
+
};
|
|
28
|
+
/** The record as stored: none, one this build can't read (never taken for none), or the record. */
|
|
29
|
+
export declare function readInstanceState(store: Store): {
|
|
30
|
+
state: "none";
|
|
31
|
+
} | {
|
|
32
|
+
state: "invalid";
|
|
33
|
+
} | {
|
|
34
|
+
state: "ok";
|
|
35
|
+
record: InstanceRecord;
|
|
36
|
+
};
|
|
37
|
+
export declare function readInstance(store: Store): InstanceRecord | null;
|
|
38
|
+
/** A stopping console forgets itself — only its own record, never a newer start's. */
|
|
39
|
+
export declare function clearInstance(store: Store, secret: string): void;
|
|
40
|
+
export declare const socketProof: (secret: string, challenge: string) => string;
|
|
41
|
+
export declare const httpProof: (secret: string, challenge: string, address: string, port: number) => string;
|
|
42
|
+
/** The HTTP answer: bound to the address and port THIS connection was received on, as the console saw them. */
|
|
43
|
+
export declare function instanceHttpAnswer(secret: string, challenge: string | null, local: {
|
|
44
|
+
address: string | undefined;
|
|
45
|
+
port: number | undefined;
|
|
46
|
+
}): {
|
|
47
|
+
status: number;
|
|
48
|
+
body: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The run directory must be private to this user: a real, canonical directory owned by this user with no access for
|
|
52
|
+
* anyone else (0700), inside a canonical directory owned by this user that nobody else can write. Checked before any
|
|
53
|
+
* change is made, and before connecting or binding.
|
|
54
|
+
*/
|
|
55
|
+
export declare function runDirProblem(dir: string, uid?: number, mustExist?: boolean): string | null;
|
|
56
|
+
/** A socket path that binds exactly (no truncation), named as a console names its own. */
|
|
57
|
+
export declare function socketPathProblem(path: string): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* `up`: the run directory, made 0700 if it isn't there. An existing one is used only as it is (never chmodded): one
|
|
60
|
+
* that isn't private to this user is refused. Everything is checked before anything is created.
|
|
61
|
+
*/
|
|
62
|
+
export declare function prepareRunDir(dir: string, uid?: number): string | null;
|
|
63
|
+
/** Ask a socket one question; null when nobody is listening (or it said nothing usable in time). */
|
|
64
|
+
export declare function askSocket(socketPath: string, request: object, timeoutMs?: number, connect?: (path: string) => Socket): Promise<{
|
|
65
|
+
answer: Record<string, unknown>;
|
|
66
|
+
} | {
|
|
67
|
+
none: "no-listener" | "no-answer";
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* The console's discovery socket: one line in, one line out. `hello` (with a challenge) answers the public facts and
|
|
71
|
+
* the socket proof; every other verb is refused. It reads nothing else and changes nothing. Bound under a fresh name
|
|
72
|
+
* no other process has, so it never clears or replaces anything; its own listener removes it on close.
|
|
73
|
+
*/
|
|
74
|
+
export declare function openInstanceSocket(databaseFile: string, facts: HelloFacts, secret: () => string | null): Promise<{
|
|
75
|
+
ok: true;
|
|
76
|
+
name: string;
|
|
77
|
+
close: () => void;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
problem: string;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* A record's console: "live" (its socket answers a fresh challenge with the record's secret and pid), "gone" (nobody
|
|
84
|
+
* is listening on its socket, or the socket isn't there), or "unknown" (anything else: slow, malformed, a wrong proof).
|
|
85
|
+
* Only "gone" lets another start take the record.
|
|
86
|
+
*/
|
|
87
|
+
export declare function recordLiveness(databaseFile: string, record: InstanceRecord, ask?: typeof askSocket): Promise<"live" | "gone" | "unknown">;
|
|
88
|
+
/**
|
|
89
|
+
* `up` claims the record for its socket — unless a live console already holds it (another start on the same
|
|
90
|
+
* database, by any spelling of its path). The write is a compare-and-set on the record it checked: two starts racing
|
|
91
|
+
* can't both win. Null: not claimed (this console runs, but `toolroll` won't attach to it).
|
|
92
|
+
*/
|
|
93
|
+
export declare function claimInstance(store: Store, databaseFile: string, facts: {
|
|
94
|
+
pid: number;
|
|
95
|
+
port: number;
|
|
96
|
+
build: string;
|
|
97
|
+
socket: string;
|
|
98
|
+
}, now: Date, liveness?: (record: InstanceRecord) => Promise<"live" | "gone" | "unknown">): Promise<string | null>;
|
|
99
|
+
export type AttachProof = {
|
|
100
|
+
ok: true;
|
|
101
|
+
url: string;
|
|
102
|
+
port: number;
|
|
103
|
+
pid: number;
|
|
104
|
+
build: string;
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
state: "none";
|
|
108
|
+
message: string;
|
|
109
|
+
} | {
|
|
110
|
+
ok: false;
|
|
111
|
+
state: "refused";
|
|
112
|
+
message: string;
|
|
113
|
+
};
|
|
114
|
+
export type AttachSeams = {
|
|
115
|
+
ask?: typeof askSocket;
|
|
116
|
+
fetch?: typeof fetch;
|
|
117
|
+
challenge?: () => string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Prove the console this installation's database names is live, on both channels, before anything is shown or
|
|
121
|
+
* opened. "none": no console runs (no record, or nobody on the socket); "refused": something answers that can't
|
|
122
|
+
* prove it is the console (or a different build) — the person restarts it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function proveLiveConsole(databaseFile: string, store: Store, build: string, seams?: AttachSeams): Promise<AttachProof>;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RUNNING CONSOLE, PROVED LIVE (docs/plans/agents-cockpit.md B1).
|
|
3
|
+
*
|
|
4
|
+
* Bare `toolroll` in a repository attaches to a console already running for this installation instead of starting a
|
|
5
|
+
* second one. Build agents run as the operator's own user, so nothing the filesystem says can tell the operator from
|
|
6
|
+
* an agent: the console's Unix socket is DISCOVERY ONLY. It answers one verb, `hello`, with facts an agent could learn
|
|
7
|
+
* anyway, and changes nothing.
|
|
8
|
+
*
|
|
9
|
+
* What proves the console is real is a fresh random instance secret, minted at each start of `up` and written to the
|
|
10
|
+
* database (behind the agent fence) with its pid, build and port. The CLI reads it from the database it opens itself,
|
|
11
|
+
* then makes the LIVE console answer two fresh challenges with it: over the socket (HMAC of "socket:" + challenge),
|
|
12
|
+
* and over the advertised HTTP listener (HMAC of "http:" + challenge + " " + address + " " + port, where the address and
|
|
13
|
+
* port are what the console itself saw as the receiving end of that very connection). The CLI requires those to be
|
|
14
|
+
* 127.0.0.1 and the recorded port, so a relay on 127.0.0.1 forwarding to the console's other address is refused, as are
|
|
15
|
+
* a replayed hello, a stale record with a recycled pid, and another process now on that port.
|
|
16
|
+
*/
|
|
17
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
18
|
+
import { lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
19
|
+
import { createConnection, createServer } from "node:net";
|
|
20
|
+
import { basename, dirname, join } from "node:path";
|
|
21
|
+
export const CONSOLE_INSTANCE_SCHEMA = `
|
|
22
|
+
CREATE TABLE IF NOT EXISTS console_instance (
|
|
23
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
24
|
+
secret TEXT NOT NULL,
|
|
25
|
+
pid INTEGER NOT NULL,
|
|
26
|
+
port INTEGER NOT NULL,
|
|
27
|
+
build TEXT NOT NULL,
|
|
28
|
+
socket TEXT NOT NULL,
|
|
29
|
+
started_at TEXT NOT NULL
|
|
30
|
+
);`;
|
|
31
|
+
/**
|
|
32
|
+
* The socket's own owner-only directory beside the database's REAL file (a symlinked database resolves to the one
|
|
33
|
+
* folder every spelling shares). Each console start binds a socket of its own, under a fresh random name recorded
|
|
34
|
+
* with its secret: no console ever needs to remove — or can remove — another console's socket.
|
|
35
|
+
*/
|
|
36
|
+
/** Toolroll's own name, so it never collides with a project or worktree that happens to sit beside the database. */
|
|
37
|
+
export const INSTANCE_DIR = ".toolroll-run";
|
|
38
|
+
export function instanceRunDir(databaseFile) {
|
|
39
|
+
let file = databaseFile;
|
|
40
|
+
try {
|
|
41
|
+
file = realpathSync(databaseFile);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
try {
|
|
45
|
+
file = join(realpathSync(dirname(databaseFile)), basename(databaseFile));
|
|
46
|
+
}
|
|
47
|
+
catch { /* not there yet: as given */ }
|
|
48
|
+
}
|
|
49
|
+
return join(dirname(file), INSTANCE_DIR);
|
|
50
|
+
}
|
|
51
|
+
const SOCKET_NAME = /^[0-9a-f]{16}\.sock$/;
|
|
52
|
+
/** The longest Unix socket path the platform binds without truncating (sun_path less its NUL). */
|
|
53
|
+
const SOCKET_PATH_MAX = process.platform === "darwin" ? 103 : 107;
|
|
54
|
+
/** The console's own answer to a challenge over HTTP (an edge route, no sign-in: it reveals nothing). */
|
|
55
|
+
export const INSTANCE_PATH = "/__instance";
|
|
56
|
+
const CHALLENGE = /^[0-9a-f]{32,128}$/;
|
|
57
|
+
const LINE_CAP = 4096;
|
|
58
|
+
/** The record as stored: none, one this build can't read (never taken for none), or the record. */
|
|
59
|
+
export function readInstanceState(store) {
|
|
60
|
+
const row = store.handle.prepare("SELECT secret, pid, port, build, socket, started_at FROM console_instance WHERE id = 1").get();
|
|
61
|
+
if (row === undefined)
|
|
62
|
+
return { state: "none" };
|
|
63
|
+
const secret = String(row["secret"]), pid = Number(row["pid"]), port = Number(row["port"]), socket = String(row["socket"]);
|
|
64
|
+
if (!/^[0-9a-f]{64}$/.test(secret) || !Number.isSafeInteger(pid) || !Number.isInteger(port) || port < 1 || port > 65535 || !SOCKET_NAME.test(socket))
|
|
65
|
+
return { state: "invalid" };
|
|
66
|
+
return { state: "ok", record: { secret, pid, port, build: String(row["build"]), socket, startedAt: String(row["started_at"]) } };
|
|
67
|
+
}
|
|
68
|
+
export function readInstance(store) {
|
|
69
|
+
const read = readInstanceState(store);
|
|
70
|
+
return read.state === "ok" ? read.record : null;
|
|
71
|
+
}
|
|
72
|
+
/** A stopping console forgets itself — only its own record, never a newer start's. */
|
|
73
|
+
export function clearInstance(store, secret) {
|
|
74
|
+
store.handle.prepare("DELETE FROM console_instance WHERE id = 1 AND secret = ?").run(secret);
|
|
75
|
+
}
|
|
76
|
+
export const socketProof = (secret, challenge) => createHmac("sha256", secret).update(`socket:${challenge}`).digest("hex");
|
|
77
|
+
export const httpProof = (secret, challenge, address, port) => createHmac("sha256", secret).update(`http:${challenge} ${address} ${port}`).digest("hex");
|
|
78
|
+
const PROOF = /^[0-9a-f]{64}$/;
|
|
79
|
+
/** Constant-time comparison of two proofs, each first required to be exactly 64 hex characters. */
|
|
80
|
+
const same = (a, b) => typeof a === "string" && PROOF.test(a) && PROOF.test(b) && timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
81
|
+
/** The HTTP answer: bound to the address and port THIS connection was received on, as the console saw them. */
|
|
82
|
+
export function instanceHttpAnswer(secret, challenge, local) {
|
|
83
|
+
if (challenge === null || !CHALLENGE.test(challenge) || local.address === undefined || local.port === undefined)
|
|
84
|
+
return { status: 400, body: JSON.stringify({ error: "challenge" }) };
|
|
85
|
+
return { status: 200, body: JSON.stringify({ proof: httpProof(secret, challenge, local.address, local.port) }) };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The run directory must be private to this user: a real, canonical directory owned by this user with no access for
|
|
89
|
+
* anyone else (0700), inside a canonical directory owned by this user that nobody else can write. Checked before any
|
|
90
|
+
* change is made, and before connecting or binding.
|
|
91
|
+
*/
|
|
92
|
+
export function runDirProblem(dir, uid = process.getuid?.() ?? -1, mustExist = true) {
|
|
93
|
+
const parent = dirname(dir);
|
|
94
|
+
let above;
|
|
95
|
+
try {
|
|
96
|
+
above = lstatSync(parent);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return `${parent} doesn't exist`;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
if (realpathSync(parent) !== parent)
|
|
103
|
+
return `${parent} isn't its own real path`;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return `${parent} doesn't exist`;
|
|
107
|
+
}
|
|
108
|
+
if (!above.isDirectory())
|
|
109
|
+
return `${parent} isn't a directory`;
|
|
110
|
+
if (uid >= 0 && above.uid !== uid)
|
|
111
|
+
return `${parent} belongs to another user`;
|
|
112
|
+
if ((above.mode & 0o022) !== 0)
|
|
113
|
+
return `${parent} can be written by other users`;
|
|
114
|
+
let info;
|
|
115
|
+
try {
|
|
116
|
+
info = lstatSync(dir);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return mustExist ? `${dir} doesn't exist` : null;
|
|
120
|
+
}
|
|
121
|
+
if (!info.isDirectory())
|
|
122
|
+
return `${dir} isn't a directory`;
|
|
123
|
+
if (uid >= 0 && info.uid !== uid)
|
|
124
|
+
return `${dir} belongs to another user`;
|
|
125
|
+
try {
|
|
126
|
+
lstatSync(join(dir, ".git"));
|
|
127
|
+
return `${dir} holds a repository, not Toolroll's sockets`;
|
|
128
|
+
}
|
|
129
|
+
catch { /* as it should be */ }
|
|
130
|
+
if ((info.mode & 0o077) !== 0)
|
|
131
|
+
return `${dir} is open to other users — Toolroll uses it only when it is yours alone (chmod 700)`;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/** A socket path that binds exactly (no truncation), named as a console names its own. */
|
|
135
|
+
export function socketPathProblem(path) {
|
|
136
|
+
if (!SOCKET_NAME.test(basename(path)))
|
|
137
|
+
return `${path} isn't a console socket`;
|
|
138
|
+
if (Buffer.byteLength(path) > SOCKET_PATH_MAX)
|
|
139
|
+
return `${path} is too long for a socket here`;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* `up`: the run directory, made 0700 if it isn't there. An existing one is used only as it is (never chmodded): one
|
|
144
|
+
* that isn't private to this user is refused. Everything is checked before anything is created.
|
|
145
|
+
*/
|
|
146
|
+
export function prepareRunDir(dir, uid = process.getuid?.() ?? -1) {
|
|
147
|
+
const before = runDirProblem(dir, uid, false);
|
|
148
|
+
if (before !== null)
|
|
149
|
+
return before;
|
|
150
|
+
try {
|
|
151
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code !== "EEXIST")
|
|
155
|
+
return `${dir} couldn't be made`;
|
|
156
|
+
}
|
|
157
|
+
return runDirProblem(dir, uid, true);
|
|
158
|
+
}
|
|
159
|
+
/** Ask a socket one question; null when nobody is listening (or it said nothing usable in time). */
|
|
160
|
+
export function askSocket(socketPath, request, timeoutMs = 2_000, connect = path => createConnection(path)) {
|
|
161
|
+
return new Promise(resolve => {
|
|
162
|
+
let settled = false, buffer = "";
|
|
163
|
+
const done = (value) => { if (settled)
|
|
164
|
+
return; settled = true; clearTimeout(timer); socket.destroy(); resolve(value); };
|
|
165
|
+
const socket = connect(socketPath);
|
|
166
|
+
const timer = setTimeout(() => done({ none: "no-answer" }), timeoutMs);
|
|
167
|
+
socket.setEncoding("utf8");
|
|
168
|
+
socket.on("error", (error) => done({ none: error.code === "ECONNREFUSED" || error.code === "ENOENT" ? "no-listener" : "no-answer" }));
|
|
169
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
170
|
+
socket.on("data", chunk => {
|
|
171
|
+
buffer += chunk;
|
|
172
|
+
if (buffer.length > LINE_CAP)
|
|
173
|
+
return done({ none: "no-answer" });
|
|
174
|
+
const end = buffer.indexOf("\n");
|
|
175
|
+
if (end < 0)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(buffer.slice(0, end));
|
|
179
|
+
done(parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? { answer: parsed } : { none: "no-answer" });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
done({ none: "no-answer" });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
socket.on("end", () => done({ none: "no-answer" }));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The console's discovery socket: one line in, one line out. `hello` (with a challenge) answers the public facts and
|
|
190
|
+
* the socket proof; every other verb is refused. It reads nothing else and changes nothing. Bound under a fresh name
|
|
191
|
+
* no other process has, so it never clears or replaces anything; its own listener removes it on close.
|
|
192
|
+
*/
|
|
193
|
+
export async function openInstanceSocket(databaseFile, facts, secret) {
|
|
194
|
+
const dir = instanceRunDir(databaseFile);
|
|
195
|
+
const problem = prepareRunDir(dir);
|
|
196
|
+
if (problem !== null)
|
|
197
|
+
return { ok: false, problem };
|
|
198
|
+
const name = `${randomBytes(8).toString("hex")}.sock`, socketPath = join(dir, name);
|
|
199
|
+
const pathProblem = socketPathProblem(socketPath);
|
|
200
|
+
if (pathProblem !== null)
|
|
201
|
+
return { ok: false, problem: pathProblem };
|
|
202
|
+
const server = createServer(socket => {
|
|
203
|
+
let buffer = "";
|
|
204
|
+
socket.setEncoding("utf8");
|
|
205
|
+
socket.setTimeout(5_000, () => socket.destroy());
|
|
206
|
+
const reply = (body) => { socket.end(`${JSON.stringify(body)}\n`); };
|
|
207
|
+
socket.on("error", () => socket.destroy());
|
|
208
|
+
socket.on("data", chunk => {
|
|
209
|
+
buffer += chunk;
|
|
210
|
+
if (buffer.length > LINE_CAP)
|
|
211
|
+
return void socket.destroy();
|
|
212
|
+
const end = buffer.indexOf("\n");
|
|
213
|
+
if (end < 0)
|
|
214
|
+
return;
|
|
215
|
+
let asked;
|
|
216
|
+
try {
|
|
217
|
+
asked = JSON.parse(buffer.slice(0, end));
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return reply({ error: "refused" });
|
|
221
|
+
}
|
|
222
|
+
const verb = asked !== null && typeof asked === "object" ? asked.verb : undefined;
|
|
223
|
+
const challenge = asked !== null && typeof asked === "object" ? asked.challenge : undefined;
|
|
224
|
+
const key = secret();
|
|
225
|
+
if (verb !== "hello" || typeof challenge !== "string" || !CHALLENGE.test(challenge) || key === null)
|
|
226
|
+
return reply({ error: "refused" });
|
|
227
|
+
reply({ ...facts, proof: socketProof(key, challenge) });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
// A Unix bind never replaces an existing path (EADDRINUSE).
|
|
231
|
+
const listening = await new Promise(resolve => { server.once("error", () => resolve(false)); server.listen(socketPath, () => resolve(true)); });
|
|
232
|
+
if (!listening)
|
|
233
|
+
return { ok: false, problem: `couldn't listen on ${socketPath}` };
|
|
234
|
+
return { ok: true, name, close: () => { server.close(); } };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A record's console: "live" (its socket answers a fresh challenge with the record's secret and pid), "gone" (nobody
|
|
238
|
+
* is listening on its socket, or the socket isn't there), or "unknown" (anything else: slow, malformed, a wrong proof).
|
|
239
|
+
* Only "gone" lets another start take the record.
|
|
240
|
+
*/
|
|
241
|
+
export async function recordLiveness(databaseFile, record, ask = askSocket) {
|
|
242
|
+
const socketPath = join(instanceRunDir(databaseFile), record.socket);
|
|
243
|
+
if (socketPathProblem(socketPath) !== null)
|
|
244
|
+
return "unknown";
|
|
245
|
+
const challenge = randomBytes(32).toString("hex");
|
|
246
|
+
const asked = await ask(socketPath, { verb: "hello", challenge }, 5_000);
|
|
247
|
+
if ("none" in asked)
|
|
248
|
+
return asked.none === "no-listener" ? "gone" : "unknown";
|
|
249
|
+
return asked.answer["pid"] === record.pid && same(asked.answer["proof"], socketProof(record.secret, challenge)) ? "live" : "unknown";
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* `up` claims the record for its socket — unless a live console already holds it (another start on the same
|
|
253
|
+
* database, by any spelling of its path). The write is a compare-and-set on the record it checked: two starts racing
|
|
254
|
+
* can't both win. Null: not claimed (this console runs, but `toolroll` won't attach to it).
|
|
255
|
+
*/
|
|
256
|
+
export async function claimInstance(store, databaseFile, facts, now, liveness = record => recordLiveness(databaseFile, record)) {
|
|
257
|
+
const rawSecret = () => { const row = store.handle.prepare("SELECT secret FROM console_instance WHERE id = 1").get(); return row === undefined ? null : String(row["secret"]); };
|
|
258
|
+
const checked = rawSecret();
|
|
259
|
+
// Taken only from nobody, or from a console confirmed gone: never on a check that merely failed, and never over a
|
|
260
|
+
// record this build can't read.
|
|
261
|
+
const current = readInstanceState(store);
|
|
262
|
+
if (current.state === "invalid")
|
|
263
|
+
return null;
|
|
264
|
+
if (current.state === "ok" && await liveness(current.record) !== "gone")
|
|
265
|
+
return null;
|
|
266
|
+
const secret = randomBytes(32).toString("hex");
|
|
267
|
+
return store.transact(() => {
|
|
268
|
+
// Exactly the record that was checked (or none): anything else is another start's newer claim.
|
|
269
|
+
if (rawSecret() !== checked)
|
|
270
|
+
return null;
|
|
271
|
+
store.handle.prepare("INSERT INTO console_instance (id, secret, pid, port, build, socket, started_at) VALUES (1, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET secret = excluded.secret, pid = excluded.pid, port = excluded.port, build = excluded.build, socket = excluded.socket, started_at = excluded.started_at")
|
|
272
|
+
.run(secret, facts.pid, facts.port, facts.build, facts.socket, now.toISOString());
|
|
273
|
+
return secret;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Prove the console this installation's database names is live, on both channels, before anything is shown or
|
|
278
|
+
* opened. "none": no console runs (no record, or nobody on the socket); "refused": something answers that can't
|
|
279
|
+
* prove it is the console (or a different build) — the person restarts it.
|
|
280
|
+
*/
|
|
281
|
+
export async function proveLiveConsole(databaseFile, store, build, seams = {}) {
|
|
282
|
+
const restart = "stop it and run `toolroll up` again";
|
|
283
|
+
// No record: nothing is running here (a console that couldn't claim one isn't offered). One this build can't read
|
|
284
|
+
// isn't "nothing": it is refused.
|
|
285
|
+
const read = readInstanceState(store);
|
|
286
|
+
if (read.state === "none")
|
|
287
|
+
return { ok: false, state: "none", message: "no console is running" };
|
|
288
|
+
if (read.state === "invalid")
|
|
289
|
+
return { ok: false, state: "refused", message: `the console's record can't be read — ${restart}` };
|
|
290
|
+
const record = read.record;
|
|
291
|
+
const dir = instanceRunDir(databaseFile);
|
|
292
|
+
const problem = runDirProblem(dir) ?? socketPathProblem(join(dir, record.socket));
|
|
293
|
+
if (problem !== null)
|
|
294
|
+
return { ok: false, state: "refused", message: `the console's socket can't be trusted: ${problem}` };
|
|
295
|
+
const socketPath = join(dir, record.socket);
|
|
296
|
+
try {
|
|
297
|
+
const socket = lstatSync(socketPath);
|
|
298
|
+
if (!socket.isSocket() || (process.getuid !== undefined && socket.uid !== process.getuid()))
|
|
299
|
+
return { ok: false, state: "refused", message: `the console's socket can't be trusted: ${socketPath} isn't this user's socket` };
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// Only "not there" means nothing is running; a socket the system won't show can't be checked.
|
|
303
|
+
if (error.code === "ENOENT")
|
|
304
|
+
return { ok: false, state: "none", message: "no console is running" };
|
|
305
|
+
return { ok: false, state: "refused", message: `the console's socket can't be checked — ${restart}` };
|
|
306
|
+
}
|
|
307
|
+
const challenge = seams.challenge ?? (() => randomBytes(32).toString("hex"));
|
|
308
|
+
const first = challenge();
|
|
309
|
+
const asked = await (seams.ask ?? askSocket)(socketPath, { verb: "hello", challenge: first });
|
|
310
|
+
if ("none" in asked) {
|
|
311
|
+
// Stale (a console that died): nobody is listening, so nothing is running. Discovery removes nothing.
|
|
312
|
+
if (asked.none === "no-listener")
|
|
313
|
+
return { ok: false, state: "none", message: "no console is running" };
|
|
314
|
+
return { ok: false, state: "refused", message: `the console's socket didn't answer — ${restart}` };
|
|
315
|
+
}
|
|
316
|
+
const hello = asked.answer;
|
|
317
|
+
if (hello["build"] !== record.build || hello["build"] !== build) {
|
|
318
|
+
return { ok: false, state: "refused", message: `the running console is a different build (${String(hello["build"] ?? "unknown")}, this is ${build}) — ${restart}` };
|
|
319
|
+
}
|
|
320
|
+
if (hello["pid"] !== record.pid || hello["port"] !== record.port || !same(hello["proof"], socketProof(record.secret, first))) {
|
|
321
|
+
return { ok: false, state: "refused", message: `the console on the socket couldn't prove it's this installation's — ${restart}` };
|
|
322
|
+
}
|
|
323
|
+
const second = challenge();
|
|
324
|
+
let body;
|
|
325
|
+
try {
|
|
326
|
+
const answer = await (seams.fetch ?? fetch)(`http://127.0.0.1:${record.port}${INSTANCE_PATH}?c=${second}`, { redirect: "manual", signal: AbortSignal.timeout(3_000) });
|
|
327
|
+
body = answer.status === 200 ? await answer.json() : null;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
body = null;
|
|
331
|
+
}
|
|
332
|
+
const proof = body !== null && typeof body === "object" ? body.proof : undefined;
|
|
333
|
+
if (!same(proof, httpProof(record.secret, second, "127.0.0.1", record.port))) {
|
|
334
|
+
return { ok: false, state: "refused", message: `the address 127.0.0.1:${record.port} couldn't prove it's the console — ${restart}` };
|
|
335
|
+
}
|
|
336
|
+
return { ok: true, url: `http://127.0.0.1:${record.port}`, port: record.port, pid: record.pid, build: record.build };
|
|
337
|
+
}
|
|
@@ -85,6 +85,20 @@ export declare const chatActionRequestSchemas: {
|
|
|
85
85
|
repo: z.ZodString;
|
|
86
86
|
starter: z.ZodString;
|
|
87
87
|
}, z.core.$strict>;
|
|
88
|
+
readonly sweep_cancel_rest: z.ZodObject<{
|
|
89
|
+
sweep: z.ZodInt;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
readonly sweep_file: z.ZodObject<{
|
|
92
|
+
repo: z.ZodString;
|
|
93
|
+
title: z.ZodString;
|
|
94
|
+
brief: z.ZodString;
|
|
95
|
+
items: z.ZodArray<z.ZodObject<{
|
|
96
|
+
title: z.ZodString;
|
|
97
|
+
item: z.ZodString;
|
|
98
|
+
}, z.core.$strict>>;
|
|
99
|
+
acceptance: z.ZodArray<z.ZodUnknown>;
|
|
100
|
+
maxParallel: z.ZodNumber;
|
|
101
|
+
}, z.core.$strict>;
|
|
88
102
|
readonly flow_edit: z.ZodObject<{
|
|
89
103
|
flow: z.ZodInt;
|
|
90
104
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -309,6 +323,8 @@ export declare const sharedActionSchema: z.ZodObject<{
|
|
|
309
323
|
subagent_state: "subagent_state";
|
|
310
324
|
subagent_tools: "subagent_tools";
|
|
311
325
|
subagent_undo: "subagent_undo";
|
|
326
|
+
sweep_cancel_rest: "sweep_cancel_rest";
|
|
327
|
+
sweep_file: "sweep_file";
|
|
312
328
|
task_cancel: "task_cancel";
|
|
313
329
|
task_resume: "task_resume";
|
|
314
330
|
tool_add: "tool_add";
|
|
@@ -41,6 +41,10 @@ export const chatActionRequestSchemas = {
|
|
|
41
41
|
tool_remove: z.strictObject({ repo: words, name: words }),
|
|
42
42
|
flow_create: z.strictObject({ repo: words, name: words, definition: nested, trigger: nested.optional() }),
|
|
43
43
|
flow_starter: z.strictObject({ repo: words, starter: words }),
|
|
44
|
+
// Lead fan-out (docs/plans/lead-fan-out.md): N tasks under one brief, one yes.
|
|
45
|
+
sweep_cancel_rest: z.strictObject({ sweep: id }),
|
|
46
|
+
sweep_file: z.strictObject({ repo: words, title: words, brief: words, items: z.array(z.strictObject({ title: words, item: words })).min(2).max(20),
|
|
47
|
+
acceptance: z.array(z.unknown()).min(1), maxParallel: z.number().int().min(1).max(8) }),
|
|
44
48
|
flow_edit: z.strictObject({ flow: id, name: words.optional(), definition: nested.optional() }),
|
|
45
49
|
flow_card_add: z.strictObject({ flow: id, title: words, description: z.unknown().optional(), zone: words.optional() }),
|
|
46
50
|
flow_card_move: z.strictObject({ card: id, zone: words }),
|
|
@@ -31,6 +31,15 @@ export declare const chatContentShape: {
|
|
|
31
31
|
}, z.core.$strict>>;
|
|
32
32
|
task: z.ZodOptional<z.ZodString>;
|
|
33
33
|
run: z.ZodOptional<z.ZodInt>;
|
|
34
|
+
/** A notice its person's watch may hold at the final fence (cockpit §3): set at planning only for a kind a watch
|
|
35
|
+
* can hold. Absent: never held (a security alert, the digest, anything unmarked). */
|
|
36
|
+
watch: z.ZodOptional<z.ZodLiteral<true>>;
|
|
37
|
+
/** Every task (and agent) a `watch` part covers, when it covers more than one: it is held only while its person is
|
|
38
|
+
* watching every one of them. */
|
|
39
|
+
subjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
40
|
+
ref: z.ZodNullable<z.ZodInt>;
|
|
41
|
+
run: z.ZodNullable<z.ZodInt>;
|
|
42
|
+
}, z.core.$strict>>>;
|
|
34
43
|
edit: z.ZodOptional<z.ZodString>;
|
|
35
44
|
phase: z.ZodOptional<z.ZodLiteral<"armed">>;
|
|
36
45
|
link: z.ZodOptional<z.ZodObject<{
|
|
@@ -123,6 +132,11 @@ export declare const chatContentSchema: z.ZodObject<{
|
|
|
123
132
|
}, z.core.$strict>>;
|
|
124
133
|
task: z.ZodOptional<z.ZodString>;
|
|
125
134
|
run: z.ZodOptional<z.ZodInt>;
|
|
135
|
+
watch: z.ZodOptional<z.ZodLiteral<true>>;
|
|
136
|
+
subjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
137
|
+
ref: z.ZodNullable<z.ZodInt>;
|
|
138
|
+
run: z.ZodNullable<z.ZodInt>;
|
|
139
|
+
}, z.core.$strict>>>;
|
|
126
140
|
edit: z.ZodOptional<z.ZodString>;
|
|
127
141
|
phase: z.ZodOptional<z.ZodLiteral<"armed">>;
|
|
128
142
|
link: z.ZodOptional<z.ZodObject<{
|
|
@@ -205,6 +219,11 @@ export declare const savedChatPartSchema: z.ZodObject<{
|
|
|
205
219
|
}, z.core.$strict>>;
|
|
206
220
|
task: z.ZodOptional<z.ZodString>;
|
|
207
221
|
run: z.ZodOptional<z.ZodInt>;
|
|
222
|
+
watch: z.ZodOptional<z.ZodLiteral<true>>;
|
|
223
|
+
subjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
224
|
+
ref: z.ZodNullable<z.ZodInt>;
|
|
225
|
+
run: z.ZodNullable<z.ZodInt>;
|
|
226
|
+
}, z.core.$strict>>>;
|
|
208
227
|
edit: z.ZodOptional<z.ZodString>;
|
|
209
228
|
phase: z.ZodOptional<z.ZodLiteral<"armed">>;
|
|
210
229
|
link: z.ZodOptional<z.ZodObject<{
|
|
@@ -26,6 +26,12 @@ export const chatContentShape = {
|
|
|
26
26
|
shot: z.strictObject({ follows: z.int().nullable() }).optional(),
|
|
27
27
|
task: z.string().optional(),
|
|
28
28
|
run: z.int().optional(),
|
|
29
|
+
/** A notice its person's watch may hold at the final fence (cockpit §3): set at planning only for a kind a watch
|
|
30
|
+
* can hold. Absent: never held (a security alert, the digest, anything unmarked). */
|
|
31
|
+
watch: z.literal(true).optional(),
|
|
32
|
+
/** Every task (and agent) a `watch` part covers, when it covers more than one: it is held only while its person is
|
|
33
|
+
* watching every one of them. */
|
|
34
|
+
subjects: z.array(z.strictObject({ ref: z.int().nullable(), run: z.int().nullable() })).max(100).optional(),
|
|
29
35
|
edit: z.string().optional(),
|
|
30
36
|
phase: z.literal("armed").optional(),
|
|
31
37
|
link: link.optional(),
|
package/dist/contracts/cli.d.ts
CHANGED
|
@@ -647,6 +647,14 @@ export declare const COMMAND_OUTPUTS: {
|
|
|
647
647
|
readable: z.ZodBoolean;
|
|
648
648
|
zones: z.ZodArray<z.ZodUnknown>;
|
|
649
649
|
triggers: z.ZodArray<z.ZodUnknown>;
|
|
650
|
+
unattended: z.ZodNullable<z.ZodObject<{
|
|
651
|
+
until: z.ZodString;
|
|
652
|
+
signedBy: z.ZodString;
|
|
653
|
+
budgetMicrousd: z.ZodNullable<z.ZodNumber>;
|
|
654
|
+
dailyRunCap: z.ZodNullable<z.ZodNumber>;
|
|
655
|
+
runsToday: z.ZodNumber;
|
|
656
|
+
covers: z.ZodArray<z.ZodString>;
|
|
657
|
+
}, z.core.$loose>>;
|
|
650
658
|
}, z.core.$loose>;
|
|
651
659
|
}, z.core.$loose>, z.ZodObject<{
|
|
652
660
|
envelopeVersion: z.ZodLiteral<1>;
|
package/dist/contracts/cli.js
CHANGED
|
@@ -184,7 +184,9 @@ export const COMMAND_OUTPUTS = {
|
|
|
184
184
|
"task unhold": answers("task unhold", { id: str }),
|
|
185
185
|
"task wait": answers("task wait", { task: str, outcome: str }),
|
|
186
186
|
"flows list": answers("flows list", { flows: z.array(flowListed) }),
|
|
187
|
-
"flows show": answers("flows show", { flow: z.looseObject({ id: int, name: str, repo: str, project: str, readable: z.boolean(), zones: list, triggers: list
|
|
187
|
+
"flows show": answers("flows show", { flow: z.looseObject({ id: int, name: str, repo: str, project: str, readable: z.boolean(), zones: list, triggers: list,
|
|
188
|
+
// Whether the flow runs on its own, and until when (null: it doesn't).
|
|
189
|
+
unattended: nullable(z.looseObject({ until: str, signedBy: str, budgetMicrousd: nullable(int), dailyRunCap: nullable(int), runsToday: int, covers: z.array(str) })) }) }),
|
|
188
190
|
grants: answers("grants", { count: int, grants: list }),
|
|
189
191
|
"runner list": answers("runner list", { count: int, runners: list }),
|
|
190
192
|
"incident list": answers("incident list", { incidents: list }),
|