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
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Build the JVM proxy agent (vendor/java-proxy-agent-src) into
3
+ * vendor/java-proxy-agent/srt-proxy-agent.jar.
4
+ *
5
+ * Same model as the seccomp / srt-win helpers: only the source is in git,
6
+ * the artifact is built by the release workflow and shipped in the npm
7
+ * package, and consumers that bundle srt point at their own copy via the
8
+ * `javaAgentJarPath` config key. The jar is reproducible: `jar --date`
9
+ * pins entry timestamps, and STORED entries in a fixed order.
10
+ *
11
+ * npm run build:java-agent (needs a JDK >= 17 on PATH or JAVA_HOME)
12
+ */
13
+ import { spawnSync } from 'node:child_process'
14
+ import {
15
+ mkdtempSync,
16
+ readdirSync,
17
+ rmSync,
18
+ statSync,
19
+ writeFileSync,
20
+ } from 'node:fs'
21
+ import { tmpdir } from 'node:os'
22
+ import { dirname, join } from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+ import { run } from '../build-common.js'
25
+
26
+ const here = dirname(fileURLToPath(import.meta.url))
27
+ const SRC = join(here, '..', 'java-proxy-agent-src')
28
+ const OUT = join(here, 'srt-proxy-agent.jar')
29
+ const AGENT_CLASS = 'com.anthropic.srt.ProxyAgent'
30
+
31
+ const bin = (tool: string) =>
32
+ process.env.JAVA_HOME ? join(process.env.JAVA_HOME, 'bin', tool) : tool
33
+ const javac = bin('javac')
34
+ const jar = bin('jar')
35
+ const version = spawnSync(javac, ['-version'], { encoding: 'utf8' })
36
+ if (version.error) {
37
+ console.error(
38
+ `java-proxy-agent build: cannot run ${javac}: ${version.error.message}`,
39
+ )
40
+ process.exit(1)
41
+ }
42
+
43
+ function walk(dir: string): string[] {
44
+ const out: string[] = []
45
+ for (const name of readdirSync(dir).sort()) {
46
+ const p = join(dir, name)
47
+ if (statSync(p).isDirectory()) out.push(...walk(p))
48
+ else out.push(p)
49
+ }
50
+ return out
51
+ }
52
+
53
+ const work = mkdtempSync(join(tmpdir(), 'srt-java-agent-'))
54
+ try {
55
+ const classes = join(work, 'classes')
56
+ const sources = walk(SRC).filter(f => f.endsWith('.java'))
57
+ // --release 8: the agent runs inside whatever JVM the sandboxed tool
58
+ // brings, and Java 8 is still common in build tooling.
59
+ run([
60
+ javac,
61
+ '--release',
62
+ '8',
63
+ '-Xlint:all,-options',
64
+ '-Werror',
65
+ '-g:none',
66
+ '-d',
67
+ classes,
68
+ ...sources,
69
+ ])
70
+
71
+ const manifest = join(work, 'MANIFEST.MF')
72
+ writeFileSync(
73
+ manifest,
74
+ `Premain-Class: ${AGENT_CLASS}\nAgent-Class: ${AGENT_CLASS}\n`,
75
+ )
76
+ // -0: STORED (no compression). --date: fixed entry timestamps. Entries
77
+ // are added in a fixed order (walk() sorts) so the bytes are reproducible.
78
+ const entries = walk(classes).map(f => f.slice(classes.length + 1))
79
+ run([
80
+ jar,
81
+ '--create',
82
+ '--file',
83
+ OUT,
84
+ '--manifest',
85
+ manifest,
86
+ '--no-compress',
87
+ '--date',
88
+ '1980-01-01T00:00:02Z',
89
+ // -C applies to the single following file, so repeat it per entry.
90
+ ...entries.flatMap(e => ['-C', classes, e]),
91
+ ])
92
+ console.log(`built ${OUT} (${statSync(OUT).size} bytes)`)
93
+ } finally {
94
+ rmSync(work, { recursive: true, force: true })
95
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { run, setup } from '../build-common.js'
4
+
5
+ const { SRC, OUT } = setup({
6
+ importMetaUrl: import.meta.url,
7
+ requirePlatform: 'linux',
8
+ srcDirName: 'seccomp-src',
9
+ })
10
+
11
+ function toCArray(bytes: Buffer): string {
12
+ const hex = Array.from(bytes, b => '0x' + b.toString(16).padStart(2, '0'))
13
+ const lines: string[] = []
14
+ for (let i = 0; i < hex.length; i += 8) {
15
+ lines.push(' ' + hex.slice(i, i + 8).join(', ') + ',')
16
+ }
17
+ return lines.join('\n')
18
+ }
19
+
20
+ const cflags = ['-static', '-O2', '-Wall', '-Wextra']
21
+
22
+ const gen = join(OUT, 'seccomp-unix-block')
23
+ run([
24
+ 'gcc',
25
+ ...cflags,
26
+ '-o',
27
+ gen,
28
+ join(SRC, 'seccomp-unix-block.c'),
29
+ '-lseccomp',
30
+ ])
31
+
32
+ const bpf: Record<string, Buffer> = {}
33
+ for (const target of ['x86_64', 'aarch64']) {
34
+ const tmp = join(OUT, target + '.bpf')
35
+ run([gen, tmp, target])
36
+ bpf[target] = readFileSync(tmp)
37
+ rmSync(tmp)
38
+ }
39
+ rmSync(gen)
40
+
41
+ const header = join(OUT, 'unix-block-bpf.h')
42
+ writeFileSync(
43
+ header,
44
+ '#if defined(__x86_64__)\n' +
45
+ 'static const unsigned char unix_block_bpf[] = {\n' +
46
+ toCArray(bpf.x86_64) +
47
+ '\n};\n' +
48
+ '#elif defined(__aarch64__)\n' +
49
+ 'static const unsigned char unix_block_bpf[] = {\n' +
50
+ toCArray(bpf.aarch64) +
51
+ '\n};\n' +
52
+ '#else\n' +
53
+ '#error "unsupported architecture for unix-block BPF filter"\n' +
54
+ '#endif\n',
55
+ )
56
+
57
+ run([
58
+ 'gcc',
59
+ ...cflags,
60
+ '-I',
61
+ OUT,
62
+ '-o',
63
+ join(OUT, 'apply-seccomp'),
64
+ join(SRC, 'apply-seccomp.c'),
65
+ ])
66
+ run(['strip', join(OUT, 'apply-seccomp')])
67
+ rmSync(header)
68
+
69
+ console.log('built ' + join(OUT, 'apply-seccomp'))
@@ -0,0 +1,21 @@
1
+ import { copyFileSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { run, setup } from '../build-common.js'
4
+
5
+ const { SRC, OUT } = setup({
6
+ importMetaUrl: import.meta.url,
7
+ requirePlatform: 'win32',
8
+ srcDirName: 'srt-win-src',
9
+ })
10
+
11
+ run(['cargo', 'build', '--release', '--manifest-path', join(SRC, 'Cargo.toml')])
12
+
13
+ const built = join(SRC, 'target', 'release', 'srt-win.exe')
14
+ if (!existsSync(built)) {
15
+ console.error('srt-win build: expected output not found at ' + built)
16
+ process.exit(1)
17
+ }
18
+
19
+ const dest = join(OUT, 'srt-win.exe')
20
+ copyFileSync(built, dest)
21
+ console.log('built ' + dest)
@@ -0,0 +1,5 @@
1
+ /** Windows uses a shared sandbox SID: never overlap FCD profile sessions. */
2
+ export declare function acquireContainmentLease(): Promise<{
3
+ release: () => Promise<void>;
4
+ lost: Promise<void>;
5
+ }>;
@@ -0,0 +1,72 @@
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.acquireContainmentLease = acquireContainmentLease;
37
+ const path = __importStar(require("path"));
38
+ const child_process_1 = require("child_process");
39
+ /** Windows uses a shared sandbox SID: never overlap FCD profile sessions. */
40
+ async function acquireContainmentLease() {
41
+ if (process.platform !== 'win32')
42
+ return { release: async () => { }, lost: new Promise(() => { }) };
43
+ const script = "$m = $null; $held = $false; try { $m = [Threading.Mutex]::new($false, 'Global\\FullCourtDefenseContainedExecution'); try { $held = $m.WaitOne(0) } catch [Threading.AbandonedMutexException] { $held = $true }; if (-not $held) { exit 2 }; [Console]::Out.WriteLine('LOCKED'); [Console]::Out.Flush(); [Console]::In.ReadLine() | Out-Null } finally { if ($held) { $m.ReleaseMutex() }; if ($m) { $m.Dispose() } }";
44
+ const executable = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
45
+ const child = (0, child_process_1.spawn)(executable, ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { windowsHide: true, stdio: ['pipe', 'pipe', 'ignore'] });
46
+ let ended = false;
47
+ let signalLoss;
48
+ const lost = new Promise(resolve => { signalLoss = resolve; });
49
+ child.stdin.on('error', () => { child.kill(); });
50
+ child.once('close', () => { ended = true; signalLoss(); });
51
+ await new Promise((resolve, reject) => {
52
+ let text = '';
53
+ const timer = setTimeout(() => { child.kill(); reject(new Error('Could not obtain the Windows containment lock.')); }, 10000);
54
+ child.once('error', () => { clearTimeout(timer); reject(new Error('Windows containment lock is unavailable.')); });
55
+ child.once('close', () => { clearTimeout(timer); reject(new Error('Another protected session is running, or the Windows containment lock is unavailable.')); });
56
+ child.stdout.on('data', data => { text += data; if (text.includes('LOCKED\r\n') || text.includes('LOCKED\n')) {
57
+ clearTimeout(timer);
58
+ resolve();
59
+ } });
60
+ });
61
+ let releasing;
62
+ return { lost, release: () => {
63
+ if (!releasing)
64
+ releasing = (async () => {
65
+ if (ended)
66
+ return;
67
+ child.stdin.end('\n');
68
+ await lost;
69
+ })();
70
+ return releasing;
71
+ } };
72
+ }
@@ -1,12 +1,31 @@
1
+ import { verifyDetectionEnvelope } from './detectionData';
2
+ export interface SignedUpdateAdapter<T extends {
3
+ revision: number;
4
+ }> {
5
+ url: string;
6
+ maxBytes: number;
7
+ bundledRevision: number;
8
+ current(): Readonly<T>;
9
+ verify(raw: string, now: number, key: string): T;
10
+ activate(data: T): boolean;
11
+ eligible?(data: T): boolean;
12
+ /** Runs only for eligible, newer content, before it replaces the cache. */
13
+ prepare?(data: T): {
14
+ activate(): boolean;
15
+ dispose(): void;
16
+ };
17
+ }
1
18
  export declare const DETECTION_UPDATE_URL = "https://storage.googleapis.com/fullcourtdefense-cli-releases/detection/stable.json";
2
19
  export interface DetectionUpdateStatus {
3
20
  revision: number;
4
21
  bundledRevision: number;
5
22
  source: 'bundled' | 'update';
6
23
  lastCheckedAt?: string;
7
- state: 'bundled' | 'current' | 'unavailable' | 'rejected';
24
+ state: 'bundled' | 'current' | 'unavailable' | 'rejected' | 'not_targeted';
8
25
  }
9
- export declare class DetectionUpdates {
26
+ export declare class DetectionUpdates<T extends {
27
+ revision: number;
28
+ } = ReturnType<typeof verifyDetectionEnvelope>> {
10
29
  private readonly cachePath;
11
30
  private readonly publicKey;
12
31
  private readonly channel;
@@ -14,7 +33,8 @@ export declare class DetectionUpdates {
14
33
  private nextLocalRead;
15
34
  private pending?;
16
35
  private status;
17
- constructor(cachePath: string, publicKey?: string, channel?: 'stable' | 'staging');
36
+ private readonly adapter;
37
+ constructor(cachePath: string, publicKey?: string, channel?: 'stable' | 'staging', adapter?: SignedUpdateAdapter<T>);
18
38
  getStatus(): DetectionUpdateStatus;
19
39
  private readCache;
20
40
  private cachedData;
@@ -49,18 +49,23 @@ class DetectionUpdates {
49
49
  pending;
50
50
  status = { revision: detectionData_1.BUNDLED_DETECTION_DATA.revision,
51
51
  bundledRevision: detectionData_1.BUNDLED_DETECTION_DATA.revision, source: 'bundled', state: 'bundled' };
52
- constructor(cachePath, publicKey = detectionData_1.DETECTION_PUBLIC_KEY, channel = 'stable') {
52
+ adapter;
53
+ constructor(cachePath, publicKey = detectionData_1.DETECTION_PUBLIC_KEY, channel = 'stable', adapter) {
53
54
  this.cachePath = cachePath;
54
55
  this.publicKey = publicKey;
55
56
  this.channel = channel;
57
+ this.adapter = adapter || { url: exports.DETECTION_UPDATE_URL, maxBytes: detectionData_1.MAX_DETECTION_BYTES,
58
+ bundledRevision: detectionData_1.BUNDLED_DETECTION_DATA.revision, current: detectionData_1.getDetectionData,
59
+ verify: detectionData_1.verifyDetectionEnvelope, activate: detectionData_1.activateDetectionData };
60
+ this.status.revision = this.status.bundledRevision = this.adapter.bundledRevision;
56
61
  }
57
62
  getStatus() {
58
- const revision = (0, detectionData_1.getDetectionData)().revision;
59
- return { ...this.status, revision, source: revision > detectionData_1.BUNDLED_DETECTION_DATA.revision ? 'update' : 'bundled' };
63
+ const revision = this.adapter.current().revision;
64
+ return { ...this.status, revision, source: revision > this.adapter.bundledRevision ? 'update' : 'bundled' };
60
65
  }
61
66
  readCache() {
62
67
  try {
63
- if (fs.statSync(this.cachePath).size > detectionData_1.MAX_DETECTION_BYTES)
68
+ if (fs.statSync(this.cachePath).size > this.adapter.maxBytes)
64
69
  return undefined;
65
70
  return fs.readFileSync(this.cachePath, 'utf8');
66
71
  }
@@ -72,7 +77,7 @@ class DetectionUpdates {
72
77
  // Expiration gates first acceptance, not continued offline use. Authenticate
73
78
  // the signed validity period before using the last verified cache after restart.
74
79
  const payload = JSON.parse(JSON.parse(raw).payload);
75
- return (0, detectionData_1.verifyDetectionEnvelope)(raw, Math.min(Date.now(), payload.issuedAt), this.publicKey);
80
+ return this.adapter.verify(raw, Math.min(Date.now(), payload.issuedAt), this.publicKey);
76
81
  }
77
82
  loadLocal() {
78
83
  if (Date.now() < this.nextLocalRead)
@@ -82,7 +87,24 @@ class DetectionUpdates {
82
87
  if (!raw)
83
88
  return;
84
89
  try {
85
- (0, detectionData_1.activateDetectionData)(this.cachedData(raw));
90
+ const data = this.cachedData(raw);
91
+ if (this.adapter.eligible && !this.adapter.eligible(data)) {
92
+ this.status.state = 'not_targeted';
93
+ return;
94
+ }
95
+ if (data.revision === this.adapter.current().revision && JSON.stringify(data) !== JSON.stringify(this.adapter.current()))
96
+ throw new Error('Cached revision reused for different content');
97
+ if (data.revision > this.adapter.current().revision) {
98
+ const prepared = this.adapter.prepare?.(data);
99
+ try {
100
+ prepared ? prepared.activate() : this.adapter.activate(data);
101
+ }
102
+ finally {
103
+ prepared?.dispose();
104
+ }
105
+ }
106
+ if (data.revision === this.adapter.current().revision)
107
+ this.status.state = 'current';
86
108
  }
87
109
  catch {
88
110
  this.status.state = 'rejected';
@@ -102,7 +124,7 @@ class DetectionUpdates {
102
124
  this.status.lastCheckedAt = new Date().toISOString();
103
125
  let raw;
104
126
  try {
105
- const response = await fetch(this.channel === 'staging' ? exports.DETECTION_UPDATE_URL.replace('/stable.json', '/staging.json') : exports.DETECTION_UPDATE_URL, { redirect: 'error', signal: AbortSignal.timeout(3000) });
127
+ const response = await fetch(this.channel === 'staging' ? this.adapter.url.replace('/stable.json', '/staging.json') : this.adapter.url, { redirect: 'error', signal: AbortSignal.timeout(3000) });
106
128
  if (!response.ok || !response.body)
107
129
  throw new Error('Unavailable');
108
130
  const reader = response.body.getReader();
@@ -114,7 +136,7 @@ class DetectionUpdates {
114
136
  if (done)
115
137
  break;
116
138
  size += value.byteLength;
117
- if (size > detectionData_1.MAX_DETECTION_BYTES)
139
+ if (size > this.adapter.maxBytes)
118
140
  throw new Error('Oversized');
119
141
  chunks.push(value);
120
142
  }
@@ -131,8 +153,13 @@ class DetectionUpdates {
131
153
  }
132
154
  let lock;
133
155
  let temporary;
156
+ let prepared;
134
157
  try {
135
- const data = (0, detectionData_1.verifyDetectionEnvelope)(raw, Date.now(), this.publicKey);
158
+ const data = this.adapter.verify(raw, Date.now(), this.publicKey);
159
+ if (this.adapter.eligible && !this.adapter.eligible(data)) {
160
+ this.status.state = 'not_targeted';
161
+ return;
162
+ }
136
163
  fs.mkdirSync(path.dirname(this.cachePath), { recursive: true });
137
164
  const lockPath = this.cachePath + '.lock';
138
165
  try {
@@ -160,14 +187,15 @@ class DetectionUpdates {
160
187
  }
161
188
  catch { /* recover corrupt cache */ }
162
189
  const cachedRevision = cached?.revision ?? 0;
163
- const currentRevision = Math.max(cachedRevision, (0, detectionData_1.getDetectionData)().revision);
190
+ const currentRevision = Math.max(cachedRevision, this.adapter.current().revision);
164
191
  if (data.revision < currentRevision)
165
192
  throw new Error('Revision downgrade');
166
193
  if (cached && data.revision === cached.revision && JSON.stringify(data) !== JSON.stringify(cached))
167
194
  throw new Error('Cached revision reused for different content');
168
- if (data.revision === (0, detectionData_1.getDetectionData)().revision && JSON.stringify(data) !== JSON.stringify((0, detectionData_1.getDetectionData)()))
195
+ if (data.revision === this.adapter.current().revision && JSON.stringify(data) !== JSON.stringify(this.adapter.current()))
169
196
  throw new Error('Revision reused for different content');
170
197
  if (data.revision > currentRevision) {
198
+ prepared = this.adapter.prepare?.(data);
171
199
  temporary = this.cachePath + '.' + (0, crypto_1.randomUUID)() + '.tmp';
172
200
  const fd = fs.openSync(temporary, 'wx', 0o600);
173
201
  try {
@@ -179,10 +207,12 @@ class DetectionUpdates {
179
207
  }
180
208
  fs.renameSync(temporary, this.cachePath);
181
209
  temporary = undefined;
182
- (0, detectionData_1.activateDetectionData)(data);
210
+ prepared ? prepared.activate() : this.adapter.activate(data);
211
+ }
212
+ else if (cached && cached.revision > this.adapter.current().revision) {
213
+ prepared = this.adapter.prepare?.(cached);
214
+ prepared ? prepared.activate() : this.adapter.activate(cached);
183
215
  }
184
- else if (cached)
185
- (0, detectionData_1.activateDetectionData)(cached);
186
216
  this.status.state = 'current';
187
217
  }
188
218
  catch {
@@ -190,6 +220,7 @@ class DetectionUpdates {
190
220
  this.nextCheck = Date.now() + 5 * 60_000;
191
221
  }
192
222
  finally {
223
+ prepared?.dispose();
193
224
  if (temporary) {
194
225
  try {
195
226
  fs.unlinkSync(temporary);
@@ -0,0 +1,17 @@
1
+ export declare const DETECTOR_ABI = 1;
2
+ export declare const MAX_DETECTOR_BYTES: number;
3
+ export declare const DETECTOR_DOMAIN = "fullcourtdefense/reviewed-detector/v1\n";
4
+ export interface DetectorArtifact {
5
+ abi: 1;
6
+ revision: number;
7
+ issuedAt: number;
8
+ expiresAt: number;
9
+ sourceCommit: string;
10
+ issueNumber: number;
11
+ sha256: string;
12
+ code: string;
13
+ /** null = approved fleet-wide rollout; otherwise explicit enrolled machine IDs. */
14
+ machineIds: string[] | null;
15
+ }
16
+ /** Official reviewed code, not user-supplied rules or tool content. */
17
+ export declare function verifyDetectorArtifact(raw: string, now?: number, publicKey?: string): DetectorArtifact;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DETECTOR_DOMAIN = exports.MAX_DETECTOR_BYTES = exports.DETECTOR_ABI = void 0;
4
+ exports.verifyDetectorArtifact = verifyDetectorArtifact;
5
+ const crypto_1 = require("crypto");
6
+ const detectionData_1 = require("./detectionData");
7
+ exports.DETECTOR_ABI = 1;
8
+ exports.MAX_DETECTOR_BYTES = 2 * 1024 * 1024;
9
+ exports.DETECTOR_DOMAIN = 'fullcourtdefense/reviewed-detector/v1\n';
10
+ /** Official reviewed code, not user-supplied rules or tool content. */
11
+ function verifyDetectorArtifact(raw, now = Date.now(), publicKey = detectionData_1.DETECTION_PUBLIC_KEY) {
12
+ if (Buffer.byteLength(raw) > exports.MAX_DETECTOR_BYTES)
13
+ throw new Error('Detector artifact oversized');
14
+ const envelope = JSON.parse(raw);
15
+ if (!envelope || Object.keys(envelope).sort().join(',') !== 'payload,signature' || typeof envelope.payload !== 'string'
16
+ || typeof envelope.signature !== 'string' || !/^[A-Za-z0-9+/]{86}==$/.test(envelope.signature))
17
+ throw new Error('Invalid detector envelope');
18
+ if (!(0, crypto_1.verify)(null, Buffer.from(exports.DETECTOR_DOMAIN + envelope.payload), (0, crypto_1.createPublicKey)(publicKey), Buffer.from(envelope.signature, 'base64')))
19
+ throw new Error('Invalid detector signature');
20
+ const data = JSON.parse(envelope.payload);
21
+ if (!data || Object.keys(data).sort().join(',') !== 'abi,code,expiresAt,issueNumber,issuedAt,machineIds,revision,sha256,sourceCommit'
22
+ || data.abi !== exports.DETECTOR_ABI || !Number.isSafeInteger(data.revision) || data.revision < 1
23
+ || !Number.isSafeInteger(data.issueNumber) || data.issueNumber < 1
24
+ || typeof data.sourceCommit !== 'string' || !/^[a-f0-9]{40}$/.test(data.sourceCommit) || typeof data.code !== 'string' || !data.code
25
+ || typeof data.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(data.sha256)
26
+ || (0, crypto_1.createHash)('sha256').update(data.code).digest('hex') !== data.sha256
27
+ || !Number.isSafeInteger(data.issuedAt) || !Number.isSafeInteger(data.expiresAt)
28
+ || data.issuedAt > now + 60_000 || data.expiresAt <= now || data.expiresAt <= data.issuedAt
29
+ || data.expiresAt - data.issuedAt > 90 * 86400_000)
30
+ throw new Error('Invalid detector artifact or compatibility');
31
+ if (data.machineIds !== null && (!Array.isArray(data.machineIds) || data.machineIds.length < 1 || data.machineIds.length > 64
32
+ || data.machineIds.some(id => typeof id !== 'string' || !/^[a-zA-Z0-9_.:-]{1,64}$/.test(id)) || new Set(data.machineIds).size !== data.machineIds.length))
33
+ throw new Error('Invalid detector rollout cohort');
34
+ return data;
35
+ }