@kognai/orchestrator-core 0.1.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/README.md +44 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +175 -0
- package/dist/lib/aar-middleware.d.ts +6 -0
- package/dist/lib/aar-middleware.js +70 -0
- package/dist/lib/aar-types.d.ts +34 -0
- package/dist/lib/aar-types.js +4 -0
- package/dist/lib/acp-engine.d.ts +68 -0
- package/dist/lib/acp-engine.js +123 -0
- package/dist/lib/acp.d.ts +61 -0
- package/dist/lib/acp.js +425 -0
- package/dist/lib/agent-registry.d.ts +50 -0
- package/dist/lib/agent-registry.js +137 -0
- package/dist/lib/anthropic-direct.d.ts +27 -0
- package/dist/lib/anthropic-direct.js +109 -0
- package/dist/lib/asmr-extractor.d.ts +40 -0
- package/dist/lib/asmr-extractor.js +151 -0
- package/dist/lib/asmr-retrieval.d.ts +76 -0
- package/dist/lib/asmr-retrieval.js +311 -0
- package/dist/lib/asmr.d.ts +8 -0
- package/dist/lib/asmr.js +24 -0
- package/dist/lib/brainx-client.d.ts +72 -0
- package/dist/lib/brainx-client.js +200 -0
- package/dist/lib/brainx-embed.d.ts +14 -0
- package/dist/lib/brainx-embed.js +139 -0
- package/dist/lib/brainx-swarm-bridge.d.ts +93 -0
- package/dist/lib/brainx-swarm-bridge.js +242 -0
- package/dist/lib/byterover-client.d.ts +19 -0
- package/dist/lib/byterover-client.js +59 -0
- package/dist/lib/ceo-wallet.d.ts +37 -0
- package/dist/lib/ceo-wallet.js +176 -0
- package/dist/lib/chomsky-gate.d.ts +24 -0
- package/dist/lib/chomsky-gate.js +178 -0
- package/dist/lib/chomsky-runner.d.ts +29 -0
- package/dist/lib/chomsky-runner.js +157 -0
- package/dist/lib/citizen-score-contract.d.ts +72 -0
- package/dist/lib/citizen-score-contract.js +16 -0
- package/dist/lib/citizen-score-registry.d.ts +25 -0
- package/dist/lib/citizen-score-registry.js +65 -0
- package/dist/lib/citizenship.d.ts +103 -0
- package/dist/lib/citizenship.js +272 -0
- package/dist/lib/clawrouter-client.d.ts +37 -0
- package/dist/lib/clawrouter-client.js +148 -0
- package/dist/lib/code-asset-crystalliser.d.ts +41 -0
- package/dist/lib/code-asset-crystalliser.js +181 -0
- package/dist/lib/code-failure-logger.d.ts +27 -0
- package/dist/lib/code-failure-logger.js +42 -0
- package/dist/lib/cto-approval-gate.d.ts +45 -0
- package/dist/lib/cto-approval-gate.js +478 -0
- package/dist/lib/cto-gate-types.d.ts +28 -0
- package/dist/lib/cto-gate-types.js +8 -0
- package/dist/lib/decomposer-feedback.d.ts +54 -0
- package/dist/lib/decomposer-feedback.js +115 -0
- package/dist/lib/emotional-safety-gate.d.ts +48 -0
- package/dist/lib/emotional-safety-gate.js +97 -0
- package/dist/lib/engine-paths.d.ts +13 -0
- package/dist/lib/engine-paths.js +32 -0
- package/dist/lib/event-bus-listener.d.ts +8 -0
- package/dist/lib/event-bus-listener.js +144 -0
- package/dist/lib/event-bus-publisher.d.ts +25 -0
- package/dist/lib/event-bus-publisher.js +188 -0
- package/dist/lib/event-bus-types.d.ts +73 -0
- package/dist/lib/event-bus-types.js +23 -0
- package/dist/lib/failure-library.d.ts +178 -0
- package/dist/lib/failure-library.js +349 -0
- package/dist/lib/ksl/error-log.d.ts +28 -0
- package/dist/lib/ksl/error-log.js +43 -0
- package/dist/lib/ksl/index.d.ts +9 -0
- package/dist/lib/ksl/index.js +25 -0
- package/dist/lib/ksl/orchestrator-tap.d.ts +16 -0
- package/dist/lib/ksl/orchestrator-tap.js +85 -0
- package/dist/lib/ksl/record-writer.d.ts +46 -0
- package/dist/lib/ksl/record-writer.js +45 -0
- package/dist/lib/llm-cost-table.d.ts +36 -0
- package/dist/lib/llm-cost-table.js +90 -0
- package/dist/lib/local-model-router.d.ts +27 -0
- package/dist/lib/local-model-router.js +61 -0
- package/dist/lib/mc-client.d.ts +51 -0
- package/dist/lib/mc-client.js +249 -0
- package/dist/lib/model-router-contract.d.ts +91 -0
- package/dist/lib/model-router-contract.js +19 -0
- package/dist/lib/model-router-registry.d.ts +24 -0
- package/dist/lib/model-router-registry.js +52 -0
- package/dist/lib/model-router.d.ts +20 -0
- package/dist/lib/model-router.js +79 -0
- package/dist/lib/monotask-state-machine.d.ts +19 -0
- package/dist/lib/monotask-state-machine.js +131 -0
- package/dist/lib/neutral-prompt-checker.d.ts +22 -0
- package/dist/lib/neutral-prompt-checker.js +130 -0
- package/dist/lib/notion-direct.d.ts +92 -0
- package/dist/lib/notion-direct.js +381 -0
- package/dist/lib/ollama-client.d.ts +37 -0
- package/dist/lib/ollama-client.js +158 -0
- package/dist/lib/omel/credential-vault.d.ts +57 -0
- package/dist/lib/omel/credential-vault.js +324 -0
- package/dist/lib/omel/human-brake.d.ts +32 -0
- package/dist/lib/omel/human-brake.js +289 -0
- package/dist/lib/omel/index.d.ts +10 -0
- package/dist/lib/omel/index.js +26 -0
- package/dist/lib/omel/phantom-workspace.d.ts +31 -0
- package/dist/lib/omel/phantom-workspace.js +256 -0
- package/dist/lib/omel/wipe-witness.d.ts +75 -0
- package/dist/lib/omel/wipe-witness.js +398 -0
- package/dist/lib/orchestrate-engine.d.ts +25 -0
- package/dist/lib/orchestrate-engine.js +4436 -0
- package/dist/lib/perm-judge.d.ts +46 -0
- package/dist/lib/perm-judge.js +173 -0
- package/dist/lib/plumber/conformance.d.ts +54 -0
- package/dist/lib/plumber/conformance.js +121 -0
- package/dist/lib/plumber/index.d.ts +9 -0
- package/dist/lib/plumber/index.js +25 -0
- package/dist/lib/plumber/observer.d.ts +52 -0
- package/dist/lib/plumber/observer.js +180 -0
- package/dist/lib/plumber/types.d.ts +78 -0
- package/dist/lib/plumber/types.js +29 -0
- package/dist/lib/research-impl-gate.d.ts +16 -0
- package/dist/lib/research-impl-gate.js +105 -0
- package/dist/lib/sherlock-memory.d.ts +29 -0
- package/dist/lib/sherlock-memory.js +105 -0
- package/dist/lib/skill-crystalliser.d.ts +44 -0
- package/dist/lib/skill-crystalliser.js +60 -0
- package/dist/lib/sprint-runner-engine.d.ts +27 -0
- package/dist/lib/sprint-runner-engine.js +1042 -0
- package/dist/lib/sprint-state.d.ts +71 -0
- package/dist/lib/sprint-state.js +202 -0
- package/dist/lib/stuck-handler.d.ts +17 -0
- package/dist/lib/stuck-handler.js +249 -0
- package/dist/lib/task-contract-checker.d.ts +17 -0
- package/dist/lib/task-contract-checker.js +29 -0
- package/dist/lib/task-router/index.d.ts +17 -0
- package/dist/lib/task-router/index.js +52 -0
- package/dist/lib/task-router/router/generate-execution-id.d.ts +10 -0
- package/dist/lib/task-router/router/generate-execution-id.js +24 -0
- package/dist/lib/task-router/router/resolve-route.d.ts +2 -0
- package/dist/lib/task-router/router/resolve-route.js +49 -0
- package/dist/lib/task-router/types.d.ts +79 -0
- package/dist/lib/task-router/types.js +39 -0
- package/dist/lib/token-budget-validator.d.ts +44 -0
- package/dist/lib/token-budget-validator.js +84 -0
- package/dist/lib/trust-score-updater.d.ts +30 -0
- package/dist/lib/trust-score-updater.js +107 -0
- package/dist/lib/wallet-state.d.ts +26 -0
- package/dist/lib/wallet-state.js +85 -0
- package/package.json +27 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface PhantomContext {
|
|
2
|
+
taskId: string;
|
|
3
|
+
tmpDir: string;
|
|
4
|
+
createdAt: number;
|
|
5
|
+
}
|
|
6
|
+
declare class PhantomWorkspace {
|
|
7
|
+
private active;
|
|
8
|
+
private createdToday;
|
|
9
|
+
private lastResetDate;
|
|
10
|
+
private resetIfNewDay;
|
|
11
|
+
private logEvent;
|
|
12
|
+
/** Create an isolated tmpdir for a task. Returns a PhantomContext handle. */
|
|
13
|
+
create(taskId: string): PhantomContext;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a relative filename to the isolated tmpdir.
|
|
16
|
+
* Use this for ALL temp files written during a task.
|
|
17
|
+
* Prevents path traversal: only allows relative paths within the tmpDir.
|
|
18
|
+
* Enforces PHANTOM_MAX_MB quota before write.
|
|
19
|
+
*/
|
|
20
|
+
resolve(ctx: PhantomContext, relativePath: string): string;
|
|
21
|
+
/** Wipe the tmpdir and deregister the context. Safe to call multiple times. */
|
|
22
|
+
cleanup(ctx: PhantomContext): void;
|
|
23
|
+
/** Cleanup all currently active phantom workspaces (crash-safe exit handler). */
|
|
24
|
+
cleanupAll(reason: string): void;
|
|
25
|
+
getStats(): {
|
|
26
|
+
active: number;
|
|
27
|
+
created_today: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare const phantomWorkspace: PhantomWorkspace;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OMEL — Phantom Workspace (Sprint 176 + 181 Hardening)
|
|
3
|
+
// AMD-13: OS-Level Monotask Enforcement Layer
|
|
4
|
+
//
|
|
5
|
+
// PhantomWorkspace creates an isolated tmpdir per task.
|
|
6
|
+
// All temp file paths must go through resolve() — never to shared paths.
|
|
7
|
+
// Cleanup fires on task completion AND on error (caller wraps in try/finally).
|
|
8
|
+
// Lifecycle events logged to logs/omel/phantom-YYYY-MM-DD.jsonl.
|
|
9
|
+
//
|
|
10
|
+
// Sprint 181 hardening additions:
|
|
11
|
+
// - Crash-safe cleanup: process exit handlers (SIGTERM, SIGINT, uncaughtException)
|
|
12
|
+
// - Quota enforcement: PHANTOM_MAX_MB (default 500MB) per workspace
|
|
13
|
+
// - Stale workspace recovery: cleanup kognai-phantom-* dirs from crashed previous runs
|
|
14
|
+
// - Performance instrumentation: creation time logged, warn if >50ms
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// const ctx = phantomWorkspace.create(task.id);
|
|
18
|
+
// try { ... phantomWorkspace.resolve(ctx, 'output.json') ... }
|
|
19
|
+
// finally { phantomWorkspace.cleanup(ctx); }
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.phantomWorkspace = void 0;
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const engine_paths_1 = require("../engine-paths");
|
|
58
|
+
const os = __importStar(require("os"));
|
|
59
|
+
const https = __importStar(require("https"));
|
|
60
|
+
const LOGS_DIR = path.join((0, engine_paths_1.resolveEnginePaths)().root, 'logs', 'omel');
|
|
61
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
62
|
+
const PHANTOM_MAX_BYTES = (() => {
|
|
63
|
+
const mb = parseFloat(process.env.PHANTOM_MAX_MB || '500');
|
|
64
|
+
return (isNaN(mb) ? 500 : mb) * 1024 * 1024;
|
|
65
|
+
})();
|
|
66
|
+
// ── Telegram fire-and-forget alert ────────────────────────────────────────────
|
|
67
|
+
function sendTelegramAlert(message) {
|
|
68
|
+
const botToken = process.env.TELEGRAM_BOT_TOKEN || '';
|
|
69
|
+
const chatId = process.env.OWNER_TELEGRAM_CHAT_ID || '';
|
|
70
|
+
if (!botToken || !chatId)
|
|
71
|
+
return;
|
|
72
|
+
const payload = JSON.stringify({ chat_id: chatId, text: message, parse_mode: 'Markdown' });
|
|
73
|
+
try {
|
|
74
|
+
const req = https.request({
|
|
75
|
+
hostname: 'api.telegram.org',
|
|
76
|
+
path: `/bot${botToken}/sendMessage`,
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
79
|
+
});
|
|
80
|
+
req.on('error', () => { });
|
|
81
|
+
req.write(payload);
|
|
82
|
+
req.end();
|
|
83
|
+
}
|
|
84
|
+
catch { /* silent */ }
|
|
85
|
+
}
|
|
86
|
+
// ── Disk helpers ──────────────────────────────────────────────────────────────
|
|
87
|
+
function getDirSizeBytes(dirPath) {
|
|
88
|
+
let total = 0;
|
|
89
|
+
try {
|
|
90
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
91
|
+
const full = path.join(dirPath, entry.name);
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
total += getDirSizeBytes(full);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
try {
|
|
97
|
+
total += fs.statSync(full).size;
|
|
98
|
+
}
|
|
99
|
+
catch { /* skip */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { /* dir may have been removed */ }
|
|
104
|
+
return total;
|
|
105
|
+
}
|
|
106
|
+
// ── Stale workspace recovery (run once at module load) ────────────────────────
|
|
107
|
+
function recoverStaleWorkspaces() {
|
|
108
|
+
const tmpDir = os.tmpdir();
|
|
109
|
+
const PHANTOM_PREFIX = 'kognai-phantom-';
|
|
110
|
+
try {
|
|
111
|
+
const entries = fs.readdirSync(tmpDir);
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.startsWith(PHANTOM_PREFIX))
|
|
114
|
+
continue;
|
|
115
|
+
const fullPath = path.join(tmpDir, entry);
|
|
116
|
+
try {
|
|
117
|
+
const stat = fs.statSync(fullPath);
|
|
118
|
+
if (!stat.isDirectory())
|
|
119
|
+
continue;
|
|
120
|
+
// Stale if older than 1 hour (created by a crashed previous process)
|
|
121
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
122
|
+
if (ageMs < 60 * 60 * 1000)
|
|
123
|
+
continue; // less than 1 hour old — skip
|
|
124
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
125
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
126
|
+
const logFile = path.join(LOGS_DIR, `phantom-${date}.jsonl`);
|
|
127
|
+
fs.appendFileSync(logFile, JSON.stringify({
|
|
128
|
+
ts: new Date().toISOString(),
|
|
129
|
+
event: 'stale_recovery',
|
|
130
|
+
path: fullPath,
|
|
131
|
+
age_ms: ageMs,
|
|
132
|
+
}) + '\n');
|
|
133
|
+
}
|
|
134
|
+
catch { /* skip individual failures */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch { /* tmpdir read failed — non-fatal */ }
|
|
138
|
+
}
|
|
139
|
+
// Run stale recovery once at module load
|
|
140
|
+
recoverStaleWorkspaces();
|
|
141
|
+
// ── PhantomWorkspace class ────────────────────────────────────────────────────
|
|
142
|
+
class PhantomWorkspace {
|
|
143
|
+
active = new Map();
|
|
144
|
+
createdToday = 0;
|
|
145
|
+
lastResetDate = new Date().toISOString().slice(0, 10);
|
|
146
|
+
resetIfNewDay() {
|
|
147
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
148
|
+
if (today !== this.lastResetDate) {
|
|
149
|
+
this.createdToday = 0;
|
|
150
|
+
this.lastResetDate = today;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
logEvent(event, ctx, extra) {
|
|
154
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
155
|
+
const logFile = path.join(LOGS_DIR, `phantom-${today}.jsonl`);
|
|
156
|
+
const entry = { ts: new Date().toISOString(), event, taskId: ctx.taskId,
|
|
157
|
+
tmpDir: ctx.tmpDir, ...extra };
|
|
158
|
+
try {
|
|
159
|
+
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
160
|
+
}
|
|
161
|
+
catch { /* silent */ }
|
|
162
|
+
}
|
|
163
|
+
/** Create an isolated tmpdir for a task. Returns a PhantomContext handle. */
|
|
164
|
+
create(taskId) {
|
|
165
|
+
this.resetIfNewDay();
|
|
166
|
+
const t0 = Date.now();
|
|
167
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `kognai-phantom-${taskId.replace(/[^a-z0-9]/gi, '-')}-`));
|
|
168
|
+
const createMs = Date.now() - t0;
|
|
169
|
+
const ctx = { taskId, tmpDir, createdAt: Date.now() };
|
|
170
|
+
this.active.set(taskId, ctx);
|
|
171
|
+
this.createdToday++;
|
|
172
|
+
this.logEvent('create', ctx, {
|
|
173
|
+
active_count: this.active.size,
|
|
174
|
+
create_ms: createMs,
|
|
175
|
+
quota_bytes: PHANTOM_MAX_BYTES,
|
|
176
|
+
});
|
|
177
|
+
if (createMs > 50) {
|
|
178
|
+
this.logEvent('perf_warn', ctx, {
|
|
179
|
+
message: `Phantom workspace creation took ${createMs}ms (threshold: 50ms)`,
|
|
180
|
+
create_ms: createMs,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return ctx;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a relative filename to the isolated tmpdir.
|
|
187
|
+
* Use this for ALL temp files written during a task.
|
|
188
|
+
* Prevents path traversal: only allows relative paths within the tmpDir.
|
|
189
|
+
* Enforces PHANTOM_MAX_MB quota before write.
|
|
190
|
+
*/
|
|
191
|
+
resolve(ctx, relativePath) {
|
|
192
|
+
// Strip leading slashes and parent traversal segments
|
|
193
|
+
const safe = relativePath.replace(/^[/\\]+/, '').replace(/\.\.[/\\]/g, '');
|
|
194
|
+
const resolved = path.join(ctx.tmpDir, safe);
|
|
195
|
+
// Ensure resolved path is still within the tmpDir
|
|
196
|
+
if (!resolved.startsWith(ctx.tmpDir)) {
|
|
197
|
+
throw new Error(`[PhantomWorkspace] Path escape attempt: ${relativePath}`);
|
|
198
|
+
}
|
|
199
|
+
// Quota check
|
|
200
|
+
const currentBytes = getDirSizeBytes(ctx.tmpDir);
|
|
201
|
+
if (currentBytes >= PHANTOM_MAX_BYTES) {
|
|
202
|
+
const mb = (currentBytes / (1024 * 1024)).toFixed(1);
|
|
203
|
+
this.logEvent('quota_exceeded', ctx, { current_mb: mb, limit_mb: PHANTOM_MAX_BYTES / (1024 * 1024) });
|
|
204
|
+
sendTelegramAlert(`⚠️ *[OMEL PhantomWorkspace] Quota Exceeded*\n` +
|
|
205
|
+
`Task: \`${ctx.taskId}\`\n` +
|
|
206
|
+
`Size: ${mb}MB / ${(PHANTOM_MAX_BYTES / (1024 * 1024)).toFixed(0)}MB limit\n` +
|
|
207
|
+
`Set PHANTOM_MAX_MB env var to adjust.`);
|
|
208
|
+
throw new Error(`[PhantomWorkspace] Quota exceeded for task ${ctx.taskId}: ${mb}MB`);
|
|
209
|
+
}
|
|
210
|
+
return resolved;
|
|
211
|
+
}
|
|
212
|
+
/** Wipe the tmpdir and deregister the context. Safe to call multiple times. */
|
|
213
|
+
cleanup(ctx) {
|
|
214
|
+
if (!this.active.has(ctx.taskId))
|
|
215
|
+
return; // already cleaned
|
|
216
|
+
const durationMs = Date.now() - ctx.createdAt;
|
|
217
|
+
try {
|
|
218
|
+
if (fs.existsSync(ctx.tmpDir)) {
|
|
219
|
+
fs.rmSync(ctx.tmpDir, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
this.logEvent('cleanup_error', ctx, { error: err.message, duration_ms: durationMs });
|
|
224
|
+
this.active.delete(ctx.taskId);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.active.delete(ctx.taskId);
|
|
228
|
+
this.logEvent('cleanup', ctx, { duration_ms: durationMs, active_count: this.active.size });
|
|
229
|
+
}
|
|
230
|
+
/** Cleanup all currently active phantom workspaces (crash-safe exit handler). */
|
|
231
|
+
cleanupAll(reason) {
|
|
232
|
+
const contexts = Array.from(this.active.values());
|
|
233
|
+
for (const ctx of contexts) {
|
|
234
|
+
try {
|
|
235
|
+
if (fs.existsSync(ctx.tmpDir)) {
|
|
236
|
+
fs.rmSync(ctx.tmpDir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
this.logEvent('cleanup_exit', ctx, { reason, duration_ms: Date.now() - ctx.createdAt });
|
|
239
|
+
}
|
|
240
|
+
catch { /* best-effort */ }
|
|
241
|
+
this.active.delete(ctx.taskId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
getStats() {
|
|
245
|
+
this.resetIfNewDay();
|
|
246
|
+
return { active: this.active.size, created_today: this.createdToday };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
exports.phantomWorkspace = new PhantomWorkspace();
|
|
250
|
+
// ── Crash-safe exit handlers ───────────────────────────────────────────────────
|
|
251
|
+
function exitHandler(reason) {
|
|
252
|
+
exports.phantomWorkspace.cleanupAll(reason);
|
|
253
|
+
}
|
|
254
|
+
process.once('SIGTERM', () => exitHandler('SIGTERM'));
|
|
255
|
+
process.once('SIGINT', () => exitHandler('SIGINT'));
|
|
256
|
+
process.once('uncaughtException', () => exitHandler('uncaughtException'));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface WitnessToken {
|
|
2
|
+
filePath: string;
|
|
3
|
+
agentId: string;
|
|
4
|
+
oldHash: string;
|
|
5
|
+
oldSizeBytes: number;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
snapshotKey?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ShrinkAlert {
|
|
10
|
+
filePath: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
oldSizeBytes: number;
|
|
13
|
+
newSizeBytes: number;
|
|
14
|
+
ratio: number;
|
|
15
|
+
ts: string;
|
|
16
|
+
rolledBack?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface IntegrityReport {
|
|
19
|
+
ts: string;
|
|
20
|
+
scan_dir: string;
|
|
21
|
+
total_files: number;
|
|
22
|
+
drifted: Array<{
|
|
23
|
+
file: string;
|
|
24
|
+
old_hash: string;
|
|
25
|
+
new_hash: string;
|
|
26
|
+
}>;
|
|
27
|
+
new_files: string[];
|
|
28
|
+
deleted_files: string[];
|
|
29
|
+
}
|
|
30
|
+
type HashStore = Record<string, string>;
|
|
31
|
+
export declare class WipeWitness {
|
|
32
|
+
private shrinkAlerts;
|
|
33
|
+
/**
|
|
34
|
+
* Capture SHA-256 hash + size of file before write.
|
|
35
|
+
* Saves a snapshot for rollback (last 3 states).
|
|
36
|
+
* If file does not exist (new file): returns token with oldSizeBytes=0 and oldHash=''.
|
|
37
|
+
*/
|
|
38
|
+
beforeWrite(filePath: string, agentId: string): WitnessToken;
|
|
39
|
+
/**
|
|
40
|
+
* Compare new size against old size.
|
|
41
|
+
* If new_size < 0.5 * old_size → emit ShrinkAlert + Telegram notification.
|
|
42
|
+
* Auto-rollback unless WIPE_WITNESS_NO_ROLLBACK=true.
|
|
43
|
+
* Skips shrink check if oldSizeBytes === 0 (new file creation).
|
|
44
|
+
*/
|
|
45
|
+
afterWrite(token: WitnessToken, newSizeBytes: number): void;
|
|
46
|
+
/**
|
|
47
|
+
* Log a deletion event before the file is removed.
|
|
48
|
+
* Captures hash + size for audit trail.
|
|
49
|
+
*/
|
|
50
|
+
beforeDelete(filePath: string, agentId: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Return all shrink alerts accumulated in this session.
|
|
53
|
+
*/
|
|
54
|
+
getShrinkAlerts(): ShrinkAlert[];
|
|
55
|
+
/**
|
|
56
|
+
* Restore a file to a previous state (one of last 3 snapshots).
|
|
57
|
+
* @param filePath Absolute path to the file to restore
|
|
58
|
+
* @param stepsBack 1 = most recent snapshot, 2 = second most recent, 3 = oldest. Default 1.
|
|
59
|
+
* @returns true if rollback succeeded, false otherwise.
|
|
60
|
+
*/
|
|
61
|
+
rollback(filePath: string, stepsBack?: number): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Take a hash snapshot of all .ts files in a directory.
|
|
64
|
+
* Returns a HashStore map for later comparison.
|
|
65
|
+
* Used by run-swarm.sh pre-run.
|
|
66
|
+
*/
|
|
67
|
+
snapshotDir(scanDir: string): HashStore;
|
|
68
|
+
/**
|
|
69
|
+
* Nightly integrity report: compare current file hashes vs last snapshot.
|
|
70
|
+
* Returns drifted, new, and deleted files.
|
|
71
|
+
*/
|
|
72
|
+
integrityReport(scanDir: string): IntegrityReport;
|
|
73
|
+
}
|
|
74
|
+
export declare const wipeWitness: WipeWitness;
|
|
75
|
+
export {};
|