fullcourtdefense-cli 1.36.0 → 1.36.2

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 (63) hide show
  1. package/dist/actionPolicyEngine.d.ts +23 -0
  2. package/dist/actionPolicyEngine.js +58 -19
  3. package/dist/approvalPrompt.d.ts +15 -0
  4. package/dist/approvalPrompt.js +157 -20
  5. package/dist/askDialog.d.ts +2 -0
  6. package/dist/askDialog.js +15 -1
  7. package/dist/askHeader.d.ts +81 -0
  8. package/dist/askHeader.js +573 -0
  9. package/dist/blockExplanation.d.ts +9 -0
  10. package/dist/blockExplanation.js +14 -0
  11. package/dist/callerEnv.d.ts +1 -1
  12. package/dist/callerEnv.js +14 -0
  13. package/dist/cloudGrant.d.ts +46 -0
  14. package/dist/cloudGrant.js +420 -0
  15. package/dist/cloudGrantTools.d.ts +97 -0
  16. package/dist/cloudGrantTools.js +1112 -0
  17. package/dist/commandNarrative.d.ts +8 -0
  18. package/dist/commandNarrative.js +222 -49
  19. package/dist/commands/cmdGuard.js +1 -1
  20. package/dist/commands/daemon.js +14 -0
  21. package/dist/commands/deterministicGuard.d.ts +8 -2
  22. package/dist/commands/deterministicGuard.js +219 -55
  23. package/dist/commands/discover.js +37 -14
  24. package/dist/commands/discoverAgentFiles.d.ts +2 -0
  25. package/dist/commands/discoverAgentFiles.js +1 -0
  26. package/dist/commands/discoverPaths.d.ts +4 -0
  27. package/dist/commands/discoverPaths.js +19 -1
  28. package/dist/commands/discoverProjectRoots.d.ts +72 -0
  29. package/dist/commands/discoverProjectRoots.js +327 -0
  30. package/dist/commands/discoverProxy.d.ts +1 -1
  31. package/dist/commands/discoverProxy.js +1 -1
  32. package/dist/commands/discoverSecrets.d.ts +44 -0
  33. package/dist/commands/discoverSecrets.js +158 -14
  34. package/dist/commands/hook.js +10 -7
  35. package/dist/commands/mcpGateway.js +9 -7
  36. package/dist/commands/posixShellGuard.js +1 -1
  37. package/dist/commands/shellGuard.js +70 -25
  38. package/dist/commands/uninstallAll.d.ts +1 -7
  39. package/dist/commands/uninstallAll.js +6 -45
  40. package/dist/config.d.ts +1 -1
  41. package/dist/config.js +3 -3
  42. package/dist/credentialGrant.d.ts +48 -0
  43. package/dist/credentialGrant.js +29 -18
  44. package/dist/credentialStore.js +1 -1
  45. package/dist/devConfirm.d.ts +44 -3
  46. package/dist/devConfirm.js +166 -30
  47. package/dist/distress.d.ts +2 -0
  48. package/dist/distress.js +2 -0
  49. package/dist/grantTargets.d.ts +21 -0
  50. package/dist/grantTargets.js +235 -0
  51. package/dist/localSafetySnapshot.js +2 -0
  52. package/dist/msiProductCode.d.ts +10 -0
  53. package/dist/msiProductCode.js +124 -0
  54. package/dist/notify.js +1 -0
  55. package/dist/selfTest.d.ts +1 -1
  56. package/dist/selfTest.js +1 -1
  57. package/dist/selfUpdate.d.ts +51 -2
  58. package/dist/selfUpdate.js +217 -19
  59. package/dist/shellAst.d.ts +2 -0
  60. package/dist/shellAst.js +7 -1
  61. package/dist/shellAstLoader.js +12 -1
  62. package/dist/version.json +1 -1
  63. package/package.json +6 -1
@@ -235,6 +235,29 @@ export declare function canonicalAgentClient(value: string): string;
235
235
  */
236
236
  export type SecretKind = 'github_token' | 'aws_access_key' | 'google_api_key' | 'openai_key' | 'slack_token' | 'jwt' | 'private_key' | 'assignment' | 'high_entropy' | 'none';
237
237
  export declare function detectSecretKind(raw: string): SecretKind;
238
+ /**
239
+ * gcloud read verbs. Token printers (`print-access-token`, `print-identity-token`)
240
+ * are credential dumps and are deliberately absent; `cat` streams object bytes
241
+ * and keeps its own stricter proof (isGcloudStorageCatRead). `versions` is a
242
+ * GROUP (`app versions`, `secrets versions`), never a verb: as a verb it made
243
+ * `gcloud app versions delete` a read. `access` is `secrets versions access`.
244
+ */
245
+ export declare const GCLOUD_READ_VERB_RE: RegExp;
246
+ /** gcloud verbs that change a cloud resource. Deletes are handled apart (DELETE). */
247
+ export declare const GCLOUD_WRITE_VERB_RE: RegExp;
248
+ /** gcloud verbs that remove a resource or its data (`kms keys versions destroy`, `secrets versions destroy`). */
249
+ export declare const GCLOUD_DELETE_VERB_RE: RegExp;
250
+ /**
251
+ * `gcloud [--global-flag …] [alpha|beta] <group…> <verb> …` on the masked,
252
+ * lowercased command words: the first token after the program that is not a
253
+ * flag and not a lowercase group word that names a further group. gcloud
254
+ * groups and verbs are both lowercase words, so the walk returns the FIRST
255
+ * word that is a known read/write/delete verb; a value, a redirect or an
256
+ * unknown word before any verb ends the walk with `null` (segment stays
257
+ * unknown). Nothing here reads the resource operand — `gs://…`, a service
258
+ * name, a secret name are what the verb acts ON, not where data goes.
259
+ */
260
+ export declare function gcloudVerbWord(value: string): string | null;
238
261
  export type ShellAstMode = 'off' | 'shadow' | 'primary';
239
262
  /** Structural subset of shellAst.ts ShellFacts that the engine reads. */
