fullcourtdefense-cli 1.35.8 → 1.35.10
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/dist/actionIdentity.js +2 -1
- package/dist/actionPolicyEngine.d.ts +1 -0
- package/dist/actionPolicyEngine.js +203 -14
- package/dist/approvalPrompt.js +2 -2
- package/dist/askDialog.js +6 -6
- package/dist/blockExplanation.js +2 -0
- package/dist/callerEnv.d.ts +43 -0
- package/dist/callerEnv.js +107 -0
- package/dist/commandNarrative.d.ts +8 -0
- package/dist/commandNarrative.js +179 -13
- package/dist/commands/agentCi.js +3 -1
- package/dist/commands/daemon.js +7 -2
- package/dist/commands/deterministicGuard.d.ts +1 -1
- package/dist/commands/deterministicGuard.js +319 -28
- package/dist/commands/discover.js +23 -0
- package/dist/commands/discoverAgentFiles.d.ts +11 -0
- package/dist/commands/discoverAgentFiles.js +107 -0
- package/dist/commands/discoverExtensions.d.ts +64 -0
- package/dist/commands/discoverExtensions.js +386 -0
- package/dist/commands/hook.d.ts +58 -1
- package/dist/commands/hook.js +223 -37
- package/dist/commands/local-scan.js +1 -0
- package/dist/commands/mcpGateway.js +273 -54
- package/dist/commands/protectedRun.d.ts +1 -1
- package/dist/commands/protectedRun.js +3 -3
- package/dist/commands/taintLedger.js +3 -1
- package/dist/commands/watchdog.js +2 -0
- package/dist/credentialGrant.d.ts +43 -0
- package/dist/credentialGrant.js +504 -0
- package/dist/daemonForensics.d.ts +27 -0
- package/dist/daemonForensics.js +47 -0
- package/dist/detectorBaseline.d.ts +2 -2
- package/dist/detectorBaseline.js +2 -2
- package/dist/devConfirm.d.ts +76 -13
- package/dist/devConfirm.js +352 -144
- package/dist/envDiagnostics.js +4 -2
- package/dist/hookSlim.d.ts +1 -1
- package/dist/hookSlim.js +26 -5
- package/dist/ideExtensionRisk.d.ts +110 -0
- package/dist/ideExtensionRisk.js +172 -0
- package/dist/machineActionVerify.d.ts +34 -8
- package/dist/machineActionVerify.js +76 -16
- package/dist/mcpProtocol.d.ts +124 -0
- package/dist/mcpProtocol.js +302 -0
- package/dist/mcpToolReview.d.ts +2 -1
- package/dist/mcpToolReview.js +6 -4
- package/dist/nativeHook.d.ts +2 -1
- package/dist/nativeHook.js +109 -3
- package/dist/notify.d.ts +7 -0
- package/dist/notify.js +65 -0
- package/dist/shellQuoteMask.d.ts +25 -0
- package/dist/shellQuoteMask.js +319 -0
- package/dist/telemetry.d.ts +4 -0
- package/dist/telemetry.js +6 -0
- package/dist/verdictIpc.js +21 -2
- package/dist/verdictIpcClient.d.ts +43 -13
- package/dist/verdictIpcClient.js +66 -27
- package/dist/version.json +1 -1
- package/package.json +8 -3
package/dist/actionIdentity.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.identityPart = identityPart;
|
|
4
4
|
exports.captureActionIdentity = captureActionIdentity;
|
|
5
|
+
const callerEnv_1 = require("./callerEnv");
|
|
5
6
|
function identityPart(value) {
|
|
6
7
|
return typeof value === 'string' && /^[A-Za-z0-9_.:@/-]{1,200}$/.test(value) ? value : undefined;
|
|
7
8
|
}
|
|
8
|
-
function captureActionIdentity(payload = {}, env =
|
|
9
|
+
function captureActionIdentity(payload = {}, env = (0, callerEnv_1.callerEnv)()) {
|
|
9
10
|
return {
|
|
10
11
|
sessionId: identityPart(payload.conversation_id || payload.conversationId || payload.session_id || payload.sessionId
|
|
11
12
|
|| env.FCD_SESSION_ID || env.CLAUDE_CODE_SESSION_ID || env.CODEX_THREAD_ID),
|
|
@@ -212,6 +212,7 @@ export declare function effectiveApprovalScope(approval: ActionPolicyApproval |
|
|
|
212
212
|
*/
|
|
213
213
|
export declare function deriveToolOperations(toolName: string, declaredActions?: string[]): string[];
|
|
214
214
|
export declare function toolCapabilities(value: string): Set<string>;
|
|
215
|
+
export declare function canonicalAgentClient(value: string): string;
|
|
215
216
|
/**
|
|
216
217
|
* "Contains a secret" means a secret VALUE is present — never that the text
|
|
217
218
|
* talks about secrets. Source code, docs, grep patterns and commit messages in
|
|
@@ -27,6 +27,7 @@ __export(actionPolicyEngine_exports, {
|
|
|
27
27
|
MACHINE_KINDS: () => MACHINE_KINDS,
|
|
28
28
|
WORKSTATION_ASK_APPROVAL: () => WORKSTATION_ASK_APPROVAL,
|
|
29
29
|
approvalExceptionError: () => approvalExceptionError,
|
|
30
|
+
canonicalAgentClient: () => canonicalAgentClient,
|
|
30
31
|
deriveToolOperations: () => deriveToolOperations,
|
|
31
32
|
detectSecretKind: () => detectSecretKind,
|
|
32
33
|
effectiveApprovalScope: () => effectiveApprovalScope,
|
|
@@ -6043,16 +6044,93 @@ function provenJavaScriptReadUrls(code) {
|
|
|
6043
6044
|
return [];
|
|
6044
6045
|
}
|
|
6045
6046
|
}
|
|
6047
|
+
var PY_READ_IMPORTS = /* @__PURE__ */ new Set(["urllib", "urllib.request", "urllib.parse", "urllib.error", "json", "re", "sys", "time", "datetime", "pprint", "textwrap", "collections", "itertools", "functools", "math", "string", "requests"]);
|
|
6048
|
+
var PY_INPUT_OR_EFFECT_RE = /__|"""|#|\b(?:open|input|stdin|argv|orig_argv|environ|getenv|getproxies\w*|proxy_bypass\w*|netrc|get_netrc_auth|get_environ_proxies|modules|exec|eval|compile|execfile|breakpoint\w*|help|codecs|importlib|getattr|setattr|delattr|globals|locals|vars|subprocess|system|popen|socket|pickle|marshal|ctypes|os|io|shutil|pathlib|glob|platform|getpass|builtins|_getframe|gi_frame|cr_frame|ag_frame|tb_frame|f_builtins|f_globals|f_locals|format|format_map|Formatter|Template|chr|post|put|patch|delete|urlretrieve|build_opener|install_opener|add_header|add_unredirected_header|add_data|Session|session|lambda|partial)\b|(?<!urllib\.)\brequest\b|\.(?:data|method|full_url|selector|host)\s*=|\b(?:urlopen|Request|get)\s*=(?!=)|\bsys\s*\.\s*(?!(?:exit|stdout|stderr)\b)\w|\.\s*sys\b|[)\]]\s*\(|"\s*\+|\+\s*"|%\s*[(\w]/;
|
|
6049
|
+
function maskPythonStrings(code) {
|
|
6050
|
+
return code.replace(/(?<!\w)([rRfF]{0,2})"((?:[^"\\\n]|\\.)*)"/g, (_m, prefix, body) => /f/i.test(prefix) ? `"${(body.match(/\{[^{}]*\}/g) || []).join(" ")}"` : '""');
|
|
6051
|
+
}
|
|
6052
|
+
var PY_REQUEST_CALL_RE = /\b(?:urllib\.request\.)?(?:urlopen|Request)\b|\brequests\.(?:get|head)\b/g;
|
|
6053
|
+
function pythonCallArguments(code, open) {
|
|
6054
|
+
const args = [];
|
|
6055
|
+
let depth = 0;
|
|
6056
|
+
let quote = "";
|
|
6057
|
+
let current2 = "";
|
|
6058
|
+
for (let i2 = open + 1; i2 < code.length; i2++) {
|
|
6059
|
+
const ch = code[i2];
|
|
6060
|
+
if (quote) {
|
|
6061
|
+
current2 += ch;
|
|
6062
|
+
if (ch === "\\") {
|
|
6063
|
+
current2 += code[++i2] ?? "";
|
|
6064
|
+
continue;
|
|
6065
|
+
}
|
|
6066
|
+
if (ch === quote) quote = "";
|
|
6067
|
+
continue;
|
|
6068
|
+
}
|
|
6069
|
+
if (ch === '"' || ch === "'") {
|
|
6070
|
+
quote = ch;
|
|
6071
|
+
current2 += ch;
|
|
6072
|
+
continue;
|
|
6073
|
+
}
|
|
6074
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
6075
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
6076
|
+
if (depth === 0) {
|
|
6077
|
+
if (ch !== ")") return void 0;
|
|
6078
|
+
args.push(current2.trim());
|
|
6079
|
+
return args.filter(Boolean);
|
|
6080
|
+
}
|
|
6081
|
+
depth--;
|
|
6082
|
+
}
|
|
6083
|
+
if (ch === "," && depth === 0) {
|
|
6084
|
+
args.push(current2.trim());
|
|
6085
|
+
current2 = "";
|
|
6086
|
+
continue;
|
|
6087
|
+
}
|
|
6088
|
+
current2 += ch;
|
|
6089
|
+
}
|
|
6090
|
+
return void 0;
|
|
6091
|
+
}
|
|
6092
|
+
function provenPythonReadUrls(code) {
|
|
6093
|
+
if (code.length > 16384) return [];
|
|
6094
|
+
const words = maskPythonStrings(code);
|
|
6095
|
+
if (PY_INPUT_OR_EFFECT_RE.test(words)) return [];
|
|
6096
|
+
for (const statement of words.matchAll(/(?:^|[\n;])[ \t]*(?:from[ \t]+([\w.]+)[ \t]+import\b([^\n;]*)|import[ \t]+([^\n;]+))/g)) {
|
|
6097
|
+
if (/\bas\b/.test(statement[2] ?? statement[3] ?? "")) return [];
|
|
6098
|
+
const modules = statement[1] !== void 0 ? [statement[1]] : statement[3].split(",").map((name) => name.trim());
|
|
6099
|
+
if (!modules.every((name) => PY_READ_IMPORTS.has(name))) return [];
|
|
6100
|
+
}
|
|
6101
|
+
const requestVariables = new Set([...words.matchAll(/\b([A-Za-z_]\w*)\s*=\s*(?:urllib\.request\.)?Request\s*\(/g)].map((m) => m[1]));
|
|
6102
|
+
const urls = [];
|
|
6103
|
+
for (const call of code.matchAll(PY_REQUEST_CALL_RE)) {
|
|
6104
|
+
const after = (call.index ?? 0) + call[0].length;
|
|
6105
|
+
const open = code.slice(after).search(/\S/) + after;
|
|
6106
|
+
if (code[open] !== "(") return [];
|
|
6107
|
+
const args = pythonCallArguments(code, open);
|
|
6108
|
+
if (!args || args.length === 0) return [];
|
|
6109
|
+
const [target, ...options] = args;
|
|
6110
|
+
if (!options.every((option) => /^timeout\s*=\s*\d+(?:\.\d+)?$/.test(option) || /^context\s*=\s*[A-Za-z_]\w*$/.test(option) || /^headers\s*=\s*\{\s*(?:"[^"\\{}]*"\s*:\s*"[^"\\{}]*"\s*,?\s*)*\}$/.test(option))) return [];
|
|
6111
|
+
if (/urlopen$/.test(call[0]) && requestVariables.has(target)) continue;
|
|
6112
|
+
const literal2 = /^[fr]?"(https?:\/\/([A-Za-z0-9.-]+)(?::\d+)?)([/?][^"\\]*)?"$/.exec(target);
|
|
6113
|
+
if (!literal2) return [];
|
|
6114
|
+
if (/^[\d.]+$/.test(literal2[2]) || isInternalHost(literal2[2])) return [];
|
|
6115
|
+
urls.push(literal2[1] + (literal2[3] || "/").replace(/\{[\s\S]*$/, ""));
|
|
6116
|
+
}
|
|
6117
|
+
return urls;
|
|
6118
|
+
}
|
|
6046
6119
|
function provenInlineJavaScriptReadUrls(command) {
|
|
6047
6120
|
const urls = [];
|
|
6048
6121
|
for (const segment of splitShellSegments(command)) {
|
|
6049
6122
|
const here = segment.match(/^\s*@'\r?\n([\s\S]*?)\r?\n'@\s*\|\s*node(?:\.exe)?(?:\s+-)?\s*$/i);
|
|
6050
6123
|
const inline = segment.match(/^\s*node(?:\.exe)?\s+(?:-e|--eval)\s+'([^']*)'\s*$/i);
|
|
6124
|
+
const python = segment.match(/^\s*python3?\s+-c\s+'([^']*)'\s*$/);
|
|
6051
6125
|
const code = here?.[1] ?? inline?.[1];
|
|
6052
6126
|
if (code !== void 0) {
|
|
6053
6127
|
const proven = provenJavaScriptReadUrls(code);
|
|
6054
6128
|
if (!proven.length) return [];
|
|
6055
6129
|
urls.push(...proven);
|
|
6130
|
+
} else if (python) {
|
|
6131
|
+
const proven = provenPythonReadUrls(python[1]);
|
|
6132
|
+
if (!proven.length) return [];
|
|
6133
|
+
urls.push(...proven);
|
|
6056
6134
|
} else {
|
|
6057
6135
|
const lead = shellSegmentLead(segment);
|
|
6058
6136
|
if (!(SHELL_READ_PROGRAMS.has(lead) || ["git", "gh", "cd", "set-location"].includes(lead)) || classifyShellCommandOperation(segment) !== "read" || /https?:\/\//i.test(segment)) return [];
|
|
@@ -6485,14 +6563,21 @@ function toolCapabilities(value) {
|
|
|
6485
6563
|
addCapabilityIfAny(capabilities, words, "mcp", ["mcp", "tool", "server"]);
|
|
6486
6564
|
return capabilities;
|
|
6487
6565
|
}
|
|
6488
|
-
function
|
|
6489
|
-
const
|
|
6490
|
-
const
|
|
6491
|
-
if (
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
const
|
|
6495
|
-
return
|
|
6566
|
+
function canonicalToolIdentity(value) {
|
|
6567
|
+
const lower = value.trim().toLowerCase();
|
|
6568
|
+
const mcp = /^mcp__(.+?)__(.+)$/.exec(lower);
|
|
6569
|
+
if (mcp) return { server: mcp[1], tool: mcp[2] };
|
|
6570
|
+
const slash = lower.lastIndexOf("/");
|
|
6571
|
+
if (slash > 0 && slash < lower.length - 1) return { server: lower.slice(0, slash).replace(/^@/, ""), tool: lower.slice(slash + 1) };
|
|
6572
|
+
const qualified = /^.+(?:__|\.)(.+)$/.exec(lower);
|
|
6573
|
+
return { tool: qualified ? qualified[1] : lower };
|
|
6574
|
+
}
|
|
6575
|
+
function toolNameIdentityMatches(actual, expected, actualServer) {
|
|
6576
|
+
const call = canonicalToolIdentity(actual);
|
|
6577
|
+
const rule = canonicalToolIdentity(expected);
|
|
6578
|
+
if (call.tool !== rule.tool) return false;
|
|
6579
|
+
const server = call.server || actualServer?.trim().toLowerCase();
|
|
6580
|
+
return !rule.server || !server || server === rule.server;
|
|
6496
6581
|
}
|
|
6497
6582
|
var IDENTITY_CONSTRAINT_FIELDS = /* @__PURE__ */ new Set(["developerName", "machineName"]);
|
|
6498
6583
|
function normalizeHostPart(value) {
|
|
@@ -6509,6 +6594,11 @@ var PATH_CONSTRAINT_FIELDS = /* @__PURE__ */ new Set(["path"]);
|
|
|
6509
6594
|
function normalizePathValue(value) {
|
|
6510
6595
|
return value.replace(/\\/g, "/");
|
|
6511
6596
|
}
|
|
6597
|
+
var AGENT_CLIENT_ALIASES = { gemini: "gemini-cli", "vs-code": "vscode" };
|
|
6598
|
+
function canonicalAgentClient(value) {
|
|
6599
|
+
const id = value.trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
6600
|
+
return AGENT_CLIENT_ALIASES[id] || id;
|
|
6601
|
+
}
|
|
6512
6602
|
function evaluateConstraint(constraint, context) {
|
|
6513
6603
|
let actual = context[constraint.field] || "";
|
|
6514
6604
|
let expected = constraint.value;
|
|
@@ -6516,6 +6606,10 @@ function evaluateConstraint(constraint, context) {
|
|
|
6516
6606
|
actual = normalizePathValue(actual);
|
|
6517
6607
|
expected = normalizePathValue(expected);
|
|
6518
6608
|
}
|
|
6609
|
+
if (constraint.field === "agentClient") {
|
|
6610
|
+
actual = canonicalAgentClient(actual);
|
|
6611
|
+
expected = canonicalAgentClient(expected);
|
|
6612
|
+
}
|
|
6519
6613
|
if (COMMAND_LINE_FIELDS.has(constraint.field) && /^(?:not_)?(?:matches|contains)$/.test(constraint.operator)) {
|
|
6520
6614
|
actual = shellCommandWords(actual);
|
|
6521
6615
|
}
|
|
@@ -6542,9 +6636,9 @@ function evaluateConstraint(constraint, context) {
|
|
|
6542
6636
|
if (constraint.field === "toolName") {
|
|
6543
6637
|
switch (constraint.operator) {
|
|
6544
6638
|
case "equals":
|
|
6545
|
-
return toolNameIdentityMatches(actual, expected);
|
|
6639
|
+
return toolNameIdentityMatches(actual, expected, context.mcpServer);
|
|
6546
6640
|
case "not_equals":
|
|
6547
|
-
return actual
|
|
6641
|
+
return !toolNameIdentityMatches(actual, expected, context.mcpServer);
|
|
6548
6642
|
case "contains":
|
|
6549
6643
|
return actual.toLowerCase().includes(expected.toLowerCase());
|
|
6550
6644
|
case "not_contains":
|
|
@@ -6948,7 +7042,7 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
6948
7042
|
if (/^sha(?:1|256|384|512)-/i.test(token)) return false;
|
|
6949
7043
|
if (/^data:/i.test(token) || /^[A-Za-z0-9+/]{200,}={0,2}$/.test(token)) return false;
|
|
6950
7044
|
if (/^(?:https?|s3|gs|file):\/\//i.test(token) || /[\\/]{1}[\w.-]+[\\/]/.test(token)) return false;
|
|
6951
|
-
if (allowWordSlug && /^[A-Za-z_][\w.-]
|
|
7045
|
+
if (allowWordSlug && /^[A-Za-z_][\w.-]*\/\w[\w.-]*\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(token)) return false;
|
|
6952
7046
|
if (allowWordSlug && /[/.]/.test(token) && token.split(/[/.]/).length >= 2 && token.split(/[/.]/).every((part) => /^[A-Za-z][a-z]*(?:[A-Z][a-z0-9]*)*$/.test(part))) return false;
|
|
6953
7047
|
if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(token) || /^[a-z]+(?:_[a-z0-9]+)+$/.test(token)) return false;
|
|
6954
7048
|
if (allowWordSlug && /^[A-Z][a-z]+-(?:[A-Z][a-z]+){2,}$/.test(token)) return false;
|
|
@@ -6964,6 +7058,12 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
6964
7058
|
var SECRET_NAME = String.raw`(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|bearer[_-]?token|client[_-]?secret|secret[_-]?key|secret|password|passwd|pwd|credentials?|private[_-]?key|token)`;
|
|
6965
7059
|
var SECRET_ASSIGNMENT_RE = new RegExp(String.raw`(?:^|[^\w])[\w.-]*?${SECRET_NAME}[\\"']*\s*(?:=>|[:=])\s*[\\"']*([^\s\\"',;&|)}\]<>]{8,})`, "gi");
|
|
6966
7060
|
var SECRET_FLAG_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)[\\"']*([^\s\\"',;&|)}\]<>-][^\s\\"',;&|)}\]<>]{7,})/gi;
|
|
7061
|
+
var SECRET_FLAG_QUOTED_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)\\?(["'])([^\s"'\\-][^\s"'\\]{7,})\\?\1/gi;
|
|
7062
|
+
function isWordPhraseWithCounters(value) {
|
|
7063
|
+
const parts = value.split(/[-_.]/);
|
|
7064
|
+
const words = parts.filter((part) => /^(?:[a-z]+|[A-Z]+|[A-Z][a-z]+)$/.test(part));
|
|
7065
|
+
return parts.length >= 3 && words.length >= 2 && words.some((word) => word.length >= 3) && parts.every((part) => words.includes(part) || /^\d{1,6}(?:[a-z]+)?$/.test(part));
|
|
7066
|
+
}
|
|
6967
7067
|
var SECRET_PLACEHOLDER_RE = /^(?:<.*>|\$\{?[A-Za-z_][\w.]*\}?|%[A-Za-z_]+%|\{\{.*\}\}|\[.*\]|\*{3,}|x{4,}|X{4,}|\.{3,}|-{3,}|_{3,}|(?:your|my|our|the|a|an)(?:[-_.][\w.-]*)?|(?:some|example|sample|dummy|fake|test|mock|placeholder|redacted|masked|hidden|changeme|change_me|replace_?me|todo|tbd|fixme|none|null|nil|undefined|true|false|required|optional|omitted|removed|unset|empty|secret|password|token|xxx+|abc+|123+)[\w.-]*)$/i;
|
|
6968
7068
|
var SECRET_CODE_EXPR_RE = /^(?:[A-Za-z_$][\w$]*(?:[.\[][^\s=]*)+|[A-Za-z_$][\w$]*\(.*\)?|process\.env\.\w+|os\.environ.*|env(?:iron)?\.\w+|new\s+\w+.*|await\s+.*|require\(.*)$/;
|
|
6969
7069
|
function looksLikeSecretValue(raw) {
|
|
@@ -6984,6 +7084,7 @@ function looksLikeSecretValue(raw) {
|
|
|
6984
7084
|
if (REPEATED_CHUNK_RE.test(value)) return false;
|
|
6985
7085
|
if (PLACEHOLDER_WORD_RE.test(value)) return false;
|
|
6986
7086
|
if (/^[a-z]+(?:[-_.][a-z]+)+$/i.test(value)) return false;
|
|
7087
|
+
if (isWordPhraseWithCounters(value)) return false;
|
|
6987
7088
|
if (/^(?:change|replace|your|my|our|example|sample|dummy|fake|test|mock|placeholder|not[-_]?real|do[-_]?not[-_]?use|insert|enter|put|paste)[-_]/i.test(value)) return false;
|
|
6988
7089
|
let classes = 0;
|
|
6989
7090
|
if (/[a-z]/.test(value)) classes++;
|
|
@@ -7022,6 +7123,10 @@ function detectSecretKind(raw) {
|
|
|
7022
7123
|
if (looksLikeSecretValue(m[1])) return "assignment";
|
|
7023
7124
|
}
|
|
7024
7125
|
}
|
|
7126
|
+
SECRET_FLAG_QUOTED_RE.lastIndex = 0;
|
|
7127
|
+
for (let m = SECRET_FLAG_QUOTED_RE.exec(value); m !== null; m = SECRET_FLAG_QUOTED_RE.exec(value)) {
|
|
7128
|
+
if (looksLikeSecretValue(m[2])) return "assignment";
|
|
7129
|
+
}
|
|
7025
7130
|
for (const re of [GENERIC_VALUE_RE, AUTH_HEADER_RE]) {
|
|
7026
7131
|
re.lastIndex = 0;
|
|
7027
7132
|
let m;
|
|
@@ -7171,6 +7276,8 @@ function withoutUnattributedJavaScriptUrls(segment) {
|
|
|
7171
7276
|
function extractUrl(args, _argsText) {
|
|
7172
7277
|
const direct = args.url || args.uri || args.endpoint || args.webhookUrl || args.callbackUrl || args.href || args.link;
|
|
7173
7278
|
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
|
7279
|
+
const listed = Array.isArray(args.urls) ? args.urls.find((url) => typeof url === "string" && url.trim()) : void 0;
|
|
7280
|
+
if (listed) return listed.trim();
|
|
7174
7281
|
for (const field of URL_ACTION_FIELDS) {
|
|
7175
7282
|
const raw = args[field];
|
|
7176
7283
|
if (typeof raw !== "string") continue;
|
|
@@ -7740,6 +7847,16 @@ function cdScopeOfSegment(segment) {
|
|
|
7740
7847
|
if (/^(?:\$\{?[a-z_][a-z0-9_]*\}?|%[a-z_][a-z0-9_]*%|\$env:[a-z_][a-z0-9_]*)\/?$/.test(lower)) return "unscoped";
|
|
7741
7848
|
return lower.split("/").some((part) => part && part !== "." && part !== "..") ? "scoped" : "unscoped";
|
|
7742
7849
|
}
|
|
7850
|
+
function isNestedScratchOrWorkPath(target) {
|
|
7851
|
+
const t = target.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
7852
|
+
const named = (parts2) => parts2.every((part) => part && !part.startsWith(".") && !/[*?[\]{}$%`]/.test(part));
|
|
7853
|
+
const tmp = /^(?:\/private)?\/(?:tmp|var\/tmp)\/(.+)$/.exec(t);
|
|
7854
|
+
if (tmp) return named(tmp[1].split("/"));
|
|
7855
|
+
const home = /^(?:~|\$\{?HOME\}?|\/home\/[^/.$*?]+|\/Users\/[^/.$*?]+|[A-Za-z]:\/Users\/[^/.$*?]+)\/(.+)$/i.exec(t);
|
|
7856
|
+
if (!home) return false;
|
|
7857
|
+
const parts = home[1].split("/");
|
|
7858
|
+
return parts.length >= 2 && named(parts) && !/^(?:Library|AppData)$/i.test(parts[0]);
|
|
7859
|
+
}
|
|
7743
7860
|
function isWorkspaceScopedRemoval(command, cwdScoped = false) {
|
|
7744
7861
|
const segments = splitShellSegments(command, true);
|
|
7745
7862
|
const removal = segments.find((seg) => /\b(rm|del|remove-item|rmdir)\b/i.test(seg)) || command;
|
|
@@ -7750,7 +7867,7 @@ function isWorkspaceScopedRemoval(command, cwdScoped = false) {
|
|
|
7750
7867
|
const t = raw.replace(/^["']|["']$/g, "");
|
|
7751
7868
|
if (t === "*" || /^\.\/?\*$/.test(t) || /^\.\\\*$/.test(t)) return cwdScoped;
|
|
7752
7869
|
if (!t || t === "." || t === "..") return false;
|
|
7753
|
-
if (/^[\/\\]/.test(t) || /^[A-Za-z]:/.test(t) || /^~/.test(t)) return
|
|
7870
|
+
if (/^[\/\\]/.test(t) || /^[A-Za-z]:/.test(t) || /^~/.test(t) || /^\$\{?HOME\}?[\/\\]/.test(t)) return isNestedScratchOrWorkPath(t);
|
|
7754
7871
|
if (/\$|%|\$\{|\$\(|`/.test(t)) return false;
|
|
7755
7872
|
if (/(^|[\/\\])\.\.([\/\\]|$)/.test(t)) return false;
|
|
7756
7873
|
if (/^\*\*?$|^[\/\\]?\*$/.test(t)) return false;
|
|
@@ -7909,10 +8026,28 @@ function classifyShellSegment(segment, depth = 0, cwdScoped = false) {
|
|
|
7909
8026
|
}
|
|
7910
8027
|
return classifyShellWords(maskQuotedSpans(raw), raw, lead, cwdScoped);
|
|
7911
8028
|
}
|
|
8029
|
+
var STDIN_DATA_MODULES = /* @__PURE__ */ new Set(["json.tool", "base64"]);
|
|
8030
|
+
var CODE_RUNS_OR_SENDS_INPUT_RE = /\b(?:exec|eval|compile|execfile|runpy|runsource|importlib|import_module|getattr|setattr|delattr|globals|locals|vars|builtins|modules|_getframe|gi_frame|f_builtins|f_globals|f_locals|Function|constructor|binding|vm|pickle|cPickle|marshal|shelve|dill|yaml|jsonpickle|require|import\s*\(|open|write|writeFile\w*|os|shutil|pathlib|fs|socket|urllib|requests|httpx|http|https|fetch|net|system|spawn|popen|Popen|subprocess|Open3|qx|readpipe|do|send|breakpoint\w*|set_trace|pdb|pty|code|interact|timeit|cProfile|profile|doctest)\b|__|(?:instance|class|module)_eval\b|[)\]]\s*\(|["']\s*\+|\+\s*["']|chr\s*\(|%x[\[{(]|`/;
|
|
8031
|
+
var PERL_CODE_REGEX_RE = /(?:^|[^\w$@%&])s\s*[^\w\s]|\(\?\??\{/;
|
|
8032
|
+
function interpreterStageReadsStdinAsData(stage) {
|
|
8033
|
+
const pieces = stage.trim().match(/"(?:[^"\\]|\\.)*"|'[^']*'|[^\s"']+/g) || [];
|
|
8034
|
+
if (pieces.join("").replace(/\s/g, "") !== stage.replace(/\s/g, "")) return false;
|
|
8035
|
+
const lead = pieces.shift() || "";
|
|
8036
|
+
if (!/^(?:python3?|perl)$/.test(lead)) return false;
|
|
8037
|
+
if (lead.startsWith("python") && pieces.length === 2 && pieces[0] === "-m") return STDIN_DATA_MODULES.has(pieces[1]);
|
|
8038
|
+
const flag = lead.startsWith("python") ? /^-c$/ : /^-[lnapF]*e$/;
|
|
8039
|
+
if (pieces.length !== 2 || !flag.test(pieces[0])) return false;
|
|
8040
|
+
const program = pieces[1];
|
|
8041
|
+
if (!/^'[^']*'$/.test(program) && !/^"[^"$`\\]*"$/.test(program)) return false;
|
|
8042
|
+
const code = program.slice(1, -1);
|
|
8043
|
+
if (lead === "perl" && PERL_CODE_REGEX_RE.test(code)) return false;
|
|
8044
|
+
return !CODE_RUNS_OR_SENDS_INPUT_RE.test(code) && !codeSpawnsProcess(code, lead);
|
|
8045
|
+
}
|
|
7912
8046
|
function classifyShellWords(masked, raw, lead, cwdScoped = false) {
|
|
7913
8047
|
const value = masked.trim().toLowerCase();
|
|
7914
8048
|
if (!value) return "read";
|
|
7915
|
-
const
|
|
8049
|
+
const interpreterStages = splitShellSegments(raw, true).slice(1).filter((stage) => /^(?:sudo\s+)?(?:python3?|node|perl|ruby)\b/i.test(stage));
|
|
8050
|
+
const pipesIntoInterpreter = /\|\s*(?:sudo\s+)?(?:ba|z|da|k)?sh\b|\|\s*(?:sudo\s+)?(?:pwsh|powershell)\b/.test(value) || /\|\s*(?:sudo\s+)?(?:python3?|node|perl|ruby)\b/.test(value) && !(interpreterStages.length > 0 && interpreterStages.every(interpreterStageReadsStdinAsData));
|
|
7916
8051
|
const segment = raw;
|
|
7917
8052
|
if (/\b(curl|wget|invoke-restmethod|invoke-webrequest|iwr|irm)\b/.test(value)) {
|
|
7918
8053
|
const explicitMethod = /(?:\s-x|\s--request|\s-method)[\s=]+["']?(post|put|patch|delete)\b/i.test(segment);
|
|
@@ -8086,7 +8221,7 @@ function astRemovalOperation(command, inContainer) {
|
|
|
8086
8221
|
const t = raw.trim().replace(/\\/g, "/");
|
|
8087
8222
|
if (/^(?:\*|\.\/+\*)$/.test(t)) return inContainer || command.cwdScope === "scoped";
|
|
8088
8223
|
if (!t || t === "." || t === "..") return false;
|
|
8089
|
-
if (/^[\/]/.test(t) || /^[A-Za-z]:/.test(t) || /^~/.test(t)) return
|
|
8224
|
+
if (/^[\/]/.test(t) || /^[A-Za-z]:/.test(t) || /^~/.test(t) || /^\$\{?HOME\}?\//.test(t)) return isNestedScratchOrWorkPath(t);
|
|
8090
8225
|
if (/\$|%|`/.test(t)) return false;
|
|
8091
8226
|
if (/(^|\/)\.\.(\/|$)/.test(t)) return false;
|
|
8092
8227
|
return true;
|
|
@@ -8211,10 +8346,35 @@ function isGcloudStorageCatRead(command) {
|
|
|
8211
8346
|
}
|
|
8212
8347
|
return objects > 0;
|
|
8213
8348
|
}
|
|
8349
|
+
function provenGitCloneOperands(tokens) {
|
|
8350
|
+
if (tokens.shift() !== "clone") return false;
|
|
8351
|
+
let urls = 0;
|
|
8352
|
+
let directories = 0;
|
|
8353
|
+
for (let i2 = 0; i2 < tokens.length; i2++) {
|
|
8354
|
+
const token = tokens[i2];
|
|
8355
|
+
if (/^(?:-q|--quiet|--single-branch|--no-single-branch|--no-tags|--no-checkout|-n|--bare|--mirror)$/.test(token)) continue;
|
|
8356
|
+
if (/^(?:--depth|--branch|-b|--origin|-o)$/.test(token) && /^[\w.\/-]+$/.test(tokens[i2 + 1] || "")) {
|
|
8357
|
+
i2++;
|
|
8358
|
+
continue;
|
|
8359
|
+
}
|
|
8360
|
+
if (/^--(?:depth|branch|origin|filter)=[\w.:\/-]+$/.test(token)) continue;
|
|
8361
|
+
if (urls === 0 && /^https?:\/\/[\w.:\/-]+$/i.test(token)) {
|
|
8362
|
+
urls++;
|
|
8363
|
+
continue;
|
|
8364
|
+
}
|
|
8365
|
+
if (urls === 1 && directories === 0 && /^[\w.~\/-]+$/.test(token) && !token.startsWith("-")) {
|
|
8366
|
+
directories++;
|
|
8367
|
+
continue;
|
|
8368
|
+
}
|
|
8369
|
+
return false;
|
|
8370
|
+
}
|
|
8371
|
+
return urls === 1;
|
|
8372
|
+
}
|
|
8214
8373
|
function provenCompoundDownload(command, detectedUrl) {
|
|
8215
8374
|
if (command.includes(detectedUrl) && provenPowerShellReadSequence(command)) return true;
|
|
8216
8375
|
const segments = splitShellSegments(command).filter((segment) => /\b(?:https?|s3|gs|ftp|sftp|smb):\/\//i.test(segment));
|
|
8217
8376
|
if (!segments.length || !command.includes(detectedUrl)) return false;
|
|
8377
|
+
if (segments.some((segment) => /^git(?:\.exe)?\s+clone\b/i.test(segment)) && splitShellSegments(command).some((segment) => !segments.includes(segment) && classifyShellSegment(segment) !== "read" && !(["rm", "rmdir", "mkdir", "cd", "pushd", "popd"].includes(shellSegmentLead(segment)) && classifyShellSegment(segment) === "write"))) return false;
|
|
8218
8378
|
return segments.every((segment) => {
|
|
8219
8379
|
const statusRead = /^\(((?:Invoke-WebRequest|iwr)\s+[^\r\n]+)\)\.(?:StatusCode|StatusDescription|RawContentLength)$/i.exec(segment);
|
|
8220
8380
|
if (statusRead) segment = statusRead[1];
|
|
@@ -8228,6 +8388,7 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
8228
8388
|
if (pieces.join("").replace(/\s/g, "") !== raw.replace(/\s/g, "")) return false;
|
|
8229
8389
|
const tokens = pieces.map((piece) => piece.replace(/^["']|["']$/g, ""));
|
|
8230
8390
|
const program = tokens.shift() || "";
|
|
8391
|
+
if (/^git(?:\.exe)?$/i.test(program)) return provenGitCloneOperands(tokens);
|
|
8231
8392
|
if (!/^(?:curl(?:\.exe)?|wget(?:\.exe)?|Invoke-RestMethod|Invoke-WebRequest|irm|iwr)$/i.test(program)) return false;
|
|
8232
8393
|
const isCurl = /^curl(?:\.exe)?$/i.test(program);
|
|
8233
8394
|
let urls = 0;
|
|
@@ -8379,6 +8540,7 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
|
|
|
8379
8540
|
if (policies.length === 0) {
|
|
8380
8541
|
return { allowed: true, verdict: "allow" };
|
|
8381
8542
|
}
|
|
8543
|
+
if (!context.toolName && toolName) context = { ...context, toolName };
|
|
8382
8544
|
const verdictPriority = {
|
|
8383
8545
|
block: 3,
|
|
8384
8546
|
require_approval: 2,
|
|
@@ -8557,8 +8719,33 @@ function inferToolContext(toolName, args) {
|
|
|
8557
8719
|
context["destination.type"] = context["request.destination.type"];
|
|
8558
8720
|
context["destination.domain"] = context["request.destination.domain"];
|
|
8559
8721
|
}
|
|
8722
|
+
applyWebReadFacts(toolName, args, context);
|
|
8560
8723
|
return { operation, context };
|
|
8561
8724
|
}
|
|
8725
|
+
var WEB_READ_TOOL_NAMES = /* @__PURE__ */ new Set(["webfetch", "fetchwebpage", "webextract"]);
|
|
8726
|
+
function applyWebReadFacts(toolName, args, context) {
|
|
8727
|
+
if (!WEB_READ_TOOL_NAMES.has(toolName.toLowerCase().replace(/[_\-.\s]/g, ""))) return;
|
|
8728
|
+
context["action.kind"] = "http";
|
|
8729
|
+
const url = extractUrl(args, "");
|
|
8730
|
+
if (!url) return;
|
|
8731
|
+
let parsed;
|
|
8732
|
+
try {
|
|
8733
|
+
parsed = new URL(url);
|
|
8734
|
+
} catch {
|
|
8735
|
+
context["url.risk"] = "invalid";
|
|
8736
|
+
return;
|
|
8737
|
+
}
|
|
8738
|
+
const hostType = isInternalHost(parsed.hostname) || isSingleLabelWebHost(parsed) ? "internal" : "external";
|
|
8739
|
+
context.url = url;
|
|
8740
|
+
context["destination.domain"] = parsed.hostname;
|
|
8741
|
+
context["url.risk"] = hostType;
|
|
8742
|
+
context["source.domain"] = parsed.hostname;
|
|
8743
|
+
context["source.type"] = hostType;
|
|
8744
|
+
if (recordHttpRequestFacts(context, args, url)) context["destination.type"] = hostType;
|
|
8745
|
+
else delete context["destination.type"];
|
|
8746
|
+
if (isLoopbackHost(parsed.hostname)) context["destination.loopback"] = "true";
|
|
8747
|
+
else delete context["destination.loopback"];
|
|
8748
|
+
}
|
|
8562
8749
|
function inferBundledToolContext(toolName, args) {
|
|
8563
8750
|
const context = {};
|
|
8564
8751
|
let operation = toolName;
|
|
@@ -8668,6 +8855,7 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8668
8855
|
context["destination.type"] = isInternalHost(context["destination.domain"]) ? "internal" : "external";
|
|
8669
8856
|
}
|
|
8670
8857
|
}
|
|
8858
|
+
applyWebReadFacts(toolName, args, context);
|
|
8671
8859
|
return { operation, context };
|
|
8672
8860
|
}
|
|
8673
8861
|
var RESERVED_INPUT_FACTS = /* @__PURE__ */ new Set([
|
|
@@ -8772,6 +8960,7 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
|
|
|
8772
8960
|
MACHINE_KINDS,
|
|
8773
8961
|
WORKSTATION_ASK_APPROVAL,
|
|
8774
8962
|
approvalExceptionError,
|
|
8963
|
+
canonicalAgentClient,
|
|
8775
8964
|
deriveToolOperations,
|
|
8776
8965
|
detectSecretKind,
|
|
8777
8966
|
effectiveApprovalScope,
|
package/dist/approvalPrompt.js
CHANGED
|
@@ -94,7 +94,7 @@ function nativeDialogView(reason, options = {}) {
|
|
|
94
94
|
'Allow once lets only this action continue.',
|
|
95
95
|
'Deny or close keeps it blocked.',
|
|
96
96
|
options.allowAlways && options.alwaysScopeLabel
|
|
97
|
-
? `Always allow
|
|
97
|
+
? `Always allow: ${line(options.alwaysScopeLabel, 180)} for ${options.alwaysLimitsLabel || '30 days'} on this machine, while the rule stays as it is written today.`
|
|
98
98
|
: '',
|
|
99
99
|
].filter(Boolean);
|
|
100
100
|
const footer = f.policy && !f.fallback
|
|
@@ -156,7 +156,7 @@ function nativeApprovalMessage(reason, options = {}) {
|
|
|
156
156
|
return [
|
|
157
157
|
'Allow this action?',
|
|
158
158
|
'', ...details, '',
|
|
159
|
-
...(options.alwaysScopeLabel ? [`Always allow
|
|
159
|
+
...(options.alwaysScopeLabel ? [`Always allow: ${line(options.alwaysScopeLabel, 200)} for ${options.alwaysLimitsLabel || '30 days'} on this machine, while the rule stays as it is written today.`, ''] : []),
|
|
160
160
|
'Allow once permits only this action. Deny or closing keeps it stopped.',
|
|
161
161
|
where,
|
|
162
162
|
].join('\n');
|
package/dist/askDialog.js
CHANGED
|
@@ -31,13 +31,14 @@ exports.resolveAskWithDialog = resolveAskWithDialog;
|
|
|
31
31
|
const devConfirm_1 = require("./devConfirm");
|
|
32
32
|
const telemetry_1 = require("./telemetry");
|
|
33
33
|
const hookIo_1 = require("./hookIo");
|
|
34
|
+
const callerEnv_1 = require("./callerEnv");
|
|
34
35
|
/** Field on the verdict JSON that carries the envelope. Stripped before the IDE sees the verdict. */
|
|
35
36
|
exports.ASK_ENVELOPE_FIELD = 'fcdAsk';
|
|
36
37
|
/** Default wait for the person. Cursor's hook window is 90 s, Claude's 60 s by default; stay inside both. */
|
|
37
38
|
exports.ASK_DIALOG_TIMEOUT_MS = 55_000;
|
|
38
39
|
/** Which channel asks the developer. `dialog` (default) = our OS dialog, `ide` = the IDE's own card. */
|
|
39
40
|
function askChannel() {
|
|
40
|
-
return (
|
|
41
|
+
return ((0, callerEnv_1.callerEnv)().FCD_ASK_CHANNEL || '').trim().toLowerCase() === 'ide' ? 'ide' : 'dialog';
|
|
41
42
|
}
|
|
42
43
|
/** Attach the envelope to a decision object (core side). */
|
|
43
44
|
function attachAskEnvelope(decision, envelope) {
|
|
@@ -140,9 +141,8 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
140
141
|
return { ...clean, resolution: 'ide' };
|
|
141
142
|
const grantHash = envelope.ruleHash || envelope.policyHash;
|
|
142
143
|
const grantLimits = envelope.grantLimits === null ? undefined : (envelope.grantLimits || { ttlMs: devConfirm_1.GRANT_TTL_MS });
|
|
143
|
-
const allowAlways = envelope.remember !== 'none' && Boolean(grantHash) && Boolean(grantLimits)
|
|
144
|
-
|
|
145
|
-
const scopeLabel = allowAlways ? (0, devConfirm_1.describeGrantScope)(envelope.scope) : undefined;
|
|
144
|
+
const allowAlways = envelope.remember !== 'none' && Boolean(grantHash) && Boolean(grantLimits) && (0, devConfirm_1.grantOffered)(envelope.scope);
|
|
145
|
+
const scopeLabel = allowAlways ? (0, devConfirm_1.describeGrantScope)(envelope.scope, { policyId: envelope.policyId, policyName: envelope.policyName }) : undefined;
|
|
146
146
|
const limitsLabel = allowAlways && grantLimits ? (0, devConfirm_1.describeGrantLimits)(grantLimits) : undefined;
|
|
147
147
|
const dialog = options.dialog || devConfirm_1.confirmNativeActionDetailed;
|
|
148
148
|
const reason = options.clientLabel ? `IDE: ${options.clientLabel}\n${envelope.reason}` : envelope.reason;
|
|
@@ -172,7 +172,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
172
172
|
toolName: envelope.toolName,
|
|
173
173
|
operation: envelope.operation,
|
|
174
174
|
reason: granted
|
|
175
|
-
? `developer chose Always allow in the FullCourtDefense dialog — ${scopeLabel} (${limitsLabel}
|
|
175
|
+
? `developer chose Always allow in the FullCourtDefense dialog — ${scopeLabel} (${limitsLabel})${policyLabel}`
|
|
176
176
|
: `developer confirmed in the FullCourtDefense dialog${policyLabel}`,
|
|
177
177
|
ruleId: envelope.policyId,
|
|
178
178
|
policyHash: envelope.policyHash,
|
|
@@ -182,7 +182,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
182
182
|
});
|
|
183
183
|
(0, telemetry_1.triggerFlush)(false);
|
|
184
184
|
const agentMsg = granted
|
|
185
|
-
? `FullCourtDefense: the developer allowed this ${envelope.event} and chose "Always allow" for ${scopeLabel} (${limitsLabel})
|
|
185
|
+
? `FullCourtDefense: the developer allowed this ${envelope.event} and chose "Always allow" for ${scopeLabel} (${limitsLabel}).`
|
|
186
186
|
: `FullCourtDefense: the developer allowed this ${envelope.event} once in the FullCourtDefense dialog.`;
|
|
187
187
|
return { ...clean, stdout: JSON.stringify(rewriteDecision(decision, true, '', agentMsg)), exitCode: 0, resolution: outcome };
|
|
188
188
|
}
|
package/dist/blockExplanation.js
CHANGED
|
@@ -84,6 +84,8 @@ function scopeWords(scope, developerName) {
|
|
|
84
84
|
return '🌐 company default — applies to every machine';
|
|
85
85
|
if (scope === 'machine' || scope === 'developer' || scope === 'local')
|
|
86
86
|
return `📍 local rule — only ${developerName || 'this machine'}`;
|
|
87
|
+
if (scope === 'shield')
|
|
88
|
+
return '📍 shield rule — only the machines on the shield it was saved on, or the ones it names';
|
|
87
89
|
return `${scope} policy`;
|
|
88
90
|
}
|
|
89
91
|
const FACT_WORDS = [
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment of the process that ASKED for a verdict (#1466).
|
|
3
|
+
*
|
|
4
|
+
* A hook answered by the resident daemon is evaluated inside the daemon's
|
|
5
|
+
* process, whose own environment belongs to a scheduled task — not to the IDE
|
|
6
|
+
* session that spawned the hook. Every fact the decision reads from the
|
|
7
|
+
* environment (is a human present, which CI / ephemeral markers are set, the
|
|
8
|
+
* session identity, the IDE's hook budget) must be the CALLER's. The thin
|
|
9
|
+
* client sends a snapshot of exactly these keys; the daemon evaluates the
|
|
10
|
+
* request with them in place of its own values, per request.
|
|
11
|
+
*
|
|
12
|
+
* Node built-ins only: this leaf is on the slim hook's warm-path module graph.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CALLER_ENV_KEYS: readonly ["FCD_HUMAN_PRESENT", "FCD_EPHEMERAL", "FULLCOURTDEFENSE_EPHEMERAL", "CI", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "DISPLAY", "WAYLAND_DISPLAY", "FCD_ASK_CHANNEL", "FCD_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID", "FCD_RUN_ID", "GITHUB_RUN_ID", "CI_PIPELINE_ID", "FCD_WORKLOAD_INSTANCE_ID", "CLAUDECODE", "CLAUDE_PROJECT_DIR", "VSCODE_PID", "TERM_PROGRAM", "WORKSPACE_PATH", "FCD_HOOK_IDE_TIMEOUT_MS", "FCD_TAINT_APPROVE_ONCE_DISABLED", "FCD_TAINT_APPROVAL_TIMEOUT_MS"];
|
|
15
|
+
export type CallerEnvSnapshot = Partial<Record<(typeof CALLER_ENV_KEYS)[number], string>>;
|
|
16
|
+
/** The keys above that are set in `env` — what the thin client sends. */
|
|
17
|
+
export declare function snapshotCallerEnv(env?: NodeJS.ProcessEnv): CallerEnvSnapshot;
|
|
18
|
+
/** The environment decision facts are read from: the requesting caller's inside a daemon request, else this process's. */
|
|
19
|
+
export declare function callerEnv(): NodeJS.ProcessEnv;
|
|
20
|
+
/**
|
|
21
|
+
* Run `fn` with the caller's snapshot in place of this process's values for
|
|
22
|
+
* CALLER_ENV_KEYS — a key the caller did not send is UNSET, never inherited
|
|
23
|
+
* from the daemon. No snapshot (older client) = this process's env, as before.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withCallerEnv<T>(snapshot: unknown, fn: () => T): T;
|
|
26
|
+
/**
|
|
27
|
+
* How long the IDE lets an approval-capable hook run before it kills the hook
|
|
28
|
+
* (and runs the action): Claude Code / Copilot / VS Code and Cursor: 90s.
|
|
29
|
+
* FCD_HOOK_IDE_TIMEOUT_MS narrows it for drills.
|
|
30
|
+
*/
|
|
31
|
+
export declare const IDE_HOOK_TIMEOUT_MS = 90000;
|
|
32
|
+
export declare const IDE_TIMEOUT_MARGIN_MS = 15000;
|
|
33
|
+
export declare function ideHookTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
34
|
+
/** Absolute time every wait inside a hook must end by, leaving the margin before the IDE's timeout. */
|
|
35
|
+
export declare function ideWaitCutoff(startedAt: number, ideTimeoutMs?: number): number;
|
|
36
|
+
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
37
|
+
export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a developer can answer an IDE permission prompt on this machine. False on CI runners
|
|
40
|
+
* and on ephemeral workloads (`FCD_EPHEMERAL=1`, exported by `fcd workload-protect`) — there a
|
|
41
|
+
* developer-scoped approval must go to the org queue or block, never self-resolve.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isHumanPresent(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDE_TIMEOUT_MARGIN_MS = exports.IDE_HOOK_TIMEOUT_MS = exports.CALLER_ENV_KEYS = void 0;
|
|
4
|
+
exports.snapshotCallerEnv = snapshotCallerEnv;
|
|
5
|
+
exports.callerEnv = callerEnv;
|
|
6
|
+
exports.withCallerEnv = withCallerEnv;
|
|
7
|
+
exports.ideHookTimeoutMs = ideHookTimeoutMs;
|
|
8
|
+
exports.ideWaitCutoff = ideWaitCutoff;
|
|
9
|
+
exports.isCiEnvironment = isCiEnvironment;
|
|
10
|
+
exports.isHumanPresent = isHumanPresent;
|
|
11
|
+
const async_hooks_1 = require("async_hooks");
|
|
12
|
+
/**
|
|
13
|
+
* The environment of the process that ASKED for a verdict (#1466).
|
|
14
|
+
*
|
|
15
|
+
* A hook answered by the resident daemon is evaluated inside the daemon's
|
|
16
|
+
* process, whose own environment belongs to a scheduled task — not to the IDE
|
|
17
|
+
* session that spawned the hook. Every fact the decision reads from the
|
|
18
|
+
* environment (is a human present, which CI / ephemeral markers are set, the
|
|
19
|
+
* session identity, the IDE's hook budget) must be the CALLER's. The thin
|
|
20
|
+
* client sends a snapshot of exactly these keys; the daemon evaluates the
|
|
21
|
+
* request with them in place of its own values, per request.
|
|
22
|
+
*
|
|
23
|
+
* Node built-ins only: this leaf is on the slim hook's warm-path module graph.
|
|
24
|
+
*/
|
|
25
|
+
exports.CALLER_ENV_KEYS = [
|
|
26
|
+
// Human presence (isHumanPresent / isCiEnvironment).
|
|
27
|
+
'FCD_HUMAN_PRESENT', 'FCD_EPHEMERAL', 'FULLCOURTDEFENSE_EPHEMERAL',
|
|
28
|
+
'CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'TF_BUILD', 'JENKINS_URL', 'BUILDKITE', 'CIRCLECI',
|
|
29
|
+
// Whether a dialog can be shown in the caller's session.
|
|
30
|
+
'DISPLAY', 'WAYLAND_DISPLAY', 'FCD_ASK_CHANNEL',
|
|
31
|
+
// Action identity (captureActionIdentity).
|
|
32
|
+
'FCD_SESSION_ID', 'CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'FCD_RUN_ID', 'GITHUB_RUN_ID', 'CI_PIPELINE_ID', 'FCD_WORKLOAD_INSTANCE_ID',
|
|
33
|
+
// Which client / workspace the verdict is written for.
|
|
34
|
+
'CLAUDECODE', 'CLAUDE_PROJECT_DIR', 'VSCODE_PID', 'TERM_PROGRAM', 'WORKSPACE_PATH',
|
|
35
|
+
// Human-wait budget and approve-once switches.
|
|
36
|
+
'FCD_HOOK_IDE_TIMEOUT_MS', 'FCD_TAINT_APPROVE_ONCE_DISABLED', 'FCD_TAINT_APPROVAL_TIMEOUT_MS',
|
|
37
|
+
];
|
|
38
|
+
/** The keys above that are set in `env` — what the thin client sends. */
|
|
39
|
+
function snapshotCallerEnv(env = process.env) {
|
|
40
|
+
const snapshot = {};
|
|
41
|
+
for (const key of exports.CALLER_ENV_KEYS) {
|
|
42
|
+
const value = env[key];
|
|
43
|
+
if (typeof value === 'string')
|
|
44
|
+
snapshot[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return snapshot;
|
|
47
|
+
}
|
|
48
|
+
const requestEnv = new async_hooks_1.AsyncLocalStorage();
|
|
49
|
+
/** The environment decision facts are read from: the requesting caller's inside a daemon request, else this process's. */
|
|
50
|
+
function callerEnv() {
|
|
51
|
+
return requestEnv.getStore() ?? process.env;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run `fn` with the caller's snapshot in place of this process's values for
|
|
55
|
+
* CALLER_ENV_KEYS — a key the caller did not send is UNSET, never inherited
|
|
56
|
+
* from the daemon. No snapshot (older client) = this process's env, as before.
|
|
57
|
+
*/
|
|
58
|
+
function withCallerEnv(snapshot, fn) {
|
|
59
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot))
|
|
60
|
+
return fn();
|
|
61
|
+
const env = { ...process.env };
|
|
62
|
+
for (const key of exports.CALLER_ENV_KEYS) {
|
|
63
|
+
const value = snapshot[key];
|
|
64
|
+
if (typeof value === 'string')
|
|
65
|
+
env[key] = value;
|
|
66
|
+
else
|
|
67
|
+
delete env[key];
|
|
68
|
+
}
|
|
69
|
+
return requestEnv.run(env, fn);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* How long the IDE lets an approval-capable hook run before it kills the hook
|
|
73
|
+
* (and runs the action): Claude Code / Copilot / VS Code and Cursor: 90s.
|
|
74
|
+
* FCD_HOOK_IDE_TIMEOUT_MS narrows it for drills.
|
|
75
|
+
*/
|
|
76
|
+
exports.IDE_HOOK_TIMEOUT_MS = 90_000;
|
|
77
|
+
exports.IDE_TIMEOUT_MARGIN_MS = 15_000;
|
|
78
|
+
function ideHookTimeoutMs(env = callerEnv()) {
|
|
79
|
+
const override = Number(env.FCD_HOOK_IDE_TIMEOUT_MS);
|
|
80
|
+
return override > 0 ? override : exports.IDE_HOOK_TIMEOUT_MS;
|
|
81
|
+
}
|
|
82
|
+
/** Absolute time every wait inside a hook must end by, leaving the margin before the IDE's timeout. */
|
|
83
|
+
function ideWaitCutoff(startedAt, ideTimeoutMs = ideHookTimeoutMs()) {
|
|
84
|
+
return startedAt + ideTimeoutMs - Math.min(exports.IDE_TIMEOUT_MARGIN_MS, ideTimeoutMs / 4);
|
|
85
|
+
}
|
|
86
|
+
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
87
|
+
function isCiEnvironment(env = callerEnv()) {
|
|
88
|
+
return env.GITHUB_ACTIONS === 'true'
|
|
89
|
+
|| env.GITLAB_CI === 'true'
|
|
90
|
+
|| env.TF_BUILD === 'True' // Azure Pipelines
|
|
91
|
+
|| !!env.JENKINS_URL
|
|
92
|
+
|| !!env.BUILDKITE
|
|
93
|
+
|| !!env.CIRCLECI
|
|
94
|
+
|| env.CI === 'true';
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether a developer can answer an IDE permission prompt on this machine. False on CI runners
|
|
98
|
+
* and on ephemeral workloads (`FCD_EPHEMERAL=1`, exported by `fcd workload-protect`) — there a
|
|
99
|
+
* developer-scoped approval must go to the org queue or block, never self-resolve.
|
|
100
|
+
*/
|
|
101
|
+
function isHumanPresent(env = callerEnv()) {
|
|
102
|
+
if (env.FCD_EPHEMERAL === '1' || env.FCD_EPHEMERAL === 'true' || env.FULLCOURTDEFENSE_EPHEMERAL === '1')
|
|
103
|
+
return false;
|
|
104
|
+
if (env.FCD_HUMAN_PRESENT === '0' || env.FCD_HUMAN_PRESENT === 'false')
|
|
105
|
+
return false;
|
|
106
|
+
return !isCiEnvironment(env);
|
|
107
|
+
}
|