toolroll 0.9.63 → 0.9.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +15 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +50 -2
- package/dist/agent-fence.js +157 -10
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +36 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +12 -4
- package/dist/contracts/console-api.js +12 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +217 -21
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +275 -17
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +170 -5
- package/dist/scope.js +251 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +247 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +4 -1
- package/dist/server/render-pages.js +5 -4
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +110 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +46 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +121 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +320 -74
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +430 -15
- package/dist/store.js +1041 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +2 -0
- package/dist/tool-broker.js +1 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/package.json +11 -2
|
@@ -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(),
|
|
@@ -66,16 +66,23 @@ export declare const CONSOLE_FORMS: {
|
|
|
66
66
|
readonly projectCeiling: FormContract<"repo">;
|
|
67
67
|
readonly demo: FormContract<"message" | "note">;
|
|
68
68
|
readonly updatesDismiss: FormContract<"quiet" | "return" | "version">;
|
|
69
|
+
readonly forgetPassword: FormContract<"return">;
|
|
70
|
+
readonly runReveal: FormContract<"return">;
|
|
69
71
|
readonly updatesChecks: FormContract<"check">;
|
|
70
72
|
readonly code: FormContract<"acceptance" | "answers" | "base" | "candidate" | "decision" | "goal" | "model" | "password" | "prompt" | "repo" | "requestId" | "title" | "visual" | `question:${string}`>;
|
|
73
|
+
readonly sweep: FormContract<"confirm" | "snapshot">;
|
|
74
|
+
readonly passwordless: FormContract<"action" | "password">;
|
|
75
|
+
readonly term: FormContract<"password" | "program" | "repo">;
|
|
71
76
|
readonly skillsRevise: FormContract<"feedback" | "nonce" | "repo" | "run">;
|
|
72
77
|
readonly skillsChange: FormContract<"action" | "content" | "files" | "identity" | "method" | "nonce" | "repo" | "restore" | "revision" | "sample" | "sha" | "url">;
|
|
73
78
|
readonly kit: FormContract<"repo">;
|
|
74
79
|
readonly subagents: FormContract<"choice" | "dailyTurns" | "id" | "manager" | "model" | "name" | "note" | "op" | "repo" | "schedule" | "soul" | "state" | "template" | "text" | "tool" | `field.${string}` | `over.${string}` | `undo.${string}` | `use.${string}`>;
|
|
75
80
|
readonly galleryUse: FormContract<string>;
|
|
76
|
-
readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "stage" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
|
|
81
|
+
readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "project" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "sender" | "stage" | "team" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
|
|
77
82
|
readonly spendBudget: FormContract<"action" | "password" | "stop" | "target" | "usd">;
|
|
78
83
|
readonly projectConcurrency: FormContract<"concurrency" | "repo">;
|
|
84
|
+
readonly projectLiveView: FormContract<"act" | "nonce" | "password" | "repo">;
|
|
85
|
+
readonly projectNetwork: FormContract<"hosts" | "local" | "mode" | "password" | "repo">;
|
|
79
86
|
readonly projectDelete: FormContract<"name" | "password" | "repo" | "step">;
|
|
80
87
|
readonly policy: FormContract<"ceiling" | "models" | "password" | "provider" | "tools">;
|
|
81
88
|
readonly approval: FormContract<"not_requester" | "password" | "paths" | "protect" | "repo">;
|
|
@@ -144,6 +151,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
144
151
|
readonly leadFollow: FormContract<"enabled" | "return">;
|
|
145
152
|
readonly leadEnd: FormContract<"return">;
|
|
146
153
|
readonly leadStop: FormContract<"return" | "turn">;
|
|
154
|
+
readonly leadQueuedRemove: FormContract<"return">;
|
|
147
155
|
readonly leadProposal: FormContract<"confirm" | "nonce" | "return" | "token">;
|
|
148
156
|
readonly coordinatorProposal: FormContract<"confirm" | "return">;
|
|
149
157
|
readonly chat: FormContract<"message" | "mode" | "project" | "request" | "request-session" | "result" | "task" | "token">;
|
|
@@ -156,7 +164,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
156
164
|
readonly revise: FormContract<"batch" | "return" | "source">;
|
|
157
165
|
readonly followUp: FormContract<"level" | "return">;
|
|
158
166
|
readonly resolveIncident: FormContract<"return">;
|
|
159
|
-
readonly taskSteer: FormContract<"note" | "return" | "run">;
|
|
167
|
+
readonly taskSteer: FormContract<"after" | "key" | "note" | "request" | "return" | "run">;
|
|
160
168
|
readonly taskReply: FormContract<"answer" | "question">;
|
|
161
169
|
readonly taskHold: FormContract<"reason">;
|
|
162
170
|
readonly taskBlock: FormContract<"on">;
|
|
@@ -170,11 +178,11 @@ export declare const CONSOLE_FORMS: {
|
|
|
170
178
|
readonly taskCancel: FormContract<"reason">;
|
|
171
179
|
readonly taskRoute: FormContract<"agent" | "clear-phase" | "model" | "phase" | "provider" | "return" | "risky" | "sawDigest" | "size">;
|
|
172
180
|
readonly taskScope: FormContract<"acceptance" | "budget-microusd" | "budget-usd" | "goal" | "goal-brief" | "not" | "permission-mode" | "quality-mode" | "requirement" | "requirement-new" | "sawDigest" | "touches">;
|
|
173
|
-
readonly taskApprove: FormContract<"digest" | "nonce" | "return" | "token">;
|
|
181
|
+
readonly taskApprove: FormContract<"digest" | "nonce" | "rebuild" | "return" | "token">;
|
|
174
182
|
readonly taskAcceptProof: FormContract<"note" | "return" | "run">;
|
|
175
183
|
readonly taskComplete: FormContract<"accept" | "note" | "publish" | "receipt" | "return" | "run">;
|
|
176
184
|
readonly taskStop: FormContract<"return" | "run">;
|
|
177
|
-
readonly taskPause: FormContract<"mode" | "note" | "request" | "return" | "run">;
|
|
185
|
+
readonly taskPause: FormContract<"key" | "mode" | "note" | "request" | "return" | "run">;
|
|
178
186
|
readonly taskResume: FormContract<"nonce" | "return" | "run" | "token">;
|
|
179
187
|
};
|
|
180
188
|
export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
|
|
@@ -75,16 +75,23 @@ export const CONSOLE_FORMS = {
|
|
|
75
75
|
projectCeiling: formContract("POST (project ceiling)", ["repo"]),
|
|
76
76
|
demo: formContract("POST /chat/demo/ask, /chat/demo/<id>/approve|change|revise|complete", ["message", "note"]),
|
|
77
77
|
updatesDismiss: formContract("POST /settings/updates/dismiss", ["version", "quiet", "return"]),
|
|
78
|
+
forgetPassword: formContract("POST /session/forget-password", ["return"]),
|
|
79
|
+
runReveal: formContract("POST /r/<run>/reveal", ["return"]),
|
|
78
80
|
updatesChecks: formContract("POST /settings/updates/checks", ["check"]),
|
|
79
81
|
code: formContract("POST /code, /code/start, /code/<session>/<act>, /code/<anything else>", ["repo", "password", "title", "model", "prompt", "requestId", "base", "candidate", "goal", "acceptance", "visual", "decision", "answers"], { prefixes: ["question:"] }),
|
|
82
|
+
sweep: formContract("POST /sweeps/<sweep>/cancel-rest", ["confirm", "snapshot"]),
|
|
83
|
+
passwordless: formContract("POST /settings/passwordless", ["action", "password"]),
|
|
84
|
+
term: formContract("POST /term/start, /term/<terminal>/close, /term/<terminal>/remove", ["repo", "program", "password"]),
|
|
80
85
|
skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
|
|
81
86
|
skillsChange: formContract("POST /settings/skills/import, /settings/skills/change", ["repo", "identity", "revision", "method", "content", "files", "url", "action", "sha", "sample", "nonce", "restore"]),
|
|
82
87
|
kit: formContract("POST /kits/<kit>/<act>", ["repo"]),
|
|
83
88
|
subagents: formContract("POST /settings/lead/subagents/new, /settings/lead/subagents/<id>/<part>, /settings/lead/subagents/questions/<id>/answer", ["choice", "text", "repo", "template", "name", "soul", "op", "id", "schedule", "tool", "state", "note", "model", "dailyTurns", "manager"], { prefixes: ["use.", "undo.", "field.", "over."] }),
|
|
84
89
|
galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed"], { dynamic: true }),
|
|
85
|
-
flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules"], { prefixes: ["param."] }),
|
|
90
|
+
flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "sender", "project", "team", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules"], { prefixes: ["param."] }),
|
|
86
91
|
spendBudget: formContract("POST /spend/budget", ["password", "target", "action", "usd", "stop"]),
|
|
87
92
|
projectConcurrency: formContract("POST /settings/project/concurrency", ["repo", "concurrency"]),
|
|
93
|
+
projectLiveView: formContract("POST /settings/project/live-view", ["repo", "act", "password", "nonce"]),
|
|
94
|
+
projectNetwork: formContract("POST /settings/project/network", ["repo", "mode", "local", "hosts", "password"]),
|
|
88
95
|
projectDelete: formContract("POST /settings/project/delete", ["repo", "name", "step", "password"]),
|
|
89
96
|
policy: formContract("POST /settings/policy", ["password", "provider", "models", "tools", "ceiling"]),
|
|
90
97
|
approval: formContract("POST /settings/approval", ["repo", "password", "protect", "paths", "not_requester"]),
|
|
@@ -153,6 +160,7 @@ export const CONSOLE_FORMS = {
|
|
|
153
160
|
leadFollow: formContract("POST /chat/mate/follow", ["enabled", "return"]),
|
|
154
161
|
leadEnd: formContract("POST /chat/mate/end", ["return"]),
|
|
155
162
|
leadStop: formContract("POST /chat/mate/stop", ["return", "turn"]),
|
|
163
|
+
leadQueuedRemove: formContract("POST /chat/queued/<message>/remove", ["return"]),
|
|
156
164
|
leadProposal: formContract("POST /chat/proposal/<id>/confirm|dismiss", ["return", "nonce", "confirm", "token"]),
|
|
157
165
|
coordinatorProposal: formContract("POST /proposals/<id>/confirm|dismiss", ["return", "confirm"]),
|
|
158
166
|
chat: formContract("POST /chat", ["task", "project", "result", "mode", "request", "request-session", "message", "token"]),
|
|
@@ -167,7 +175,7 @@ export const CONSOLE_FORMS = {
|
|
|
167
175
|
followUp: formContract("POST /r/<run>/checks|add-tests", ["return", "level"]),
|
|
168
176
|
resolveIncident: formContract("POST /i/<id>/resolve", ["return"]),
|
|
169
177
|
// A task's verbs (/t/<task>/<verb>), one body each.
|
|
170
|
-
taskSteer: formContract("POST /t/<task>/steer", ["note", "return", "run"]),
|
|
178
|
+
taskSteer: formContract("POST /t/<task>/steer", ["note", "return", "run", "key", "after", "request"]),
|
|
171
179
|
taskReply: formContract("POST /t/<task>/reply", ["question", "answer"]),
|
|
172
180
|
taskHold: formContract("POST /t/<task>/hold", ["reason"]),
|
|
173
181
|
taskBlock: formContract("POST /t/<task>/block|unblock", ["on"]),
|
|
@@ -181,13 +189,13 @@ export const CONSOLE_FORMS = {
|
|
|
181
189
|
taskCancel: formContract("POST /t/<task>/cancel", ["reason"]),
|
|
182
190
|
taskRoute: formContract("POST /t/<task>/route", ["size", "risky", "phase", "clear-phase", "agent", "provider", "model", "sawDigest", "return"]),
|
|
183
191
|
taskScope: formContract("POST /t/<task>/scope", ["sawDigest", "permission-mode", "quality-mode", "budget-usd", "budget-microusd", "requirement", "requirement-new", "goal-brief", "goal", "not", "touches", "acceptance"]),
|
|
184
|
-
taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce"]),
|
|
192
|
+
taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce", "rebuild"]),
|
|
185
193
|
taskAcceptProof: formContract("POST /t/<task>/accept-proof", ["run", "note", "return"]),
|
|
186
194
|
taskComplete: formContract("POST /t/<task>/complete", ["receipt", "run", "accept", "note", "publish", "return"]),
|
|
187
195
|
taskStop: formContract("POST /t/<task>/stop", ["run", "return"]),
|
|
188
196
|
// Pause, or Pause and steer when a note rides along, or Resume (mode=resume): the exact run shown, and the page's own
|
|
189
197
|
// request id.
|
|
190
|
-
taskPause: formContract("POST /t/<task>/pause", ["run", "request", "note", "mode", "return"]),
|
|
198
|
+
taskPause: formContract("POST /t/<task>/pause", ["run", "request", "note", "mode", "return", "key"]),
|
|
191
199
|
taskResume: formContract("POST /t/<task>/resume|resume-arm", ["run", "token", "nonce", "return"]),
|
|
192
200
|
};
|
|
193
201
|
/** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
|
|
@@ -29,6 +29,10 @@ export declare const flowCardSchema: z.ZodObject<{
|
|
|
29
29
|
outputs: z.ZodObject<{}, z.core.$catchall<z.ZodString>>;
|
|
30
30
|
attached: z.ZodObject<{}, z.core.$catchall<z.ZodString>>;
|
|
31
31
|
createdBy: z.ZodString;
|
|
32
|
+
createdKind: z.ZodEnum<{
|
|
33
|
+
automation: "automation";
|
|
34
|
+
person: "person";
|
|
35
|
+
}>;
|
|
32
36
|
createdAt: z.ZodString;
|
|
33
37
|
updatedAt: z.ZodString;
|
|
34
38
|
owner: z.ZodNullable<z.ZodString>;
|
|
@@ -30,6 +30,9 @@ export const flowCardSchema = z.strictObject({
|
|
|
30
30
|
outputs: cardOutputsSchema.shape.outputs,
|
|
31
31
|
attached: cardOutputsSchema.shape.attached,
|
|
32
32
|
createdBy: z.string(),
|
|
33
|
+
/** v126: whether a person or an automation (a delivery, a poll, a schedule, a public form) made it. Decided where it
|
|
34
|
+
* was made, never read from its label: filing passes it on as the requester's kind. */
|
|
35
|
+
createdKind: z.enum(["person", "automation"]),
|
|
33
36
|
createdAt: z.string(),
|
|
34
37
|
updatedAt: z.string(),
|
|
35
38
|
owner: z.string().nullable(),
|