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.
Files changed (74) hide show
  1. package/dist/actionIdentity.d.ts +1 -0
  2. package/dist/actionIdentity.js +2 -0
  3. package/dist/actionPolicyEngine.js +255 -33
  4. package/dist/agentTerminal.d.ts +1 -3
  5. package/dist/agentTerminal.js +14 -29
  6. package/dist/approvalPrompt.d.ts +7 -0
  7. package/dist/approvalPrompt.js +46 -0
  8. package/dist/askDialog.d.ts +87 -0
  9. package/dist/askDialog.js +209 -0
  10. package/dist/blockExplanation.js +18 -6
  11. package/dist/boundaryVerification.d.ts +20 -0
  12. package/dist/boundaryVerification.js +147 -0
  13. package/dist/commands/daemon.js +197 -70
  14. package/dist/commands/deterministicGuard.d.ts +2 -0
  15. package/dist/commands/deterministicGuard.js +161 -9
  16. package/dist/commands/hook.js +69 -22
  17. package/dist/commands/installClaudeHook.js +11 -5
  18. package/dist/commands/installCursorHook.js +5 -4
  19. package/dist/commands/mcpGateway.d.ts +2 -0
  20. package/dist/commands/mcpGateway.js +163 -56
  21. package/dist/commands/onboard.js +6 -5
  22. package/dist/commands/onboardingSummary.d.ts +5 -0
  23. package/dist/commands/onboardingSummary.js +11 -0
  24. package/dist/commands/protectedRun.d.ts +14 -0
  25. package/dist/commands/protectedRun.js +207 -0
  26. package/dist/containment-runtime/index.mjs +30118 -0
  27. package/dist/containment-runtime/licenses/_anthropic-ai_sandbox-runtime.txt +201 -0
  28. package/dist/containment-runtime/licenses/_pondwader_socks5-server.txt +20 -0
  29. package/dist/containment-runtime/licenses/commander.txt +22 -0
  30. package/dist/containment-runtime/licenses/node-forge.txt +331 -0
  31. package/dist/containment-runtime/licenses/zod.txt +21 -0
  32. package/dist/containment-runtime/runtime-version.json +1 -0
  33. package/dist/containment-runtime/vendor/java-proxy-agent/build.ts +95 -0
  34. package/dist/containment-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
  35. package/dist/containment-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
  36. package/dist/containment-runtime/vendor/seccomp/build.ts +69 -0
  37. package/dist/containment-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
  38. package/dist/containment-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
  39. package/dist/containment-runtime/vendor/srt-win/build.ts +21 -0
  40. package/dist/containment-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
  41. package/dist/containmentLease.d.ts +5 -0
  42. package/dist/containmentLease.js +72 -0
  43. package/dist/detectorBaseline.d.ts +2 -4
  44. package/dist/detectorBaseline.js +2 -4
  45. package/dist/devConfirm.d.ts +97 -15
  46. package/dist/devConfirm.js +447 -117
  47. package/dist/distress.d.ts +2 -0
  48. package/dist/distress.js +2 -0
  49. package/dist/gatewayRuntime.d.ts +19 -0
  50. package/dist/gatewayRuntime.js +93 -0
  51. package/dist/hookSlim.js +17 -18
  52. package/dist/index.js +27 -0
  53. package/dist/localSafetySnapshot.d.ts +9 -5
  54. package/dist/localSafetySnapshot.js +42 -15
  55. package/dist/mcpToolReview.d.ts +17 -0
  56. package/dist/mcpToolReview.js +127 -0
  57. package/dist/mcpToolTrust.d.ts +22 -0
  58. package/dist/mcpToolTrust.js +66 -0
  59. package/dist/nativeHook.d.ts +4 -0
  60. package/dist/nativeHook.js +31 -17
  61. package/dist/policyRefresh.d.ts +3 -0
  62. package/dist/policyRefresh.js +27 -0
  63. package/dist/protectionMaintenance.d.ts +10 -0
  64. package/dist/protectionMaintenance.js +64 -0
  65. package/dist/protectionProfile.d.ts +26 -0
  66. package/dist/protectionProfile.js +63 -0
  67. package/dist/runtimeConfig.d.ts +2 -0
  68. package/dist/runtimeConfig.js +65 -26
  69. package/dist/selfTest.d.ts +2 -0
  70. package/dist/selfTest.js +9 -1
  71. package/dist/sessionLimits.d.ts +2 -0
  72. package/dist/sessionLimits.js +24 -0
  73. package/dist/version.json +1 -1
  74. package/package.json +8 -5
