fullcourtdefense-cli 1.36.3 → 1.37.0
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/dist/actionPolicyEngine.d.ts +19 -0
- package/dist/actionPolicyEngine.js +147 -0
- package/dist/askDialog.d.ts +1 -0
- package/dist/askDialog.js +20 -3
- package/dist/callerEnv.d.ts +29 -2
- package/dist/callerEnv.js +48 -2
- package/dist/commandNarrative.js +87 -7
- package/dist/commands/daemon.d.ts +10 -0
- package/dist/commands/daemon.js +348 -170
- package/dist/commands/desktopChatGuard.js +2 -0
- package/dist/commands/deterministicGuard.d.ts +11 -0
- package/dist/commands/deterministicGuard.js +445 -28
- package/dist/commands/discoverPaths.d.ts +12 -9
- package/dist/commands/discoverPaths.js +30 -0
- package/dist/commands/ephemeralStack.js +7 -0
- package/dist/commands/flushSpool.d.ts +2 -0
- package/dist/commands/flushSpool.js +9 -2
- package/dist/commands/hook.d.ts +14 -0
- package/dist/commands/hook.js +302 -101
- package/dist/commands/httpGatewayCommand.d.ts +1 -0
- package/dist/commands/httpGatewayCommand.js +7 -1
- package/dist/commands/installClaudeHook.js +32 -10
- package/dist/commands/installCursorHook.js +20 -9
- package/dist/commands/login.js +8 -1
- package/dist/commands/mcpGateway.d.ts +21 -0
- package/dist/commands/mcpGateway.js +90 -32
- package/dist/commands/refreshPolicyCache.d.ts +28 -0
- package/dist/commands/refreshPolicyCache.js +59 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +30 -5
- package/dist/config.d.ts +36 -0
- package/dist/config.js +78 -12
- package/dist/daemonDetectorBoot.d.ts +1 -0
- package/dist/daemonDetectorBoot.js +12 -0
- package/dist/daemonForensics.d.ts +14 -1
- package/dist/daemonForensics.js +32 -0
- package/dist/detectorInProcess.d.ts +18 -0
- package/dist/detectorInProcess.js +234 -0
- package/dist/detectorInThread.d.ts +15 -0
- package/dist/detectorInThread.js +129 -0
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/fileWriteCanon.d.ts +17 -0
- package/dist/fileWriteCanon.js +141 -0
- package/dist/gateReports.d.ts +144 -0
- package/dist/gateReports.js +488 -0
- package/dist/hookIo.d.ts +59 -0
- package/dist/hookIo.js +319 -9
- package/dist/hookSlim.d.ts +15 -8
- package/dist/hookSlim.js +187 -25
- package/dist/housekeeping.d.ts +64 -0
- package/dist/housekeeping.js +384 -0
- package/dist/housekeepingOps.d.ts +150 -0
- package/dist/housekeepingOps.js +218 -0
- package/dist/housekeepingWorker.d.ts +1 -0
- package/dist/housekeepingWorker.js +113 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -0
- package/dist/localDetectionData.d.ts +10 -0
- package/dist/localDetectionData.js +48 -0
- package/dist/localDetectionUpdates.d.ts +2 -2
- package/dist/localDetectionUpdates.js +4 -4
- package/dist/localSafetySnapshot.d.ts +48 -2
- package/dist/localSafetySnapshot.js +241 -25
- package/dist/machineIdentity.d.ts +7 -0
- package/dist/machineIdentity.js +20 -2
- package/dist/monitorFastPath.d.ts +191 -0
- package/dist/monitorFastPath.js +1198 -0
- package/dist/nativeHook.js +9 -2
- package/dist/pendingDrain.d.ts +82 -0
- package/dist/pendingDrain.js +215 -0
- package/dist/pendingDrainer.d.ts +30 -0
- package/dist/pendingDrainer.js +217 -0
- package/dist/perfSnapshot.d.ts +1 -1
- package/dist/perfSnapshot.js +28 -4
- package/dist/policyCacheFiles.d.ts +73 -0
- package/dist/policyCacheFiles.js +320 -0
- package/dist/policyGateHealth.d.ts +8 -0
- package/dist/policyGateHealth.js +17 -0
- package/dist/policyRefresh.d.ts +28 -0
- package/dist/policyRefresh.js +69 -0
- package/dist/protectionMaintenance.d.ts +15 -0
- package/dist/protectionMaintenance.js +43 -1
- package/dist/runtimeConfig.d.ts +77 -20
- package/dist/runtimeConfig.js +278 -57
- package/dist/selfTest.d.ts +6 -1
- package/dist/selfTest.js +11 -4
- package/dist/telemetry.d.ts +136 -4
- package/dist/telemetry.js +493 -96
- package/dist/verdictIpc.d.ts +60 -0
- package/dist/verdictIpc.js +504 -19
- package/dist/verdictIpcClient.d.ts +175 -18
- package/dist/verdictIpcClient.js +277 -59
- package/dist/version.json +1 -1
- package/package.json +2 -2
|
@@ -84,27 +84,39 @@ async function watchdogCommand(args, config) {
|
|
|
84
84
|
// with the alive-marker freshness + a verdict-pipe probe.
|
|
85
85
|
let health = state.alive ? 'healthy' : 'dead';
|
|
86
86
|
if (state.alive && state.pid) {
|
|
87
|
+
const probe = await (0, verdictIpcClient_1.probeVerdictServerDetail)(1_500);
|
|
87
88
|
health = (0, daemonForensics_1.classifyDaemonHealth)({
|
|
88
89
|
pidAlive: true,
|
|
89
90
|
pid: state.pid,
|
|
90
91
|
marker: (0, daemonForensics_1.readAliveMarker)(),
|
|
91
92
|
staleAfterMs: (0, daemonForensics_1.daemonHungAfterMs)(),
|
|
92
|
-
pipeProbe:
|
|
93
|
+
pipeProbe: probe.result,
|
|
94
|
+
pipeAuthorized: probe.authorized,
|
|
93
95
|
machineSuspended,
|
|
94
96
|
});
|
|
95
97
|
}
|
|
98
|
+
// The daemon repairs a replaced token itself within a minute (verdictIpc
|
|
99
|
+
// checkToken). Still rejecting hooks one tick later (>= 5 min): recycle it
|
|
100
|
+
// like a hung daemon so the restart publishes a fresh token.
|
|
101
|
+
const notServingTicks = (0, daemonForensics_1.recordNotServing)(health === 'not_serving' ? state.pid : undefined);
|
|
102
|
+
if (health === 'not_serving' && notServingTicks === 1) {
|
|
103
|
+
(0, distress_1.reportDistress)('watchdog', distress_1.DISTRESS.IPC_NOT_SERVING, `daemon pid ${state.pid} rejects this profile's verdict token (token file replaced) — waiting one tick for its self-repair`);
|
|
104
|
+
}
|
|
105
|
+
const recycleNotServing = health === 'not_serving' && notServingTicks >= 2;
|
|
96
106
|
// An updater run is replacing the install: the daemon is down ON PURPOSE.
|
|
97
107
|
// Reviving the old build now puts a second daemon (and its update kick)
|
|
98
108
|
// in the middle of msiexec. Stand back; stage 2 relaunches the daemon when
|
|
99
109
|
// it finishes, and a lock older than its max age no longer counts, so the
|
|
100
110
|
// next tick revives normally if the run never completes.
|
|
101
111
|
const updaterRun = (0, selfUpdate_1.readActiveUpdaterRun)();
|
|
102
|
-
if (health === 'hung' && state.pid && !updaterRun && process.env.FCD_WATCHDOG_NO_RELAUNCH !== '1') {
|
|
112
|
+
if ((health === 'hung' || recycleNotServing) && state.pid && !updaterRun && process.env.FCD_WATCHDOG_NO_RELAUNCH !== '1') {
|
|
103
113
|
// Positive evidence this pid IS our daemon (it still owns the verdict
|
|
104
114
|
// endpoint) and it is wedged — recycle it so the relaunch below starts
|
|
105
115
|
// clean. The distress ledger survives; the fresh daemon's first heartbeat
|
|
106
116
|
// ships the story to the fleet.
|
|
107
|
-
(0, distress_1.reportDistress)('watchdog', distress_1.DISTRESS.
|
|
117
|
+
(0, distress_1.reportDistress)('watchdog', recycleNotServing ? distress_1.DISTRESS.IPC_NOT_SERVING : distress_1.DISTRESS.DAEMON_HUNG, recycleNotServing
|
|
118
|
+
? `daemon pid ${state.pid} kept rejecting this profile's verdict token for ${notServingTicks} ticks — recycled by watchdog`
|
|
119
|
+
: `daemon pid ${state.pid} unresponsive (alive-marker stale + verdict pipe silent) — recycled by watchdog`);
|
|
108
120
|
// Claim the kill first: otherwise the next daemon boots, finds a marker
|
|
109
121
|
// with no clean-exit stamp, and reports EDR/AV killed it.
|
|
110
122
|
(0, daemonForensics_1.recordWatchdogRecycle)(state.pid);
|
|
@@ -114,7 +126,7 @@ async function watchdogCommand(args, config) {
|
|
|
114
126
|
// kill it. The daemon is simply gone; fall through to the dead path. The
|
|
115
127
|
// relaunched daemon's own startup detects the recycled pid-file owner and
|
|
116
128
|
// takes over the lock without touching that process.
|
|
117
|
-
const effectivelyDead = !state.alive || health === 'hung' || health === 'pid_reused';
|
|
129
|
+
const effectivelyDead = !state.alive || health === 'hung' || health === 'pid_reused' || recycleNotServing;
|
|
118
130
|
// No post-mortem during an updater run either: a daemon the updater
|
|
119
131
|
// stopped was not killed by EDR/AV.
|
|
120
132
|
if (effectivelyDead && !updaterRun) {
|
|
@@ -151,7 +163,7 @@ async function watchdogCommand(args, config) {
|
|
|
151
163
|
daemonVersion: state.version,
|
|
152
164
|
daemonStartedAt: state.startedAt,
|
|
153
165
|
relaunched: relaunched || undefined,
|
|
154
|
-
daemonHealth: health === 'hung' || health === 'pid_reused' ? health : undefined,
|
|
166
|
+
daemonHealth: health === 'hung' || health === 'pid_reused' || health === 'not_serving' ? health : undefined,
|
|
155
167
|
lastCrash: crashSummary(postmortem),
|
|
156
168
|
};
|
|
157
169
|
const headers = { 'Content-Type': 'application/json' };
|
|
@@ -171,4 +183,17 @@ async function watchdogCommand(args, config) {
|
|
|
171
183
|
(0, daemonForensics_1.markPostmortemReported)();
|
|
172
184
|
}
|
|
173
185
|
catch { /* offline / endpoint not deployed yet — next tick retries */ }
|
|
186
|
+
// Policy freshness while the daemon is down. Hooks and gateways never fetch
|
|
187
|
+
// policy, so without the daemon's poll a suspension or a mode change would
|
|
188
|
+
// not reach this machine; this tick delivers it within one interval.
|
|
189
|
+
// Loaded lazily: a healthy tick never pays for the policy modules. Not
|
|
190
|
+
// during an updater run: the install is being replaced (the daemon is down
|
|
191
|
+
// on purpose), and hooks keep serving the last good cache meanwhile.
|
|
192
|
+
if (effectivelyDead && !updaterRun) {
|
|
193
|
+
try {
|
|
194
|
+
const { refreshPolicyCachesInBackground } = await Promise.resolve().then(() => __importStar(require('./refreshPolicyCache')));
|
|
195
|
+
await refreshPolicyCachesInBackground(creds, { scheduled: true });
|
|
196
|
+
}
|
|
197
|
+
catch { /* quiet: the next tick retries */ }
|
|
198
|
+
}
|
|
174
199
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export declare function getCredentialResolutionTrace(): CredentialResolutionTrac
|
|
|
113
113
|
export declare function resolveCliCredentials(config: BotGuardConfig, overrides?: Partial<ResolvedCliCredentials>, options?: {
|
|
114
114
|
skipDpapi?: boolean;
|
|
115
115
|
}): ResolvedCliCredentials;
|
|
116
|
+
/**
|
|
117
|
+
* Shield, endpoint and override key only — no key store is read. For verdict
|
|
118
|
+
* paths that decide from the offline cache and resolve the key
|
|
119
|
+
* (resolveCliCredentials) only when a network call needs it.
|
|
120
|
+
*/
|
|
121
|
+
export declare function resolveCliCredentialsWithoutKey(config: BotGuardConfig, overrides?: Partial<ResolvedCliCredentials>): ResolvedCliCredentials;
|
|
116
122
|
/**
|
|
117
123
|
* Credential resolution that is guaranteed to spawn NO child process — for
|
|
118
124
|
* frequent low-privilege callers (watchdog beacon every 5 min). Skips the
|
|
@@ -121,6 +127,36 @@ export declare function resolveCliCredentials(config: BotGuardConfig, overrides?
|
|
|
121
127
|
* (unauthenticated) beacon instead.
|
|
122
128
|
*/
|
|
123
129
|
export declare function resolveCliCredentialsShellFree(config: BotGuardConfig, overrides?: Partial<ResolvedCliCredentials>): ResolvedCliCredentials;
|
|
130
|
+
/**
|
|
131
|
+
* The key this machine holds for one particular shield — what a hook of that
|
|
132
|
+
* shield resolves on its shell-free path (env handoff, Credential Manager,
|
|
133
|
+
* machine key file, saved config, env), but each source only for the shield it
|
|
134
|
+
* belongs to: never another shield's key. Read-only and spawn-free (no DPAPI
|
|
135
|
+
* decrypt, no migration or mirror write). `shieldId` must come from this
|
|
136
|
+
* process's configuration (machineEnrollment), never from a file another
|
|
137
|
+
* process could write.
|
|
138
|
+
*/
|
|
139
|
+
export declare function storedShieldKeyFor(config: BotGuardConfig, shieldId: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* This machine's own enrollment as a hook started without flags resolves it:
|
|
142
|
+
* shield and API URL from ~/.fullcourtdefense.yml and the environment, and the
|
|
143
|
+
* key stored for THAT shield (storedShieldKeyFor). Read-only and spawn-free.
|
|
144
|
+
* The key belongs with this API URL only: a caller sends it nowhere else.
|
|
145
|
+
*/
|
|
146
|
+
export declare function machineEnrollment(env?: NodeJS.ProcessEnv): {
|
|
147
|
+
shieldId?: string;
|
|
148
|
+
apiUrl: string;
|
|
149
|
+
shieldKey?: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* machineEnrollment without the key: the shield this machine is enrolled
|
|
153
|
+
* with and the API URL configured with it — the only destination its key may
|
|
154
|
+
* go to. Reads ~/.fullcourtdefense.yml and the environment, no key store.
|
|
155
|
+
*/
|
|
156
|
+
export declare function machineEnrollmentTarget(env?: NodeJS.ProcessEnv): {
|
|
157
|
+
shieldId?: string;
|
|
158
|
+
apiUrl: string;
|
|
159
|
+
};
|
|
124
160
|
export declare function isCliSetupComplete(config: BotGuardConfig): boolean;
|
|
125
161
|
/** Fail fast with a clear message when setup was not run. */
|
|
126
162
|
export declare function requireCliSetup(config: BotGuardConfig, overrides?: Partial<ResolvedCliCredentials>, options?: {
|
package/dist/config.js
CHANGED
|
@@ -45,7 +45,11 @@ exports.saveShieldConfig = saveShieldConfig;
|
|
|
45
45
|
exports.daemonHandoffShieldKey = daemonHandoffShieldKey;
|
|
46
46
|
exports.getCredentialResolutionTrace = getCredentialResolutionTrace;
|
|
47
47
|
exports.resolveCliCredentials = resolveCliCredentials;
|
|
48
|
+
exports.resolveCliCredentialsWithoutKey = resolveCliCredentialsWithoutKey;
|
|
48
49
|
exports.resolveCliCredentialsShellFree = resolveCliCredentialsShellFree;
|
|
50
|
+
exports.storedShieldKeyFor = storedShieldKeyFor;
|
|
51
|
+
exports.machineEnrollment = machineEnrollment;
|
|
52
|
+
exports.machineEnrollmentTarget = machineEnrollmentTarget;
|
|
49
53
|
exports.isCliSetupComplete = isCliSetupComplete;
|
|
50
54
|
exports.requireCliSetup = requireCliSetup;
|
|
51
55
|
exports.saveSetupConfig = saveSetupConfig;
|
|
@@ -55,8 +59,12 @@ const os = __importStar(require("os"));
|
|
|
55
59
|
const path = __importStar(require("path"));
|
|
56
60
|
const child_process_1 = require("child_process");
|
|
57
61
|
const distress_1 = require("./distress");
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
// The credential stores load only when a shield key is actually resolved or
|
|
63
|
+
// saved: a hook deciding from the offline cache never touches them.
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
65
|
+
const credentialStore = () => require('./credentialStore');
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
67
|
+
const machineKeyFile = () => require('./machineKeyFile');
|
|
60
68
|
const CONFIG_FILENAMES = [
|
|
61
69
|
'.fullcourtdefense.yml',
|
|
62
70
|
'.fullcourtdefense.yaml',
|
|
@@ -354,13 +362,13 @@ function getCredentialResolutionTrace() {
|
|
|
354
362
|
// long-lived (must not hammer the Credential Manager every 30s poll).
|
|
355
363
|
let nativeMigrationAttempted = false;
|
|
356
364
|
function maybeMigrateShieldKeyToNativeStore(shieldId, shieldKey) {
|
|
357
|
-
if (nativeMigrationAttempted || !(
|
|
365
|
+
if (nativeMigrationAttempted || !credentialStore().nativeCredentialStoreAvailable())
|
|
358
366
|
return;
|
|
359
367
|
nativeMigrationAttempted = true;
|
|
360
368
|
try {
|
|
361
|
-
if ((
|
|
369
|
+
if (credentialStore().readShieldKeyNative(shieldId))
|
|
362
370
|
return; // already migrated
|
|
363
|
-
(
|
|
371
|
+
credentialStore().saveShieldKeyNative(shieldId, shieldKey); // read-back verified inside
|
|
364
372
|
}
|
|
365
373
|
catch { /* migration is opportunistic — legacy sources keep working */ }
|
|
366
374
|
}
|
|
@@ -378,9 +386,9 @@ function maybeMirrorShieldKeyToMachineFile(shieldId, shieldKey) {
|
|
|
378
386
|
return;
|
|
379
387
|
machineFileMirrorAttempted = true;
|
|
380
388
|
try {
|
|
381
|
-
if ((
|
|
389
|
+
if (machineKeyFile().readShieldKeyFromMachineFile(shieldId) === shieldKey)
|
|
382
390
|
return; // mirror current
|
|
383
|
-
(
|
|
391
|
+
machineKeyFile().saveShieldKeyToMachineFile(shieldId, shieldKey); // read-back verified inside
|
|
384
392
|
}
|
|
385
393
|
catch { /* mirroring is opportunistic — primary stores keep working */ }
|
|
386
394
|
}
|
|
@@ -410,7 +418,7 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
|
|
|
410
418
|
// in-process win32 API read that EDRs don't flag and CLM can't constrain.
|
|
411
419
|
// Silently absent on machines that never migrated (see the migration
|
|
412
420
|
// below) — the DPAPI chain still stands behind it.
|
|
413
|
-
shieldKey = (
|
|
421
|
+
shieldKey = credentialStore().readShieldKeyNative(resolvedShieldId);
|
|
414
422
|
if (shieldKey)
|
|
415
423
|
shieldKeySource = 'native_store';
|
|
416
424
|
}
|
|
@@ -420,7 +428,7 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
|
|
|
420
428
|
// S4U logon sessions do not have — the read above returns nothing there.
|
|
421
429
|
// The machine key file is protected by NTFS ACL (SID-based, which S4U
|
|
422
430
|
// does carry) and is mirrored by interactive-session processes below.
|
|
423
|
-
shieldKey = (
|
|
431
|
+
shieldKey = machineKeyFile().readShieldKeyFromMachineFile(resolvedShieldId);
|
|
424
432
|
if (shieldKey)
|
|
425
433
|
shieldKeySource = 'machine_file';
|
|
426
434
|
}
|
|
@@ -461,6 +469,10 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
|
|
|
461
469
|
&& (shieldKeySource === 'native_store' || shieldKeySource === 'dpapi' || shieldKeySource === 'plaintext_config')) {
|
|
462
470
|
maybeMirrorShieldKeyToMachineFile(resolvedShieldId, shieldKey);
|
|
463
471
|
}
|
|
472
|
+
return { ...credentialFields(config, overrides), shieldKey };
|
|
473
|
+
}
|
|
474
|
+
/** Everything but the shield key: config, env and overrides only. */
|
|
475
|
+
function credentialFields(config, overrides) {
|
|
464
476
|
return {
|
|
465
477
|
apiKey: overrides.apiKey
|
|
466
478
|
|| config.apiKey
|
|
@@ -477,9 +489,16 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
|
|
|
477
489
|
|| process.env.FCD_SHIELD_ID
|
|
478
490
|
|| process.env.FULLCOURTDEFENSE_SHIELD_ID
|
|
479
491
|
|| process.env.AGENTGUARD_SHIELD_ID,
|
|
480
|
-
shieldKey,
|
|
481
492
|
};
|
|
482
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Shield, endpoint and override key only — no key store is read. For verdict
|
|
496
|
+
* paths that decide from the offline cache and resolve the key
|
|
497
|
+
* (resolveCliCredentials) only when a network call needs it.
|
|
498
|
+
*/
|
|
499
|
+
function resolveCliCredentialsWithoutKey(config, overrides = {}) {
|
|
500
|
+
return { ...credentialFields(config, overrides), shieldKey: overrides.shieldKey };
|
|
501
|
+
}
|
|
483
502
|
/**
|
|
484
503
|
* Credential resolution that is guaranteed to spawn NO child process — for
|
|
485
504
|
* frequent low-privilege callers (watchdog beacon every 5 min). Skips the
|
|
@@ -490,6 +509,53 @@ function resolveCliCredentials(config, overrides = {}, options = {}) {
|
|
|
490
509
|
function resolveCliCredentialsShellFree(config, overrides = {}) {
|
|
491
510
|
return resolveCliCredentials(config, overrides, { skipDpapi: true });
|
|
492
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* The key this machine holds for one particular shield — what a hook of that
|
|
514
|
+
* shield resolves on its shell-free path (env handoff, Credential Manager,
|
|
515
|
+
* machine key file, saved config, env), but each source only for the shield it
|
|
516
|
+
* belongs to: never another shield's key. Read-only and spawn-free (no DPAPI
|
|
517
|
+
* decrypt, no migration or mirror write). `shieldId` must come from this
|
|
518
|
+
* process's configuration (machineEnrollment), never from a file another
|
|
519
|
+
* process could write.
|
|
520
|
+
*/
|
|
521
|
+
function storedShieldKeyFor(config, shieldId, env = process.env) {
|
|
522
|
+
if (!shieldId)
|
|
523
|
+
return undefined;
|
|
524
|
+
if (env.FCD_SHIELD_ID === shieldId && env.FCD_SHIELD_KEY)
|
|
525
|
+
return env.FCD_SHIELD_KEY;
|
|
526
|
+
const stored = credentialStore().readShieldKeyNative(shieldId) || machineKeyFile().readShieldKeyFromMachineFile(shieldId);
|
|
527
|
+
if (stored)
|
|
528
|
+
return stored;
|
|
529
|
+
const envShieldId = env.FCD_SHIELD_ID || env.FULLCOURTDEFENSE_SHIELD_ID || env.AGENTGUARD_SHIELD_ID;
|
|
530
|
+
if (config.shieldKey && (config.shieldId || envShieldId) === shieldId)
|
|
531
|
+
return config.shieldKey;
|
|
532
|
+
const envKey = env.FCD_SHIELD_KEY || env.FULLCOURTDEFENSE_SHIELD_KEY || env.AGENTGUARD_SHIELD_KEY;
|
|
533
|
+
return envKey && (envShieldId || config.shieldId) === shieldId ? envKey : undefined;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* This machine's own enrollment as a hook started without flags resolves it:
|
|
537
|
+
* shield and API URL from ~/.fullcourtdefense.yml and the environment, and the
|
|
538
|
+
* key stored for THAT shield (storedShieldKeyFor). Read-only and spawn-free.
|
|
539
|
+
* The key belongs with this API URL only: a caller sends it nowhere else.
|
|
540
|
+
*/
|
|
541
|
+
function machineEnrollment(env = process.env) {
|
|
542
|
+
const config = loadMachineConfig();
|
|
543
|
+
const { shieldId, apiUrl } = enrollmentTargetOf(config, env);
|
|
544
|
+
return { shieldId, apiUrl, shieldKey: shieldId ? storedShieldKeyFor(config, shieldId, env) : undefined };
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* machineEnrollment without the key: the shield this machine is enrolled
|
|
548
|
+
* with and the API URL configured with it — the only destination its key may
|
|
549
|
+
* go to. Reads ~/.fullcourtdefense.yml and the environment, no key store.
|
|
550
|
+
*/
|
|
551
|
+
function machineEnrollmentTarget(env = process.env) {
|
|
552
|
+
return enrollmentTargetOf(loadMachineConfig(), env);
|
|
553
|
+
}
|
|
554
|
+
function enrollmentTargetOf(config, env) {
|
|
555
|
+
const shieldId = config.shieldId || env.FCD_SHIELD_ID || env.FULLCOURTDEFENSE_SHIELD_ID || env.AGENTGUARD_SHIELD_ID;
|
|
556
|
+
const apiUrl = (config.apiUrl || env.FULLCOURTDEFENSE_API_URL || env.FCD_API_URL || DEFAULT_API_URL).replace(/\/$/, '');
|
|
557
|
+
return { shieldId, apiUrl };
|
|
558
|
+
}
|
|
493
559
|
function isCliSetupComplete(config) {
|
|
494
560
|
const creds = resolveCliCredentials(config);
|
|
495
561
|
return Boolean(creds.shieldId && creds.apiKey && creds.organizationId);
|
|
@@ -542,11 +608,11 @@ function writeSetupConfig(target, input) {
|
|
|
542
608
|
// verifies: rolling back to a ≤1.21.33 CLI keeps working, and machines
|
|
543
609
|
// where the native binding is blocked keep their PowerShell path.
|
|
544
610
|
if (input.shieldKey && input.shieldId) {
|
|
545
|
-
(
|
|
611
|
+
credentialStore().saveShieldKeyNative(input.shieldId, input.shieldKey);
|
|
546
612
|
// S4U mirror: the daemon/watchdog tasks cannot read Credential Manager or
|
|
547
613
|
// user-scope DPAPI (S4U logon has no password-derived master key) — give
|
|
548
614
|
// them the ACL-protected file at enrollment so they are never key-less.
|
|
549
|
-
(
|
|
615
|
+
machineKeyFile().saveShieldKeyToMachineFile(input.shieldId, input.shieldKey);
|
|
550
616
|
}
|
|
551
617
|
const protectedShieldKey = input.shieldKey && process.platform === 'win32'
|
|
552
618
|
? protectShieldKeyForCurrentWindowsUser(input.shieldKey)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Side-effect module, imported FIRST by index.ts: in the resident daemon the
|
|
5
|
+
* signed detector is compiled on the verdict thread (detectorInThread.ts), and
|
|
6
|
+
* that choice must be made before the hook module graph imported next loads
|
|
7
|
+
* the detector. Every other command (hooks included) loads nothing here.
|
|
8
|
+
*/
|
|
9
|
+
if (process.argv[2] === 'daemon') {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
require('./detectorInThread').useInThreadDetector();
|
|
12
|
+
}
|
|
@@ -116,7 +116,12 @@ export declare function recordCleanExit(reason: string): void;
|
|
|
116
116
|
* marker stale + no pipe endpoint → pid recycled (daemon is gone; do NOT
|
|
117
117
|
* kill the unrelated process)
|
|
118
118
|
*/
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* 'not_serving': the daemon is alive and its pipe answers, but it rejects this
|
|
121
|
+
* profile's token ('unauthorized') — the token file is not the serving
|
|
122
|
+
* daemon's, so every hook evaluates locally while heartbeats look healthy.
|
|
123
|
+
*/
|
|
124
|
+
export type DaemonHealthClass = 'dead' | 'healthy' | 'hung' | 'pid_reused' | 'not_serving' | 'indeterminate';
|
|
120
125
|
/** Marker older than this is stale — 4 missed 5-minute heartbeats. */
|
|
121
126
|
export declare function daemonHungAfterMs(): number;
|
|
122
127
|
export declare function classifyDaemonHealth(input: {
|
|
@@ -125,6 +130,8 @@ export declare function classifyDaemonHealth(input: {
|
|
|
125
130
|
marker: DaemonAliveMarker | undefined;
|
|
126
131
|
staleAfterMs: number;
|
|
127
132
|
pipeProbe: 'responsive' | 'unresponsive' | 'no-endpoint' | 'not-probed';
|
|
133
|
+
/** With a 'responsive' probe: false when the endpoint rejected this profile's token. */
|
|
134
|
+
pipeAuthorized?: boolean;
|
|
128
135
|
/**
|
|
129
136
|
* True when the machine itself stopped running since the previous check
|
|
130
137
|
* (sleep/hibernate). Marker staleness is then not evidence of anything —
|
|
@@ -165,6 +172,12 @@ export interface WatchdogTick {
|
|
|
165
172
|
export declare function watchdogTickFile(): string;
|
|
166
173
|
export declare function readWatchdogTick(): WatchdogTick | undefined;
|
|
167
174
|
export declare function recordWatchdogTick(now?: Date): void;
|
|
175
|
+
/**
|
|
176
|
+
* Consecutive watchdog ticks on which daemon `pid` was alive but rejected this
|
|
177
|
+
* profile's token (DaemonHealthClass 'not_serving'). `undefined` (any other
|
|
178
|
+
* class) resets the count. Returns the count including this tick.
|
|
179
|
+
*/
|
|
180
|
+
export declare function recordNotServing(pid: number | undefined, now?: Date): number;
|
|
168
181
|
/**
|
|
169
182
|
* Pure: did the machine stop running between the previous watchdog tick and
|
|
170
183
|
* this one?
|
package/dist/daemonForensics.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.classifyDaemonHealth = classifyDaemonHealth;
|
|
|
52
52
|
exports.watchdogTickFile = watchdogTickFile;
|
|
53
53
|
exports.readWatchdogTick = readWatchdogTick;
|
|
54
54
|
exports.recordWatchdogTick = recordWatchdogTick;
|
|
55
|
+
exports.recordNotServing = recordNotServing;
|
|
55
56
|
exports.detectMachineSuspension = detectMachineSuspension;
|
|
56
57
|
exports.recordWatchdogRecycle = recordWatchdogRecycle;
|
|
57
58
|
exports.markPostmortemReported = markPostmortemReported;
|
|
@@ -246,6 +247,10 @@ function classifyDaemonHealth(input) {
|
|
|
246
247
|
// freshness cannot be judged — stay conservative, never kill on a guess.
|
|
247
248
|
if (!marker || marker.pid !== input.pid)
|
|
248
249
|
return 'indeterminate';
|
|
250
|
+
// Alive and answering, but not for this profile's hooks: a fresh marker
|
|
251
|
+
// does not make that healthy (2026-10-02: 11 h of 'unauthorized').
|
|
252
|
+
if (input.pipeProbe === 'responsive' && input.pipeAuthorized === false)
|
|
253
|
+
return 'not_serving';
|
|
249
254
|
const last = Date.parse(marker.lastAliveAt || marker.startedAt || '');
|
|
250
255
|
const age = (input.nowMs ?? Date.now()) - last;
|
|
251
256
|
if (!Number.isFinite(age) || age <= input.staleAfterMs)
|
|
@@ -306,6 +311,33 @@ function readWatchdogTick() {
|
|
|
306
311
|
function recordWatchdogTick(now = new Date()) {
|
|
307
312
|
writeJson(watchdogTickFile(), { lastTickAt: now.toISOString() });
|
|
308
313
|
}
|
|
314
|
+
function notServingFile() {
|
|
315
|
+
return path.join(stateDir(), 'watchdog.not-serving.json');
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Consecutive watchdog ticks on which daemon `pid` was alive but rejected this
|
|
319
|
+
* profile's token (DaemonHealthClass 'not_serving'). `undefined` (any other
|
|
320
|
+
* class) resets the count. Returns the count including this tick.
|
|
321
|
+
*/
|
|
322
|
+
function recordNotServing(pid, now = new Date()) {
|
|
323
|
+
if (pid === undefined) {
|
|
324
|
+
try {
|
|
325
|
+
fs.unlinkSync(notServingFile());
|
|
326
|
+
}
|
|
327
|
+
catch { /* none */ }
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
let ticks = 0;
|
|
331
|
+
try {
|
|
332
|
+
const prev = JSON.parse(fs.readFileSync(notServingFile(), 'utf8'));
|
|
333
|
+
if (prev && prev.pid === pid && typeof prev.ticks === 'number')
|
|
334
|
+
ticks = prev.ticks;
|
|
335
|
+
}
|
|
336
|
+
catch { /* first tick */ }
|
|
337
|
+
ticks += 1;
|
|
338
|
+
writeJson(notServingFile(), { pid, ticks, lastAt: now.toISOString() });
|
|
339
|
+
return ticks;
|
|
340
|
+
}
|
|
309
341
|
/**
|
|
310
342
|
* Pure: did the machine stop running between the previous watchdog tick and
|
|
311
343
|
* this one?
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make the signed detector available to this evaluation without loading it:
|
|
3
|
+
* the signed vocabulary is read (cheap), and a placeholder is installed that
|
|
4
|
+
* verifies and compiles the artifact on the engine's first inference. No-op
|
|
5
|
+
* where the Worker loader already owns the detector.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureEvaluationDetector(): void;
|
|
8
|
+
/**
|
|
9
|
+
* Detector stamp for spooled events (revision / sha256 / active), without
|
|
10
|
+
* loading anything: the Worker loader's status where it owns the detector,
|
|
11
|
+
* otherwise the in-process detector once a decision has used it. Undefined
|
|
12
|
+
* when this process never consulted the detector.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectorStampForEvent(): {
|
|
15
|
+
revision: number;
|
|
16
|
+
sha256?: string;
|
|
17
|
+
active: boolean;
|
|
18
|
+
} | undefined;
|
|
@@ -0,0 +1,234 @@
|
|
|
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.ensureEvaluationDetector = ensureEvaluationDetector;
|
|
37
|
+
exports.detectorStampForEvent = detectorStampForEvent;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const vm_1 = require("vm");
|
|
42
|
+
const detectionData_1 = require("./detectionData");
|
|
43
|
+
const detectorArtifact_1 = require("./detectorArtifact");
|
|
44
|
+
const detectorRuntime_1 = require("./detectorRuntime");
|
|
45
|
+
const localDetectionData_1 = require("./localDetectionData");
|
|
46
|
+
const machineIdentity_1 = require("./machineIdentity");
|
|
47
|
+
const MAX_INPUT_BYTES = 256 * 1024;
|
|
48
|
+
const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
49
|
+
let installed = false;
|
|
50
|
+
let resolved = false;
|
|
51
|
+
let chosen;
|
|
52
|
+
/** The Worker loader owns the detector in this process (daemon, gateways). */
|
|
53
|
+
function longRunningLoaderLoaded() {
|
|
54
|
+
try {
|
|
55
|
+
return Boolean(require.cache[require.resolve('./localDetectionUpdates')]);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Same VM setup as detectorModule's Worker, on this thread. Synchronous, no Atomics.wait. */
|
|
62
|
+
function compileInProcess(artifact) {
|
|
63
|
+
const context = (0, vm_1.createContext)(Object.create(null), { codeGeneration: { strings: false, wasm: false } });
|
|
64
|
+
// Expose parsed scalar fields, not Node's host URL constructor or native
|
|
65
|
+
// exceptions. The wrapper and all values visible to code belong to the VM.
|
|
66
|
+
const makeUrl = new vm_1.Script('(parse => class URL { constructor(input, base) { const fields=JSON.parse(parse(String(input), base===undefined?undefined:String(base))); if(fields.error)throw new TypeError("Invalid URL"); Object.assign(this,fields); } toString(){return this.href;} })').runInContext(context, { timeout: 1000 });
|
|
67
|
+
context.URL = makeUrl((input, base) => {
|
|
68
|
+
try {
|
|
69
|
+
if (typeof input !== 'string' || input.length > 256 * 1024 || (base !== undefined && (typeof base !== 'string' || base.length > 256 * 1024)))
|
|
70
|
+
return '{"error":true}';
|
|
71
|
+
const value = new URL(input, base);
|
|
72
|
+
const fields = {};
|
|
73
|
+
for (const key of ['href', 'origin', 'protocol', 'username', 'password', 'host', 'hostname', 'port', 'pathname', 'search', 'hash'])
|
|
74
|
+
fields[key] = value[key];
|
|
75
|
+
return JSON.stringify(fields);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return '{"error":true}';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
new vm_1.Script(artifact.code).runInContext(context, { timeout: 1000 });
|
|
82
|
+
new vm_1.Script('if(typeof FcdDetector?.inferToolContext!=="function")throw new Error("Missing export")').runInContext(context, { timeout: 1000 });
|
|
83
|
+
const evaluate = new vm_1.Script('JSON.stringify(FcdDetector.inferToolContext(...JSON.parse(inputJson)))');
|
|
84
|
+
let stopped = false;
|
|
85
|
+
const invoke = (tool, args) => {
|
|
86
|
+
if (stopped)
|
|
87
|
+
throw new detectorRuntime_1.DetectorWorkerUnavailable('Detector unavailable');
|
|
88
|
+
const input = JSON.stringify([tool, args, (0, detectionData_1.getDetectionData)()]);
|
|
89
|
+
if (Buffer.byteLength(input) > MAX_INPUT_BYTES)
|
|
90
|
+
throw new Error('Detector input exceeds bound');
|
|
91
|
+
let text;
|
|
92
|
+
try {
|
|
93
|
+
context.inputJson = input;
|
|
94
|
+
text = evaluate.runInContext(context, { timeout: 250 });
|
|
95
|
+
if (typeof text !== 'string')
|
|
96
|
+
throw new Error('Invalid output');
|
|
97
|
+
if (Buffer.byteLength(text) > MAX_OUTPUT_BYTES)
|
|
98
|
+
throw new Error('Output too large');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// The Worker retires itself on any inference fault; so does this copy.
|
|
102
|
+
stopped = true;
|
|
103
|
+
throw new detectorRuntime_1.DetectorWorkerUnavailable('Detector timed out or failed');
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
delete context.inputJson;
|
|
107
|
+
}
|
|
108
|
+
return JSON.parse(text);
|
|
109
|
+
};
|
|
110
|
+
return Object.assign(invoke, { dispose: () => { stopped = true; } });
|
|
111
|
+
}
|
|
112
|
+
/** Compile and smoke-test exactly as detectorUpdates.prepareDetector does. */
|
|
113
|
+
function prepare(artifact) {
|
|
114
|
+
let infer;
|
|
115
|
+
try {
|
|
116
|
+
infer = compileInProcess(artifact);
|
|
117
|
+
(0, detectorRuntime_1.validateDetectorFacts)(infer('read_file', { path: 'sample.txt' }));
|
|
118
|
+
return infer;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
infer?.dispose();
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function bundledBaseline() {
|
|
126
|
+
try {
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
128
|
+
const { BUNDLED_DETECTOR_ENVELOPE } = require('./detectorBaseline');
|
|
129
|
+
if (!BUNDLED_DETECTOR_ENVELOPE)
|
|
130
|
+
return undefined;
|
|
131
|
+
const issuedAt = JSON.parse(JSON.parse(BUNDLED_DETECTOR_ENVELOPE).payload).issuedAt;
|
|
132
|
+
const bundled = (0, detectorArtifact_1.verifyDetectorArtifact)(BUNDLED_DETECTOR_ENVELOPE, Math.min(Date.now(), issuedAt));
|
|
133
|
+
return bundled.machineIds === null ? bundled : undefined;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** The daemon's accepted update cache, under the same verification and eligibility as DetectorUpdates.loadLocal. */
|
|
140
|
+
function cachedUpdate() {
|
|
141
|
+
try {
|
|
142
|
+
const file = path.join(os.homedir(), '.fullcourtdefense', 'detector-update-v1.json');
|
|
143
|
+
if (fs.statSync(file).size > detectorArtifact_1.MAX_DETECTOR_BYTES)
|
|
144
|
+
return undefined;
|
|
145
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
146
|
+
// Expiration gates first acceptance, not continued offline use.
|
|
147
|
+
const data = (0, detectorArtifact_1.verifyDetectorArtifact)(raw, Math.min(Date.now(), JSON.parse(JSON.parse(raw).payload).issuedAt));
|
|
148
|
+
if (data.machineIds !== null && !data.machineIds.includes((0, machineIdentity_1.getMachineIdentity)().machineId))
|
|
149
|
+
return undefined;
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The artifact the Worker loader would have active: it activates the baseline
|
|
158
|
+
* first, then a cached update only when strictly newer than what is active
|
|
159
|
+
* (any revision when the baseline did not activate). Each candidate must
|
|
160
|
+
* compile and pass the smoke inference.
|
|
161
|
+
*/
|
|
162
|
+
function selectDetector() {
|
|
163
|
+
const baseline = bundledBaseline();
|
|
164
|
+
const update = cachedUpdate();
|
|
165
|
+
const updateFirst = Boolean(update && (!baseline || update.revision > baseline.revision));
|
|
166
|
+
if (update && updateFirst) {
|
|
167
|
+
const infer = prepare(update);
|
|
168
|
+
if (infer)
|
|
169
|
+
return { artifact: update, infer };
|
|
170
|
+
}
|
|
171
|
+
if (baseline) {
|
|
172
|
+
const infer = prepare(baseline);
|
|
173
|
+
if (infer)
|
|
174
|
+
return { artifact: baseline, infer };
|
|
175
|
+
}
|
|
176
|
+
if (update && !updateFirst) {
|
|
177
|
+
const infer = prepare(update);
|
|
178
|
+
if (infer)
|
|
179
|
+
return { artifact: update, infer };
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Make the signed detector available to this evaluation without loading it:
|
|
185
|
+
* the signed vocabulary is read (cheap), and a placeholder is installed that
|
|
186
|
+
* verifies and compiles the artifact on the engine's first inference. No-op
|
|
187
|
+
* where the Worker loader already owns the detector.
|
|
188
|
+
*/
|
|
189
|
+
function ensureEvaluationDetector() {
|
|
190
|
+
try {
|
|
191
|
+
localDetectionData_1.localDetectionUpdates.loadLocal();
|
|
192
|
+
}
|
|
193
|
+
catch { /* bundled vocabulary decides */ }
|
|
194
|
+
if (installed || longRunningLoaderLoaded() || (0, detectorRuntime_1.detectorRuntimeStatus)().revision > 0)
|
|
195
|
+
return;
|
|
196
|
+
installed = true;
|
|
197
|
+
let infer;
|
|
198
|
+
const lazy = Object.assign((tool, args) => {
|
|
199
|
+
if (!resolved) {
|
|
200
|
+
resolved = true;
|
|
201
|
+
const selected = selectDetector();
|
|
202
|
+
chosen = selected?.artifact;
|
|
203
|
+
infer = selected?.infer;
|
|
204
|
+
}
|
|
205
|
+
// No usable artifact: retire the placeholder; the bundled inference decides.
|
|
206
|
+
if (!infer)
|
|
207
|
+
throw new detectorRuntime_1.DetectorWorkerUnavailable('No signed detector available');
|
|
208
|
+
return infer(tool, args);
|
|
209
|
+
}, { dispose: () => { infer?.dispose(); } });
|
|
210
|
+
// Revision 1 orders below every published artifact, so a Worker loader that
|
|
211
|
+
// starts later in this process still replaces the placeholder.
|
|
212
|
+
try {
|
|
213
|
+
(0, detectorRuntime_1.installDetector)(lazy, 1);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
installed = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Detector stamp for spooled events (revision / sha256 / active), without
|
|
221
|
+
* loading anything: the Worker loader's status where it owns the detector,
|
|
222
|
+
* otherwise the in-process detector once a decision has used it. Undefined
|
|
223
|
+
* when this process never consulted the detector.
|
|
224
|
+
*/
|
|
225
|
+
function detectorStampForEvent() {
|
|
226
|
+
if (longRunningLoaderLoaded()) {
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
228
|
+
const status = require('./localDetectionUpdates').localDetectorUpdates.getDetectorStatus();
|
|
229
|
+
return { revision: status.revision, sha256: status.sha256, active: status.active };
|
|
230
|
+
}
|
|
231
|
+
if (!installed || !resolved)
|
|
232
|
+
return undefined;
|
|
233
|
+
return { revision: chosen?.revision ?? 0, sha256: chosen?.sha256, active: (0, detectorRuntime_1.detectorRuntimeStatus)().active };
|
|
234
|
+
}
|