fullcourtdefense-cli 1.34.21 → 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 (91) hide show
  1. package/README.md +1 -0
  2. package/dist/actionIdentity.d.ts +1 -0
  3. package/dist/actionIdentity.js +2 -0
  4. package/dist/actionPolicyEngine.d.ts +5 -0
  5. package/dist/actionPolicyEngine.js +273 -30
  6. package/dist/agentTerminal.d.ts +1 -3
  7. package/dist/agentTerminal.js +14 -29
  8. package/dist/approvalPrompt.d.ts +7 -0
  9. package/dist/approvalPrompt.js +46 -0
  10. package/dist/askDialog.d.ts +87 -0
  11. package/dist/askDialog.js +209 -0
  12. package/dist/blockExplanation.js +18 -6
  13. package/dist/boundaryVerification.d.ts +20 -0
  14. package/dist/boundaryVerification.js +147 -0
  15. package/dist/commands/daemon.js +198 -70
  16. package/dist/commands/deterministicGuard.d.ts +2 -0
  17. package/dist/commands/deterministicGuard.js +161 -9
  18. package/dist/commands/hook.js +81 -29
  19. package/dist/commands/installClaudeHook.js +11 -5
  20. package/dist/commands/installCursorHook.js +5 -4
  21. package/dist/commands/mcpGateway.d.ts +2 -0
  22. package/dist/commands/mcpGateway.js +163 -56
  23. package/dist/commands/onboard.js +6 -5
  24. package/dist/commands/onboardingSummary.d.ts +5 -0
  25. package/dist/commands/onboardingSummary.js +11 -0
  26. package/dist/commands/protectedRun.d.ts +14 -0
  27. package/dist/commands/protectedRun.js +207 -0
  28. package/dist/containment-runtime/index.mjs +30118 -0
  29. package/dist/containment-runtime/licenses/_anthropic-ai_sandbox-runtime.txt +201 -0
  30. package/dist/containment-runtime/licenses/_pondwader_socks5-server.txt +20 -0
  31. package/dist/containment-runtime/licenses/commander.txt +22 -0
  32. package/dist/containment-runtime/licenses/node-forge.txt +331 -0
  33. package/dist/containment-runtime/licenses/zod.txt +21 -0
  34. package/dist/containment-runtime/runtime-version.json +1 -0
  35. package/dist/containment-runtime/vendor/java-proxy-agent/build.ts +95 -0
  36. package/dist/containment-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
  37. package/dist/containment-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
  38. package/dist/containment-runtime/vendor/seccomp/build.ts +69 -0
  39. package/dist/containment-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
  40. package/dist/containment-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
  41. package/dist/containment-runtime/vendor/srt-win/build.ts +21 -0
  42. package/dist/containment-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
  43. package/dist/containmentLease.d.ts +5 -0
  44. package/dist/containmentLease.js +72 -0
  45. package/dist/detectionUpdates.d.ts +23 -3
  46. package/dist/detectionUpdates.js +45 -14
  47. package/dist/detectorArtifact.d.ts +17 -0
  48. package/dist/detectorArtifact.js +35 -0
  49. package/dist/detectorBaseline.d.ts +2 -0
  50. package/dist/detectorBaseline.js +5 -0
  51. package/dist/detectorModule.d.ts +9 -0
  52. package/dist/detectorModule.js +84 -0
  53. package/dist/detectorRuntime.d.ts +21 -0
  54. package/dist/detectorRuntime.js +49 -0
  55. package/dist/detectorUpdates.d.ts +21 -0
  56. package/dist/detectorUpdates.js +86 -0
  57. package/dist/devConfirm.d.ts +97 -15
  58. package/dist/devConfirm.js +447 -117
  59. package/dist/distress.d.ts +2 -0
  60. package/dist/distress.js +2 -0
  61. package/dist/frictionRollout.js +103 -0
  62. package/dist/gatewayRuntime.d.ts +19 -0
  63. package/dist/gatewayRuntime.js +93 -0
  64. package/dist/hookSlim.js +17 -18
  65. package/dist/index.js +35 -0
  66. package/dist/localDetectionUpdates.d.ts +3 -1
  67. package/dist/localDetectionUpdates.js +8 -1
  68. package/dist/localSafetySnapshot.d.ts +9 -5
  69. package/dist/localSafetySnapshot.js +42 -15
  70. package/dist/mcpToolReview.d.ts +17 -0
  71. package/dist/mcpToolReview.js +127 -0
  72. package/dist/mcpToolTrust.d.ts +22 -0
  73. package/dist/mcpToolTrust.js +66 -0
  74. package/dist/nativeHook.d.ts +4 -0
  75. package/dist/nativeHook.js +31 -17
  76. package/dist/policyRefresh.d.ts +3 -0
  77. package/dist/policyRefresh.js +27 -0
  78. package/dist/protectionMaintenance.d.ts +10 -0
  79. package/dist/protectionMaintenance.js +64 -0
  80. package/dist/protectionProfile.d.ts +26 -0
  81. package/dist/protectionProfile.js +63 -0
  82. package/dist/runtimeConfig.d.ts +2 -0
  83. package/dist/runtimeConfig.js +65 -26
  84. package/dist/selfTest.d.ts +2 -0
  85. package/dist/selfTest.js +9 -1
  86. package/dist/sessionLimits.d.ts +2 -0
  87. package/dist/sessionLimits.js +24 -0
  88. package/dist/telemetry.d.ts +5 -0
  89. package/dist/telemetry.js +8 -0
  90. package/dist/version.json +1 -1
  91. package/package.json +10 -6
