instar 1.3.761 → 1.3.763
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +92 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +28 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/FileClassifier.d.ts.map +1 -1
- package/dist/core/FileClassifier.js +5 -0
- package/dist/core/FileClassifier.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +12 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +54 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +16 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/componentCategories.d.ts.map +1 -1
- package/dist/core/componentCategories.js +1 -0
- package/dist/core/componentCategories.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +50 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +18 -0
- package/dist/data/llmBenchCoverage.js.map +1 -1
- package/dist/monitoring/ExternalHogArmMarker.d.ts +65 -0
- package/dist/monitoring/ExternalHogArmMarker.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogArmMarker.js +0 -0
- package/dist/monitoring/ExternalHogArmMarker.js.map +1 -0
- package/dist/monitoring/ExternalHogArmStore.d.ts +46 -0
- package/dist/monitoring/ExternalHogArmStore.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogArmStore.js +124 -0
- package/dist/monitoring/ExternalHogArmStore.js.map +1 -0
- package/dist/monitoring/ExternalHogClassifier.d.ts +62 -0
- package/dist/monitoring/ExternalHogClassifier.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogClassifier.js +0 -0
- package/dist/monitoring/ExternalHogClassifier.js.map +1 -0
- package/dist/monitoring/ExternalHogClassifierPrompt.d.ts +29 -0
- package/dist/monitoring/ExternalHogClassifierPrompt.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogClassifierPrompt.js +70 -0
- package/dist/monitoring/ExternalHogClassifierPrompt.js.map +1 -0
- package/dist/monitoring/ExternalHogCpuDelta.d.ts +50 -0
- package/dist/monitoring/ExternalHogCpuDelta.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogCpuDelta.js +79 -0
- package/dist/monitoring/ExternalHogCpuDelta.js.map +1 -0
- package/dist/monitoring/ExternalHogFactBuilder.d.ts +71 -0
- package/dist/monitoring/ExternalHogFactBuilder.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogFactBuilder.js +111 -0
- package/dist/monitoring/ExternalHogFactBuilder.js.map +1 -0
- package/dist/monitoring/ExternalHogFloor.d.ts +87 -0
- package/dist/monitoring/ExternalHogFloor.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogFloor.js +123 -0
- package/dist/monitoring/ExternalHogFloor.js.map +1 -0
- package/dist/monitoring/ExternalHogGuardStatus.d.ts +29 -0
- package/dist/monitoring/ExternalHogGuardStatus.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogGuardStatus.js +27 -0
- package/dist/monitoring/ExternalHogGuardStatus.js.map +1 -0
- package/dist/monitoring/ExternalHogKillFunnel.d.ts +77 -0
- package/dist/monitoring/ExternalHogKillFunnel.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogKillFunnel.js +72 -0
- package/dist/monitoring/ExternalHogKillFunnel.js.map +1 -0
- package/dist/monitoring/ExternalHogKillLedger.d.ts +76 -0
- package/dist/monitoring/ExternalHogKillLedger.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogKillLedger.js +84 -0
- package/dist/monitoring/ExternalHogKillLedger.js.map +1 -0
- package/dist/monitoring/ExternalHogNoticeCoalescer.d.ts +46 -0
- package/dist/monitoring/ExternalHogNoticeCoalescer.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogNoticeCoalescer.js +67 -0
- package/dist/monitoring/ExternalHogNoticeCoalescer.js.map +1 -0
- package/dist/monitoring/ExternalHogOwnership.d.ts +48 -0
- package/dist/monitoring/ExternalHogOwnership.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogOwnership.js +60 -0
- package/dist/monitoring/ExternalHogOwnership.js.map +1 -0
- package/dist/monitoring/ExternalHogProcTable.d.ts +37 -0
- package/dist/monitoring/ExternalHogProcTable.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogProcTable.js +65 -0
- package/dist/monitoring/ExternalHogProcTable.js.map +1 -0
- package/dist/monitoring/ExternalHogRealAdapters.d.ts +91 -0
- package/dist/monitoring/ExternalHogRealAdapters.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogRealAdapters.js +247 -0
- package/dist/monitoring/ExternalHogRealAdapters.js.map +1 -0
- package/dist/monitoring/ExternalHogSampler.d.ts +62 -0
- package/dist/monitoring/ExternalHogSampler.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogSampler.js +0 -0
- package/dist/monitoring/ExternalHogSampler.js.map +1 -0
- package/dist/monitoring/ExternalHogScanTick.d.ts +89 -0
- package/dist/monitoring/ExternalHogScanTick.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogScanTick.js +126 -0
- package/dist/monitoring/ExternalHogScanTick.js.map +1 -0
- package/dist/monitoring/ExternalHogSentinel.d.ts +127 -0
- package/dist/monitoring/ExternalHogSentinel.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogSentinel.js +168 -0
- package/dist/monitoring/ExternalHogSentinel.js.map +1 -0
- package/dist/monitoring/ExternalHogServerPrimitives.d.ts +50 -0
- package/dist/monitoring/ExternalHogServerPrimitives.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogServerPrimitives.js +93 -0
- package/dist/monitoring/ExternalHogServerPrimitives.js.map +1 -0
- package/dist/monitoring/ExternalHogSustained.d.ts +49 -0
- package/dist/monitoring/ExternalHogSustained.d.ts.map +1 -0
- package/dist/monitoring/ExternalHogSustained.js +62 -0
- package/dist/monitoring/ExternalHogSustained.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +24 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +64 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +51 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lint-scrape-fixture-realness.js +14 -0
- package/scripts/model-registry-freshness.manifest.json +2 -2
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/llmBenchCoverage.ts +18 -0
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.762.md +29 -0
- package/upgrades/1.3.763.md +23 -0
- package/upgrades/side-effects/doorway-model-registry-inc5.md +118 -0
- package/upgrades/side-effects/external-hog-sentinel.md +832 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogKillFunnel — the hardened kill sequence of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §4).
|
|
4
|
+
*
|
|
5
|
+
* The ONLY place a real signal is sent. Structured so the watch-only guarantee is by
|
|
6
|
+
* construction: unless a LIVE kill is authorized (`canKillLive` — enabled && !dryRun && a valid
|
|
7
|
+
* PIN armed marker for this class), the funnel sends NO signal and returns a `would-kill`
|
|
8
|
+
* record. All I/O (fact re-read, arm re-read, fd-probe, signal send, aliveness, clock, wait) is
|
|
9
|
+
* INJECTED so the sequence is fully testable without ever killing a process.
|
|
10
|
+
*
|
|
11
|
+
* Sequence (a candidate that has already passed §4 Stage-A CPU admission + §5 classifier=kill):
|
|
12
|
+
* 1. Gate: re-read the LIVE arm state → not authorized ⇒ `would-kill` (dry-run / not-armed).
|
|
13
|
+
* 2. Stage-B (pre-SIGTERM): re-read the LIVE facts → the §4 floor must still PERMIT; abort→alert.
|
|
14
|
+
* 3. SIGTERM → wait `sigtermGraceMs`.
|
|
15
|
+
* 4. If the process exited during grace → `sigterm-exited` (no SIGKILL needed).
|
|
16
|
+
* 5. Stage-B (pre-SIGKILL): re-read facts + arm → any change/disarm ⇒ abort (never SIGKILL).
|
|
17
|
+
* 6. fd-skip: an open writable WORKSPACE file ⇒ DEFER the SIGKILL (bounded by maxKillDeferrals).
|
|
18
|
+
* 7. SIGKILL → `killed`.
|
|
19
|
+
*/
|
|
20
|
+
import { evaluateKillFloor, matchAllowlistClass } from './ExternalHogFloor.js';
|
|
21
|
+
import { canKillLive } from './ExternalHogArmMarker.js';
|
|
22
|
+
/** Re-check the full authorization + floor for a target, live. Returns null if OK to proceed,
|
|
23
|
+
* or a `would-kill`/`aborted` outcome to short-circuit. */
|
|
24
|
+
async function reCheck(target, deps) {
|
|
25
|
+
const arm = deps.reReadArmState();
|
|
26
|
+
if (!canKillLive(arm.config, arm.marker, arm.lastDisarmEpoch, target.classId, deps.currentClassContentHash(target.classId))) {
|
|
27
|
+
return { action: 'would-kill', reason: arm.config && arm.config.dryRun !== false ? 'dry-run' : 'not-armed' };
|
|
28
|
+
}
|
|
29
|
+
const facts = await deps.reReadFacts(target.pid, target.startTime);
|
|
30
|
+
if (!facts)
|
|
31
|
+
return { action: 'aborted', reason: 'identity-changed-or-gone' };
|
|
32
|
+
// Re-verify the target is still in the allowlist class it was matched under (belt + suspenders).
|
|
33
|
+
if (matchAllowlistClass(facts.name, facts.argv) !== target.classId) {
|
|
34
|
+
return { action: 'aborted', reason: 'class-changed' };
|
|
35
|
+
}
|
|
36
|
+
const floor = evaluateKillFloor(facts);
|
|
37
|
+
if (!floor.permitted)
|
|
38
|
+
return { action: 'aborted', reason: `floor-veto:${floor.vetoReason}` };
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run the kill funnel for one target. Sends real signals ONLY when a live kill is authorized at
|
|
43
|
+
* BOTH re-check points; otherwise returns a non-destructive outcome. Pure control flow over the
|
|
44
|
+
* injected I/O.
|
|
45
|
+
*/
|
|
46
|
+
export async function runKillFunnel(target, opts, deps) {
|
|
47
|
+
// (1)+(2) Pre-SIGTERM gate + Stage-B floor re-check.
|
|
48
|
+
const pre = await reCheck(target, deps);
|
|
49
|
+
if (pre)
|
|
50
|
+
return pre; // would-kill (watch-only/unarmed) or aborted — NO signal sent.
|
|
51
|
+
// (3) SIGTERM → grace.
|
|
52
|
+
deps.sendSignal(target.pid, 'SIGTERM');
|
|
53
|
+
await deps.wait(Math.max(0, opts.sigtermGraceMs));
|
|
54
|
+
// (4) Exited gracefully during grace? Done.
|
|
55
|
+
if (!deps.stillAlive(target.pid, target.startTime))
|
|
56
|
+
return { action: 'sigterm-exited' };
|
|
57
|
+
// (5) Pre-SIGKILL re-check: a disarm / identity change / floor veto in the grace window aborts
|
|
58
|
+
// the escalation (the SIGTERM already sent is a graceful ask; we do NOT force-kill).
|
|
59
|
+
const post = await reCheck(target, deps);
|
|
60
|
+
if (post) {
|
|
61
|
+
// If it de-authorized (would-kill) or a fact changed (aborted) mid-grace, do NOT SIGKILL.
|
|
62
|
+
return post.action === 'would-kill' ? { action: 'aborted', reason: `disarmed-mid-grace:${post.reason}` } : post;
|
|
63
|
+
}
|
|
64
|
+
// (6) fd-skip: an in-progress workspace write DEFERS the SIGKILL (bounded).
|
|
65
|
+
if ((await deps.hasOpenWritableWorkspaceFile(target.pid)) && opts.currentDeferrals < opts.maxKillDeferrals) {
|
|
66
|
+
return { action: 'deferred', reason: 'writable-workspace-file' };
|
|
67
|
+
}
|
|
68
|
+
// (7) SIGKILL.
|
|
69
|
+
deps.sendSignal(target.pid, 'SIGKILL');
|
|
70
|
+
return { action: 'killed' };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=ExternalHogKillFunnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogKillFunnel.js","sourceRoot":"","sources":["../../src/monitoring/ExternalHogKillFunnel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogKillLedger — the P19 loop brakes of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §6).
|
|
4
|
+
*
|
|
5
|
+
* A durable ledger of kills that STOPS the sentinel from fighting a respawning process
|
|
6
|
+
* forever (the #863 reaper-kill-loop shape: 17,503 identical requests). Two brakes, both
|
|
7
|
+
* pure state machines here (the durable persistence + the actual kill are the caller's):
|
|
8
|
+
*
|
|
9
|
+
* 1. RESPAWN BREAKER — after K kills of the same respawn-surviving signature within a
|
|
10
|
+
* rolling window, STOP killing it and surface ONE degradation ("keeps respawning — may
|
|
11
|
+
* be managed"). The key is the command-hash + a stable discriminator (`--user-data-dir`);
|
|
12
|
+
* when only a VOLATILE discriminator is available (round-9), the anti-loop guarantee
|
|
13
|
+
* falls back to a CLASS-level breaker (a per-volatile-key breaker could never trip), so
|
|
14
|
+
* the loop is still bounded. The breaker shields same-key hogs from further KILL only —
|
|
15
|
+
* NEVER from the §4 observability floor.
|
|
16
|
+
*
|
|
17
|
+
* 2. IN-FLIGHT SET — a size-capped set of (pid, start-time) SIGTERM'd-but-not-yet-dead
|
|
18
|
+
* targets, so a process seen again before it dies is not re-killed each scan. Eviction is
|
|
19
|
+
* tied to CONFIRMED exit OR `inFlightKillTtlMs` (~3×sigtermGrace, NOT 2× — which could
|
|
20
|
+
* SIGKILL a mid-write language server early).
|
|
21
|
+
*/
|
|
22
|
+
export interface KillRecord {
|
|
23
|
+
/** The respawn-surviving ledger key (command-hash + stable discriminator). */
|
|
24
|
+
readonly key: string;
|
|
25
|
+
/** The allowlist class id (used for the class-level fallback breaker). */
|
|
26
|
+
readonly classId: string;
|
|
27
|
+
/** Monotonic-ish wall timestamp of the kill (ms). */
|
|
28
|
+
readonly atMs: number;
|
|
29
|
+
}
|
|
30
|
+
export interface KillLedgerState {
|
|
31
|
+
readonly records: readonly KillRecord[];
|
|
32
|
+
}
|
|
33
|
+
export declare const EMPTY_KILL_LEDGER: KillLedgerState;
|
|
34
|
+
export interface BreakerOpts {
|
|
35
|
+
readonly nowMs: number;
|
|
36
|
+
/** Rolling window (ms) the K-count is measured over (default 1h). */
|
|
37
|
+
readonly windowMs: number;
|
|
38
|
+
/** K — kills of the same key within the window before the breaker trips (default 3). */
|
|
39
|
+
readonly maxPerWindow: number;
|
|
40
|
+
/** True when the ledger key is a VOLATILE fallback (no stable discriminator) — count by
|
|
41
|
+
* CLASS instead of key, since a per-volatile-key count could never accumulate. */
|
|
42
|
+
readonly keyIsVolatile: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Append a kill and PRUNE records older than the retention bound (so the durable ledger can
|
|
46
|
+
* never grow without limit). Returns a NEW state (pure).
|
|
47
|
+
*
|
|
48
|
+
* CALLER PRECONDITION: `retentionMs >= isBreakerTripped(...).windowMs`. `recordKill` cannot
|
|
49
|
+
* see the breaker window, so if retention were shorter it would prune kills the breaker still
|
|
50
|
+
* needs and silently UNDERCOUNT (a spurious not-tripped). The config wires retention ≥ window;
|
|
51
|
+
* a test asserts the breaker still trips at K when retention == window.
|
|
52
|
+
*/
|
|
53
|
+
export declare function recordKill(state: KillLedgerState, record: KillRecord, retentionMs: number, nowMs: number): KillLedgerState;
|
|
54
|
+
/**
|
|
55
|
+
* Is the breaker TRIPPED for this (key, classId) — i.e. should the sentinel STOP killing it?
|
|
56
|
+
* True when ≥ `maxPerWindow` kills of the same signature fall within the rolling window. For a
|
|
57
|
+
* volatile key, the signature is the CLASS; otherwise it is the exact key.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isBreakerTripped(state: KillLedgerState, key: string, classId: string, opts: BreakerOpts): boolean;
|
|
60
|
+
/** Count of in-window kills for a signature (for the degradation message / observability). */
|
|
61
|
+
export declare function killCountInWindow(state: KillLedgerState, key: string, classId: string, opts: BreakerOpts): number;
|
|
62
|
+
export interface InFlightKill {
|
|
63
|
+
readonly pid: number;
|
|
64
|
+
readonly startTime: string;
|
|
65
|
+
/** When SIGTERM was sent (ms). */
|
|
66
|
+
readonly sigtermAtMs: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Should an in-flight entry be EVICTED? Yes on CONFIRMED exit, or once `ttlMs` has elapsed
|
|
70
|
+
* since SIGTERM (a ceiling comfortably longer than sigtermGrace so a mid-write LS is not
|
|
71
|
+
* re-entered early). A non-finite `nowMs`/`ttlMs` evicts (fail toward not leaking the set).
|
|
72
|
+
*/
|
|
73
|
+
export declare function shouldEvictInFlight(entry: InFlightKill, nowMs: number, ttlMs: number, confirmedGone: boolean): boolean;
|
|
74
|
+
/** Is this pid+start-time already in-flight (so we must NOT re-kill it this scan)? */
|
|
75
|
+
export declare function isInFlight(set: readonly InFlightKill[], pid: number, startTime: string): boolean;
|
|
76
|
+
//# sourceMappingURL=ExternalHogKillLedger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogKillLedger.d.ts","sourceRoot":"","sources":["../../src/monitoring/ExternalHogKillLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;CACzC;AAED,eAAO,MAAM,iBAAiB,EAAE,eAAiC,CAAC;AAElE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;uFACmF;IACnF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CAI1H;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAqBjH;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAGjH;AAID,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kCAAkC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAItH;AAED,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,GAAG,EAAE,SAAS,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAEhG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogKillLedger — the P19 loop brakes of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §6).
|
|
4
|
+
*
|
|
5
|
+
* A durable ledger of kills that STOPS the sentinel from fighting a respawning process
|
|
6
|
+
* forever (the #863 reaper-kill-loop shape: 17,503 identical requests). Two brakes, both
|
|
7
|
+
* pure state machines here (the durable persistence + the actual kill are the caller's):
|
|
8
|
+
*
|
|
9
|
+
* 1. RESPAWN BREAKER — after K kills of the same respawn-surviving signature within a
|
|
10
|
+
* rolling window, STOP killing it and surface ONE degradation ("keeps respawning — may
|
|
11
|
+
* be managed"). The key is the command-hash + a stable discriminator (`--user-data-dir`);
|
|
12
|
+
* when only a VOLATILE discriminator is available (round-9), the anti-loop guarantee
|
|
13
|
+
* falls back to a CLASS-level breaker (a per-volatile-key breaker could never trip), so
|
|
14
|
+
* the loop is still bounded. The breaker shields same-key hogs from further KILL only —
|
|
15
|
+
* NEVER from the §4 observability floor.
|
|
16
|
+
*
|
|
17
|
+
* 2. IN-FLIGHT SET — a size-capped set of (pid, start-time) SIGTERM'd-but-not-yet-dead
|
|
18
|
+
* targets, so a process seen again before it dies is not re-killed each scan. Eviction is
|
|
19
|
+
* tied to CONFIRMED exit OR `inFlightKillTtlMs` (~3×sigtermGrace, NOT 2× — which could
|
|
20
|
+
* SIGKILL a mid-write language server early).
|
|
21
|
+
*/
|
|
22
|
+
export const EMPTY_KILL_LEDGER = { records: [] };
|
|
23
|
+
/**
|
|
24
|
+
* Append a kill and PRUNE records older than the retention bound (so the durable ledger can
|
|
25
|
+
* never grow without limit). Returns a NEW state (pure).
|
|
26
|
+
*
|
|
27
|
+
* CALLER PRECONDITION: `retentionMs >= isBreakerTripped(...).windowMs`. `recordKill` cannot
|
|
28
|
+
* see the breaker window, so if retention were shorter it would prune kills the breaker still
|
|
29
|
+
* needs and silently UNDERCOUNT (a spurious not-tripped). The config wires retention ≥ window;
|
|
30
|
+
* a test asserts the breaker still trips at K when retention == window.
|
|
31
|
+
*/
|
|
32
|
+
export function recordKill(state, record, retentionMs, nowMs) {
|
|
33
|
+
const cutoff = nowMs - retentionMs;
|
|
34
|
+
const kept = state.records.filter((r) => r.atMs >= cutoff);
|
|
35
|
+
return { records: [...kept, record] };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Is the breaker TRIPPED for this (key, classId) — i.e. should the sentinel STOP killing it?
|
|
39
|
+
* True when ≥ `maxPerWindow` kills of the same signature fall within the rolling window. For a
|
|
40
|
+
* volatile key, the signature is the CLASS; otherwise it is the exact key.
|
|
41
|
+
*/
|
|
42
|
+
export function isBreakerTripped(state, key, classId, opts) {
|
|
43
|
+
// Fail toward the SAFE direction for a loop brake: if we can't reason about the window,
|
|
44
|
+
// TRIP (stop killing) rather than risk an unbounded loop. This must reject not only
|
|
45
|
+
// NON-FINITE inputs but also a NON-POSITIVE `windowMs` (≤ 0 is finite but nonsensical: it
|
|
46
|
+
// would make `since = now - windowMs >= now`, so every real record falls before `since`, the
|
|
47
|
+
// count collapses to ~0, and the breaker would spuriously report NOT-tripped — the dangerous
|
|
48
|
+
// unbounded-loop direction). `maxPerWindow ≤ 0` is safe either way (trips always) but is
|
|
49
|
+
// treated as unreasoned too. (round-11 — second-pass reviewer.)
|
|
50
|
+
if (!Number.isFinite(opts.nowMs) ||
|
|
51
|
+
!(Number.isFinite(opts.windowMs) && opts.windowMs > 0) ||
|
|
52
|
+
!(Number.isFinite(opts.maxPerWindow) && opts.maxPerWindow > 0)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const since = opts.nowMs - opts.windowMs;
|
|
56
|
+
const count = state.records.filter((r) => {
|
|
57
|
+
if (r.atMs < since)
|
|
58
|
+
return false;
|
|
59
|
+
return opts.keyIsVolatile ? r.classId === classId : r.key === key;
|
|
60
|
+
}).length;
|
|
61
|
+
return count >= opts.maxPerWindow;
|
|
62
|
+
}
|
|
63
|
+
/** Count of in-window kills for a signature (for the degradation message / observability). */
|
|
64
|
+
export function killCountInWindow(state, key, classId, opts) {
|
|
65
|
+
const since = opts.nowMs - opts.windowMs;
|
|
66
|
+
return state.records.filter((r) => r.atMs >= since && (opts.keyIsVolatile ? r.classId === classId : r.key === key)).length;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Should an in-flight entry be EVICTED? Yes on CONFIRMED exit, or once `ttlMs` has elapsed
|
|
70
|
+
* since SIGTERM (a ceiling comfortably longer than sigtermGrace so a mid-write LS is not
|
|
71
|
+
* re-entered early). A non-finite `nowMs`/`ttlMs` evicts (fail toward not leaking the set).
|
|
72
|
+
*/
|
|
73
|
+
export function shouldEvictInFlight(entry, nowMs, ttlMs, confirmedGone) {
|
|
74
|
+
if (confirmedGone)
|
|
75
|
+
return true;
|
|
76
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(ttlMs) || !Number.isFinite(entry.sigtermAtMs))
|
|
77
|
+
return true;
|
|
78
|
+
return nowMs - entry.sigtermAtMs >= ttlMs;
|
|
79
|
+
}
|
|
80
|
+
/** Is this pid+start-time already in-flight (so we must NOT re-kill it this scan)? */
|
|
81
|
+
export function isInFlight(set, pid, startTime) {
|
|
82
|
+
return set.some((e) => e.pid === pid && e.startTime === startTime);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=ExternalHogKillLedger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogKillLedger.js","sourceRoot":"","sources":["../../src/monitoring/ExternalHogKillLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAeH,MAAM,CAAC,MAAM,iBAAiB,GAAoB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AAalE;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB,EAAE,MAAkB,EAAE,WAAmB,EAAE,KAAa;IACvG,MAAM,MAAM,GAAG,KAAK,GAAG,WAAW,CAAC;IACnC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC;IAC3D,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAsB,EAAE,GAAW,EAAE,OAAe,EAAE,IAAiB;IACtG,wFAAwF;IACxF,oFAAoF;IACpF,0FAA0F;IAC1F,6FAA6F;IAC7F,6FAA6F;IAC7F,yFAAyF;IACzF,gEAAgE;IAChE,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QAC5B,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QACtD,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,EAC9D,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACvC,IAAI,CAAC,CAAC,IAAI,GAAG,KAAK;YAAE,OAAO,KAAK,CAAC;QACjC,OAAO,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC;IACpE,CAAC,CAAC,CAAC,MAAM,CAAC;IACV,OAAO,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC;AACpC,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,iBAAiB,CAAC,KAAsB,EAAE,GAAW,EAAE,OAAe,EAAE,IAAiB;IACvG,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;AAC7H,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAmB,EAAE,KAAa,EAAE,KAAa,EAAE,aAAsB;IAC3G,IAAI,aAAa;QAAE,OAAO,IAAI,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3G,OAAO,KAAK,GAAG,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC;AAC5C,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,UAAU,CAAC,GAA4B,EAAE,GAAW,EAAE,SAAiB;IACrF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogNoticeCoalescer — the P17 notification-bounding of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §6).
|
|
4
|
+
*
|
|
5
|
+
* A SINGLE coalescing chokepoint over EVERY notice class — kills, decider-unavailable alerts,
|
|
6
|
+
* floor-veto downgrades, and the deterministic "sustained hog left alive" observability
|
|
7
|
+
* alerts — so a persistent condition (a hog the model keeps sparing, a provider outage) raises
|
|
8
|
+
* ONE coalesced notice, not one-per-scan. Pure selection logic (the actual delivery is the
|
|
9
|
+
* caller's). Rules (§6):
|
|
10
|
+
* - per-signature DEDUP: the same (class, signature) within the window collapses to one.
|
|
11
|
+
* - per-window BUDGET (`noticeBudgetPerWindow`): at most N notices per window.
|
|
12
|
+
* - budget-exhaustion SEVERITY ORDER: kill > decider-unavailable > floor-veto-downgrade >
|
|
13
|
+
* hog-left-alive. Lower classes are dropped first, and the DROPPED count is reported.
|
|
14
|
+
* - a LIVE KILL notice ALWAYS pierces the budget (never dropped) — an actual kill must be
|
|
15
|
+
* surfaced no matter how noisy the window is.
|
|
16
|
+
*/
|
|
17
|
+
export type NoticeClass = 'kill' | 'decider-unavailable' | 'floor-veto-downgrade' | 'hog-left-alive';
|
|
18
|
+
export interface Notice {
|
|
19
|
+
readonly cls: NoticeClass;
|
|
20
|
+
/** Per-signature dedup key (e.g. the command-hash / class id). */
|
|
21
|
+
readonly signature: string;
|
|
22
|
+
/** Human-facing text (already scrubbed by the caller). */
|
|
23
|
+
readonly text: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CoalesceResult {
|
|
26
|
+
/** The notices to actually emit this cycle (deduped, budget-bounded, kills always kept). */
|
|
27
|
+
readonly emitted: readonly Notice[];
|
|
28
|
+
/** How many notices of each class were dropped by the budget (for the coalesced summary). */
|
|
29
|
+
readonly droppedByClass: Readonly<Partial<Record<NoticeClass, number>>>;
|
|
30
|
+
/** Total dropped. */
|
|
31
|
+
readonly droppedTotal: number;
|
|
32
|
+
}
|
|
33
|
+
export interface CoalesceOpts {
|
|
34
|
+
/** Max notices per window (default 4). Live kills are exempt (always kept). */
|
|
35
|
+
readonly budgetPerWindow: number;
|
|
36
|
+
/** Signatures already emitted earlier in this window (per-signature dedup across the window). */
|
|
37
|
+
readonly alreadyEmittedSignatures?: ReadonlySet<string>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Select which notices to emit this cycle. Dedups by (class, signature) within the batch and
|
|
41
|
+
* against `alreadyEmittedSignatures`, keeps ALL live kills (they pierce the budget), then fills
|
|
42
|
+
* the remaining budget with the highest-severity classes first; the rest are dropped and
|
|
43
|
+
* counted. Pure — returns a new result, mutates nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function coalesceNotices(notices: readonly Notice[], opts: CoalesceOpts): CoalesceResult;
|
|
46
|
+
//# sourceMappingURL=ExternalHogNoticeCoalescer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogNoticeCoalescer.d.ts","sourceRoot":"","sources":["../../src/monitoring/ExternalHogNoticeCoalescer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,qBAAqB,GAAG,sBAAsB,GAAG,gBAAgB,CAAC;AAUrG,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6FAA6F;IAC7F,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACxE,qBAAqB;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,iGAAiG;IACjG,QAAQ,CAAC,wBAAwB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,GAAG,cAAc,CAkC9F"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogNoticeCoalescer — the P17 notification-bounding of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §6).
|
|
4
|
+
*
|
|
5
|
+
* A SINGLE coalescing chokepoint over EVERY notice class — kills, decider-unavailable alerts,
|
|
6
|
+
* floor-veto downgrades, and the deterministic "sustained hog left alive" observability
|
|
7
|
+
* alerts — so a persistent condition (a hog the model keeps sparing, a provider outage) raises
|
|
8
|
+
* ONE coalesced notice, not one-per-scan. Pure selection logic (the actual delivery is the
|
|
9
|
+
* caller's). Rules (§6):
|
|
10
|
+
* - per-signature DEDUP: the same (class, signature) within the window collapses to one.
|
|
11
|
+
* - per-window BUDGET (`noticeBudgetPerWindow`): at most N notices per window.
|
|
12
|
+
* - budget-exhaustion SEVERITY ORDER: kill > decider-unavailable > floor-veto-downgrade >
|
|
13
|
+
* hog-left-alive. Lower classes are dropped first, and the DROPPED count is reported.
|
|
14
|
+
* - a LIVE KILL notice ALWAYS pierces the budget (never dropped) — an actual kill must be
|
|
15
|
+
* surfaced no matter how noisy the window is.
|
|
16
|
+
*/
|
|
17
|
+
/** Severity for budget-exhaustion ordering (higher = kept first). */
|
|
18
|
+
const CLASS_SEVERITY = {
|
|
19
|
+
kill: 3,
|
|
20
|
+
'decider-unavailable': 2,
|
|
21
|
+
'floor-veto-downgrade': 1,
|
|
22
|
+
'hog-left-alive': 0,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Select which notices to emit this cycle. Dedups by (class, signature) within the batch and
|
|
26
|
+
* against `alreadyEmittedSignatures`, keeps ALL live kills (they pierce the budget), then fills
|
|
27
|
+
* the remaining budget with the highest-severity classes first; the rest are dropped and
|
|
28
|
+
* counted. Pure — returns a new result, mutates nothing.
|
|
29
|
+
*/
|
|
30
|
+
export function coalesceNotices(notices, opts) {
|
|
31
|
+
const budget = Number.isFinite(opts.budgetPerWindow) && opts.budgetPerWindow > 0 ? Math.floor(opts.budgetPerWindow) : 0;
|
|
32
|
+
const already = opts.alreadyEmittedSignatures ?? new Set();
|
|
33
|
+
// (1) per-signature dedup within the batch (first wins) AND vs the window's prior emissions.
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const deduped = [];
|
|
36
|
+
for (const n of notices) {
|
|
37
|
+
if (!n || !isNoticeClass(n.cls))
|
|
38
|
+
continue;
|
|
39
|
+
const key = `${n.cls}::${n.signature}`;
|
|
40
|
+
if (seen.has(key))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
if (already.has(key))
|
|
44
|
+
continue; // already surfaced this window
|
|
45
|
+
deduped.push(n);
|
|
46
|
+
}
|
|
47
|
+
// (2) live kills ALWAYS pierce the budget.
|
|
48
|
+
const kills = deduped.filter((n) => n.cls === 'kill');
|
|
49
|
+
const nonKills = deduped.filter((n) => n.cls !== 'kill');
|
|
50
|
+
// (3) fill the remaining budget with the highest-severity non-kill classes first (stable).
|
|
51
|
+
const remaining = Math.max(0, budget - kills.length);
|
|
52
|
+
const orderedNonKills = [...nonKills].sort((a, b) => CLASS_SEVERITY[b.cls] - CLASS_SEVERITY[a.cls]);
|
|
53
|
+
const keptNonKills = orderedNonKills.slice(0, remaining);
|
|
54
|
+
const droppedNonKills = orderedNonKills.slice(remaining);
|
|
55
|
+
const droppedByClass = {};
|
|
56
|
+
for (const n of droppedNonKills)
|
|
57
|
+
droppedByClass[n.cls] = (droppedByClass[n.cls] ?? 0) + 1;
|
|
58
|
+
return {
|
|
59
|
+
emitted: [...kills, ...keptNonKills],
|
|
60
|
+
droppedByClass,
|
|
61
|
+
droppedTotal: droppedNonKills.length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function isNoticeClass(c) {
|
|
65
|
+
return c === 'kill' || c === 'decider-unavailable' || c === 'floor-veto-downgrade' || c === 'hog-left-alive';
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ExternalHogNoticeCoalescer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogNoticeCoalescer.js","sourceRoot":"","sources":["../../src/monitoring/ExternalHogNoticeCoalescer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,qEAAqE;AACrE,MAAM,cAAc,GAAgC;IAClD,IAAI,EAAE,CAAC;IACP,qBAAqB,EAAE,CAAC;IACxB,sBAAsB,EAAE,CAAC;IACzB,gBAAgB,EAAE,CAAC;CACpB,CAAC;AA0BF;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAA0B,EAAE,IAAkB;IAC5E,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxH,MAAM,OAAO,GAAG,IAAI,CAAC,wBAAwB,IAAI,IAAI,GAAG,EAAU,CAAC;IAEnE,6FAA6F;IAC7F,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC;YAAE,SAAS;QAC1C,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,+BAA+B;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,2CAA2C;IAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;IAEzD,2FAA2F;IAC3F,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,eAAe,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpG,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACzD,MAAM,eAAe,GAAG,eAAe,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAEzD,MAAM,cAAc,GAAyC,EAAE,CAAC;IAChE,KAAK,MAAM,CAAC,IAAI,eAAe;QAAE,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAE1F,OAAO;QACL,OAAO,EAAE,CAAC,GAAG,KAAK,EAAE,GAAG,YAAY,CAAC;QACpC,cAAc;QACd,YAAY,EAAE,eAAe,CAAC,MAAM;KACrC,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,CAAU;IAC/B,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,qBAAqB,IAAI,CAAC,KAAK,sBAAsB,IAAI,CAAC,KAAK,gBAAgB,CAAC;AAC/G,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogOwnership — the instar-own-process exclusion of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §1).
|
|
4
|
+
*
|
|
5
|
+
* A candidate must be EXCLUDED from discovery if its ancestry reaches an instar-owned pid —
|
|
6
|
+
* otherwise a legitimately-busy build/test child of a live instar session (a vitest worker,
|
|
7
|
+
* `tsc`, a compiler) becomes a confirmed sustained hog and spams the observability floor
|
|
8
|
+
* during exactly the heavy work instar is designed for (the 2026-07-02 29-vitest-root storm).
|
|
9
|
+
*
|
|
10
|
+
* This is the PURE ancestry walk over a process-tree SNAPSHOT (the actual `ps` read that
|
|
11
|
+
* builds the tree is a later I/O slice). Two round-9/10-reviewed design points:
|
|
12
|
+
*
|
|
13
|
+
* 1. START-TIME-AWARE (not a bare pid walk): `resolveOwningSession` supplies only the
|
|
14
|
+
* cycle-guard + hop-bound and takes NO start-time — so this EXTENDS it. Each owned pid is
|
|
15
|
+
* matched by BOTH pid AND its recorded start-time, so a REUSED pid that coincidentally
|
|
16
|
+
* equals an instar-owned pid number does NOT falsely exclude (its start-time won't match).
|
|
17
|
+
*
|
|
18
|
+
* 2. INCLUDE-on-uncertainty is the ANTI-EVASION direction: if the chain can't be resolved
|
|
19
|
+
* (a hop's pid isn't in the snapshot, or a cycle/hop-bound), the candidate is treated as
|
|
20
|
+
* NOT instar-owned (stays a candidate). instar-own KILL protection is carried NOT by this
|
|
21
|
+
* walk but by reparent-to-pid-1 semantics + the §4 allowlist floor (an instar build child
|
|
22
|
+
* won't match the exthost allowlist regex anyway) — so INCLUDE-on-uncertainty costs only
|
|
23
|
+
* occasional observability noise, never a wrong kill, and it prevents an external hog from
|
|
24
|
+
* FALSE-EXCLUDING itself by faking an instar ancestor via a stale/foreign edge.
|
|
25
|
+
*
|
|
26
|
+
* BOTH a tmux-pane ancestor AND an own-root pid count (round-10 integration: under launchd
|
|
27
|
+
* supervision the launchd-direct lifeline has NO tmux ancestor, so a tmux-only walk misses
|
|
28
|
+
* its descendants). The caller supplies both sets, merged into `ownedRefs`.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProcNode {
|
|
31
|
+
readonly pid: number;
|
|
32
|
+
readonly ppid: number;
|
|
33
|
+
/** The process's start-time (from `ps lstart=`), used to defeat pid reuse. */
|
|
34
|
+
readonly startTime: string;
|
|
35
|
+
}
|
|
36
|
+
/** A process-tree snapshot: pid → its node. Built by the sampler from one `ps` read. */
|
|
37
|
+
export type ProcTree = ReadonlyMap<number, ProcNode>;
|
|
38
|
+
/** instar-owned pids (server `process.pid`, sampler pid, resolvable lifeline pid, tmux panes),
|
|
39
|
+
* each mapped to its EXPECTED start-time so a reused pid can't spoof ownership. */
|
|
40
|
+
export type OwnedRefs = ReadonlyMap<number, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Is `candidatePid` instar-owned — i.e. does its ppid-chain reach an owned pid whose recorded
|
|
43
|
+
* start-time matches? Walks up to `maxHops`, cycle-guarded. Returns FALSE (not owned → stays a
|
|
44
|
+
* candidate) on any unresolvable edge or on reaching init (ppid ≤ 1) — the anti-evasion
|
|
45
|
+
* direction. `maxHops <= 0` or a non-finite candidate → not owned (nothing to verify).
|
|
46
|
+
*/
|
|
47
|
+
export declare function isInstarOwned(candidatePid: number, tree: ProcTree, ownedRefs: OwnedRefs, maxHops: number): boolean;
|
|
48
|
+
//# sourceMappingURL=ExternalHogOwnership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogOwnership.d.ts","sourceRoot":"","sources":["../../src/monitoring/ExternalHogOwnership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,wFAAwF;AACxF,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAErD;oFACoF;AACpF,MAAM,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAqBlH"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogOwnership — the instar-own-process exclusion of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §1).
|
|
4
|
+
*
|
|
5
|
+
* A candidate must be EXCLUDED from discovery if its ancestry reaches an instar-owned pid —
|
|
6
|
+
* otherwise a legitimately-busy build/test child of a live instar session (a vitest worker,
|
|
7
|
+
* `tsc`, a compiler) becomes a confirmed sustained hog and spams the observability floor
|
|
8
|
+
* during exactly the heavy work instar is designed for (the 2026-07-02 29-vitest-root storm).
|
|
9
|
+
*
|
|
10
|
+
* This is the PURE ancestry walk over a process-tree SNAPSHOT (the actual `ps` read that
|
|
11
|
+
* builds the tree is a later I/O slice). Two round-9/10-reviewed design points:
|
|
12
|
+
*
|
|
13
|
+
* 1. START-TIME-AWARE (not a bare pid walk): `resolveOwningSession` supplies only the
|
|
14
|
+
* cycle-guard + hop-bound and takes NO start-time — so this EXTENDS it. Each owned pid is
|
|
15
|
+
* matched by BOTH pid AND its recorded start-time, so a REUSED pid that coincidentally
|
|
16
|
+
* equals an instar-owned pid number does NOT falsely exclude (its start-time won't match).
|
|
17
|
+
*
|
|
18
|
+
* 2. INCLUDE-on-uncertainty is the ANTI-EVASION direction: if the chain can't be resolved
|
|
19
|
+
* (a hop's pid isn't in the snapshot, or a cycle/hop-bound), the candidate is treated as
|
|
20
|
+
* NOT instar-owned (stays a candidate). instar-own KILL protection is carried NOT by this
|
|
21
|
+
* walk but by reparent-to-pid-1 semantics + the §4 allowlist floor (an instar build child
|
|
22
|
+
* won't match the exthost allowlist regex anyway) — so INCLUDE-on-uncertainty costs only
|
|
23
|
+
* occasional observability noise, never a wrong kill, and it prevents an external hog from
|
|
24
|
+
* FALSE-EXCLUDING itself by faking an instar ancestor via a stale/foreign edge.
|
|
25
|
+
*
|
|
26
|
+
* BOTH a tmux-pane ancestor AND an own-root pid count (round-10 integration: under launchd
|
|
27
|
+
* supervision the launchd-direct lifeline has NO tmux ancestor, so a tmux-only walk misses
|
|
28
|
+
* its descendants). The caller supplies both sets, merged into `ownedRefs`.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Is `candidatePid` instar-owned — i.e. does its ppid-chain reach an owned pid whose recorded
|
|
32
|
+
* start-time matches? Walks up to `maxHops`, cycle-guarded. Returns FALSE (not owned → stays a
|
|
33
|
+
* candidate) on any unresolvable edge or on reaching init (ppid ≤ 1) — the anti-evasion
|
|
34
|
+
* direction. `maxHops <= 0` or a non-finite candidate → not owned (nothing to verify).
|
|
35
|
+
*/
|
|
36
|
+
export function isInstarOwned(candidatePid, tree, ownedRefs, maxHops) {
|
|
37
|
+
if (!Number.isInteger(candidatePid) || candidatePid <= 0)
|
|
38
|
+
return false;
|
|
39
|
+
if (!Number.isFinite(maxHops) || maxHops <= 0)
|
|
40
|
+
return false;
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
let current = candidatePid;
|
|
43
|
+
for (let hop = 0; hop < maxHops; hop++) {
|
|
44
|
+
if (seen.has(current))
|
|
45
|
+
return false; // cycle → can't resolve → not owned (anti-evasion)
|
|
46
|
+
seen.add(current);
|
|
47
|
+
const node = tree.get(current);
|
|
48
|
+
if (!node)
|
|
49
|
+
return false; // pid not in the snapshot → unresolvable → not owned
|
|
50
|
+
// Owned iff this pid is a known instar pid AND its start-time matches (defeats pid reuse).
|
|
51
|
+
const expectedStart = ownedRefs.get(current);
|
|
52
|
+
if (expectedStart !== undefined && expectedStart === node.startTime)
|
|
53
|
+
return true;
|
|
54
|
+
if (node.ppid <= 1)
|
|
55
|
+
return false; // reached init/launchd — a genuine orphan, no instar ancestor
|
|
56
|
+
current = node.ppid;
|
|
57
|
+
}
|
|
58
|
+
return false; // hop-bound exceeded → not owned (bounded; the floor still gates any kill)
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=ExternalHogOwnership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogOwnership.js","sourceRoot":"","sources":["../../src/monitoring/ExternalHogOwnership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAgBH;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,IAAc,EAAE,SAAoB,EAAE,OAAe;IACvG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACvE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAE5D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,OAAO,GAAG,YAAY,CAAC;IAC3B,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,mDAAmD;QACxF,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAElB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC,CAAC,qDAAqD;QAE9E,2FAA2F;QAC3F,MAAM,aAAa,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7C,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,KAAK,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAEjF,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,8DAA8D;QAChG,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC;IACtB,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,2EAA2E;AAC3F,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogProcTable — the whole-table `ps` parser of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §1).
|
|
4
|
+
*
|
|
5
|
+
* Parses the output of `ps -o pid=,ppid=,uid=,lstart=,time=,comm=` (no header) into rows the
|
|
6
|
+
* sampler uses to build the ProcTree + the per-pid cumulative-CPU-time map. This parser is
|
|
7
|
+
* LOAD-BEARING for kill eligibility (the CPU-delta pivots on `time=`), so it is REGISTERED in
|
|
8
|
+
* SCRAPE_PARSERS with a captured realness fixture (§Testing) — it must survive the real
|
|
9
|
+
* structural bytes: a `[dd-]hh:mm:ss` day-prefix time (the ~24h anchor case), an `lstart`
|
|
10
|
+
* value with embedded spaces + space-padded day-of-month, a `comm` with embedded spaces, a
|
|
11
|
+
* `<defunct>` row, and a short/permission-denied row.
|
|
12
|
+
*
|
|
13
|
+
* Fail-closed: a row whose pid/ppid/uid can't be parsed is SKIPPED (an unidentifiable process
|
|
14
|
+
* is not a candidate); a row whose `time=` is malformed keeps the row but sets
|
|
15
|
+
* `cputimeSeconds: undefined` → the CPU-delta yields UNKNOWN → alert-never-kill.
|
|
16
|
+
*/
|
|
17
|
+
export interface ProcTableRow {
|
|
18
|
+
readonly pid: number;
|
|
19
|
+
readonly ppid: number;
|
|
20
|
+
readonly uid: number;
|
|
21
|
+
/** The `lstart` value (5 whitespace tokens joined) — an OPAQUE identity token (pid-reuse
|
|
22
|
+
* guard), never parsed as a date. */
|
|
23
|
+
readonly startTime: string;
|
|
24
|
+
/** Cumulative (user+system) CPU seconds from `time=`, or `undefined` if unparseable. */
|
|
25
|
+
readonly cputimeSeconds: number | undefined;
|
|
26
|
+
/** The command (last field; may contain spaces). Attacker-controllable — treated as data. */
|
|
27
|
+
readonly comm: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse `ps -o pid=,ppid=,uid=,lstart=,time=,comm=` output into rows. Column layout per line:
|
|
31
|
+
* pid ppid uid <Dow Mon DD HH:MM:SS YYYY> time comm...
|
|
32
|
+
* The first 3 fields are numeric, `lstart` is exactly 5 whitespace tokens (the space-padded
|
|
33
|
+
* day-of-month collapses under a whitespace-run split), `time` is one token, and `comm` is the
|
|
34
|
+
* remainder. Splitting on whitespace RUNS makes the space-padding and column alignment robust.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseProcTable(psOutput: string): ProcTableRow[];
|
|
37
|
+
//# sourceMappingURL=ExternalHogProcTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogProcTable.d.ts","sourceRoot":"","sources":["../../src/monitoring/ExternalHogProcTable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;0CACsC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAQD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAuB/D"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExternalHogProcTable — the whole-table `ps` parser of the External-Hog sentinel
|
|
3
|
+
* (CMT-1901, docs/specs/external-hog-zombie-autokill-sentinel.md §1).
|
|
4
|
+
*
|
|
5
|
+
* Parses the output of `ps -o pid=,ppid=,uid=,lstart=,time=,comm=` (no header) into rows the
|
|
6
|
+
* sampler uses to build the ProcTree + the per-pid cumulative-CPU-time map. This parser is
|
|
7
|
+
* LOAD-BEARING for kill eligibility (the CPU-delta pivots on `time=`), so it is REGISTERED in
|
|
8
|
+
* SCRAPE_PARSERS with a captured realness fixture (§Testing) — it must survive the real
|
|
9
|
+
* structural bytes: a `[dd-]hh:mm:ss` day-prefix time (the ~24h anchor case), an `lstart`
|
|
10
|
+
* value with embedded spaces + space-padded day-of-month, a `comm` with embedded spaces, a
|
|
11
|
+
* `<defunct>` row, and a short/permission-denied row.
|
|
12
|
+
*
|
|
13
|
+
* Fail-closed: a row whose pid/ppid/uid can't be parsed is SKIPPED (an unidentifiable process
|
|
14
|
+
* is not a candidate); a row whose `time=` is malformed keeps the row but sets
|
|
15
|
+
* `cputimeSeconds: undefined` → the CPU-delta yields UNKNOWN → alert-never-kill.
|
|
16
|
+
*/
|
|
17
|
+
import { parseProcTimeToSeconds } from '../core/SessionManager.js';
|
|
18
|
+
/** A `time=` value: optional `dd-`, then `[[hh:]mm:]ss[.ff]` — at most TWO colon groups
|
|
19
|
+
* (`hh:mm:ss`). Bounded to the real ps format (the round-9 reviewer noted an unbounded
|
|
20
|
+
* `(\d+:)*` was cosmetically lax though unreachable via t[8]; this closes it, strictly toward
|
|
21
|
+
* fail-closed). */
|
|
22
|
+
const PROC_TIME_RE = /^(\d+-)?(\d+:){0,2}\d+(\.\d+)?$/;
|
|
23
|
+
/**
|
|
24
|
+
* Parse `ps -o pid=,ppid=,uid=,lstart=,time=,comm=` output into rows. Column layout per line:
|
|
25
|
+
* pid ppid uid <Dow Mon DD HH:MM:SS YYYY> time comm...
|
|
26
|
+
* The first 3 fields are numeric, `lstart` is exactly 5 whitespace tokens (the space-padded
|
|
27
|
+
* day-of-month collapses under a whitespace-run split), `time` is one token, and `comm` is the
|
|
28
|
+
* remainder. Splitting on whitespace RUNS makes the space-padding and column alignment robust.
|
|
29
|
+
*/
|
|
30
|
+
export function parseProcTable(psOutput) {
|
|
31
|
+
if (typeof psOutput !== 'string')
|
|
32
|
+
return [];
|
|
33
|
+
const rows = [];
|
|
34
|
+
for (const rawLine of psOutput.split('\n')) {
|
|
35
|
+
const line = rawLine.trim();
|
|
36
|
+
if (!line)
|
|
37
|
+
continue;
|
|
38
|
+
const t = line.split(/\s+/);
|
|
39
|
+
// Need pid ppid uid (3) + lstart (5) + time (1) + >=1 comm token = >=10 tokens.
|
|
40
|
+
if (t.length < 10)
|
|
41
|
+
continue;
|
|
42
|
+
const pid = toPosInt(t[0]);
|
|
43
|
+
const ppid = toInt(t[1]);
|
|
44
|
+
const uid = toInt(t[2]);
|
|
45
|
+
if (pid === null || ppid === null || uid === null)
|
|
46
|
+
continue; // unidentifiable → skip
|
|
47
|
+
const startTime = t.slice(3, 8).join(' ');
|
|
48
|
+
const timeTok = t[8];
|
|
49
|
+
const cputimeSeconds = PROC_TIME_RE.test(timeTok) ? parseProcTimeToSeconds(timeTok) : undefined;
|
|
50
|
+
const comm = t.slice(9).join(' ');
|
|
51
|
+
rows.push({ pid, ppid, uid, startTime, cputimeSeconds, comm });
|
|
52
|
+
}
|
|
53
|
+
return rows;
|
|
54
|
+
}
|
|
55
|
+
function toInt(s) {
|
|
56
|
+
if (s === undefined || !/^-?\d+$/.test(s))
|
|
57
|
+
return null;
|
|
58
|
+
const n = Number(s);
|
|
59
|
+
return Number.isInteger(n) ? n : null;
|
|
60
|
+
}
|
|
61
|
+
function toPosInt(s) {
|
|
62
|
+
const n = toInt(s);
|
|
63
|
+
return n !== null && n > 0 ? n : null;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=ExternalHogProcTable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExternalHogProcTable.js","sourceRoot":"","sources":["../../src/monitoring/ExternalHogProcTable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAenE;;;oBAGoB;AACpB,MAAM,YAAY,GAAG,iCAAiC,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAmB,EAAE,CAAC;IAChC,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5B,gFAAgF;QAChF,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;YAAE,SAAS;QAE5B,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI;YAAE,SAAS,CAAC,wBAAwB;QAErF,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACtB,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAChG,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,KAAK,CAAC,CAAqB;IAClC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED,SAAS,QAAQ,CAAC,CAAqB;IACrC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACnB,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC"}
|