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
@@ -33,11 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseMsiProductCodeFromRegOutput = exports.findMsiProductCode = void 0;
36
37
  exports.listFcdConfigBackups = listFcdConfigBackups;
37
38
  exports.removeFcdConfigBackups = removeFcdConfigBackups;
38
39
  exports.purgeLocalState = purgeLocalState;
39
- exports.parseMsiProductCodeFromRegOutput = parseMsiProductCodeFromRegOutput;
40
- exports.findMsiProductCode = findMsiProductCode;
41
40
  exports.detachCommand = detachCommand;
42
41
  exports.reportUninstallToFleet = reportUninstallToFleet;
43
42
  exports.uninstallAllCommand = uninstallAllCommand;
@@ -62,6 +61,7 @@ const credentialStore_1 = require("../credentialStore");
62
61
  const machineKeyFile_1 = require("../machineKeyFile");
63
62
  const selfUpdate_1 = require("../selfUpdate");
64
63
  const backupManifest_1 = require("../backupManifest");
64
+ const msiProductCode_1 = require("../msiProductCode");
65
65
  const BOLD = '\x1b[1m';
66
66
  const GREEN = '\x1b[32m';
67
67
  const YELLOW = '\x1b[33m';
@@ -193,48 +193,9 @@ function purgeLocalState(config) {
193
193
  }
194
194
  return { failures };
195
195
  }
196
- /**
197
- * Product code of the installed FullCourtDefense MSI, from the registry
198
- * uninstall hive (both 64-bit and WOW6432Node views). Undefined on npm
199
- * installs or when the MSI entry is missing.
200
- */
201
- function parseMsiProductCodeFromRegOutput(output) {
202
- // `reg query … /s /f FullCourtDefense /d` prints the matching key path line
203
- // followed by its matching values. We want the {GUID} keyed entry whose
204
- // DisplayName is FullCourtDefense.
205
- const lines = output.split(/\r?\n/);
206
- let currentGuid;
207
- for (const line of lines) {
208
- const keyMatch = line.match(/\\Uninstall\\(\{[0-9A-Fa-f-]{36}\})\s*$/);
209
- if (keyMatch) {
210
- currentGuid = keyMatch[1];
211
- continue;
212
- }
213
- if (currentGuid && /DisplayName\s+REG_SZ\s+FullCourtDefense\s*$/i.test(line.trim())) {
214
- return currentGuid;
215
- }
216
- }
217
- return undefined;
218
- }
219
- function findMsiProductCode() {
220
- if (process.platform !== 'win32')
221
- return undefined;
222
- const hives = [
223
- 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
224
- 'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
225
- ];
226
- for (const hive of hives) {
227
- const query = (0, child_process_1.spawnSync)('reg', ['query', hive, '/s', '/f', 'FullCourtDefense', '/d'], {
228
- encoding: 'utf8', windowsHide: true, timeout: 30_000,
229
- });
230
- if (query.status !== 0 || !query.stdout)
231
- continue;
232
- const code = parseMsiProductCodeFromRegOutput(query.stdout);
233
- if (code)
234
- return code;
235
- }
236
- return undefined;
237
- }
196
+ var msiProductCode_2 = require("../msiProductCode");
197
+ Object.defineProperty(exports, "findMsiProductCode", { enumerable: true, get: function () { return msiProductCode_2.findMsiProductCode; } });
198
+ Object.defineProperty(exports, "parseMsiProductCodeFromRegOutput", { enumerable: true, get: function () { return msiProductCode_2.parseMsiProductCodeFromRegOutput; } });
238
199
  /**
239
200
  * The shared local teardown: daemon autostart, self-heal schedule, MCP gateway
240
201
  * wraps, IDE hooks, terminal guards, and the daily discovery schedule. Each
@@ -405,7 +366,7 @@ async function uninstallAllCommand(args, config) {
405
366
  }
406
367
  }
407
368
  if (msiHandoff) {
408
- const productCode = findMsiProductCode();
369
+ const productCode = (0, msiProductCode_1.findMsiProductCode)();
409
370
  if (productCode && launchMsiUninstall(productCode)) {
410
371
  console.log(`${GREEN}${BOLD}Local teardown done — Windows uninstaller launched.${RESET}`);
411
372
  console.log(`${DIM}Approve the Windows elevation prompt (UAC) to finish. That step removes the "FullCourtDefense Updater" SYSTEM task, the HKLM PowerShell audit policies FCD enabled, and the application files under Program Files.${RESET}`);
package/dist/config.d.ts CHANGED
@@ -36,7 +36,7 @@ export interface PowershellHealth {
36
36
  export declare function getPowershellHealth(): PowershellHealth | undefined;
37
37
  /**
38
38
  * PowerShell language mode — the "is this a hardened WDAC/AppLocker fleet
39
- * machine?" bit that explained the lptx1110 incident. Spawned only on the
39
+ * machine?" bit that explained the devbox-01 incident. Spawned only on the
40
40
  * DPAPI failure path (and from the deep self-test), never on healthy runs.
41
41
  */
