toolroll 0.9.72 → 0.9.74
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 +108 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/builder.js +1 -1
- package/dist/chat-actions.js +13 -8
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- package/dist/chat-quiet.js +3 -3
- package/dist/check-report.js +2 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +17 -0
- package/dist/commit-extract.js +82 -4
- package/dist/contracts/console-api.d.ts +3 -3
- package/dist/contracts/console-api.js +4 -4
- package/dist/contracts/release-terms.d.ts +7 -1
- package/dist/contracts/release-terms.js +6 -2
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +35 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +20 -19
- package/dist/first-run.d.ts +17 -1
- package/dist/first-run.js +38 -0
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/operate.js +3 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +84 -14
- package/dist/release-authority.d.ts +6 -0
- package/dist/release-authority.js +9 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +3 -0
- package/dist/release-carry.js +23 -4
- package/dist/release-cli.js +36 -3
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release.d.ts +11 -2
- package/dist/release.js +82 -6
- package/dist/repository-context-ui.js +2 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +34 -23
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +88 -0
- package/dist/server/chat.js +84 -30
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/flows.js +40 -10
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +81 -1
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +8 -4
- package/dist/server/render-pages.js +55 -50
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.js +10 -3
- package/dist/server/route-table.js +5 -1
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +29 -8
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +59 -41
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +34 -1
- package/dist/store.js +81 -2
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/subscription-chat.js +5 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/package.json +1 -1
package/dist/commit-extract.js
CHANGED
|
@@ -167,10 +167,11 @@ export function extractCommit(gitDir, sha, dest, limits = EXTRACTION_LIMITS) {
|
|
|
167
167
|
return { ok: true, files: files.length };
|
|
168
168
|
}
|
|
169
169
|
/** A reader over the project's object store that hashes every object it hands back, or why it can't be one. */
|
|
170
|
-
function verifiedReader(gitDir) {
|
|
170
|
+
function verifiedReader(gitDir, runner = {}) {
|
|
171
171
|
const refuse = (message) => ({ ok: false, message });
|
|
172
|
-
const env = { ...checkoutGitEnv(), GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_NO_REPLACE_OBJECTS: "1", GIT_NO_LAZY_FETCH: "1", GIT_ATTR_NOSYSTEM: "1" };
|
|
173
|
-
|
|
172
|
+
const env = { ...(runner.env ?? checkoutGitEnv()), GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_NO_REPLACE_OBJECTS: "1", GIT_NO_LAZY_FETCH: "1", GIT_ATTR_NOSYSTEM: "1" };
|
|
173
|
+
// The commit-graph file is a cache a build can write: parents come from the objects themselves, never from it.
|
|
174
|
+
const git = (args, input) => spawnSync(runner.git ?? "git", ["--git-dir", gitDir, "--no-replace-objects", ...HARDENED_GIT_ARGS, "-c", "core.commitGraph=false", ...args], { cwd: "/", env, input, maxBuffer: MAX_BYTES, timeout: 300_000 });
|
|
174
175
|
// History a build could have rewritten around the objects: a shallow boundary, grafts, or replacement refs. Each is
|
|
175
176
|
// refused outright, so no reader here ever has to decide whether to believe one.
|
|
176
177
|
if (existsSync(join(gitDir, "shallow")))
|
|
@@ -221,7 +222,7 @@ function verifiedReader(gitDir) {
|
|
|
221
222
|
}
|
|
222
223
|
return found;
|
|
223
224
|
};
|
|
224
|
-
return { ok: true, value: { format, hexLength, read } };
|
|
225
|
+
return { ok: true, value: { format, hexLength, read, git } };
|
|
225
226
|
}
|
|
226
227
|
/** Parse a commit from its verified bytes: tree, parents, message. Null when malformed. */
|
|
227
228
|
function parseCommit(id, bytes, hexLength) {
|
|
@@ -434,3 +435,80 @@ export function verifiedBlobs(gitDir, tree, paths) {
|
|
|
434
435
|
return { ok: false, message: `A file couldn't be verified: ${blobs}.` };
|
|
435
436
|
return { ok: true, value: new Map(wanted.map(one => [one.path, { id: one.id, bytes: blobs.get(one.id).bytes }])) };
|
|
436
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* The commits reachable from `to` and not from `from`, as Git lists them (newest first), each read from its own verified
|
|
440
|
+
* bytes: what a release card says changed (release-card.ts). Git only names them. Each is hashed here and its parents
|
|
441
|
+
* checked against Git's listing; every commit the range stops at (a parent outside it) must be reached from `from`
|
|
442
|
+
* through verified parents within `reach` commits, so history a build rewrote around the objects can't shorten the
|
|
443
|
+
* list. A shallow boundary, grafts or replacement refs refuse (verifiedReader), as does a range longer than `max`.
|
|
444
|
+
*/
|
|
445
|
+
export function verifiedRange(gitDir, from, to, options = {}) {
|
|
446
|
+
const { max = 1_000, reach = 5_000 } = options;
|
|
447
|
+
const reader = verifiedReader(gitDir, options);
|
|
448
|
+
if (!reader.ok)
|
|
449
|
+
return reader;
|
|
450
|
+
const { git, read, hexLength } = reader.value;
|
|
451
|
+
const id = new RegExp(`^[0-9a-f]{${hexLength}}$`);
|
|
452
|
+
if (!id.test(from) || !id.test(to))
|
|
453
|
+
return { ok: false, message: "A commit id isn't a full id." };
|
|
454
|
+
/** Git's listing: each line's commit (`-` marks a boundary) and the parents it names. */
|
|
455
|
+
const listing = (args) => {
|
|
456
|
+
const listed = git(["rev-list", "--parents", ...args]);
|
|
457
|
+
if (listed.status !== 0)
|
|
458
|
+
return null;
|
|
459
|
+
const rows = String(listed.stdout).split("\n").filter(line => line !== "").map(line => {
|
|
460
|
+
const [first = "", ...parents] = line.split(" ");
|
|
461
|
+
return { id: first.replace(/^-/, ""), boundary: first.startsWith("-"), parents };
|
|
462
|
+
});
|
|
463
|
+
return rows.every(row => id.test(row.id) && row.parents.every(one => id.test(one))) ? rows : null;
|
|
464
|
+
};
|
|
465
|
+
const range = listing(["--boundary", `${from}..${to}`, "--"]);
|
|
466
|
+
if (range === null)
|
|
467
|
+
return { ok: false, message: "The commits between the releases couldn't be listed." };
|
|
468
|
+
const inside = range.filter(row => !row.boundary), stops = new Set(range.filter(row => row.boundary).map(row => row.id));
|
|
469
|
+
if (inside.length > max)
|
|
470
|
+
return { ok: false, message: `More than ${max} commits lie between the releases.` };
|
|
471
|
+
const objects = read(inside.map(row => row.id));
|
|
472
|
+
if (typeof objects === "string")
|
|
473
|
+
return { ok: false, message: `A commit couldn't be verified: ${objects}.` };
|
|
474
|
+
const listed = new Set(inside.map(row => row.id));
|
|
475
|
+
const commits = [];
|
|
476
|
+
for (const row of inside) {
|
|
477
|
+
const object = objects.get(row.id);
|
|
478
|
+
const commit = object.type === "commit" ? parseCommit(row.id, object.bytes, hexLength) : null;
|
|
479
|
+
if (commit === null)
|
|
480
|
+
return { ok: false, message: `${row.id.slice(0, 7)} isn't a commit.` };
|
|
481
|
+
// Git's parents must be the commit's own, and each one inside the range or where it stops.
|
|
482
|
+
if (commit.parents.join(" ") !== row.parents.join(" "))
|
|
483
|
+
return { ok: false, message: `Git's history of ${row.id.slice(0, 7)} isn't its own.` };
|
|
484
|
+
if (commit.parents.some(parent => !listed.has(parent) && !stops.has(parent)))
|
|
485
|
+
return { ok: false, message: `The history of ${row.id.slice(0, 7)} is incomplete.` };
|
|
486
|
+
commits.push(commit);
|
|
487
|
+
}
|
|
488
|
+
if (inside.length > 0 && !listed.has(to))
|
|
489
|
+
return { ok: false, message: `${to.slice(0, 7)} isn't in its own range.` };
|
|
490
|
+
// Where the range stops: each must be `from` or one of its verified ancestors. An empty range says `to` is already in
|
|
491
|
+
// the last release: that too is proved, never taken from Git's listing.
|
|
492
|
+
const wanted = [...stops, ...(inside.length === 0 ? [to] : [])].filter(one => one !== from);
|
|
493
|
+
if (wanted.length === 0)
|
|
494
|
+
return { ok: true, value: commits };
|
|
495
|
+
const before = listing([`--max-count=${reach}`, from, "--"]);
|
|
496
|
+
if (before === null)
|
|
497
|
+
return { ok: false, message: "The last release's history couldn't be listed." };
|
|
498
|
+
const ancestry = read(before.map(row => row.id));
|
|
499
|
+
if (typeof ancestry === "string")
|
|
500
|
+
return { ok: false, message: `The last release's history couldn't be verified: ${ancestry}.` };
|
|
501
|
+
const reached = new Set([from]);
|
|
502
|
+
const queue = [from];
|
|
503
|
+
while (queue.length > 0) {
|
|
504
|
+
const object = ancestry.get(queue.shift());
|
|
505
|
+
const commit = object === undefined || object.type !== "commit" ? null : parseCommit("", object.bytes, hexLength);
|
|
506
|
+
for (const parent of commit?.parents ?? [])
|
|
507
|
+
if (!reached.has(parent)) {
|
|
508
|
+
reached.add(parent);
|
|
509
|
+
queue.push(parent);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const unreached = wanted.find(one => !reached.has(one));
|
|
513
|
+
return unreached === undefined ? { ok: true, value: commits } : { ok: false, message: `${unreached.slice(0, 7)} isn't verified as part of the last release's history.` };
|
|
514
|
+
}
|
|
@@ -75,8 +75,8 @@ export declare const CONSOLE_FORMS: {
|
|
|
75
75
|
readonly term: FormContract<"password" | "program" | "repo">;
|
|
76
76
|
readonly skillsRevise: FormContract<"feedback" | "nonce" | "repo" | "run">;
|
|
77
77
|
readonly skillsChange: FormContract<"action" | "content" | "files" | "identity" | "method" | "nonce" | "repo" | "restore" | "revision" | "sample" | "sha" | "url">;
|
|
78
|
-
readonly kit: FormContract<"repo">;
|
|
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}`>;
|
|
78
|
+
readonly kit: FormContract<"password" | "repo">;
|
|
79
|
+
readonly subagents: FormContract<"choice" | "dailyTurns" | "id" | "manager" | "model" | "name" | "note" | "op" | "password" | "repo" | "schedule" | "soul" | "state" | "template" | "text" | "tool" | `field.${string}` | `over.${string}` | `undo.${string}` | `use.${string}`>;
|
|
80
80
|
readonly galleryUse: FormContract<string>;
|
|
81
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}`>;
|
|
82
82
|
readonly spendBudget: FormContract<"action" | "password" | "stop" | "target" | "usd">;
|
|
@@ -118,7 +118,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
118
118
|
readonly teams: FormContract<"app-id" | "password" | "secret" | "tenant">;
|
|
119
119
|
readonly discord: FormContract<"bot-token" | "password">;
|
|
120
120
|
readonly messaging: FormContract<"primary">;
|
|
121
|
-
readonly permissionDefault: FormContract<"permission-mode">;
|
|
121
|
+
readonly permissionDefault: FormContract<"password" | "permission-mode">;
|
|
122
122
|
readonly qualityDefault: FormContract<"quality-mode">;
|
|
123
123
|
readonly notifications: FormContract<"digest" | "mode" | "screenshots">;
|
|
124
124
|
readonly notificationsMute: FormContract<"pings" | "repo">;
|
|
@@ -84,9 +84,9 @@ export const CONSOLE_FORMS = {
|
|
|
84
84
|
term: formContract("POST /term/start, /term/<terminal>/close, /term/<terminal>/remove", ["repo", "program", "password"]),
|
|
85
85
|
skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
|
|
86
86
|
skillsChange: formContract("POST /settings/skills/import, /settings/skills/change", ["repo", "identity", "revision", "method", "content", "files", "url", "action", "sha", "sample", "nonce", "restore"]),
|
|
87
|
-
kit: formContract("POST /kits/<kit>/<act>", ["repo"]),
|
|
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."] }),
|
|
89
|
-
galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed"], { dynamic: true }),
|
|
87
|
+
kit: formContract("POST /kits/<kit>/<act>", ["repo", "password"]),
|
|
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", "password"], { prefixes: ["use.", "undo.", "field.", "over."] }),
|
|
89
|
+
galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed", "password"], { dynamic: true }),
|
|
90
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."] }),
|
|
91
91
|
spendBudget: formContract("POST /spend/budget", ["password", "target", "action", "usd", "stop"]),
|
|
92
92
|
projectConcurrency: formContract("POST /settings/project/concurrency", ["repo", "concurrency"]),
|
|
@@ -127,7 +127,7 @@ export const CONSOLE_FORMS = {
|
|
|
127
127
|
teams: formContract("POST /settings/teams/connect|pair|unpair|disconnect|alerts", ["password", "app-id", "tenant", "secret"]),
|
|
128
128
|
discord: formContract("POST /settings/discord/connect|pair|unpair|disconnect|alerts", ["password", "bot-token"]),
|
|
129
129
|
messaging: formContract("POST /settings/messaging", ["primary"]),
|
|
130
|
-
permissionDefault: formContract("POST /settings/permission-default", ["permission-mode"]),
|
|
130
|
+
permissionDefault: formContract("POST /settings/permission-default", ["permission-mode", "password"]),
|
|
131
131
|
qualityDefault: formContract("POST /settings/quality-default", ["quality-mode"]),
|
|
132
132
|
notifications: formContract("POST /settings/notifications", ["mode", "digest", "screenshots"]),
|
|
133
133
|
notificationsMute: formContract("POST /settings/notifications/mute", ["repo", "pings"]),
|
|
@@ -13,6 +13,7 @@ export declare const releasePolicySchema: z.ZodObject<{
|
|
|
13
13
|
files: z.ZodInt;
|
|
14
14
|
churn: z.ZodInt;
|
|
15
15
|
}, z.core.$strict>;
|
|
16
|
+
unsandboxedCarry: z.ZodOptional<z.ZodBoolean>;
|
|
16
17
|
}, z.core.$strict>;
|
|
17
18
|
export type ReleasePolicy = z.infer<typeof releasePolicySchema>;
|
|
18
19
|
export declare const releaseTermsSchema: z.ZodObject<{
|
|
@@ -35,12 +36,17 @@ export declare const releaseTermsSchema: z.ZodObject<{
|
|
|
35
36
|
files: z.ZodInt;
|
|
36
37
|
churn: z.ZodInt;
|
|
37
38
|
}, z.core.$strict>;
|
|
39
|
+
unsandboxedCarry: z.ZodOptional<z.ZodBoolean>;
|
|
38
40
|
}, z.core.$strict>;
|
|
39
41
|
grant: z.ZodObject<{
|
|
40
42
|
id: z.ZodInt;
|
|
41
43
|
digest: z.ZodString;
|
|
42
44
|
}, z.core.$strict>;
|
|
43
|
-
access: z.
|
|
45
|
+
access: z.ZodEnum<{
|
|
46
|
+
sandboxed: "sandboxed";
|
|
47
|
+
"sign-ins": "sign-ins";
|
|
48
|
+
unsandboxed: "unsandboxed";
|
|
49
|
+
}>;
|
|
44
50
|
expiresAt: z.ZodISODateTime;
|
|
45
51
|
}, z.core.$strict>;
|
|
46
52
|
export type ReleaseTerms = z.infer<typeof releaseTermsSchema>;
|
|
@@ -17,6 +17,9 @@ export const releasePolicySchema = versioned(1, {
|
|
|
17
17
|
protectProject: z.boolean(),
|
|
18
18
|
protectedPaths: z.array(z.string().min(1).max(400)).max(200),
|
|
19
19
|
limits: z.strictObject({ lines: z.int().min(0).max(1_000_000), files: z.int().min(0).max(100_000), churn: z.int().min(0).max(1_000_000) }),
|
|
20
|
+
/** "Carry release approvals when checks aren't sandboxed" (a person's project setting, off by default). Absent (a
|
|
21
|
+
* policy sealed before it existed): off. */
|
|
22
|
+
unsandboxedCarry: z.boolean().optional(),
|
|
20
23
|
});
|
|
21
24
|
const sha = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
|
|
22
25
|
export const releaseTermsSchema = versioned(1, {
|
|
@@ -29,8 +32,9 @@ export const releaseTermsSchema = versioned(1, {
|
|
|
29
32
|
approvedTree: sha,
|
|
30
33
|
policy: releasePolicySchema,
|
|
31
34
|
grant: z.strictObject({ id: z.int(), digest: z.string().min(1).max(128) }),
|
|
32
|
-
/** The check access the release check runs with:
|
|
33
|
-
|
|
35
|
+
/** The check access the release check runs with at approval: the fence, or (only with unsandboxedCarry sealed in the
|
|
36
|
+
* policy) a wider one. Never wider later: a check that reaches further than this stops the release. */
|
|
37
|
+
access: z.enum(["sandboxed", "sign-ins", "unsandboxed"]),
|
|
34
38
|
expiresAt: z.iso.datetime(),
|
|
35
39
|
});
|
|
36
40
|
export function readReleaseTerms(input) {
|
package/dist/deploy-gate.js
CHANGED
|
@@ -18,8 +18,11 @@ export function deploymentGate(store, input) {
|
|
|
18
18
|
if (run === null || ref === null || head === null)
|
|
19
19
|
return refuse(`Run ${input.runId} is not a built candidate.`);
|
|
20
20
|
// `task regate` reruns the approved check on the last built commit and records no-change: a candidate only for that commit.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A prepared candidate's build records a commit of its own holding exactly the candidate's tree; its rerun under the same
|
|
22
|
+
// signed scope is the same check of that commit (the digest is held to the approved one below).
|
|
23
|
+
const built = store.runsFor(run.taskRef).find(one => one.id < input.runId && one.role === "builder" && one.outcome === "built");
|
|
24
|
+
const regated = run.outcome === "no-change" && built?.headRevision === head &&
|
|
25
|
+
(scope?.candidate === head || ((scope?.candidate ?? null) !== null && built.scopeDigest === run.scopeDigest));
|
|
23
26
|
if (run.role !== "builder" || !(run.outcome === "built" || regated))
|
|
24
27
|
return refuse(`Run ${input.runId} is not a built candidate at ${head.slice(0, 7)}.`);
|
|
25
28
|
if (scope === null || scope.approvedDigest !== scope.digest || scope.digest !== run.scopeDigest)
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
95
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
96
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
+
version?: never;
|
|
97
98
|
active: boolean;
|
|
98
99
|
phase: string;
|
|
99
100
|
detail: string;
|
|
@@ -107,7 +108,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
107
108
|
wasRunning?: never;
|
|
108
109
|
backupPath?: never;
|
|
109
110
|
updateId?: never;
|
|
110
|
-
version?: never;
|
|
111
111
|
buildId?: never;
|
|
112
112
|
workDir?: never;
|
|
113
113
|
} | {
|
package/dist/dispose.js
CHANGED
|
@@ -23,7 +23,7 @@ import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
|
23
23
|
import {} from "./proof.js";
|
|
24
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
25
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
26
|
-
import { propose, approve } from "./scope.js";
|
|
26
|
+
import { propose, approve, liveReleaseOf } from "./scope.js";
|
|
27
27
|
import { headWithin } from "./names.js";
|
|
28
28
|
/** The reasons tick classifies as the attempt itself breaking. */
|
|
29
29
|
const TICK_FAILURE_REASONS = new Set([
|
|
@@ -456,6 +456,35 @@ export function regateTask(store, taskId, now, approval) {
|
|
|
456
456
|
return { ok: false, reason: "no-attempt", message: `${taskId} has no scope` };
|
|
457
457
|
const head = last.headRevision;
|
|
458
458
|
const actor = approval.kind === "operator" ? approval.name : `mode ${approval.name}`;
|
|
459
|
+
// A prepared candidate (a release gate, the nightly check) already names the approved commit, and the builder loads
|
|
460
|
+
// exactly its tree on every attempt. The rerun keeps that scope and its approval as they are: nothing is proposed or
|
|
461
|
+
// sealed again, so a regate can never swap the approved commit, its release terms or the person's yes for others
|
|
462
|
+
// (2026-10-27: a regate rewrote release-0.9.73's candidate to its own build commit and the release refused it).
|
|
463
|
+
if ((scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null) {
|
|
464
|
+
const candidate = scope.candidate ?? null;
|
|
465
|
+
if (candidate === null || scope.approvedDigest !== scope.digest || last.scopeDigest !== scope.digest) {
|
|
466
|
+
return { ok: false, reason: "not-approved", message: `${taskId}'s approved commit or scope changed since attempt #${last.id}, or its approval no longer stands; approve its scope again instead` };
|
|
467
|
+
}
|
|
468
|
+
// A release's task runs again only while its one approval still stands for it (the dispatch would refuse it anyway).
|
|
469
|
+
const release = liveReleaseOf(store, taskId, "dispatch", now);
|
|
470
|
+
if (release.applies && !release.ok)
|
|
471
|
+
return { ok: false, reason: "not-approved", message: `${taskId} can't run again: ${release.why}` };
|
|
472
|
+
// Any other seal stands only while its authority does (an expired mode, a revoked approver): the queue would never
|
|
473
|
+
// dispatch it, so the regate says so instead of queueing a rerun that can't start.
|
|
474
|
+
if (!store.modeApprovalLive(ref.id, now))
|
|
475
|
+
return { ok: false, reason: "not-approved", message: `${taskId}'s approval no longer stands; approve its scope again instead` };
|
|
476
|
+
return store.transact(() => {
|
|
477
|
+
if (store.getTask(taskId)?.state !== "queued") {
|
|
478
|
+
const queued = store.requeueTask(taskId, actor, now);
|
|
479
|
+
if (!queued.ok) {
|
|
480
|
+
const why = { claimed: "claimed", "accepted-result": "accepted-result", published: "published" };
|
|
481
|
+
return { ok: false, reason: why[queued.reason] ?? "not-rejected", message: `${taskId} could not be queued again: ${queued.reason}` };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
store.addRunNote(last.id, "Toolroll", `The check will run again on the approved commit ${candidate.slice(0, 7)}, under the same approval, as a new attempt, ${approval.kind === "operator" ? `on ${approval.name}'s say-so` : "automatically once, because the failure was outside this change"}.`, now);
|
|
485
|
+
return { ok: true, run: last.id, head: candidate };
|
|
486
|
+
});
|
|
487
|
+
}
|
|
459
488
|
return store.transact(() => {
|
|
460
489
|
const proposed = propose(store, {
|
|
461
490
|
taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd,
|
|
@@ -520,7 +549,11 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
|
|
|
520
549
|
// to a person. No repair draft, no third pass, on either road.
|
|
521
550
|
{
|
|
522
551
|
const rerunTask = store.refById(run.taskRef)?.externalId ?? "";
|
|
523
|
-
|
|
552
|
+
// A prepared candidate's attempts each record their own commit of the candidate's tree, so a rerun under the same
|
|
553
|
+
// approved scope is the same commit's check even when its head isn't the candidate itself.
|
|
554
|
+
const rerunScope = store.getScope(rerunTask);
|
|
555
|
+
const sameCommit = rerunScope !== null && (rerunScope.candidate === run.headRevision || ((rerunScope.candidate ?? null) !== null && rerunScope.digest === run.scopeDigest));
|
|
556
|
+
if (run.headRevision && sameCommit && store.builderAttemptsAt(run.taskRef, run.headRevision) >= 2) {
|
|
524
557
|
const contradicted = cause === "review";
|
|
525
558
|
store.enqueueNotification({ source: { run: sourceRunId }, dedupeKey: `regate-failed:${sourceRunId}`, kind: "repair-evidence", pushClass: "attention",
|
|
526
559
|
subject: contradicted ? "The rerun on the same commit was contradicted by its review" : "The project check failed again on the same commit",
|
package/dist/exec.d.ts
CHANGED
|
@@ -200,7 +200,7 @@ export declare function confined(file: string, args: readonly string[], bag: {
|
|
|
200
200
|
env: Record<string, string>;
|
|
201
201
|
};
|
|
202
202
|
/** A spawn's environment with what its sandbox needs added (nothing added: the caller's, as it was). */
|
|
203
|
-
export declare function withSandboxEnv<T extends Record<string, string | undefined>>(env: T | undefined, added: Record<string, string
|
|
203
|
+
export declare function withSandboxEnv<T extends Record<string, string | undefined>>(env: T | undefined, added: Record<string, string>, fenced?: boolean): T | Record<string, string | undefined> | undefined;
|
|
204
204
|
/** Run IDs are local to a database. Connections to the same file share
|
|
205
205
|
* custody; separate files and separate in-memory databases never do. */
|
|
206
206
|
export declare function runOwnerTag(store: Store, runId: number): string;
|
package/dist/exec.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ACP_LINE_CAP, createAcpConversation } from "./acp-client.js";
|
|
10
10
|
import { linuxFenceAvailable, macosFenceAvailable, sandboxedSpawn } from "./agent-fence.js";
|
|
11
|
+
import { withoutProtectedEnv } from "./protected-storage.js";
|
|
11
12
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
12
13
|
import { readlinkSync, realpathSync } from "node:fs";
|
|
13
14
|
import { observeProcessTree, stopProcessTree } from "./process-tree.js";
|
|
@@ -15,6 +16,7 @@ import { jsonlDiscriminants } from "./jsonl-discriminants.js";
|
|
|
15
16
|
import { createContainer, currentContainment } from "./containment.js";
|
|
16
17
|
import { claudeLimitsOf, noteLimits } from "./provider-limits.js";
|
|
17
18
|
import { sleep as defaultSleep } from "./sleep.js";
|
|
19
|
+
import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
|
|
18
20
|
/** Shell convention for "could not be executed": the refusal before any target ran. */
|
|
19
21
|
export const CONTAINMENT_REFUSED_CODE = 126;
|
|
20
22
|
/** Live provider children, for the deterministic stop. Registered only when `processGroup` was set. */
|
|
@@ -75,7 +77,10 @@ export function confined(file, args, bag) {
|
|
|
75
77
|
return { file, args: [...args], env: {} };
|
|
76
78
|
}
|
|
77
79
|
/** A spawn's environment with what its sandbox needs added (nothing added: the caller's, as it was). */
|
|
78
|
-
export function withSandboxEnv(env, added) {
|
|
80
|
+
export function withSandboxEnv(env, added, fenced = false) {
|
|
81
|
+
// A fenced launch never gets a chat or vault variable (protected-storage.ts), whoever made its environment.
|
|
82
|
+
if (fenced)
|
|
83
|
+
return withoutProtectedEnv({ ...(env ?? process.env), ...added });
|
|
79
84
|
return Object.keys(added).length === 0 ? env : { ...(env ?? process.env), ...added };
|
|
80
85
|
}
|
|
81
86
|
/** A spawn callback refused custody AFTER the child existed: the caller
|
|
@@ -107,7 +112,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
107
112
|
// The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
|
|
108
113
|
const sandboxed = confined(file, args, { ...bag, cwd: spawnOptions.cwd, temp: spawnOptions.env?.["TMPDIR"] });
|
|
109
114
|
({ file, args } = sandboxed);
|
|
110
|
-
spawnOptions = { ...spawnOptions, env: withSandboxEnv(spawnOptions.env, sandboxed.env) };
|
|
115
|
+
spawnOptions = { ...spawnOptions, env: withSandboxEnv(spawnOptions.env, sandboxed.env, (bag.fence?.length ?? 0) > 0) };
|
|
111
116
|
// The policy refusal comes first: nothing is reserved, recorded or
|
|
112
117
|
// spawned for a spawn that cannot be contained as required.
|
|
113
118
|
const effective = contain ? currentContainment() : null;
|
|
@@ -583,7 +588,7 @@ export function run(file, args, options = {}) {
|
|
|
583
588
|
encoding: "utf8",
|
|
584
589
|
shell: false,
|
|
585
590
|
windowsHide: true,
|
|
586
|
-
...((env => env === undefined ? {} : { env }))(withSandboxEnv(childEnv, wrapped.env)),
|
|
591
|
+
...((env => env === undefined ? {} : { env }))(withSandboxEnv(childEnv, wrapped.env, (options.fence?.length ?? 0) > 0)),
|
|
587
592
|
}, (error, stdout, stderr) => {
|
|
588
593
|
if (error === null) {
|
|
589
594
|
resolve({ result: { code: 0, stdout, stderr, timedOut: false, notFound: false }, transient: false });
|
|
@@ -1756,23 +1761,17 @@ function syntheticMessageLine(content, truncated) {
|
|
|
1756
1761
|
*
|
|
1757
1762
|
* - a bounded identity witness for `system`/`init`: the first init signal,
|
|
1758
1763
|
* the first usable session id, and one conflicting id
|
|
1759
|
-
* - the FIRST primary `result` event, selected STRUCTURALLY (finding 10
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
1764
|
+
* - the FIRST primary `result` event, selected STRUCTURALLY (finding 10;
|
|
1765
|
+
* stream-scope.ts `primaryResultEvent`): top-level, and origin absent or
|
|
1766
|
+
* `origin.kind === "human"` — an allowlist, so a subagent's or background
|
|
1767
|
+
* task's result (or any future origin kind) can never be mistaken for the
|
|
1768
|
+
* main query's accounting. Later results are dropped. A subagent's init
|
|
1769
|
+
* never enters the identity witness.
|
|
1763
1770
|
*
|
|
1764
1771
|
* The result's stdout is the retained lines joined — a synthetic, bounded
|
|
1765
1772
|
* envelope `claudeParse` reads exactly like test fixtures. Timeout,
|
|
1766
1773
|
* process-group, kill, and spawn semantics match `runStreamJsonl`.
|
|
1767
1774
|
*/
|
|
1768
|
-
function primaryResultOrigin(event) {
|
|
1769
|
-
const origin = event["origin"];
|
|
1770
|
-
if (origin === undefined || origin === null)
|
|
1771
|
-
return true;
|
|
1772
|
-
if (typeof origin === "object" && String(origin["kind"] ?? "") === "human")
|
|
1773
|
-
return true;
|
|
1774
|
-
return false; // task-notification, channel, peer, coordinator, unknown: never the envelope
|
|
1775
|
-
}
|
|
1776
1775
|
/** The usage fields Claude reports per message (not per session): a live session's account adds them up. */
|
|
1777
1776
|
const LIVE_SUMMED_USAGE = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"];
|
|
1778
1777
|
/**
|
|
@@ -1930,7 +1929,10 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1930
1929
|
if (event === null || typeof event !== "object")
|
|
1931
1930
|
return;
|
|
1932
1931
|
const type = String(event["type"] ?? "");
|
|
1933
|
-
|
|
1932
|
+
// A subagent's init, words and result are not the session's (stream-scope.ts): they never witness its identity,
|
|
1933
|
+
// name its session, fire its receipt or stand as its result. They still reach the observers below, which filter
|
|
1934
|
+
// for themselves.
|
|
1935
|
+
if (type === "system" && String(event["subtype"] ?? "") === "init" && topLevelEvent(event)) {
|
|
1934
1936
|
if (initLine === null)
|
|
1935
1937
|
initLine = line;
|
|
1936
1938
|
const id = transportSessionId(event["session_id"]);
|
|
@@ -1959,15 +1961,14 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1959
1961
|
// Top-level only (finding 11): assistant events carry no origin;
|
|
1960
1962
|
// parent_tool_use_id is the correlation field, and a subagent's
|
|
1961
1963
|
// words prove nothing about the main prompt.
|
|
1962
|
-
|
|
1963
|
-
if (parent === undefined || parent === null)
|
|
1964
|
+
if (topLevelEvent(event))
|
|
1964
1965
|
fireReceipt();
|
|
1965
1966
|
}
|
|
1966
1967
|
else if (type === "rate_limit_event") {
|
|
1967
1968
|
// v105: the plan's usage windows, as Claude says them each turn (the Tasks page's limits).
|
|
1968
1969
|
noteLimits(claudeLimitsOf(event));
|
|
1969
1970
|
}
|
|
1970
|
-
else if (type === "result" && (resultLine === null || live !== null) &&
|
|
1971
|
+
else if (type === "result" && (resultLine === null || live !== null) && primaryResultEvent(event)) {
|
|
1971
1972
|
// A live session ends a turn with a result each time: the run's account is the last one, with every main
|
|
1972
1973
|
// result's usage and turns added up (account). After an oversized main result the retained result stays the
|
|
1973
1974
|
// fail-closed overflow, but later results still count toward its cost.
|
package/dist/first-run.d.ts
CHANGED
|
@@ -77,10 +77,13 @@ export declare function firstResultWords(since: string, at: string): string | nu
|
|
|
77
77
|
export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
|
|
78
78
|
export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
|
|
79
79
|
/** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
|
|
80
|
+
/** `skipped`: a stage the task went past without it happening (its checks didn't run or couldn't be read). `needsYou`:
|
|
81
|
+
* the current or stuck step waits on a person, so it is the one step drawn amber. */
|
|
80
82
|
export type JourneyStep = {
|
|
81
83
|
key: JourneyKey;
|
|
82
84
|
label: string;
|
|
83
|
-
state: "done" | "current" | "stuck" | "todo";
|
|
85
|
+
state: "done" | "current" | "stuck" | "skipped" | "todo";
|
|
86
|
+
needsYou?: true;
|
|
84
87
|
};
|
|
85
88
|
/**
|
|
86
89
|
* Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
|
|
@@ -91,4 +94,17 @@ export declare function firstTaskJourney(read: {
|
|
|
91
94
|
stage: "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
92
95
|
need?: string | undefined;
|
|
93
96
|
}, approved: boolean, built?: boolean): JourneyStep[];
|
|
97
|
+
/** What a task's checks recorded, for its road: `null` when nothing says (the step is then shown skipped, never done). */
|
|
98
|
+
export type RoadChecks = "passed" | "running" | "failed" | "not-run" | "unread" | null;
|
|
99
|
+
/**
|
|
100
|
+
* Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
|
|
101
|
+
* from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
|
|
102
|
+
* stands there again); a task that never needed one shows it skipped. Checks is done only when a check passed; a
|
|
103
|
+
* running, failed, missing or unread check says so, and with no checks fact it is skipped. A research report has
|
|
104
|
+
* no Checks step: a report stopped there stops at Ready. The step that waits on a person carries `needsYou`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function taskRoad(read: Parameters<typeof firstTaskJourney>[0], approved: boolean, built: boolean, facts: {
|
|
107
|
+
report?: boolean;
|
|
108
|
+
checks?: RoadChecks;
|
|
109
|
+
}): JourneyStep[];
|
|
94
110
|
export {};
|
package/dist/first-run.js
CHANGED
|
@@ -141,3 +141,41 @@ export function firstTaskJourney(read, approved, built = false) {
|
|
|
141
141
|
return JOURNEY_STEPS.map(([key, label], index) => ({ key, label,
|
|
142
142
|
state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
|
|
146
|
+
* from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
|
|
147
|
+
* stands there again); a task that never needed one shows it skipped. Checks is done only when a check passed; a
|
|
148
|
+
* running, failed, missing or unread check says so, and with no checks fact it is skipped. A research report has
|
|
149
|
+
* no Checks step: a report stopped there stops at Ready. The step that waits on a person carries `needsYou`.
|
|
150
|
+
*/
|
|
151
|
+
export function taskRoad(read, approved, built, facts) {
|
|
152
|
+
const replan = read.stage === "needs-you" && read.need === "replan";
|
|
153
|
+
const plainApproved = approved && !replan;
|
|
154
|
+
let steps = firstTaskJourney(replan ? { stage: "needs-you", need: "approval" } : read, plainApproved, built);
|
|
155
|
+
steps = steps.map(step => {
|
|
156
|
+
if (step.key === "approve" && step.state === "done" && !plainApproved)
|
|
157
|
+
return { ...step, state: "skipped" };
|
|
158
|
+
if (step.key !== "checks" || (step.state !== "done" && step.state !== "current"))
|
|
159
|
+
return step;
|
|
160
|
+
// At Checks now: only a recorded failure or a running check changes how it reads.
|
|
161
|
+
if (step.state === "current")
|
|
162
|
+
return facts.checks === "failed" ? { ...step, state: "stuck", label: "Checks failed" } : facts.checks === "running" ? { ...step, label: "Checks running" } : step;
|
|
163
|
+
switch (facts.checks ?? null) {
|
|
164
|
+
case "passed": return step;
|
|
165
|
+
case "running": return { ...step, state: "current", label: "Checks running" };
|
|
166
|
+
case "failed": return { ...step, state: "stuck", label: "Checks failed" };
|
|
167
|
+
case "not-run": return { ...step, state: "skipped", label: "Checks not run" };
|
|
168
|
+
case "unread": return { ...step, state: "skipped", label: "Checks unread" };
|
|
169
|
+
default: return { ...step, state: "skipped" };
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (facts.report === true) {
|
|
173
|
+
const checks = steps.find(step => step.key === "checks");
|
|
174
|
+
if (checks !== undefined && (checks.state === "current" || checks.state === "stuck")) {
|
|
175
|
+
steps = steps.map(step => step.key === "ready" ? { ...step, state: checks.state } : step);
|
|
176
|
+
}
|
|
177
|
+
steps = steps.filter(step => step.key !== "checks");
|
|
178
|
+
}
|
|
179
|
+
const waiting = read.stage === "needs-you";
|
|
180
|
+
return steps.map(step => waiting && (step.state === "current" || step.state === "stuck") ? { ...step, needsYou: true } : step);
|
|
181
|
+
}
|
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* A template's tools show their marks and whether the project has them; one it lacks offers Connect first.
|
|
6
6
|
*/
|
|
7
7
|
import { brandMarkHtml } from "./brand-mark.js";
|
|
8
|
-
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
8
|
+
import { BLANK, GALLERY, GALLERY_GROUPS, galleryAdmitsAnyone, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
|
|
9
9
|
import { choiceTargets } from "./flows.js";
|
|
10
10
|
import { html, joinHtml, postForm, reauth } from "./html.js";
|
|
11
11
|
export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
|
|
@@ -107,7 +107,9 @@ export function galleryUseHtml(input) {
|
|
|
107
107
|
: html `<input type="hidden" name="repo" value="${input.repo}">`;
|
|
108
108
|
const preview = input.preview === null ? "" : html `<section class="gallery-preview" aria-labelledby="gallery-will"><h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="gallery-never">${input.preview.built.never}</p>${needing}${input.preview.startsFrom.length === 0 ? html `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => html `<p class="meta">Starts from: ${one}</p>`)}<details><summary>Every step</summary><ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol></details></section>`;
|
|
109
109
|
const sendResult = !sendsAlready(template) && html `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`;
|
|
110
|
-
|
|
110
|
+
// A flow that brings in items from anyone takes the password to make (chat-management.md §0a.3).
|
|
111
|
+
const step = input.preview !== null && galleryAdmitsAnyone(template) ? reauth(html `<label>Your password <span class="meta">(it brings in items from anyone)</span><input type="password" name="password" autocomplete="current-password"></label>`) : "";
|
|
112
|
+
const actions = html `${step}<p class="gallery-actions">${input.preview !== null && html `<button type="submit" name="intent" value="create"${missing.length > 0 && html ` class="secondary" data-without-tools`}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>`;
|
|
111
113
|
const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(field)}<label>Name<input name="name" value="${input.name}" maxlength="80"></label>${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true }, hidden: { previewed: input.preview?.digest ?? "" } });
|
|
112
114
|
return html `<section class="gallery-use" data-gallery-template="${template.id}">${input.problem !== null && html `<p class="problem" role="alert">${input.problem}</p>`}${input.said ? html `<p role="status">${input.said}</p>` : ""}<p>${template.promise}</p>${input.diagram !== null && stepStrip(input.diagram)}${needsList(template)}${toolsBlock}${form}<p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
113
115
|
}
|
package/dist/flow-gallery.d.ts
CHANGED
|
@@ -136,9 +136,12 @@ export type GalleryUsed = {
|
|
|
136
136
|
said: string;
|
|
137
137
|
};
|
|
138
138
|
/** Use a template in a project: its scripts, its flow and its triggers, together or not at all. */
|
|
139
|
+
/** Whether a template's triggers bring in items from anyone: making it takes the person's password. */
|
|
140
|
+
export declare function galleryAdmitsAnyone(template: GalleryTemplate): boolean;
|
|
139
141
|
export declare function useGalleryTemplate(store: Store, template: GalleryTemplate, repo: string, given: GalleryAnswers, input: {
|
|
140
142
|
name?: string | null;
|
|
141
143
|
by: string;
|
|
142
144
|
now: Date;
|
|
143
145
|
dir: string | null;
|
|
146
|
+
widening?: "proved";
|
|
144
147
|
}): GalleryUsed;
|
package/dist/flow-gallery.js
CHANGED
|
@@ -20,7 +20,7 @@ import { scanForSecrets } from "./evidence.js";
|
|
|
20
20
|
import { flowDefinitionOf } from "./flow-engine.js";
|
|
21
21
|
import { saveScript, validateScript } from "./flow-scripts.js";
|
|
22
22
|
import { isToolrollRepo, planeReviewSteps } from "./flow-starters.js";
|
|
23
|
-
import { addFlowTriggerTo, describeTrigger, githubRepoOf, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
23
|
+
import { addFlowTriggerTo, describeTrigger, githubRepoOf, rawAdmitsAnyone, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
24
24
|
import { choiceTargets, clockTime, flowFromSteps, flowTerms, FLOW_TEMPLATES, ISSUE_LABEL } from "./flows.js";
|
|
25
25
|
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
26
26
|
import { publishingOf } from "./pull-request-flow.js";
|
|
@@ -640,6 +640,10 @@ export function galleryDigest(id, repo, answers) {
|
|
|
640
640
|
return createHash("sha256").update(JSON.stringify([id, repo, Object.entries(answers).sort(([a], [b]) => a.localeCompare(b))])).digest("hex").slice(0, 24);
|
|
641
641
|
}
|
|
642
642
|
/** Use a template in a project: its scripts, its flow and its triggers, together or not at all. */
|
|
643
|
+
/** Whether a template's triggers bring in items from anyone: making it takes the person's password. */
|
|
644
|
+
export function galleryAdmitsAnyone(template) {
|
|
645
|
+
return template.triggers.some(one => rawAdmitsAnyone(one));
|
|
646
|
+
}
|
|
643
647
|
export function useGalleryTemplate(store, template, repo, given, input) {
|
|
644
648
|
try {
|
|
645
649
|
const built = buildFromGallery(store, template, repo, given);
|
|
@@ -656,7 +660,7 @@ export function useGalleryTemplate(store, template, repo, given, input) {
|
|
|
656
660
|
const made = store.getFlow(id);
|
|
657
661
|
for (const trigger of built.triggers) {
|
|
658
662
|
// No one is shown the gallery's addresses: a webhook is made with none until its panel makes one (v126).
|
|
659
|
-
const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir, { shown: false });
|
|
663
|
+
const added = addFlowTriggerTo(store, made, { ...trigger, zone: trigger["zone"] ?? flowDefinitionOf(made).start }, input.by, input.now, input.dir, { shown: false, ...(input.widening === undefined ? {} : { widening: input.widening }) });
|
|
660
664
|
if (!added.ok)
|
|
661
665
|
throw new Error(added.message);
|
|
662
666
|
if (added.reveal !== null)
|
package/dist/flow-triggers.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export declare const senderOf: (config: Extract<TriggerConfig, {
|
|
|
57
57
|
/** Whether this trigger takes deliveries at a secret address rather than being checked. */
|
|
58
58
|
/** Whether a raw trigger the caller is about to add would take deliveries (an address): read leniently, before validation. */
|
|
59
59
|
export declare function rawTakesDeliveries(raw: unknown): boolean;
|
|
60
|
+
/** Whether a trigger brings in items from anyone (chat-management.md §0a.3): GitHub issues or pull requests from anyone,
|
|
61
|
+
* or mail from any sender. A widening: it takes a person's password, on every road that adds one. */
|
|
62
|
+
export declare function admitsAnyone(config: TriggerConfig): boolean;
|
|
63
|
+
/** The same, read leniently from a raw trigger before validation (so the form can ask for the password first). */
|
|
64
|
+
export declare function rawAdmitsAnyone(raw: unknown): boolean;
|
|
60
65
|
export declare function takesDeliveries(config: TriggerConfig): boolean;
|
|
61
66
|
/** A webhook's signing secret (made here, or pasted from its sender). In the keychain (or an owner-only file where there's none), never a column. */
|
|
62
67
|
export declare function saveHookSecret(dir: string, trigger: number, secret: string, credential?: string): void;
|
|
@@ -104,6 +109,9 @@ export type TriggerMade = {
|
|
|
104
109
|
export declare function addFlowTriggerTo(store: Store, flow: FlowRow, raw: unknown, actor: string, now: Date, dir: string | null, options?: {
|
|
105
110
|
shown?: boolean;
|
|
106
111
|
ingress?: "proved";
|
|
112
|
+
/** The person proved themselves for this act (a password, or a session under Passwordless): a trigger that admits
|
|
113
|
+
* anyone is added only then. */
|
|
114
|
+
widening?: "proved";
|
|
107
115
|
}): TriggerMade;
|
|
108
116
|
/** A new secret address for a webhook trigger (the old one stops working at once). */
|
|
109
117
|
export declare function renewFlowHook(store: Store, trigger: FlowTriggerRow, now: Date, dir: string): TriggerMade;
|