@aria_asi/cli 0.2.30 → 0.2.31
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/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/claude-code.js +88 -20
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +526 -2
- package/dist/aria-connector/src/connectors/codex.js.map +1 -1
- package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts +7 -0
- package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/doctrine-trigger-map.js +87 -0
- package/dist/aria-connector/src/connectors/doctrine-trigger-map.js.map +1 -0
- package/dist/aria-connector/src/connectors/must-read.d.ts +4 -0
- package/dist/aria-connector/src/connectors/must-read.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/must-read.js +111 -0
- package/dist/aria-connector/src/connectors/must-read.js.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/opencode.js +2 -0
- package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
- package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/runtime.js +231 -19
- package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
- package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/shell.js +76 -3
- package/dist/aria-connector/src/connectors/shell.js.map +1 -1
- package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +52 -25
- package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +185 -76
- package/dist/assets/hooks/aria-preturn-memory-gate.mjs +63 -14
- package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +2 -0
- package/dist/assets/hooks/aria-stop-gate.mjs +225 -52
- package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
- package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
- package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
- package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
- package/dist/assets/opencode-plugins/harness-gate/index.js +23 -1
- package/dist/assets/opencode-plugins/harness-stop/index.js +93 -4
- package/dist/runtime/auth-middleware.mjs +251 -0
- package/dist/runtime/codex-bridge.mjs +644 -0
- package/dist/runtime/discipline/CLAUDE.md +12 -0
- package/dist/runtime/discipline/doctrine_trigger_map.json +479 -0
- package/dist/runtime/doctrine_trigger_map.json +479 -0
- package/dist/runtime/fleet-engine.mjs +231 -0
- package/dist/runtime/harness-daemon.mjs +433 -0
- package/dist/runtime/manifest.json +1 -1
- package/dist/runtime/metering.mjs +100 -0
- package/dist/runtime/onboarding-engine.mjs +89 -0
- package/dist/runtime/plugin-engine.mjs +196 -0
- package/dist/runtime/sdk/BUNDLED.json +1 -1
- package/dist/runtime/sdk/index.d.ts +7 -0
- package/dist/runtime/sdk/index.js +120 -14
- package/dist/runtime/sdk/index.js.map +1 -1
- package/dist/runtime/service.mjs +1094 -47
- package/dist/runtime/workflow-engine.mjs +322 -0
- package/dist/sdk/BUNDLED.json +1 -1
- package/dist/sdk/index.d.ts +7 -0
- package/dist/sdk/index.js +120 -14
- package/dist/sdk/index.js.map +1 -1
- package/hooks/aria-cognition-substrate-binding.mjs +52 -25
- package/hooks/aria-harness-via-sdk.mjs +126 -12
- package/hooks/aria-pre-tool-gate.mjs +185 -76
- package/hooks/aria-preturn-memory-gate.mjs +63 -14
- package/hooks/aria-repo-doctrine-gate.mjs +2 -0
- package/hooks/aria-stop-gate.mjs +225 -52
- package/hooks/lib/canonical-lenses.mjs +6 -5
- package/hooks/lib/gate-loop-state.mjs +50 -0
- package/hooks/lib/hook-message-window.mjs +121 -0
- package/hooks/test-tier-lens-labeling.mjs +26 -58
- package/opencode-plugins/harness-gate/index.js +23 -1
- package/opencode-plugins/harness-stop/index.js +93 -4
- package/package.json +1 -1
- package/runtime-src/auth-middleware.mjs +251 -0
- package/runtime-src/codex-bridge.mjs +644 -0
- package/runtime-src/fleet-engine.mjs +231 -0
- package/runtime-src/harness-daemon.mjs +433 -0
- package/runtime-src/metering.mjs +100 -0
- package/runtime-src/onboarding-engine.mjs +89 -0
- package/runtime-src/plugin-engine.mjs +196 -0
- package/runtime-src/service.mjs +1094 -47
- package/runtime-src/workflow-engine.mjs +322 -0
- package/scripts/bundle-sdk.mjs +5 -0
- package/src/connectors/claude-code.ts +98 -20
- package/src/connectors/codex.ts +534 -1
- package/src/connectors/doctrine-trigger-map.ts +112 -0
- package/src/connectors/must-read.ts +113 -0
- package/src/connectors/opencode.ts +3 -0
- package/src/connectors/runtime.ts +241 -21
- package/src/connectors/shell.ts +78 -3
- package/dist/cli-0.2.0.tgz +0 -0
package/dist/runtime/service.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { createHash, createCipheriv, createDecipheriv, randomBytes, randomUUID, scryptSync } from 'node:crypto';
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
|
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
extractAnthropicUserMessage,
|
|
15
15
|
extractOpenAIUserMessage,
|
|
16
16
|
} from './provider-proxy.mjs';
|
|
17
|
+
import { recordTokenUsage, brandModel, getUsageSummary, getBillingSummary, getSubscriptionTier } from './metering.mjs';
|
|
18
|
+
import { deployFleet, loadFleet, saveFleet, getFleetStatus, buildAgentSystemPrompt } from './fleet-engine.mjs';
|
|
19
|
+
import { listPlugins, installPlugin, configurePlugin, dispatchHook } from './plugin-engine.mjs';
|
|
20
|
+
import { listWorkflowTemplates, configureWorkflow, getWorkflowStatus, startWorkflow, approveWorkflowStep } from './workflow-engine.mjs';
|
|
21
|
+
import { hqAuthMiddleware, generateApiKey, loginUser, registerUser, revokeSession, listAllTenants } from './auth-middleware.mjs';
|
|
17
22
|
import {
|
|
18
23
|
ARISTOTLE_28_MODULES,
|
|
19
24
|
NOOR_COGNITIVE_SUITE,
|
|
@@ -37,8 +42,13 @@ const { runFullChain } = require('./vendor/aria-gate-runtime/index.js');
|
|
|
37
42
|
const DEFAULT_HOST = process.env.ARIA_RUNTIME_HOST || '127.0.0.1';
|
|
38
43
|
const DEFAULT_PORT = Number(process.env.ARIA_RUNTIME_PORT || 4319);
|
|
39
44
|
const DEFAULT_HARNESS_URL =
|
|
45
|
+
process.env.ARIA_HARNESS_DAEMON_URL ||
|
|
46
|
+
process.env.ARIA_HIVE_RUNTIME_URL ||
|
|
40
47
|
process.env.ARIA_HARNESS_BASE_URL ||
|
|
41
48
|
process.env.ARIA_HARNESS_URL ||
|
|
49
|
+
process.env.ARIA_SOUL_URL ||
|
|
50
|
+
process.env.ARIAS_SOUL_URL ||
|
|
51
|
+
process.env.ARIA_SOUL_BASE_URL ||
|
|
42
52
|
'https://harness.ariasos.com';
|
|
43
53
|
const DEFAULT_RUNTIME_URL = process.env.ARIA_RUNTIME_URL || `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
|
|
44
54
|
const DEFAULT_QDRANT_URL = process.env.ARIA_QDRANT_URL || 'http://127.0.0.1:6333';
|
|
@@ -48,16 +58,23 @@ const DEFAULT_FORGE_SERVICE_URL =
|
|
|
48
58
|
process.env.FORGE_SERVICE_URL ||
|
|
49
59
|
`${DEFAULT_HARNESS_URL.replace(/\/$/, '')}/api/forge/psi`;
|
|
50
60
|
const DEFAULT_HEARTBEAT_GRACE_SECONDS = Number(process.env.ARIA_RUNTIME_HEARTBEAT_GRACE_SECONDS || 900);
|
|
61
|
+
const DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS = Number(process.env.ARIA_RUNTIME_OFFLINE_BUNDLE_SOFT_TTL_SECONDS || 86400);
|
|
62
|
+
const DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS = Number(process.env.ARIA_RUNTIME_OFFLINE_BUNDLE_HARD_TTL_SECONDS || 259200);
|
|
51
63
|
const DEFAULT_JOB_CLAIM_SECONDS = Number(process.env.ARIA_RUNTIME_JOB_CLAIM_SECONDS || 120);
|
|
52
64
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
53
65
|
const STATE_DIR = join(__dirname, 'state');
|
|
54
66
|
const LEASE_PATH = join(STATE_DIR, 'lease.enc');
|
|
67
|
+
const OFFLINE_BUNDLE_PATH = join(STATE_DIR, 'offline-policy-bundle.enc');
|
|
55
68
|
const RUNTIME_META_PATH = join(STATE_DIR, 'runtime-meta.json');
|
|
56
69
|
const AUTONOMY_STATE_PATH = join(STATE_DIR, 'autonomy.json');
|
|
57
70
|
const COGNITION_STATE_PATH = join(STATE_DIR, 'cognition-state.enc');
|
|
58
71
|
const REVOCATION_LOCK_PATH = join(STATE_DIR, 'revoked.json');
|
|
59
72
|
const CONFIG_PATH = join(process.env.HOME || '', '.aria', 'config.json');
|
|
60
73
|
const CODEBASE_AWARENESS_STATE_PATH = join(process.env.HOME || '', '.aria', 'codebase-awareness-state.json');
|
|
74
|
+
const LEGACY_PACKET_CACHE_CANDIDATES = [
|
|
75
|
+
join(process.env.HOME || '', '.aria', '.aria-harness-last-packet.json'),
|
|
76
|
+
join(process.env.HOME || '', '.claude', '.aria-harness-last-packet.json'),
|
|
77
|
+
];
|
|
61
78
|
const leaseCache = new Map();
|
|
62
79
|
const TELEMETRY_LIMIT = 250;
|
|
63
80
|
const DECISION_LIMIT = 250;
|
|
@@ -88,10 +105,14 @@ const READABLE_LENS_SLOTS = [
|
|
|
88
105
|
const DOCTRINE_TRIGGER_MAP_CANDIDATES = [
|
|
89
106
|
join(__dirname, 'discipline', 'doctrine_trigger_map.json'),
|
|
90
107
|
join(__dirname, 'doctrine_trigger_map.json'),
|
|
108
|
+
join(process.env.HOME || '', '.aria', 'runtime', 'discipline', 'doctrine_trigger_map.json'),
|
|
109
|
+
join(process.env.HOME || '', '.aria', 'runtime', 'doctrine_trigger_map.json'),
|
|
91
110
|
join(process.env.HOME || '', '.claude', 'hooks', 'doctrine_trigger_map.json'),
|
|
92
111
|
join(process.env.HOME || '', '.claude', 'projects', '-home-hamzaibrahim1', 'memory', 'doctrine_trigger_map.json'),
|
|
93
112
|
join(process.env.HOME || '', '.codex', 'doctrine_trigger_map.json'),
|
|
113
|
+
join(process.env.HOME || '', '.opencode', 'doctrine_trigger_map.json'),
|
|
94
114
|
];
|
|
115
|
+
const DOCTRINE_TRIGGER_SYNC_INTERVAL_MS = Number(process.env.ARIA_DOCTRINE_TRIGGER_SYNC_INTERVAL_MS || 5000);
|
|
95
116
|
const TOOL_DEPLOY_PATTERNS = [
|
|
96
117
|
/\b(?:\.\/)?scripts\/deploy-/i,
|
|
97
118
|
/\bkubectl\s+apply\b/i,
|
|
@@ -115,12 +136,75 @@ const TOOL_DESTRUCTIVE_PATTERNS = [
|
|
|
115
136
|
/\b(?:DROP|TRUNCATE)\s+(?:TABLE|DATABASE|SCHEMA|INDEX)\b/i,
|
|
116
137
|
/\bkubectl\s+delete\b/i,
|
|
117
138
|
];
|
|
139
|
+
let doctrineTriggerMapCache = { sourcePath: null, map: { triggers: [] } };
|
|
140
|
+
let doctrineTriggerMapSyncedAt = 0;
|
|
118
141
|
|
|
119
142
|
function json(res, status, payload) {
|
|
120
143
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
121
144
|
res.end(JSON.stringify(payload, null, 2));
|
|
122
145
|
}
|
|
123
146
|
|
|
147
|
+
function buildWebSocketAccept(key) {
|
|
148
|
+
return createHash('sha1')
|
|
149
|
+
.update(`${String(key || '').trim()}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
150
|
+
.digest('base64');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sendWebSocketCloseFrame(socket, code = 1008, reason = '') {
|
|
154
|
+
const reasonBuffer = Buffer.from(String(reason || ''), 'utf8');
|
|
155
|
+
const payload = Buffer.allocUnsafe(2 + reasonBuffer.length);
|
|
156
|
+
payload.writeUInt16BE(code, 0);
|
|
157
|
+
reasonBuffer.copy(payload, 2);
|
|
158
|
+
|
|
159
|
+
const header =
|
|
160
|
+
payload.length < 126
|
|
161
|
+
? Buffer.from([0x88, payload.length])
|
|
162
|
+
: Buffer.from([0x88, 126, (payload.length >> 8) & 0xff, payload.length & 0xff]);
|
|
163
|
+
|
|
164
|
+
socket.write(Buffer.concat([header, payload]));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleWebSocketUpgrade(req, socket) {
|
|
168
|
+
const url = new URL(req.url || '/', DEFAULT_RUNTIME_URL);
|
|
169
|
+
if (url.pathname !== '/v1/responses' && url.pathname !== '/responses') {
|
|
170
|
+
socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
|
|
171
|
+
socket.destroy();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const upgrade = String(req.headers.upgrade || '').toLowerCase();
|
|
176
|
+
const connection = String(req.headers.connection || '').toLowerCase();
|
|
177
|
+
const websocketKey = req.headers['sec-websocket-key'];
|
|
178
|
+
if (upgrade !== 'websocket' || !connection.includes('upgrade') || !websocketKey) {
|
|
179
|
+
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
180
|
+
socket.destroy();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const accept = buildWebSocketAccept(websocketKey);
|
|
185
|
+
socket.write(
|
|
186
|
+
[
|
|
187
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
188
|
+
'Upgrade: websocket',
|
|
189
|
+
'Connection: Upgrade',
|
|
190
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
191
|
+
'\r\n',
|
|
192
|
+
].join('\r\n'),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Codex can fall back to the HTTPS Responses path after a real websocket
|
|
196
|
+
// handshake closes. Until Aria owns the full Responses websocket protocol,
|
|
197
|
+
// keep this seam explicit instead of returning a misleading 404.
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
try {
|
|
200
|
+
sendWebSocketCloseFrame(socket, 1008, 'aria-runtime-http-fallback');
|
|
201
|
+
} catch {}
|
|
202
|
+
try {
|
|
203
|
+
socket.end();
|
|
204
|
+
} catch {}
|
|
205
|
+
}, 25).unref?.();
|
|
206
|
+
}
|
|
207
|
+
|
|
124
208
|
function isNonTrivialAssistantTurn(text, toolIntents = []) {
|
|
125
209
|
const body = String(text || '').trim();
|
|
126
210
|
if (toolIntents.length > 0) return true;
|
|
@@ -198,6 +282,17 @@ function inferToolAction(toolName, args) {
|
|
|
198
282
|
return 'tool';
|
|
199
283
|
}
|
|
200
284
|
|
|
285
|
+
function normalizeHarnessPacketPayload(payload) {
|
|
286
|
+
let current = payload;
|
|
287
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
288
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) break;
|
|
289
|
+
if (!('packet' in current) || !current.packet || typeof current.packet !== 'object') break;
|
|
290
|
+
if (!('timestamp' in current) && !('version' in current)) break;
|
|
291
|
+
current = current.packet;
|
|
292
|
+
}
|
|
293
|
+
return current;
|
|
294
|
+
}
|
|
295
|
+
|
|
201
296
|
function summarizeToolTarget(toolName, args) {
|
|
202
297
|
if (typeof args?.command === 'string' && args.command.trim()) return args.command.trim();
|
|
203
298
|
if (typeof args?.file_path === 'string' && args.file_path.trim()) return args.file_path.trim();
|
|
@@ -244,11 +339,49 @@ function extractProviderToolIntents(providerStyle, providerMeta) {
|
|
|
244
339
|
}
|
|
245
340
|
|
|
246
341
|
function loadDoctrineTriggerMap() {
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
if (now - doctrineTriggerMapSyncedAt < DOCTRINE_TRIGGER_SYNC_INTERVAL_MS) {
|
|
344
|
+
return doctrineTriggerMapCache.map;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const validCandidates = [];
|
|
247
348
|
for (const candidate of DOCTRINE_TRIGGER_MAP_CANDIDATES) {
|
|
248
349
|
const map = readJsonFile(candidate, null);
|
|
249
|
-
if (map
|
|
350
|
+
if (!map || !Array.isArray(map.triggers)) continue;
|
|
351
|
+
try {
|
|
352
|
+
validCandidates.push({
|
|
353
|
+
path: candidate,
|
|
354
|
+
mtimeMs: statSync(candidate).mtimeMs,
|
|
355
|
+
map,
|
|
356
|
+
});
|
|
357
|
+
} catch {}
|
|
250
358
|
}
|
|
251
|
-
|
|
359
|
+
|
|
360
|
+
if (validCandidates.length === 0) {
|
|
361
|
+
doctrineTriggerMapCache = { sourcePath: null, map: { triggers: [] } };
|
|
362
|
+
doctrineTriggerMapSyncedAt = now;
|
|
363
|
+
return doctrineTriggerMapCache.map;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
validCandidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
367
|
+
const latest = validCandidates[0];
|
|
368
|
+
const normalized = JSON.stringify(latest.map, null, 2) + '\n';
|
|
369
|
+
for (const candidate of DOCTRINE_TRIGGER_MAP_CANDIDATES) {
|
|
370
|
+
try {
|
|
371
|
+
const dirPath = dirname(candidate);
|
|
372
|
+
if (!existsSync(dirPath)) {
|
|
373
|
+
mkdirSync(dirPath, { recursive: true, mode: 0o755 });
|
|
374
|
+
}
|
|
375
|
+
const current = existsSync(candidate) ? readFileSync(candidate, 'utf8') : null;
|
|
376
|
+
if (current !== normalized) {
|
|
377
|
+
writeFileSync(candidate, normalized, { mode: 0o644 });
|
|
378
|
+
}
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
doctrineTriggerMapCache = { sourcePath: latest.path, map: latest.map };
|
|
383
|
+
doctrineTriggerMapSyncedAt = now;
|
|
384
|
+
return latest.map;
|
|
252
385
|
}
|
|
253
386
|
|
|
254
387
|
function collectDoctrineTriggerHits(text) {
|
|
@@ -341,6 +474,26 @@ function synthesizeOwnerLease(apiKey, reason) {
|
|
|
341
474
|
};
|
|
342
475
|
}
|
|
343
476
|
|
|
477
|
+
function buildOfflineBundleFallbackPacket(bundle, error) {
|
|
478
|
+
const normalized = normalizeHarnessPacketPayload(bundle?.packet);
|
|
479
|
+
const packet = normalized && typeof normalized === 'object'
|
|
480
|
+
? JSON.parse(JSON.stringify(normalized))
|
|
481
|
+
: null;
|
|
482
|
+
if (!packet) return null;
|
|
483
|
+
packet.runtimeOfflineBundle = {
|
|
484
|
+
phase: error?.softExpired ? 'degraded' : 'fresh',
|
|
485
|
+
cachedAt: bundle.cachedAt || null,
|
|
486
|
+
lastUpstreamOkAt: bundle.lastUpstreamOkAt || null,
|
|
487
|
+
softExpiresAt: error?.softExpiresAt || null,
|
|
488
|
+
hardExpiresAt: error?.hardExpiresAt || null,
|
|
489
|
+
ageSeconds: error?.ageSeconds ?? null,
|
|
490
|
+
doctrineBundleHash: bundle.doctrineBundleHash || null,
|
|
491
|
+
source: bundle.source || null,
|
|
492
|
+
lastUpstreamError: bundle.lastUpstreamError || null,
|
|
493
|
+
};
|
|
494
|
+
return packet;
|
|
495
|
+
}
|
|
496
|
+
|
|
344
497
|
function deriveEncryptionKey(secret) {
|
|
345
498
|
return scryptSync(secret, 'aria-mounted-runtime', 32);
|
|
346
499
|
}
|
|
@@ -390,10 +543,130 @@ function saveEncryptedLease(lease, secret) {
|
|
|
390
543
|
writeFileSync(LEASE_PATH, encryptJson(lease, secret), { mode: 0o600 });
|
|
391
544
|
}
|
|
392
545
|
|
|
546
|
+
function loadEncryptedOfflineBundle(secret) {
|
|
547
|
+
try {
|
|
548
|
+
if (!existsSync(OFFLINE_BUNDLE_PATH)) return null;
|
|
549
|
+
return decryptJson(readFileSync(OFFLINE_BUNDLE_PATH, 'utf8'), secret);
|
|
550
|
+
} catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function saveEncryptedOfflineBundle(bundle, secret) {
|
|
556
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
557
|
+
writeFileSync(OFFLINE_BUNDLE_PATH, encryptJson(bundle, secret), { mode: 0o600 });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function computeOfflineBundleStatus(bundle, now = Date.now()) {
|
|
561
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
562
|
+
return {
|
|
563
|
+
present: false,
|
|
564
|
+
phase: 'missing',
|
|
565
|
+
usable: false,
|
|
566
|
+
softExpired: true,
|
|
567
|
+
hardExpired: true,
|
|
568
|
+
ageSeconds: null,
|
|
569
|
+
softTtlSeconds: DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS,
|
|
570
|
+
hardTtlSeconds: DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS,
|
|
571
|
+
softExpiresAt: null,
|
|
572
|
+
hardExpiresAt: null,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const cachedAtMs = Date.parse(bundle.cachedAt || bundle.lastUpdatedAt || bundle.lastUpstreamOkAt || '');
|
|
577
|
+
const softExpiresAt = Number(bundle.softExpiresAt || 0);
|
|
578
|
+
const hardExpiresAt = Number(bundle.hardExpiresAt || 0);
|
|
579
|
+
const ageSeconds = Number.isFinite(cachedAtMs) ? Math.max(0, Math.round((now - cachedAtMs) / 1000)) : null;
|
|
580
|
+
const softExpired = !softExpiresAt || softExpiresAt <= now;
|
|
581
|
+
const hardExpired = !hardExpiresAt || hardExpiresAt <= now;
|
|
582
|
+
const phase = hardExpired ? 'expired' : (softExpired ? 'degraded' : 'fresh');
|
|
583
|
+
return {
|
|
584
|
+
present: true,
|
|
585
|
+
phase,
|
|
586
|
+
usable: !hardExpired,
|
|
587
|
+
softExpired,
|
|
588
|
+
hardExpired,
|
|
589
|
+
ageSeconds,
|
|
590
|
+
softTtlSeconds: Number(bundle.softTtlSeconds || DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS),
|
|
591
|
+
hardTtlSeconds: Number(bundle.hardTtlSeconds || DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS),
|
|
592
|
+
softExpiresAt: softExpiresAt ? new Date(softExpiresAt).toISOString() : null,
|
|
593
|
+
hardExpiresAt: hardExpiresAt ? new Date(hardExpiresAt).toISOString() : null,
|
|
594
|
+
cachedAt: bundle.cachedAt || null,
|
|
595
|
+
lastUpstreamOkAt: bundle.lastUpstreamOkAt || null,
|
|
596
|
+
source: bundle.source || null,
|
|
597
|
+
doctrineBundleHash: bundle.doctrineBundleHash || null,
|
|
598
|
+
lastUpstreamError: bundle.lastUpstreamError || null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function persistOfflineBundle(secret, payload) {
|
|
603
|
+
const existing = loadEncryptedOfflineBundle(secret);
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
const base = existing && typeof existing === 'object' ? existing : {};
|
|
606
|
+
const cachedAt = typeof base.cachedAt === 'string' && base.cachedAt ? base.cachedAt : new Date(now).toISOString();
|
|
607
|
+
const softTtlSeconds = Math.max(60, Number(payload.softTtlSeconds || base.softTtlSeconds || DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS));
|
|
608
|
+
const hardTtlSeconds = Math.max(softTtlSeconds, Number(payload.hardTtlSeconds || base.hardTtlSeconds || DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS));
|
|
609
|
+
const refreshedAt = new Date(now).toISOString();
|
|
610
|
+
const bundle = {
|
|
611
|
+
...base,
|
|
612
|
+
...payload,
|
|
613
|
+
cachedAt,
|
|
614
|
+
refreshedAt,
|
|
615
|
+
lastUpdatedAt: refreshedAt,
|
|
616
|
+
lastUpstreamOkAt: payload.lastUpstreamOkAt || refreshedAt,
|
|
617
|
+
softTtlSeconds,
|
|
618
|
+
hardTtlSeconds,
|
|
619
|
+
softExpiresAt: now + softTtlSeconds * 1000,
|
|
620
|
+
hardExpiresAt: now + hardTtlSeconds * 1000,
|
|
621
|
+
};
|
|
622
|
+
saveEncryptedOfflineBundle(bundle, secret);
|
|
623
|
+
return bundle;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function readLegacyPacketCache() {
|
|
627
|
+
const candidates = [];
|
|
628
|
+
for (const candidate of LEGACY_PACKET_CACHE_CANDIDATES) {
|
|
629
|
+
try {
|
|
630
|
+
if (!existsSync(candidate)) continue;
|
|
631
|
+
const raw = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
632
|
+
const packet = normalizeHarnessPacketPayload(raw.packet ?? raw);
|
|
633
|
+
const harness = typeof packet?.harness === 'string' ? packet.harness : '';
|
|
634
|
+
if (!harness.trim()) continue;
|
|
635
|
+
candidates.push({
|
|
636
|
+
path: candidate,
|
|
637
|
+
packet,
|
|
638
|
+
mtimeMs: statSync(candidate).mtimeMs,
|
|
639
|
+
});
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
643
|
+
return candidates[0] || null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function ensureOfflineBundleSeeded(secret, lease = null) {
|
|
647
|
+
const existing = loadEncryptedOfflineBundle(secret);
|
|
648
|
+
const normalizedExisting = normalizeHarnessPacketPayload(existing?.packet);
|
|
649
|
+
if (normalizedExisting && typeof normalizedExisting.harness === 'string' && normalizedExisting.harness.trim()) {
|
|
650
|
+
return existing;
|
|
651
|
+
}
|
|
652
|
+
const legacy = readLegacyPacketCache();
|
|
653
|
+
if (!legacy) return existing;
|
|
654
|
+
return persistOfflineBundle(secret, {
|
|
655
|
+
...(existing && typeof existing === 'object' ? existing : {}),
|
|
656
|
+
keyHash: hashKey(secret),
|
|
657
|
+
packet: legacy.packet,
|
|
658
|
+
lease: lease || existing?.lease || loadEncryptedLease(secret),
|
|
659
|
+
source: `legacy-cache:${legacy.path}`,
|
|
660
|
+
doctrineBundleHash: lease?.claims?.doctrine_bundle_hash || existing?.doctrineBundleHash || null,
|
|
661
|
+
lastUpstreamError: existing?.lastUpstreamError || null,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
393
665
|
function defaultCognitionState() {
|
|
394
666
|
return {
|
|
395
667
|
telemetry: [],
|
|
396
668
|
decisions: [],
|
|
669
|
+
pendingDecisions: [],
|
|
397
670
|
evolutionPrinciples: [],
|
|
398
671
|
aegisPatterns: [],
|
|
399
672
|
heartbeats: [],
|
|
@@ -586,6 +859,7 @@ function summarizeCognitionState(state) {
|
|
|
586
859
|
return {
|
|
587
860
|
telemetryTurns: Array.isArray(state.telemetry) ? state.telemetry.length : 0,
|
|
588
861
|
decisions: Array.isArray(state.decisions) ? state.decisions.length : 0,
|
|
862
|
+
pendingDecisionUploads: Array.isArray(state.pendingDecisions) ? state.pendingDecisions.length : 0,
|
|
589
863
|
evolutionPrinciples: Array.isArray(state.evolutionPrinciples) ? state.evolutionPrinciples.length : 0,
|
|
590
864
|
aegisPatterns: Array.isArray(state.aegisPatterns) ? state.aegisPatterns.length : 0,
|
|
591
865
|
heartbeats: Array.isArray(state.heartbeats) ? state.heartbeats.length : 0,
|
|
@@ -827,6 +1101,15 @@ async function heartbeatUpstream(req, body, client, apiKey) {
|
|
|
827
1101
|
},
|
|
828
1102
|
};
|
|
829
1103
|
clearRevocationLock();
|
|
1104
|
+
const existingBundle = loadEncryptedOfflineBundle(apiKey);
|
|
1105
|
+
persistOfflineBundle(apiKey, {
|
|
1106
|
+
keyHash: lease.keyHash,
|
|
1107
|
+
lease,
|
|
1108
|
+
packet: existingBundle?.packet || null,
|
|
1109
|
+
source: existingBundle?.source || client.baseUrl || DEFAULT_HARNESS_URL,
|
|
1110
|
+
doctrineBundleHash: lease.claims?.doctrine_bundle_hash || existingBundle?.doctrineBundleHash || null,
|
|
1111
|
+
lastUpstreamError: null,
|
|
1112
|
+
});
|
|
830
1113
|
} catch (error) {
|
|
831
1114
|
if (!ownerBypassAllowed) {
|
|
832
1115
|
throw error;
|
|
@@ -883,14 +1166,45 @@ async function ensureLease(req, body, client) {
|
|
|
883
1166
|
|
|
884
1167
|
const keyHash = hashKey(apiKey);
|
|
885
1168
|
const cached = leaseCache.get(keyHash) || loadEncryptedLease(apiKey);
|
|
1169
|
+
ensureOfflineBundleSeeded(apiKey, cached);
|
|
886
1170
|
if (cached && cached.hardStopAt > Date.now()) {
|
|
887
1171
|
leaseCache.set(keyHash, cached);
|
|
888
1172
|
if (cached.nextRequiredAt > Date.now()) {
|
|
889
1173
|
return cached;
|
|
890
1174
|
}
|
|
891
1175
|
}
|
|
892
|
-
|
|
893
|
-
|
|
1176
|
+
try {
|
|
1177
|
+
return await heartbeatUpstream(req, body, client, apiKey);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
const bundle = ensureOfflineBundleSeeded(apiKey, cached) || loadEncryptedOfflineBundle(apiKey);
|
|
1180
|
+
const bundleStatus = computeOfflineBundleStatus(bundle);
|
|
1181
|
+
if (bundle?.keyHash === keyHash && bundleStatus.usable) {
|
|
1182
|
+
const fallbackLease = cached || bundle.lease || null;
|
|
1183
|
+
if (fallbackLease) {
|
|
1184
|
+
const lease = {
|
|
1185
|
+
...fallbackLease,
|
|
1186
|
+
offlineBundle: {
|
|
1187
|
+
phase: bundleStatus.phase,
|
|
1188
|
+
ageSeconds: bundleStatus.ageSeconds,
|
|
1189
|
+
softExpiresAt: bundleStatus.softExpiresAt,
|
|
1190
|
+
hardExpiresAt: bundleStatus.hardExpiresAt,
|
|
1191
|
+
doctrineBundleHash: bundleStatus.doctrineBundleHash,
|
|
1192
|
+
source: bundleStatus.source,
|
|
1193
|
+
lastUpstreamError: error instanceof Error ? error.message : String(error),
|
|
1194
|
+
},
|
|
1195
|
+
};
|
|
1196
|
+
leaseCache.set(keyHash, lease);
|
|
1197
|
+
persistOfflineBundle(apiKey, {
|
|
1198
|
+
...bundle,
|
|
1199
|
+
lease,
|
|
1200
|
+
lastUpstreamError: error instanceof Error ? error.message : String(error),
|
|
1201
|
+
lastUpstreamOkAt: bundle.lastUpstreamOkAt || bundle.cachedAt || new Date().toISOString(),
|
|
1202
|
+
});
|
|
1203
|
+
return lease;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
894
1208
|
}
|
|
895
1209
|
|
|
896
1210
|
function deriveSessionId(req, body, prefix = 'runtime') {
|
|
@@ -949,6 +1263,7 @@ async function pushTelemetryUpstream(client, apiKey, payload) {
|
|
|
949
1263
|
}
|
|
950
1264
|
|
|
951
1265
|
async function pushDecisionUpstream(client, apiKey, payload) {
|
|
1266
|
+
const normalizedPayload = normalizeDecisionPayload(payload);
|
|
952
1267
|
const url = `${client.baseUrl || DEFAULT_HARNESS_URL}/api/decisions/log`;
|
|
953
1268
|
const response = await fetch(url, {
|
|
954
1269
|
method: 'POST',
|
|
@@ -956,7 +1271,7 @@ async function pushDecisionUpstream(client, apiKey, payload) {
|
|
|
956
1271
|
Authorization: `Bearer ${apiKey}`,
|
|
957
1272
|
'Content-Type': 'application/json',
|
|
958
1273
|
},
|
|
959
|
-
body: JSON.stringify(
|
|
1274
|
+
body: JSON.stringify(normalizedPayload),
|
|
960
1275
|
});
|
|
961
1276
|
if (!response.ok) {
|
|
962
1277
|
const body = await response.text().catch(() => response.statusText);
|
|
@@ -965,6 +1280,88 @@ async function pushDecisionUpstream(client, apiKey, payload) {
|
|
|
965
1280
|
return response.json().catch(() => ({ logged: true }));
|
|
966
1281
|
}
|
|
967
1282
|
|
|
1283
|
+
function normalizeDecisionPayload(payload) {
|
|
1284
|
+
const sessionId = payload?.session_id || payload?.sessionId || null;
|
|
1285
|
+
const decisionType = payload?.decision_type || payload?.decisionType || 'operational';
|
|
1286
|
+
const category = payload?.category || payload?.decision_category || payload?.decisionCategory || payload?.surface || 'runtime';
|
|
1287
|
+
const decision = payload?.decision || payload?.summary || payload?.outcome || decisionType;
|
|
1288
|
+
const reasoning =
|
|
1289
|
+
payload?.reasoning ||
|
|
1290
|
+
payload?.summary ||
|
|
1291
|
+
(payload?.outcome ? `Outcome: ${payload.outcome}` : 'Runtime decision log');
|
|
1292
|
+
const context =
|
|
1293
|
+
payload?.context ||
|
|
1294
|
+
(sessionId ? `Session ${sessionId}` : 'Runtime decision log');
|
|
1295
|
+
const outcomeRaw = String(payload?.outcome || '').toLowerCase();
|
|
1296
|
+
const outcome =
|
|
1297
|
+
outcomeRaw === 'validated' ? 'success'
|
|
1298
|
+
: outcomeRaw === 'error' ? 'failure'
|
|
1299
|
+
: ['success', 'failure', 'neutral', 'pending'].includes(outcomeRaw) ? outcomeRaw
|
|
1300
|
+
: undefined;
|
|
1301
|
+
|
|
1302
|
+
return {
|
|
1303
|
+
...payload,
|
|
1304
|
+
session_id: sessionId,
|
|
1305
|
+
decision_type: decisionType,
|
|
1306
|
+
category,
|
|
1307
|
+
context,
|
|
1308
|
+
decision,
|
|
1309
|
+
reasoning,
|
|
1310
|
+
...(outcome ? { outcome } : {}),
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function delay(ms) {
|
|
1315
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async function pushDecisionUpstreamWithRetry(client, apiKey, payload, attempts = 3) {
|
|
1319
|
+
let lastError = null;
|
|
1320
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1321
|
+
try {
|
|
1322
|
+
return await pushDecisionUpstream(client, apiKey, payload);
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
lastError = error;
|
|
1325
|
+
if (attempt === attempts - 1) break;
|
|
1326
|
+
await delay(250 * (2 ** attempt));
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
throw lastError || new Error('decision upstream failed');
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
async function flushPendingDecisionUploads(client, apiKey) {
|
|
1333
|
+
const state = loadEncryptedCognitionState(apiKey);
|
|
1334
|
+
const pending = Array.isArray(state.pendingDecisions) ? state.pendingDecisions : [];
|
|
1335
|
+
if (!pending.length) {
|
|
1336
|
+
return { flushed: 0, retained: 0, lastError: null };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const remaining = [];
|
|
1340
|
+
let flushed = 0;
|
|
1341
|
+
let lastError = null;
|
|
1342
|
+
for (const entry of pending) {
|
|
1343
|
+
try {
|
|
1344
|
+
await pushDecisionUpstreamWithRetry(client, apiKey, entry.payload);
|
|
1345
|
+
flushed++;
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1348
|
+
remaining.push({
|
|
1349
|
+
...entry,
|
|
1350
|
+
attempts: Number(entry.attempts || 0) + 1,
|
|
1351
|
+
lastError,
|
|
1352
|
+
lastTriedAt: new Date().toISOString(),
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
mutateCognitionState(apiKey, (current) => ({
|
|
1358
|
+
...current,
|
|
1359
|
+
pendingDecisions: remaining,
|
|
1360
|
+
}));
|
|
1361
|
+
|
|
1362
|
+
return { flushed, retained: remaining.length, lastError };
|
|
1363
|
+
}
|
|
1364
|
+
|
|
968
1365
|
function buildMinimalInjection(packet, task, aegisLearnings = null) {
|
|
969
1366
|
return {
|
|
970
1367
|
harness: packet,
|
|
@@ -1022,9 +1419,35 @@ function buildOwnerBypassPacket(message, reason = 'owner-local-bypass') {
|
|
|
1022
1419
|
|
|
1023
1420
|
async function loadRuntimePacket(req, body, client, packetRequest, message) {
|
|
1024
1421
|
if (body.packet) return body.packet;
|
|
1422
|
+
const apiKey = resolveApiKey(req, body);
|
|
1423
|
+
ensureOfflineBundleSeeded(apiKey, leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey));
|
|
1025
1424
|
try {
|
|
1026
|
-
|
|
1425
|
+
const wrapped = await client.getHarnessPacket(packetRequest || {});
|
|
1426
|
+
const packet = normalizeHarnessPacketPayload(wrapped?.packet || wrapped);
|
|
1427
|
+
const lease = leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey);
|
|
1428
|
+
persistOfflineBundle(apiKey, {
|
|
1429
|
+
keyHash: hashKey(apiKey),
|
|
1430
|
+
packet,
|
|
1431
|
+
lease,
|
|
1432
|
+
source: client.baseUrl || DEFAULT_HARNESS_URL,
|
|
1433
|
+
doctrineBundleHash: lease?.claims?.doctrine_bundle_hash || null,
|
|
1434
|
+
lastUpstreamError: null,
|
|
1435
|
+
});
|
|
1436
|
+
return packet;
|
|
1027
1437
|
} catch (error) {
|
|
1438
|
+
const bundle = ensureOfflineBundleSeeded(apiKey, leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey)) || loadEncryptedOfflineBundle(apiKey);
|
|
1439
|
+
const bundleStatus = computeOfflineBundleStatus(bundle);
|
|
1440
|
+
if (bundle?.keyHash === hashKey(apiKey) && bundleStatus.usable) {
|
|
1441
|
+
const fallbackPacket = buildOfflineBundleFallbackPacket(bundle, bundleStatus);
|
|
1442
|
+
if (fallbackPacket) {
|
|
1443
|
+
persistOfflineBundle(apiKey, {
|
|
1444
|
+
...bundle,
|
|
1445
|
+
lastUpstreamError: error instanceof Error ? error.message : String(error),
|
|
1446
|
+
lastUpstreamOkAt: bundle.lastUpstreamOkAt || bundle.cachedAt || new Date().toISOString(),
|
|
1447
|
+
});
|
|
1448
|
+
return fallbackPacket;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1028
1451
|
if (!isOwnerBypassRequest(req, body)) {
|
|
1029
1452
|
throw error;
|
|
1030
1453
|
}
|
|
@@ -1266,6 +1689,174 @@ function anthropicResponseEnvelope(text, providerMeta, extra = {}, debug = false
|
|
|
1266
1689
|
};
|
|
1267
1690
|
}
|
|
1268
1691
|
|
|
1692
|
+
function flattenResponsesTextContent(content) {
|
|
1693
|
+
if (typeof content === 'string') return content;
|
|
1694
|
+
if (!Array.isArray(content)) {
|
|
1695
|
+
if (typeof content?.text === 'string') return content.text;
|
|
1696
|
+
if (typeof content?.content === 'string') return content.content;
|
|
1697
|
+
return '';
|
|
1698
|
+
}
|
|
1699
|
+
return content
|
|
1700
|
+
.map((part) => {
|
|
1701
|
+
if (typeof part === 'string') return part;
|
|
1702
|
+
if (typeof part?.text === 'string') return part.text;
|
|
1703
|
+
if (typeof part?.content === 'string') return part.content;
|
|
1704
|
+
if ((part?.type === 'input_text' || part?.type === 'output_text' || part?.type === 'text') && typeof part?.text === 'string') {
|
|
1705
|
+
return part.text;
|
|
1706
|
+
}
|
|
1707
|
+
return '';
|
|
1708
|
+
})
|
|
1709
|
+
.filter(Boolean)
|
|
1710
|
+
.join('\n');
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function normalizeResponsesTool(tool) {
|
|
1714
|
+
if (!tool || typeof tool !== 'object') return null;
|
|
1715
|
+
if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
|
|
1716
|
+
return tool;
|
|
1717
|
+
}
|
|
1718
|
+
if (tool.type === 'function' || typeof tool.name === 'string') {
|
|
1719
|
+
return {
|
|
1720
|
+
type: 'function',
|
|
1721
|
+
function: {
|
|
1722
|
+
name: tool.name || tool.function?.name || 'tool',
|
|
1723
|
+
description: tool.description || tool.function?.description || '',
|
|
1724
|
+
parameters: tool.parameters || tool.function?.parameters || { type: 'object', properties: {} },
|
|
1725
|
+
},
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
return tool;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function responsesInputToMessages(input) {
|
|
1732
|
+
if (typeof input === 'string' && input.trim()) {
|
|
1733
|
+
return [{ role: 'user', content: input.trim() }];
|
|
1734
|
+
}
|
|
1735
|
+
if (!Array.isArray(input)) return [];
|
|
1736
|
+
|
|
1737
|
+
const messages = [];
|
|
1738
|
+
for (const item of input) {
|
|
1739
|
+
if (!item || typeof item !== 'object') continue;
|
|
1740
|
+
if (item.type === 'message') {
|
|
1741
|
+
const role = typeof item.role === 'string' ? item.role : 'user';
|
|
1742
|
+
const text = flattenResponsesTextContent(item.content);
|
|
1743
|
+
if (text) messages.push({ role, content: text });
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
if (item.type === 'input_text' || item.type === 'output_text' || item.type === 'text') {
|
|
1747
|
+
const role = item.role === 'assistant' ? 'assistant' : 'user';
|
|
1748
|
+
const text = flattenResponsesTextContent(item);
|
|
1749
|
+
if (text) messages.push({ role, content: text });
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
if (item.type === 'function_call_output') {
|
|
1753
|
+
const outputText =
|
|
1754
|
+
typeof item.output === 'string'
|
|
1755
|
+
? item.output
|
|
1756
|
+
: JSON.stringify(item.output || {});
|
|
1757
|
+
if (outputText) {
|
|
1758
|
+
messages.push({
|
|
1759
|
+
role: 'tool',
|
|
1760
|
+
tool_call_id: item.call_id || item.id || null,
|
|
1761
|
+
content: outputText,
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
if (item.type === 'function_call') {
|
|
1767
|
+
const args =
|
|
1768
|
+
typeof item.arguments === 'string'
|
|
1769
|
+
? item.arguments
|
|
1770
|
+
: JSON.stringify(item.arguments || item.input || {});
|
|
1771
|
+
messages.push({
|
|
1772
|
+
role: 'assistant',
|
|
1773
|
+
content: `${item.name || 'function_call'} ${args}`.trim(),
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return messages;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function responsesRequestToOpenAIBody(body = {}) {
|
|
1781
|
+
const messages = responsesInputToMessages(body.input);
|
|
1782
|
+
const instructions = typeof body.instructions === 'string' && body.instructions.trim()
|
|
1783
|
+
? [{ role: 'system', content: body.instructions.trim() }]
|
|
1784
|
+
: [];
|
|
1785
|
+
|
|
1786
|
+
return {
|
|
1787
|
+
...body,
|
|
1788
|
+
client: body.client || 'codex',
|
|
1789
|
+
surface: body.surface || 'codex',
|
|
1790
|
+
messages: [...instructions, ...messages],
|
|
1791
|
+
tools: Array.isArray(body.tools) ? body.tools.map(normalizeResponsesTool).filter(Boolean) : undefined,
|
|
1792
|
+
tool_choice: body.tool_choice,
|
|
1793
|
+
max_completion_tokens: body.max_output_tokens || body.max_completion_tokens,
|
|
1794
|
+
metadata: {
|
|
1795
|
+
...(body.metadata && typeof body.metadata === 'object' ? body.metadata : {}),
|
|
1796
|
+
response_api: true,
|
|
1797
|
+
client: body.client || 'codex',
|
|
1798
|
+
surface: body.surface || 'codex',
|
|
1799
|
+
},
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function openAiCompletionToResponsesEnvelope(body, completion) {
|
|
1804
|
+
const message = completion?.choices?.[0]?.message || {};
|
|
1805
|
+
const text = typeof message.content === 'string' ? message.content : '';
|
|
1806
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
1807
|
+
const output = [];
|
|
1808
|
+
|
|
1809
|
+
if (text) {
|
|
1810
|
+
output.push({
|
|
1811
|
+
id: `msg_${randomUUID().replace(/-/g, '')}`,
|
|
1812
|
+
type: 'message',
|
|
1813
|
+
role: 'assistant',
|
|
1814
|
+
status: 'completed',
|
|
1815
|
+
content: [
|
|
1816
|
+
{
|
|
1817
|
+
type: 'output_text',
|
|
1818
|
+
text,
|
|
1819
|
+
},
|
|
1820
|
+
],
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
for (const toolCall of toolCalls) {
|
|
1825
|
+
const callId = toolCall?.id || `call_${randomUUID().replace(/-/g, '')}`;
|
|
1826
|
+
const functionInfo = toolCall?.function || {};
|
|
1827
|
+
output.push({
|
|
1828
|
+
id: callId,
|
|
1829
|
+
type: 'function_call',
|
|
1830
|
+
status: 'completed',
|
|
1831
|
+
call_id: callId,
|
|
1832
|
+
name: functionInfo.name || toolCall?.name || 'tool',
|
|
1833
|
+
arguments:
|
|
1834
|
+
typeof functionInfo.arguments === 'string'
|
|
1835
|
+
? functionInfo.arguments
|
|
1836
|
+
: JSON.stringify(functionInfo.arguments || toolCall?.arguments || {}),
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return {
|
|
1841
|
+
id: `resp_${randomUUID().replace(/-/g, '')}`,
|
|
1842
|
+
object: 'response',
|
|
1843
|
+
created_at: new Date().toISOString(),
|
|
1844
|
+
status: 'completed',
|
|
1845
|
+
model: completion?.model || body?.model || 'aria-runtime',
|
|
1846
|
+
output,
|
|
1847
|
+
output_text: text,
|
|
1848
|
+
usage: completion?.usage
|
|
1849
|
+
? {
|
|
1850
|
+
input_tokens: completion.usage.prompt_tokens || 0,
|
|
1851
|
+
output_tokens: completion.usage.completion_tokens || 0,
|
|
1852
|
+
total_tokens: completion.usage.total_tokens || ((completion.usage.prompt_tokens || 0) + (completion.usage.completion_tokens || 0)),
|
|
1853
|
+
}
|
|
1854
|
+
: undefined,
|
|
1855
|
+
aria: completion?.aria || null,
|
|
1856
|
+
metadata: body?.metadata || null,
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1269
1860
|
function toReadableSignal(name) {
|
|
1270
1861
|
const map = {
|
|
1271
1862
|
fitrah_gate: 'truth boundary',
|
|
@@ -1644,6 +2235,10 @@ function computeForgeContractIssues(manifest, forgeResult) {
|
|
|
1644
2235
|
}
|
|
1645
2236
|
|
|
1646
2237
|
async function persistTurnArtifacts(req, body, client, apiKey, turn) {
|
|
2238
|
+
persistMizanBundle(apiKey, turn.preBundle, 'runtime/v1-pre');
|
|
2239
|
+
persistMizanBundle(apiKey, turn.midBundle, 'runtime/v1-mid');
|
|
2240
|
+
persistMizanBundle(apiKey, turn.postBundle, 'runtime/v1-post');
|
|
2241
|
+
|
|
1647
2242
|
const evolutionPrinciples = extractEvolutionPrinciples(turn.packet, [turn.preResult, turn.midResult, turn.postResult]);
|
|
1648
2243
|
const aegisPatterns = [
|
|
1649
2244
|
...extractAegisPatterns(turn.midResult || {}),
|
|
@@ -1783,39 +2378,93 @@ async function persistTurnArtifacts(req, body, client, apiKey, turn) {
|
|
|
1783
2378
|
await pushTelemetryUpstream(client, apiKey, telemetryPayload);
|
|
1784
2379
|
} catch {}
|
|
1785
2380
|
|
|
1786
|
-
|
|
2381
|
+
const isDecisionTurn =
|
|
2382
|
+
(turn.turnClass?.intensity && turn.turnClass.intensity !== 'light') ||
|
|
2383
|
+
isNonTrivialAssistantTurn(turn.userMessage || '', []) ||
|
|
2384
|
+
isNonTrivialAssistantTurn(turn.finalText || '', []);
|
|
2385
|
+
if (isDecisionTurn) {
|
|
2386
|
+
const surface =
|
|
2387
|
+
body?.surface ||
|
|
2388
|
+
body?.platform ||
|
|
2389
|
+
body?.client ||
|
|
2390
|
+
body?.metadata?.surface ||
|
|
2391
|
+
body?.metadata?.client ||
|
|
2392
|
+
'aria-mounted-runtime';
|
|
2393
|
+
const reasoning = [
|
|
2394
|
+
...(turn.preResult?.notes || []),
|
|
2395
|
+
...(turn.midResult?.notes || []),
|
|
2396
|
+
...(turn.postResult?.notes || []),
|
|
2397
|
+
...(Array.isArray(turn.validation?.violations) ? turn.validation.violations : []),
|
|
2398
|
+
...(Array.isArray(turn.layer3?.failures)
|
|
2399
|
+
? turn.layer3.failures.map((failure) => failure?.detail).filter(Boolean)
|
|
2400
|
+
: []),
|
|
2401
|
+
]
|
|
2402
|
+
.join(' | ')
|
|
2403
|
+
.slice(0, 4000) || 'Aria runtime cognition turn';
|
|
1787
2404
|
const decisionPayload = {
|
|
1788
2405
|
decision_type: body?.metadata?.decision_type || 'runtime-turn',
|
|
1789
2406
|
category: body?.metadata?.decision_category || 'cognition-control-plane',
|
|
1790
|
-
context:
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
...(turn.postResult?.notes || []),
|
|
1796
|
-
].join(' | ').slice(0, 4000) || 'Aria runtime cognition turn',
|
|
2407
|
+
context:
|
|
2408
|
+
(turn.userMessage && turn.userMessage.slice(0, 2000)) ||
|
|
2409
|
+
`${surface} runtime turn (${turn.providerMeta.provider || 'provider'})`,
|
|
2410
|
+
decision: turn.success ? 'turn completed' : 'turn blocked by runtime gate',
|
|
2411
|
+
reasoning,
|
|
1797
2412
|
outcome: 'pending',
|
|
1798
|
-
|
|
1799
|
-
|
|
2413
|
+
outcome_details: {
|
|
2414
|
+
expected: body?.metadata?.expected_outcome || null,
|
|
2415
|
+
immediate_actual: {
|
|
2416
|
+
success: turn.success,
|
|
2417
|
+
validation_severity: turn.validation?.severity || null,
|
|
2418
|
+
layer3_pass: turn.layer3?.pass ?? null,
|
|
2419
|
+
doctrine_hits: Array.isArray(turn.layer3?.doctrine?.hits)
|
|
2420
|
+
? turn.layer3.doctrine.hits.length
|
|
2421
|
+
: 0,
|
|
2422
|
+
},
|
|
2423
|
+
anchors: [
|
|
2424
|
+
turn.preReceipt?.receiptId ? `pre_receipt:${turn.preReceipt.receiptId}` : null,
|
|
2425
|
+
turn.midReceipt?.receiptId ? `mid_receipt:${turn.midReceipt.receiptId}` : null,
|
|
2426
|
+
turn.postReceipt?.receiptId ? `post_receipt:${turn.postReceipt.receiptId}` : null,
|
|
2427
|
+
].filter(Boolean),
|
|
2428
|
+
},
|
|
2429
|
+
expected_outcome: body?.metadata?.expected_outcome || {
|
|
2430
|
+
predicate: 'turn survives runtime validation and compounds into central telemetry with canonical receipts',
|
|
1800
2431
|
measurable_type: 'boolean',
|
|
1801
2432
|
threshold: true,
|
|
1802
2433
|
},
|
|
1803
|
-
|
|
2434
|
+
metadata: {
|
|
2435
|
+
session_id: turn.sessionId,
|
|
2436
|
+
surface,
|
|
2437
|
+
provider: turn.providerMeta.provider || null,
|
|
2438
|
+
finish_reason: turn.providerMeta.finishReason || null,
|
|
2439
|
+
pre_receipt_id: turn.preReceipt?.receiptId || null,
|
|
2440
|
+
mid_receipt_id: turn.midReceipt?.receiptId || null,
|
|
2441
|
+
post_receipt_id: turn.postReceipt?.receiptId || null,
|
|
2442
|
+
validation_severity: turn.validation?.severity || null,
|
|
2443
|
+
validation_passed: turn.validation?.passed ?? null,
|
|
2444
|
+
layer3_pass: turn.layer3?.pass ?? null,
|
|
2445
|
+
packet_bypassed: turn.packetBypassed === true,
|
|
2446
|
+
},
|
|
2447
|
+
source: body?.metadata?.decision_source || `${surface}-runtime`,
|
|
1804
2448
|
model_used: turn.providerMeta.model,
|
|
1805
2449
|
code_links: body?.metadata?.code_links || null,
|
|
1806
2450
|
};
|
|
2451
|
+
let decisionResult = null;
|
|
2452
|
+
let decisionError = null;
|
|
1807
2453
|
try {
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2454
|
+
decisionResult = await pushDecisionUpstream(client, apiKey, decisionPayload);
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
decisionError = error instanceof Error ? error.message : String(error);
|
|
2457
|
+
}
|
|
2458
|
+
mutateCognitionState(apiKey, (state) => ({
|
|
2459
|
+
...state,
|
|
2460
|
+
decisions: appendBounded(state.decisions, {
|
|
2461
|
+
at: new Date().toISOString(),
|
|
2462
|
+
sessionId: turn.sessionId,
|
|
2463
|
+
decision: decisionPayload,
|
|
2464
|
+
result: decisionResult,
|
|
2465
|
+
error: decisionError,
|
|
2466
|
+
}, DECISION_LIMIT),
|
|
2467
|
+
}));
|
|
1819
2468
|
}
|
|
1820
2469
|
|
|
1821
2470
|
if (body.ariaGarden !== false && turn.userMessage && turn.finalText) {
|
|
@@ -1841,6 +2490,20 @@ async function handleProviderProxy(req, body, client, providerStyle) {
|
|
|
1841
2490
|
? await callProviderForAnthropic(body, turn.ariaSystemPrompt)
|
|
1842
2491
|
: await callProviderForOpenAI(body, turn.ariaSystemPrompt);
|
|
1843
2492
|
|
|
2493
|
+
try {
|
|
2494
|
+
recordTokenUsage({
|
|
2495
|
+
tenantId: body?.metadata?.jti || body?.jti || 'owner-local',
|
|
2496
|
+
agentId: body?.metadata?.agentId || body?.metadata?.roleProfile || null,
|
|
2497
|
+
agentName: body?.metadata?.agentName || null,
|
|
2498
|
+
department: body?.metadata?.department || null,
|
|
2499
|
+
sessionId: turn.sessionId,
|
|
2500
|
+
provider: providerMeta.provider,
|
|
2501
|
+
model: providerMeta.model,
|
|
2502
|
+
usage: providerMeta.usage,
|
|
2503
|
+
requestType: body?.metadata?.requestType || 'chat',
|
|
2504
|
+
});
|
|
2505
|
+
} catch {}
|
|
2506
|
+
|
|
1844
2507
|
let candidateText = providerMeta.text || '';
|
|
1845
2508
|
const toolIntents = extractProviderToolIntents(providerStyle, providerMeta);
|
|
1846
2509
|
const requiresReadableCognition = isNonTrivialAssistantTurn(candidateText, toolIntents);
|
|
@@ -1856,7 +2519,7 @@ async function handleProviderProxy(req, body, client, providerStyle) {
|
|
|
1856
2519
|
try {
|
|
1857
2520
|
validation = await client.validateOutput(candidateText, turn.sessionId);
|
|
1858
2521
|
} catch (error) {
|
|
1859
|
-
if (!turn.packetBypassed) throw error;
|
|
2522
|
+
if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
|
|
1860
2523
|
validation = {
|
|
1861
2524
|
passed: true,
|
|
1862
2525
|
severity: 'warn',
|
|
@@ -1871,7 +2534,7 @@ async function handleProviderProxy(req, body, client, providerStyle) {
|
|
|
1871
2534
|
try {
|
|
1872
2535
|
validation = await client.validateOutput(candidateText, turn.sessionId);
|
|
1873
2536
|
} catch (error) {
|
|
1874
|
-
if (!turn.packetBypassed) throw error;
|
|
2537
|
+
if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
|
|
1875
2538
|
validation = {
|
|
1876
2539
|
passed: true,
|
|
1877
2540
|
severity: 'warn',
|
|
@@ -1911,7 +2574,7 @@ async function handleProviderProxy(req, body, client, providerStyle) {
|
|
|
1911
2574
|
toolGateBlockers.push(`${intent.toolName}: ${runtimeCheck.reason || 'runtime action gate blocked this tool request'}`);
|
|
1912
2575
|
}
|
|
1913
2576
|
} catch (error) {
|
|
1914
|
-
if (!turn.packetBypassed) throw error;
|
|
2577
|
+
if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
|
|
1915
2578
|
toolGateBlockers.push(`${intent.toolName}: runtime action gate unavailable during owner-local-bypass`);
|
|
1916
2579
|
}
|
|
1917
2580
|
}
|
|
@@ -2260,6 +2923,8 @@ function packetToSubstrateSet(packet) {
|
|
|
2260
2923
|
function runtimeManifest() {
|
|
2261
2924
|
const runtimeMeta = ensureRuntimeMeta();
|
|
2262
2925
|
const state = sweepAutonomyState(loadAutonomyState());
|
|
2926
|
+
const ownerToken = readOwnerToken();
|
|
2927
|
+
const offlineBundleStatus = ownerToken ? computeOfflineBundleStatus(loadEncryptedOfflineBundle(ownerToken)) : computeOfflineBundleStatus(null);
|
|
2263
2928
|
return {
|
|
2264
2929
|
ok: true,
|
|
2265
2930
|
runtime: 'aria-mounted-runtime',
|
|
@@ -2306,6 +2971,8 @@ function runtimeManifest() {
|
|
|
2306
2971
|
'POST /forge/synthesize',
|
|
2307
2972
|
'POST /codebase/state',
|
|
2308
2973
|
'POST /v1/chat/completions',
|
|
2974
|
+
'POST /v1/responses',
|
|
2975
|
+
'POST /responses',
|
|
2309
2976
|
'POST /v1/messages',
|
|
2310
2977
|
],
|
|
2311
2978
|
mount: {
|
|
@@ -2323,9 +2990,13 @@ function runtimeManifest() {
|
|
|
2323
2990
|
},
|
|
2324
2991
|
security: {
|
|
2325
2992
|
encrypted_local_lease: LEASE_PATH,
|
|
2993
|
+
encrypted_offline_bundle: OFFLINE_BUNDLE_PATH,
|
|
2326
2994
|
encrypted_cognition_state: COGNITION_STATE_PATH,
|
|
2327
2995
|
revocation_lock: REVOCATION_LOCK_PATH,
|
|
2328
2996
|
upstream_heartbeat: '/api/license/heartbeat',
|
|
2997
|
+
offline_bundle_soft_ttl_seconds: DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS,
|
|
2998
|
+
offline_bundle_hard_ttl_seconds: DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS,
|
|
2999
|
+
offline_bundle_status: offlineBundleStatus,
|
|
2329
3000
|
},
|
|
2330
3001
|
memory: {
|
|
2331
3002
|
qdrant_url: DEFAULT_QDRANT_URL,
|
|
@@ -2354,12 +3025,27 @@ async function runLayer3(req, body, client) {
|
|
|
2354
3025
|
substrate: packetToSubstrateSet(packet),
|
|
2355
3026
|
requireCognitionBlock: body.requireCognitionBlock ?? false,
|
|
2356
3027
|
});
|
|
3028
|
+
const doctrineHits = collectDoctrineTriggerHits(body.text);
|
|
3029
|
+
const doctrineFailures = doctrineHits.map((hit) => ({
|
|
3030
|
+
severity: String(hit.severity || 'block').toLowerCase() === 'block' ? 'block' : 'warn',
|
|
3031
|
+
kind: 'drift_trigger',
|
|
3032
|
+
detail: `${hit.trigger} (${hit.memory || 'doctrine_trigger_map.json'}): ${hit.message || hit.teaching || 'Doctrine trigger matched.'}`,
|
|
3033
|
+
}));
|
|
3034
|
+
const allFailures = [...result.failures, ...doctrineFailures];
|
|
3035
|
+
const hardFailures = allFailures.filter((failure) => failure.severity === 'block');
|
|
2357
3036
|
|
|
2358
3037
|
return {
|
|
2359
|
-
pass:
|
|
2360
|
-
summary:
|
|
2361
|
-
|
|
3038
|
+
pass: hardFailures.length === 0,
|
|
3039
|
+
summary:
|
|
3040
|
+
hardFailures.length === 0
|
|
3041
|
+
? `full_chain: pass (${allFailures.length} warns)`
|
|
3042
|
+
: `full_chain: ${hardFailures.length} hard failures across ${allFailures.length} total`,
|
|
3043
|
+
failures: allFailures,
|
|
2362
3044
|
packetTimestamp: packet.timestamp || null,
|
|
3045
|
+
doctrine: {
|
|
3046
|
+
sourcePath: doctrineTriggerMapCache.sourcePath,
|
|
3047
|
+
hits: doctrineHits,
|
|
3048
|
+
},
|
|
2363
3049
|
};
|
|
2364
3050
|
}
|
|
2365
3051
|
|
|
@@ -2698,6 +3384,12 @@ async function handleRoute(req, res) {
|
|
|
2698
3384
|
return json(res, 200, response);
|
|
2699
3385
|
}
|
|
2700
3386
|
|
|
3387
|
+
if (url.pathname === '/v1/responses' || url.pathname === '/responses') {
|
|
3388
|
+
const responseBody = responsesRequestToOpenAIBody(body);
|
|
3389
|
+
const completion = await handleProviderProxy(req, responseBody, client, 'openai');
|
|
3390
|
+
return json(res, 200, openAiCompletionToResponsesEnvelope(body, completion));
|
|
3391
|
+
}
|
|
3392
|
+
|
|
2701
3393
|
if (url.pathname === '/v1/messages') {
|
|
2702
3394
|
const response = await handleProviderProxy(req, body, client, 'anthropic');
|
|
2703
3395
|
return json(res, 200, response);
|
|
@@ -2728,16 +3420,55 @@ async function handleRoute(req, res) {
|
|
|
2728
3420
|
|
|
2729
3421
|
if (url.pathname === '/decision/log') {
|
|
2730
3422
|
const apiKey = resolveApiKey(req, body);
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3423
|
+
const normalizedBody = normalizeDecisionPayload(body);
|
|
3424
|
+
const flush = await flushPendingDecisionUploads(client, apiKey);
|
|
3425
|
+
try {
|
|
3426
|
+
const result = await pushDecisionUpstreamWithRetry(client, apiKey, normalizedBody);
|
|
3427
|
+
mutateCognitionState(apiKey, (state) => ({
|
|
3428
|
+
...state,
|
|
3429
|
+
decisions: appendBounded(state.decisions, {
|
|
3430
|
+
at: new Date().toISOString(),
|
|
3431
|
+
decision: normalizedBody,
|
|
3432
|
+
result,
|
|
3433
|
+
flushedPending: flush.flushed,
|
|
3434
|
+
}, DECISION_LIMIT),
|
|
3435
|
+
}));
|
|
3436
|
+
return json(res, 200, {
|
|
3437
|
+
ok: true,
|
|
2737
3438
|
result,
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
3439
|
+
flushedPending: flush.flushed,
|
|
3440
|
+
retainedPending: flush.retained,
|
|
3441
|
+
});
|
|
3442
|
+
} catch (error) {
|
|
3443
|
+
const upstreamError = error instanceof Error ? error.message : String(error);
|
|
3444
|
+
mutateCognitionState(apiKey, (state) => ({
|
|
3445
|
+
...state,
|
|
3446
|
+
decisions: appendBounded(state.decisions, {
|
|
3447
|
+
at: new Date().toISOString(),
|
|
3448
|
+
decision: normalizedBody,
|
|
3449
|
+
result: {
|
|
3450
|
+
logged: false,
|
|
3451
|
+
queuedUpstream: true,
|
|
3452
|
+
upstreamError,
|
|
3453
|
+
},
|
|
3454
|
+
flushedPending: flush.flushed,
|
|
3455
|
+
}, DECISION_LIMIT),
|
|
3456
|
+
pendingDecisions: appendBounded(state.pendingDecisions, {
|
|
3457
|
+
at: new Date().toISOString(),
|
|
3458
|
+
payload: normalizedBody,
|
|
3459
|
+
attempts: 0,
|
|
3460
|
+
lastError: upstreamError,
|
|
3461
|
+
queuedBy: 'decision/log',
|
|
3462
|
+
}, DECISION_LIMIT),
|
|
3463
|
+
}));
|
|
3464
|
+
return json(res, 200, {
|
|
3465
|
+
ok: true,
|
|
3466
|
+
queuedUpstream: true,
|
|
3467
|
+
upstreamError,
|
|
3468
|
+
flushedPending: flush.flushed,
|
|
3469
|
+
retainedPending: flush.retained + 1,
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
2741
3472
|
}
|
|
2742
3473
|
|
|
2743
3474
|
if (url.pathname === '/aegis/patterns') {
|
|
@@ -2799,12 +3530,12 @@ async function handleRoute(req, res) {
|
|
|
2799
3530
|
}
|
|
2800
3531
|
}
|
|
2801
3532
|
|
|
2802
|
-
if (url.pathname === '/packet') {
|
|
3533
|
+
if (url.pathname === '/packet' || url.pathname === '/api/harness/codex') {
|
|
2803
3534
|
const packet = await loadRuntimePacket(req, body, client, body.packetRequest || body, body.message || '');
|
|
2804
3535
|
return json(res, 200, { ok: true, packet });
|
|
2805
3536
|
}
|
|
2806
3537
|
|
|
2807
|
-
if (url.pathname === '/consult') {
|
|
3538
|
+
if (url.pathname === '/consult' || url.pathname === '/api/harness/delegate') {
|
|
2808
3539
|
const result = await client.consult(body);
|
|
2809
3540
|
return json(res, 200, { ok: true, ...result });
|
|
2810
3541
|
}
|
|
@@ -2824,7 +3555,7 @@ async function handleRoute(req, res) {
|
|
|
2824
3555
|
return json(res, 200, { ok: true, ...result });
|
|
2825
3556
|
}
|
|
2826
3557
|
|
|
2827
|
-
if (url.pathname === '/validate-output') {
|
|
3558
|
+
if (url.pathname === '/validate-output' || url.pathname === '/api/harness/validate') {
|
|
2828
3559
|
if (typeof body.text !== 'string' || typeof body.sessionId !== 'string') {
|
|
2829
3560
|
throw new Error('validate-output requires text and sessionId');
|
|
2830
3561
|
}
|
|
@@ -3003,6 +3734,313 @@ async function handleRoute(req, res) {
|
|
|
3003
3734
|
return json(res, 200, { ok: true, ...result });
|
|
3004
3735
|
}
|
|
3005
3736
|
|
|
3737
|
+
// ── /hq/* routes: Agentic HQ API ──────────────────────────────
|
|
3738
|
+
|
|
3739
|
+
if (url.pathname.startsWith('/hq/')) {
|
|
3740
|
+
const auth = hqAuthMiddleware(url.pathname, req);
|
|
3741
|
+
if (!auth.authorized) {
|
|
3742
|
+
return json(res, 401, { ok: false, error: auth.error || 'Unauthorized' });
|
|
3743
|
+
}
|
|
3744
|
+
if (auth.tenantId && !body.tenantId) body.tenantId = auth.tenantId;
|
|
3745
|
+
req.hqAuth = auth;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
// ── /hq/auth/* routes (public, handled before auth gate takes effect) ──
|
|
3749
|
+
if (url.pathname === '/hq/auth/login') {
|
|
3750
|
+
const { username, password } = body;
|
|
3751
|
+
if (!username || !password) return json(res, 400, { ok: false, error: 'username and password required' });
|
|
3752
|
+
const result = loginUser(username, password);
|
|
3753
|
+
if (!result.ok) return json(res, 401, result);
|
|
3754
|
+
return json(res, 200, { ok: true, token: result.session.token, role: result.session.role, tenantId: result.session.tenantId, email: result.session.email });
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
if (url.pathname === '/hq/auth/register') {
|
|
3758
|
+
const { email, password, tenantId } = body;
|
|
3759
|
+
if (!email || !password || !tenantId) return json(res, 400, { ok: false, error: 'email, password, and tenantId required' });
|
|
3760
|
+
const result = registerUser(email, password, tenantId);
|
|
3761
|
+
if (!result.ok) return json(res, 409, result);
|
|
3762
|
+
const loginResult = loginUser(email, password);
|
|
3763
|
+
return json(res, 200, { ok: true, token: loginResult.session.token, role: loginResult.session.role, tenantId: loginResult.session.tenantId, email: loginResult.session.email });
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
if (url.pathname === '/hq/auth/session') {
|
|
3767
|
+
const authHeader = req.headers['authorization'] || '';
|
|
3768
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
3769
|
+
if (!token) return json(res, 401, { ok: false, error: 'Token required' });
|
|
3770
|
+
const { validateSession } = await import('./auth-middleware.mjs');
|
|
3771
|
+
const session = validateSession(token);
|
|
3772
|
+
if (!session.valid) return json(res, 401, { ok: false, error: 'Invalid or expired session' });
|
|
3773
|
+
return json(res, 200, { ok: true, role: session.role, tenantId: session.tenantId, email: session.email });
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
if (url.pathname === '/hq/auth/logout') {
|
|
3777
|
+
const authHeader = req.headers['authorization'] || '';
|
|
3778
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
3779
|
+
if (token) revokeSession(token);
|
|
3780
|
+
return json(res, 200, { ok: true });
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
// ── /hq/owner/* routes (owner-only) ──
|
|
3784
|
+
if (url.pathname === '/hq/owner/tenants') {
|
|
3785
|
+
const auth = req.hqAuth;
|
|
3786
|
+
if (!auth || auth.role !== 'owner') return json(res, 403, { ok: false, error: 'Owner access required' });
|
|
3787
|
+
const tenants = listAllTenants();
|
|
3788
|
+
const enriched = tenants.map(t => {
|
|
3789
|
+
const fleet = loadFleet(t.tenantId);
|
|
3790
|
+
return { ...t, fleet: fleet ? { deployed: true, activeAgents: fleet.agents?.length || 0, industry: fleet.config?.industry } : { deployed: false } };
|
|
3791
|
+
});
|
|
3792
|
+
return json(res, 200, { ok: true, tenants: enriched });
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
if (url.pathname === '/hq/owner/tenant/dashboard') {
|
|
3796
|
+
const auth = req.hqAuth;
|
|
3797
|
+
if (!auth || auth.role !== 'owner') return json(res, 403, { ok: false, error: 'Owner access required' });
|
|
3798
|
+
const { tenantId: targetTenant } = body;
|
|
3799
|
+
if (!targetTenant) return json(res, 400, { ok: false, error: 'targetTenant required' });
|
|
3800
|
+
const fleet = loadFleet(targetTenant);
|
|
3801
|
+
if (!fleet) return json(res, 404, { ok: false, error: 'No fleet for that tenant' });
|
|
3802
|
+
return json(res, 200, { ok: true, ...getFleetStatus(targetTenant) });
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
if (url.pathname === '/hq/metering/usage') {
|
|
3806
|
+
const { tenantId, from, to } = body;
|
|
3807
|
+
if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
|
|
3808
|
+
return json(res, 200, { ok: true, ...getUsageSummary(tenantId, { from, to }) });
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
if (url.pathname === '/hq/metering/billing') {
|
|
3812
|
+
const { tenantId, tier } = body;
|
|
3813
|
+
if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
|
|
3814
|
+
return json(res, 200, { ok: true, ...getBillingSummary(tenantId, tier || 'starter') });
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
if (url.pathname === '/hq/onboarding/chat') {
|
|
3818
|
+
const { tenantId, message, state: clientState } = body;
|
|
3819
|
+
if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
|
|
3820
|
+
const { createOnboardingSession, loadSession, saveSession, processResponse, getStepPrompt, advanceStep } = await import('./onboarding-engine.mjs');
|
|
3821
|
+
let session = loadSession(tenantId) || createOnboardingSession(tenantId);
|
|
3822
|
+
if (clientState) Object.assign(session.data, clientState);
|
|
3823
|
+
session.history.push({ role: 'user', content: message || '', step: session.step, timestamp: new Date().toISOString() });
|
|
3824
|
+
|
|
3825
|
+
let ariaText = '';
|
|
3826
|
+
|
|
3827
|
+
// Password collection: skip LLM, take user message directly
|
|
3828
|
+
if (session.step === 'credentials' && session.data.email && !session.data.password) {
|
|
3829
|
+
session.data.password = message;
|
|
3830
|
+
session.step = advanceStep(session.step);
|
|
3831
|
+
ariaText = `Great, I have your email as ${session.data.email}. Now please type a password for your dashboard login. Make it at least 8 characters.`;
|
|
3832
|
+
} else {
|
|
3833
|
+
const stepPrompt = getStepPrompt(session.step);
|
|
3834
|
+
try {
|
|
3835
|
+
const providerResult = await callProviderForOpenAI({
|
|
3836
|
+
...body,
|
|
3837
|
+
messages: [
|
|
3838
|
+
{ role: 'system', content: stepPrompt },
|
|
3839
|
+
{ role: 'user', content: message || 'Hello' },
|
|
3840
|
+
],
|
|
3841
|
+
});
|
|
3842
|
+
ariaText = providerResult.text || '';
|
|
3843
|
+
} catch (err) {
|
|
3844
|
+
ariaText = `I'd love to chat more about setting up your AI workforce! Unfortunately, I'm having trouble connecting right now. Could you try again? (Error: ${err.message})`;
|
|
3845
|
+
}
|
|
3846
|
+
session = processResponse(session, ariaText);
|
|
3847
|
+
// After email is extracted, append password prompt
|
|
3848
|
+
if (session.step === 'credentials' && session.data.email && !session.data.password) {
|
|
3849
|
+
ariaText += '\n\nNow please type a password for your dashboard login (at least 8 characters). Your next message will be saved as your password.';
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
saveSession(session);
|
|
3853
|
+
|
|
3854
|
+
if (session.step === 'complete' && session.data.confirmed) {
|
|
3855
|
+
const { buildFleetConfig } = await import('./onboarding-engine.mjs');
|
|
3856
|
+
const fleetConfig = buildFleetConfig(session);
|
|
3857
|
+
const enqueueJob = (jobDef) => {
|
|
3858
|
+
const autonomyState = loadAutonomyState();
|
|
3859
|
+
const job = { jobId: randomUUID(), kind: jobDef.kind, surface: 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
|
|
3860
|
+
autonomyState.jobs.push(job);
|
|
3861
|
+
saveAutonomyState(autonomyState);
|
|
3862
|
+
return job;
|
|
3863
|
+
};
|
|
3864
|
+
const { fleet, workerRegistrations, initialJobs } = deployFleet(tenantId, fleetConfig, enqueueJob);
|
|
3865
|
+
const autonomyState = loadAutonomyState();
|
|
3866
|
+
for (const reg of workerRegistrations) { ensureWorker(autonomyState, reg.workerId, reg); }
|
|
3867
|
+
for (const jobDef of initialJobs) {
|
|
3868
|
+
const job = { jobId: randomUUID(), kind: jobDef.kind, surface: 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
|
|
3869
|
+
autonomyState.jobs.push(job);
|
|
3870
|
+
}
|
|
3871
|
+
saveAutonomyState(autonomyState);
|
|
3872
|
+
const apiKey = generateApiKey(tenantId);
|
|
3873
|
+
let authToken = null;
|
|
3874
|
+
if (session.data.email && session.data.password) {
|
|
3875
|
+
const regResult = registerUser(session.data.email, session.data.password, tenantId);
|
|
3876
|
+
if (regResult.ok) {
|
|
3877
|
+
const loginResult = loginUser(session.data.email, session.data.password);
|
|
3878
|
+
authToken = loginResult.ok ? loginResult.session.token : null;
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
return json(res, 200, { ok: true, step: 'complete', data: session.data, ariaMessage: ariaText, fleet: { deployed: true, agents: fleet.agents.length }, apiKey, authToken });
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
return json(res, 200, { ok: true, step: session.step, data: session.data, ariaMessage: ariaText });
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
if (url.pathname === '/hq/onboarding/status') {
|
|
3888
|
+
const { tenantId } = body;
|
|
3889
|
+
if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
|
|
3890
|
+
const { loadSession } = await import('./onboarding-engine.mjs');
|
|
3891
|
+
const session = loadSession(tenantId);
|
|
3892
|
+
if (!session) return json(res, 200, { ok: true, step: 'not-started', data: {} });
|
|
3893
|
+
return json(res, 200, { ok: true, step: session.step, data: session.data, updatedAt: session.updatedAt });
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
if (url.pathname === '/hq/fleet/deploy') {
|
|
3897
|
+
const { tenantId, config } = body;
|
|
3898
|
+
if (!tenantId || !config) return json(res, 400, { ok: false, error: 'tenantId and config required' });
|
|
3899
|
+
const enqueueJob = (jobDef) => {
|
|
3900
|
+
const autonomyState = loadAutonomyState();
|
|
3901
|
+
const job = { jobId: randomUUID(), kind: jobDef.kind, surface: jobDef.surface || 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
|
|
3902
|
+
autonomyState.jobs.push(job);
|
|
3903
|
+
saveAutonomyState(autonomyState);
|
|
3904
|
+
return job;
|
|
3905
|
+
};
|
|
3906
|
+
const { fleet, workerRegistrations, initialJobs } = deployFleet(tenantId, config, enqueueJob);
|
|
3907
|
+
const autonomyState = loadAutonomyState();
|
|
3908
|
+
for (const reg of workerRegistrations) {
|
|
3909
|
+
ensureWorker(autonomyState, reg.workerId, reg);
|
|
3910
|
+
}
|
|
3911
|
+
for (const jobDef of initialJobs) {
|
|
3912
|
+
const job = { jobId: randomUUID(), kind: jobDef.kind, surface: jobDef.surface || 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
|
|
3913
|
+
autonomyState.jobs.push(job);
|
|
3914
|
+
}
|
|
3915
|
+
saveAutonomyState(autonomyState);
|
|
3916
|
+
return json(res, 200, { ok: true, fleet, workersRegistered: workerRegistrations.length, jobsEnqueued: initialJobs.length });
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
if (url.pathname === '/hq/fleet/status') {
|
|
3920
|
+
const { tenantId } = body;
|
|
3921
|
+
if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
|
|
3922
|
+
return json(res, 200, getFleetStatus(tenantId));
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
if (url.pathname === '/hq/fleet/agent/chat') {
|
|
3926
|
+
const { tenantId, agentId, message: agentMsg } = body;
|
|
3927
|
+
if (!tenantId || !agentId || !agentMsg) return json(res, 400, { ok: false, error: 'tenantId, agentId, and message required' });
|
|
3928
|
+
const fleet = loadFleet(tenantId);
|
|
3929
|
+
if (!fleet) return json(res, 404, { ok: false, error: 'No fleet deployed' });
|
|
3930
|
+
const agent = (fleet.agents || []).find(a => a.id === agentId || a.templateId === agentId);
|
|
3931
|
+
if (!agent) return json(res, 404, { ok: false, error: `Agent ${agentId} not found` });
|
|
3932
|
+
const systemPrompt = buildAgentSystemPrompt(agent, fleet.config);
|
|
3933
|
+
let reply = '';
|
|
3934
|
+
try {
|
|
3935
|
+
const providerResult = await callProviderForOpenAI({
|
|
3936
|
+
...body,
|
|
3937
|
+
messages: [
|
|
3938
|
+
{ role: 'system', content: systemPrompt },
|
|
3939
|
+
{ role: 'user', content: agentMsg },
|
|
3940
|
+
],
|
|
3941
|
+
metadata: { ...body.metadata, agentId: agent.id, agentName: agent.name, department: agent.department, requestType: 'fleet-chat' },
|
|
3942
|
+
});
|
|
3943
|
+
reply = providerResult.text || '';
|
|
3944
|
+
} catch (err) {
|
|
3945
|
+
reply = `I'm having trouble connecting right now. Please try again. (Error: ${err.message})`;
|
|
3946
|
+
}
|
|
3947
|
+
try {
|
|
3948
|
+
if (providerResult.usage) {
|
|
3949
|
+
recordTokenUsage({
|
|
3950
|
+
tenantId: body.tenantId || 'owner-local',
|
|
3951
|
+
agentId: agent.id,
|
|
3952
|
+
agentName: agent.name,
|
|
3953
|
+
department: agent.department,
|
|
3954
|
+
sessionId: null,
|
|
3955
|
+
provider: 'deepseek',
|
|
3956
|
+
model: body.model || 'deepseek-v4-flash',
|
|
3957
|
+
usage: providerResult.usage,
|
|
3958
|
+
requestType: 'fleet-chat',
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
} catch {}
|
|
3962
|
+
agent.lastActivity = new Date().toISOString();
|
|
3963
|
+
agent.stats.messagesSent = (agent.stats.messagesSent || 0) + 1;
|
|
3964
|
+
saveFleet(fleet);
|
|
3965
|
+
return json(res, 200, { ok: true, agentId, agentName: agent.name, department: agent.department, reply, status: agent.status });
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
if (url.pathname === '/hq/fleet/agent/action') {
|
|
3969
|
+
const { tenantId, agentId, action, payload: actionPayload } = body;
|
|
3970
|
+
if (!tenantId || !agentId || !action) return json(res, 400, { ok: false, error: 'tenantId, agentId, and action required' });
|
|
3971
|
+
const fleet = loadFleet(tenantId);
|
|
3972
|
+
if (!fleet) return json(res, 404, { ok: false, error: 'No fleet deployed' });
|
|
3973
|
+
const agent = (fleet.agents || []).find(a => a.id === agentId || a.templateId === agentId);
|
|
3974
|
+
if (!agent) return json(res, 404, { ok: false, error: `Agent ${agentId} not found` });
|
|
3975
|
+
const autonomyState = loadAutonomyState();
|
|
3976
|
+
const job = { jobId: randomUUID(), kind: `fleet:${agent.id}:${action}`, surface: 'fleet', sessionId: null, payload: actionPayload || {}, metadata: { tenantId, agentId: agent.id, agentName: agent.name, department: agent.department, action, requestType: 'fleet-action' }, priority: 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
|
|
3977
|
+
autonomyState.jobs.push(job);
|
|
3978
|
+
saveAutonomyState(autonomyState);
|
|
3979
|
+
agent.lastActivity = new Date().toISOString();
|
|
3980
|
+
saveFleet(fleet);
|
|
3981
|
+
return json(res, 200, { ok: true, agentId, action, jobId: job.jobId, result: 'enqueued' });
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
if (url.pathname === '/hq/plugins/list') {
|
|
3985
|
+
const { tenantId } = body;
|
|
3986
|
+
return json(res, 200, { ok: true, plugins: listPlugins(tenantId || 'owner-local') });
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
if (url.pathname === '/hq/plugins/install') {
|
|
3990
|
+
const { tenantId, pluginId, config: pluginConfig } = body;
|
|
3991
|
+
if (!tenantId || !pluginId) return json(res, 400, { ok: false, error: 'tenantId and pluginId required' });
|
|
3992
|
+
const result = installPlugin(tenantId, pluginId, pluginConfig);
|
|
3993
|
+
if (!result.ok) return json(res, 400, result);
|
|
3994
|
+
return json(res, 200, result);
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
if (url.pathname === '/hq/plugins/configure') {
|
|
3998
|
+
const { tenantId, pluginId, config: pluginConfig } = body;
|
|
3999
|
+
if (!tenantId || !pluginId) return json(res, 400, { ok: false, error: 'tenantId and pluginId required' });
|
|
4000
|
+
const result = configurePlugin(tenantId, pluginId, pluginConfig);
|
|
4001
|
+
if (!result.ok) return json(res, result.error.startsWith('No plugins') ? 404 : 400, result);
|
|
4002
|
+
return json(res, 200, result);
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
if (url.pathname === '/hq/workflows/list') {
|
|
4006
|
+
return json(res, 200, { ok: true, workflows: listWorkflowTemplates() });
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
if (url.pathname === '/hq/workflows/configure') {
|
|
4010
|
+
const { tenantId, workflowId, config: workflowConfig } = body;
|
|
4011
|
+
if (!tenantId || !workflowId) return json(res, 400, { ok: false, error: 'tenantId and workflowId required' });
|
|
4012
|
+
const result = configureWorkflow(tenantId, workflowId, workflowConfig);
|
|
4013
|
+
if (!result.ok) return json(res, 400, result);
|
|
4014
|
+
return json(res, 200, result);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
if (url.pathname === '/hq/workflows/start') {
|
|
4018
|
+
const { tenantId, workflowId, payload } = body;
|
|
4019
|
+
if (!tenantId || !workflowId) return json(res, 400, { ok: false, error: 'tenantId and workflowId required' });
|
|
4020
|
+
const result = startWorkflow(tenantId, workflowId, payload);
|
|
4021
|
+
if (!result.ok) return json(res, 400, result);
|
|
4022
|
+
return json(res, 200, result);
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
if (url.pathname === '/hq/workflows/approve') {
|
|
4026
|
+
const { tenantId, instanceId, approved } = body;
|
|
4027
|
+
if (!tenantId || !instanceId) return json(res, 400, { ok: false, error: 'tenantId and instanceId required' });
|
|
4028
|
+
const result = approveWorkflowStep(tenantId, instanceId, approved !== false);
|
|
4029
|
+
if (!result.ok) return json(res, 400, result);
|
|
4030
|
+
return json(res, 200, result);
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
if (url.pathname === '/hq/workflows/status') {
|
|
4034
|
+
const { tenantId } = body;
|
|
4035
|
+
if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
|
|
4036
|
+
return json(res, 200, { ok: true, ...getWorkflowStatus(tenantId) });
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
if (url.pathname === '/hq/subscription/tier') {
|
|
4040
|
+
const { tier } = body;
|
|
4041
|
+
return json(res, 200, { ok: true, tier: getSubscriptionTier(tier) });
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3006
4044
|
return json(res, 404, { ok: false, error: `No route for POST ${url.pathname}` });
|
|
3007
4045
|
} catch (error) {
|
|
3008
4046
|
return json(res, 500, {
|
|
@@ -3028,6 +4066,15 @@ export async function startRuntimeServer(options = {}) {
|
|
|
3028
4066
|
});
|
|
3029
4067
|
});
|
|
3030
4068
|
});
|
|
4069
|
+
server.on('upgrade', (req, socket) => {
|
|
4070
|
+
try {
|
|
4071
|
+
handleWebSocketUpgrade(req, socket);
|
|
4072
|
+
} catch {
|
|
4073
|
+
try {
|
|
4074
|
+
socket.destroy();
|
|
4075
|
+
} catch {}
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
3031
4078
|
|
|
3032
4079
|
await new Promise((resolve, reject) => {
|
|
3033
4080
|
server.once('error', reject);
|