toolroll 0.9.72 → 0.9.73
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 +76 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/browser/workspace.js +28 -28
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- 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/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- 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/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +80 -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 +8 -2
- package/dist/release.js +79 -6
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- package/dist/server/settings.js +26 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +3 -0
- package/dist/store.js +6 -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/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release coordinator's Git, as one hardened runner's pieces (docs/plans/chat-management.md §0a.2): the recorded
|
|
3
|
+
* repository's shape, the programs by absolute path, the allowlisted environment, the fixed settings and the checkout
|
|
4
|
+
* config allowlist. Kept apart from release-adapters.ts so the release card (release-card.ts) reads Git the same way
|
|
5
|
+
* without loading the coordinator.
|
|
6
|
+
*/
|
|
7
|
+
import { accessSync, constants as fsConstants, statSync } from "node:fs";
|
|
8
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
9
|
+
import { HARDENED_GIT_ARGS } from "./agent-fence.js";
|
|
10
|
+
import { TEARDOWN_FLAGS } from "./teardown-git.js";
|
|
11
|
+
/** owner/name and transport of a recorded GitHub remote, read strictly (https or ssh to github.com, nothing else). */
|
|
12
|
+
export function releaseRemote(url) {
|
|
13
|
+
const part = "([A-Za-z0-9_.-]+)";
|
|
14
|
+
const https = new RegExp(`^https://github\\.com/${part}/${part}?(?:\\.git)?/?$`).exec(url);
|
|
15
|
+
const ssh = new RegExp(`^(?:ssh://)?git@github\\.com[:/]${part}/${part}?(?:\\.git)?/?$`).exec(url);
|
|
16
|
+
const match = https ?? ssh;
|
|
17
|
+
if (match === null || match[2] === undefined || match[1].startsWith(".") || match[2].startsWith("."))
|
|
18
|
+
return null;
|
|
19
|
+
return { slug: `${match[1]}/${match[2].replace(/\.git$/, "")}`, ssh: https === null };
|
|
20
|
+
}
|
|
21
|
+
const executable = (path) => { try {
|
|
22
|
+
accessSync(path, fsConstants.X_OK);
|
|
23
|
+
return statSync(path).isFile();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
} };
|
|
28
|
+
/** Fixed system folders: the only PATH the coordinator's Git and gh run with. */
|
|
29
|
+
export const RELEASE_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
30
|
+
/** Git, gh and ssh by absolute path: the system's Git (or the first in the fixed system folders), the installed gh as
|
|
31
|
+
* this process's PATH finds it now, and the system ssh. Resolved once; never looked up again. */
|
|
32
|
+
export function releaseTools(path = process.env["PATH"] ?? "") {
|
|
33
|
+
const find = (name, dirs) => {
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
if (!isAbsolute(dir))
|
|
36
|
+
continue;
|
|
37
|
+
const candidate = join(dir, name);
|
|
38
|
+
if (executable(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
const system = ["/usr/bin", "/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
44
|
+
return {
|
|
45
|
+
git: find("git", system) ?? "/usr/bin/git",
|
|
46
|
+
gh: find("gh", [...path.split(delimiter), ...system]) ?? "/usr/bin/gh",
|
|
47
|
+
ssh: find("ssh", ["/usr/bin", "/bin"]) ?? "/usr/bin/ssh",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The coordinator's Git and gh environment: an allowlist, a fixed PATH, no system or global Git config, no prompts. */
|
|
51
|
+
export function releaseGitEnv(base = process.env) {
|
|
52
|
+
const kept = ["HOME", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "USER", "LOGNAME", "SSH_AUTH_SOCK", "GH_CONFIG_DIR", "XDG_CONFIG_HOME", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST"];
|
|
53
|
+
return {
|
|
54
|
+
...Object.fromEntries(kept.flatMap(name => base[name] === undefined ? [] : [[name, base[name]]])),
|
|
55
|
+
PATH: RELEASE_PATH, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0", GH_PROMPT_DISABLED: "1", GIT_ASKPASS: "", SSH_ASKPASS: "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Every coordinator Git command's fixed settings, ahead of its own arguments: the recorded Git directory and checkout,
|
|
59
|
+
* no hooks, fsmonitor, replacement objects, pager or submodule recursion, only https (and ssh for an ssh remote), the
|
|
60
|
+
* system ssh, and gh as the only credential helper. */
|
|
61
|
+
export function releaseGitArgs(repo, tools) {
|
|
62
|
+
return [`--git-dir=${repo.gitDir}`, `--work-tree=${repo.checkout}`, "--no-replace-objects", "--no-pager", ...HARDENED_GIT_ARGS, ...TEARDOWN_FLAGS,
|
|
63
|
+
"-c", "protocol.allow=never", "-c", "protocol.https.allow=always", ...(repo.ssh ? ["-c", "protocol.ssh.allow=always"] : []),
|
|
64
|
+
"-c", `core.sshCommand=${tools.ssh}`, "-c", "core.askPass=", "-c", "fetch.recurseSubmodules=false", "-c", "core.pager=cat",
|
|
65
|
+
// Where it connects and what it sends: verified TLS, no proxy, no extra header or cookie file. (An empty CA file
|
|
66
|
+
// or path can't be given here, Git then trusts nothing; a checkout setting either stops the release instead.)
|
|
67
|
+
"-c", "http.sslVerify=true", "-c", "http.proxy=", "-c", "http.extraHeader=", "-c", "http.cookieFile=",
|
|
68
|
+
// The recorded URL, as a remote that exists only here.
|
|
69
|
+
"-c", `remote.${RELEASE_REMOTE}.url=${repo.remote}`, "-c", `remote.${RELEASE_REMOTE}.pushurl=${repo.remote}`, "-c", `remote.${RELEASE_REMOTE}.proxy=`,
|
|
70
|
+
...(repo.ssh ? [] : ["-c", `credential.helper=!'${tools.gh.replace(/'/g, "'\\''")}' auth git-credential`])];
|
|
71
|
+
}
|
|
72
|
+
/** The named remote the coordinator fetches, looks and pushes through: defined only on its own command line (`-c`), so
|
|
73
|
+
* no checkout setting names or rewrites it. */
|
|
74
|
+
export const RELEASE_REMOTE = "toolroll-release";
|
|
75
|
+
/** The local and worktree settings a gate checkout may hold for a release (an allowlist): plain values Git writes
|
|
76
|
+
* itself and that never decide where it connects, what it sends or what it runs. Anything else stops the release. */
|
|
77
|
+
const RELEASE_KEYS = [
|
|
78
|
+
/^core\.(repositoryformatversion|filemode|bare|logallrefupdates|ignorecase|precomposeunicode|symlinks|autocrlf|eol|safecrlf|quotepath|longpaths|checkstat|trustctime)$/,
|
|
79
|
+
/^extensions\.(worktreeconfig|objectformat|refstorage)$/,
|
|
80
|
+
/^remote\.[^/:\\]+\.(url|fetch|pushurl|tagopt|prune)$/,
|
|
81
|
+
/^branch\..+\.(remote|merge|rebase|pushremote|description|vscode-merge-base)$/,
|
|
82
|
+
/^(user\.(name|email)|pull\.(rebase|ff)|init\.defaultbranch|lfs\.repositoryformatversion)$/,
|
|
83
|
+
];
|
|
84
|
+
/** The first local or worktree setting a gate checkout's config holds that a release won't run with, or null: one
|
|
85
|
+
* outside the allowlist (an include, a command, a proxy, TLS or header setting, a credential helper, a URL rewrite, a
|
|
86
|
+
* hooks folder), a remote named like a URL or after the coordinator's own, or a remote over a transport helper. */
|
|
87
|
+
export function releaseConfigProblem(listing) {
|
|
88
|
+
const fields = listing.split("\0");
|
|
89
|
+
for (let at = 0; at + 1 < fields.length; at += 2) {
|
|
90
|
+
if (fields[at] !== "local" && fields[at] !== "worktree")
|
|
91
|
+
continue;
|
|
92
|
+
const entry = fields[at + 1];
|
|
93
|
+
const newline = entry.indexOf("\n");
|
|
94
|
+
const key = (newline < 0 ? entry : entry.slice(0, newline)).toLowerCase();
|
|
95
|
+
const value = (newline < 0 ? "" : entry.slice(newline + 1)).trim();
|
|
96
|
+
if (key.includes(RELEASE_REMOTE))
|
|
97
|
+
return key;
|
|
98
|
+
if (key === "core.fsmonitor" && /^(false|no|off|0)$/i.test(value))
|
|
99
|
+
continue;
|
|
100
|
+
if (!RELEASE_KEYS.some(pattern => pattern.test(key)))
|
|
101
|
+
return key;
|
|
102
|
+
if (/^remote\./.test(key) && /^[A-Za-z][A-Za-z0-9+.-]*::/.test(value))
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* gate → approval → check → complete → quiet → deploy → link → ci → merge → tree → tag → publish → homebrew → done
|
|
7
7
|
*
|
|
8
|
-
* gate
|
|
8
|
+
* gate, before filing a task for that commit, looks at the pull request's required checks on it: one that failed stops
|
|
9
|
+
* the release before anything is filed or sent for approval; it waits, naming them, for checks not started or still
|
|
10
|
+
* running, and stops if they don't finish within its limit (a task already filed is not looked at again; ci checks
|
|
11
|
+
* once more before merging). It then files a prepared-candidate task pinned to the branch's exact commit, with the full
|
|
12
|
+
* release check. approval
|
|
9
13
|
* waits for an active approver's yes to that exact scope (never a mode's, an AI's or the lead's). check requires a Full
|
|
10
14
|
* result of that commit: a failed check stops the release and stays visible, nothing reruns it. complete marks that
|
|
11
15
|
* exact result Complete through the task's own authorization. quiet waits until nothing builds. deploy runs the
|
|
@@ -35,7 +39,7 @@ export declare const REQUIRED_PR_CHECKS: readonly ["node 22 · ubuntu-latest", "
|
|
|
35
39
|
export declare const RELEASE_TAG_CHECKS: readonly ["node 22 · macos-latest", "node 24 · macos-latest", "publish"];
|
|
36
40
|
export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
37
41
|
export type ReleaseStep = typeof RELEASE_STEPS[number];
|
|
38
|
-
type WaitingStep = "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
42
|
+
type WaitingStep = "gate" | "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
39
43
|
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
|
|
40
44
|
export declare const RELEASE_LIMITS: Readonly<Record<WaitingStep, number>>;
|
|
41
45
|
/** One check attempt; `app` is the GitHub App that made it (github-actions for a workflow), when known. */
|
|
@@ -467,4 +471,6 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
|
|
|
467
471
|
};
|
|
468
472
|
/** Run the release from wherever its journal stopped. */
|
|
469
473
|
export declare function runRelease(options: ReleaseOptions, adapters: ReleaseAdapters): Promise<ReleaseOutcome>;
|
|
474
|
+
/** A probe's failure that is only the network to GitHub being slow, never a decision (a Stop) or another error. */
|
|
475
|
+
export declare function transientNetworkError(error: unknown): boolean;
|
|
470
476
|
export {};
|
package/dist/release.js
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*
|
|
6
6
|
* gate → approval → check → complete → quiet → deploy → link → ci → merge → tree → tag → publish → homebrew → done
|
|
7
7
|
*
|
|
8
|
-
* gate
|
|
8
|
+
* gate, before filing a task for that commit, looks at the pull request's required checks on it: one that failed stops
|
|
9
|
+
* the release before anything is filed or sent for approval; it waits, naming them, for checks not started or still
|
|
10
|
+
* running, and stops if they don't finish within its limit (a task already filed is not looked at again; ci checks
|
|
11
|
+
* once more before merging). It then files a prepared-candidate task pinned to the branch's exact commit, with the full
|
|
12
|
+
* release check. approval
|
|
9
13
|
* waits for an active approver's yes to that exact scope (never a mode's, an AI's or the lead's). check requires a Full
|
|
10
14
|
* result of that commit: a failed check stops the release and stays visible, nothing reruns it. complete marks that
|
|
11
15
|
* exact result Complete through the task's own authorization. quiet waits until nothing builds. deploy runs the
|
|
@@ -45,7 +49,7 @@ export const REQUIRED_PR_CHECKS = [
|
|
|
45
49
|
export const RELEASE_TAG_CHECKS = ["node 22 · macos-latest", "node 24 · macos-latest", "publish"];
|
|
46
50
|
export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
47
51
|
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
|
|
48
|
-
export const RELEASE_LIMITS = { approval: 24 * 60, check: 120, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
|
|
52
|
+
export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 120, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
|
|
49
53
|
const POLL_MS = 15_000;
|
|
50
54
|
/** A stop the release reports: the step it stopped at and why, in words. */
|
|
51
55
|
class Stop extends Error {
|
|
@@ -177,16 +181,29 @@ export async function runRelease(options, adapters) {
|
|
|
177
181
|
delete j.intents.deploy;
|
|
178
182
|
save();
|
|
179
183
|
};
|
|
180
|
-
/** Ask `probe` until it answers, within this step's limit.
|
|
184
|
+
/** Ask `probe` until it answers, within this step's limit. A short network failure on the way to GitHub (git or gh
|
|
185
|
+
* timing out) is no answer yet, never the release's end: the next poll asks again, until the step's own limit. */
|
|
181
186
|
const waitFor = async (at, what, probe) => {
|
|
182
187
|
const deadline = adapters.now().getTime() + limits[at] * 60_000;
|
|
183
|
-
let said = false;
|
|
188
|
+
let said = false, unreachable = null;
|
|
184
189
|
for (;;) {
|
|
185
|
-
|
|
190
|
+
let answer;
|
|
191
|
+
try {
|
|
192
|
+
answer = await probe();
|
|
193
|
+
unreachable = null;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (!transientNetworkError(error))
|
|
197
|
+
throw error;
|
|
198
|
+
if (unreachable === null)
|
|
199
|
+
say(` GitHub didn't answer in time (${networkWords(error)}); asking again`);
|
|
200
|
+
unreachable = networkWords(error);
|
|
201
|
+
answer = null;
|
|
202
|
+
}
|
|
186
203
|
if (answer !== null)
|
|
187
204
|
return answer;
|
|
188
205
|
if (adapters.now().getTime() >= deadline)
|
|
189
|
-
throw new Stop("timeout", `Still waiting for ${what} after ${limits[at]} minutes.`);
|
|
206
|
+
throw new Stop("timeout", `Still waiting for ${what} after ${limits[at]} minutes${unreachable === null ? "" : ` (GitHub last didn't answer: ${unreachable})`}.`);
|
|
190
207
|
if (!said) {
|
|
191
208
|
say(` waiting for ${what} (up to ${limits[at]} min)`);
|
|
192
209
|
said = true;
|
|
@@ -464,11 +481,13 @@ export async function runRelease(options, adapters) {
|
|
|
464
481
|
if (existing.exists && j.freshTask === true && j.intents.gate === undefined)
|
|
465
482
|
throw new Stop("approval", `Task ${j.task.id} appeared before this release filed it; rerun with --new.`);
|
|
466
483
|
if (existing.exists) {
|
|
484
|
+
// Filed already (a resume after a lost answer): the task stands, and the ci step judges its checks.
|
|
467
485
|
if (existing.candidate !== j.candidate.sha)
|
|
468
486
|
throw new Stop("conflict", `Task ${j.task.id} exists for a different commit (${existing.candidate?.slice(0, 7) ?? "none"}).`);
|
|
469
487
|
j.task.scopeDigest = existing.scopeDigest;
|
|
470
488
|
}
|
|
471
489
|
else {
|
|
490
|
+
await earlyChecks(adapters, j, (what, probe) => waitFor("gate", what, probe));
|
|
472
491
|
intend("gate");
|
|
473
492
|
await adapters.repo.sync(j.branch);
|
|
474
493
|
const filed = await adapters.toolroll.fileGate({
|
|
@@ -886,3 +905,57 @@ async function deployed(adapters, candidate) {
|
|
|
886
905
|
const [runtime] = runtimes;
|
|
887
906
|
return runtime !== undefined && runtime !== "" && await adapters.toolroll.runtimeCommit(runtime) === candidate ? runtime : null;
|
|
888
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* The gate's look at the pull request's required checks on the exact commit, before anything is filed or anyone is
|
|
910
|
+
* asked to approve (#237): a required check that failed or was cancelled stops here; checks not started or still
|
|
911
|
+
* running are named, and the gate waits for them; ones that never finish within the gate's limit stop here too. GitHub
|
|
912
|
+
* not answering is waited out the same way, never taken for a pass. The ci step still checks again before merging.
|
|
913
|
+
*/
|
|
914
|
+
async function earlyChecks(adapters, j, wait) {
|
|
915
|
+
const sha = j.candidate.sha.slice(0, 7);
|
|
916
|
+
const required = REQUIRED_PR_CHECKS;
|
|
917
|
+
let named = "", unread = false;
|
|
918
|
+
try {
|
|
919
|
+
await wait(`the pull request checks on ${sha}`, async () => {
|
|
920
|
+
let runs;
|
|
921
|
+
try {
|
|
922
|
+
runs = await adapters.github.checks(j.github, j.candidate.sha);
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
if (!unread)
|
|
926
|
+
adapters.say(` Couldn't read the pull request checks on ${sha} (${error?.message ?? error}); trying again.`);
|
|
927
|
+
unread = true;
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
const verdict = checksVerdict(runs.filter(run => required.includes(run.name)), required);
|
|
931
|
+
if (verdict.state === "failed") {
|
|
932
|
+
throw new Stop("ci-failed", `Pull request checks already failed on ${sha}: ${verdict.failed.join(", ")}. Nothing was filed or sent for approval. Fix the branch, then release the new commit with \`toolroll release ${j.branch} --repo ${j.checkout} --new\` (or rerun the failed job on GitHub and run this command again).`);
|
|
933
|
+
}
|
|
934
|
+
if (verdict.state === "passed")
|
|
935
|
+
return true;
|
|
936
|
+
const now = verdict.waiting.join(", ");
|
|
937
|
+
if (now !== named) {
|
|
938
|
+
adapters.say(` Pull request checks on ${sha} not finished yet: ${now}.`);
|
|
939
|
+
named = now;
|
|
940
|
+
}
|
|
941
|
+
return null;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (!(error instanceof Stop) || error.reason !== "timeout")
|
|
946
|
+
throw error;
|
|
947
|
+
throw new Stop("ci-missing", `Pull request checks on ${sha} didn't all finish in time${named === "" ? (unread ? " (GitHub didn't answer)" : "") : `: ${named}`}. Nothing was filed or sent for approval. Start or rerun them on GitHub, then run this command again.`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/** The network failures a release's waits ride out: git or gh timing out (or cut off) on the way to GitHub. */
|
|
951
|
+
const TRANSIENT_NETWORK = /Recv failure: (?:Operation timed out|Connection reset by peer)|TLS handshake timeout|Operation timed out|Connection timed out|i\/o timeout|Connection reset by peer|Failed to connect to [^\s]*github\.com port \d+/i;
|
|
952
|
+
/** A probe's failure that is only the network to GitHub being slow, never a decision (a Stop) or another error. */
|
|
953
|
+
export function transientNetworkError(error) {
|
|
954
|
+
if (error instanceof Stop || error instanceof Restart)
|
|
955
|
+
return false;
|
|
956
|
+
return TRANSIENT_NETWORK.test(String(error?.message ?? error));
|
|
957
|
+
}
|
|
958
|
+
/** The part of a network failure worth saying: its matching phrase. */
|
|
959
|
+
function networkWords(error) {
|
|
960
|
+
return TRANSIENT_NETWORK.exec(String(error?.message ?? error))?.[0] ?? "a network failure";
|
|
961
|
+
}
|
package/dist/scope.d.ts
CHANGED
|
@@ -945,3 +945,10 @@ export declare function liveReleaseOf(store: Store, taskId: string, action: Rele
|
|
|
945
945
|
* check was composed from a targeted run: a full pass is needed first. Null when nothing blocks.
|
|
946
946
|
*/
|
|
947
947
|
export declare function mainDebtProblem(store: Store, repo: string, taskId: string): string | null;
|
|
948
|
+
/**
|
|
949
|
+
* A deploy gate (docs/plans/chat-management.md §0a.5): a scope whose signed terms name a prepared candidate or carry
|
|
950
|
+
* release terms. Both are in the scope digest and written at filing, before any release authority exists, so this is
|
|
951
|
+
* read from the scope alone: a first approval and a fallback gate filed without --release are deploy gates too. From
|
|
952
|
+
* chat it is approved only through the release card.
|
|
953
|
+
*/
|
|
954
|
+
export declare function deployGateScope(scope: Pick<Scope, "candidate" | "releaseTerms">): boolean;
|
package/dist/scope.js
CHANGED
|
@@ -1327,7 +1327,8 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1327
1327
|
export function releaseTermsSentence(terms) {
|
|
1328
1328
|
if (!terms.policy.carry)
|
|
1329
1329
|
return `Release ${terms.packageVersion} from pull request #${terms.pr} at ${terms.approvedSha.slice(0, 7)}. Every later commit asks for your approval again.`;
|
|
1330
|
-
|
|
1330
|
+
const outside = terms.access === "sandboxed" ? "" : " This project's checks don't run in the sandbox: those later test-only commits will run outside the sandbox without being shown to you first.";
|
|
1331
|
+
return `Release ${terms.packageVersion} from pull request #${terms.pr} at ${terms.approvedSha.slice(0, 7)}. Later commits on this pull request that only change tests, docs or the changelog ship without asking you again, until it merges or for 3 days. You'll see each one in the release summary.${outside}`;
|
|
1331
1332
|
}
|
|
1332
1333
|
/** What asks again, in full: the terms behind the sentence. */
|
|
1333
1334
|
export function releaseAsksAgainWords(terms) {
|
|
@@ -1338,7 +1339,7 @@ export function releaseAsksAgainWords(terms) {
|
|
|
1338
1339
|
`when it deletes, renames or changes the mode of a file, adds a binary, a symlink or a submodule; when it changes the production build;`,
|
|
1339
1340
|
`when more than ${p.limits.lines} lines or ${p.limits.files} files change after this approval, or ${p.limits.churn} lines of churn;`,
|
|
1340
1341
|
...(p.protectProject ? ["when it changes anything: this project is protected;"] : p.protectedPaths.length === 0 ? [] : [`when it changes a protected path: ${p.protectedPaths.join(", ")};`]),
|
|
1341
|
-
`and always once the deploy starts. Repository ${terms.repo}, branch ${terms.branch} into ${terms.base}, check grant ${terms.grant.id}, checks in the sandbox, until ${terms.expiresAt}.`,
|
|
1342
|
+
`and always once the deploy starts. Repository ${terms.repo}, branch ${terms.branch} into ${terms.base}, check grant ${terms.grant.id}, ${terms.access === "sandboxed" ? "checks in the sandbox" : `checks outside the sandbox (${terms.access}), carried because the project allows it`}, until ${terms.expiresAt}.`,
|
|
1342
1343
|
];
|
|
1343
1344
|
}
|
|
1344
1345
|
function releaseTermsWords(terms) {
|
|
@@ -1358,7 +1359,7 @@ function describeApproval(approval) {
|
|
|
1358
1359
|
export { personProof } from "./person-proof.js";
|
|
1359
1360
|
import { personProof } from "./person-proof.js";
|
|
1360
1361
|
import { releaseTermsDigest } from "./contracts/release-terms.js";
|
|
1361
|
-
import { newestReleaseGeneration, releaseAuthorityOfTask, releaseSealProblem } from "./release-authority.js";
|
|
1362
|
+
import { CHECK_ACCESS_WIDTH, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseSealProblem } from "./release-authority.js";
|
|
1362
1363
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
1363
1364
|
export const machineActing = () => currentActor()?.lead === true || activeRemote() !== null;
|
|
1364
1365
|
/** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
|
|
@@ -1494,8 +1495,15 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
|
|
|
1494
1495
|
const grant = store.liveVerifyCommand(a.repo);
|
|
1495
1496
|
if (grant === null || grant.id !== a.verifyCommand || grant.digest !== a.verifyDigest)
|
|
1496
1497
|
return refuse("the release check command changed since the approval");
|
|
1497
|
-
|
|
1498
|
-
|
|
1498
|
+
// The checks reach no further than at the approval. Wider than the sandbox, a carried commit stands only while both
|
|
1499
|
+
// the sealed and the current "Carry release approvals when checks aren't sandboxed" allow it (the stricter of the two).
|
|
1500
|
+
const access = store.projectCheckAccess(a.repo);
|
|
1501
|
+
const sealedWidth = CHECK_ACCESS_WIDTH[a.accessDigest], nowWidth = CHECK_ACCESS_WIDTH[access];
|
|
1502
|
+
if (sealedWidth === undefined || nowWidth === undefined || nowWidth > sealedWidth)
|
|
1503
|
+
return refuse(a.accessDigest === "sandboxed" ? "the project's checks no longer run in the sandbox" : "the project's checks reach further than when the release was approved");
|
|
1504
|
+
if (carry !== null && (a.accessDigest !== "sandboxed" || access !== "sandboxed") && (a.policy?.unsandboxedCarry !== true || !carriesUnsandboxed(store, a.repo))) {
|
|
1505
|
+
return refuse("carrying release approvals when checks aren't sandboxed is off");
|
|
1506
|
+
}
|
|
1499
1507
|
if (a.policy === null)
|
|
1500
1508
|
return refuse("this Toolroll doesn't know the release's carry rules; update Toolroll first");
|
|
1501
1509
|
if (action === "deploy" || action === "merge") {
|
|
@@ -1522,3 +1530,12 @@ export function mainDebtProblem(store, repo, taskId) {
|
|
|
1522
1530
|
return null;
|
|
1523
1531
|
return `the nightly full check on main has ${open} open failure${open === 1 ? "" : "s"}, so a check made from a re-run of what failed can't deploy; a full check has to pass first`;
|
|
1524
1532
|
}
|
|
1533
|
+
/**
|
|
1534
|
+
* A deploy gate (docs/plans/chat-management.md §0a.5): a scope whose signed terms name a prepared candidate or carry
|
|
1535
|
+
* release terms. Both are in the scope digest and written at filing, before any release authority exists, so this is
|
|
1536
|
+
* read from the scope alone: a first approval and a fallback gate filed without --release are deploy gates too. From
|
|
1537
|
+
* chat it is approved only through the release card.
|
|
1538
|
+
*/
|
|
1539
|
+
export function deployGateScope(scope) {
|
|
1540
|
+
return (scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null;
|
|
1541
|
+
}
|
package/dist/server/flows.js
CHANGED
|
@@ -12,13 +12,13 @@ import { run as execRun } from "../exec.js";
|
|
|
12
12
|
import { setFlowSecret } from "../flow-actions.js";
|
|
13
13
|
import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowDefinitionOf, moveCardInFlow } from "../flow-engine.js";
|
|
14
14
|
import { galleryHtml, galleryUseHtml } from "../flow-gallery-ui.js";
|
|
15
|
-
import { galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
|
|
15
|
+
import { galleryAdmitsAnyone, galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
|
|
16
16
|
import { flowInsights } from "../flow-insights.js";
|
|
17
17
|
import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people.js";
|
|
18
18
|
import { saveScript } from "../flow-scripts.js";
|
|
19
19
|
import { chooseFlowCard } from "../flow-send.js";
|
|
20
20
|
import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
|
|
21
|
-
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries } from "../flow-triggers.js";
|
|
21
|
+
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries, rawAdmitsAnyone } from "../flow-triggers.js";
|
|
22
22
|
import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
|
|
23
23
|
import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
|
|
24
24
|
import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
|
|
@@ -32,7 +32,7 @@ import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWo
|
|
|
32
32
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
33
33
|
import { createSubagentFrom, labelOf, nameOf, saveSoul, sendSubagentSummaries, setSubagentState, subagentSettings } from "../subagent-admin.js";
|
|
34
34
|
import { addRoutine, removeRoutine, runRoutine } from "../subagent-desk.js";
|
|
35
|
-
import { editMemory, forgetMemory, tellSubagent } from "../subagent-memory.js";
|
|
35
|
+
import { ACCEPT as ACCEPT_SUGGESTION, editMemory, forgetMemory, tellSubagent } from "../subagent-memory.js";
|
|
36
36
|
import { grantTool, revokeTool, rulesFromForm, setToolRules } from "../subagent-tools.js";
|
|
37
37
|
import { sendSubagentWeeklies, undoCall } from "../subagent-week.js";
|
|
38
38
|
import { answerSubagentQuestion } from "../subagent-work.js";
|
|
@@ -301,11 +301,16 @@ export function createFlowsHandlers(runtime) {
|
|
|
301
301
|
return redirect(response, `/kits?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
302
302
|
const page = (key, words) => redirect(response, `/kits/${kit.id}?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
303
303
|
if (step === "setup") {
|
|
304
|
-
|
|
304
|
+
// Its tools come with it only on the person's proof (a password, or the session under Passwordless): a grant widens.
|
|
305
|
+
const proved = authenticateApprover(who, body.get("password") ?? "", repo).ok;
|
|
306
|
+
const made = await setUpKit(store, kit, repo, who.name, now, options.configDir ?? null, { toolHome }, proved ? { widening: "proved" } : {});
|
|
305
307
|
return page(made.ok ? "said" : "problem", made.said);
|
|
306
308
|
}
|
|
307
309
|
if (step === "github") {
|
|
308
|
-
|
|
310
|
+
// Issues from anyone on GitHub come into a flow: a widening, so it takes a password (chat-management.md §0a.3).
|
|
311
|
+
if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
|
|
312
|
+
return page("problem", "Enter your Toolroll password to bring in GitHub issues. Nothing changed.");
|
|
313
|
+
const made = addKitGithubTrigger(store, kit, repo, who.name, now, options.configDir ?? null, "proved");
|
|
309
314
|
return page(made.ok ? "said" : "problem", made.said);
|
|
310
315
|
}
|
|
311
316
|
const tried = addKitSample(store, kit, repo, who.name, now);
|
|
@@ -330,7 +335,10 @@ export function createFlowsHandlers(runtime) {
|
|
|
330
335
|
const card = question === null ? null : store.getFlowCard(question.card);
|
|
331
336
|
const flow = card === null ? null : store.getFlow(card.flow);
|
|
332
337
|
const done = question === null || flow === null || !visible(flow.repo) ? { ok: false, said: "No such question." }
|
|
333
|
-
|
|
338
|
+
// Accepting a looser rule it suggested takes the password (chat-management.md §0a.3); every other answer doesn't.
|
|
339
|
+
: question.suggestion !== null && body.get("choice") === ACCEPT_SUGGESTION && !authenticateApprover(who, body.get("password") ?? "").ok
|
|
340
|
+
? { ok: false, said: "Enter your Toolroll password to change the rule." }
|
|
341
|
+
: answerSubagentQuestion(store, question.id, { choice: body.get("choice"), text: body.get("text"), by: who.name, via: "web", ...(question.suggestion !== null ? { widening: "proved" } : {}) }, now);
|
|
334
342
|
if (wantsJson)
|
|
335
343
|
return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
|
|
336
344
|
const back = question === null ? "/settings/lead/subagents" : `/settings/lead/subagents/${question.subagent}`;
|
|
@@ -403,9 +411,23 @@ export function createFlowsHandlers(runtime) {
|
|
|
403
411
|
// v94: which project tools it may use, and its rule for each action.
|
|
404
412
|
const tool = body.get("tool") ?? "", op = body.get("op");
|
|
405
413
|
const grant = store.subagentGrant(mate.id, tool);
|
|
406
|
-
|
|
414
|
+
// Giving a subagent a tool widens what it does unasked: a password (chat-management.md §0a.3). Revoking and
|
|
415
|
+
// tightening a rule don't need one.
|
|
416
|
+
if (op === "grant" && !authenticateApprover(who, body.get("password") ?? "", mate.repo).ok) {
|
|
417
|
+
return redirect(response, `${back}?problem=${encodeURIComponent(`Enter your Toolroll password to let ${nameOf(mate)} use ${tool}. Nothing changed.`)}#tools`);
|
|
418
|
+
}
|
|
419
|
+
const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome }, { widening: "proved" })
|
|
407
420
|
: op === "revoke" ? revokeTool(store, mate, tool, who.name, now)
|
|
408
|
-
: grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } :
|
|
421
|
+
: grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : (() => {
|
|
422
|
+
// Loosening a rule widens what it does unasked: a password (chat-management.md §0a.3); tightening needs none.
|
|
423
|
+
const changes = rulesFromForm(grant, key => body.sent.get(key));
|
|
424
|
+
const tried = setToolRules(store, mate, tool, changes, who.name, now);
|
|
425
|
+
if (tried.ok || !/takes your password/.test(tried.said))
|
|
426
|
+
return tried;
|
|
427
|
+
if (!authenticateApprover(who, body.get("password") ?? "", mate.repo).ok)
|
|
428
|
+
return { ok: false, said: `Enter your Toolroll password to let ${nameOf(mate)} do more on its own. Nothing changed.` };
|
|
429
|
+
return setToolRules(store, mate, tool, changes, who.name, now, { widening: "proved" });
|
|
430
|
+
})();
|
|
409
431
|
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
|
|
410
432
|
}
|
|
411
433
|
const done = part === "state" ? setSubagentState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
|
|
@@ -445,7 +467,11 @@ export function createFlowsHandlers(runtime) {
|
|
|
445
467
|
}
|
|
446
468
|
if (digest === null || digest !== body.get("previewed"))
|
|
447
469
|
return sendGalleryUse(response, who, template, projects, repo, answers, name, digest === null ? null : "Your answers changed. Check the preview, then create the flow.");
|
|
448
|
-
|
|
470
|
+
// A template whose trigger brings in items from anyone takes the password (chat-management.md §0a.3).
|
|
471
|
+
const anyone = galleryAdmitsAnyone(template);
|
|
472
|
+
if (anyone && !authenticateApprover(who, body.get("password") ?? "", repo).ok)
|
|
473
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, "Enter your Toolroll password: this flow brings in items from anyone.");
|
|
474
|
+
const used = useGalleryTemplate(store, template, repo, answers, { name, by: who.name, now: clock(), dir: options.configDir ?? null, ...(anyone ? { widening: "proved" } : {}) });
|
|
449
475
|
if (!used.ok)
|
|
450
476
|
return sendGalleryUse(response, who, template, projects, repo, answers, name, used.said);
|
|
451
477
|
return redirect(response, `/flows/${used.flow}`);
|
|
@@ -576,7 +602,11 @@ export function createFlowsHandlers(runtime) {
|
|
|
576
602
|
const ingress = rawTakesDeliveries(raw);
|
|
577
603
|
if (ingress && !authenticateCredential(who, body.get("password") ?? "").ok)
|
|
578
604
|
return answer(403, { ok: false, said: "Enter your Toolroll password to make a webhook address." });
|
|
579
|
-
|
|
605
|
+
// Items from anyone: a widening, an ordinary step-up (chat-management.md §0a.3).
|
|
606
|
+
const anyone = !ingress && rawAdmitsAnyone(raw);
|
|
607
|
+
if (anyone && !authenticateApprover(who, body.get("password") ?? "", flow.repo).ok)
|
|
608
|
+
return answer(403, { ok: false, said: "Enter your Toolroll password to bring in items from anyone." });
|
|
609
|
+
const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir, ingress ? { ingress: "proved" } : anyone ? { widening: "proved" } : {});
|
|
580
610
|
if (made.ok)
|
|
581
611
|
await prepareUnattendedCandidates(store, flow.id);
|
|
582
612
|
return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
|
|
@@ -170,7 +170,7 @@ export function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging
|
|
|
170
170
|
: joinHtml([
|
|
171
171
|
html `<h2>Unattended permissions</h2>`,
|
|
172
172
|
permissionDefault.canManage && csrf !== ""
|
|
173
|
-
? postForm("/settings/permission-default", html `${permissionModeChoices("permission-mode", permissionDefault.mode)}<button type="submit">Save default</button>`, { attrs: { class: "card permission-policy"
|
|
173
|
+
? postForm("/settings/permission-default", html `${permissionModeChoices("permission-mode", permissionDefault.mode)}${permissionDefault.mode === "bypassPermissions" ? "" : reauth(html `<label>Your password <span class="meta">(needed for Full access)</span><input type="password" name="password" autocomplete="current-password"></label>`)}<button type="submit">Save default</button>`, { attrs: { class: "card permission-policy" } })
|
|
174
174
|
: html `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">An approver can change this default</p></div>`,
|
|
175
175
|
permissionDefault.updatedAt === null
|
|
176
176
|
? ""
|
package/dist/server/settings.js
CHANGED
|
@@ -1422,7 +1422,12 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1422
1422
|
for (const [key, visit] of connectVisits)
|
|
1423
1423
|
if (visit.expires < Date.now())
|
|
1424
1424
|
connectVisits.delete(key);
|
|
1425
|
-
|
|
1425
|
+
// The password above proves one thing: this connection, by this account as it is now, and (from a kit whose subagent
|
|
1426
|
+
// is set up) letting that subagent use this tool in this project. The callback holds the visit to exactly that.
|
|
1427
|
+
const kitSet = kit === null || also !== null ? null : kitOf(kit);
|
|
1428
|
+
const installed = kitSet === null || !kitSet.tools.some(one => one.tool === service.id) ? null : kitInstalled(store, kitSet, target);
|
|
1429
|
+
connectVisits.set(started.state, { ...started.visit, generation: store.accountOf(who.name)?.generation ?? -1,
|
|
1430
|
+
grant: installed === null ? null : { subagent: installed.mate.id, tool: service.id, repo: target } });
|
|
1426
1431
|
return goOutside(response, started.go, `Going to ${service.label} to sign in…`, { state: started.state, path: CONNECT_CALLBACK, secure: origin.startsWith("https:") });
|
|
1427
1432
|
}
|
|
1428
1433
|
async function toolsChange(ctx) {
|
|
@@ -2015,6 +2020,11 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2015
2020
|
if (wanted !== "auto" && wanted !== "bypassPermissions") {
|
|
2016
2021
|
return refuse(response, who, 400, "permissions must be Auto or Full access", "/settings");
|
|
2017
2022
|
}
|
|
2023
|
+
// Widening every new task to Full access takes a password (chat-management.md §0a.3): an ordinary step-up, so a
|
|
2024
|
+
// signed-in session does under Passwordless. Back to Auto narrows, and needs none.
|
|
2025
|
+
if (wanted === "bypassPermissions" && store.permissionDefault().mode !== "bypassPermissions" && !authenticateApprover(who, body.get("password") ?? "").ok) {
|
|
2026
|
+
return redirect(response, `/settings?said=${encodeURIComponent("Enter your Toolroll password to start new tasks with Full access. Nothing changed.")}`);
|
|
2027
|
+
}
|
|
2018
2028
|
store.setPermissionDefault(wanted, who.name, now);
|
|
2019
2029
|
return redirect(response, `/settings?said=${encodeURIComponent(wanted === "bypassPermissions"
|
|
2020
2030
|
? "New tasks now start with Full access. Approved tasks keep their setting."
|
|
@@ -2413,7 +2423,14 @@ ${modeWords(terms)
|
|
|
2413
2423
|
return done("/settings/tools", "problem", "That sign-in was started in another browser. Connect again from this one.");
|
|
2414
2424
|
const visit = connectVisits.get(state);
|
|
2415
2425
|
connectVisits.delete(state);
|
|
2416
|
-
|
|
2426
|
+
// The signer as at Connect: still an approver, not revoked, the same generation, still in the project. Checked here,
|
|
2427
|
+
// before the connection changes, and again at the grant, after every wait.
|
|
2428
|
+
const standing = () => {
|
|
2429
|
+
const account = visit === undefined ? null : store.accountOf(visit.by);
|
|
2430
|
+
return visit !== undefined && account !== null && account.role === "approver" && account.revokedAt === null && visit.generation !== undefined
|
|
2431
|
+
&& account.generation === visit.generation && store.accountCanAccess(visit.by, visit.repo);
|
|
2432
|
+
};
|
|
2433
|
+
if (visit === undefined || visit.expires < Date.now() || !standing())
|
|
2417
2434
|
return done("/settings/tools", "problem", "That sign-in expired. Connect again.");
|
|
2418
2435
|
const service = oneClickOf(visit.service);
|
|
2419
2436
|
const kit = visit.kit === null ? null : kitOf(visit.kit);
|
|
@@ -2430,13 +2447,17 @@ ${modeWords(terms)
|
|
|
2430
2447
|
const now = clock();
|
|
2431
2448
|
if (!adapterPolicy({ caller: "service", capability: "none" }).ok)
|
|
2432
2449
|
return respond(response, 403, "text/plain", "Forbidden");
|
|
2433
|
-
const finished = await finishConnect(store, visit, code, now, { fetcher: options.connectFetch ?? fetch, home: toolHome, omitEnv: ALL_CREDENTIAL_ENV });
|
|
2450
|
+
const finished = await finishConnect(store, visit, code, now, { fetcher: options.connectFetch ?? fetch, home: toolHome, omitEnv: ALL_CREDENTIAL_ENV, stillStanding: standing });
|
|
2434
2451
|
if (!finished.ok)
|
|
2435
2452
|
return done(back, "problem", finished.said);
|
|
2436
2453
|
// From a kit: its teammate may use the tool now (reading freely, the rest after a person approves each call).
|
|
2437
2454
|
const set = kit === null || !kit.tools.some(one => one.tool === service.id) ? null : kitInstalled(store, kit, visit.repo);
|
|
2438
|
-
|
|
2439
|
-
|
|
2455
|
+
const bound = visit.grant ?? null;
|
|
2456
|
+
if (set !== null && bound !== null && bound.subagent === set.mate.id && bound.tool === service.id && bound.repo === visit.repo && set.mate.repo === visit.repo
|
|
2457
|
+
&& store.subagentGrant(set.mate.id, service.id) === null && standing()) {
|
|
2458
|
+
// The password at Connect (toolsConnect) is this grant's proof, for exactly the grant the visit names, while the
|
|
2459
|
+
// signer still stands: re-checked after the tool's listing, in the step that saves the grant.
|
|
2460
|
+
const granted = await grantTool(store, set.mate, service.id, visit.by, now, { toolHome }, { widening: "proved", stillProved: standing });
|
|
2440
2461
|
if (granted.ok)
|
|
2441
2462
|
return done(back, "said", `${service.label} is connected to ${to}, and ${nameOf(set.mate)} can use it: reading freely, the rest after you approve each call.`);
|
|
2442
2463
|
}
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/store.d.ts
CHANGED
|
@@ -2354,6 +2354,9 @@ export declare class Store {
|
|
|
2354
2354
|
sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
|
|
2355
2355
|
kind: "mode" | "chat" | "grant" | "schedule";
|
|
2356
2356
|
modeDigest: string;
|
|
2357
|
+
/** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). */
|
|
2358
|
+
card?: "release";
|
|
2359
|
+
cardDigest?: string;
|
|
2357
2360
|
}, approverKind?: ApproverKind): boolean;
|
|
2358
2361
|
/** Whether this task's scope carries a LIVE seal: approved, and the
|
|
2359
2362
|
* approved digest is the current one (a rewritten scope unseals). The
|
package/dist/store.js
CHANGED
|
@@ -6989,6 +6989,12 @@ export class Store {
|
|
|
6989
6989
|
const terms = storedReleaseTerms(working["release_terms_json"]);
|
|
6990
6990
|
if (!terms.ok)
|
|
6991
6991
|
return false;
|
|
6992
|
+
// A DEPLOY GATE FROM CHAT (chat-management.md §0a.5): a signed candidate or release terms seal from chat only
|
|
6993
|
+
// with the release card's evidence (its act and card digest), never through the plain plan card or a generic
|
|
6994
|
+
// one. Read from the raw row, so a terms column that doesn't parse is a deploy gate too.
|
|
6995
|
+
const deployGate = (typeof working["candidate"] === "string" && working["candidate"] !== "") || working["release_terms_json"] != null;
|
|
6996
|
+
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
6997
|
+
return false;
|
|
6992
6998
|
if (terms.terms !== null && basis !== undefined && basis.kind !== "chat")
|
|
6993
6999
|
return false;
|
|
6994
7000
|
if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
|
package/dist/subagent-memory.js
CHANGED
|
@@ -183,7 +183,7 @@ export function answerSuggestion(store, mate, suggestion, answer, now) {
|
|
|
183
183
|
store.decideSubagentSuggestion(offered.id, "stale", null, now);
|
|
184
184
|
return { ok: false, said: "Its rules changed since it suggested this, so nothing was changed." };
|
|
185
185
|
}
|
|
186
|
-
const changed = setToolRules(store, mate, offered.tool, { [offered.action]: { use: offered.rule.use, limit: offered.rule.limit ?? null, undo: current.undo ?? "" } }, answer.by, now);
|
|
186
|
+
const changed = setToolRules(store, mate, offered.tool, { [offered.action]: { use: offered.rule.use, limit: offered.rule.limit ?? null, undo: current.undo ?? "" } }, answer.by, now, answer.widening === undefined ? {} : { widening: answer.widening });
|
|
187
187
|
if (!changed.ok)
|
|
188
188
|
return changed;
|
|
189
189
|
store.decideSubagentSuggestion(offered.id, "accepted", answer.by, now);
|