fullcourtdefense-cli 1.34.22 → 1.34.25
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.d.ts +1 -0
- package/dist/actionIdentity.js +2 -0
- package/dist/actionPolicyEngine.js +255 -33
- package/dist/agentTerminal.d.ts +1 -3
- package/dist/agentTerminal.js +14 -29
- package/dist/approvalPrompt.d.ts +7 -0
- package/dist/approvalPrompt.js +46 -0
- package/dist/askDialog.d.ts +87 -0
- package/dist/askDialog.js +209 -0
- package/dist/blockExplanation.js +18 -6
- package/dist/boundaryVerification.d.ts +20 -0
- package/dist/boundaryVerification.js +147 -0
- package/dist/commands/daemon.js +197 -70
- package/dist/commands/deterministicGuard.d.ts +2 -0
- package/dist/commands/deterministicGuard.js +161 -9
- package/dist/commands/hook.js +69 -22
- package/dist/commands/installClaudeHook.js +11 -5
- package/dist/commands/installCursorHook.js +5 -4
- package/dist/commands/mcpGateway.d.ts +2 -0
- package/dist/commands/mcpGateway.js +163 -56
- package/dist/commands/onboard.js +6 -5
- package/dist/commands/onboardingSummary.d.ts +5 -0
- package/dist/commands/onboardingSummary.js +11 -0
- package/dist/commands/protectedRun.d.ts +14 -0
- package/dist/commands/protectedRun.js +207 -0
- package/dist/containment-runtime/index.mjs +30118 -0
- package/dist/containment-runtime/licenses/_anthropic-ai_sandbox-runtime.txt +201 -0
- package/dist/containment-runtime/licenses/_pondwader_socks5-server.txt +20 -0
- package/dist/containment-runtime/licenses/commander.txt +22 -0
- package/dist/containment-runtime/licenses/node-forge.txt +331 -0
- package/dist/containment-runtime/licenses/zod.txt +21 -0
- package/dist/containment-runtime/runtime-version.json +1 -0
- package/dist/containment-runtime/vendor/java-proxy-agent/build.ts +95 -0
- package/dist/containment-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
- package/dist/containment-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/containment-runtime/vendor/seccomp/build.ts +69 -0
- package/dist/containment-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/containment-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
- package/dist/containment-runtime/vendor/srt-win/build.ts +21 -0
- package/dist/containment-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
- package/dist/containmentLease.d.ts +5 -0
- package/dist/containmentLease.js +72 -0
- package/dist/detectorBaseline.d.ts +2 -4
- package/dist/detectorBaseline.js +2 -4
- package/dist/devConfirm.d.ts +97 -15
- package/dist/devConfirm.js +447 -117
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/gatewayRuntime.d.ts +19 -0
- package/dist/gatewayRuntime.js +93 -0
- package/dist/hookSlim.js +17 -18
- package/dist/index.js +27 -0
- package/dist/localSafetySnapshot.d.ts +9 -5
- package/dist/localSafetySnapshot.js +42 -15
- package/dist/mcpToolReview.d.ts +17 -0
- package/dist/mcpToolReview.js +127 -0
- package/dist/mcpToolTrust.d.ts +22 -0
- package/dist/mcpToolTrust.js +66 -0
- package/dist/nativeHook.d.ts +4 -0
- package/dist/nativeHook.js +31 -17
- package/dist/policyRefresh.d.ts +3 -0
- package/dist/policyRefresh.js +27 -0
- package/dist/protectionMaintenance.d.ts +10 -0
- package/dist/protectionMaintenance.js +64 -0
- package/dist/protectionProfile.d.ts +26 -0
- package/dist/protectionProfile.js +63 -0
- package/dist/runtimeConfig.d.ts +2 -0
- package/dist/runtimeConfig.js +65 -26
- package/dist/selfTest.d.ts +2 -0
- package/dist/selfTest.js +9 -1
- package/dist/sessionLimits.d.ts +2 -0
- package/dist/sessionLimits.js +24 -0
- package/dist/version.json +1 -1
- package/package.json +8 -5
package/dist/actionIdentity.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface ActionIdentity {
|
|
|
3
3
|
sessionId?: string;
|
|
4
4
|
runId?: string;
|
|
5
5
|
instanceId?: string;
|
|
6
|
+
attemptId?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function identityPart(value: unknown): string | undefined;
|
|
8
9
|
export declare function captureActionIdentity(payload?: Record<string, unknown>, env?: NodeJS.ProcessEnv): ActionIdentity;
|
package/dist/actionIdentity.js
CHANGED
|
@@ -11,5 +11,7 @@ function captureActionIdentity(payload = {}, env = process.env) {
|
|
|
11
11
|
|| env.FCD_SESSION_ID || env.CLAUDE_CODE_SESSION_ID || env.CODEX_THREAD_ID),
|
|
12
12
|
runId: identityPart(env.FCD_RUN_ID || env.GITHUB_RUN_ID || env.CI_PIPELINE_ID),
|
|
13
13
|
instanceId: identityPart(env.FCD_WORKLOAD_INSTANCE_ID),
|
|
14
|
+
// Never use a generic request id: some clients reuse it for unrelated calls.
|
|
15
|
+
attemptId: identityPart(payload.tool_use_id || payload.tool_call_id),
|
|
14
16
|
};
|
|
15
17
|
}
|
|
@@ -5792,6 +5792,27 @@ function provenJavaScriptReadUrls(code) {
|
|
|
5792
5792
|
}
|
|
5793
5793
|
case "EmptyStatement":
|
|
5794
5794
|
return literal2(void 0);
|
|
5795
|
+
case "IfStatement": {
|
|
5796
|
+
ev(node.test);
|
|
5797
|
+
evaluate(node.consequent, new Map(env), depth + 1);
|
|
5798
|
+
if (node.alternate) evaluate(node.alternate, new Map(env), depth + 1);
|
|
5799
|
+
return literal2(void 0);
|
|
5800
|
+
}
|
|
5801
|
+
case "UnaryExpression": {
|
|
5802
|
+
if (node.operator !== "!") return fail();
|
|
5803
|
+
ev(node.argument);
|
|
5804
|
+
return { kind: "data" };
|
|
5805
|
+
}
|
|
5806
|
+
case "ThrowStatement":
|
|
5807
|
+
return ev(node.argument);
|
|
5808
|
+
case "TemplateLiteral": {
|
|
5809
|
+
for (const expression of node.expressions) {
|
|
5810
|
+
const value = ev(expression);
|
|
5811
|
+
if (!["literal", "data", "string"].includes(value.kind)) return fail();
|
|
5812
|
+
if (value.kind === "literal" && value.value !== null && typeof value.value === "object") return fail();
|
|
5813
|
+
}
|
|
5814
|
+
return { kind: "string" };
|
|
5815
|
+
}
|
|
5795
5816
|
case "ExpressionStatement":
|
|
5796
5817
|
case "AwaitExpression":
|
|
5797
5818
|
case "ChainExpression":
|
|
@@ -5835,6 +5856,7 @@ function provenJavaScriptReadUrls(code) {
|
|
|
5835
5856
|
case "BinaryExpression": {
|
|
5836
5857
|
const left = ev(node.left);
|
|
5837
5858
|
const right = ev(node.right);
|
|
5859
|
+
if (node.operator === "===" || node.operator === "!==") return { kind: "data" };
|
|
5838
5860
|
if (node.operator !== "+") return fail();
|
|
5839
5861
|
if (left.kind === "literal" && right.kind === "literal" && typeof left.value === "string" && typeof right.value === "string") {
|
|
5840
5862
|
if (left.value.length + right.value.length > 32768) return fail();
|
|
@@ -5870,6 +5892,12 @@ function provenJavaScriptReadUrls(code) {
|
|
|
5870
5892
|
}
|
|
5871
5893
|
case "NewExpression": {
|
|
5872
5894
|
const constructor = ev(node.callee);
|
|
5895
|
+
if (constructor.kind === "call" && constructor.name === "Error") {
|
|
5896
|
+
if (node.arguments.length !== 1) return fail();
|
|
5897
|
+
const message = ev(node.arguments[0]);
|
|
5898
|
+
if (message.kind !== "string" && !(message.kind === "literal" && typeof message.value === "string")) return fail();
|
|
5899
|
+
return { kind: "data" };
|
|
5900
|
+
}
|
|
5873
5901
|
if (constructor.kind !== "call" || constructor.name !== "GoogleAuth" || node.arguments.length !== 1) return fail();
|
|
5874
5902
|
const options = ev(node.arguments[0]);
|
|
5875
5903
|
if (options.kind !== "object" || options.fields.size !== 1) return fail();
|
|
@@ -6000,6 +6028,7 @@ function provenJavaScriptReadUrls(code) {
|
|
|
6000
6028
|
evaluate(tree, /* @__PURE__ */ new Map([
|
|
6001
6029
|
["fetch", callable("fetch")],
|
|
6002
6030
|
["require", callable("require")],
|
|
6031
|
+
["Error", callable("Error")],
|
|
6003
6032
|
["JSON", object([["stringify", callable("JSON.stringify")]])],
|
|
6004
6033
|
["console", object([["log", callable("console.log")], ["error", callable("console.error")]])]
|
|
6005
6034
|
]));
|
|
@@ -6740,14 +6769,17 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
6740
6769
|
"recipients",
|
|
6741
6770
|
"from",
|
|
6742
6771
|
"cc",
|
|
6743
|
-
"bcc"
|
|
6772
|
+
"bcc",
|
|
6773
|
+
"domain",
|
|
6774
|
+
"destinationType",
|
|
6775
|
+
"toolStatus"
|
|
6744
6776
|
]);
|
|
6745
6777
|
function buildOperationMatchText(_toolName, context) {
|
|
6746
6778
|
const parts = [];
|
|
6747
6779
|
for (const [key, value] of Object.entries(context)) {
|
|
6748
6780
|
const fieldWords = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_.-]/);
|
|
6749
6781
|
const identifierField = /^(?:id|ids|uuid|uuids|guid|guids)$/.test(fieldWords[fieldWords.length - 1]);
|
|
6750
|
-
if (OPERATION_MATCH_SKIP_FIELDS.has(key) || identifierField) continue;
|
|
6782
|
+
if (OPERATION_MATCH_SKIP_FIELDS.has(key) || identifierField || key.startsWith("args.") || key.startsWith("request.")) continue;
|
|
6751
6783
|
if (typeof value !== "string" || value.length === 0 || value.length > 2e3) continue;
|
|
6752
6784
|
const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : value;
|
|
6753
6785
|
parts.push(`${key} ${text}`);
|
|
@@ -6820,6 +6852,7 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
6820
6852
|
if (/^sha(?:1|256|384|512)-/i.test(token)) return false;
|
|
6821
6853
|
if (/^data:/i.test(token) || /^[A-Za-z0-9+/]{200,}={0,2}$/.test(token)) return false;
|
|
6822
6854
|
if (/^(?:https?|s3|gs|file):\/\//i.test(token) || /[\\/]{1}[\w.-]+[\\/]/.test(token)) return false;
|
|
6855
|
+
if (allowWordSlug && /^[A-Za-z_][\w.-]*\/[A-Za-z_][\w.-]*\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(token)) return false;
|
|
6823
6856
|
if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(token) || /^[a-z]+(?:_[a-z0-9]+)+$/.test(token)) return false;
|
|
6824
6857
|
if (allowWordSlug && /^[A-Z][a-z]+-(?:[A-Z][a-z]+){2,}$/.test(token)) return false;
|
|
6825
6858
|
if (allowWordSlug && /^[a-z]+(?:-[a-z]+)+(?:-\d+)?$/.test(token)) return false;
|
|
@@ -6990,8 +7023,15 @@ function explicitGitHubWriteRepository(command) {
|
|
|
6990
7023
|
function withoutUnattributedJavaScriptUrls(segment) {
|
|
6991
7024
|
if (segment.length > 32768) return segment;
|
|
6992
7025
|
const inline = /^\s*node(?:\.exe)?\s+(?:-e|--eval)\s+(?:"([^"$`\\]*)"|'([^']*)')\s*$/i.exec(segment);
|
|
6993
|
-
|
|
6994
|
-
|
|
7026
|
+
let code;
|
|
7027
|
+
if (inline) code = inline[1] ?? inline[2];
|
|
7028
|
+
else {
|
|
7029
|
+
const raw = segment.trim();
|
|
7030
|
+
const opening = /^@'\r?\n/.exec(raw);
|
|
7031
|
+
const end = opening ? hereStringEnd(raw, 0) : -1;
|
|
7032
|
+
if (!opening || end < 0 || !/^\s*\|\s*node(?:\.exe)?(?:\s+-)?\s*$/i.test(raw.slice(end))) return segment;
|
|
7033
|
+
code = raw.slice(opening[0].length, end - 3);
|
|
7034
|
+
}
|
|
6995
7035
|
let budget = 4e3;
|
|
6996
7036
|
try {
|
|
6997
7037
|
const tree = parse3(code, { ecmaVersion: "latest", sourceType: "script", allowAwaitOutsideFunction: true });
|
|
@@ -7053,6 +7093,8 @@ function extractUrl(args, _argsText) {
|
|
|
7053
7093
|
}
|
|
7054
7094
|
function shellUrlActionText(command) {
|
|
7055
7095
|
if (command.length > 65536) return command;
|
|
7096
|
+
if (/\$env:(?:TEMP|TMP)\s*=/i.test(maskQuotedSpans(command))) return command;
|
|
7097
|
+
const fileSyntax = (text) => text.replace(/"\$env:(?:TEMP|TMP)([\\/][A-Za-z0-9_ ./\\-]+)"/gi, '"fcd-temporary$1"').replace(/\(Join-Path\s+\$env:(?:TEMP|TMP)\s+'([A-Za-z0-9_ ./\\-]+)'\)/gi, '"fcd-temporary/$1"');
|
|
7056
7098
|
const originalSegments = splitShellSegments(command);
|
|
7057
7099
|
const localPaths = /* @__PURE__ */ new Map();
|
|
7058
7100
|
const literalValues = /* @__PURE__ */ new Map();
|
|
@@ -7095,12 +7137,12 @@ function shellUrlActionText(command) {
|
|
|
7095
7137
|
const value = localPaths.get(name.toLowerCase());
|
|
7096
7138
|
return value ? expand(match, `"${value}"`) : match;
|
|
7097
7139
|
});
|
|
7098
|
-
const expanded = prefix + tail;
|
|
7140
|
+
const expanded = prefix + fileSyntax(tail);
|
|
7099
7141
|
return expanded.replace(
|
|
7100
|
-
/^(Set-Content|Add-Content|Out-File)(\s+[\s\S]*?\s+-(?:Value|InputObject)\s+)\$([A-Za-z_][\w]*)\s
|
|
7101
|
-
(match, writer, options, name) => {
|
|
7142
|
+
/^(Set-Content|Add-Content|Out-File)(\s+[\s\S]*?\s+-(?:Value|InputObject)\s+)\$([A-Za-z_][\w]*)(\s+-[\s\S]*|\s*)$/i,
|
|
7143
|
+
(match, writer, options, name, suffix) => {
|
|
7102
7144
|
const value = literalValues.get(name.toLowerCase());
|
|
7103
|
-
return value ? expand(match, writer + options + value) : match;
|
|
7145
|
+
return value ? expand(match, writer + options + value + suffix) : match;
|
|
7104
7146
|
}
|
|
7105
7147
|
);
|
|
7106
7148
|
});
|
|
@@ -7140,6 +7182,16 @@ function shellUrlActionText(command) {
|
|
|
7140
7182
|
if (dataAssignments.has(index)) return true;
|
|
7141
7183
|
if (pathAssignments.has(index)) return true;
|
|
7142
7184
|
if (filtered[index] !== segment) return true;
|
|
7185
|
+
if (/^\$[A-Za-z_][\w]*\s*=\s*\$null$/i.test(segment)) return true;
|
|
7186
|
+
if (/^\[System\.Management\.Automation\.Language\.Parser\]::ParseFile\("[^"$`\r\n|;&<>]*",\s*\[ref\]\$[A-Za-z_][\w]*,\s*\[ref\]\$[A-Za-z_][\w]*\)\s*\|\s*Out-Null$/i.test(segment)) return true;
|
|
7187
|
+
if (/^if\s*\(\$([A-Za-z_][\w]*)\.Count\)\s*\{\s*throw\s+\$\1\[0\]\s*\}$/i.test(segment)) return true;
|
|
7188
|
+
if (/^Write-Output\s+'(?:[^']|'')*'$/i.test(segment)) return true;
|
|
7189
|
+
if (/^gh\s+pr\s+checks\s+\d+\s+--json\s+[a-z,]+\s+--jq\s+'(?:[^']|'')*'$/i.test(segment)) return true;
|
|
7190
|
+
const read = /^Get-Content\s+(?:(?:-LiteralPath|-Path)\s+)?(?:'([^'$`\r\n]*)'|"([^"$`\r\n]*)"|([^\s'"$`|;&(){}<>]+))(?:\s+-(?:Tail|TotalCount)\s+\d{1,6})?(?:\s+-Raw)?\s*$/i.exec(segment);
|
|
7191
|
+
if (read) {
|
|
7192
|
+
const target = read[1] ?? read[2] ?? read[3];
|
|
7193
|
+
if (target && !target.startsWith("-") && !/^[\\/]{2}|[\r\n*?\[\]<>]/.test(target) && !/:/.test(target.replace(/^[A-Za-z]:[\\/]/, ""))) return true;
|
|
7194
|
+
}
|
|
7143
7195
|
const directory = /^New-Item\s+-ItemType\s+Directory\s+(?:-Force\s+)?-(?:LiteralPath|Path)\s+(?:'([^']*)'|"([^"$`]*)")\s*(?:\|\s*Out-Null)?$/i.exec(segment);
|
|
7144
7196
|
if (directory && /^(?:[A-Za-z]:[\\/]|\/(?!\/))[\w ./\\-]+$/.test(directory[1] ?? directory[2])) return true;
|
|
7145
7197
|
const words = maskQuotedSpans(segment);
|
|
@@ -7384,14 +7436,22 @@ function shellSegmentLead(segment) {
|
|
|
7384
7436
|
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
7385
7437
|
let i2 = 0;
|
|
7386
7438
|
while (i2 < tokens.length && (/^[A-Za-z_][\w]*=/.test(tokens[i2]) || /^(?:sudo|time|nohup|command|builtin|exec|nice|env)$/i.test(tokens[i2]))) i2++;
|
|
7387
|
-
|
|
7439
|
+
if (i2 < tokens.length && /^\$[\w:]+$/.test(tokens[i2]) && tokens[i2 + 1] === "=") i2 += 2;
|
|
7440
|
+
else if (i2 < tokens.length && /^\$[\w:]+=$/.test(tokens[i2])) i2 += 1;
|
|
7441
|
+
else if (i2 < tokens.length && /^\$[\w:]+=./.test(tokens[i2])) tokens[i2] = tokens[i2].replace(/^\$[\w:]+=/, "");
|
|
7442
|
+
while (i2 < tokens.length && /^(?:\$|@)?\(+$/.test(tokens[i2])) i2++;
|
|
7443
|
+
const lead = (tokens[i2] || "").toLowerCase().replace(/^(?:\$|@)?\(+/, "").replace(/^["']|["']$/g, "");
|
|
7388
7444
|
return lead.replace(/^.*[\\/]/, "").replace(/\.(exe|cmd|bat|ps1)$/i, "");
|
|
7389
7445
|
}
|
|
7446
|
+
var PS_SESSION_VARIABLE_ASSIGN_RE = /^\s*\$(?:psdefaultparametervalues|psmodulepath|profile|executioncontext|env:(?:path|pathext|psmodulepath))\s*(?:\[[^\]]*\]\s*)?(?:\+?=)/i;
|
|
7447
|
+
var PS_VALUE_EXECUTOR_RE = /\b(?:invoke-expression|iex|invoke-command|icm|start-process|saps|invoke-item|ii)\b|&\s*[$(]|\.\s*invoke\s*\(/i;
|
|
7390
7448
|
var worstShellOp = (ops, floor = "read") => ops.reduce((w, op) => (SHELL_OP_RANK[op] || 0) > (SHELL_OP_RANK[w] || 0) ? op : w, floor);
|
|
7391
7449
|
function classifyShellSegment(segment, depth = 0) {
|
|
7392
7450
|
const raw = segment.trim();
|
|
7393
7451
|
if (!raw) return "read";
|
|
7452
|
+
if (PS_SESSION_VARIABLE_ASSIGN_RE.test(raw)) return "SHELL";
|
|
7394
7453
|
const lead = shellSegmentLead(segment);
|
|
7454
|
+
if ((/^\$env:[\w]+$/.test(lead) || /^\$\w+$/.test(lead)) && PS_VALUE_EXECUTOR_RE.test(maskQuotedSpans(raw))) return "SHELL";
|
|
7395
7455
|
if (SHELL_NEUTRAL_BUILTINS.has(lead) || /^\$env:[\w]+$/.test(lead) || /^\$\w+$/.test(lead)) return "read";
|
|
7396
7456
|
const rawLower = raw.toLowerCase();
|
|
7397
7457
|
const sqlClient = SHELL_SQL_CLIENTS.has(lead) || /\|\s*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlcmd|sqlite3|mongosh?|clickhouse-client|bq|cockroach|usql)\b/.test(rawLower);
|
|
@@ -7469,8 +7529,10 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7469
7529
|
if (/\b(git\s+(?:add|commit|push|merge|rebase|cherry-pick|stash|checkout|switch|restore|tag\s+\S)|npm\s+(?:install|ci|i|uninstall|update|publish)|pnpm\s+(?:install|add|i)|yarn(?:\s+add|\s+install)?|pip3?\s+install|poetry\s+(?:install|add)|cargo\s+(?:build|install)|go\s+(?:build|install|get|mod)|gcloud\s+run\s+deploy|firebase\s+deploy|terraform\s+apply|kubectl\s+apply|docker\s+(?:push|build|compose\s+up)|helm\s+(?:install|upgrade))\b/.test(value)) {
|
|
7470
7530
|
return "write";
|
|
7471
7531
|
}
|
|
7532
|
+
if (SHELL_BLOCK_STRUCTURE_RE.test(value)) return "read";
|
|
7472
7533
|
return "SHELL";
|
|
7473
7534
|
}
|
|
7535
|
+
var SHELL_BLOCK_STRUCTURE_RE = /^(?:\s|[{}()=\-\d]|\+=|""|\$[\w:.]+|\b(?:try|catch|finally|if|then|else|elseif|elif|fi|do|done|esac|end|exit|return|break|continue)\b)*$/i;
|
|
7474
7536
|
function classifyShellCommandOperation(command, depth = 0) {
|
|
7475
7537
|
if (provenPowerShellReadSequence(command)) return "read";
|
|
7476
7538
|
if (shellUrlActionText(command) !== command) return "write";
|
|
@@ -7478,6 +7540,44 @@ function classifyShellCommandOperation(command, depth = 0) {
|
|
|
7478
7540
|
if (segments.length === 0) return "SHELL";
|
|
7479
7541
|
return worstShellOp(segments.map((segment) => classifyShellSegment(segment, depth)), "read");
|
|
7480
7542
|
}
|
|
7543
|
+
function isPowerShellReadFormatter(segment) {
|
|
7544
|
+
if (/^(?:Select-Object\s+[\w., *-]+|Format-Table(?:\s+[\w., *-]+)?|Format-List(?:\s+[\w., *-]+)?|Out-Null|ConvertFrom-Json)$/i.test(segment)) return true;
|
|
7545
|
+
const tokens = segment.trim().split(/\s+/);
|
|
7546
|
+
if (tokens.shift()?.toLowerCase() !== "convertto-json") return false;
|
|
7547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7548
|
+
for (let i2 = 0; i2 < tokens.length; i2++) {
|
|
7549
|
+
const flag = tokens[i2].toLowerCase();
|
|
7550
|
+
if (seen.has(flag)) return false;
|
|
7551
|
+
seen.add(flag);
|
|
7552
|
+
if (flag === "-compress") continue;
|
|
7553
|
+
if (flag === "-depth" && /^(?:\d{1,2}|100)$/.test(tokens[++i2] || "")) continue;
|
|
7554
|
+
return false;
|
|
7555
|
+
}
|
|
7556
|
+
return true;
|
|
7557
|
+
}
|
|
7558
|
+
function isGcloudStorageCatRead(command) {
|
|
7559
|
+
if (command.length > 8192 || /[$`{}()<>;&|]/.test(command)) return false;
|
|
7560
|
+
const pieces = command.match(/"[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+/g) || [];
|
|
7561
|
+
if (pieces.join("").replace(/\s/g, "") !== command.replace(/\s/g, "")) return false;
|
|
7562
|
+
const tokens = pieces.map((piece) => piece.replace(/^["']|["']$/g, ""));
|
|
7563
|
+
if (!/^gcloud(?:\.cmd|\.exe)?$/i.test(tokens.shift() || "") || tokens.shift() !== "storage" || tokens.shift() !== "cat" || tokens.length > 32) return false;
|
|
7564
|
+
let objects = 0;
|
|
7565
|
+
for (let i2 = 0; i2 < tokens.length; i2++) {
|
|
7566
|
+
const token = tokens[i2];
|
|
7567
|
+
if (/^gs:\/\/[a-z0-9][a-z0-9._-]*\/[A-Za-z0-9._~/*?-]+$/.test(token)) {
|
|
7568
|
+
objects++;
|
|
7569
|
+
continue;
|
|
7570
|
+
}
|
|
7571
|
+
if (token === "--display-url" || token === "-d") continue;
|
|
7572
|
+
if ((token === "--range" || token === "-r") && /^(?:\d+-\d*|-\d+)$/.test(tokens[i2 + 1] || "")) {
|
|
7573
|
+
i2++;
|
|
7574
|
+
continue;
|
|
7575
|
+
}
|
|
7576
|
+
if (/^--range=(?:\d+-\d*|-\d+)$/.test(token)) continue;
|
|
7577
|
+
return false;
|
|
7578
|
+
}
|
|
7579
|
+
return objects > 0;
|
|
7580
|
+
}
|
|
7481
7581
|
function provenCompoundDownload(command, detectedUrl) {
|
|
7482
7582
|
if (command.includes(detectedUrl) && provenPowerShellReadSequence(command)) return true;
|
|
7483
7583
|
const segments = splitShellSegments(command).filter((segment) => /\b(?:https?|s3|gs|ftp|sftp|smb):\/\//i.test(segment));
|
|
@@ -7487,8 +7587,9 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
7487
7587
|
if (statusRead) segment = statusRead[1];
|
|
7488
7588
|
if (/[$`{}()<>;&]/.test(segment)) return false;
|
|
7489
7589
|
const pipeline = splitShellSegments(segment, true);
|
|
7490
|
-
if (pipeline.slice(1).some((part) =>
|
|
7590
|
+
if (pipeline.slice(1).some((part) => !isPowerShellReadFormatter(part))) return false;
|
|
7491
7591
|
const raw = pipeline[0];
|
|
7592
|
+
if (isGcloudStorageCatRead(raw)) return true;
|
|
7492
7593
|
const pieces = raw.match(/"[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+/g) || [];
|
|
7493
7594
|
if (pieces.join("").replace(/\s/g, "") !== raw.replace(/\s/g, "")) return false;
|
|
7494
7595
|
const tokens = pieces.map((piece) => piece.replace(/^["']|["']$/g, ""));
|
|
@@ -7532,13 +7633,41 @@ function provenCompoundDownload(command, detectedUrl) {
|
|
|
7532
7633
|
return urls === 1;
|
|
7533
7634
|
});
|
|
7534
7635
|
}
|
|
7636
|
+
function isPowerShellClockDisplay(value) {
|
|
7637
|
+
return /^\(\s*Get-Date\s*\)\s*\.\s*(?:ToUniversalTime\s*\(\s*\)\s*\.\s*)?ToString\s*\(\s*(?:'[^'\r\n]{0,80}'|"[^"$`\r\n]{0,80}")\s*\)$/i.test(value);
|
|
7638
|
+
}
|
|
7639
|
+
function isPowerShellResponseProjection(segment, responses) {
|
|
7640
|
+
const object = /^\[pscustomobject\]\s*@\{([^{}]+)\}\s*(?:\|\s*(.+))?$/i.exec(segment);
|
|
7641
|
+
if (!object) return false;
|
|
7642
|
+
if (object[2] && object[2].split("|").some((part) => !isPowerShellReadFormatter(part.trim()))) return false;
|
|
7643
|
+
const fields = object[1].trim().replace(/;\s*$/, "").split(";");
|
|
7644
|
+
if (fields.length > 32) return false;
|
|
7645
|
+
const labels = /* @__PURE__ */ new Set();
|
|
7646
|
+
return fields.every((field) => {
|
|
7647
|
+
const entry = /^\s*([A-Za-z_][\w]*)\s*=\s*(.+?)\s*$/s.exec(field);
|
|
7648
|
+
if (!entry) return false;
|
|
7649
|
+
const label = entry[1].toLowerCase();
|
|
7650
|
+
if (labels.has(label)) return false;
|
|
7651
|
+
labels.add(label);
|
|
7652
|
+
if (isPowerShellClockDisplay(entry[2])) return true;
|
|
7653
|
+
const member = /^(\[int\]\s*)?\$([A-Za-z_][\w]*)\.(StatusCode|StatusDescription|RawContentLength|Content)$/i.exec(entry[2]);
|
|
7654
|
+
return Boolean(member && responses.has(member[2].toLowerCase()) && (!member[1] || /^(?:StatusCode|RawContentLength)$/i.test(member[3])));
|
|
7655
|
+
});
|
|
7656
|
+
}
|
|
7535
7657
|
function provenPowerShellReadSequence(command) {
|
|
7536
|
-
if (command.length > 65536 || !/(?:^|[;&\n])\s
|
|
7658
|
+
if (command.length > 65536 || !/(?:^|[;&\n])\s*(?:\$[A-Za-z_][\w]*\s*=\s*(?:Invoke-RestMethod|Invoke-WebRequest|irm|iwr)\s|(?:\$[A-Za-z_][\w]*\s*=\s*)?gcloud(?:\.cmd|\.exe)?\s+storage\s+cat\s)/i.test(command)) return false;
|
|
7537
7659
|
const segments = splitShellSegments(command);
|
|
7538
|
-
if (segments.length > 32 ||
|
|
7660
|
+
if (segments.length > 32 || /`/.test(command)) return false;
|
|
7539
7661
|
const responses = /* @__PURE__ */ new Set();
|
|
7540
7662
|
let requests = 0;
|
|
7541
|
-
for (let
|
|
7663
|
+
for (let index = 0; index < segments.length; index++) {
|
|
7664
|
+
let segment = segments[index];
|
|
7665
|
+
if (/^\[pscustomobject\]\s*@\{/i.test(segment)) {
|
|
7666
|
+
while (!segment.includes("}") && index + 1 < segments.length) segment += ";" + segments[++index];
|
|
7667
|
+
if (!isPowerShellResponseProjection(segment, responses)) return false;
|
|
7668
|
+
continue;
|
|
7669
|
+
}
|
|
7670
|
+
if (/[{}]/.test(maskQuotedSpans(segment))) return false;
|
|
7542
7671
|
let binding;
|
|
7543
7672
|
const assignment = /^\$([A-Za-z_][\w]*)\s*=\s*(.+)$/s.exec(segment);
|
|
7544
7673
|
if (assignment) {
|
|
@@ -7550,15 +7679,18 @@ function provenPowerShellReadSequence(command) {
|
|
|
7550
7679
|
if (property) segment = property[1];
|
|
7551
7680
|
const pipeline = splitShellSegments(segment, true);
|
|
7552
7681
|
const lead = pipeline.shift() || "";
|
|
7553
|
-
if (pipeline.some((part) =>
|
|
7554
|
-
if (
|
|
7682
|
+
if (pipeline.some((part) => !isPowerShellReadFormatter(part))) return false;
|
|
7683
|
+
if (isGcloudStorageCatRead(lead)) {
|
|
7684
|
+
requests++;
|
|
7685
|
+
} else if (/^(?:Invoke-RestMethod|Invoke-WebRequest|irm|iwr)\s/i.test(lead)) {
|
|
7555
7686
|
const url = lead.match(/https?:\/\/[^\s"'<>]+/i)?.[0];
|
|
7556
7687
|
if (!url || !provenCompoundDownload(lead, url)) return false;
|
|
7557
7688
|
requests++;
|
|
7558
|
-
} else if (/^\$[A-Za-z_][\w]*$/.test(lead)) {
|
|
7559
|
-
if (
|
|
7689
|
+
} else if (/^\$[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*$/.test(lead)) {
|
|
7690
|
+
if (!responses.has(lead.slice(1).split(".")[0].toLowerCase())) return false;
|
|
7560
7691
|
} else {
|
|
7561
7692
|
if (binding) return false;
|
|
7693
|
+
if (/^gh\s+(?:run|pr)\s+view\s+\d+\s+--json\s+(?:[A-Za-z][A-Za-z,]*|'[A-Za-z][A-Za-z, ]*'|"[A-Za-z][A-Za-z, ]*")(?:\s+--jq\s+'[^'$`\r\n]{1,2000}')?$/i.test(lead) || /^npm\s+view\s+(?:@[\w.-]+\/)?[\w.-]+\s+version$/i.test(lead) || /^Get-ScheduledTask\s+-TaskName\s+(?:[\w.-]+|'[^'$`\r\n]+'|"[^"$`\r\n]+")$/i.test(lead)) continue;
|
|
7562
7694
|
if (pipeline.length === 0 && /^(?:git\s+status(?:\s+--short)?|cd\s+(?:[A-Za-z]:[\\/][\w./\\-]+|'[A-Za-z]:[\\/][^'$`\r\n]+'))$/i.test(lead)) continue;
|
|
7563
7695
|
const file = /^Get-Content\s+(?:'([^'\r\n]+)'|"([^"$`\r\n]+)")((?:\s+-(?:Raw|Tail\s+\d+|TotalCount\s+\d+))*)$/i.exec(lead);
|
|
7564
7696
|
if (!file || !/^[A-Za-z]:[\\/]/.test(file[1] || file[2]) || /[?*]/.test(file[1] || file[2])) return false;
|
|
@@ -7740,7 +7872,33 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
|
|
|
7740
7872
|
return worstResult;
|
|
7741
7873
|
}
|
|
7742
7874
|
function inferToolContext(toolName, args) {
|
|
7743
|
-
|
|
7875
|
+
const detectorArgs = {};
|
|
7876
|
+
const untrustedInput = /* @__PURE__ */ Object.create(null);
|
|
7877
|
+
for (const [key, value] of Object.entries(args)) {
|
|
7878
|
+
if (reservedInputFact(key) || LEGACY_TRUST_LABEL_ARGS.has(key)) untrustedInput[key] = value;
|
|
7879
|
+
else detectorArgs[key] = value;
|
|
7880
|
+
}
|
|
7881
|
+
if (Object.keys(untrustedInput).length) {
|
|
7882
|
+
let carrier = "__fcd_untrustedInput";
|
|
7883
|
+
while (Object.hasOwn(detectorArgs, carrier)) carrier += "_";
|
|
7884
|
+
detectorArgs[carrier] = untrustedInput;
|
|
7885
|
+
}
|
|
7886
|
+
const updated = (0, import_detectorRuntime.updatedDetectorFacts)(toolName, detectorArgs);
|
|
7887
|
+
if (!updated) return inferBundledToolContext(toolName, args);
|
|
7888
|
+
const context = { ...updated.context };
|
|
7889
|
+
for (const key of RESERVED_INPUT_FACTS) delete context[key];
|
|
7890
|
+
const argsText = stringifyArgs(args);
|
|
7891
|
+
context["toolArgs.bytes"] = String(Buffer.byteLength(argsText, "utf8"));
|
|
7892
|
+
for (const [key, value] of Object.entries(args)) {
|
|
7893
|
+
if (!["string", "number", "boolean"].includes(typeof value)) continue;
|
|
7894
|
+
context[`args.${key}`] = String(value);
|
|
7895
|
+
if (!reservedInputFact(key) && LEGACY_TRUST_LABEL_ARGS.has(key) && !Object.hasOwn(context, key)) context[key] = String(value);
|
|
7896
|
+
}
|
|
7897
|
+
if (!Object.hasOwn(context, "request.containsSecret") && recordHttpRequestFacts(context, args, extractUrl(args, argsText))) {
|
|
7898
|
+
context["destination.type"] = context["request.destination.type"];
|
|
7899
|
+
context["destination.domain"] = context["request.destination.domain"];
|
|
7900
|
+
}
|
|
7901
|
+
return { operation: updated.operation, context };
|
|
7744
7902
|
}
|
|
7745
7903
|
function inferBundledToolContext(toolName, args) {
|
|
7746
7904
|
const context = {};
|
|
@@ -7754,19 +7912,13 @@ function inferBundledToolContext(toolName, args) {
|
|
|
7754
7912
|
Number.isFinite(retryCount) ? retryCount : 0,
|
|
7755
7913
|
Number.isFinite(fanoutCount) ? fanoutCount : 0
|
|
7756
7914
|
);
|
|
7757
|
-
context
|
|
7758
|
-
const secretKind = detectSecretKind(argsText);
|
|
7759
|
-
context["toolArgs.containsSecret"] = secretKind !== "none" ? "true" : "false";
|
|
7760
|
-
context["toolArgs.secretKind"] = secretKind;
|
|
7761
|
-
context["toolArgs.containsPii"] = hasPiiLikeValue(argsText) ? "true" : "false";
|
|
7915
|
+
Object.assign(context, inputContentFacts(argsText));
|
|
7762
7916
|
context["retry.count"] = String(burstCount);
|
|
7763
7917
|
context["fanout.count"] = String(burstCount);
|
|
7764
7918
|
for (const [key, val] of Object.entries(args)) {
|
|
7765
|
-
if (typeof val
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
context[key] = String(val);
|
|
7769
|
-
}
|
|
7919
|
+
if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") continue;
|
|
7920
|
+
context[`args.${key}`] = String(val);
|
|
7921
|
+
if (!reservedInputFact(key)) context[key] = String(val);
|
|
7770
7922
|
}
|
|
7771
7923
|
const nameWords = toolNameLower.replace(/[_\-.]/g, " ");
|
|
7772
7924
|
const isSearchTool = /\b(search|grep|find|lookup|locate)\b/.test(nameWords) && !/\b(delete|remove|drop|write|update|insert|replace|create)\b/.test(nameWords);
|
|
@@ -7799,7 +7951,7 @@ function inferBundledToolContext(toolName, args) {
|
|
|
7799
7951
|
else if (writeOps.some((op) => toolNameLower.includes(op))) operation = "write";
|
|
7800
7952
|
else if (deleteOps.some((op) => toolNameLower.includes(op))) operation = "delete";
|
|
7801
7953
|
}
|
|
7802
|
-
const methodArg =
|
|
7954
|
+
const methodArg = typeof args.method === "string" ? args.method.toUpperCase() : "";
|
|
7803
7955
|
if (["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(methodArg)) {
|
|
7804
7956
|
operation = methodArg;
|
|
7805
7957
|
if (args.url) context.url = args.url;
|
|
@@ -7814,20 +7966,19 @@ function inferBundledToolContext(toolName, args) {
|
|
|
7814
7966
|
const inbound = operation === "read" || ["GET", "HEAD", "OPTIONS", "SELECT", "SHOW", "DESCRIBE", "EXPLAIN"].includes(operation) || isCodeExecTool && !hasSecretLikeValue(argsText) && provenCompoundDownload(commandText, detectedUrl) || provenReadUrls.includes(detectedUrl) || operation === toolName && /\b(fetch|get|read|search|list|lookup|browse|navigate|download|clone|pull|view|open|crawl|scrape)\b/.test(nameWords) && !/\b(post|put|send|upload|push|write|submit|publish|export|create|update|delete|patch)\b/.test(nameWords);
|
|
7815
7967
|
context["destination.domain"] = parsed.hostname;
|
|
7816
7968
|
context["url.risk"] = hostType;
|
|
7969
|
+
const requestHasSecret = recordHttpRequestFacts(context, args, detectedUrl);
|
|
7817
7970
|
if (inbound) {
|
|
7818
7971
|
context["source.domain"] = parsed.hostname;
|
|
7819
7972
|
context["source.type"] = hostType;
|
|
7820
|
-
}
|
|
7973
|
+
}
|
|
7974
|
+
if (!inbound || requestHasSecret) {
|
|
7821
7975
|
context["destination.type"] = hostType;
|
|
7822
7976
|
}
|
|
7823
7977
|
} catch {
|
|
7824
7978
|
context["url.risk"] = "invalid";
|
|
7825
7979
|
}
|
|
7826
7980
|
}
|
|
7827
|
-
if (typeof args.destinationType === "string") context["destination.type"] = args.destinationType;
|
|
7828
7981
|
if (typeof args.destination === "string") context.destination = args.destination;
|
|
7829
|
-
if (typeof args.domain === "string") context["destination.domain"] = args.domain;
|
|
7830
|
-
if (typeof args.toolStatus === "string") context["tool.status"] = args.toolStatus;
|
|
7831
7982
|
if (args.to || args.recipient || args.email) {
|
|
7832
7983
|
context.to = args.to || args.recipient || args.email;
|
|
7833
7984
|
context["destination.type"] = context["destination.type"] || "external";
|
|
@@ -7850,6 +8001,77 @@ function inferBundledToolContext(toolName, args) {
|
|
|
7850
8001
|
}
|
|
7851
8002
|
return { operation, context };
|
|
7852
8003
|
}
|
|
8004
|
+
var RESERVED_INPUT_FACTS = /* @__PURE__ */ new Set([
|
|
8005
|
+
"__proto__",
|
|
8006
|
+
"constructor",
|
|
8007
|
+
"prototype",
|
|
8008
|
+
"verdict",
|
|
8009
|
+
"decision",
|
|
8010
|
+
"approval",
|
|
8011
|
+
"toolName",
|
|
8012
|
+
"agentName",
|
|
8013
|
+
"agentClient",
|
|
8014
|
+
"developerName",
|
|
8015
|
+
"machineName",
|
|
8016
|
+
"machineKind",
|
|
8017
|
+
"machineId",
|
|
8018
|
+
"humanPresent",
|
|
8019
|
+
"organizationId",
|
|
8020
|
+
"tenantId",
|
|
8021
|
+
"shieldId",
|
|
8022
|
+
"userId",
|
|
8023
|
+
"ipAddress",
|
|
8024
|
+
"gateway",
|
|
8025
|
+
"mcpServer"
|
|
8026
|
+
]);
|
|
8027
|
+
var RESERVED_FACT_NAMESPACES = /* @__PURE__ */ new Set([
|
|
8028
|
+
"args",
|
|
8029
|
+
"toolArgs",
|
|
8030
|
+
"tool",
|
|
8031
|
+
"agent",
|
|
8032
|
+
"action",
|
|
8033
|
+
"operation",
|
|
8034
|
+
"url",
|
|
8035
|
+
"destination",
|
|
8036
|
+
"source",
|
|
8037
|
+
"request",
|
|
8038
|
+
"retry",
|
|
8039
|
+
"fanout",
|
|
8040
|
+
"runtime",
|
|
8041
|
+
"machine",
|
|
8042
|
+
"session",
|
|
8043
|
+
"policy",
|
|
8044
|
+
"tenant",
|
|
8045
|
+
"organization",
|
|
8046
|
+
"role",
|
|
8047
|
+
"roles"
|
|
8048
|
+
]);
|
|
8049
|
+
function reservedInputFact(key) {
|
|
8050
|
+
return RESERVED_INPUT_FACTS.has(key) || key.includes(".") && RESERVED_FACT_NAMESPACES.has(key.split(".")[0]);
|
|
8051
|
+
}
|
|
8052
|
+
var LEGACY_TRUST_LABEL_ARGS = /* @__PURE__ */ new Set(["destinationType", "domain", "toolStatus"]);
|
|
8053
|
+
function inputContentFacts(argsText) {
|
|
8054
|
+
const secretKind = detectSecretKind(argsText);
|
|
8055
|
+
return {
|
|
8056
|
+
"toolArgs.bytes": String(Buffer.byteLength(argsText, "utf8")),
|
|
8057
|
+
"toolArgs.containsSecret": secretKind !== "none" ? "true" : "false",
|
|
8058
|
+
"toolArgs.secretKind": secretKind,
|
|
8059
|
+
"toolArgs.containsPii": hasPiiLikeValue(argsText) ? "true" : "false"
|
|
8060
|
+
};
|
|
8061
|
+
}
|
|
8062
|
+
function recordHttpRequestFacts(context, args, detectedUrl) {
|
|
8063
|
+
try {
|
|
8064
|
+
const parsed = new URL(detectedUrl);
|
|
8065
|
+
if (!["http:", "https:"].includes(parsed.protocol)) return false;
|
|
8066
|
+
const containsSecret = hasSecretLikeValue(stringifyArgs({ url: detectedUrl, headers: args.headers, body: args.body, data: args.data }));
|
|
8067
|
+
context["request.destination.domain"] = parsed.hostname;
|
|
8068
|
+
context["request.destination.type"] = isInternalHost(parsed.hostname) ? "internal" : "external";
|
|
8069
|
+
context["request.containsSecret"] = containsSecret ? "true" : "false";
|
|
8070
|
+
return containsSecret;
|
|
8071
|
+
} catch {
|
|
8072
|
+
return false;
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
7853
8075
|
function inferInventoryToolOperations(toolName, toolActions = []) {
|
|
7854
8076
|
const ops = /* @__PURE__ */ new Set([toolName, toolName.toUpperCase(), ...toolActions]);
|
|
7855
8077
|
const upper = toolName.toUpperCase();
|
package/dist/agentTerminal.d.ts
CHANGED
|
@@ -14,9 +14,7 @@ export declare const AGENT_TERMINAL_ASK_BLOCKED: string;
|
|
|
14
14
|
/** Shown when the IDE prompt already answered for this exact command. */
|
|
15
15
|
export declare const AGENT_TERMINAL_IDE_CONFIRMED = "allowed through the IDE permission gate; confirmation is recorded by the IDE post-execution hook.";
|
|
16
16
|
/** Pending-marker key the IDE hook wrote for this shell command (see devConfirm.pendingKey). */
|
|
17
|
-
export declare function ideShellPendingKey(command: string): string;
|
|
18
|
-
/** Remembered-confirmation key for a Local Safety ask rule on the shell tool (see devConfirm rememberKey). */
|
|
19
|
-
export declare function ideLocalSafetyRememberKey(ruleId: string): string;
|
|
17
|
+
export declare function ideShellPendingKey(command: string, cwd?: string): string;
|
|
20
18
|
/** Maximum lifetime of an exact-command IDE permission handoff (not consent). */
|
|
21
19
|
export declare const IDE_CONFIRMATION_WINDOW_MS: number;
|
|
22
20
|
/**
|
package/dist/agentTerminal.js
CHANGED
|
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.IDE_CONFIRMATION_WINDOW_MS = exports.AGENT_TERMINAL_IDE_CONFIRMED = exports.AGENT_TERMINAL_ASK_BLOCKED = exports.AGENT_TERMINAL_MARKERS = void 0;
|
|
37
37
|
exports.isAgentDrivenTerminal = isAgentDrivenTerminal;
|
|
38
38
|
exports.ideShellPendingKey = ideShellPendingKey;
|
|
39
|
-
exports.ideLocalSafetyRememberKey = ideLocalSafetyRememberKey;
|
|
40
39
|
exports.ideConfirmedFor = ideConfirmedFor;
|
|
41
40
|
exports.buildAgentTerminalJs = buildAgentTerminalJs;
|
|
42
41
|
exports.buildAgentTerminalPs = buildAgentTerminalPs;
|
|
@@ -84,19 +83,13 @@ exports.AGENT_TERMINAL_ASK_BLOCKED = 'an AI agent is driving this terminal, so n
|
|
|
84
83
|
/** Shown when the IDE prompt already answered for this exact command. */
|
|
85
84
|
exports.AGENT_TERMINAL_IDE_CONFIRMED = 'allowed through the IDE permission gate; confirmation is recorded by the IDE post-execution hook.';
|
|
86
85
|
// ---------------------------------------------------------------------------
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// with event 'shell' and tool 'shell', and a remembered entry keyed by
|
|
90
|
-
// sha256(`shell|local-safety:${ruleId}|${ruleId}`) for Local Safety ask rules.
|
|
86
|
+
// Exact command + working directory handoff keys match devConfirm v3.
|
|
87
|
+
// A handoff delegates to the IDE permission gate; it is never consent.
|
|
91
88
|
// ---------------------------------------------------------------------------
|
|
92
89
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
93
90
|
/** Pending-marker key the IDE hook wrote for this shell command (see devConfirm.pendingKey). */
|
|
94
|
-
function ideShellPendingKey(command) {
|
|
95
|
-
return sha256(
|
|
96
|
-
}
|
|
97
|
-
/** Remembered-confirmation key for a Local Safety ask rule on the shell tool (see devConfirm rememberKey). */
|
|
98
|
-
function ideLocalSafetyRememberKey(ruleId) {
|
|
99
|
-
return sha256(`shell|local-safety:${ruleId}|${ruleId}`);
|
|
91
|
+
function ideShellPendingKey(command, cwd = process.cwd()) {
|
|
92
|
+
return sha256(JSON.stringify(['shell', 'shell', sha256(`args-v3:${JSON.stringify({ command, cwd })}`)]));
|
|
100
93
|
}
|
|
101
94
|
/** Maximum lifetime of an exact-command IDE permission handoff (not consent). */
|
|
102
95
|
exports.IDE_CONFIRMATION_WINDOW_MS = 10 * 60_000;
|
|
@@ -106,12 +99,9 @@ exports.IDE_CONFIRMATION_WINDOW_MS = 10 * 60_000;
|
|
|
106
99
|
* by the generated guard code below.
|
|
107
100
|
*/
|
|
108
101
|
function ideConfirmedFor(ledger, command, ruleId, now = Date.now(), sessionId) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// separately by the generated guard, without claiming developer consent.
|
|
113
|
-
const rememberKey = ideLocalSafetyRememberKey(ruleId);
|
|
114
|
-
return (ledger.remembered || []).some(r => r?.key === rememberKey && typeof r.expiresAt === 'string' && Date.parse(r.expiresAt) > now && (r.remember === '1h' || (r.remember === 'session' && !!sessionId && r.sessionId === sessionId)));
|
|
102
|
+
// Terminal guards lack the hook's complete policy/context snapshot. They
|
|
103
|
+
// cannot reconstruct a scoped approval and must never infer one from a rule.
|
|
104
|
+
return false;
|
|
115
105
|
}
|
|
116
106
|
// ---------------------------------------------------------------------------
|
|
117
107
|
// Rendered snippets for the generated guards (single source of truth).
|
|
@@ -129,7 +119,7 @@ function buildAgentTerminalJs() {
|
|
|
129
119
|
`function agentTerminal(){const e=process.env;return AGENT_MARKERS.some(m=>m.v!==undefined?e[m.n]===m.v:(typeof e[m.n]==='string'&&e[m.n].length>0));}`,
|
|
130
120
|
`const IDE_WINDOW_MS=${exports.IDE_CONFIRMATION_WINDOW_MS};`,
|
|
131
121
|
`function sha(s){return crypto.createHash('sha256').update(s).digest('hex');}`,
|
|
132
|
-
`function ideConfirmed(line,ruleId){try{const ledger=JSON.parse(fs.readFileSync(path.join(os.homedir(),'.fullcourtdefense','dev-confirm.json'),'utf8'));const now=Date.now();const pk=sha('shell
|
|
122
|
+
`function ideConfirmed(line,ruleId){try{const ledger=JSON.parse(fs.readFileSync(path.join(os.homedir(),'.fullcourtdefense','dev-confirm.json'),'utf8'));if(ledger.formatVersion!==3)return false;const now=Date.now();const pk=sha(JSON.stringify(['shell','shell',sha('args-v3:'+JSON.stringify({command:line,cwd:process.cwd()}))]));const session=process.env.FCD_SESSION_ID||process.env.CLAUDE_CODE_SESSION_ID||process.env.CODEX_THREAD_ID;if(!session)return false;return (ledger.pending||[]).some(p=>p&&p.permissionHandoff===true&&p.sessionId===session&&p.key===pk&&typeof p.raisedAt==='string'&&now>=Date.parse(p.raisedAt)&&now-Date.parse(p.raisedAt)<IDE_WINDOW_MS);}catch{return false;}}`,
|
|
133
123
|
`const AGENT_ASK_BLOCKED=${JSON.stringify(exports.AGENT_TERMINAL_ASK_BLOCKED)};`,
|
|
134
124
|
`const AGENT_IDE_CONFIRMED=${JSON.stringify(exports.AGENT_TERMINAL_IDE_CONFIRMED)};`,
|
|
135
125
|
].join('\n');
|
|
@@ -165,24 +155,19 @@ function buildAgentTerminalPs() {
|
|
|
165
155
|
` $ledgerPath = Join-Path $HOME '.fullcourtdefense\\dev-confirm.json'`,
|
|
166
156
|
` if (-not (Test-Path -LiteralPath $ledgerPath)) { return $false }`,
|
|
167
157
|
` $ledger = Get-Content -LiteralPath $ledgerPath -Raw | ConvertFrom-Json`,
|
|
158
|
+
` if ($ledger.formatVersion -ne 3) { return $false }`,
|
|
159
|
+
` $session = $env:FCD_SESSION_ID; if (-not $session) { $session = $env:CLAUDE_CODE_SESSION_ID }; if (-not $session) { $session = $env:CODEX_THREAD_ID }; if (-not $session) { return $false }`,
|
|
168
160
|
` $now = [DateTimeOffset]::UtcNow`,
|
|
169
|
-
` $
|
|
161
|
+
` $argsJson = ConvertTo-Json -Compress -InputObject ([ordered]@{command=$Line;cwd=(Get-Location).ProviderPath})`,
|
|
162
|
+
` $digest = Get-FcdSha256 ('args-v3:' + $argsJson)`,
|
|
163
|
+
` $pk = Get-FcdSha256 (ConvertTo-Json -Compress -InputObject @('shell','shell',$digest))`,
|
|
170
164
|
` foreach ($p in @($ledger.pending)) {`,
|
|
171
|
-
` if ($null -ne $p -and $p.permissionHandoff -eq $true -and $p.key -
|
|
165
|
+
` if ($null -ne $p -and $p.permissionHandoff -eq $true -and $p.sessionId -ceq $session -and $p.key -ceq $pk -and $p.raisedAt) {`,
|
|
172
166
|
` $raised = if ($p.raisedAt -is [DateTime]) { [DateTimeOffset]$p.raisedAt } else { [DateTimeOffset]::Parse([string]$p.raisedAt, [Globalization.CultureInfo]::InvariantCulture) }`,
|
|
173
167
|
` $age = $now - $raised`,
|
|
174
168
|
` if ($age.TotalMilliseconds -ge 0 -and $age.TotalMilliseconds -lt ${exports.IDE_CONFIRMATION_WINDOW_MS}) { return $true }`,
|
|
175
169
|
` }`,
|
|
176
170
|
` }`,
|
|
177
|
-
` if ($ledger.formatVersion -ne 2) { return $false }`,
|
|
178
|
-
` $rk = Get-FcdSha256 ('shell|local-safety:' + $RuleId + '|' + $RuleId)`,
|
|
179
|
-
` $session = $env:FCD_SESSION_ID; if (-not $session) { $session = $env:CLAUDE_CODE_SESSION_ID }; if (-not $session) { $session = $env:CODEX_THREAD_ID }`,
|
|
180
|
-
` foreach ($r in @($ledger.remembered)) {`,
|
|
181
|
-
` if ($null -ne $r -and $r.key -eq $rk -and $r.expiresAt -and ($r.remember -eq '1h' -or ($r.remember -eq 'session' -and $session -and $r.sessionId -eq $session))) {`,
|
|
182
|
-
` $expiry = if ($r.expiresAt -is [DateTime]) { [DateTimeOffset]$r.expiresAt } else { [DateTimeOffset]::Parse([string]$r.expiresAt, [Globalization.CultureInfo]::InvariantCulture) }`,
|
|
183
|
-
` if ($expiry -gt $now) { return $true }`,
|
|
184
|
-
` }`,
|
|
185
|
-
` }`,
|
|
186
171
|
` } catch { }`,
|
|
187
172
|
` return $false`,
|
|
188
173
|
`}`,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ApprovalMessageOptions {
|
|
2
|
+
/** The dialog offers "Always allow"; the footer must say what that covers. */
|
|
3
|
+
allowAlways?: boolean;
|
|
4
|
+
/** Human wording of the grant scope, e.g. "`gh` commands (write) to github.com". */
|
|
5
|
+
alwaysScopeLabel?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function nativeApprovalMessage(reason: string, options?: ApprovalMessageOptions): string;
|