240
263
  export interface ShellAstFactsLike {
@@ -24,6 +24,9 @@ __export(actionPolicyEngine_exports, {
24
24
  ACTION_POLICY_GRANT_MAX_USES: () => ACTION_POLICY_GRANT_MAX_USES,
25
25
  CONTENT_FACT_FIELDS: () => CONTENT_FACT_FIELDS,
26
26
  DEFAULT_ACTION_POLICY_APPROVAL: () => DEFAULT_ACTION_POLICY_APPROVAL,
27
+ GCLOUD_DELETE_VERB_RE: () => GCLOUD_DELETE_VERB_RE,
28
+ GCLOUD_READ_VERB_RE: () => GCLOUD_READ_VERB_RE,
29
+ GCLOUD_WRITE_VERB_RE: () => GCLOUD_WRITE_VERB_RE,
27
30
  MACHINE_KINDS: () => MACHINE_KINDS,
28
31
  WORKSTATION_ASK_APPROVAL: () => WORKSTATION_ASK_APPROVAL,
29
32
  approvalExceptionError: () => approvalExceptionError,
@@ -32,6 +35,7 @@ __export(actionPolicyEngine_exports, {
32
35
  detectSecretKind: () => detectSecretKind,
33
36
  effectiveApprovalScope: () => effectiveApprovalScope,
34
37
  evaluateActionPolicies: () => evaluateActionPolicies,
38
+ gcloudVerbWord: () => gcloudVerbWord,
35
39
  getShellAstMode: () => getShellAstMode,
36
40
  inferBundledToolContext: () => inferBundledToolContext,
37
41
  inferToolContext: () => inferToolContext,
@@ -6822,6 +6826,7 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
6822
6826
  // its operation/capability; the words a person typed into a title are not a verb.
6823
6827
  "title",
6824
6828
  "subject",
6829
+ "topic",
6825
6830
  "description",
6826
6831
  "message",
6827
6832
  "msg",
@@ -6903,25 +6908,30 @@ function buildOperationMatchText(_toolName, context) {
6903
6908
  const identifierField = /^(?:id|ids|uuid|uuids|guid|guids)$/.test(fieldWords[fieldWords.length - 1]);
6904
6909
  if (OPERATION_MATCH_SKIP_FIELDS.has(key) || identifierField || key.startsWith("args.") || key.startsWith("request.")) continue;
6905
6910
  if (typeof value !== "string" || value.length === 0 || value.length > 2e3) continue;
6906
- const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : value;
6911
+ const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : SQL_STATEMENT_FIELDS.has(key) && !SQL_EXECUTES_STRING_RE.test(value) ? maskQuotedSpans(value) : value;
6907
6912
  parts.push(`${key} ${text}`);
6908
6913
  }
6909
6914
  return parts.join(" ");
6910
6915
  }
6911
6916
  var COMMAND_LINE_FIELDS = /* @__PURE__ */ new Set(["command", "cmd", "commandLine", "script", "args"]);
6917
+ var SQL_STATEMENT_FIELDS = /* @__PURE__ */ new Set(["query", "sql", "statement"]);
6918
+ var SQL_EXECUTES_STRING_RE = /\b(?:exec(?:ute)?|sp_executesql|prepare)\b/i;
6912
6919
  var CODE_EXEC_TOOL_NAME_RE = /\b(node ?repl|repl|shell|exec|execute|terminal|command|cmd|run ?code|bash|zsh|sh|powershell|pwsh)\b/i;
6913
6920
  function shellVerbWords(command) {
6914
6921
  let lead = "";
6915
6922
  let first = true;
6916
6923
  let scriptArgs = false;
6917
6924
  let previousBare = "";
6918
- return shellCommandWords(command).split(/\s+/).filter(Boolean).map((token) => {
6919
- if (token === ";") {
6925
+ let previousToken = "";
6926
+ return shellCommandWords(command).replace(/\|/g, " | ").split(/\s+/).filter(Boolean).map((token) => {
6927
+ const afterToken = previousToken;
6928
+ previousToken = token;
6929
+ if (token === ";" || token === "|") {
6920
6930
  lead = "";
6921
6931
  first = true;
6922
6932
  scriptArgs = false;
6923
6933
  previousBare = "";
6924
- return token;
6934
+ return token === "|" ? "" : token;
6925
6935
  }
6926
6936
  if (scriptArgs) return "";
6927
6937
  const flag = /^(-{1,2})([A-Za-z][A-Za-z0-9_-]*)(?:=.*)?$/.exec(token);
@@ -6933,6 +6943,14 @@ function shellVerbWords(command) {
6933
6943
  return "";
6934
6944
  }
6935
6945
  if (!flag && !lead) lead = word.toLowerCase();
6946
+ else if (!flag && PACKAGE_EXECUTORS.has(lead) && previousBare === lead) lead = word.toLowerCase();
6947
+ if (!flag && !leading && SEARCH_PROGRAMS.has(lead)) return "";
6948
+ if (!flag && !leading && TEST_RUNNERS.has(lead)) return "";
6949
+ if (!flag && PACKAGE_RUNNERS.has(lead) && (previousBare === "run" || previousBare === "run-script")) {
6950
+ scriptArgs = true;
6951
+ return "";
6952
+ }
6953
+ if (!flag && VCS_PROGRAMS.has(lead) && /^(?:-[a-z]*m|--message)$/.test(afterToken)) return "";
6936
6954
  if (flag) {
6937
6955
  if (VCS_PROGRAMS.has(lead)) return "";
6938
6956
  const segs = word.split(/[-_]+/).filter(Boolean);
@@ -6952,6 +6970,10 @@ function shellVerbWords(command) {
6952
6970
  }).filter(Boolean).join(" ");
6953
6971
  }
6954
6972
  var VCS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "hg", "svn", "glab"]);
6973
+ var SEARCH_PROGRAMS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "zgrep", "rg", "ag", "ack", "findstr", "select-string", "sls"]);
6974
+ var TEST_RUNNERS = /* @__PURE__ */ new Set(["pytest", "jest", "vitest", "mocha"]);
6975
+ var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
6976
+ var PACKAGE_EXECUTORS = /* @__PURE__ */ new Set(["npx", "bunx", "pnpx"]);
6955
6977
  var GH_CONFIG_RESOURCES = /* @__PURE__ */ new Set(["secret", "variable", "cache", "alias", "label", "ssh-key", "gpg-key"]);
6956
6978
  var SCRIPT_RUNNER_LEADS = /* @__PURE__ */ new Set([
6957
6979
  "bash",
@@ -7058,7 +7080,7 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
7058
7080
  return shannonEntropy(token) >= 4;
7059
7081
  }
7060
7082
  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)`;
7061
- var SECRET_ASSIGNMENT_RE = new RegExp(String.raw`(?:^|[^\w])[\w.-]*?${SECRET_NAME}[\\"']*\s*(?:=>|[:=])\s*[\\"']*([^\s\\"',;&|)}\]<>]{8,})`, "gi");
7083
+ var SECRET_ASSIGNMENT_RE = new RegExp(String.raw`${SECRET_NAME}[\\"']*\s*(?:=>|[:=])\s*[\\"']*([^\s\\"',;&|)}\]<>]{8,})`, "gi");
7062
7084
  var SECRET_FLAG_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)[\\"']*([^\s\\"',;&|)}\]<>-][^\s\\"',;&|)}\]<>]{7,})/gi;
7063
7085
  var SECRET_FLAG_QUOTED_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)\\?(["'])([^\s"'\\-][^\s"'\\]{7,})\\?\1/gi;
7064
7086
  function isWordPhraseWithCounters(value) {
@@ -7144,7 +7166,7 @@ var AUTH_HEADER_RE = /\b(?:bearer|basic|token|apikey|x-api-key)\s+([A-Za-z0-9._~
7144
7166
  function hasSecretLikeValue(value) {
7145
7167
  return detectSecretKind(value) !== "none";
7146
7168
  }
7147
- var PII_EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
7169
+ var PII_EMAIL_RE = /(?<![A-Z0-9._%+-])[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
7148
7170
  var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]+|[\w.-]*\.gserviceaccount\.com|[\w.-]*\.svc\.id\.goog)$|^(?:no-?reply|noreply|do-?not-?reply|test\d*|example|user\d*|admin|root|foo|bar|baz|someone|somebody|you|me|name|first\.last|john\.doe|jane\.doe|email)@)/i;
7149
7171
  var PII_PHONE_RE = /(?:^|[^\w.@/:-])(\+?\(?\d{1,4}\)?[\s-]\(?\d{2,4}\)?[\s-]\d{2,4}(?:[\s-]\d{2,4}){0,2})(?![\w.@/:-])/g;
7150
7172
  function hasPiiLikeValue(raw) {
@@ -8090,7 +8112,7 @@ function classifyShellWords(masked, raw, lead, cwdScoped = false) {
8090
8112
  if (redirect && !/^\/dev\/null$|^nul$|^\$null$/i.test(redirect[1])) return "write";
8091
8113
  return "read";
8092
8114
  }
8093
- if (gcloudVerb === "delete") return "DELETE";
8115
+ if (GCLOUD_DELETE_VERB_RE.test(gcloudVerb)) return "DELETE";
8094
8116
  if (GCLOUD_WRITE_VERB_RE.test(gcloudVerb)) return pipesIntoInterpreter ? "SHELL" : "write";
8095
8117
  }
8096
8118
  if (/^(?:kubectl\s+(?:get|describe|logs|top|config\s+(?:current-context|get-contexts|view(?!\s+--raw)))|docker\s+(?:ps|images|logs|inspect|version|info)|terraform\s+(?:plan|validate|fmt|show|state\s+list|output)|aws\s+(?:s3\s+ls|sts\s+get-caller-identity)|az\s+account\s+show|npm\s+(?:ls|list|view|outdated|--version)|node\s+--version|python3?\s+--version)\b/.test(value)) return "read";
@@ -8126,15 +8148,16 @@ function classifyShellWords(masked, raw, lead, cwdScoped = false) {
8126
8148
  if (SHELL_BLOCK_STRUCTURE_RE.test(value)) return "read";
8127
8149
  return "SHELL";
8128
8150
  }
8129
- var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|versions)$/;
8130
- var GCLOUD_WRITE_VERB_RE = /^(?:create|update|patch|replace|enable|disable|deploy|apply|import|export|start|stop|restart|resume|suspend|scale|promote|migrate|rollback|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
8151
+ var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|access)$/;
8152
+ var GCLOUD_WRITE_VERB_RE = /^(?:create|update|patch|replace|enable|disable|deploy|apply|import|export|start|stop|restart|resume|suspend|scale|promote|migrate|rollback|add|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
8153
+ var GCLOUD_DELETE_VERB_RE = /^(?:delete|destroy)$/;
8131
8154
  function gcloudVerbWord(value) {
8132
8155
  const tokens = value.split("|")[0].trim().split(/\s+/);
8133
8156
  if (!/^gcloud(?:\.cmd|\.exe)?$/.test(tokens.shift() || "")) return null;
8134
8157
  let depth = 0;
8135
8158
  for (const token of tokens) {
8136
8159
  if (token.startsWith("-")) continue;
8137
- if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || token === "delete") return token;
8160
+ if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || GCLOUD_DELETE_VERB_RE.test(token)) return token;
8138
8161
  if (!/^[a-z][a-z0-9-]*$/.test(token) || ++depth > 6) return null;
8139
8162
  }
8140
8163
  return null;
@@ -8315,7 +8338,7 @@ function shellCommandTextOf(toolName, args) {
8315
8338
  return text || void 0;
8316
8339
  }
8317
8340
  function isPowerShellReadFormatter(segment) {
8318
- if (/^(?:Select-Object\s+[\w., *-]+|Format-Table(?:\s+[\w., *-]+)?|Format-List(?:\s+[\w., *-]+)?|Out-Null|ConvertFrom-Json)$/i.test(segment)) return true;
8341
+ if (/^(?:Select-Object\s+[\w., *-]+|Format-Table(?:\s+[\w., *-]+)?|Format-List(?:\s+[\w., *-]+)?|Out-Null|Out-String(?:\s+-Stream)?|ConvertFrom-Json)$/i.test(segment)) return true;
8319
8342
  const tokens = segment.trim().split(/\s+/);
8320
8343
  if (tokens.shift()?.toLowerCase() !== "convertto-json") return false;
8321
8344
  const seen = /* @__PURE__ */ new Set();
@@ -8384,10 +8407,11 @@ function provenCompoundDownload(command, detectedUrl) {
8384
8407
  return segments.every((segment) => {
8385
8408
  const statusRead = /^\(((?:Invoke-WebRequest|iwr)\s+[^\r\n]+)\)\.(?:StatusCode|StatusDescription|RawContentLength)$/i.exec(segment);
8386
8409
  if (statusRead) segment = statusRead[1];
8410
+ segment = segment.replace(/\s+(?:[12&]?>\s*(?:\/dev\/null|\$null|nul)|2>&1)(?=\s|$)/gi, "");
8387
8411
  const withoutWriteOut = segment.replace(/(\s(?:-w|--write-out)\s+)(?:"[^"\r\n$`@]*"|'[^'\r\n$`@]*')/g, '$1"fcd-write-out"');