@@ -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;
@@ -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
- if (!inline) return segment;
6994
- const code = inline[1] ?? inline[2];
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*$/i,
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
- const lead = (tokens[i2] || "").toLowerCase().replace(/^["']|["']$/g, "");
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) => !/^(?:Select-Object|Format-Table|Format-List)\s+[\w., *-]+$|^Out-Null$/i.test(part))) return false;
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*\$[A-Za-z_][\w]*\s*=\s*(?:Invoke-RestMethod|Invoke-WebRequest|irm|iwr)\s/i.test(command)) return false;
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 || /[`{}]/.test(command)) return false;
7660
+ if (segments.length > 32 || /`/.test(command)) return false;
7539
7661
  const responses = /* @__PURE__ */ new Set();
7540
7662
  let requests = 0;
7541
- for (let segment of segments) {
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) => !/^(?:ConvertTo-Json(?:\s+-Compress)?(?:\s+-Depth\s+\d{1,2})?|ConvertFrom-Json|Select-Object\s+[\w., -]+|Format-List|Format-Table|Out-Null)$/i.test(part))) return false;
7554
- if (/^(?:Invoke-RestMethod|Invoke-WebRequest|irm|iwr)\s/i.test(lead)) {
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 (binding || !responses.has(lead.slice(1).toLowerCase())) return false;
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
- return (0, import_detectorRuntime.updatedDetectorFacts)(toolName, args) || inferBundledToolContext(toolName, args);
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["toolArgs.bytes"] = String(Buffer.byteLength(argsText, "utf8"));
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 === "string") {
7766
- context[key] = val;
7767
- } else if (typeof val === "number") {
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 = (args.method || "").toUpperCase();
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
- } else {
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();
@@ -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
  /**
@@ -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
- // IDE-answer lookup keys. These MUST match devConfirm.ts: the hook raises a
88
- // pending marker keyed by sha256(`${event}|${tool}|${sha256('cmd:'+command)}`)
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(`shell|shell|${sha256(`cmd:${command.trim()}`)}`);
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
- if (!ledger || ledger.formatVersion !== 2)
110
- return false;
111
- // A pending prompt is not an affirmative answer. IDE handoffs are handled
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|shell|'+sha('cmd:'+line.trim()));if((ledger.pending||[]).some(p=>p&&p.permissionHandoff===true&&p.key===pk&&typeof p.raisedAt==='string'&&now>=Date.parse(p.raisedAt)&&now-Date.parse(p.raisedAt)<IDE_WINDOW_MS))return true;if(ledger.formatVersion!==2)return false;const rk=sha('shell|local-safety:'+ruleId+'|'+ruleId);const session=process.env.FCD_SESSION_ID||process.env.CLAUDE_CODE_SESSION_ID||process.env.CODEX_THREAD_ID;return (ledger.remembered||[]).some(r=>r&&r.key===rk&&typeof r.expiresAt==='string'&&Date.parse(r.expiresAt)>now&&(r.remember==='1h'||(r.remember==='session'&&session&&r.sessionId===session)));}catch{return false;}}`,
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
- ` $pk = Get-FcdSha256 ('shell|shell|' + (Get-FcdSha256 ('cmd:' + $Line.Trim())))`,
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 -eq $pk -and $p.raisedAt) {`,
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;