toolroll 0.9.76 → 0.9.78
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 +273 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +46 -10
- package/dist/assignment.js +3 -0
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +18 -3
- package/dist/builder.js +152 -47
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +295 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +67 -6
- package/dist/check-report.js +254 -9
- package/dist/checkout-cleanup.js +2 -2
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-service.js +4 -1
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.js +5 -0
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +9 -5
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +19 -4
- package/dist/exec.d.ts +9 -0
- package/dist/exec.js +22 -2
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/grant.js +5 -1
- package/dist/guides.js +22 -11
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +969 -328
- package/dist/planner.js +2 -2
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +2 -2
- package/dist/proof.js +6 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +80 -2
- package/dist/release-adapters.js +441 -16
- package/dist/release-authority.js +6 -2
- package/dist/release-auto.d.ts +38 -7
- package/dist/release-auto.js +131 -15
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +95 -17
- package/dist/release-coverage.js +5 -2
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +8 -2
- package/dist/release.d.ts +144 -14
- package/dist/release.js +421 -81
- package/dist/review-context.js +8 -6
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +73 -9
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +26 -3
- package/dist/scout.js +2 -2
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +112 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/task-views.js +7 -2
- package/dist/server/tasks.js +4 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage-sweep.js +1 -1
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +135 -2
- package/dist/store.js +275 -13
- package/dist/surface.js +3 -1
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +52 -3
- package/dist/teardown-git.js +145 -33
- package/dist/test-infra.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +81 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/workspace-revision.js +3 -1
- package/package.json +2 -1
package/dist/teardown-git.js
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* nothing is removed.
|
|
12
12
|
*/
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { closeSync, constants as fsConstants, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
14
|
+
import { accessSync, closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
15
15
|
import { randomBytes } from "node:crypto";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { ensureGitViewsRoot, withCheckoutLock } from "./git-views.js";
|
|
18
18
|
import { gitIncident } from "./git-incident.js";
|
|
19
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
19
|
+
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
20
20
|
import { run } from "./exec.js";
|
|
21
21
|
import { ENV, ENV_ALLOWLIST } from "./remote-main.js";
|
|
22
22
|
import { WORKTREE_TIMEOUT_MS } from "./worktree.js";
|
|
@@ -304,29 +304,37 @@ export function teardownGit(runner = run, binding = {}) {
|
|
|
304
304
|
* git at any file, a pipe that never ends among them. Each file is read only as the regular file it is, bounded; one
|
|
305
305
|
* that can't be read so counts as including.
|
|
306
306
|
*/
|
|
307
|
-
export function checkoutIncludes(checkout) {
|
|
307
|
+
export function checkoutIncludes(checkout, gitFile = "git") {
|
|
308
308
|
const dirs = gitDirsOf(checkout);
|
|
309
309
|
if (dirs === null)
|
|
310
310
|
return true;
|
|
311
311
|
for (const path of [join(dirs.common, "config"), join(dirs.gitdir, "config.worktree")]) {
|
|
312
312
|
if (!existsSync(path) && path.endsWith("config.worktree"))
|
|
313
313
|
continue;
|
|
314
|
-
if (
|
|
314
|
+
if (includeIn(path, gitFile) !== null)
|
|
315
315
|
return true;
|
|
316
316
|
}
|
|
317
317
|
return false;
|
|
318
318
|
}
|
|
319
319
|
/** One config file, read only as the regular file it is (bounded), or null. */
|
|
320
320
|
function regularFile(path) {
|
|
321
|
+
// Opened without following a link and without blocking, then judged by what was opened: a FIFO swapped in after a
|
|
322
|
+
// look never hangs the read (eval-harness review 1, finding 7).
|
|
323
|
+
let fd = null;
|
|
321
324
|
try {
|
|
322
|
-
|
|
325
|
+
fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
326
|
+
const held = fstatSync(fd);
|
|
323
327
|
if (!held.isFile() || held.size > 1024 * 1024)
|
|
324
328
|
return null;
|
|
325
|
-
return readFileSync(
|
|
329
|
+
return readFileSync(fd, "utf8");
|
|
326
330
|
}
|
|
327
331
|
catch {
|
|
328
332
|
return null;
|
|
329
333
|
}
|
|
334
|
+
finally {
|
|
335
|
+
if (fd !== null)
|
|
336
|
+
closeSync(fd);
|
|
337
|
+
}
|
|
330
338
|
}
|
|
331
339
|
/** A checkout's git directory and common directory, as git finds them from its `.git` (a folder, or a one-line
|
|
332
340
|
* `gitdir: ` file); null when the pointer isn't one git would read the same way. */
|
|
@@ -350,18 +358,13 @@ function gitDirsOf(checkout) {
|
|
|
350
358
|
const named = regularFile(join(gitdir, "commondir"))?.trim();
|
|
351
359
|
return { gitdir, common: named === undefined || named === "" ? gitdir : resolve(gitdir, named) };
|
|
352
360
|
}
|
|
353
|
-
/** Whether one config file includes another (or can't be read as a plain file): by a plain reading, and as git itself
|
|
354
|
-
* parses it, includes not followed (only this one regular file, from a neutral folder, with no other config read). */
|
|
355
|
-
function configIncludes(path) {
|
|
356
|
-
return includeIn(path) !== null;
|
|
357
|
-
}
|
|
358
361
|
/** The include key one config file sets (lowercased, as git names it), "include" when only a plain reading finds one,
|
|
359
362
|
* or "an unreadable file" when it can't be read as a plain file or parsed; null when it includes nothing. */
|
|
360
|
-
function includeIn(path) {
|
|
363
|
+
function includeIn(path, gitFile = "git") {
|
|
361
364
|
const text = regularFile(path);
|
|
362
365
|
if (text === null)
|
|
363
366
|
return "an unreadable file";
|
|
364
|
-
const parsed = spawnSync(
|
|
367
|
+
const parsed = spawnSync(gitFile, ["config", "--file", path, "--no-includes", "--name-only", "--list", "-z"], { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
|
|
365
368
|
if (parsed.status !== 0)
|
|
366
369
|
return "an unreadable file";
|
|
367
370
|
const named = parsed.stdout.split("\0").find(name => /^(include\.path|includeif\..*\.path)$/i.test(name));
|
|
@@ -471,7 +474,7 @@ function boundDirs(checkout, binding = {}) {
|
|
|
471
474
|
const ADMIN_LINKED = ["index", "logs", "refs", "reftable"];
|
|
472
475
|
/** What git keeps in a common directory (its path.c common_list, and reftable), linked whether or not it exists yet. */
|
|
473
476
|
const COMMON_NAMES = ["branches", "gc.pid", "logs", "lost-found", "modules", "objects", "packed-refs", "reftable", "refs", "remotes", "rr-cache", "shallow", "svn", "worktrees"];
|
|
474
|
-
function gitView(checkout, kind, binding = {}) {
|
|
477
|
+
function gitView(checkout, kind, binding = {}, gitFile = "git") {
|
|
475
478
|
const dirs = boundDirs(checkout, binding);
|
|
476
479
|
const root = ensureGitViewsRoot(realpathSync(homedir()));
|
|
477
480
|
const view = realpathSync(mkdtempSync(join(root, `${kind}-`)));
|
|
@@ -485,9 +488,9 @@ function gitView(checkout, kind, binding = {}) {
|
|
|
485
488
|
throw new Error("the checkout's git config couldn't be read as a plain file");
|
|
486
489
|
const configPath = join(view, "config");
|
|
487
490
|
writeFileSync(configPath, config, { mode: 0o600 });
|
|
488
|
-
const plain = { cwd: "/", encoding: "utf8", timeout: 5_000, env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } };
|
|
491
|
+
const plain = { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } };
|
|
489
492
|
if (/worktreeconfig/i.test(config)) {
|
|
490
|
-
const flag = spawnSync(
|
|
493
|
+
const flag = spawnSync(gitFile, ["config", "--file", configPath, "--no-includes", "--type=bool", "--get", "extensions.worktreeConfig"], plain);
|
|
491
494
|
if (flag.status !== 0 && flag.status !== 1)
|
|
492
495
|
throw new Error("the checkout's git config couldn't be read");
|
|
493
496
|
if (flag.stdout.trim() === "true") {
|
|
@@ -499,11 +502,11 @@ function gitView(checkout, kind, binding = {}) {
|
|
|
499
502
|
config = `${config}\n${own}`;
|
|
500
503
|
writeFileSync(configPath, config, { mode: 0o600 });
|
|
501
504
|
}
|
|
502
|
-
const unset = spawnSync(
|
|
505
|
+
const unset = spawnSync(gitFile, ["config", "--file", configPath, "--unset-all", "extensions.worktreeConfig"], plain);
|
|
503
506
|
if (unset.status !== 0 && unset.status !== 5)
|
|
504
507
|
throw new Error("the checkout's git config couldn't be read");
|
|
505
508
|
}
|
|
506
|
-
const include = includeIn(configPath);
|
|
509
|
+
const include = includeIn(configPath, gitFile);
|
|
507
510
|
if (include !== null)
|
|
508
511
|
throw new Error(`the checkout's git config includes another file (${include})`);
|
|
509
512
|
mkdirSync(join(view, "info"));
|
|
@@ -1026,7 +1029,7 @@ export const WRITE_VIEW_FLAGS = ["-c", "worktree.useRelativePaths=false", "-c",
|
|
|
1026
1029
|
/** Whether the checkout's .lfsconfig (read as git parses it, that file only) sets any git-lfs key that could name a
|
|
1027
1030
|
* program (anything but INERT_LFS). A file that
|
|
1028
1031
|
* isn't a plain one, or doesn't parse, counts as naming one. */
|
|
1029
|
-
export function lfsExtensionIn(checkout) {
|
|
1032
|
+
export function lfsExtensionIn(checkout, gitFile = "git") {
|
|
1030
1033
|
const path = join(checkout, ".lfsconfig");
|
|
1031
1034
|
try {
|
|
1032
1035
|
if (!lstatSync(path).isFile())
|
|
@@ -1035,7 +1038,7 @@ export function lfsExtensionIn(checkout) {
|
|
|
1035
1038
|
catch {
|
|
1036
1039
|
return false;
|
|
1037
1040
|
}
|
|
1038
|
-
const parsed = spawnSync(
|
|
1041
|
+
const parsed = spawnSync(gitFile, ["config", "--file", path, "--no-includes", "--name-only", "--list", "-z"], { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
|
|
1039
1042
|
return parsed.status !== 0 || parsed.stdout.split("\0").filter(Boolean).some(name => lfsCommand(name) || ["include", "includeif"].includes(keyParts(name).section));
|
|
1040
1043
|
}
|
|
1041
1044
|
/** Every command runs against a view of the checkout's repository (gitView) whose config and info are copies the
|
|
@@ -1070,12 +1073,15 @@ export function checkoutGit(inner = run, mode = "build", binding = {}) {
|
|
|
1070
1073
|
const key = commandSetting(own.stdout, { lfsChecked: true });
|
|
1071
1074
|
if (key !== null)
|
|
1072
1075
|
return refused(`the checkout's git config sets ${key}, which can run a command`);
|
|
1076
|
+
// A git without --no-lazy-fetch: a repository that could fetch lazily is refused, from the view's frozen config.
|
|
1077
|
+
if (!lazyFetchSupported() && lazyFetchSetting(own.stdout) !== null)
|
|
1078
|
+
return refused(PARTIAL_CLONE_REFUSAL);
|
|
1073
1079
|
// The operator's own config contributes values only (carriedSettings); git then reads no global or system config.
|
|
1074
1080
|
// A read carries no filter: it reads attributes from the empty tree anyway.
|
|
1075
1081
|
const carried = mode === "read" ? withoutFilters(carriedSettings(own.stdout)) : carriedSettings(own.stdout);
|
|
1076
1082
|
const env = { ...viewed.env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" };
|
|
1077
1083
|
// Never into a submodule or nested repository, whose own config this never read: status and diff skip them.
|
|
1078
|
-
const safeArgs =
|
|
1084
|
+
const safeArgs = withoutUnsupportedFlags(args);
|
|
1079
1085
|
const command = safeArgs.findIndex(arg => arg === "status" || arg === "diff");
|
|
1080
1086
|
if (command >= 0 && !safeArgs.includes("--ignore-submodules=all"))
|
|
1081
1087
|
safeArgs.splice(command + 1, 0, "--ignore-submodules=all");
|
|
@@ -1117,8 +1123,8 @@ export const PAGE_READ_FLAGS = [
|
|
|
1117
1123
|
* its config as it starts, so the flags alone don't stop a filter planted in both after the inspection: every read
|
|
1118
1124
|
* also runs against a view of the checkout's repository (gitView; checkoutGitSync, checkoutGit "read"), whose config and info are the plane's. The cost is cosmetic: a checkout's line-ending or encoding rules are not applied, so
|
|
1119
1125
|
* a file they would normalize can read as changed (fail closed: shipping then asks for a clean commit). */
|
|
1120
|
-
export function readFlags() {
|
|
1121
|
-
return PAGE_READ_FLAGS;
|
|
1126
|
+
export function readFlags(gitFile = "git") {
|
|
1127
|
+
return lazyFetchSupported(gitFile) ? PAGE_READ_FLAGS : PAGE_READ_FLAGS.filter(flag => flag !== "--no-lazy-fetch");
|
|
1122
1128
|
}
|
|
1123
1129
|
/**
|
|
1124
1130
|
* checkoutGit, synchronously, for reads a page shows from a checkout an agent writes (docs/plans/get-side-effects.md):
|
|
@@ -1127,22 +1133,32 @@ export function readFlags() {
|
|
|
1127
1133
|
* or info/attributes change after they were read), submodules skipped, bounded. A refusal throws.
|
|
1128
1134
|
*/
|
|
1129
1135
|
export function checkoutGitSync(cwd, args, options = {}) {
|
|
1130
|
-
|
|
1136
|
+
const ran = viewedGitSync(cwd, args, options);
|
|
1137
|
+
if (ran.status !== 0)
|
|
1138
|
+
throw new Error(`git ${args[0] ?? ""} failed`);
|
|
1139
|
+
return ran.stdout;
|
|
1140
|
+
}
|
|
1141
|
+
export function viewedGitSync(cwd, args, options = {}) {
|
|
1142
|
+
const gitFile = options.git ?? "git";
|
|
1143
|
+
if (checkoutIncludes(cwd, gitFile))
|
|
1131
1144
|
throw new Error("the checkout's git config includes another file, or couldn't be read as a plain file");
|
|
1132
|
-
if (lfsExtensionIn(cwd))
|
|
1145
|
+
if (lfsExtensionIn(cwd, gitFile))
|
|
1133
1146
|
throw new Error("the checkout's .lfsconfig sets a git-lfs key that can run a command");
|
|
1134
1147
|
// Its config and info/attributes as read once, in a folder of the plane's own: neither can change before git runs.
|
|
1135
|
-
const view = gitView(cwd, "read", options.binding);
|
|
1148
|
+
const view = gitView(cwd, "read", options.binding, gitFile);
|
|
1136
1149
|
try {
|
|
1137
1150
|
const env = { PATH: process.env["PATH"] ?? "", HOME: process.env["HOME"] ?? "", LANG: "C", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0", ...view.env };
|
|
1138
|
-
const bounded = { cwd, env, encoding: "utf8", timeout: options.timeoutMs ?? 60_000, maxBuffer: options.maxBuffer ?? 1_048_576, stdio: ["ignore", "pipe", "pipe"] };
|
|
1139
|
-
const own = spawnSync(
|
|
1151
|
+
const bounded = { cwd, env, encoding: "utf8", timeout: options.timeoutMs ?? 60_000, killSignal: "SIGKILL", maxBuffer: options.maxBuffer ?? 1_048_576, stdio: ["ignore", "pipe", "pipe"] };
|
|
1152
|
+
const own = spawnSync(gitFile, ["--no-optional-locks", "config", "--list", "--show-scope", "-z"], bounded);
|
|
1140
1153
|
if (own.status !== 0)
|
|
1141
1154
|
throw new Error("its config couldn't be read");
|
|
1142
1155
|
const key = commandSetting(own.stdout, { lfsChecked: true });
|
|
1143
1156
|
if (key !== null)
|
|
1144
1157
|
throw new Error(`the checkout's git config sets ${key}, which can run a command`);
|
|
1145
|
-
|
|
1158
|
+
// A git without --no-lazy-fetch: a repository that could fetch lazily is refused, from the view's frozen config.
|
|
1159
|
+
if (!lazyFetchSupported(gitFile) && lazyFetchSetting(own.stdout) !== null)
|
|
1160
|
+
throw new Error(PARTIAL_CLONE_REFUSAL);
|
|
1161
|
+
const safeArgs = withoutUnsupportedFlags(args, gitFile);
|
|
1146
1162
|
const command = safeArgs.findIndex(arg => arg === "status" || arg === "diff");
|
|
1147
1163
|
if (command >= 0 && !safeArgs.includes("--ignore-submodules=all"))
|
|
1148
1164
|
safeArgs.splice(command + 1, 0, "--ignore-submodules=all");
|
|
@@ -1151,13 +1167,20 @@ export function checkoutGitSync(cwd, args, options = {}) {
|
|
|
1151
1167
|
safeArgs.splice(command + 1, 0, "--no-ext-diff", "--no-textconv");
|
|
1152
1168
|
// The operator's own values, but no filter: PAGE_READ_FLAGS reads attributes from the empty tree anyway.
|
|
1153
1169
|
const carried = withoutFilters(carriedSettings(own.stdout));
|
|
1154
|
-
const
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1170
|
+
const argv = [...TEARDOWN_FLAGS, ...readFlags(gitFile), "-c", "diff.ignoreSubmodules=all", ...carried, ...safeArgs];
|
|
1171
|
+
if (options.buffer === true) {
|
|
1172
|
+
// stdin is the caller's own bytes when given (cat-file --batch), else closed.
|
|
1173
|
+
const ran = spawnSync(gitFile, argv, { ...bounded, encoding: "buffer", ...(options.input === undefined ? {} : { input: options.input, stdio: ["pipe", "pipe", "pipe"] }) });
|
|
1174
|
+
const why = view.settle();
|
|
1175
|
+
if (why !== null)
|
|
1176
|
+
throw new Error(why);
|
|
1177
|
+
return { status: ran.error === undefined ? ran.status : null, stdout: ran.stdout };
|
|
1178
|
+
}
|
|
1179
|
+
const ran = spawnSync(gitFile, argv, bounded);
|
|
1157
1180
|
const why = view.settle();
|
|
1158
1181
|
if (why !== null)
|
|
1159
1182
|
throw new Error(why);
|
|
1160
|
-
return view.restore(ran.stdout, args);
|
|
1183
|
+
return { status: ran.error === undefined ? ran.status : null, stdout: view.restore(ran.stdout ?? "", args) };
|
|
1161
1184
|
}
|
|
1162
1185
|
finally {
|
|
1163
1186
|
view.dispose();
|
|
@@ -1183,3 +1206,92 @@ export function hardenedGit(inner = run, mode = "build", binding = {}) {
|
|
|
1183
1206
|
return hardened("git", rest, { ...options, ...(cwd === undefined ? {} : { cwd }) });
|
|
1184
1207
|
};
|
|
1185
1208
|
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Git older than 2.45 has no `--no-lazy-fetch` (and no GIT_NO_LAZY_FETCH): there, every hardened read leaves the flag
|
|
1211
|
+
* out and instead refuses a repository whose config makes lazy fetching possible (lazyFetchSetting), while
|
|
1212
|
+
* `protocol.allow=never` stays on every read and write, so a lazy fetch that slipped past would still find no
|
|
1213
|
+
* transport. Asked once per process, as the plane's other plain git probes run: no system or global config.
|
|
1214
|
+
*/
|
|
1215
|
+
const lazyFetchByGit = new Map();
|
|
1216
|
+
let lazyFetchOverride;
|
|
1217
|
+
/** The git executable `file` names, as the real path it runs (a bare name looked up on PATH), or `file` itself. */
|
|
1218
|
+
function gitExecutablePath(file) {
|
|
1219
|
+
const candidates = file.includes("/") ? [file] : (process.env["PATH"] ?? "").split(delimiter).filter(dir => isAbsolute(dir)).map(dir => join(dir, file));
|
|
1220
|
+
for (const candidate of candidates) {
|
|
1221
|
+
try {
|
|
1222
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
1223
|
+
return realpathSync(candidate);
|
|
1224
|
+
}
|
|
1225
|
+
catch { /* next */ }
|
|
1226
|
+
}
|
|
1227
|
+
return file;
|
|
1228
|
+
}
|
|
1229
|
+
/** Asked once per git executable (eval-harness review 2, follow-up): a fixed git given to viewedGitSync is probed
|
|
1230
|
+
* itself, never answered for by the one PATH names. */
|
|
1231
|
+
export function lazyFetchSupported(gitFile = "git") {
|
|
1232
|
+
if (lazyFetchOverride !== undefined)
|
|
1233
|
+
return lazyFetchOverride;
|
|
1234
|
+
const executable = gitExecutablePath(gitFile);
|
|
1235
|
+
let known = lazyFetchByGit.get(executable);
|
|
1236
|
+
if (known === undefined) {
|
|
1237
|
+
const probe = spawnSync(executable, ["--no-lazy-fetch", "version"], { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", stdio: ["ignore", "pipe", "pipe"],
|
|
1238
|
+
env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
|
|
1239
|
+
known = probe.status === 0;
|
|
1240
|
+
lazyFetchByGit.set(executable, known);
|
|
1241
|
+
}
|
|
1242
|
+
return known;
|
|
1243
|
+
}
|
|
1244
|
+
/** For tests: forget the probes' answers, or set one for every executable (null forgets). */
|
|
1245
|
+
export function setLazyFetchSupportedForTest(value) { lazyFetchOverride = value ?? undefined; lazyFetchByGit.clear(); }
|
|
1246
|
+
/** The words for a partial clone refused on a git without `--no-lazy-fetch`. */
|
|
1247
|
+
export const PARTIAL_CLONE_REFUSAL = "this project is a partial clone; protected reads need git 2.45 or newer";
|
|
1248
|
+
/** `--no-lazy-fetch` where the installed git has it, else nothing (and lazyFetchSetting refuses partial clones). */
|
|
1249
|
+
export function noLazyFetch() { return lazyFetchSupported() ? ["--no-lazy-fetch"] : []; }
|
|
1250
|
+
/** A caller's own arguments with `--no-lazy-fetch` taken out when the installed git doesn't know it. */
|
|
1251
|
+
function withoutUnsupportedFlags(args, gitFile = "git") {
|
|
1252
|
+
return lazyFetchSupported(gitFile) ? [...args] : args.filter(arg => arg !== "--no-lazy-fetch");
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* The repository setting (local or worktree scope of a `git config --list --show-scope -z` listing) that makes lazy
|
|
1256
|
+
* fetching possible, or null: `extensions.partialClone` set at all, any `remote.<name>.promisor` not plainly false, or
|
|
1257
|
+
* any `remote.<name>.partialCloneFilter` set (git makes that remote a promisor). Fails closed: a listing that can't be
|
|
1258
|
+
* read names a setting. `remote.<name>.vcs` (a foreign-VCS helper) is named too, though the classifier already refuses
|
|
1259
|
+
* it (commandSetting: not in the remote allowlist), as it does every `protocol.*` key and every include.
|
|
1260
|
+
*/
|
|
1261
|
+
export function lazyFetchSetting(listing) {
|
|
1262
|
+
try {
|
|
1263
|
+
const fields = listing.split("\0");
|
|
1264
|
+
for (let at = 0; at + 1 < fields.length; at += 2) {
|
|
1265
|
+
const scope = fields[at];
|
|
1266
|
+
if (scope !== "local" && scope !== "worktree")
|
|
1267
|
+
continue;
|
|
1268
|
+
const entry = fields[at + 1];
|
|
1269
|
+
const newline = entry.indexOf("\n");
|
|
1270
|
+
const key = newline < 0 ? entry : entry.slice(0, newline);
|
|
1271
|
+
const value = newline < 0 ? null : entry.slice(newline + 1);
|
|
1272
|
+
const { section, subsection, variable } = keyParts(key);
|
|
1273
|
+
if (section === "extensions" && variable === "partialclone")
|
|
1274
|
+
return key.toLowerCase();
|
|
1275
|
+
if (section === "remote" && subsection !== null && (variable === "partialclonefilter" || variable === "vcs"))
|
|
1276
|
+
return key.toLowerCase();
|
|
1277
|
+
if (section === "remote" && subsection !== null && variable === "promisor" && !/^(false|no|off|0)$/i.test((value ?? "true").trim()))
|
|
1278
|
+
return key.toLowerCase();
|
|
1279
|
+
}
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
return "a setting that couldn't be read";
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* For the reads that run git directly on a repository rather than through a view: with `--no-lazy-fetch` they run as
|
|
1288
|
+
* they always have (the flag stops any fetch). Without it, they must not check a repository's config and then run git
|
|
1289
|
+
* on it (the config could change in between, or an include could set what the check never saw): `inner`, wrapped so
|
|
1290
|
+
* every git command runs through a frozen read view (checkoutGit "read"), whose own config listing (includes refused,
|
|
1291
|
+
* command and protocol settings refused, a partial clone refused) is the config git then runs with.
|
|
1292
|
+
*/
|
|
1293
|
+
export function lazyFetchSafeGit(inner = run) {
|
|
1294
|
+
// On every git version (eval-harness review 1, finding 7): with --no-lazy-fetch the direct read still took the
|
|
1295
|
+
// repository's config and its includes as git found them, and an include naming a FIFO could hang the plane.
|
|
1296
|
+
return checkoutGit(inner, "read");
|
|
1297
|
+
}
|
package/dist/test-infra.js
CHANGED
|
@@ -20,7 +20,7 @@ const VITEST_CONFIG = /(^|\/)vitest\.config\.[cm]?[jt]s$/;
|
|
|
20
20
|
const EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "mjs", "cjs", "jsx"];
|
|
21
21
|
/** The product's entry points: the CLI binary, the processes it starts by file name, and the browser bundles. */
|
|
22
22
|
export const PRODUCT_ENTRIES = Object.freeze([
|
|
23
|
-
"src/bin.ts", "src/cli.ts", "src/controller-service.ts", "src/desktop-host.ts", "src/sandbox-launch.ts", "src/tool-launcher.ts",
|
|
23
|
+
"src/bin.ts", "src/cli.ts", "src/controller-service.ts", "src/up-role-child.ts", "src/desktop-host.ts", "src/sandbox-launch.ts", "src/tool-launcher.ts",
|
|
24
24
|
"src/browser/app.tsx", "src/browser/terminal-app.tsx",
|
|
25
25
|
]);
|
|
26
26
|
const join = (dir, spec) => {
|
package/dist/toolroll-update.js
CHANGED
|
@@ -37,7 +37,7 @@ import { removeTerminalUpdateGate } from "./terminal-catalog.js";
|
|
|
37
37
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
|
|
38
38
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
39
39
|
import { NAME } from "./names.js";
|
|
40
|
-
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS } from "./store.js";
|
|
40
|
+
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS } from "./store.js";
|
|
41
41
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
42
42
|
import { markNeverIndex } from "./never-index.js";
|
|
43
43
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -409,7 +409,7 @@ const HISTORY_RULES = {
|
|
|
409
409
|
// v126: a receipt's state, acceptance time and held item, a trigger's credential generation and turned-away count,
|
|
410
410
|
// and who made a card read none, 0, or none. A table with an earlier rule (flow_trigger's v115 move) keeps it too.
|
|
411
411
|
// v128: a lead message's pending flag reads waiting exactly for a team queue's unadmitted operator message.
|
|
412
|
-
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS))
|
|
412
|
+
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS))
|
|
413
413
|
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
414
414
|
};
|
|
415
415
|
/** The tables a migration may remove whole (v115's removed features). */
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type UpRoleName = "console" | "worker";
|
|
2
|
+
export type UpConsoleConfig = {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
allowedHosts: string[] | null;
|
|
6
|
+
publicUrl: string | null;
|
|
7
|
+
localRunner: string;
|
|
8
|
+
poolRoot: string;
|
|
9
|
+
repos: string[];
|
|
10
|
+
projectRoots: string[];
|
|
11
|
+
registryPath: string;
|
|
12
|
+
editor: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type UpWorkerConfig = {
|
|
15
|
+
runner: string;
|
|
16
|
+
token: string;
|
|
17
|
+
repos: string[];
|
|
18
|
+
pinnedRepos: string[];
|
|
19
|
+
projectRoots: string[];
|
|
20
|
+
registryPath: string;
|
|
21
|
+
pool: string;
|
|
22
|
+
forMs: number | null;
|
|
23
|
+
publicUrl: string | null;
|
|
24
|
+
stopGraceMs: number;
|
|
25
|
+
verbose: boolean;
|
|
26
|
+
terminal: boolean;
|
|
27
|
+
/** How long a start waits for a predecessor's watch lease to expire (a crashed worker's, after a restart). */
|
|
28
|
+
leaseWaitMs: number;
|
|
29
|
+
};
|
|
30
|
+
export type UpRoleStart = {
|
|
31
|
+
type: "start";
|
|
32
|
+
role: "console";
|
|
33
|
+
databaseFile: string;
|
|
34
|
+
containment: string | null;
|
|
35
|
+
build: string;
|
|
36
|
+
config: UpConsoleConfig;
|
|
37
|
+
} | {
|
|
38
|
+
type: "start";
|
|
39
|
+
role: "worker";
|
|
40
|
+
databaseFile: string;
|
|
41
|
+
containment: string | null;
|
|
42
|
+
build: string;
|
|
43
|
+
config: UpWorkerConfig;
|
|
44
|
+
};
|
|
45
|
+
export type ParentToRole = {
|
|
46
|
+
type: "repos";
|
|
47
|
+
repos: string[];
|
|
48
|
+
} | {
|
|
49
|
+
type: "stopping";
|
|
50
|
+
} | {
|
|
51
|
+
type: "hard-stop";
|
|
52
|
+
} | {
|
|
53
|
+
type: "sign-in-link";
|
|
54
|
+
id: number;
|
|
55
|
+
account: string;
|
|
56
|
+
};
|
|
57
|
+
export type RoleToParent = {
|
|
58
|
+
type: "ready";
|
|
59
|
+
role: UpRoleName;
|
|
60
|
+
build: string;
|
|
61
|
+
pid: number;
|
|
62
|
+
url: string | null;
|
|
63
|
+
port: number | null;
|
|
64
|
+
} | {
|
|
65
|
+
type: "log";
|
|
66
|
+
line: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: "repos";
|
|
69
|
+
repos: string[];
|
|
70
|
+
} | {
|
|
71
|
+
type: "failed";
|
|
72
|
+
reason: "port-busy" | "start";
|
|
73
|
+
detail: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: "fatal";
|
|
76
|
+
detail: string;
|
|
77
|
+
} | {
|
|
78
|
+
type: "sign-in-link";
|
|
79
|
+
id: number;
|
|
80
|
+
link: string | null;
|
|
81
|
+
} | {
|
|
82
|
+
type: "closed";
|
|
83
|
+
problem: string | null;
|
|
84
|
+
};
|
|
85
|
+
/** One role's side of the channel: the same shape over IPC (a child process) and in memory (one process). */
|
|
86
|
+
export type RolePort = {
|
|
87
|
+
send: (message: RoleToParent) => void;
|
|
88
|
+
onMessage: (handler: (message: ParentToRole) => void) => void;
|
|
89
|
+
/** First call: drain. Every later call: stop now (the second signal's hard stop). */
|
|
90
|
+
onStop: (handler: (hard: boolean) => void) => void;
|
|
91
|
+
};
|
|
92
|
+
/** The parent's word to a role, checked before anything acts on it. */
|
|
93
|
+
export declare function parentMessage(value: unknown): ParentToRole | null;
|
|
94
|
+
/** A role's word to the parent, checked before the parent relays or acts on it. */
|
|
95
|
+
export declare function roleMessage(value: unknown, role: UpRoleName): RoleToParent | null;
|
|
96
|
+
/** The start message a role child waits for. The console's config carries no runner token; anything else refuses. */
|
|
97
|
+
export declare function startMessage(value: unknown): UpRoleStart | null;
|
|
98
|
+
/** Two ends of an in-memory channel: one process composing both roles (tests and the desktop host), same messages. */
|
|
99
|
+
export declare function memoryChannel(role: UpRoleName): {
|
|
100
|
+
port: RolePort;
|
|
101
|
+
parent: {
|
|
102
|
+
send: (message: ParentToRole) => void;
|
|
103
|
+
onMessage: (handler: (message: RoleToParent) => void) => void;
|
|
104
|
+
stop: (hard: boolean) => void;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/** A build identity: the version and the exact code of the module that composes `up`. Both roles must report the
|
|
108
|
+
* parent's own, or the parent stops rather than run a console and a worker from different builds. */
|
|
109
|
+
export declare function buildIdentity(version: string, moduleFile: string): string;
|
|
110
|
+
/** How many replacements in a row may end before becoming ready, with no refusal said, before `up` stops. */
|
|
111
|
+
export declare const ROLE_NOT_READY_REPLACEMENTS = 3;
|
|
112
|
+
/**
|
|
113
|
+
* One supervised role's generations (Codex review round 1, finding 5): readiness is per generation, not for the role's
|
|
114
|
+
* whole life. When a generation ends, the verdict is:
|
|
115
|
+
* - `fail-start`: the role never became ready at all; the start fails as one, as before the split;
|
|
116
|
+
* - `restart`: this generation was ready and crashed afterwards (the supervisor starts it again);
|
|
117
|
+
* - `end`: a replacement refused to start (another build, another database, a start refused) — permanent, so the whole
|
|
118
|
+
* `up` stops rather than loop — or ROLE_NOT_READY_REPLACEMENTS replacements in a row ended before readiness.
|
|
119
|
+
*/
|
|
120
|
+
export declare class RoleGenerations {
|
|
121
|
+
private everReady;
|
|
122
|
+
private generationReady;
|
|
123
|
+
private refusal;
|
|
124
|
+
private notReadyStreak;
|
|
125
|
+
/** A new generation was spawned: nothing about it is known yet. */
|
|
126
|
+
spawned(): void;
|
|
127
|
+
/** This generation became ready; true when it is the role's first readiness ever. */
|
|
128
|
+
ready(): boolean;
|
|
129
|
+
/** This generation refused its start for good (it said why, or its build is another). */
|
|
130
|
+
refused(detail: string): void;
|
|
131
|
+
/** What this generation said when it refused, if it did. */
|
|
132
|
+
refusedWith(): string | null;
|
|
133
|
+
/** Whether any generation ever became ready. */
|
|
134
|
+
wasReady(): boolean;
|
|
135
|
+
/** This generation ended (not stopped by `up`): what happens next. */
|
|
136
|
+
ended(): "fail-start" | "restart" | "end";
|
|
137
|
+
}
|