8388
8412
  if (/[$`{}()<>;&]/.test(withoutWriteOut)) return false;
8389
8413
  const pipeline = splitShellSegments(segment, true);
8390
- if (pipeline.slice(1).some((part) => !isPowerShellReadFormatter(part))) return false;
8414
+ if (pipeline.slice(1).some((part) => !isPowerShellReadFormatter(part) && !(classifyShellSegment(part) === "read" && !/[a-z][a-z0-9+.-]*:\/\//i.test(part)))) return false;
8391
8415
  const raw = pipeline[0];
8392
8416
  if (isGcloudStorageCatRead(raw)) return true;
8393
8417
  const pieces = raw.match(/"[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+/g) || [];
@@ -8431,6 +8455,13 @@ function provenCompoundDownload(command, detectedUrl) {
8431
8455
  continue;
8432
8456
  }
8433
8457
  if (/^-[sfSLI]+$|^--(?:silent|fail|location|head)$|^-UseBasicParsing$/.test(token)) continue;
8458
+ if (!isCurl && /^wget/i.test(program)) {
8459
+ if (/^-q$|^--quiet$|^-q?O-$|^--output-document=-$/.test(token)) continue;
8460
+ if (/^-q?O$|^--output-document$/.test(token) && tokens[i2 + 1] === "-") {
8461
+ i2++;
8462
+ continue;
8463
+ }
8464
+ }
8434
8465
  if (/^-TimeoutSec$/i.test(token) && /^\d+$/.test(tokens[i2 + 1] || "")) {
8435
8466
  i2++;
8436
8467
  continue;
@@ -8765,6 +8796,7 @@ function applyWebReadFacts(toolName, args, context) {
8765
8796
  if (isLoopbackHost(parsed.hostname)) context["destination.loopback"] = "true";
8766
8797
  else delete context["destination.loopback"];
8767
8798
  }
8799
+ var SEARCH_PATTERN_FIELDS = ["query", "q", "pattern", "search", "searchQuery", "search_query", "term", "keyword", "keywords", "regex"];
8768
8800
  function inferBundledToolContext(toolName, args) {
8769
8801
  const context = {};
8770
8802
  let operation = toolName;
@@ -8786,16 +8818,19 @@ function inferBundledToolContext(toolName, args) {
8786
8818
  if (!reservedInputFact(key)) context[key] = String(val);
8787
8819
  }
8788
8820
  const nameWords = toolNameLower.replace(/[_\-.]/g, " ");
8789
- const isSearchTool = /\b(search|grep|find|lookup|locate)\b/.test(nameWords) && !/\b(delete|remove|drop|write|update|insert|replace|create)\b/.test(nameWords);
8790
- if (isSearchTool) {
8791
- operation = "read";
8792
- if (typeof context.query === "string") {
8793
- context["search.pattern"] = context.query;
8794
- delete context.query;
8821
+ const notMutating = !/\b(delete|remove|drop|write|update|insert|replace|create)\b/.test(nameWords);
8822
+ const isSearchTool = /\b(search|grep|find|lookup|locate)\b/.test(nameWords) && notMutating;
8823
+ const isFilterTool = isSearchTool || /\blist\b/.test(nameWords) && notMutating;
8824
+ if (isSearchTool) operation = "read";
8825
+ if (isFilterTool) {
8826
+ const patternKeys = SEARCH_PATTERN_FIELDS.filter((key) => typeof context[key] === "string");
8827
+ if (patternKeys.length) {
8828
+ context["search.pattern"] = patternKeys.map((key) => context[key]).join(" ");
8829
+ for (const key of patternKeys) delete context[key];
8795
8830
  }
8796
8831
  }
8797
8832
  const isCodeExecTool = operation === toolName && CODE_EXEC_TOOL_NAME_RE.test(nameWords);
8798
- const sqlArg = args.query || args.sql || args.statement || (isCodeExecTool ? "" : args.command || args.code || args.script || args.input) || "";
8833
+ const sqlArg = (isFilterTool ? "" : args.query) || args.sql || args.statement || (isCodeExecTool ? "" : args.command || args.code || args.script || args.input) || "";
8799
8834
  if (!isSearchTool && typeof sqlArg === "string" && sqlArg.length > 0) {
8800
8835
  const sqlMatch = sqlArg.match(/(?<!-)\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|SHOW|DESCRIBE|EXPLAIN)\b(?!-)/i);
8801
8836
  if (sqlMatch) {
@@ -8977,6 +9012,9 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
8977
9012
  ACTION_POLICY_GRANT_MAX_USES,
8978
9013
  CONTENT_FACT_FIELDS,
8979
9014
  DEFAULT_ACTION_POLICY_APPROVAL,
9015
+ GCLOUD_DELETE_VERB_RE,
9016
+ GCLOUD_READ_VERB_RE,
9017
+ GCLOUD_WRITE_VERB_RE,
8980
9018
  MACHINE_KINDS,
8981
9019
  WORKSTATION_ASK_APPROVAL,
8982
9020
  approvalExceptionError,
@@ -8985,6 +9023,7 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
8985
9023
  detectSecretKind,
8986
9024
  effectiveApprovalScope,
8987
9025
  evaluateActionPolicies,
9026
+ gcloudVerbWord,
8988
9027
  getShellAstMode,
8989
9028
  inferBundledToolContext,
8990
9029
  inferToolContext,
@@ -1,3 +1,5 @@
1
+ import { type AskHeader, type AskSubject } from './askHeader';
2
+ export type { AskHeader, AskSubject } from './askHeader';
1
3
  export interface ApprovalMessageOptions {
2
4
  /** The dialog offers "Always allow"; the footer must say what that covers. */
3
5
  allowAlways?: boolean;
@@ -5,9 +7,20 @@ export interface ApprovalMessageOptions {
5
7
  alwaysScopeLabel?: string;
6
8
  /** How long it holds: "30 days" (default) or "7 days or 20 uses". */
7
9
  alwaysLimitsLabel?: string;
10
+ /** What / How / Where the grant covers (compact rows). Absent: the scope label is the What. */
11
+ alwaysCovers?: AlwaysCovers;
12
+ /** Two-button ask: one short line saying why "Always allow" is not offered. */
13
+ alwaysNotOffered?: string;
14
+ /** Facts from the ask envelope (grant scope, rule id) for the "Why you're asked" header. */
15
+ subject?: AskSubject;
8
16
  /** Visual variant. Ask is the interactive dialog; approval/block share the card, not extra buttons. */
9
17
  tone?: DialogTone;
10
18
  }
19
+ export interface AlwaysCovers {
20
+ what: string;
21
+ how?: string;
22
+ where?: string;
23
+ }
11
24
  /** Which policy outcome this window is showing. Icons and copy follow this; buttons do not. */
12
25
  export type DialogTone = 'ask' | 'approval' | 'block';
13
26
  export interface NativeDialogView {
@@ -15,6 +28,8 @@ export interface NativeDialogView {
15
28
  badge: string;
16
29
  headline: string;
17
30
  subtitle: string;
31
+ /** Ask dialogs: why it popped up and the facts behind it, above everything else. */
32
+ header?: AskHeader;
18
33
  kind?: string;
19
34
  /** The action: the whole script when the explanation carried it, else the short "What" line. */
20
35
  action?: string;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nativeDialogView = nativeDialogView;
4
4
  exports.nativeApprovalTitle = nativeApprovalTitle;
5
5
  exports.nativeApprovalMessage = nativeApprovalMessage;
6
+ const askHeader_1 = require("./askHeader");
6
7
  /** Compact OS prompt. The complete decision remains in the IDE response and audit. */
7
8
  function line(value, limit) {
8
9
  const text = value.replace(/\s+/g, ' ').trim();
@@ -12,6 +13,23 @@ function line(value, limit) {
12
13
  const head = Math.ceil((limit - 3) * 0.6);
13
14
  return `${text.slice(0, head)} … ${text.slice(-(limit - head - 3))}`;
14
15
  }
16
+ /** `What: gcloud · How: change · Where: project acme-staging · 30 days, this machine`. */
17
+ function alwaysCoversParts(options) {
18
+ const covers = options.alwaysCovers || (options.alwaysScopeLabel ? { what: options.alwaysScopeLabel } : undefined);
19
+ if (!options.allowAlways || !covers)
20
+ return undefined;
21
+ return [
22
+ `What: ${line(covers.what, 90)}`,
23
+ covers.how ? `How: ${line(covers.how, 60)}` : '',
24
+ covers.where ? `Where: ${line(covers.where, 180)}` : '',
25
+ `${options.alwaysLimitsLabel || '30 days'}, this machine`,
26
+ ].filter(Boolean).join(' · ');
27
+ }
28
+ /** `Always allow covers — What: gcloud · How: change · Where: project acme-staging · 30 days, this machine`. */
29
+ function alwaysCoversLine(options) {
30
+ const parts = alwaysCoversParts(options);
31
+ return parts ? `Always allow covers — ${parts}` : undefined;
32
+ }
15
33
  /** The full script is shown in a scrolling box; this only guards against a runaway payload. */
16
34
  const MAX_DIALOG_SCRIPT = 6000;
17
35
  const CEILING_NOTE_RE = /\s*This policy is set to Block[.;]\s*(?:Because it is not marked "Hard block", your workstation requires your approval before continuing\.|on a developer workstation it asks you instead\.)?\s*(?:An admin can (?:review the matching rule and its scope in the console|mark it "Hard block" in the console to stop this without asking)\.)?/;
@@ -19,13 +37,72 @@ const CEILING_NOTE_SHORT = 'set to Block; on your workstation it asks you first.
19
37
  /** Parse the `Rule:` line the block explanation writes into the name the console lists and its scope. */
20
38
  function ruleParts(rule) {
21
39
  const policy = /^Action Policy "(.+?)" \((.+?)\)(?: — rule: (.+))?$/.exec(rule);
22
- if (policy)
23
- return { policy: policy[1], scope: [policy[2].replace(/^[^\w]+/, ''), policy[3]].filter(Boolean).join(' · ') };
40
+ if (policy) {
41
+ const scopeWords = policy[2].replace(/^[^\w]+/, '');
42
+ return { policy: policy[1], scopeWords, ...(policy[3] ? { matched: policy[3] } : {}), scope: [scopeWords, policy[3]].filter(Boolean).join(' · ') };
43
+ }
24
44
  const local = /^(built-in Local Safety rule|org custom rule) "(.+?)"(?: in Local Safety)?(?: \((.+)\))?$/.exec(rule);
25
45
  if (local)
26
- return { policy: local[2], localSafety: true, scope: [local[1] === 'org custom rule' ? 'Local Safety — org custom rule' : 'Local Safety — built-in rule', local[3]].filter(Boolean).join(' · ') };
46
+ return { policy: local[2], localSafety: true, customRule: local[1] === 'org custom rule', ...(local[3] ? { ruleDetail: local[3] } : {}), scope: [local[1] === 'org custom rule' ? 'Local Safety — org custom rule' : 'Local Safety — built-in rule', local[3]].filter(Boolean).join(' · ') };
27
47
  return { policy: rule };
28
48
  }
49
+ /** `toolArgs.command matches gcloud *` → "gcloud commands"; unknown shapes → undefined (the IDE message keeps the raw rule). */
50
+ function constraintWords(constraint) {
51
+ const m = /^(?:toolArgs\.)?([\w.]+) (equals|matches|contains|startsWith|in|notEquals|not_contains) (.+)$/.exec(constraint.trim());
52
+ if (!m)
53
+ return undefined;
54
+ const [, field, op, raw] = m;
55
+ const value = raw.trim();
56
+ if (field === 'command' || field === 'cmd') {
57
+ const lead = value.replace(/\s*\*+\s*$/, '').trim();
58
+ if ((op === 'matches' || op === 'startsWith') && /^[\w.-]+(?: [\w.-]+)?$/.test(lead))
59
+ return `${lead} commands`;
60
+ if (op === 'contains')
61
+ return `commands that contain "${value}"`;
62
+ return undefined;
63
+ }
64
+ if (field === 'destination.type' && op === 'equals')
65
+ return `actions that reach ${value === 'external' ? 'an external' : `a ${value}`} destination`;
66
+ if ((field === 'destination.domain' || field === 'destination.host') && op === 'equals')
67
+ return `actions that reach ${value}`;
68
+ if (field === 'agentClient' && op === 'equals')
69
+ return `actions by ${value}`;
70
+ return undefined;
71
+ }
72
+ const VERDICT_WORDS = { require_approval: 'asks for approval', block: 'blocks', allow: 'allows', log: 'records', warn: 'warns' };
73
+ const OPERATION_WORDS = { SHELL: 'shell commands', EXECUTE: 'commands', DELETE: 'deletes', write: 'writes', read: 'reads', delete: 'deletes' };
74
+ /**
75
+ * The rule that asked, in customer words — never engine syntax:
76
+ * `Company policy "Cloud changes need OK" asks for approval on gcloud commands, for every machine.`
77
+ * The raw matcher (`SHELL → require_approval (toolArgs.command matches gcloud *)`) stays in
78
+ * the IDE message's `Rule:` line.
79
+ */
80
+ function ruleSentence(f, tone) {
81
+ if (!f.policy || f.fallback)
82
+ return undefined;
83
+ const name = `"${line(f.policy, 80)}"`;
84
+ if (f.localSafety) {
85
+ const category = (f.ruleDetail || '').split(',').map(s => s.trim()).find(s => s && !/^id /.test(s));
86
+ const kind = `${f.customRule ? "Your organization's" : 'Built-in'} Local Safety rule ${name}${category ? ` (${category.replace(/_/g, ' ')})` : ''}`;
87
+ return `${kind} ${tone === 'block' ? 'blocks this' : tone === 'approval' ? 'needs approval for this' : 'asks before this runs'}.`;
88
+ }
89
+ const owner = /^company default/i.test(f.scopeWords || '') ? 'Company policy' : /^local rule/i.test(f.scopeWords || '') ? 'Local rule' : 'Policy';
90
+ const whom = /applies to (.+)$/i.exec(f.scopeWords || '')?.[1] || /only (.+)$/i.exec(f.scopeWords || '')?.[1];
91
+ const m = /^(\S+) → (\w+)(?: \((.*)\))?$/.exec(f.matched || '');
92
+ const verb = m ? VERDICT_WORDS[m[2]] || 'asks' : 'asks';
93
+ const on = m && m[3] ? m[3].split(/,\s*(?=[\w.]+ (?:equals|matches|contains|startsWith|in|notEquals|not_contains) )/).map(constraintWords) : [];
94
+ const what = on.length && on.every(Boolean) ? on.join(' and ')
95
+ : m && !m[3] ? unique(m[1].split('/').map(op => OPERATION_WORDS[op]).filter(Boolean)).join(' and ') || undefined
96
+ : undefined;
97
+ return `${owner} ${name} ${verb}${what ? ` on ${what}` : ''}${whom ? `, for ${whom}` : ''}.`;
98
+ }
99
+ /** The header sentence already states the fact the rule fired on (cloud, credential, a local removal). */
100
+ function saysTheFact(header) {
101
+ return header.source === 'cloud' || header.source === 'credential' || (header.source === 'operation' && Boolean(header.affects));
102
+ }
103
+ function unique(values) {
104
+ return [...new Set(values)];
105
+ }
29
106
  function promptFacts(reason) {
30
107
  const rows = reason.split(/\r?\n/).map(row => row.trim()).filter(Boolean);
31
108
  const field = (name) => rows.find(row => row.startsWith(`${name}:`))?.slice(name.length + 1).trim();
@@ -61,8 +138,10 @@ function promptFacts(reason) {
61
138
  facts.kind = kind?.[2];
62
139
  facts.action = kind ? kind[1] : what;
63
140
  }
64
- if (tool)
141
+ if (tool) {
65
142
  facts.tool = [tool, evidence].filter(Boolean).join(' · ');
143
+ facts.toolName = tool;
144
+ }
66
145
  if (why) {
67
146
  const stripped = why.replace(CEILING_NOTE_RE, ' ').replace(/\s+/g, ' ').trim();
68
147
  if (stripped !== why.trim())
@@ -74,6 +153,24 @@ function promptFacts(reason) {
74
153
  function sentences(text) {
75
154
  return text.split(/(?<=\.)\s+/).map(part => part.trim()).filter(Boolean);
76
155
  }
156
+ /** The full action text: the script when the explanation carried it, else the "What" line. */
157
+ function actionText(f) {
158
+ return f.script ? f.script.replace(/ ⏎ /g, '\n') : f.action;
159
+ }
160
+ function askHeaderFor(f, options) {
161
+ return (0, askHeader_1.buildAskHeader)({
162
+ command: actionText(f),
163
+ kind: f.kind,
164
+ tool: f.toolName,
165
+ policy: f.policy,
166
+ ruleDetail: f.ruleDetail,
167
+ ide: f.ide,
168
+ subject: options.subject,
169
+ alwaysOffered: Boolean(options.allowAlways),
170
+ alwaysCovers: alwaysCoversParts(options),
171
+ alwaysNotOffered: options.alwaysNotOffered,
172
+ });
173
+ }
77
174
  /**
78
175
  * Layout model for the branded OS dialog. Same facts as the compact prompt;
79
176
  * skips mockup-only chrome (other-outcome chips, Request approval, project tags).
@@ -81,8 +178,19 @@ function sentences(text) {
81
178
  function nativeDialogView(reason, options = {}) {
82
179
  const f = promptFacts(reason);
83
180
  const tone = options.tone || 'ask';
181
+ // Free-form text carries no facts to put in the header.
182
+ const header = tone === 'ask' && !f.fallback ? askHeaderFor(f, options) : undefined;
183
+ const because = f.because ? sentences(line(f.because, 400)) : [];
184
+ // The header already says the fact a cloud / credential rule fired on; the rule line only names the rule.
185
+ if (header && saysTheFact(header))
186
+ because.shift();
187
+ // The rule by name, its source and who it applies to — in customer words; the engine
188
+ // matcher stays in the IDE message. Then the fact it fired on.
189
+ if (because.length && /^[a-z]/.test(because[0]))
190
+ because[0] = `Because ${because[0]}`;
84
191
  const why = [
85
- ...(f.because ? sentences(line(f.because, 400)) : []),
192
+ ruleSentence(f, tone) || '',
193
+ ...because,
86
194
  f.note ? line(f.note, 180) : '',
87
195
  f.fallback ? line(f.fallback, 400) : '',
88
196
  ].filter(Boolean);
@@ -90,13 +198,14 @@ function nativeDialogView(reason, options = {}) {
90
198
  ? ['This action is stopped.', 'An admin can change the rule in the console.']
91
199
  : tone === 'approval'
92
200
  ? ['An admin or manager can approve it in the console.', 'Closing this window does not approve it.']
201
+ // With a header, what Always allow covers (or why it is not offered) is its last row.
93
202
  : [
94
- 'Allow once lets only this action continue.',
95
- 'Deny or close keeps it blocked.',
96
- options.allowAlways && options.alwaysScopeLabel
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
- : '',
203
+ 'Allow once lets only this action run; Deny or close keeps it stopped.',
204
+ header ? '' : alwaysCoversLine(options) || (!options.allowAlways && options.alwaysNotOffered ? line(options.alwaysNotOffered, 120) : ''),
99
205
  ].filter(Boolean);
206
+ // One statement shown whole in the header's Command row: the step list and script box would repeat it.
207
+ const full = actionText(f)?.replace(/\s+/g, ' ').trim();
208
+ const shownWhole = Boolean(header && full && header.command === full);
100
209
  const footer = f.policy && !f.fallback
101
210
  ? `Full details: the IDE message and this machine’s Activity. Console → Action Policies → ${f.localSafety ? 'Source: Local Safety → ' : ''}"${line(f.policy, 80)}".`
102
211
  : 'Full details are in the IDE message and this machine’s Activity in FullCourtDefense.';
@@ -109,10 +218,11 @@ function nativeDialogView(reason, options = {}) {
109
218
  : tone === 'approval'
110
219
  ? 'A security policy paused this action for an admin or manager.'
111
220
  : 'A security policy paused this action. Review the details and choose what to do next.',
221
+ ...(header ? { header } : {}),
112
222
  kind: f.kind,
113
223
  // The whole script when we have it (the box scrolls); the short line otherwise.
114
- action: f.script ? f.script.replace(/ ⏎ /g, '\n') : f.action ? line(f.action, 280) : undefined,
115
- steps: (f.steps || []).slice(0, 14).map(step => line(step, 220)),
224
+ action: shownWhole ? undefined : f.script ? actionText(f) : f.action ? line(f.action, 280) : undefined,
225
+ steps: shownWhole ? [] : (f.steps || []).slice(0, 14).map(step => line(step, 220)),
116
226
  policy: f.policy ? line(f.policy, 160) : undefined,
117
227
  scope: f.scope ? line(f.scope, 220) : undefined,
118
228
  agent: f.agent ? line(f.agent, 80) : undefined,
@@ -129,17 +239,41 @@ function nativeApprovalTitle(reason) {
129
239
  const parts = ['FullCourtDefense', f.kind ? `Approval needed: ${f.kind}` : 'Approval needed', f.policy ? line(f.policy, 60) : ''].filter(Boolean);
130
240
  return parts.join(' · ');
131
241
  }
242
+ /** The header as text rows: the sentence, then Command / Action / Where / Always allow. */
243
+ function headerLines(header) {
244
+ return [
245
+ header.who,
246
+ `Why you're asked: ${header.why}`,
247
+ `${`${header.commandLabel}:`.padEnd(9)}${header.command}`,
248
+ // One line in the compact text (the card draws Affects as its own row).
249
+ `Action: ${header.action}${header.affects ? ` — affects ${header.affects}` : ''}`,
250
+ `Where: ${header.whereText}`,
251
+ ...(header.secretSentTo ? [`${header.secretSentTo.label}: ${header.secretSentTo.value}`] : []),
252
+ `Always allow: ${header.always}`,
253
+ ];
254
+ }
132
255
  function nativeApprovalMessage(reason, options = {}) {
133
256
  const f = promptFacts(reason);
257
+ const header = !f.fallback && (options.tone || 'ask') === 'ask' ? askHeaderFor(f, options) : undefined;
258
+ // As in the card: the fact a cloud / credential rule fired on is the header's sentence, not repeated.
259
+ const because = header && saysTheFact(header) && f.because
260
+ ? sentences(f.because).slice(1).join(' ')
261
+ : f.because;
262
+ const full = actionText(f)?.replace(/\s+/g, ' ').trim();
263
+ const steps = header && full && header.command === full ? [] : f.steps || [];
134
264
  const details = f.fallback ? [line(f.fallback, 650)] : [
265
+ ...(header ? headerLines(header) : []),
135
266
  f.policy ? `Policy: ${line(f.policy, 160)}` : '',
136
- f.scope ? ` ${line(f.scope, 200)}` : '',
137
- f.action ? `Action: ${f.kind ? `${f.kind} — ` : ''}${line(f.action, 230)}` : '',
138
- ...(f.steps || []).slice(0, 6).map((step, index) => `${index === 0 ? 'Does: ' : ' '}${index + 1}. ${line(step, 160)}`),
267
+ // Where the rule comes from, in customer words (the raw matcher is on the IDE message's Rule: line).
268
+ f.policy ? ` ${line(ruleSentence(f, options.tone || 'ask') || f.scope || '', 220)}` : '',
269
+ // The header's Command row is the action; without a header the action line stays.
270
+ f.action && !header ? `Action: ${f.kind ? `${f.kind} — ` : ''}${line(f.action, 230)}` : '',
271
+ ...steps.slice(0, 6).map((step, index) => `${index === 0 ? 'Does: ' : ' '}${index + 1}. ${line(step, 160)}`),
139
272
  f.agent ? `Agent: ${line(f.agent, 80)}` : '',
140
- f.ide ? `IDE: ${line(f.ide, 40)}` : '',
273
+ // The header's first line already names the client.
274
+ f.ide && !header ? `IDE: ${line(f.ide, 40)}` : '',
141
275
  f.tool ? `Tool: ${line(f.tool, 230)}` : '',
142
- f.because ? `Because: ${line(f.because, 300)}` : '',
276
+ because ? `Because: ${line(because, 300)}` : '',
143
277
  f.note ? `Note: ${f.note}` : '',
144
278
  ].filter(Boolean);
145
279
  const where = f.policy && !f.fallback
@@ -148,15 +282,18 @@ function nativeApprovalMessage(reason, options = {}) {
148
282
  if (!options.allowAlways) {
149
283
  return [
150
284
  'Allow this action once?',
151
- '', ...details, '',
285
+ // The header's "Why you're asked" answers the question directly below it.
286
+ ...(header ? [] : ['']), ...details, '',
287
+ ...(options.alwaysNotOffered && !header ? [line(options.alwaysNotOffered, 120)] : []),
152
288
  'Allow once permits only this action. Deny or closing keeps it stopped.',
153
289
  where,
154
290
  ].join('\n');
155
291
  }
292
+ const covers = header ? undefined : alwaysCoversLine(options);
156
293
  return [
157
294
  'Allow this action?',
158
- '', ...details, '',
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.`, ''] : []),
295
+ ...(header ? [] : ['']), ...details, '',
296
+ ...(covers ? [covers, ''] : []),
160
297
  'Allow once permits only this action. Deny or closing keeps it stopped.',
161
298
  where,
162
299
  ].join('\n');
@@ -54,6 +54,8 @@ export interface AskEnvelope {
54
54
  grantLimits?: GrantLimits | null;
55
55
  /** Text for the dialog (the same explanation the IDE card shows). */
56
56
  reason: string;
57
+ /** The client the runtime event records as `agentClient` (cursor, claude-code, …): the dialog names the same agent as the console. */
58
+ agentClient?: string;
57
59
  }
58
60
  export type AskResolution = 'allow' | 'allow_always' | 'deny' | 'timeout' | 'ide';
59
61
  export interface HookVerdict {
package/dist/askDialog.js CHANGED
@@ -144,12 +144,26 @@ async function resolveAskWithDialog(verdict, options = {}) {
144
144
  const allowAlways = envelope.remember !== 'none' && Boolean(grantHash) && Boolean(grantLimits) && (0, devConfirm_1.grantOffered)(envelope.scope);
145
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
+ const alwaysCovers = allowAlways ? (0, devConfirm_1.grantCoverage)(envelope.scope) : undefined;
148
+ // The scope's own refusal; `remember: none` / org switch-off / no rule identity travel as `subject.alwaysOff`.
149
+ const alwaysNotOffered = !allowAlways && envelope.remember !== 'none' && grantHash && grantLimits ? (0, devConfirm_1.grantNotOfferedReason)(envelope.scope) : undefined;
150
+ const subject = {
151
+ event: envelope.event,
152
+ operation: envelope.operation,
153
+ ...(envelope.scope.cloud ? { cloud: envelope.scope.cloud } : {}),
154
+ ...(envelope.scope.destinationDomain ? { destinationDomain: envelope.scope.destinationDomain } : {}),
155
+ ...(envelope.scope.credential?.hosts?.length ? { credentialHosts: envelope.scope.credential.hosts } : {}),
156
+ ...(envelope.policyId ? { ruleId: envelope.policyId } : {}),
157
+ // The client the runtime record carries; older hook builds leave it off (the `--agent-client` flag then).
158
+ ...(envelope.agentClient || options.clientLabel ? { agentClient: envelope.agentClient || options.clientLabel } : {}),
159
+ ...(allowAlways || alwaysNotOffered ? {} : { alwaysOff: envelope.remember === 'none' ? 'rule' : envelope.grantLimits === null ? 'org' : 'no_rule' }),
160
+ };
147
161
  const dialog = options.dialog || devConfirm_1.confirmNativeActionDetailed;
148
162
  const reason = options.clientLabel ? `IDE: ${options.clientLabel}\n${envelope.reason}` : envelope.reason;
149
163
  let outcome = 'unavailable';
150
164
  let latencyMs = 0;
151
165
  try {
152
- ({ outcome, latencyMs } = await dialog(reason, options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, { allowAlways, alwaysScopeLabel: scopeLabel, alwaysLimitsLabel: limitsLabel }));
166
+ ({ outcome, latencyMs } = await dialog(reason, options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, { allowAlways, alwaysScopeLabel: scopeLabel, alwaysLimitsLabel: limitsLabel, alwaysCovers, alwaysNotOffered, subject }));
153
167
  }
154
168
  catch {
155
169
  outcome = 'unavailable';