pi-claude-supervisor 0.5.2 → 0.5.3
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 +12 -1
- package/README.cn.md +45 -36
- package/README.md +70 -41
- package/docs/architecture.md +65 -38
- package/docs/automation-hardening-plan.md +23 -22
- package/docs/autonomy-target.md +127 -0
- package/docs/engineering-plan.md +136 -138
- package/docs/implementation-review.md +29 -14
- package/docs/independent-review.md +23 -18
- package/docs/releasing.md +7 -2
- package/docs/testing.md +36 -22
- package/package.json +1 -1
- package/src/acceptance.ts +16 -0
- package/src/config.ts +31 -0
- package/src/decision-session-store.ts +19 -1
- package/src/decision-worker.ts +46 -18
- package/src/index.ts +37 -48
- package/src/notifications.ts +29 -11
- package/src/policy.ts +196 -21
- package/src/reviewer.ts +26 -1
- package/src/state.ts +7 -6
- package/src/supervisor.ts +329 -119
- package/src/types.ts +23 -0
- package/src/verifier.ts +88 -6
- package/src/worker/environment.ts +169 -0
- package/src/worker/process-adapter.ts +15 -0
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type SupervisorState =
|
|
|
6
6
|
| "paused"
|
|
7
7
|
| "verifying"
|
|
8
8
|
| "completed"
|
|
9
|
+
| "blocked"
|
|
9
10
|
| "failed"
|
|
10
11
|
| "stopped";
|
|
11
12
|
|
|
@@ -55,6 +56,11 @@ export interface WorkerStartInput {
|
|
|
55
56
|
abortSignal?: AbortSignal;
|
|
56
57
|
/** Internal token that scopes out-of-band startup cancellation. */
|
|
57
58
|
startupToken?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Automatic-start repository assertion. Built-in adapters invoke this after
|
|
61
|
+
* all asynchronous setup and immediately before spawning the Worker.
|
|
62
|
+
*/
|
|
63
|
+
preSpawnCheck?: () => Promise<void>;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export interface WorkerHandle {
|
|
@@ -166,6 +172,15 @@ export interface AcceptanceCheckResult {
|
|
|
166
172
|
finishedAt: string;
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
export interface TaskAutonomy {
|
|
176
|
+
/** Local development continues without synchronous human approval. */
|
|
177
|
+
unattended: boolean;
|
|
178
|
+
/** The Worker should commit the candidate on its local branch before completion. */
|
|
179
|
+
requireLocalCommit: boolean;
|
|
180
|
+
/** Number of autonomous Decision Worker retries before parking a candidate. */
|
|
181
|
+
maxDecisionRetries: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
export interface TaskSpec {
|
|
170
185
|
goal: string;
|
|
171
186
|
scope: string[];
|
|
@@ -173,8 +188,12 @@ export interface TaskSpec {
|
|
|
173
188
|
forbidden: string[];
|
|
174
189
|
acceptance: AcceptanceCheck[];
|
|
175
190
|
maxRepairRounds: number;
|
|
191
|
+
autonomy: TaskAutonomy;
|
|
176
192
|
}
|
|
177
193
|
|
|
194
|
+
/** Public input shape; nested autonomy fields may be omitted and are defaulted. */
|
|
195
|
+
export type TaskSpecInput = Partial<Omit<TaskSpec, "autonomy">> & { autonomy?: Partial<TaskAutonomy> };
|
|
196
|
+
|
|
178
197
|
export type ReviewVerdict = "pass" | "revise" | "human";
|
|
179
198
|
export type ReviewSeverity = "P0" | "P1" | "P2" | "P3";
|
|
180
199
|
|
|
@@ -203,6 +222,10 @@ export interface TaskContext {
|
|
|
203
222
|
cwd: string;
|
|
204
223
|
maxTurns: number;
|
|
205
224
|
startedAt: string;
|
|
225
|
+
/** Repository HEAD before this task; used to review local commits as well as worktree changes. */
|
|
226
|
+
baseCommit?: string;
|
|
227
|
+
/** Non-protected local branch recorded before automatic work begins. */
|
|
228
|
+
baseBranch?: string;
|
|
206
229
|
spec: TaskSpec;
|
|
207
230
|
repairRound: number;
|
|
208
231
|
lastFindingSignature?: string;
|
package/src/verifier.ts
CHANGED
|
@@ -38,11 +38,85 @@ export interface RepositoryEvidence {
|
|
|
38
38
|
untracked?: string;
|
|
39
39
|
/** False when a command/file could not be safely or completely collected. */
|
|
40
40
|
complete?: boolean;
|
|
41
|
+
/** Commit summaries made after the task baseline. */
|
|
42
|
+
commits?: string;
|
|
43
|
+
/** Baseline HEAD used for the diff and commit range, when available. */
|
|
44
|
+
baseRef?: string;
|
|
45
|
+
/** Current branch used for the local candidate, when available. */
|
|
46
|
+
branch?: string;
|
|
41
47
|
/** True when any evidence field was bounded or omitted. */
|
|
42
48
|
truncated?: boolean;
|
|
43
49
|
collectedAt: string;
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
/** Read the repository HEAD without invoking a shell. */
|
|
53
|
+
export async function repositoryHead(cwd: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
54
|
+
try {
|
|
55
|
+
const result = await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], {
|
|
56
|
+
cwd,
|
|
57
|
+
timeout: 30_000,
|
|
58
|
+
maxBuffer: 1024,
|
|
59
|
+
signal,
|
|
60
|
+
env: workerEnvironment(process.env, { GIT_TERMINAL_PROMPT: "0" }),
|
|
61
|
+
});
|
|
62
|
+
const head = String(result.stdout).trim();
|
|
63
|
+
return /^[0-9a-f]{40,64}$/u.test(head) ? head : undefined;
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Verify that a full commit object is still present without invoking a shell. */
|
|
70
|
+
export async function repositoryCommitExists(cwd: string, commit: string, signal?: AbortSignal): Promise<boolean> {
|
|
71
|
+
if (!/^[0-9a-f]{40,64}$/iu.test(commit)) return false;
|
|
72
|
+
try {
|
|
73
|
+
const result = await execFileAsync("git", ["cat-file", "-e", `${commit}^{commit}`], {
|
|
74
|
+
cwd,
|
|
75
|
+
timeout: 30_000,
|
|
76
|
+
maxBuffer: 1024,
|
|
77
|
+
signal,
|
|
78
|
+
env: workerEnvironment(process.env, { GIT_TERMINAL_PROMPT: "0" }),
|
|
79
|
+
});
|
|
80
|
+
return result.stderr.length === 0;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Determine whether cwd is a non-bare Git worktree without invoking a shell. */
|
|
87
|
+
export async function repositoryWorkTree(cwd: string, signal?: AbortSignal): Promise<boolean | undefined> {
|
|
88
|
+
try {
|
|
89
|
+
const result = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
90
|
+
cwd,
|
|
91
|
+
timeout: 30_000,
|
|
92
|
+
maxBuffer: 1024,
|
|
93
|
+
signal,
|
|
94
|
+
env: workerEnvironment(process.env, { GIT_TERMINAL_PROMPT: "0" }),
|
|
95
|
+
});
|
|
96
|
+
const value = String(result.stdout).trim();
|
|
97
|
+
return value === "true" ? true : value === "false" ? false : undefined;
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read the current symbolic branch without invoking a shell. */
|
|
104
|
+
export async function repositoryBranch(cwd: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
105
|
+
try {
|
|
106
|
+
const result = await execFileAsync("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
107
|
+
cwd,
|
|
108
|
+
timeout: 30_000,
|
|
109
|
+
maxBuffer: 1024,
|
|
110
|
+
signal,
|
|
111
|
+
env: workerEnvironment(process.env, { GIT_TERMINAL_PROMPT: "0" }),
|
|
112
|
+
});
|
|
113
|
+
const branch = String(result.stdout).trim();
|
|
114
|
+
return branch && /^[A-Za-z0-9._/-]+$/u.test(branch) ? branch : undefined;
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
46
120
|
export async function verify(
|
|
47
121
|
cwd: string,
|
|
48
122
|
command: VerificationCommand = { command: "git", args: ["diff", "--check"] },
|
|
@@ -91,21 +165,29 @@ export async function verifyAll(cwd: string, checks: readonly AcceptanceCheck[],
|
|
|
91
165
|
}
|
|
92
166
|
|
|
93
167
|
/** Collect bounded repository evidence for the independent read-only Reviewer. */
|
|
94
|
-
export async function collectRepositoryEvidence(cwd: string, options: Pick<VerificationOptions, "signal"> = {}): Promise<RepositoryEvidence> {
|
|
168
|
+
export async function collectRepositoryEvidence(cwd: string, options: Pick<VerificationOptions, "signal"> & { baseRef?: string } = {}): Promise<RepositoryEvidence> {
|
|
95
169
|
throwIfAborted(options.signal);
|
|
96
|
-
const
|
|
170
|
+
const baseRef = options.baseRef;
|
|
171
|
+
const diffRef = baseRef ?? "HEAD";
|
|
172
|
+
const commitArgs = baseRef ? ["log", "--format=%h %s", "--no-decorate", `${baseRef}..HEAD`, "--"] : ["log", "--format=%h %s", "--no-decorate", "-20", "--"];
|
|
173
|
+
const [statusResult, diffResult, commitsResult, branchResult, untrackedResult] = await Promise.all([
|
|
97
174
|
readGitEvidence(cwd, ["status", "--short", "--untracked-files=all"], options.signal),
|
|
98
|
-
//
|
|
99
|
-
readGitEvidence(cwd, ["diff", "--no-ext-diff", "--unified=3",
|
|
175
|
+
// A baseline-relative diff includes committed, staged, and unstaged changes.
|
|
176
|
+
readGitEvidence(cwd, ["diff", "--no-ext-diff", "--unified=3", diffRef, "--"], options.signal),
|
|
177
|
+
readGitEvidence(cwd, commitArgs, options.signal),
|
|
178
|
+
readGitEvidence(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], options.signal),
|
|
100
179
|
collectUntrackedEvidence(cwd, options.signal),
|
|
101
180
|
]);
|
|
102
181
|
throwIfAborted(options.signal);
|
|
103
182
|
return {
|
|
104
183
|
status: statusResult.text,
|
|
105
184
|
diff: diffResult.text,
|
|
185
|
+
commits: commitsResult.text,
|
|
106
186
|
untracked: untrackedResult.text,
|
|
107
|
-
|
|
108
|
-
|
|
187
|
+
...(baseRef ? { baseRef } : {}),
|
|
188
|
+
...(branchResult.text.trim() !== "(none)" ? { branch: branchResult.text.trim() } : {}),
|
|
189
|
+
complete: statusResult.complete && diffResult.complete && commitsResult.complete && branchResult.complete && untrackedResult.complete,
|
|
190
|
+
truncated: statusResult.truncated || diffResult.truncated || commitsResult.truncated || branchResult.truncated || untrackedResult.truncated,
|
|
109
191
|
collectedAt: new Date().toISOString(),
|
|
110
192
|
};
|
|
111
193
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { delimiter, dirname, join } from "node:path";
|
|
4
|
+
|
|
1
5
|
const inheritedNames = [
|
|
2
6
|
"PATH",
|
|
3
7
|
"HOME",
|
|
@@ -29,3 +33,168 @@ export function workerEnvironment(
|
|
|
29
33
|
}
|
|
30
34
|
return result;
|
|
31
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Restrict explicit variables supplied to an unattended Worker. This is not a
|
|
39
|
+
* network sandbox, but it removes common remote-repository credentials and
|
|
40
|
+
* disables the Git/package-manager credential helpers before command policy
|
|
41
|
+
* gets a chance to inspect a structured Bash request.
|
|
42
|
+
*/
|
|
43
|
+
export function automaticWorkerEnvironment(explicit: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
|
44
|
+
const result: NodeJS.ProcessEnv = {};
|
|
45
|
+
for (const [name, value] of Object.entries(explicit)) {
|
|
46
|
+
if (value !== undefined && isAutomaticAllowedName(name) && (!isRemoteCredentialName(name) || isProviderCredentialName(name))) result[name] = value;
|
|
47
|
+
}
|
|
48
|
+
result.GIT_CONFIG_NOSYSTEM = "1";
|
|
49
|
+
result.GIT_CONFIG_SYSTEM = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
50
|
+
result.GIT_CONFIG_GLOBAL = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
51
|
+
result.GIT_CONFIG_COUNT = "3";
|
|
52
|
+
result.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
53
|
+
result.GIT_CONFIG_VALUE_0 = "";
|
|
54
|
+
result.GIT_CONFIG_KEY_1 = "http.proxy";
|
|
55
|
+
result.GIT_CONFIG_VALUE_1 = "http://127.0.0.1:9";
|
|
56
|
+
result.GIT_CONFIG_KEY_2 = "https.proxy";
|
|
57
|
+
result.GIT_CONFIG_VALUE_2 = "http://127.0.0.1:9";
|
|
58
|
+
result.GIT_TERMINAL_PROMPT = "0";
|
|
59
|
+
result.GIT_SSH_COMMAND = "false";
|
|
60
|
+
result.GH_CONFIG_DIR = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
61
|
+
result.NPM_CONFIG_USERCONFIG = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
62
|
+
result.npm_config_userconfig = result.NPM_CONFIG_USERCONFIG;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ask Claude Code to sandbox Bash and its descendants. The API process keeps
|
|
68
|
+
* its provider connection, while Worker-launched commands get no outbound
|
|
69
|
+
* network and cannot silently fall back to an unsandboxed shell. Automatic mode
|
|
70
|
+
* admits only the direct Claude command name. Startup resolves and pins its
|
|
71
|
+
* operator-owned executable path so a custom path or writable replacement
|
|
72
|
+
* cannot silently omit this boundary.
|
|
73
|
+
*/
|
|
74
|
+
export function automaticClaudeArgs(command: string, args: readonly string[] = []): string[] {
|
|
75
|
+
if (!isDirectClaudeName(command)) {
|
|
76
|
+
throw new Error("automatic supervision requires the direct Claude executable command name; custom executable paths need their own host boundary");
|
|
77
|
+
}
|
|
78
|
+
if (args.some((arg) => arg === "--settings" || arg.startsWith("--settings="))) {
|
|
79
|
+
throw new Error("automatic Claude supervision controls --settings; remove the caller-provided settings override");
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
...args,
|
|
83
|
+
"--settings",
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
sandbox: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
failIfUnavailable: true,
|
|
88
|
+
allowUnsandboxedCommands: false,
|
|
89
|
+
network: { allowedDomains: [] },
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve and pin the executable used by automatic mode before preflight and
|
|
97
|
+
* spawn. A bare command is resolved from the supervisor's own PATH; explicit
|
|
98
|
+
* paths and writable/untrusted executable locations are rejected. Operators
|
|
99
|
+
* who do not want PATH to be the trust root can set
|
|
100
|
+
* PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE to the expected executable path.
|
|
101
|
+
*/
|
|
102
|
+
export async function assertTrustedAutomaticClaudeExecutable(command: string, expectedPath?: string): Promise<string> {
|
|
103
|
+
if (!isDirectClaudeName(command)) {
|
|
104
|
+
throw new Error("automatic supervision requires the direct Claude executable command name; custom executable paths need their own host boundary");
|
|
105
|
+
}
|
|
106
|
+
const candidate = await resolveExecutable(command, process.env.PATH);
|
|
107
|
+
if (!candidate) throw new Error("automatic supervision could not resolve the trusted Claude executable from PATH");
|
|
108
|
+
const resolved = await realpath(candidate);
|
|
109
|
+
await assertSecureExecutablePath(resolved);
|
|
110
|
+
const configured = expectedPath?.trim() || process.env.PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE?.trim();
|
|
111
|
+
if (configured) {
|
|
112
|
+
let expected: string;
|
|
113
|
+
try {
|
|
114
|
+
expected = await realpath(configured);
|
|
115
|
+
await assertSecureExecutablePath(expected);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof Error && /writable|owned|regular file/u.test(error.message)) throw error;
|
|
118
|
+
throw new Error("the expected Claude executable path is not a readable executable");
|
|
119
|
+
}
|
|
120
|
+
if (expected !== resolved) {
|
|
121
|
+
throw new Error("resolved Claude executable does not match the expected pinned identity");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isDirectClaudeName(command: string): boolean {
|
|
128
|
+
if (command.includes("/") || command.includes("\\")) return false;
|
|
129
|
+
const executable = command.toLowerCase();
|
|
130
|
+
return executable === "claude" || executable === "claude.exe";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function resolveExecutable(command: string, pathValue: string | undefined): Promise<string | undefined> {
|
|
134
|
+
for (const directory of (pathValue ?? "").split(delimiter).filter(Boolean)) {
|
|
135
|
+
const candidate = join(directory, command);
|
|
136
|
+
try {
|
|
137
|
+
await access(candidate, fsConstants.X_OK);
|
|
138
|
+
return candidate;
|
|
139
|
+
} catch {
|
|
140
|
+
// Continue to the next PATH entry.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function assertSecureExecutablePath(path: string): Promise<void> {
|
|
147
|
+
await access(path, fsConstants.X_OK);
|
|
148
|
+
const executable = await stat(path);
|
|
149
|
+
if (!executable.isFile()) throw new Error("resolved Claude executable is not a regular file");
|
|
150
|
+
if (process.platform !== "win32") {
|
|
151
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
152
|
+
if ((executable.mode & 0o022) !== 0 || (uid !== undefined && executable.uid !== uid && executable.uid !== 0)) {
|
|
153
|
+
throw new Error("resolved Claude executable is writable by or owned by an untrusted user");
|
|
154
|
+
}
|
|
155
|
+
let directory = dirname(path);
|
|
156
|
+
while (true) {
|
|
157
|
+
const info = await stat(directory);
|
|
158
|
+
if ((info.mode & 0o022) !== 0 || (uid !== undefined && info.uid !== uid && info.uid !== 0)) {
|
|
159
|
+
throw new Error("a directory containing the resolved Claude executable is writable by or owned by an untrusted user");
|
|
160
|
+
}
|
|
161
|
+
const parent = dirname(directory);
|
|
162
|
+
if (parent === directory) break;
|
|
163
|
+
directory = parent;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const automaticEnvironmentAllowlist = new Set([
|
|
169
|
+
"ANTHROPIC_API_KEY",
|
|
170
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
171
|
+
"ANTHROPIC_BASE_URL",
|
|
172
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
173
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
174
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
175
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
176
|
+
"AWS_PROFILE",
|
|
177
|
+
"AWS_REGION",
|
|
178
|
+
"AWS_DEFAULT_REGION",
|
|
179
|
+
"AWS_SDK_LOAD_CONFIG",
|
|
180
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
181
|
+
"GOOGLE_CLOUD_REGION",
|
|
182
|
+
"CLOUD_ML_REGION",
|
|
183
|
+
"NO_COLOR",
|
|
184
|
+
"CI",
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
function isAutomaticAllowedName(name: string): boolean {
|
|
188
|
+
return automaticEnvironmentAllowlist.has(name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isProviderCredentialName(name: string): boolean {
|
|
192
|
+
return name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN" || name === "CLAUDE_CODE_OAUTH_TOKEN";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function isRemoteCredentialName(name: string): boolean {
|
|
196
|
+
if (isProviderCredentialName(name)) return false;
|
|
197
|
+
return /^(?:SSH_AUTH_SOCK|GIT_ASKPASS|GIT_SSH_COMMAND|GIT_CREDENTIAL_HELPER|GIT_CONFIG(?:_|$)|GH_CONFIG_DIR|NPM_CONFIG_USERCONFIG|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN|AZURE_CLIENT_ID|AZURE_CLIENT_SECRET|AZURE_TENANT_ID|GOOGLE_APPLICATION_CREDENTIALS|KUBECONFIG)$/iu.test(name)
|
|
198
|
+
|| /(?:^|_)(?:GITHUB|GH|GITLAB|BITBUCKET|NPM|NODE_AUTH|CODEARTIFACT|HUGGINGFACE|DOCKER|AWS|AZURE|GOOGLE|CI_JOB)(?:_|$)/iu.test(name)
|
|
199
|
+
|| /(?:^|_)(?:TOKEN|PASSWORD|PASSWD|SECRET|PRIVATE_KEY|ACCESS_KEY|AUTH_TOKEN|API_KEY|CREDENTIALS?)(?:_|$)/iu.test(name);
|
|
200
|
+
}
|
|
@@ -172,6 +172,21 @@ export class ProcessWorkerAdapter implements WorkerAdapter {
|
|
|
172
172
|
const launch = cgroupPath
|
|
173
173
|
? cgroupBootstrapLaunch(input.command, args, input.cwd, workerEnv, cgroupPath)
|
|
174
174
|
: { command: input.command, args, env: workerEnv };
|
|
175
|
+
try {
|
|
176
|
+
// This is the last asynchronous operation before spawn. Automatic mode
|
|
177
|
+
// uses it for an exact repository HEAD assertion; adapters that add
|
|
178
|
+
// setup work must invoke the hook only after that work is complete.
|
|
179
|
+
await input.preSpawnCheck?.();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (cgroupPath) await rm(cgroupPath, { recursive: true, force: true }).catch(() => {});
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const finalAbortReason = input.startupToken ? this.#pendingStartupAborts.get(input.startupToken) : undefined;
|
|
185
|
+
if (input.startupToken) this.#pendingStartupAborts.delete(input.startupToken);
|
|
186
|
+
if (input.abortSignal?.aborted || finalAbortReason !== undefined) {
|
|
187
|
+
if (cgroupPath) await rm(cgroupPath, { recursive: true, force: true }).catch(() => {});
|
|
188
|
+
throw new Error(`worker startup aborted${finalAbortReason ? `: ${finalAbortReason}` : " before spawn"}`);
|
|
189
|
+
}
|
|
175
190
|
let child: ChildProcess;
|
|
176
191
|
try {
|
|
177
192
|
child = spawn(launch.command, launch.args, {
|