pi-claude-supervisor 0.5.2 → 0.5.4

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/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
 
@@ -51,10 +52,17 @@ export interface WorkerStartInput {
51
52
  };
52
53
  /** Adopted sessions must not replay the task as a new user message. */
53
54
  sendInitialInput?: boolean;
55
+ /** Automatic mode selects the structured tmux bridge instead of the manual TUI transport. */
56
+ automatic?: boolean;
54
57
  /** Cancel startup before a worker is fully returned to the supervisor. */
55
58
  abortSignal?: AbortSignal;
56
59
  /** Internal token that scopes out-of-band startup cancellation. */
57
60
  startupToken?: string;
61
+ /**
62
+ * Automatic-start repository assertion. Built-in adapters invoke this after
63
+ * all asynchronous setup and immediately before spawning the Worker.
64
+ */
65
+ preSpawnCheck?: () => Promise<void>;
58
66
  }
59
67
 
60
68
  export interface WorkerHandle {
@@ -105,7 +113,7 @@ export interface PermissionDecision {
105
113
  export interface WorkerAdapter {
106
114
  capabilities(): WorkerCapabilities;
107
115
  /** Validate host/transport prerequisites before starting a Worker. */
108
- preflight?(input: Pick<WorkerStartInput, "cwd" | "command" | "args" | "env" | "approval">): Promise<void>;
116
+ preflight?(input: Pick<WorkerStartInput, "cwd" | "command" | "args" | "env" | "approval" | "automatic">): Promise<void>;
109
117
  start(input: WorkerStartInput): Promise<WorkerHandle>;
110
118
  /** Cancel adapter-owned startup work before a WorkerHandle is returned. */
111
119
  abortStart?(reason: string, startupToken?: string): Promise<void>;
@@ -166,6 +174,15 @@ export interface AcceptanceCheckResult {
166
174
  finishedAt: string;
167
175
  }
168
176
 
177
+ export interface TaskAutonomy {
178
+ /** Local development continues without synchronous human approval. */
179
+ unattended: boolean;
180
+ /** The Worker should commit the candidate on its local branch before completion. */
181
+ requireLocalCommit: boolean;
182
+ /** Number of autonomous Decision Worker retries before parking a candidate. */
183
+ maxDecisionRetries: number;
184
+ }
185
+
169
186
  export interface TaskSpec {
170
187
  goal: string;
171
188
  scope: string[];
@@ -173,8 +190,12 @@ export interface TaskSpec {
173
190
  forbidden: string[];
174
191
  acceptance: AcceptanceCheck[];
175
192
  maxRepairRounds: number;
193
+ autonomy: TaskAutonomy;
176
194
  }
177
195
 
196
+ /** Public input shape; nested autonomy fields may be omitted and are defaulted. */
197
+ export type TaskSpecInput = Partial<Omit<TaskSpec, "autonomy">> & { autonomy?: Partial<TaskAutonomy> };
198
+
178
199
  export type ReviewVerdict = "pass" | "revise" | "human";
179
200
  export type ReviewSeverity = "P0" | "P1" | "P2" | "P3";
180
201
 
@@ -203,6 +224,10 @@ export interface TaskContext {
203
224
  cwd: string;
204
225
  maxTurns: number;
205
226
  startedAt: string;
227
+ /** Repository HEAD before this task; used to review local commits as well as worktree changes. */
228
+ baseCommit?: string;
229
+ /** Non-protected local branch recorded before automatic work begins. */
230
+ baseBranch?: string;
206
231
  spec: TaskSpec;
207
232
  repairRound: number;
208
233
  lastFindingSignature?: string;
package/src/verifier.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { constants as fsConstants } from "node:fs";
3
- import { lstat, open, realpath } from "node:fs/promises";
3
+ import { lstat, open, readlink, realpath } from "node:fs/promises";
4
4
  import { isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import type { AcceptanceCheck, AcceptanceCheckResult, AcceptanceReport, VerificationResult } from "./types.ts";
@@ -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 [statusResult, diffResult, untrackedResult] = await Promise.all([
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
- // HEAD-relative diff includes both staged and unstaged changes for tracked files.
99
- readGitEvidence(cwd, ["diff", "--no-ext-diff", "--unified=3", "HEAD", "--"], options.signal),
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
- complete: statusResult.complete && diffResult.complete && untrackedResult.complete,
108
- truncated: statusResult.truncated || diffResult.truncated || untrackedResult.truncated,
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
  }
@@ -228,6 +310,12 @@ async function collectUntrackedEvidence(cwd: string, signal?: AbortSignal): Prom
228
310
  sections.push(`--- ${JSON.stringify(path)} [non-regular file omitted]`);
229
311
  continue;
230
312
  }
313
+ if (opened.nlink > 1) {
314
+ complete = false;
315
+ sections.push(`--- ${JSON.stringify(path)} [hard-link file omitted]`);
316
+ continue;
317
+ }
318
+ await assertOpenedEvidencePath(root, handle.fd);
231
319
  const buffer = Buffer.alloc(MAX_UNTRACKED_FILE_BYTES + 1);
232
320
  const read = await handle.read(buffer, 0, buffer.length, 0);
233
321
  const bytes = buffer.subarray(0, read.bytesRead);
@@ -256,6 +344,17 @@ async function collectUntrackedEvidence(cwd: string, signal?: AbortSignal): Prom
256
344
  return { ...bounded, complete: complete && bounded.complete, truncated: truncated || bounded.truncated };
257
345
  }
258
346
 
347
+ async function assertOpenedEvidencePath(root: string, fd: number): Promise<void> {
348
+ if (process.platform !== "linux") throw new Error("opened evidence identity cannot be verified on this platform");
349
+ const target = (await readlink(`/proc/self/fd/${fd}`)).replace(/ \(deleted\)$/u, "");
350
+ const normalizedTarget = target.replaceAll("\\", "/");
351
+ if (normalizedTarget.split("/").some((segment) => segment.toLowerCase() === ".git")) throw new Error("opened evidence path resolves into Git metadata");
352
+ const relativeTarget = relative(root, target);
353
+ if (isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`) || relativeTarget.startsWith(`..${"\\"}`)) {
354
+ throw new Error("opened evidence path escaped the repository root");
355
+ }
356
+ }
357
+
259
358
  async function assertNoSymlinkComponents(root: string, relativePath: string): Promise<void> {
260
359
  const parts = relativePath.split(sep).filter(Boolean);
261
360
  let current = root;
@@ -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,216 @@ 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
+ const automaticToolAllowlist = new Set([
75
+ "Bash",
76
+ "Edit",
77
+ "Glob",
78
+ "Grep",
79
+ "Read",
80
+ "Write",
81
+ "NotebookEdit",
82
+ "AskUserQuestion",
83
+ ]);
84
+
85
+ /**
86
+ * The automatic Worker is the only Claude session allowed to drive the local
87
+ * lifecycle. Keep its tool surface explicit: nested agents/background tasks
88
+ * can start a second reviewer, outlive the Worker, or hide work from the
89
+ * Supervisor's event stream. The safe-mode and strict-MCP flags also prevent
90
+ * project/user customizations from reintroducing that capability.
91
+ */
92
+ export function automaticClaudeArgs(command: string, args: readonly string[] = []): string[] {
93
+ if (!isDirectClaudeName(command)) {
94
+ throw new Error("automatic supervision requires the direct Claude executable command name; custom executable paths need their own host boundary");
95
+ }
96
+ if (args.some((arg) => arg === "--settings" || arg.startsWith("--settings="))) {
97
+ throw new Error("automatic Claude supervision controls --settings; remove the caller-provided settings override");
98
+ }
99
+ for (const option of ["--allowedTools", "--allowed-tools", "--agent", "--agents", "--plugin-dir", "--plugin-url", "--resume", "-r", "--continue", "-c", "--bg", "--background", "--remote-control", "--tmux", "--worktree", "-w"]) {
100
+ if (args.some((arg) => arg === option || arg.startsWith(`${option}=`))) {
101
+ throw new Error(`automatic Claude supervision controls ${option}; remove the caller-provided session or tool-extension override`);
102
+ }
103
+ }
104
+ const result = [...args];
105
+ const tools = requestedAutomaticTools(result);
106
+ if (tools === undefined) result.push("--tools", [...automaticToolAllowlist].join(","));
107
+ else if (tools.some((tool) => !automaticToolAllowlist.has(tool))) {
108
+ throw new Error("automatic Claude supervision permits only the bounded local tool allowlist; nested agents and background reviewers are disabled");
109
+ }
110
+ result.push(
111
+ "--safe-mode",
112
+ "--strict-mcp-config",
113
+ "--disallowed-tools",
114
+ "Task,TaskOutput,Agent,Skill,SendUserMessage",
115
+ "--settings",
116
+ JSON.stringify({
117
+ sandbox: {
118
+ enabled: true,
119
+ failIfUnavailable: true,
120
+ allowUnsandboxedCommands: false,
121
+ network: { allowedDomains: [] },
122
+ },
123
+ }),
124
+ );
125
+ return result;
126
+ }
127
+
128
+ function requestedAutomaticTools(args: string[]): string[] | undefined {
129
+ const index = args.findIndex((arg) => arg === "--tools" || arg.startsWith("--tools="));
130
+ if (index < 0) return undefined;
131
+ const values = args[index].startsWith("--tools=")
132
+ ? [args[index].slice("--tools=".length)]
133
+ : (() => {
134
+ const collected: string[] = [];
135
+ for (let cursor = index + 1; cursor < args.length && !args[cursor]!.startsWith("-"); cursor += 1) collected.push(args[cursor]!);
136
+ return collected;
137
+ })();
138
+ const tools = values.flatMap((value) => value.split(/[\s,]+/u)).map((value) => value.trim()).filter(Boolean);
139
+ if (tools.includes("default")) throw new Error("automatic Claude supervision requires an explicit tool allowlist; --tools default is not permitted");
140
+ return tools.map((tool) => tool.replace(/\(.*/u, ""));
141
+ }
142
+
143
+ /**
144
+ * Resolve and pin the executable used by automatic mode before preflight and
145
+ * spawn. A bare command is resolved from the supervisor's own PATH; explicit
146
+ * paths and writable/untrusted executable locations are rejected. Operators
147
+ * who do not want PATH to be the trust root can set
148
+ * PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE to the expected executable path.
149
+ */
150
+ export async function assertTrustedAutomaticClaudeExecutable(command: string, expectedPath?: string): Promise<string> {
151
+ if (!isDirectClaudeName(command)) {
152
+ throw new Error("automatic supervision requires the direct Claude executable command name; custom executable paths need their own host boundary");
153
+ }
154
+ const candidate = await resolveExecutable(command, process.env.PATH);
155
+ if (!candidate) throw new Error("automatic supervision could not resolve the trusted Claude executable from PATH");
156
+ const resolved = await realpath(candidate);
157
+ await assertSecureExecutablePath(resolved);
158
+ const configured = expectedPath?.trim() || process.env.PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE?.trim();
159
+ if (configured) {
160
+ let expected: string;
161
+ try {
162
+ expected = await realpath(configured);
163
+ await assertSecureExecutablePath(expected);
164
+ } catch (error) {
165
+ if (error instanceof Error && /writable|owned|regular file/u.test(error.message)) throw error;
166
+ throw new Error("the expected Claude executable path is not a readable executable");
167
+ }
168
+ if (expected !== resolved) {
169
+ throw new Error("resolved Claude executable does not match the expected pinned identity");
170
+ }
171
+ }
172
+ return resolved;
173
+ }
174
+
175
+ function isDirectClaudeName(command: string): boolean {
176
+ if (command.includes("/") || command.includes("\\")) return false;
177
+ const executable = command.toLowerCase();
178
+ return executable === "claude" || executable === "claude.exe";
179
+ }
180
+
181
+ async function resolveExecutable(command: string, pathValue: string | undefined): Promise<string | undefined> {
182
+ for (const directory of (pathValue ?? "").split(delimiter).filter(Boolean)) {
183
+ const candidate = join(directory, command);
184
+ try {
185
+ await access(candidate, fsConstants.X_OK);
186
+ return candidate;
187
+ } catch {
188
+ // Continue to the next PATH entry.
189
+ }
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ async function assertSecureExecutablePath(path: string): Promise<void> {
195
+ await access(path, fsConstants.X_OK);
196
+ const executable = await stat(path);
197
+ if (!executable.isFile()) throw new Error("resolved Claude executable is not a regular file");
198
+ if (process.platform !== "win32") {
199
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
200
+ if ((executable.mode & 0o022) !== 0 || (uid !== undefined && executable.uid !== uid && executable.uid !== 0)) {
201
+ throw new Error("resolved Claude executable is writable by or owned by an untrusted user");
202
+ }
203
+ let directory = dirname(path);
204
+ while (true) {
205
+ const info = await stat(directory);
206
+ if ((info.mode & 0o022) !== 0 || (uid !== undefined && info.uid !== uid && info.uid !== 0)) {
207
+ throw new Error("a directory containing the resolved Claude executable is writable by or owned by an untrusted user");
208
+ }
209
+ const parent = dirname(directory);
210
+ if (parent === directory) break;
211
+ directory = parent;
212
+ }
213
+ }
214
+ }
215
+
216
+ const automaticEnvironmentAllowlist = new Set([
217
+ "ANTHROPIC_API_KEY",
218
+ "ANTHROPIC_AUTH_TOKEN",
219
+ "ANTHROPIC_BASE_URL",
220
+ "CLAUDE_CODE_OAUTH_TOKEN",
221
+ "CLAUDE_CODE_USE_BEDROCK",
222
+ "CLAUDE_CODE_USE_VERTEX",
223
+ "CLAUDE_CODE_USE_FOUNDRY",
224
+ "AWS_PROFILE",
225
+ "AWS_REGION",
226
+ "AWS_DEFAULT_REGION",
227
+ "AWS_SDK_LOAD_CONFIG",
228
+ "GOOGLE_CLOUD_PROJECT",
229
+ "GOOGLE_CLOUD_REGION",
230
+ "CLOUD_ML_REGION",
231
+ "NO_COLOR",
232
+ "CI",
233
+ ]);
234
+
235
+ function isAutomaticAllowedName(name: string): boolean {
236
+ return automaticEnvironmentAllowlist.has(name);
237
+ }
238
+
239
+ function isProviderCredentialName(name: string): boolean {
240
+ return name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN" || name === "CLAUDE_CODE_OAUTH_TOKEN";
241
+ }
242
+
243
+ export function isRemoteCredentialName(name: string): boolean {
244
+ if (isProviderCredentialName(name)) return false;
245
+ 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)
246
+ || /(?:^|_)(?:GITHUB|GH|GITLAB|BITBUCKET|NPM|NODE_AUTH|CODEARTIFACT|HUGGINGFACE|DOCKER|AWS|AZURE|GOOGLE|CI_JOB)(?:_|$)/iu.test(name)
247
+ || /(?:^|_)(?:TOKEN|PASSWORD|PASSWD|SECRET|PRIVATE_KEY|ACCESS_KEY|AUTH_TOKEN|API_KEY|CREDENTIALS?)(?:_|$)/iu.test(name);
248
+ }