42
42
  export declare function capturePowershellLanguageMode(): string | undefined;
package/dist/config.js CHANGED
@@ -171,7 +171,7 @@ function getPowershellHealth() {
171
171
  }
172
172
  /**
173
173
  * PowerShell language mode — the "is this a hardened WDAC/AppLocker fleet
174
- * machine?" bit that explained the lptx1110 incident. Spawned only on the
174
+ * machine?" bit that explained the devbox-01 incident. Spawned only on the
175
175
  * DPAPI failure path (and from the deep self-test), never on healthy runs.
176
176
  */
177
177
  function capturePowershellLanguageMode() {
@@ -250,7 +250,7 @@ function unprotectShieldKeyForCurrentWindowsUser(value) {
250
250
  const result = powershellDpapi(exports.DPAPI_DECRYPT_SNIPPET, value);
251
251
  if (!result && value) {
252
252
  // The machine HAS a DPAPI-protected key it cannot read — the exact
253
- // credential-broken state that 401-bricked lptx1110. Emit a coded
253
+ // credential-broken state that 401-bricked devbox-01. Emit a coded
254
254
  // distress signal so the fleet console sees it without asking the user.
255
255
  const health = powershellHealth;
256
256
  if (health && !health.spawnOk) {
@@ -446,7 +446,7 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
446
446
  lastCredentialTrace = { shieldKeySource, dpapiAttempted, at: new Date().toISOString() };
447
447
  // Self-healing migration: whenever the key still resolves through a legacy
448
448
  // source (DPAPI blob or plaintext config), copy it into the Credential
449
- // Manager once. On fleets like lptx1110 the daemon's context CAN decrypt
449
+ // Manager once. On fleets like devbox-01 the daemon's context CAN decrypt
450
450
  // while IDE-spawned hooks CANNOT — after the daemon's first resolution the
451
451
  // native entry exists and every process reads it without PowerShell.
452
452
  if (shieldKey && resolvedShieldId
@@ -1,3 +1,34 @@
1
+ /**
2
+ * "Always allow" for a credential command, bound to where the credential goes (#1535).
3
+ *
4
+ * A grant under `gcloud_access_token_cmd` used to remember only programs
5
+ * (`curl`, `gcloud`), so for 30 days any line that minted a Google token and
6
+ * ran `curl` rode it — whatever host received the token. A credential grant
7
+ * now also binds:
8
+ *
9
+ * - ISSUER rules (the command prints a token its own provider accepts:
10
+ * gcloud / az access tokens, `gh auth token`, AWS keys): every host the line
11
+ * sends to must sit under the issuer's domain (`*.googleapis.com`). The
12
+ * 2026-09-14 fix survives — `logging.` and `monitoring.googleapis.com`
13
+ * share one grant — and a token sent anywhere else asks again.
14
+ * - EXACT rules (the command reads a stored secret — Secret Manager, AWS
15
+ * secrets, Vault, git / keychain / kubeconfig dumps): the store is not the
16
+ * issuer of what it holds (a GitHub token read from Secret Manager goes to
17
+ * `api.github.com`), so the grant binds the exact hosts and the named
18
+ * secrets seen when the developer pressed the button.
19
+ *
20
+ * A destination or secret this module cannot read (a host in a variable it
21
+ * cannot resolve, a program it does not model such as `node` or `Set-Content`,
22
+ * a proxy / `--resolve` override) refuses the grant: no "Always allow" on the
23
+ * dialog, and no standing grant matches — the developer is asked.
24
+ *
25
+ * Grants recorded before this binding carry no `credential` field; for a
26
+ * credential rule they match nothing (asked again, never wider).
27
+ *
28
+ * Deterministic structure only (words, flags, literal URLs, same-line
29
+ * assignments). Pure: no I/O.
30
+ */
31
+ import { type ShellDialect } from './shellQuoteMask';
1
32
  export type CredentialBindingMode = 'issuer' | 'exact';
2
33
  export interface CredentialGrantBinding {
3
34
  mode: CredentialBindingMode;
@@ -24,6 +55,23 @@ export interface CredentialRuleFacts {
24
55
  }
25
56
  /** Binding mode for the credential rules in one ask. Mixed or unknown rules bind exact hosts. */
26
57
  export declare function credentialRuleFacts(itemIds: string[]): CredentialRuleFacts;
58
+ export declare const unquote: (word: string) => string;
59
+ export interface LineVars {
60
+ /** Variable -> every literal value assigned to it on the line; null = assigned from something unreadable. */
61
+ values: Map<string, string[] | null>;
62
+ /** The line can set variables in ways not read here (`Set-Variable`, `-OutVariable`, `read`, `source`): nothing resolves. */
63
+ poisoned: boolean;
64
+ }
65
+ /**
66
+ * Every assignment on the line, wherever it sits (statement start, a script
67
+ * block, an `else` branch, a condition). Read on the masked line, so quoted
68
+ * text never counts as an assignment.
69
+ */
70
+ export declare function lineVars(command: string, dialect: ShellDialect): LineVars;
71
+ /** `--name value` / `--name=value` as a literal; undefined when absent, null when dynamic. */
72
+ export declare function flagValue(words: string[], names: string[], vars: LineVars): string | null | undefined;
73
+ /** A literal word or a variable holding exactly one literal; null when dynamic. */
74
+ export declare function literalWord(word: string | undefined, vars: LineVars): string | null;
27
75
  /**
28
76
  * The binding for one credential-command line. `uses` are the program uses the
29
77
  * grant scanner found (`scanShellPrograms`), `localOnly` the programs that only
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CREDENTIAL_GRANT_TTL_MS = exports.CREDENTIAL_ISSUERS = void 0;
3
+ exports.unquote = exports.CREDENTIAL_GRANT_TTL_MS = exports.CREDENTIAL_ISSUERS = void 0;
4
4
  exports.credentialRuleFacts = credentialRuleFacts;
5
+ exports.lineVars = lineVars;
6
+ exports.flagValue = flagValue;
7
+ exports.literalWord = literalWord;
5
8
  exports.credentialBindingFor = credentialBindingFor;
6
9
  exports.withinIssuer = withinIssuer;
7
10
  exports.credentialBindingCovers = credentialBindingCovers;
@@ -60,6 +63,7 @@ function credentialRuleFacts(itemIds) {
60
63
  // Destinations
61
64
  // ---------------------------------------------------------------------------
62
65
  const unquote = (word) => word.replace(/^(["'])([\s\S]*)\1$/, '$2');
66
+ exports.unquote = unquote;
63
67
  function assign(vars, name, value) {
64
68
  const key = name.toLowerCase();
65
69
  const prior = vars.values.get(key);
@@ -71,13 +75,19 @@ function assign(vars, name, value) {
71
75
  }
72
76
  vars.values.set(key, [...(prior || []), value]);
73
77
  }
74
- /** A right-hand side this module can use as data: literal text, no command or sub-expression. */
75
- function literalValue(rhs) {
78
+ /**
79
+ * A right-hand side this module can use as data: literal text, no command or sub-expression.
80
+ * In PowerShell a bare word on the right (`$p = Read-Host`) RUNS that command; only quoted
81
+ * text, a hashtable or a number is data there.
82
+ */
83
+ function literalValue(rhs, ps = false) {
76
84
  const text = rhs.trim();
77
85
  if (!text || /\$\(|@\(|`/.test(text))
78
86
  return null;
79
87
  if (/^'[^']*'$/.test(text) || /^"[^"]*"$/.test(text) || /^@\{[^{}]*\}$/.test(text))
80
88
  return text;
89
+ if (ps)
90
+ return /^-?\d+(?:\.\d+)?$/.test(text) ? text : null;
81
91
  if (/^[\w.:/@%+?=#~-]+$/.test(text))
82
92
  return text;
83
93
  return null;
@@ -118,7 +128,7 @@ function lineVars(command, dialect) {
118
128
  for (const m of code.matchAll(assignment)) {
119
129
  const from = m.index + m[0].length;
120
130
  const rhs = command.slice(from, expressionEnd(code, from, !ps));
121
- assign(vars, m[1], m[2] ? null : literalValue(rhs));
131
+ assign(vars, m[1], m[2] ? null : literalValue(rhs, ps));
122
132
  }
123
133
  const loops = ps ? /\bforeach\s*\(\s*\$([A-Za-z_]\w*)\s+in\s+/gi : /\bfor\s+([A-Za-z_]\w*)\s+in\s+/g;
124
134
  for (const m of code.matchAll(loops)) {
@@ -128,14 +138,14 @@ function lineVars(command, dialect) {
128
138
  if (!items.length)
129
139
  assign(vars, m[1], null);
130
140
  for (const item of items)
131
- assign(vars, m[1], literalValue(item));
141
+ assign(vars, m[1], literalValue(item, ps));
132
142
  }
133
143
  return vars;
134
144
  }
135
145
  const HOST_OK = /^(?:[a-z0-9-]+\.)*[a-z0-9-]+$|^\[[0-9a-f:.]+\]$/;
136
146
  /** Host of one URL-ish value; `unknown` when the host itself is dynamic or malformed. */
137
147
  function hostOfUrl(value) {
138
- const text = unquote(value).trim();
148
+ const text = (0, exports.unquote)(value).trim();
139
149
  const match = text.match(/^(?:([a-z][a-z0-9+.-]*):\/\/)?([^/?#\s]*)/i);
140
150
  const authority = (match?.[2] || '').replace(/^[^@]*@/, '');
141
151
  if (!authority || /[$`({%]/.test(authority))
@@ -156,7 +166,7 @@ function urlHostsIn(text) {
156
166
  }
157
167
  /** A destination word: a literal URL / host, or `$var` / `"$var/path"` resolved through the line's literal assignments. */
158
168
  function resolveDestination(word, vars) {
159
- const text = unquote(word).trim();
169
+ const text = (0, exports.unquote)(word).trim();
160
170
  const ref = text.match(/^[$@]\{?(?:(?:script|global|local|private):)?([A-Za-z_]\w*)\}?/);
161
171
  if (ref) {
162
172
  if (vars.poisoned)
@@ -221,7 +231,7 @@ function senderDestinations(grammar, words, vars) {
221
231
  let positionalSeen = false;
222
232
  for (let k = 1; k < words.length; k++) {
223
233
  const word = words[k];
224
- const bare = unquote(word);
234
+ const bare = (0, exports.unquote)(word);
225
235
  if (grammar.powershell) {
226
236
  const param = bare.match(/^-([A-Za-z]+)(?::(.*))?$/);
227
237
  if (param) {
@@ -317,7 +327,7 @@ function registryDestination(words, vars) {
317
327
  const valued = new Set(['-u', '--username', '-p', '--password', '--authfile', '--cert-dir']);
318
328
  const start = verb === 'registry' ? verbAt + 2 : verbAt + 1;
319
329
  for (let k = start; k < words.length; k++) {
320
- const bare = unquote(words[k]);
330
+ const bare = (0, exports.unquote)(words[k]);
321
331
  if (bare.startsWith('-')) {
322
332
  if (valued.has(bare.split('=')[0].toLowerCase()) && !bare.includes('='))
323
333
  k++;
@@ -330,9 +340,10 @@ function registryDestination(words, vars) {
330
340
  // ---------------------------------------------------------------------------
331
341
  // Secret resources
332
342
  // ---------------------------------------------------------------------------
343
+ /** `--name value` / `--name=value` as a literal; undefined when absent, null when dynamic. */
333
344
  function flagValue(words, names, vars) {
334
345
  for (let k = 0; k < words.length; k++) {
335
- const bare = unquote(words[k]);
346
+ const bare = (0, exports.unquote)(words[k]);
336
347
  for (const name of names) {
337
348
  if (bare === name)
338
349
  return literalWord(words[k + 1], vars);
@@ -346,13 +357,13 @@ function flagValue(words, names, vars) {
346
357
  function literalWord(word, vars) {
347
358
  if (word === undefined)
348
359
  return null;
349
- const text = unquote(word).trim();
360
+ const text = (0, exports.unquote)(word).trim();
350
361
  const ref = text.match(/^\$\{?(?:(?:script|global|local|private):)?([A-Za-z_]\w*)\}?$/);
351
362
  if (ref) {
352
363
  if (vars.poisoned)
353
364
  return null;
354
365
  const values = vars.values.get(ref[1].toLowerCase());
355
- return values && values.length && new Set(values).size === 1 ? unquote(values[0]) : null;
366
+ return values && values.length && new Set(values).size === 1 ? (0, exports.unquote)(values[0]) : null;
356
367
  }
357
368
  return text && !/[$`(){}]/.test(text) ? text : null;
358
369
  }
@@ -360,15 +371,15 @@ function secretResources(ruleId, uses, dialect, vars) {
360
371
  const out = [];
361
372
  for (const use of uses) {
362
373
  const words = (0, shellQuoteMask_1.shellWords)(use.text.trim(), dialect);
363
- const at = words.findIndex(w => unquote(w).toLowerCase().replace(/\.(exe|cmd)$/, '').endsWith(use.name));
374
+ const at = words.findIndex(w => (0, exports.unquote)(w).toLowerCase().replace(/\.(exe|cmd)$/, '').endsWith(use.name));
364
375
  const args = words.slice(at + 1);
365
- const lower = args.map(w => unquote(w).toLowerCase());
376
+ const lower = args.map(w => (0, exports.unquote)(w).toLowerCase());
366
377
  if (ruleId === 'gcloud_secret_access_cmd' && use.name === 'gcloud') {
367
378
  const idx = lower.findIndex((w, k) => w === 'secrets' && lower[k + 1] === 'versions' && lower[k + 2] === 'access');
368
379
  if (idx < 0)
369
380
  continue;
370
- const positional = args.slice(idx + 3).find(w => !unquote(w).startsWith('-'));
371
- const full = positional && unquote(positional).match(/^projects\/([^/]+)\/secrets\/([^/]+)/);
381
+ const positional = args.slice(idx + 3).find(w => !(0, exports.unquote)(w).startsWith('-'));
382
+ const full = positional && (0, exports.unquote)(positional).match(/^projects\/([^/]+)\/secrets\/([^/]+)/);
372
383
  if (full) {
373
384
  out.push(`projects/${full[1]}/secrets/${full[2]}`);
374
385
  continue;
@@ -401,7 +412,7 @@ function secretResources(ruleId, uses, dialect, vars) {
401
412
  if (!/^(?:get|read)$/.test(lower[verbAt] || ''))
402
413
  continue;
403
414
  const mount = flagValue(args, ['-mount'], vars);
404
- const paths = args.slice(verbAt + 1).filter(w => !unquote(w).startsWith('-'));
415
+ const paths = args.slice(verbAt + 1).filter(w => !(0, exports.unquote)(w).startsWith('-'));
405
416
  if (mount === null || paths.length !== 1)
406
417
  return null;
407
418
  const path = literalWord(paths[0], vars);
@@ -429,7 +440,7 @@ function credentialBindingFor(command, uses, localOnly, ruleCommands, facts) {
429
440
  if (ruleCommands.includes(use.name) || localOnly(use.name))
430
441
  continue;
431
442
  const words = (0, shellQuoteMask_1.shellWords)(use.text.trim(), dialect);
432
- const at = words.findIndex(w => unquote(w).toLowerCase().replace(/^.*[\\/]/, '').replace(/\.(exe|cmd|bat)$/, '') === use.name);
443
+ const at = words.findIndex(w => (0, exports.unquote)(w).toLowerCase().replace(/^.*[\\/]/, '').replace(/\.(exe|cmd|bat)$/, '') === use.name);
433
444
  const argv = at >= 0 ? words.slice(at) : words;
434
445
  const grammar = SENDERS[use.name];
435
446
  const read = grammar ? senderDestinations(grammar, argv, vars)
@@ -10,7 +10,7 @@ const distress_1 = require("./distress");
10
10
  * Native credential store — Windows Credential Manager via `@napi-rs/keyring`
11
11
  * (a prebuilt N-API binding; in-process win32 API calls, no PowerShell).
12
12
  *
13
- * WHY: the lptx1110 incident class. The shield key at rest was DPAPI-protected
13
+ * WHY: the devbox-01 incident class. The shield key at rest was DPAPI-protected
14
14
  * and decrypted by spawning powershell.exe — exactly the pattern EDRs flag
15
15
  * ("IDE process spawns PowerShell that touches credentials"). On hardened
16
16
  * fleets PowerShell is additionally locked to Constrained Language Mode or
@@ -1,3 +1,4 @@
1
+ import { type AlwaysCovers, type AskSubject, type NativeDialogView } from './approvalPrompt';
1
2
  import { type CredentialGrantBinding, type CredentialRuleFacts } from './credentialGrant';
2
3
  export { isCiEnvironment, isHumanPresent } from './callerEnv';
3
4
  export type ConfirmRemember = 'none' | 'session' | '1h';
@@ -44,9 +45,30 @@ export interface NativeDialogOptions {
44
45
  alwaysScopeLabel?: string;
45
46
  /** How long / how many times it would hold, e.g. "30 days" or "7 days or 20 uses". */
46
47
  alwaysLimitsLabel?: string;
48
+ /** What / How / Where the grant covers (`grantCoverage`), shown as one compact row. */
49
+ alwaysCovers?: AlwaysCovers;
50
+ /** Two buttons: why "Always allow" is not offered (`grantNotOfferedReason`), one short line. */
51
+ alwaysNotOffered?: string;
52
+ /** Envelope facts for the "Why you're asked" header (grant scope, rule id). */
53
+ subject?: AskSubject;
47
54
  /** Icon and copy. Buttons stay Deny / Allow once / Always allow for `ask`. */
48
55
  tone?: 'ask' | 'approval' | 'block';
49
56
  }
57
+ /** Card HTML for macOS WKWebView. Button labels stay the existing Deny / Allow once / Always allow. */
58
+ export declare function dialogCardHtml(view: NativeDialogView, options: NativeDialogOptions): string;
59
+ /**
60
+ * The layout facts for the Windows card, base64 JSON, written to the dialog's STDIN.
61
+ * `-EncodedCommand` carries the script as UTF-16 base64 and Windows caps a command line
62
+ * at 32,767 characters: with the view inside it, a long script (a 9-step token query)
63
+ * failed to spawn and the ask silently fell back to the IDE card.
64
+ */
65
+ export declare function windowsDialogPayload(view: NativeDialogView): string;
66
+ /**
67
+ * Windows branded card. Compact prompt text stays the first payload so tests still
68
+ * read the dialog copy; layout facts arrive on stdin (`windowsDialogPayload`).
69
+ * Exit codes: 0 = Allow once, 3 = Always allow, 2 = timed out, anything else = Deny.
70
+ */
71
+ export declare function windowsDialogScript(message: string, timeoutSeconds: number, options: NativeDialogOptions, title: string, view: NativeDialogView): string;
50
72
  export declare function confirmNativeActionDetailed(reason: string, timeoutMs?: number, options?: NativeDialogOptions): Promise<{
51
73
  outcome: NativeDialogOutcome;
52
74
  latencyMs: number;
@@ -138,6 +160,18 @@ export interface GrantScope {
138
160
  * binding existed) never covers a credential-rule ask — asked again.
139
161
  */
140
162
  credential?: CredentialGrantBinding;
163
+ /**
164
+ * Shell only, when the line runs a cloud CLI: `gcloud|<how>|<where>` per use,
165
+ * sorted and unique (`cloudGrant.ts`). A grant without it (recorded before
166
+ * the binding existed) never covers a cloud line — asked again.
167
+ */
168
+ cloud?: string[];
169
+ /**
170
+ * Set when the rule that asked is about a removal that can never be granted
171
+ * (home folder, a drive, the filesystem root): no Always allow is offered,
172
+ * none is stored, and no standing grant covers it. Not part of the grant key.
173
+ */
174
+ refusal?: 'destructive_scope';
141
175
  }
142
176
  /**
143
177
  * A developer's standing "Always allow" for one scope under one rule. Bound to the
@@ -344,10 +378,13 @@ export declare function shellProgramsForRule(command: string, ruleCommands?: rea
344
378
  export declare function splitLocalSafetyRuleIds(matchedRule?: string): string[] | undefined;
345
379
  /**
346
380
  * Can "Always allow" exist for this scope at all? Not for a shell line whose
347
- * programs could not be read (empty set) and not for a credential line whose
348
- * destination or secret could not be read, or that sends the credential
349
- * outside its issuer. The dialog offers two buttons; no grant matches.
381
+ * programs could not be read (empty set), not for a credential line whose
382
+ * destination or secret could not be read or that sends the credential
383
+ * outside its issuer, and not for a cloud line that deletes, changes
384
+ * permissions or whose target cannot be read. The dialog offers two
385
+ * buttons and says why in one line; no grant matches.
350
386
  */
387
+ export declare function grantNotOfferedReason(scope: GrantScope): string | undefined;
351
388
  export declare function grantOffered(scope: GrantScope): boolean;
352
389
  /** The caps a grant for this scope is created under: credential grants live 7 days at most. */
353
390
  export declare function capGrantLimitsForScope(limits: GrantLimits | null, scope: GrantScope): GrantLimits | null;
@@ -391,6 +428,8 @@ export declare function describeGrantScope(scope: GrantScope, rule?: {
391
428
  policyId?: string;
392
429
  policyName?: string;
393
430
  }): string;
431
+ /** What / How / Where an "Always allow" covers, as the dialog's compact rows. */
432
+ export declare function grantCoverage(scope: GrantScope): AlwaysCovers;
394
433
  /**
395
434
  * Which line-level facts the RULE that asked actually depends on. A grant binds
396
435
  * the facts the rule fired on and nothing else: `gcloud_access_token_cmd` fired
@@ -414,6 +453,8 @@ export interface GrantRuleFacts {
414
453
  * credential goes (issuer domain or exact hosts) and the secret read.
415
454
  */
416
455
  credential?: CredentialRuleFacts;
456
+ /** The rule is a home / drive / root delete (`NEVER_GRANTED_ITEMS`): the scope carries a refusal. */
457
+ neverGrant?: boolean;
417
458
  }
418
459
  /**
419
460
  * Derive `GrantRuleFacts` from the source of the ask. Unknown = bound (the