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.
- package/README.md +1 -0
- package/dist/actionIdentity.d.ts +1 -0
- package/dist/actionIdentity.js +2 -0
- package/dist/actionPolicyEngine.d.ts +5 -0
- package/dist/actionPolicyEngine.js +273 -30
- package/dist/agentTerminal.d.ts +1 -3
- package/dist/agentTerminal.js +14 -29
- package/dist/approvalPrompt.d.ts +7 -0
- package/dist/approvalPrompt.js +46 -0
- package/dist/askDialog.d.ts +87 -0
- package/dist/askDialog.js +209 -0
- package/dist/blockExplanation.js +18 -6
- package/dist/boundaryVerification.d.ts +20 -0
- package/dist/boundaryVerification.js +147 -0
- package/dist/commands/daemon.js +198 -70
- package/dist/commands/deterministicGuard.d.ts +2 -0
- package/dist/commands/deterministicGuard.js +161 -9
- package/dist/commands/hook.js +81 -29
- package/dist/commands/installClaudeHook.js +11 -5
- package/dist/commands/installCursorHook.js +5 -4
- package/dist/commands/mcpGateway.d.ts +2 -0
- package/dist/commands/mcpGateway.js +163 -56
- package/dist/commands/onboard.js +6 -5
- package/dist/commands/onboardingSummary.d.ts +5 -0
- package/dist/commands/onboardingSummary.js +11 -0
- package/dist/commands/protectedRun.d.ts +14 -0
- package/dist/commands/protectedRun.js +207 -0
- package/dist/containment-runtime/index.mjs +30118 -0
- package/dist/containment-runtime/licenses/_anthropic-ai_sandbox-runtime.txt +201 -0
- package/dist/containment-runtime/licenses/_pondwader_socks5-server.txt +20 -0
- package/dist/containment-runtime/licenses/commander.txt +22 -0
- package/dist/containment-runtime/licenses/node-forge.txt +331 -0
- package/dist/containment-runtime/licenses/zod.txt +21 -0
- package/dist/containment-runtime/runtime-version.json +1 -0
- package/dist/containment-runtime/vendor/java-proxy-agent/build.ts +95 -0
- package/dist/containment-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
- package/dist/containment-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/containment-runtime/vendor/seccomp/build.ts +69 -0
- package/dist/containment-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/containment-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
- package/dist/containment-runtime/vendor/srt-win/build.ts +21 -0
- package/dist/containment-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
- package/dist/containmentLease.d.ts +5 -0
- package/dist/containmentLease.js +72 -0
- package/dist/detectionUpdates.d.ts +23 -3
- package/dist/detectionUpdates.js +45 -14
- package/dist/detectorArtifact.d.ts +17 -0
- package/dist/detectorArtifact.js +35 -0
- package/dist/detectorBaseline.d.ts +2 -0
- package/dist/detectorBaseline.js +5 -0
- package/dist/detectorModule.d.ts +9 -0
- package/dist/detectorModule.js +84 -0
- package/dist/detectorRuntime.d.ts +21 -0
- package/dist/detectorRuntime.js +49 -0
- package/dist/detectorUpdates.d.ts +21 -0
- package/dist/detectorUpdates.js +86 -0
- package/dist/devConfirm.d.ts +97 -15
- package/dist/devConfirm.js +447 -117
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/frictionRollout.js +103 -0
- package/dist/gatewayRuntime.d.ts +19 -0
- package/dist/gatewayRuntime.js +93 -0
- package/dist/hookSlim.js +17 -18
- package/dist/index.js +35 -0
- package/dist/localDetectionUpdates.d.ts +3 -1
- package/dist/localDetectionUpdates.js +8 -1
- package/dist/localSafetySnapshot.d.ts +9 -5
- package/dist/localSafetySnapshot.js +42 -15
- package/dist/mcpToolReview.d.ts +17 -0
- package/dist/mcpToolReview.js +127 -0
- package/dist/mcpToolTrust.d.ts +22 -0
- package/dist/mcpToolTrust.js +66 -0
- package/dist/nativeHook.d.ts +4 -0
- package/dist/nativeHook.js +31 -17
- package/dist/policyRefresh.d.ts +3 -0
- package/dist/policyRefresh.js +27 -0
- package/dist/protectionMaintenance.d.ts +10 -0
- package/dist/protectionMaintenance.js +64 -0
- package/dist/protectionProfile.d.ts +26 -0
- package/dist/protectionProfile.js +63 -0
- package/dist/runtimeConfig.d.ts +2 -0
- package/dist/runtimeConfig.js +65 -26
- package/dist/selfTest.d.ts +2 -0
- package/dist/selfTest.js +9 -1
- package/dist/sessionLimits.d.ts +2 -0
- package/dist/sessionLimits.js +24 -0
- package/dist/telemetry.d.ts +5 -0
- package/dist/telemetry.js +8 -0
- package/dist/version.json +1 -1
- 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
|
+
}
|
|
Binary file
|
|
@@ -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'))
|
|
Binary file
|
|
Binary file
|
|
@@ -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)
|
|
Binary file
|
|
@@ -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
|
-
|
|
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;
|
package/dist/detectionUpdates.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
59
|
-
return { ...this.status, revision, source: revision >
|
|
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 >
|
|
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
|
|
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
|
-
|
|
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' ?
|
|
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 >
|
|
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 =
|
|
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,
|
|
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 ===
|
|
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
|
-
(
|
|
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
|
+
}
|