@@ -32,6 +32,8 @@ export declare const DISTRESS: {
32
32
  readonly AUTH_BROKEN: "auth_broken";
33
33
  /** Control plane unreachable (network / DNS / proxy / timeout). */
34
34
  readonly NETWORK_DOWN: "network_down";
35
+ /** A policy response arrived, but the replacement cache could not be saved. */
36
+ readonly POLICY_CACHE_WRITE_FAILED: "policy_cache_write_failed";
35
37
  /**
36
38
  * Control plane REACHED and credentials accepted, but it refused the policy
37
39
  * request (HTTP 400/404/409/422/5xx). A server-side or data fault — the fix
package/dist/distress.js CHANGED
@@ -77,6 +77,8 @@ exports.DISTRESS = {
77
77
  AUTH_BROKEN: 'auth_broken',
78
78
  /** Control plane unreachable (network / DNS / proxy / timeout). */
79
79
  NETWORK_DOWN: 'network_down',
80
+ /** A policy response arrived, but the replacement cache could not be saved. */
81
+ POLICY_CACHE_WRITE_FAILED: 'policy_cache_write_failed',
80
82
  /**
81
83
  * Control plane REACHED and credentials accepted, but it refused the policy
82
84
  * request (HTTP 400/404/409/422/5xx). A server-side or data fault — the fix
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // ../../backend/src/modules/discovery/friction-rollout.ts
21
+ var friction_rollout_exports = {};
22
+ __export(friction_rollout_exports, {
23
+ assessFrictionRollout: () => assessFrictionRollout
24
+ });
25
+ module.exports = __toCommonJS(friction_rollout_exports);
26
+ function assessFrictionRollout(input) {
27
+ const value = input;
28
+ if (!value || typeof value.baselineVersion !== "string" || typeof value.candidateVersion !== "string" || !/^\d+\.\d+\.\d+$/.test(value.baselineVersion) || !/^\d+\.\d+\.\d+$/.test(value.candidateVersion) || !["replay", "real_hook", "real_ide", "production"].includes(String(value.evidenceLevel)) || !Array.isArray(value.observations) || value.observations.length > 2e3) throw new Error("Invalid paired evidence");
29
+ const artifact = (raw, bundled = false) => {
30
+ const item = raw;
31
+ if (!item || !Number.isSafeInteger(item.revision) || Number(item.revision) < (bundled ? 0 : 1) || typeof item.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item.sha256)) throw new Error("Invalid detector artifact identity");
32
+ return { revision: Number(item.revision), sha256: item.sha256 };
33
+ };
34
+ const hasDetection = value.baselineDetection !== void 0 || value.candidateDetection !== void 0;
35
+ const baselineDetection = hasDetection ? artifact(value.baselineDetection, true) : void 0;
36
+ const candidateDetection = hasDetection ? artifact(value.candidateDetection) : void 0;
37
+ const sameCli = value.baselineVersion === value.candidateVersion;
38
+ if (baselineDetection && candidateDetection) {
39
+ if (candidateDetection.revision < baselineDetection.revision || candidateDetection.revision === baselineDetection.revision && candidateDetection.sha256 !== baselineDetection.sha256) {
40
+ throw new Error("Detector revision downgrade or reuse");
41
+ }
42
+ }
43
+ if (sameCli && (!baselineDetection || !candidateDetection || candidateDetection.revision <= baselineDetection.revision || candidateDetection.sha256 === baselineDetection.sha256)) throw new Error("Same CLI requires distinct, increasing detector artifacts");
44
+ const verdicts = ["allow", "block", "require_approval"];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ let fixed = 0, regressed = 0, unchanged = 0, missingEffects = 0, remainingErrors = 0;
47
+ let benign = 0, guarded = 0, baselineInterruptions = 0, candidateInterruptions = 0;
48
+ const families = /* @__PURE__ */ new Set();
49
+ const machines = /* @__PURE__ */ new Set();
50
+ for (const row of value.observations) {
51
+ if (!row || !["caseId", "family", "machineId", "policyHash"].every((key2) => typeof row[key2] === "string" && /^[a-zA-Z0-9_.:-]{1,100}$/.test(row[key2])) || ![row.expected, row.baseline, row.candidate].every((v) => verdicts.includes(v)) || typeof row.sideEffectVerified !== "boolean") throw new Error("Invalid observation");
52
+ const key = JSON.stringify([row.machineId, row.caseId, row.policyHash]);
53
+ if (seen.has(key)) throw new Error("Duplicate paired observation");
54
+ seen.add(key);
55
+ families.add(row.family);
56
+ machines.add(row.machineId);
57
+ const before = row.baseline === row.expected, after = row.candidate === row.expected;
58
+ if (!before && after) fixed++;
59
+ else if (before && !after) regressed++;
60
+ else unchanged++;
61
+ if (!after) remainingErrors++;
62
+ if (!row.sideEffectVerified) missingEffects++;
63
+ if (row.expected === "allow") {
64
+ benign++;
65
+ if (row.baseline !== "allow") baselineInterruptions++;
66
+ if (row.candidate !== "allow") candidateInterruptions++;
67
+ } else guarded++;
68
+ }
69
+ const reasons = [];
70
+ if (!benign || !guarded) reasons.push("Both benign actions and prohibited/approval counterparts are required");
71
+ if (!fixed) reasons.push("No measured correction");
72
+ if (regressed || remainingErrors) reasons.push("Candidate has regressions or unresolved mismatches");
73
+ if (missingEffects) reasons.push("Actual side-effect verification is missing");
74
+ if (value.evidenceLevel !== "production") reasons.push("This evidence cannot establish production improvement");
75
+ return {
76
+ schema: "fcd.paired-feedback.v1",
77
+ baselineVersion: value.baselineVersion,
78
+ candidateVersion: value.candidateVersion,
79
+ ...hasDetection ? { baselineDetection, candidateDetection } : {},
80
+ changeKind: sameCli ? "detector_only" : hasDetection && baselineDetection.sha256 !== candidateDetection.sha256 ? "cli_and_detector" : "cli",
81
+ evidenceLevel: value.evidenceLevel,
82
+ cases: seen.size,
83
+ families: families.size,
84
+ machines: machines.size,
85
+ fixed,
86
+ regressed,
87
+ unchanged,
88
+ remainingErrors,
89
+ missingEffects,
90
+ benign,
91
+ guarded,
92
+ baselineInterruptions,
93
+ candidateInterruptions,
94
+ eligibleForOwnerReview: reasons.length === 0,
95
+ reasons,
96
+ automaticPublication: false,
97
+ note: "Labels and side effects are reviewer-supplied evidence, not independently verified by this gate. Publication requires reviewed signed release evidence."
98
+ };
99
+ }
100
+ // Annotate the CommonJS export names for ESM import in node:
101
+ 0 && (module.exports = {
102
+ assessFrictionRollout
103
+ });
@@ -0,0 +1,19 @@
1
+ export interface GatewayRuntime {
2
+ executable: string;
3
+ entry: string;
4
+ }
5
+ /**
6
+ * Keep wrappers already owned by the MSI on its maintained runtime when a
7
+ * developer invokes another CLI copy. Unknown/missing runtimes still converge
8
+ * to the invoking CLI; arbitrary existing executables are never trusted merely
9
+ * because they exist. Only the canonical packaged Windows installation qualifies.
10
+ */
11
+ export declare function resolveGatewayRuntime(current: GatewayRuntime, existing?: {
12
+ command?: unknown;
13
+ args?: unknown;
14
+ }, options?: {
15
+ platform?: NodeJS.Platform;
16
+ env?: NodeJS.ProcessEnv;
17
+ }): GatewayRuntime;
18
+ /** Hooks share the gateway's installed owner, including the older full entry. */
19
+ export declare function resolveHookRuntime(current: GatewayRuntime, existingCommand?: string): GatewayRuntime;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveGatewayRuntime = resolveGatewayRuntime;
37
+ exports.resolveHookRuntime = resolveHookRuntime;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ /**
41
+ * Keep wrappers already owned by the MSI on its maintained runtime when a
42
+ * developer invokes another CLI copy. Unknown/missing runtimes still converge
43
+ * to the invoking CLI; arbitrary existing executables are never trusted merely
44
+ * because they exist. Only the canonical packaged Windows installation qualifies.
45
+ */
46
+ function resolveGatewayRuntime(current, existing, options = {}) {
47
+ if ((options.platform || process.platform) !== 'win32')
48
+ return current;
49
+ const env = options.env || process.env;
50
+ const normalize = (value) => path.resolve(value).toLowerCase();
51
+ const existingArgs = Array.isArray(existing?.args) ? existing.args : [];
52
+ for (const base of [env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', env.ProgramFiles || 'C:\\Program Files']) {
53
+ const root = path.join(base, 'FullCourtDefense');
54
+ const installed = { executable: path.join(root, 'runtime', 'node.exe'), entry: path.join(root, 'dist', 'index.js') };
55
+ // A fresh development-only config remains local to that runtime. Once the
56
+ // installed daemon owns it, other copies preserve that owner on every pass.
57
+ const belongsToInstall = [current.entry, existingArgs[0]].some(value => typeof value === 'string' && normalize(value) === normalize(installed.entry));
58
+ if (!belongsToInstall)
59
+ continue;
60
+ try {
61
+ const files = [root, path.dirname(installed.executable), path.dirname(installed.entry), installed.executable, installed.entry, path.join(root, 'package.json')];
62
+ if (files.some(file => fs.lstatSync(file).isSymbolicLink()))
63
+ continue;
64
+ if (!fs.statSync(installed.executable).isFile() || !fs.statSync(installed.entry).isFile())
65
+ continue;
66
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
67
+ if (pkg.name !== 'fullcourtdefense-cli')
68
+ continue;
69
+ return installed;
70
+ }
71
+ catch { /* Missing installation: repair using the running CLI. */ }
72
+ }
73
+ return current;
74
+ }
75
+ /** Hooks share the gateway's installed owner, including the older full entry. */
76
+ function resolveHookRuntime(current, existingCommand) {
77
+ const invocation = existingCommand?.match(/^(?:"[^"]*"|\S+)\s+(?:"([^"]*)"|(\S+))\s+hook(?:\s|$)/);
78
+ const gatewayEntry = (entry) => path.basename(entry).toLowerCase() === 'hookslim.js'
79
+ ? path.join(path.dirname(entry), 'index.js') : entry;
80
+ const previousEntry = invocation?.[1] || invocation?.[2];
81
+ const mapped = { ...current, entry: gatewayEntry(current.entry) };
82
+ const owner = resolveGatewayRuntime(mapped, previousEntry ? { args: [gatewayEntry(previousEntry)] } : undefined);
83
+ if (owner === mapped)
84
+ return current;
85
+ const slim = path.join(path.dirname(owner.entry), 'hookSlim.js');
86
+ try {
87
+ const stat = fs.lstatSync(slim);
88
+ if (stat.isFile() && !stat.isSymbolicLink())
89
+ return { ...owner, entry: slim };
90
+ }
91
+ catch { /* Older installed releases use the full entry. */ }
92
+ return owner;
93
+ }
package/dist/hookSlim.js CHANGED
@@ -91,19 +91,28 @@ async function main() {
91
91
  const ipc = await (0, verdictIpcClient_1.requestDaemonVerdict)(args, raw, undefined, startedAt);
92
92
  if (ipc.outcome === 'verdict') {
93
93
  (0, hookIo_1.dbg)({ phase: 'ipc_verdict', event: args.event, exitCode: ipc.exitCode, entry: 'slim' });
94
- if (ipc.stderr) {
94
+ const verdict = { stdout: ipc.stdout || '', stderr: ipc.stderr || '', exitCode: ipc.exitCode ?? 0 };
95
+ // The daemon never opens a window; an `ask` carries a dialog envelope for
96
+ // THIS process (the developer's session). Loaded only when one is present,
97
+ // so the warm allow/deny path stays three files.
98
+ if (verdict.stdout.includes('"fcdAsk"')) {
99
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
100
+ const { emitVerdictForIde } = require('./askDialog');
101
+ await emitVerdictForIde(args, verdict, 'slim-ipc');
102
+ }
103
+ if (verdict.stderr) {
95
104
  try {
96
- process.stderr.write(ipc.stderr);
105
+ process.stderr.write(verdict.stderr);
97
106
  }
98
107
  catch { /* stderr gone */ }
99
108
  }
100
- if (ipc.stdout) {
109
+ if (verdict.stdout) {
101
110
  try {
102
- process.stdout.write(ipc.stdout);
111
+ process.stdout.write(verdict.stdout);
103
112
  }
104
113
  catch { /* stdout gone */ }
105
114
  }
106
- process.exit(ipc.exitCode ?? 0);
115
+ process.exit(verdict.exitCode);
107
116
  }
108
117
  (0, hookIo_1.dbg)({ phase: 'ipc_unavailable', event: args.event, detail: ipc.detail, entry: 'slim' });
109
118
  }
@@ -118,19 +127,9 @@ async function main() {
118
127
  // eslint-disable-next-line @typescript-eslint/no-var-requires
119
128
  const { markVerdictTiming } = require('./telemetry');
120
129
  const result = await markVerdictTiming({ startedAt, path: 'local' }, () => evaluateHookRequest(args, raw, loadConfig()));
121
- if (result.stderr) {
122
- try {
123
- process.stderr.write(result.stderr);
124
- }
125
- catch { /* stderr gone */ }
126
- }
127
- if (result.stdout) {
128
- try {
129
- process.stdout.write(result.stdout);
130
- }
131
- catch { /* stdout gone */ }
132
- }
133
- process.exit(result.exitCode);
130
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
131
+ const { emitVerdictForIde } = require('./askDialog');
132
+ await emitVerdictForIde(args, result, 'slim-local');
134
133
  }
135
134
  catch (err) {
136
135
  emitFailOpen(args.event, err instanceof Error ? err.message : String(err));
package/dist/index.js CHANGED
@@ -137,6 +137,13 @@ function printHelp() {
137
137
  Exits 1 when remnants are found. Works via npx after the CLI
138
138
  itself is uninstalled.
139
139
  doctor First step. Checks outbound HTTPS access to FullCourtDefense.
140
+ protection-check
141
+ Test supported action boundaries and report verified coverage.
142
+ protected-run
143
+ Launch with this computer's signed folder/site limits.
144
+ --inspect shows settings; --command "your-agent" starts it.
145
+ Missing OS prerequisites or expired settings stop the launch.
146
+ hotfix-status Show loaded detector revision, update state and fallback status.
140
147
  --perf measures this machine's real per-event overhead
141
148
  (hook latency, spawn floor, cmd-guard cost, disk footprint).
142
149
  --health prints an mdatp-health-style per-subsystem status
@@ -459,6 +466,7 @@ async function main() {
459
466
  apiKey: flags['api-key'],
460
467
  });
461
468
  const buildGatewayArgs = () => ({
469
+ mcpServerName: flags['mcp-server-name'],
462
470
  mcpCommand: flags['mcp-command'],
463
471
  mcpArgs: flags['mcp-args'],
464
472
  mcpUrl: flags['mcp-url'],
@@ -542,6 +550,26 @@ async function main() {
542
550
  printHelp();
543
551
  break;
544
552
  }
553
+ case 'protected-run': {
554
+ const { protectedRunCommand } = await Promise.resolve().then(() => __importStar(require('./commands/protectedRun')));
555
+ await protectedRunCommand(config, flags.command, flags.inspect === 'true');
556
+ break;
557
+ }
558
+ case 'protection-check': {
559
+ const { runBoundaryVerification } = await Promise.resolve().then(() => __importStar(require('./boundaryVerification')));
560
+ const report = await runBoundaryVerification();
561
+ if (flags.json === 'true')
562
+ console.log(JSON.stringify(report, null, 2));
563
+ else {
564
+ console.log('Action Protection — local checker verification');
565
+ console.log('Private sample policy; no submitted command or tool is executed.');
566
+ for (const check of report.checks)
567
+ console.log(`${check.boundary}: ${check.status} (${check.allowed} allowed sample, ${check.denied} blocked sample)`);
568
+ console.log('This checks the CLI, not IDE integration or OS containment. Use --json for the full report.');
569
+ }
570
+ process.exitCode = report.checks.some(check => check.status === 'failed') ? 1 : 0;
571
+ break;
572
+ }
545
573
  case 'doctor': {
546
574
  const args = {
547
575
  apiUrl: flags['api-url'],
@@ -553,6 +581,13 @@ async function main() {
553
581
  await doctorCommand(args, config);
554
582
  break;
555
583
  }
584
+ case 'hotfix-status': {
585
+ const { localDetectorUpdates } = await Promise.resolve().then(() => __importStar(require('./localDetectionUpdates')));
586
+ const { cliVersion } = await Promise.resolve().then(() => __importStar(require('./cliVersion')));
587
+ console.log(JSON.stringify({ cliVersion: cliVersion(), detector: localDetectorUpdates.getDetectorStatus(),
588
+ note: 'Acceptance is not proof of correction. Compare the reported action and prohibited counterparts under the same policy.' }, null, 2));
589
+ break;
590
+ }
556
591
  case 'demo-actions': {
557
592
  const { demoActionsCommand } = await Promise.resolve().then(() => __importStar(require('./commands/demoActions')));
558
593
  await demoActionsCommand({ live: flags.live, json: flags.json, demoPolicies: flags['demo-policies'], timeout: flags.timeout }, config);
@@ -1,2 +1,4 @@
1
1
  import { DetectionUpdates } from './detectionUpdates';
2
- export declare const localDetectionUpdates: DetectionUpdates;
2
+ import { DetectorUpdates } from './detectorUpdates';
3
+ export declare const localDetectionUpdates: DetectionUpdates<import("./detectionData").DetectionData>;
4
+ export declare const localDetectorUpdates: DetectorUpdates;
@@ -33,9 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.localDetectionUpdates = void 0;
36
+ exports.localDetectorUpdates = exports.localDetectionUpdates = void 0;
37
37
  const os = __importStar(require("os"));
38
38
  const path = __importStar(require("path"));
39
39
  const detectionUpdates_1 = require("./detectionUpdates");
40
+ const detectorUpdates_1 = require("./detectorUpdates");
41
+ const machineIdentity_1 = require("./machineIdentity");
40
42
  exports.localDetectionUpdates = new detectionUpdates_1.DetectionUpdates(path.join(os.homedir(), '.fullcourtdefense', 'detection-update.json'));
41
43
  exports.localDetectionUpdates.loadLocal();
44
+ exports.localDetectorUpdates = new detectorUpdates_1.DetectorUpdates(path.join(os.homedir(), '.fullcourtdefense', 'detector-update-v1.json'), undefined, 'stable', (0, machineIdentity_1.getMachineIdentity)().machineId);
45
+ exports.localDetectorUpdates.loadLocal();
46
+ // Long-running gateways observe the daemon's accepted cache without a restart.
47
+ // This timer is background-only and never keeps a short-lived hook alive.
48
+ setInterval(() => { exports.localDetectionUpdates.loadLocal(); exports.localDetectorUpdates.loadLocal(); }, 30_000).unref();
@@ -17,6 +17,8 @@ export interface LocalSafetySnapshotInput {
17
17
  machineName: string;
18
18
  expectedPolicyHash?: string;
19
19
  timeoutMs?: number;
20
+ /** Explicit background refresh must fetch and persist a verified replacement. */
21
+ requireServerValidation?: boolean;
20
22
  /**
21
23
  * Hot-path caller on a monitor/shadow machine: any cached snapshot wins
22
24
  * immediately (even stale / hash-mismatched) — the resident daemon owns
@@ -37,13 +39,15 @@ export declare function hookSnapshotIdentity(): {
37
39
  };
38
40
  export declare function clearLocalSafetySnapshotCache(input: Pick<LocalSafetySnapshotInput, 'apiUrl' | 'shieldId' | 'developerName' | 'machineName'>): void;
39
41
  /**
40
- * Remove EVERY Local Safety snapshot cache file, regardless of the identity
41
- * that keyed it. Used on (re-)enrollment and explicit policy refresh: cache
42
- * files are keyed by sha1(apiUrl|shieldId|developer|machine), so after a
42
+ * Remove Local Safety snapshot cache files, regardless of their identity key.
43
+ * Enrollment clears all; a verified refresh preserves the newly saved hash.
44
+ * Returns false when an old cache could not be removed, so remote refresh
45
+ * does not claim convergence while another surface can still use stale rules.
46
+ * Files are keyed by sha1(apiUrl|shieldId|developer|machine), so after a
43
47
  * shield/identity change a targeted unlink can miss files written under an
44
48
  * old key — leaving a stale (possibly enforcing) snapshot for whichever
45
49
  * surface still resolves that old identity.
46
50
  */
47
- export declare function clearAllLocalSafetySnapshotCaches(): void;
51
+ export declare function clearAllLocalSafetySnapshotCaches(preservePolicyHash?: string): boolean;
48
52
  export declare function loadLocalSafetySnapshot(input: LocalSafetySnapshotInput): Promise<LocalSafetySnapshot | undefined>;
49
- export declare function snapshotToScanOptions(snapshot: LocalSafetySnapshot | undefined, extra?: Pick<LocalSafetyScanOptions, 'cwd' | 'inspectScripts'>): LocalSafetyScanOptions;
53
+ export declare function snapshotToScanOptions(snapshot: LocalSafetySnapshot | undefined, extra?: Pick<LocalSafetyScanOptions, 'cwd' | 'inspectScripts' | 'mcpServerName'>): LocalSafetyScanOptions;
@@ -44,6 +44,7 @@ const os = __importStar(require("os"));
44
44
  const path = __importStar(require("path"));
45
45
  const deterministicGuard_1 = require("./commands/deterministicGuard");
46
46
  const honeypot_1 = require("./honeypot");
47
+ const distress_1 = require("./distress");
47
48
  const CACHE_TTL_MS = 60_000;
48
49
  const CACHE_DIR = path.join(os.homedir(), '.fullcourtdefense');
49
50
  function cachePath(input) {
@@ -55,9 +56,12 @@ function cachePath(input) {
55
56
  function readCache(file) {
56
57
  try {
57
58
  const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
58
- if (!parsed?.snapshot || typeof parsed.loadedAt !== 'number')
59
+ if (!parsed?.snapshot || typeof parsed.loadedAt !== 'number' || !Number.isFinite(parsed.loadedAt) || parsed.loadedAt < 0)
59
60
  return undefined;
60
- return parsed;
61
+ // Parseable JSON can still be a partial write or an invalid old cache.
62
+ // Apply the same policy validation used for a server replacement.
63
+ const snapshot = toSnapshot(parsed.snapshot);
64
+ return snapshot ? { loadedAt: parsed.loadedAt, snapshot } : undefined;
61
65
  }
62
66
  catch {
63
67
  return undefined;
@@ -72,7 +76,10 @@ function writeCache(file, snapshot) {
72
76
  fs.writeFileSync(tmp, JSON.stringify({ loadedAt: Date.now(), snapshot }, null, 2), { encoding: 'utf8', mode: 0o600 });
73
77
  fs.renameSync(tmp, file);
74
78
  }
75
- catch { /* best-effort */ }
79
+ catch (error) {
80
+ (0, distress_1.reportDistress)('shell_policy', distress_1.DISTRESS.POLICY_CACHE_WRITE_FAILED, 'Could not save the shell policy cache; refresh is incomplete');
81
+ throw error;
82
+ }
76
83
  }
77
84
  /**
78
85
  * Fallback when the derived cache key has no file: the freshest snapshot on
@@ -109,8 +116,14 @@ function toRuleAction(value) {
109
116
  function toSnapshot(data) {
110
117
  if (!data || typeof data !== 'object')
111
118
  return undefined;
112
- // Anything unrecognized falls back to 'block' (fail strict); recognized
113
- // entries — including explicit 'block' — are kept verbatim.
119
+ if (typeof data.policyHash !== 'string' || !data.policyHash.trim()
120
+ || !Array.isArray(data.disabledBuiltInItemIds) || data.disabledBuiltInItemIds.some((id) => typeof id !== 'string')
121
+ || !Array.isArray(data.customBlocks) || data.customBlocks.some((item) => !item || typeof item.pattern !== 'string' || !item.pattern
122
+ || (item.action !== undefined && !toRuleAction(item.action)))
123
+ || (data.itemActions !== undefined && (!data.itemActions || typeof data.itemActions !== 'object' || Array.isArray(data.itemActions)
124
+ || Object.values(data.itemActions).some(value => !toRuleAction(value)))))
125
+ return undefined;
126
+ // Invalid actions were rejected above; keep explicit 'block' overrides too.
114
127
  let itemActions;
115
128
  if (data.itemActions && typeof data.itemActions === 'object' && !Array.isArray(data.itemActions)) {
116
129
  for (const [itemId, raw] of Object.entries(data.itemActions)) {
@@ -179,33 +192,45 @@ function clearLocalSafetySnapshotCache(input) {
179
192
  catch { /* cache may not exist */ }
180
193
  }
181
194
  /**
182
- * Remove EVERY Local Safety snapshot cache file, regardless of the identity
183
- * that keyed it. Used on (re-)enrollment and explicit policy refresh: cache
184
- * files are keyed by sha1(apiUrl|shieldId|developer|machine), so after a
195
+ * Remove Local Safety snapshot cache files, regardless of their identity key.
196
+ * Enrollment clears all; a verified refresh preserves the newly saved hash.
197
+ * Returns false when an old cache could not be removed, so remote refresh
198
+ * does not claim convergence while another surface can still use stale rules.
199
+ * Files are keyed by sha1(apiUrl|shieldId|developer|machine), so after a
185
200
  * shield/identity change a targeted unlink can miss files written under an
186
201
  * old key — leaving a stale (possibly enforcing) snapshot for whichever
187
202
  * surface still resolves that old identity.
188
203
  */
189
- function clearAllLocalSafetySnapshotCaches() {
204
+ function clearAllLocalSafetySnapshotCaches(preservePolicyHash) {
205
+ let cleared = true;
190
206
  try {
191
207
  for (const name of fs.readdirSync(CACHE_DIR)) {
192
208
  if (/^local-safety-[0-9a-f]{40}\.json$/i.test(name)) {
209
+ if (preservePolicyHash && readCache(path.join(CACHE_DIR, name))?.snapshot.policyHash === preservePolicyHash)
210
+ continue;
193
211
  try {
194
212
  fs.unlinkSync(path.join(CACHE_DIR, name));
195
213
  }
196
- catch { /* best-effort */ }
214
+ catch (error) {
215
+ if (error.code !== 'ENOENT')
216
+ cleared = false;
217
+ }
197
218
  }
198
219
  }
199
220
  }
200
- catch { /* cache dir may not exist */ }
221
+ catch (error) {
222
+ if (error.code !== 'ENOENT')
223
+ cleared = false;
224
+ }
225
+ return cleared;
201
226
  }
202
227
  async function loadLocalSafetySnapshot(input) {
203
228
  const file = cachePath(input);
204
229
  const cached = readCache(file) || readFreshestAnyCache();
205
230
  const hashMatches = !input.expectedPolicyHash || cached?.snapshot.policyHash === input.expectedPolicyHash;
206
- if (cached && hashMatches && Date.now() - cached.loadedAt < CACHE_TTL_MS)
231
+ if (!input.requireServerValidation && cached && hashMatches && Date.now() - cached.loadedAt < CACHE_TTL_MS)
207
232
  return cached.snapshot;
208
- if (cached && input.preferCached)
233
+ if (!input.requireServerValidation && cached && input.preferCached)
209
234
  return cached.snapshot;
210
235
  const headers = { 'Content-Type': 'application/json' };
211
236
  if (input.shieldKey)
@@ -223,9 +248,9 @@ async function loadLocalSafetySnapshot(input) {
223
248
  });
224
249
  if (resp.ok) {
225
250
  const body = await resp.json().catch(() => ({}));
226
- if (body.success) {
251
+ if (body.success === true) {
227
252
  const snapshot = toSnapshot(body.data);
228
- if (snapshot) {
253
+ if (snapshot && (!input.requireServerValidation || !input.expectedPolicyHash || snapshot.policyHash === input.expectedPolicyHash)) {
229
254
  writeCache(file, snapshot);
230
255
  return snapshot;
231
256
  }
@@ -233,6 +258,8 @@ async function loadLocalSafetySnapshot(input) {
233
258
  }
234
259
  }
235
260
  catch { /* offline-safe fallback below */ }
261
+ if (input.requireServerValidation)
262
+ throw new Error('Shell policy refresh did not receive and save the expected snapshot; cached rules remain available');
236
263
  return cached?.snapshot;
237
264
  }
238
265
  function snapshotToScanOptions(snapshot, extra = {}) {
@@ -0,0 +1,17 @@
1
+ import { VerifiableMachineAction } from './machineActionVerify';
2
+ import { ToolTrustSettings } from './mcpToolTrust';
3
+ /** Retain the signed requirement even when the general bundle ages out offline. */
4
+ export declare class ToolReviewLease {
5
+ private machineId;
6
+ private shieldId;
7
+ private file;
8
+ private remembered?;
9
+ private loaded;
10
+ constructor(machineId: string, shieldId: string, apiUrl: string, directory?: string);
11
+ settings(candidate?: VerifiableMachineAction | null): ToolTrustSettings | undefined;
12
+ }
13
+ export declare function verifiedToolTrust(envelope: VerifiableMachineAction, machineId: string, shieldId: string): ToolTrustSettings;
14
+ /** Preserve every declared field and exhaust pagination before fingerprinting. */
15
+ export declare function readCompleteToolCatalog(request: (params: Record<string, unknown>) => Promise<unknown>): Promise<Array<Record<string, unknown> & {
16
+ name: string;
17
+ }>>;