fullcourtdefense-cli 1.37.0 → 1.38.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/askDialog.d.ts +11 -0
- package/dist/askDialog.js +26 -0
- package/dist/awakeClock.d.ts +34 -0
- package/dist/awakeClock.js +81 -0
- package/dist/backupManifest.d.ts +14 -1
- package/dist/backupManifest.js +180 -19
- package/dist/callerEnv.d.ts +21 -1
- package/dist/callerEnv.js +40 -0
- package/dist/commands/cmdGuard.d.ts +1 -0
- package/dist/commands/cmdGuard.js +14 -1
- package/dist/commands/daemon.d.ts +20 -0
- package/dist/commands/daemon.js +310 -28
- package/dist/commands/desktopChatGuard.d.ts +20 -0
- package/dist/commands/desktopChatGuard.js +60 -11
- package/dist/commands/doctor.js +5 -1
- package/dist/commands/hook.d.ts +9 -0
- package/dist/commands/hook.js +77 -13
- package/dist/commands/mcpGateway.js +16 -10
- package/dist/commands/posixShellGuard.js +3 -2
- package/dist/commands/shellGuard.js +25 -6
- package/dist/commands/uninstallAll.js +5 -0
- package/dist/commands/watchdog.d.ts +8 -0
- package/dist/commands/watchdog.js +19 -0
- package/dist/daemonForensics.d.ts +11 -0
- package/dist/daemonForensics.js +25 -1
- package/dist/devConfirm.d.ts +13 -0
- package/dist/devConfirm.js +3 -2
- package/dist/distress.d.ts +10 -0
- package/dist/distress.js +273 -32
- package/dist/gateReports.js +15 -0
- package/dist/hookIo.d.ts +20 -2
- package/dist/hookIo.js +106 -15
- package/dist/hookSlim.js +14 -5
- package/dist/housekeeping.js +1 -0
- package/dist/housekeepingOps.d.ts +5 -5
- package/dist/housekeepingOps.js +17 -2
- package/dist/index.js +26 -0
- package/dist/monitorFastPath.d.ts +39 -3
- package/dist/monitorFastPath.js +67 -8
- package/dist/nativeHook.d.ts +21 -0
- package/dist/nativeHook.js +90 -16
- package/dist/notify.js +51 -1
- package/dist/pendingDrainer.d.ts +14 -4
- package/dist/pendingDrainer.js +74 -24
- package/dist/perfSnapshot.d.ts +40 -2
- package/dist/perfSnapshot.js +117 -31
- package/dist/policyCacheFiles.d.ts +10 -0
- package/dist/policyCacheFiles.js +75 -0
- package/dist/runtimeConfig.d.ts +6 -0
- package/dist/runtimeConfig.js +14 -0
- package/dist/telemetry.d.ts +66 -1
- package/dist/telemetry.js +127 -5
- package/dist/testRunGuard.d.ts +28 -0
- package/dist/testRunGuard.js +228 -0
- package/dist/verdictIpc.js +159 -36
- package/dist/verdictIpcClient.d.ts +80 -3
- package/dist/verdictIpcClient.js +214 -26
- package/dist/version.json +1 -1
- package/package.json +4 -2
package/dist/askDialog.d.ts
CHANGED
|
@@ -56,6 +56,17 @@ export interface AskEnvelope {
|
|
|
56
56
|
reason: string;
|
|
57
57
|
/** The client the runtime event records as `agentClient` (cursor, claude-code, …): the dialog names the same agent as the console. */
|
|
58
58
|
agentClient?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The asking call's correlation identity (session / run / tool call id): the
|
|
61
|
+
* dialog's outcome is recorded by this process, outside that call's
|
|
62
|
+
* evaluation, and must name the same action. Older hook builds omit it.
|
|
63
|
+
*/
|
|
64
|
+
identity?: {
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
runId?: string;
|
|
67
|
+
instanceId?: string;
|
|
68
|
+
attemptId?: string;
|
|
69
|
+
};
|
|
59
70
|
}
|
|
60
71
|
export type AskResolution = 'allow' | 'allow_always' | 'deny' | 'timeout' | 'ide';
|
|
61
72
|
export interface HookVerdict {
|
package/dist/askDialog.js
CHANGED
|
@@ -30,6 +30,7 @@ exports.resolveAskWithDialog = resolveAskWithDialog;
|
|
|
30
30
|
*/
|
|
31
31
|
const devConfirm_1 = require("./devConfirm");
|
|
32
32
|
const telemetry_1 = require("./telemetry");
|
|
33
|
+
const actionIdentity_1 = require("./actionIdentity");
|
|
33
34
|
const hookIo_1 = require("./hookIo");
|
|
34
35
|
const callerEnv_1 = require("./callerEnv");
|
|
35
36
|
/** Field on the verdict JSON that carries the envelope. Stripped before the IDE sees the verdict. */
|
|
@@ -188,6 +189,14 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
188
189
|
if (outcome === 'unavailable')
|
|
189
190
|
return { ...clean, resolution: 'ide' };
|
|
190
191
|
const policyLabel = envelope.policyName ? ` (${envelope.policyName})` : '';
|
|
192
|
+
// The outcome is the asking call's: its client and identity, not this process's.
|
|
193
|
+
// No latency / verdict path: the ask itself recorded the hook's wait (one health
|
|
194
|
+
// sample per call), and the person's answer time is confirmLatencyMs.
|
|
195
|
+
const outcomeFacts = (askOutcome) => ({
|
|
196
|
+
askOutcome,
|
|
197
|
+
...(envelope.agentClient || options.clientLabel ? { agentClient: envelope.agentClient || options.clientLabel } : {}),
|
|
198
|
+
...envelopeIdentity(envelope),
|
|
199
|
+
});
|
|
191
200
|
if (outcome === 'allow' || outcome === 'allow_always') {
|
|
192
201
|
// Same bookkeeping the post-execution hook would have done for an IDE Allow.
|
|
193
202
|
(0, devConfirm_1.settleDeveloperConfirmation)(envelope.event, envelope.toolName, envelope.argsDigest);
|
|
@@ -210,6 +219,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
210
219
|
approvalScope: 'developer',
|
|
211
220
|
approvedBy: 'developer',
|
|
212
221
|
confirmLatencyMs: latencyMs,
|
|
222
|
+
...outcomeFacts(granted ? 'allowed_always' : 'allowed'),
|
|
213
223
|
});
|
|
214
224
|
(0, telemetry_1.triggerFlush)(false);
|
|
215
225
|
const agentMsg = granted
|
|
@@ -230,6 +240,9 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
230
240
|
policyHash: envelope.policyHash,
|
|
231
241
|
approvalScope: 'developer',
|
|
232
242
|
confirmLatencyMs: latencyMs,
|
|
243
|
+
// No answer is not a rule's block: the action did not run (the IDE gets the
|
|
244
|
+
// same deny), but nobody decided against it — recorded as an unanswered ask.
|
|
245
|
+
...outcomeFacts(timedOut ? 'unanswered' : 'denied'),
|
|
233
246
|
});
|
|
234
247
|
(0, telemetry_1.triggerFlush)(true);
|
|
235
248
|
const userMsg = timedOut
|
|
@@ -240,3 +253,16 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
240
253
|
: `FullCourtDefense: the developer denied this ${envelope.event} in the confirmation dialog. Do not retry or work around it.`;
|
|
241
254
|
return { ...clean, stdout: JSON.stringify(rewriteDecision(decision, false, userMsg, agentMsg)), exitCode: 0, resolution: outcome };
|
|
242
255
|
}
|
|
256
|
+
/** The asking call's identity from the envelope: well-formed parts only. */
|
|
257
|
+
function envelopeIdentity(envelope) {
|
|
258
|
+
const raw = envelope.identity;
|
|
259
|
+
if (!raw || typeof raw !== 'object')
|
|
260
|
+
return {};
|
|
261
|
+
const out = {};
|
|
262
|
+
for (const key of ['sessionId', 'runId', 'instanceId', 'attemptId']) {
|
|
263
|
+
const value = (0, actionIdentity_1.identityPart)(raw[key]);
|
|
264
|
+
if (value)
|
|
265
|
+
out[key] = value;
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Was the MACHINE asleep, or only the daemon's event loop?
|
|
3
|
+
*
|
|
4
|
+
* The daemon detects a resume from sleep by a beat that observes far more
|
|
5
|
+
* wall time than its interval. A long synchronous stretch on its own loop
|
|
6
|
+
* looks the same: 2026-10-02 06:58, a 201 s protect-all pass ended and two
|
|
7
|
+
* seconds later the daemon logged "Resume detected: 237s ... suspended",
|
|
8
|
+
* re-reported in and recorded network_down [x30] from timeouts that ran out
|
|
9
|
+
* inside the stall — while hook.log showed hooks every few seconds throughout.
|
|
10
|
+
*
|
|
11
|
+
* This clock ticks on a worker thread, which keeps running while the main
|
|
12
|
+
* loop is blocked and stops only when the whole process (the machine) does.
|
|
13
|
+
* A long main-loop gap with no long gap here was a stall, not a suspend.
|
|
14
|
+
* One thread, a 1 s timer, no process and no I/O.
|
|
15
|
+
*/
|
|
16
|
+
export declare const AWAKE_TICK_MS = 1000;
|
|
17
|
+
/** A worker gap this short through a long main-loop gap: the machine stayed awake. */
|
|
18
|
+
export declare const AWAKE_GAP_MAX_MS = 10000;
|
|
19
|
+
export interface AwakeClock {
|
|
20
|
+
/** Longest gap between ticks since the previous call (including the time since the last tick). Undefined once the clock is gone. */
|
|
21
|
+
takeMaxGapMs(): number | undefined;
|
|
22
|
+
stop(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function startAwakeClock(tickMs?: number): AwakeClock | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Pure: what a gap of `driftMs` between two beats of the daemon's loop was.
|
|
27
|
+
* Without the awake clock (it could not start) every long gap stays a suspend,
|
|
28
|
+
* as before.
|
|
29
|
+
*/
|
|
30
|
+
export declare function classifyClockJump(input: {
|
|
31
|
+
driftMs: number;
|
|
32
|
+
jumpMs: number;
|
|
33
|
+
awakeGapMs?: number;
|
|
34
|
+
}): 'none' | 'stall' | 'suspend';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AWAKE_GAP_MAX_MS = exports.AWAKE_TICK_MS = void 0;
|
|
4
|
+
exports.startAwakeClock = startAwakeClock;
|
|
5
|
+
exports.classifyClockJump = classifyClockJump;
|
|
6
|
+
const worker_threads_1 = require("worker_threads");
|
|
7
|
+
/**
|
|
8
|
+
* Was the MACHINE asleep, or only the daemon's event loop?
|
|
9
|
+
*
|
|
10
|
+
* The daemon detects a resume from sleep by a beat that observes far more
|
|
11
|
+
* wall time than its interval. A long synchronous stretch on its own loop
|
|
12
|
+
* looks the same: 2026-10-02 06:58, a 201 s protect-all pass ended and two
|
|
13
|
+
* seconds later the daemon logged "Resume detected: 237s ... suspended",
|
|
14
|
+
* re-reported in and recorded network_down [x30] from timeouts that ran out
|
|
15
|
+
* inside the stall — while hook.log showed hooks every few seconds throughout.
|
|
16
|
+
*
|
|
17
|
+
* This clock ticks on a worker thread, which keeps running while the main
|
|
18
|
+
* loop is blocked and stops only when the whole process (the machine) does.
|
|
19
|
+
* A long main-loop gap with no long gap here was a stall, not a suspend.
|
|
20
|
+
* One thread, a 1 s timer, no process and no I/O.
|
|
21
|
+
*/
|
|
22
|
+
exports.AWAKE_TICK_MS = 1_000;
|
|
23
|
+
/** A worker gap this short through a long main-loop gap: the machine stayed awake. */
|
|
24
|
+
exports.AWAKE_GAP_MAX_MS = 10_000;
|
|
25
|
+
function startAwakeClock(tickMs = exports.AWAKE_TICK_MS) {
|
|
26
|
+
try {
|
|
27
|
+
// [0] last tick (wall ms), [1] longest gap between ticks since the last read.
|
|
28
|
+
const shared = new Float64Array(new SharedArrayBuffer(16));
|
|
29
|
+
shared[0] = Date.now();
|
|
30
|
+
const source = `
|
|
31
|
+
const { workerData } = require('worker_threads');
|
|
32
|
+
const clock = new Float64Array(workerData);
|
|
33
|
+
let last = Date.now();
|
|
34
|
+
setInterval(() => {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (now - last > clock[1]) clock[1] = now - last;
|
|
37
|
+
last = now;
|
|
38
|
+
clock[0] = now;
|
|
39
|
+
}, ${Math.max(100, Math.floor(tickMs))});
|
|
40
|
+
`;
|
|
41
|
+
const worker = new worker_threads_1.Worker(source, {
|
|
42
|
+
eval: true,
|
|
43
|
+
workerData: shared.buffer,
|
|
44
|
+
resourceLimits: { maxOldGenerationSizeMb: 16, maxYoungGenerationSizeMb: 4 },
|
|
45
|
+
stdout: true,
|
|
46
|
+
stderr: true,
|
|
47
|
+
});
|
|
48
|
+
worker.unref();
|
|
49
|
+
let alive = true;
|
|
50
|
+
worker.on('error', () => { alive = false; });
|
|
51
|
+
worker.on('exit', () => { alive = false; });
|
|
52
|
+
return {
|
|
53
|
+
takeMaxGapMs() {
|
|
54
|
+
if (!alive)
|
|
55
|
+
return undefined;
|
|
56
|
+
const gap = Math.max(shared[1], Date.now() - shared[0]);
|
|
57
|
+
shared[1] = 0;
|
|
58
|
+
return gap;
|
|
59
|
+
},
|
|
60
|
+
stop() {
|
|
61
|
+
alive = false;
|
|
62
|
+
void worker.terminate().catch(() => { });
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Pure: what a gap of `driftMs` between two beats of the daemon's loop was.
|
|
72
|
+
* Without the awake clock (it could not start) every long gap stays a suspend,
|
|
73
|
+
* as before.
|
|
74
|
+
*/
|
|
75
|
+
function classifyClockJump(input) {
|
|
76
|
+
if (input.driftMs < input.jumpMs)
|
|
77
|
+
return 'none';
|
|
78
|
+
if (input.awakeGapMs !== undefined && input.awakeGapMs < exports.AWAKE_GAP_MAX_MS)
|
|
79
|
+
return 'stall';
|
|
80
|
+
return 'suspend';
|
|
81
|
+
}
|
package/dist/backupManifest.d.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
/** Distinct recent versions kept per config, besides its oldest (original) copy. */
|
|
2
|
+
export declare const KEEP_RECENT_BACKUPS = 3;
|
|
1
3
|
export declare function backupManifestPath(): string;
|
|
2
4
|
/** Every recorded backup path (absent/corrupt manifest = empty list). */
|
|
3
5
|
export declare function readBackupManifest(): string[];
|
|
4
|
-
/** Record one backup file path. Deduped,
|
|
6
|
+
/** Record one backup file path. Deduped, never throws. */
|
|
5
7
|
export declare function recordConfigBackup(backupPath: string): void;
|
|
6
8
|
/** Drop entries whose backup file no longer exists (post-cleanup compaction). */
|
|
7
9
|
export declare function pruneBackupManifest(): void;
|
|
10
|
+
/** This config's safety copies next to it (`<name>.fcd-backup-<ms>`), oldest first. */
|
|
11
|
+
export declare function listConfigBackups(file: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Back `file` up next to itself before FullCourtDefense rewrites it — once per
|
|
14
|
+
* distinct content: a version already kept (a rewrite that only flips the
|
|
15
|
+
* runtime path back and forth, a pass that writes the same content) adds no
|
|
16
|
+
* copy. Afterwards the config keeps its oldest copy (the original, before
|
|
17
|
+
* FullCourtDefense first changed it) and its KEEP_RECENT newest. Returns the
|
|
18
|
+
* new copy, or undefined when none was needed (or it failed). Never throws.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createConfigBackup(file: string, now?: number): string | undefined;
|
package/dist/backupManifest.js
CHANGED
|
@@ -33,10 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.KEEP_RECENT_BACKUPS = void 0;
|
|
36
37
|
exports.backupManifestPath = backupManifestPath;
|
|
37
38
|
exports.readBackupManifest = readBackupManifest;
|
|
38
39
|
exports.recordConfigBackup = recordConfigBackup;
|
|
39
40
|
exports.pruneBackupManifest = pruneBackupManifest;
|
|
41
|
+
exports.listConfigBackups = listConfigBackups;
|
|
42
|
+
exports.createConfigBackup = createConfigBackup;
|
|
40
43
|
const fs = __importStar(require("fs"));
|
|
41
44
|
const os = __importStar(require("os"));
|
|
42
45
|
const path = __importStar(require("path"));
|
|
@@ -51,11 +54,21 @@ const path = __importStar(require("path"));
|
|
|
51
54
|
* authoritative list at cleanup time (union-ed with the known-dir scan for
|
|
52
55
|
* backups created by older CLI versions).
|
|
53
56
|
*
|
|
57
|
+
* Bounded (2026-10: 5,517 copies, 9.8 MB, and a manifest silently cut at its
|
|
58
|
+
* newest 2,000 entries, so uninstall missed most of them): a config keeps the
|
|
59
|
+
* copy taken before FullCourtDefense first changed it plus its last
|
|
60
|
+
* KEEP_RECENT distinct versions (createConfigBackup), and the manifest drops
|
|
61
|
+
* only entries whose file is gone — never a live one.
|
|
62
|
+
*
|
|
54
63
|
* Best-effort by design: a manifest write failure must never break a protect
|
|
55
64
|
* operation, and cleanup falls back to the directory scan.
|
|
56
65
|
*/
|
|
57
66
|
const MANIFEST_NAME = 'backup-manifest.json';
|
|
58
|
-
|
|
67
|
+
/** Size guard for the manifest file itself; reached only after every missing-file entry is dropped. */
|
|
68
|
+
const MAX_ENTRIES = 20_000;
|
|
69
|
+
/** Distinct recent versions kept per config, besides its oldest (original) copy. */
|
|
70
|
+
exports.KEEP_RECENT_BACKUPS = 3;
|
|
71
|
+
const BACKUP_INFIX = '.fcd-backup-';
|
|
59
72
|
function backupManifestPath() {
|
|
60
73
|
return path.join(os.homedir(), '.fullcourtdefense', MANIFEST_NAME);
|
|
61
74
|
}
|
|
@@ -70,23 +83,105 @@ function readBackupManifest() {
|
|
|
70
83
|
return [];
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
function sleepSync(ms) {
|
|
87
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read-modify-write the manifest under a short lock, written atomically
|
|
91
|
+
* (temp + rename): concurrent protect passes (daemon, CLI, hooks) no longer
|
|
92
|
+
* drop each other's entries. Bounded wait; never throws.
|
|
93
|
+
*/
|
|
94
|
+
function updateManifest(change) {
|
|
95
|
+
const file = backupManifestPath();
|
|
96
|
+
const lock = `${file}.lock`;
|
|
97
|
+
let held = false;
|
|
75
98
|
try {
|
|
76
|
-
const existing = readBackupManifest();
|
|
77
|
-
if (existing.includes(backupPath))
|
|
78
|
-
return;
|
|
79
|
-
const entries = [...existing, backupPath].slice(-MAX_ENTRIES);
|
|
80
|
-
const file = backupManifestPath();
|
|
81
99
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
82
|
-
|
|
100
|
+
// Protect passes only (daemon, housekeeping worker, CLI) — never a hook: a short wait is fine.
|
|
101
|
+
const deadline = Date.now() + 2_000;
|
|
102
|
+
for (let attempt = 0; attempt < 1_000 && !held; attempt += 1) {
|
|
103
|
+
try {
|
|
104
|
+
fs.writeFileSync(lock, String(process.pid), { flag: 'wx' });
|
|
105
|
+
held = true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// EPERM/EACCES: Windows reports a lock file being deleted that way — still contention.
|
|
109
|
+
const code = error.code;
|
|
110
|
+
if (code !== 'EEXIST' && code !== 'EPERM' && code !== 'EACCES')
|
|
111
|
+
break;
|
|
112
|
+
try {
|
|
113
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 10_000) {
|
|
114
|
+
fs.unlinkSync(lock);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { /* released meanwhile */ }
|
|
119
|
+
if (Date.now() >= deadline)
|
|
120
|
+
break;
|
|
121
|
+
sleepSync(5);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const before = readBackupManifest();
|
|
125
|
+
const next = change(before);
|
|
126
|
+
if (!next)
|
|
127
|
+
return;
|
|
128
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
129
|
+
try {
|
|
130
|
+
if (next.length === 0) {
|
|
131
|
+
fs.rmSync(file, { force: true });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ backups: next }, null, 2)}\n`, 'utf8');
|
|
135
|
+
for (let attempt = 0;; attempt += 1) {
|
|
136
|
+
try {
|
|
137
|
+
fs.renameSync(tmp, file);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Windows: a reader (or a scanner) holding the file open fails the rename for a moment.
|
|
142
|
+
const code = error.code;
|
|
143
|
+
if (attempt >= 20 || (code !== 'EPERM' && code !== 'EACCES' && code !== 'EBUSY'))
|
|
144
|
+
throw error;
|
|
145
|
+
sleepSync(10);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
try {
|
|
151
|
+
fs.unlinkSync(tmp);
|
|
152
|
+
}
|
|
153
|
+
catch { /* not there */ }
|
|
154
|
+
}
|
|
83
155
|
}
|
|
84
156
|
catch { /* best effort — cleanup still has the directory scan */ }
|
|
157
|
+
finally {
|
|
158
|
+
if (held) {
|
|
159
|
+
try {
|
|
160
|
+
fs.unlinkSync(lock);
|
|
161
|
+
}
|
|
162
|
+
catch { /* gone */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Drop entries whose file is gone; only then, if still over the guard, the oldest. */
|
|
167
|
+
function bounded(entries) {
|
|
168
|
+
if (entries.length <= MAX_ENTRIES)
|
|
169
|
+
return entries;
|
|
170
|
+
const live = entries.filter(entry => { try {
|
|
171
|
+
return fs.existsSync(entry);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return true;
|
|
175
|
+
} });
|
|
176
|
+
return live.length <= MAX_ENTRIES ? live : live.slice(-MAX_ENTRIES);
|
|
177
|
+
}
|
|
178
|
+
/** Record one backup file path. Deduped, never throws. */
|
|
179
|
+
function recordConfigBackup(backupPath) {
|
|
180
|
+
updateManifest(existing => (existing.includes(backupPath) ? undefined : bounded([...existing, backupPath])));
|
|
85
181
|
}
|
|
86
182
|
/** Drop entries whose backup file no longer exists (post-cleanup compaction). */
|
|
87
183
|
function pruneBackupManifest() {
|
|
88
|
-
|
|
89
|
-
const existing = readBackupManifest();
|
|
184
|
+
updateManifest((existing) => {
|
|
90
185
|
const remaining = existing.filter(entry => {
|
|
91
186
|
try {
|
|
92
187
|
return fs.existsSync(entry);
|
|
@@ -95,14 +190,80 @@ function pruneBackupManifest() {
|
|
|
95
190
|
return true;
|
|
96
191
|
}
|
|
97
192
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
193
|
+
return remaining.length === existing.length ? undefined : remaining;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** This config's safety copies next to it (`<name>.fcd-backup-<ms>`), oldest first. */
|
|
197
|
+
function listConfigBackups(file) {
|
|
198
|
+
const dir = path.dirname(file);
|
|
199
|
+
const prefix = `${path.basename(file)}${BACKUP_INFIX}`;
|
|
200
|
+
try {
|
|
201
|
+
return fs.readdirSync(dir)
|
|
202
|
+
.filter(name => name.startsWith(prefix) && /^\d+$/.test(name.slice(prefix.length)))
|
|
203
|
+
.sort((a, b) => Number(a.slice(prefix.length)) - Number(b.slice(prefix.length)))
|
|
204
|
+
.map(name => path.join(dir, name));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function sameContent(file, content) {
|
|
211
|
+
try {
|
|
212
|
+
return fs.statSync(file).size === content.length && fs.readFileSync(file).equals(content);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Back `file` up next to itself before FullCourtDefense rewrites it — once per
|
|
220
|
+
* distinct content: a version already kept (a rewrite that only flips the
|
|
221
|
+
* runtime path back and forth, a pass that writes the same content) adds no
|
|
222
|
+
* copy. Afterwards the config keeps its oldest copy (the original, before
|
|
223
|
+
* FullCourtDefense first changed it) and its KEEP_RECENT newest. Returns the
|
|
224
|
+
* new copy, or undefined when none was needed (or it failed). Never throws.
|
|
225
|
+
*/
|
|
226
|
+
function createConfigBackup(file, now = Date.now()) {
|
|
227
|
+
try {
|
|
228
|
+
const content = fs.readFileSync(file);
|
|
229
|
+
const existing = listConfigBackups(file);
|
|
230
|
+
if (existing.some(copy => sameContent(copy, content)))
|
|
231
|
+
return undefined;
|
|
232
|
+
let stamp = now;
|
|
233
|
+
let backup = '';
|
|
234
|
+
for (let attempt = 0; attempt < 5; attempt += 1, stamp += 1) {
|
|
235
|
+
backup = `${file}${BACKUP_INFIX}${stamp}`;
|
|
236
|
+
try {
|
|
237
|
+
fs.copyFileSync(file, backup, fs.constants.COPYFILE_EXCL);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error.code !== 'EEXIST' || attempt === 4)
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
104
244
|
}
|
|
105
|
-
|
|
245
|
+
// Backups can embed live credentials and land in project-local dirs no
|
|
246
|
+
// scan can enumerate — record each one so uninstall can delete them all.
|
|
247
|
+
recordConfigBackup(backup);
|
|
248
|
+
const all = [...existing, backup];
|
|
249
|
+
const keep = new Set([all[0], ...all.slice(-exports.KEEP_RECENT_BACKUPS)]);
|
|
250
|
+
const pruned = [];
|
|
251
|
+
for (const copy of all) {
|
|
252
|
+
if (keep.has(copy))
|
|
253
|
+
continue;
|
|
254
|
+
try {
|
|
255
|
+
fs.rmSync(copy, { force: true });
|
|
256
|
+
pruned.push(copy);
|
|
257
|
+
}
|
|
258
|
+
catch { /* left for uninstall */ }
|
|
259
|
+
}
|
|
260
|
+
if (pruned.length > 0) {
|
|
261
|
+
const gone = new Set(pruned);
|
|
262
|
+
updateManifest(entries => entries.filter(entry => !gone.has(entry)));
|
|
263
|
+
}
|
|
264
|
+
return backup;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return undefined;
|
|
106
268
|
}
|
|
107
|
-
catch { /* best effort */ }
|
|
108
269
|
}
|
package/dist/callerEnv.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Node built-ins only: this leaf is on the slim hook's warm-path module graph.
|
|
13
13
|
*/
|
|
14
|
-
export declare const CALLER_ENV_KEYS: readonly ["FCD_HUMAN_PRESENT", "FCD_EPHEMERAL", "FULLCOURTDEFENSE_EPHEMERAL", "CI", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "DISPLAY", "WAYLAND_DISPLAY", "FCD_ASK_CHANNEL", "FCD_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID", "FCD_RUN_ID", "GITHUB_RUN_ID", "CI_PIPELINE_ID", "FCD_WORKLOAD_INSTANCE_ID", "CLAUDECODE", "CLAUDE_PROJECT_DIR", "VSCODE_PID", "TERM_PROGRAM", "WORKSPACE_PATH", "FCD_HOOK_IDE_TIMEOUT_MS", "FCD_TAINT_APPROVE_ONCE_DISABLED", "FCD_TAINT_APPROVAL_TIMEOUT_MS", "FCD_HOOK_DEBUG", "CLOUDSDK_CORE_PROJECT", "CLOUDSDK_ACTIVE_CONFIG_NAME", "CLOUDSDK_CONFIG", "APPDATA", "AWS_PROFILE", "AWS_DEFAULT_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE", "AWS_ACCESS_KEY_ID", "KUBECONFIG", "HELM_KUBECONTEXT", "HELM_NAMESPACE", "HELM_KUBEAPISERVER", "HELM_KUBETOKEN", "HELM_KUBEASUSER", "DOCKER_HOST", "DOCKER_CONTEXT", "AZURE_CONFIG_DIR", "AZURE_DEFAULTS_GROUP", "TF_WORKSPACE", "TF_DATA_DIR", "PGHOST", "PGDATABASE", "PGSERVICE", "MYSQL_HOST"];
|
|
14
|
+
export declare const CALLER_ENV_KEYS: readonly ["FCD_HUMAN_PRESENT", "FCD_EPHEMERAL", "FULLCOURTDEFENSE_EPHEMERAL", "CI", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "DISPLAY", "WAYLAND_DISPLAY", "FCD_ASK_CHANNEL", "FCD_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID", "FCD_RUN_ID", "GITHUB_RUN_ID", "CI_PIPELINE_ID", "FCD_WORKLOAD_INSTANCE_ID", "CLAUDECODE", "CLAUDE_PROJECT_DIR", "VSCODE_PID", "TERM_PROGRAM", "WORKSPACE_PATH", "FCD_HOOK_IDE_TIMEOUT_MS", "FCD_TAINT_APPROVE_ONCE_DISABLED", "FCD_TAINT_APPROVAL_TIMEOUT_MS", "FCD_HOOK_DEBUG", "FCD_MONITOR_HINTS", "FCD_PERF_PROBE", "CLOUDSDK_CORE_PROJECT", "CLOUDSDK_ACTIVE_CONFIG_NAME", "CLOUDSDK_CONFIG", "APPDATA", "AWS_PROFILE", "AWS_DEFAULT_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE", "AWS_ACCESS_KEY_ID", "KUBECONFIG", "HELM_KUBECONTEXT", "HELM_NAMESPACE", "HELM_KUBEAPISERVER", "HELM_KUBETOKEN", "HELM_KUBEASUSER", "DOCKER_HOST", "DOCKER_CONTEXT", "AZURE_CONFIG_DIR", "AZURE_DEFAULTS_GROUP", "TF_WORKSPACE", "TF_DATA_DIR", "PGHOST", "PGDATABASE", "PGSERVICE", "MYSQL_HOST"];
|
|
15
15
|
export type CallerEnvSnapshot = Partial<Record<(typeof CALLER_ENV_KEYS)[number], string>>;
|
|
16
16
|
/**
|
|
17
17
|
* Keys a decision reads only for WHETHER they are set (cloudGrantTools: static
|
|
@@ -60,6 +60,26 @@ export declare function ideHookTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
|
60
60
|
export declare function applyIdeTimeoutArg(seconds: string | undefined, env?: NodeJS.ProcessEnv): void;
|
|
61
61
|
/** Absolute time every wait inside a hook must end by, leaving the margin before the IDE's timeout. */
|
|
62
62
|
export declare function ideWaitCutoff(startedAt: number, ideTimeoutMs?: number): number;
|
|
63
|
+
/**
|
|
64
|
+
* One truthiness rule for FCD_* switches: `1`, `true`, `yes`, `on` (any case,
|
|
65
|
+
* surrounding spaces ignored). Unset, empty, `0`, `false`, `off` and anything
|
|
66
|
+
* else are off. New switches use it; the existing decision switches
|
|
67
|
+
* (FCD_HUMAN_PRESENT, FCD_EPHEMERAL, ...) keep their exact historical spellings,
|
|
68
|
+
* because widening them would change a verdict for the same input.
|
|
69
|
+
*/
|
|
70
|
+
export declare function envFlag(value: string | undefined): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The test-run markers set on THIS process (names; empty for every install).
|
|
73
|
+
* scripts/lib/no-real-dialog.js sets FCD_TEST_NO_REAL_DIALOG, the sandboxed
|
|
74
|
+
* daemon harnesses FCD_TEST_SANDBOX_ROOT, scripts/lib/fake-os-dialog.js
|
|
75
|
+
* FCD_FAKE_DIALOG_PID. No install, MSI, scheduled task or IDE hook config sets
|
|
76
|
+
* one. Read from process.env, never from a caller snapshot: the guards they
|
|
77
|
+
* drive (testRunGuard.ts, devConfirm, notify) protect this process's desktop
|
|
78
|
+
* and profile.
|
|
79
|
+
*/
|
|
80
|
+
export declare function testRunMarkers(env?: NodeJS.ProcessEnv): string[];
|
|
81
|
+
/** A test run that promised no real OS dialog or notification on this desktop (devConfirm, notify). */
|
|
82
|
+
export declare function noRealUiUnderTest(env?: NodeJS.ProcessEnv): boolean;
|
|
63
83
|
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
64
84
|
export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
|
|
65
85
|
/**
|
package/dist/callerEnv.js
CHANGED
|
@@ -8,6 +8,9 @@ exports.withCallerEnv = withCallerEnv;
|
|
|
8
8
|
exports.ideHookTimeoutMs = ideHookTimeoutMs;
|
|
9
9
|
exports.applyIdeTimeoutArg = applyIdeTimeoutArg;
|
|
10
10
|
exports.ideWaitCutoff = ideWaitCutoff;
|
|
11
|
+
exports.envFlag = envFlag;
|
|
12
|
+
exports.testRunMarkers = testRunMarkers;
|
|
13
|
+
exports.noRealUiUnderTest = noRealUiUnderTest;
|
|
11
14
|
exports.isCiEnvironment = isCiEnvironment;
|
|
12
15
|
exports.isHumanPresent = isHumanPresent;
|
|
13
16
|
const async_hooks_1 = require("async_hooks");
|
|
@@ -38,6 +41,10 @@ exports.CALLER_ENV_KEYS = [
|
|
|
38
41
|
'FCD_HOOK_IDE_TIMEOUT_MS', 'FCD_TAINT_APPROVE_ONCE_DISABLED', 'FCD_TAINT_APPROVAL_TIMEOUT_MS',
|
|
39
42
|
// Support diagnostics: per-phase hook.log lines for this caller's verdicts (never a decision fact).
|
|
40
43
|
'FCD_HOOK_DEBUG',
|
|
44
|
+
// Labs / diagnostics: keep the advisory "[FullCourtDefense shadow] would block" text on monitor verdicts (never a decision fact).
|
|
45
|
+
'FCD_MONITOR_HINTS',
|
|
46
|
+
// The perf snapshot's probe hook (telemetry.isPerfProbeRequest): recorded as no action (never a decision fact).
|
|
47
|
+
'FCD_PERF_PROBE',
|
|
41
48
|
// Which GCP project a gcloud line targets when it names none (cloudGrant.activeGcloudProject).
|
|
42
49
|
'CLOUDSDK_CORE_PROJECT', 'CLOUDSDK_ACTIVE_CONFIG_NAME', 'CLOUDSDK_CONFIG', 'APPDATA',
|
|
43
50
|
// Which aws profile / region and which kube context an aws / kubectl / helm line targets
|
|
@@ -143,6 +150,39 @@ function applyIdeTimeoutArg(seconds, env = process.env) {
|
|
|
143
150
|
function ideWaitCutoff(startedAt, ideTimeoutMs = ideHookTimeoutMs()) {
|
|
144
151
|
return startedAt + ideTimeoutMs - Math.min(exports.IDE_TIMEOUT_MARGIN_MS, ideTimeoutMs / 4);
|
|
145
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* One truthiness rule for FCD_* switches: `1`, `true`, `yes`, `on` (any case,
|
|
155
|
+
* surrounding spaces ignored). Unset, empty, `0`, `false`, `off` and anything
|
|
156
|
+
* else are off. New switches use it; the existing decision switches
|
|
157
|
+
* (FCD_HUMAN_PRESENT, FCD_EPHEMERAL, ...) keep their exact historical spellings,
|
|
158
|
+
* because widening them would change a verdict for the same input.
|
|
159
|
+
*/
|
|
160
|
+
function envFlag(value) {
|
|
161
|
+
return typeof value === 'string' && /^\s*(1|true|yes|on)\s*$/i.test(value);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The test-run markers set on THIS process (names; empty for every install).
|
|
165
|
+
* scripts/lib/no-real-dialog.js sets FCD_TEST_NO_REAL_DIALOG, the sandboxed
|
|
166
|
+
* daemon harnesses FCD_TEST_SANDBOX_ROOT, scripts/lib/fake-os-dialog.js
|
|
167
|
+
* FCD_FAKE_DIALOG_PID. No install, MSI, scheduled task or IDE hook config sets
|
|
168
|
+
* one. Read from process.env, never from a caller snapshot: the guards they
|
|
169
|
+
* drive (testRunGuard.ts, devConfirm, notify) protect this process's desktop
|
|
170
|
+
* and profile.
|
|
171
|
+
*/
|
|
172
|
+
function testRunMarkers(env = process.env) {
|
|
173
|
+
const found = [];
|
|
174
|
+
if (envFlag(env.FCD_TEST_NO_REAL_DIALOG))
|
|
175
|
+
found.push('FCD_TEST_NO_REAL_DIALOG');
|
|
176
|
+
if (env.FCD_TEST_SANDBOX_ROOT)
|
|
177
|
+
found.push('FCD_TEST_SANDBOX_ROOT');
|
|
178
|
+
if (env.FCD_FAKE_DIALOG_PID)
|
|
179
|
+
found.push('FCD_FAKE_DIALOG_PID');
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
/** A test run that promised no real OS dialog or notification on this desktop (devConfirm, notify). */
|
|
183
|
+
function noRealUiUnderTest(env = process.env) {
|
|
184
|
+
return envFlag(env.FCD_TEST_NO_REAL_DIALOG) || !!env.FCD_TEST_SANDBOX_ROOT;
|
|
185
|
+
}
|
|
146
186
|
/** CI / pipeline environments — no human at a keyboard. Shared by hooks and discovery. */
|
|
147
187
|
function isCiEnvironment(env = callerEnv()) {
|
|
148
188
|
return env.GITHUB_ACTIONS === 'true'
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const INTERCEPTED_COMMANDS: string[];
|
|
10
10
|
export declare function buildGuardJs(nodePath: string, cliEntry: string): string;
|
|
11
|
+
export declare function buildAutorunBat(nodePath: string): string;
|
|
11
12
|
export interface CmdGuardStatus {
|
|
12
13
|
supported: boolean;
|
|
13
14
|
installed: boolean;
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.INTERCEPTED_COMMANDS = void 0;
|
|
37
37
|
exports.buildGuardJs = buildGuardJs;
|
|
38
|
+
exports.buildAutorunBat = buildAutorunBat;
|
|
38
39
|
exports.isCmdGuardInstalled = isCmdGuardInstalled;
|
|
39
40
|
exports.getCmdGuardStatus = getCmdGuardStatus;
|
|
40
41
|
exports.installCmdGuardCommand = installCmdGuardCommand;
|
|
@@ -180,7 +181,8 @@ function buildGuardJs(nodePath, cliEntry) {
|
|
|
180
181
|
// delegate. A stack trace or nonzero exit here IS an outage for the shell.
|
|
181
182
|
`const line=args.join(' ');let mode='monitor';let hit=null;try{const r=loadRules();mode=r.mode;hit=matchLine(line,r.rules);}catch{hit=null;}`,
|
|
182
183
|
`if(!hit)delegate(args);`,
|
|
183
|
-
|
|
184
|
+
`// Monitor is quiet: a would-block / would-ask is recorded ('warn') and the command runs with nothing printed. A warn-only rule is a real warning (below).`,
|
|
185
|
+
`if(mode==='monitor'&&hit.action!=='warn'){spoolEvent(hit,line,'warn');delegate(args);}`,
|
|
184
186
|
`if(hit.action==='warn'){console.error('[FullCourtDefense] warning ['+(hit.severity||'')+']: '+hit.reason+' (rule '+hit.id+') — allowed by org policy, reported to your security dashboard.');spoolEvent(hit,line,'warn');delegate(args);}`,
|
|
185
187
|
`if(hit.action==='ask'){`,
|
|
186
188
|
` if(agentTerminal()){`,
|
|
@@ -224,6 +226,17 @@ function buildAutorunBat(nodePath) {
|
|
|
224
226
|
'REM Disable for one session: set FCD_CMD_GUARD=off',
|
|
225
227
|
'REM Remove permanently: fullcourtdefense uninstall-cmd-guard',
|
|
226
228
|
'if defined FCD_CMD_AUTORUN_DONE goto :eof',
|
|
229
|
+
// A one-shot cmd (`cmd /c ...`: build tools, npm scripts, agents) reads no
|
|
230
|
+
// typed line, and doskey macros only ever expand typed input — registering
|
|
231
|
+
// them there was pure overhead (+19-209 ms per start). `/k` (interactive
|
|
232
|
+
// after its command) keeps the guard. Decided before FCD_CMD_AUTORUN_DONE
|
|
233
|
+
// is set, so an interactive cmd started from a one-shot one still gets the
|
|
234
|
+
// guard. Delayed expansion: CMDCMDLINE may hold quotes and & | < > that a
|
|
235
|
+
// %-expanded test would execute or break on.
|
|
236
|
+
'setlocal EnableDelayedExpansion',
|
|
237
|
+
'set "_fcd_cmdline=!CMDCMDLINE!"',
|
|
238
|
+
'if /i not "!_fcd_cmdline:/c =!"=="!_fcd_cmdline!" if /i "!_fcd_cmdline:/k =!"=="!_fcd_cmdline!" (endlocal & goto :eof)',
|
|
239
|
+
'endlocal',
|
|
227
240
|
'set FCD_CMD_AUTORUN_DONE=1',
|
|
228
241
|
// FAIL OPEN: if the runtime or checker is gone (uninstall/upgrade left this
|
|
229
242
|
// bat behind), register NO macros — wrapped commands (docker, git, kubectl…)
|
|
@@ -2,6 +2,8 @@ import { BotGuardConfig } from '../config';
|
|
|
2
2
|
import { ProtectAllArgs } from './mcpGateway';
|
|
3
3
|
import '../askDialog';
|
|
4
4
|
import { windowsTaskRunsInteractive, windowsTaskRunsVisibly } from '../windowsTaskState';
|
|
5
|
+
/** Pure: is a discovery catch-up sweep due (`last` = the last successful upload, if known)? */
|
|
6
|
+
export declare function discoveryCatchUpDue(last: Date | undefined, nowMs?: number, staleMs?: number): boolean;
|
|
5
7
|
export interface DaemonArgs extends ProtectAllArgs {
|
|
6
8
|
install?: string;
|
|
7
9
|
uninstall?: string;
|
|
@@ -47,6 +49,24 @@ export declare function discoverSweepCredentialEnv(credentials?: {
|
|
|
47
49
|
apiUrl?: string;
|
|
48
50
|
}): Record<string, string>;
|
|
49
51
|
export declare function cliVersion(): string | undefined;
|
|
52
|
+
/** Up to five paths for a log line: " (a, b, c, +2 more)". */
|
|
53
|
+
export declare function namedFiles(files: string[], max?: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Take the single-instance lock. A NEWER build supersedes a running older
|
|
56
|
+
* daemon (e.g. the MSI-bundled copy kept running after an npm update): the old
|
|
57
|
+
* process is stopped and this one takes over, so the fleet never keeps running
|
|
58
|
+
* stale enforcement code silently. Same-or-newer versions keep the lock.
|
|
59
|
+
* `recycledPid` (from detectRecycledPidOwner) marks a pid-file owner that is
|
|
60
|
+
* NOT our daemon — take over without touching that unrelated process.
|
|
61
|
+
*
|
|
62
|
+
* Atomic (2026-09-27: two daemons started 1 ms apart both passed the old
|
|
63
|
+
* read-then-write lock and ran side by side): a missing pid file is created
|
|
64
|
+
* exclusively, and an existing one is taken over only under the take-over
|
|
65
|
+
* lock, after re-reading it there — of two contenders, the second sees the
|
|
66
|
+
* first's live pid and yields.
|
|
67
|
+
*/
|
|
68
|
+
export declare function acquirePidLock(recycledPid?: number): boolean;
|
|
69
|
+
export declare function releasePidLock(): void;
|
|
50
70
|
export type TaskLogonType = 'S4U' | 'InteractiveToken';
|
|
51
71
|
/** Fully-qualified current user (DOMAIN\\user) for the task principal. */
|
|
52
72
|
export declare function taskUserId(): string;
|