@aria_asi/cli 0.2.29 → 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.
Files changed (98) hide show
  1. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  2. package/dist/aria-connector/src/connectors/claude-code.js +88 -20
  3. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  4. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.js +526 -2
  6. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  7. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts +7 -0
  8. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts.map +1 -0
  9. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js +87 -0
  10. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js.map +1 -0
  11. package/dist/aria-connector/src/connectors/must-read.d.ts +4 -0
  12. package/dist/aria-connector/src/connectors/must-read.d.ts.map +1 -0
  13. package/dist/aria-connector/src/connectors/must-read.js +111 -0
  14. package/dist/aria-connector/src/connectors/must-read.js.map +1 -0
  15. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  16. package/dist/aria-connector/src/connectors/opencode.js +2 -0
  17. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  18. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  19. package/dist/aria-connector/src/connectors/runtime.js +231 -19
  20. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  21. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  22. package/dist/aria-connector/src/connectors/shell.js +76 -3
  23. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  24. package/dist/aria-connector/src/self-update.d.ts +2 -1
  25. package/dist/aria-connector/src/self-update.d.ts.map +1 -1
  26. package/dist/aria-connector/src/self-update.js +84 -8
  27. package/dist/aria-connector/src/self-update.js.map +1 -1
  28. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +53 -34
  29. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  30. package/dist/assets/hooks/aria-pre-tool-gate.mjs +185 -76
  31. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +63 -14
  32. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +2 -0
  33. package/dist/assets/hooks/aria-stop-gate.mjs +225 -52
  34. package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
  35. package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
  36. package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
  37. package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
  38. package/dist/assets/opencode-plugins/harness-gate/index.js +24 -2
  39. package/dist/assets/opencode-plugins/harness-stop/index.js +94 -5
  40. package/dist/runtime/auth-middleware.mjs +251 -0
  41. package/dist/runtime/codex-bridge.mjs +644 -0
  42. package/dist/runtime/discipline/CLAUDE.md +12 -0
  43. package/dist/runtime/discipline/doctrine_trigger_map.json +479 -0
  44. package/dist/runtime/doctrine_trigger_map.json +479 -0
  45. package/dist/runtime/fleet-engine.mjs +231 -0
  46. package/dist/runtime/harness-daemon.mjs +433 -0
  47. package/dist/runtime/local-phase.mjs +18 -0
  48. package/dist/runtime/manifest.json +1 -1
  49. package/dist/runtime/metering.mjs +100 -0
  50. package/dist/runtime/onboarding-engine.mjs +89 -0
  51. package/dist/runtime/plugin-engine.mjs +196 -0
  52. package/dist/runtime/sdk/BUNDLED.json +1 -1
  53. package/dist/runtime/sdk/index.d.ts +7 -0
  54. package/dist/runtime/sdk/index.js +120 -14
  55. package/dist/runtime/sdk/index.js.map +1 -1
  56. package/dist/runtime/service.mjs +1464 -67
  57. package/dist/runtime/vendor/aria-gate-runtime/index.d.ts +1 -1
  58. package/dist/runtime/vendor/aria-gate-runtime/index.d.ts.map +1 -1
  59. package/dist/runtime/vendor/aria-gate-runtime/index.js +16 -1
  60. package/dist/runtime/vendor/aria-gate-runtime/index.js.map +1 -1
  61. package/dist/runtime/workflow-engine.mjs +322 -0
  62. package/dist/sdk/BUNDLED.json +1 -1
  63. package/dist/sdk/index.d.ts +7 -0
  64. package/dist/sdk/index.js +120 -14
  65. package/dist/sdk/index.js.map +1 -1
  66. package/hooks/aria-cognition-substrate-binding.mjs +53 -34
  67. package/hooks/aria-harness-via-sdk.mjs +126 -12
  68. package/hooks/aria-pre-tool-gate.mjs +185 -76
  69. package/hooks/aria-preturn-memory-gate.mjs +63 -14
  70. package/hooks/aria-repo-doctrine-gate.mjs +2 -0
  71. package/hooks/aria-stop-gate.mjs +225 -52
  72. package/hooks/lib/canonical-lenses.mjs +6 -5
  73. package/hooks/lib/gate-loop-state.mjs +50 -0
  74. package/hooks/lib/hook-message-window.mjs +121 -0
  75. package/hooks/test-tier-lens-labeling.mjs +26 -58
  76. package/opencode-plugins/harness-gate/index.js +24 -2
  77. package/opencode-plugins/harness-stop/index.js +94 -5
  78. package/package.json +2 -2
  79. package/runtime-src/auth-middleware.mjs +251 -0
  80. package/runtime-src/codex-bridge.mjs +644 -0
  81. package/runtime-src/fleet-engine.mjs +231 -0
  82. package/runtime-src/harness-daemon.mjs +433 -0
  83. package/runtime-src/local-phase.mjs +18 -0
  84. package/runtime-src/metering.mjs +100 -0
  85. package/runtime-src/onboarding-engine.mjs +89 -0
  86. package/runtime-src/plugin-engine.mjs +196 -0
  87. package/runtime-src/service.mjs +1464 -67
  88. package/runtime-src/workflow-engine.mjs +322 -0
  89. package/scripts/bundle-sdk.mjs +5 -0
  90. package/src/connectors/claude-code.ts +98 -20
  91. package/src/connectors/codex.ts +534 -1
  92. package/src/connectors/doctrine-trigger-map.ts +112 -0
  93. package/src/connectors/must-read.ts +113 -0
  94. package/src/connectors/opencode.ts +3 -0
  95. package/src/connectors/runtime.ts +241 -21
  96. package/src/connectors/shell.ts +78 -3
  97. package/src/self-update.ts +89 -8
  98. package/dist/cli-0.2.0.tgz +0 -0
@@ -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;
@@ -65,12 +82,345 @@ const PRINCIPLE_LIMIT = 400;
65
82
  const PATTERN_LIMIT = 400;
66
83
  const RECEIPT_LIMIT = 500;
67
84
  const OWNER_TOKEN_PATH = join(process.env.HOME || '', '.aria', 'owner-token');
85
+ const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
86
+ const VERIFY_BLOCK_RX =
87
+ /<verify>[\s\S]*?target\s*:[\s\S]*?role\s*:[\s\S]*?verified\s*:[\s\S]*?rollback\s*:[\s\S]*?axiom\s*:[\s\S]*?<\/verify>/i;
88
+ const EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
89
+ const MEASURABLE_PREDICATE_RX =
90
+ /\b(?:exit_code|exit|rc|status|state|count|latency|error_rate|healthy|exists|http|rows?|bytes?|sha|heartbeat|predicate)\b\s*(?:[:=]|==|>=|<=|>|<)\s*[^\n]+|\b(?:true|false)\b|\b\d+(?:\.\d+)?%?\b/i;
91
+ const QUALITATIVE_DRIFT_RX = /\b(?:better|improved|should work|more reliable|cleaner|enhanced)\b/i;
92
+ const DECISION_SIGNAL_RX = /(?:should|recommend|propose|suggest|let'?s|go with|i'd|i would|here'?s the plan|i'll|next step|action item|ship it|yes do|let me)/i;
93
+ const TRIVIAL_ACK_RX = /^(?:got it|on it|ok|sure|yes|no|done|ack)\b/i;
94
+ const NON_TRIVIAL_MIN_CHARS = 300;
95
+ const READABLE_LENS_SLOTS = [
96
+ { key: 'truth', aliases: ['truth', 'nur', 'zahir'] },
97
+ { key: 'harm', aliases: ['harm', 'mizan', 'batin'] },
98
+ { key: 'trust', aliases: ['trust', 'hikma', 'hikmah'] },
99
+ { key: 'power', aliases: ['power', 'tafakkur', 'sabab'] },
100
+ { key: 'reflection', aliases: ['reflection', 'tadabbur', 'aqibah'] },
101
+ { key: 'context', aliases: ['context', 'ilham'] },
102
+ { key: 'impact', aliases: ['impact', 'wahi', 'meta'] },
103
+ { key: 'beauty', aliases: ['beauty', 'firasah', 'fitrah'] },
104
+ ];
105
+ const DOCTRINE_TRIGGER_MAP_CANDIDATES = [
106
+ join(__dirname, 'discipline', 'doctrine_trigger_map.json'),
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'),
110
+ join(process.env.HOME || '', '.claude', 'hooks', 'doctrine_trigger_map.json'),
111
+ join(process.env.HOME || '', '.claude', 'projects', '-home-hamzaibrahim1', 'memory', 'doctrine_trigger_map.json'),
112
+ join(process.env.HOME || '', '.codex', 'doctrine_trigger_map.json'),
113
+ join(process.env.HOME || '', '.opencode', 'doctrine_trigger_map.json'),
114
+ ];
115
+ const DOCTRINE_TRIGGER_SYNC_INTERVAL_MS = Number(process.env.ARIA_DOCTRINE_TRIGGER_SYNC_INTERVAL_MS || 5000);
116
+ const TOOL_DEPLOY_PATTERNS = [
117
+ /\b(?:\.\/)?scripts\/deploy-/i,
118
+ /\bkubectl\s+apply\b/i,
119
+ /\bkubectl\s+set\s+image\b/i,
120
+ /\bkubectl\s+rollout\s+restart\b/i,
121
+ /\bkubectl\s+rollout\s+undo\b/i,
122
+ /\bdocker\s+push\b/i,
123
+ /\bdocker\s+build\b.*--push\b/i,
124
+ ];
125
+ const TOOL_DESTRUCTIVE_PATTERNS = [
126
+ /(?:^|[;&|]\s*|\$\(\s*|`\s*)sudo\s+\S/i,
127
+ /systemctl\s+(?:disable|stop|mask|reset-failed|kill)\b/i,
128
+ /\brm\s+-[rRfF]+/i,
129
+ /\bgit\s+push\b.*\b--force\b/i,
130
+ /\bgit\s+reset\s+--hard\b/i,
131
+ /\bgit\s+checkout\s+--\b/i,
132
+ /\b--no-verify\b/i,
133
+ /\b--no-gpg-sign\b/i,
134
+ /\bkill\s+-(?:9|KILL|TERM|HUP|INT)\b/i,
135
+ /\bpkill\b/i,
136
+ /\b(?:DROP|TRUNCATE)\s+(?:TABLE|DATABASE|SCHEMA|INDEX)\b/i,
137
+ /\bkubectl\s+delete\b/i,
138
+ ];
139
+ let doctrineTriggerMapCache = { sourcePath: null, map: { triggers: [] } };
140
+ let doctrineTriggerMapSyncedAt = 0;
68
141
 
69
142
  function json(res, status, payload) {
70
143
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
71
144
  res.end(JSON.stringify(payload, null, 2));
72
145
  }
73
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
+
208
+ function isNonTrivialAssistantTurn(text, toolIntents = []) {
209
+ const body = String(text || '').trim();
210
+ if (toolIntents.length > 0) return true;
211
+ if (!body) return false;
212
+ if (TRIVIAL_ACK_RX.test(body)) return false;
213
+ return body.length >= NON_TRIVIAL_MIN_CHARS || DECISION_SIGNAL_RX.test(body);
214
+ }
215
+
216
+ function extractVisibleCognitionContract(text) {
217
+ const match = String(text || '').match(COGNITION_BLOCK_RX);
218
+ if (!match) {
219
+ return {
220
+ present: false,
221
+ readable: null,
222
+ rawBlock: null,
223
+ firstPrinciple: null,
224
+ labels: [],
225
+ };
226
+ }
227
+
228
+ const inner = match[1];
229
+ const readable = {};
230
+ const labels = [];
231
+ for (const slot of READABLE_LENS_SLOTS) {
232
+ const aliasPattern = slot.aliases.join('|');
233
+ const lensRx = new RegExp(
234
+ `\\b(?:${aliasPattern})\\s*:\\s*([^\\n]*(?:\\n(?!\\s*(?:${READABLE_LENS_SLOTS.flatMap((entry) => entry.aliases).join('|')})\\s*:|<\\/cognition>)[^\\n]*)*)`,
235
+ 'i',
236
+ );
237
+ const lensMatch = inner.match(lensRx);
238
+ if (!lensMatch) continue;
239
+ readable[slot.key] = (lensMatch[1] || '').trim();
240
+ labels.push(slot.key);
241
+ }
242
+
243
+ const firstPrincipleMatch = inner.match(/\bfirst[_\s-]?principle\s*:\s*([^\n][\s\S]*?)(?=\n\s*[a-z_ -]+\s*:|$)/i);
244
+ return {
245
+ present: true,
246
+ readable,
247
+ rawBlock: match[0],
248
+ firstPrinciple: firstPrincipleMatch ? firstPrincipleMatch[1].trim() : null,
249
+ labels,
250
+ };
251
+ }
252
+
253
+ function hasMeasurableExpectedBlock(text) {
254
+ const match = String(text || '').match(EXPECTED_BLOCK_RX);
255
+ if (!match) return false;
256
+ const inner = match[1].trim();
257
+ if (!inner) return false;
258
+ if (QUALITATIVE_DRIFT_RX.test(inner) && !MEASURABLE_PREDICATE_RX.test(inner)) return false;
259
+ return MEASURABLE_PREDICATE_RX.test(inner);
260
+ }
261
+
262
+ function parseToolArgs(rawArgs) {
263
+ if (!rawArgs) return {};
264
+ if (typeof rawArgs === 'object') return rawArgs;
265
+ if (typeof rawArgs !== 'string') return {};
266
+ try {
267
+ return JSON.parse(rawArgs);
268
+ } catch {
269
+ return { raw: rawArgs };
270
+ }
271
+ }
272
+
273
+ function inferToolAction(toolName, args) {
274
+ const lower = String(toolName || '').toLowerCase();
275
+ if (args && typeof args.command === 'string') return 'bash';
276
+ if (args && (typeof args.file_path === 'string' || typeof args.notebook_path === 'string')) return 'edit';
277
+ if (/\b(?:bash|shell|terminal|command)\b/.test(lower)) return 'bash';
278
+ if (/\b(?:edit|write|notebook)\b/.test(lower)) return 'edit';
279
+ if (/\b(?:deploy|rollout|release)\b/.test(lower)) return 'deploy';
280
+ if (/\b(?:delete|destroy|drop|wipe|purge)\b/.test(lower)) return 'delete';
281
+ if (/\b(?:build|compile)\b/.test(lower)) return 'build';
282
+ return 'tool';
283
+ }
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
+
296
+ function summarizeToolTarget(toolName, args) {
297
+ if (typeof args?.command === 'string' && args.command.trim()) return args.command.trim();
298
+ if (typeof args?.file_path === 'string' && args.file_path.trim()) return args.file_path.trim();
299
+ if (typeof args?.notebook_path === 'string' && args.notebook_path.trim()) return args.notebook_path.trim();
300
+ if (typeof args?.path === 'string' && args.path.trim()) return args.path.trim();
301
+ if (typeof args?.target === 'string' && args.target.trim()) return args.target.trim();
302
+ return `${toolName}${Object.keys(args || {}).length ? ` ${JSON.stringify(args).slice(0, 200)}` : ''}`.trim();
303
+ }
304
+
305
+ function extractProviderToolIntents(providerStyle, providerMeta) {
306
+ if (providerStyle === 'anthropic') {
307
+ const content = Array.isArray(providerMeta?.raw?.content) ? providerMeta.raw.content : [];
308
+ return content
309
+ .filter((block) => block?.type === 'tool_use')
310
+ .map((block) => {
311
+ const args = parseToolArgs(block.input || {});
312
+ return {
313
+ provider: 'anthropic',
314
+ id: block.id || null,
315
+ toolName: block.name || 'tool_use',
316
+ args,
317
+ action: inferToolAction(block.name || 'tool_use', args),
318
+ target: summarizeToolTarget(block.name || 'tool_use', args),
319
+ raw: block,
320
+ };
321
+ });
322
+ }
323
+
324
+ const rawMessage = providerMeta?.raw?.choices?.[0]?.message || {};
325
+ const toolCalls = Array.isArray(rawMessage.tool_calls) ? rawMessage.tool_calls : [];
326
+ return toolCalls.map((toolCall) => {
327
+ const name = toolCall?.function?.name || toolCall?.name || 'tool_call';
328
+ const args = parseToolArgs(toolCall?.function?.arguments || toolCall?.arguments || {});
329
+ return {
330
+ provider: 'openai',
331
+ id: toolCall?.id || null,
332
+ toolName: name,
333
+ args,
334
+ action: inferToolAction(name, args),
335
+ target: summarizeToolTarget(name, args),
336
+ raw: toolCall,
337
+ };
338
+ });
339
+ }
340
+
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 = [];
348
+ for (const candidate of DOCTRINE_TRIGGER_MAP_CANDIDATES) {
349
+ const map = readJsonFile(candidate, null);
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 {}
358
+ }
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;
385
+ }
386
+
387
+ function collectDoctrineTriggerHits(text) {
388
+ const triggerMap = loadDoctrineTriggerMap();
389
+ const source = String(text || '');
390
+ const lowerText = source.toLowerCase();
391
+ const hits = [];
392
+ for (const entry of triggerMap.triggers || []) {
393
+ try {
394
+ const rx = new RegExp(entry.trigger, 'ig');
395
+ const matched = [...source.matchAll(rx)][0];
396
+ if (!matched) continue;
397
+ const memoryName = typeof entry.memory === 'string' ? entry.memory.replace(/\.md$/, '') : '';
398
+ const memoryCited = memoryName && lowerText.includes(memoryName.toLowerCase());
399
+ if (memoryCited) continue;
400
+ hits.push({
401
+ trigger: entry.trigger,
402
+ memory: entry.memory || null,
403
+ teaching: entry.teaching || null,
404
+ message: entry.message || null,
405
+ severity: entry.severity || 'block',
406
+ });
407
+ } catch {}
408
+ }
409
+ return hits;
410
+ }
411
+
412
+ function buildToolGateBlockMessage(blockers) {
413
+ const reasons = blockers.map((blocker) => `- ${blocker}`).join('\n');
414
+ return [
415
+ 'Aria runtime blocked the requested tool action.',
416
+ '',
417
+ reasons,
418
+ '',
419
+ 'Re-draft with a readable <cognition> block before the tool request.',
420
+ 'If the action is deploy, destructive, or state-mutating, include <verify> and <expected> blocks as well.',
421
+ ].join('\n');
422
+ }
423
+
74
424
  async function readJson(req) {
75
425
  const chunks = [];
76
426
  for await (const chunk of req) chunks.push(chunk);
@@ -124,6 +474,26 @@ function synthesizeOwnerLease(apiKey, reason) {
124
474
  };
125
475
  }
126
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
+
127
497
  function deriveEncryptionKey(secret) {
128
498
  return scryptSync(secret, 'aria-mounted-runtime', 32);
129
499
  }
@@ -173,10 +543,130 @@ function saveEncryptedLease(lease, secret) {
173
543
  writeFileSync(LEASE_PATH, encryptJson(lease, secret), { mode: 0o600 });
174
544
  }
175
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
+
176
665
  function defaultCognitionState() {
177
666
  return {
178
667
  telemetry: [],
179
668
  decisions: [],
669
+ pendingDecisions: [],
180
670
  evolutionPrinciples: [],
181
671
  aegisPatterns: [],
182
672
  heartbeats: [],
@@ -369,6 +859,7 @@ function summarizeCognitionState(state) {
369
859
  return {
370
860
  telemetryTurns: Array.isArray(state.telemetry) ? state.telemetry.length : 0,
371
861
  decisions: Array.isArray(state.decisions) ? state.decisions.length : 0,
862
+ pendingDecisionUploads: Array.isArray(state.pendingDecisions) ? state.pendingDecisions.length : 0,
372
863
  evolutionPrinciples: Array.isArray(state.evolutionPrinciples) ? state.evolutionPrinciples.length : 0,
373
864
  aegisPatterns: Array.isArray(state.aegisPatterns) ? state.aegisPatterns.length : 0,
374
865
  heartbeats: Array.isArray(state.heartbeats) ? state.heartbeats.length : 0,
@@ -610,6 +1101,15 @@ async function heartbeatUpstream(req, body, client, apiKey) {
610
1101
  },
611
1102
  };
612
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
+ });
613
1113
  } catch (error) {
614
1114
  if (!ownerBypassAllowed) {
615
1115
  throw error;
@@ -666,14 +1166,45 @@ async function ensureLease(req, body, client) {
666
1166
 
667
1167
  const keyHash = hashKey(apiKey);
668
1168
  const cached = leaseCache.get(keyHash) || loadEncryptedLease(apiKey);
1169
+ ensureOfflineBundleSeeded(apiKey, cached);
669
1170
  if (cached && cached.hardStopAt > Date.now()) {
670
1171
  leaseCache.set(keyHash, cached);
671
1172
  if (cached.nextRequiredAt > Date.now()) {
672
1173
  return cached;
673
1174
  }
674
1175
  }
675
-
676
- return heartbeatUpstream(req, body, client, apiKey);
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
+ }
677
1208
  }
678
1209
 
679
1210
  function deriveSessionId(req, body, prefix = 'runtime') {
@@ -732,6 +1263,7 @@ async function pushTelemetryUpstream(client, apiKey, payload) {
732
1263
  }
733
1264
 
734
1265
  async function pushDecisionUpstream(client, apiKey, payload) {
1266
+ const normalizedPayload = normalizeDecisionPayload(payload);
735
1267
  const url = `${client.baseUrl || DEFAULT_HARNESS_URL}/api/decisions/log`;
736
1268
  const response = await fetch(url, {
737
1269
  method: 'POST',
@@ -739,7 +1271,7 @@ async function pushDecisionUpstream(client, apiKey, payload) {
739
1271
  Authorization: `Bearer ${apiKey}`,
740
1272
  'Content-Type': 'application/json',
741
1273
  },
742
- body: JSON.stringify(payload),
1274
+ body: JSON.stringify(normalizedPayload),
743
1275
  });
744
1276
  if (!response.ok) {
745
1277
  const body = await response.text().catch(() => response.statusText);
@@ -748,6 +1280,88 @@ async function pushDecisionUpstream(client, apiKey, payload) {
748
1280
  return response.json().catch(() => ({ logged: true }));
749
1281
  }
750
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
+
751
1365
  function buildMinimalInjection(packet, task, aegisLearnings = null) {
752
1366
  return {
753
1367
  harness: packet,
@@ -805,9 +1419,35 @@ function buildOwnerBypassPacket(message, reason = 'owner-local-bypass') {
805
1419
 
806
1420
  async function loadRuntimePacket(req, body, client, packetRequest, message) {
807
1421
  if (body.packet) return body.packet;
1422
+ const apiKey = resolveApiKey(req, body);
1423
+ ensureOfflineBundleSeeded(apiKey, leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey));
808
1424
  try {
809
- return await client.getHarnessPacket(packetRequest || {});
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;
810
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
+ }
811
1451
  if (!isOwnerBypassRequest(req, body)) {
812
1452
  throw error;
813
1453
  }
@@ -982,6 +1622,9 @@ async function buildDirectTurnContext(req, body, client, options = {}) {
982
1622
 
983
1623
  function openAiResponseEnvelope(body, text, providerMeta, extra = {}) {
984
1624
  const debug = body?.ariaDebug === true;
1625
+ const toolCalls = !extra.blocked && Array.isArray(providerMeta?.raw?.choices?.[0]?.message?.tool_calls)
1626
+ ? providerMeta.raw.choices[0].message.tool_calls
1627
+ : undefined;
985
1628
  return {
986
1629
  id: `chatcmpl_${randomUUID().replace(/-/g, '')}`,
987
1630
  object: 'chat.completion',
@@ -990,10 +1633,11 @@ function openAiResponseEnvelope(body, text, providerMeta, extra = {}) {
990
1633
  choices: [
991
1634
  {
992
1635
  index: 0,
993
- finish_reason: providerMeta.finishReason || 'stop',
1636
+ finish_reason: toolCalls?.length ? (providerMeta.finishReason || 'tool_calls') : (providerMeta.finishReason || 'stop'),
994
1637
  message: {
995
1638
  role: 'assistant',
996
1639
  content: text,
1640
+ ...(toolCalls?.length ? { tool_calls: toolCalls } : {}),
997
1641
  },
998
1642
  },
999
1643
  ],
@@ -1009,13 +1653,32 @@ function openAiResponseEnvelope(body, text, providerMeta, extra = {}) {
1009
1653
  }
1010
1654
 
1011
1655
  function anthropicResponseEnvelope(text, providerMeta, extra = {}, debug = false) {
1656
+ const rawContent = Array.isArray(providerMeta?.raw?.content) ? providerMeta.raw.content : [];
1657
+ const content = !extra.blocked && rawContent.length
1658
+ ? rawContent.map((block) => {
1659
+ if (block?.type === 'text') {
1660
+ return { type: 'text', text: typeof block.text === 'string' ? block.text : text };
1661
+ }
1662
+ if (block?.type === 'tool_use') {
1663
+ return {
1664
+ type: 'tool_use',
1665
+ id: block.id,
1666
+ name: block.name,
1667
+ input: block.input || {},
1668
+ };
1669
+ }
1670
+ return block;
1671
+ })
1672
+ : [{ type: 'text', text }];
1012
1673
  return {
1013
1674
  id: `msg_${randomUUID().replace(/-/g, '')}`,
1014
1675
  type: 'message',
1015
1676
  role: 'assistant',
1016
1677
  model: providerMeta.model,
1017
- stop_reason: providerMeta.finishReason || 'end_turn',
1018
- content: [{ type: 'text', text }],
1678
+ stop_reason: rawContent.some((block) => block?.type === 'tool_use') && !extra.blocked
1679
+ ? (providerMeta.finishReason || 'tool_use')
1680
+ : (providerMeta.finishReason || 'end_turn'),
1681
+ content,
1019
1682
  usage: providerMeta.usage
1020
1683
  ? {
1021
1684
  input_tokens: providerMeta.usage.promptTokens || 0,
@@ -1026,6 +1689,174 @@ function anthropicResponseEnvelope(text, providerMeta, extra = {}, debug = false
1026
1689
  };
1027
1690
  }
1028
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
+
1029
1860
  function toReadableSignal(name) {
1030
1861
  const map = {
1031
1862
  fitrah_gate: 'truth boundary',
@@ -1111,6 +1942,30 @@ function buildReadableAriaEnvelope(extra = {}, debug = false) {
1111
1942
  : [],
1112
1943
  } : null;
1113
1944
 
1945
+ const cognition = extra.cognitionContract ? {
1946
+ visible: Boolean(extra.cognitionContract.present),
1947
+ labels: Array.isArray(extra.cognitionContract.labels) ? extra.cognitionContract.labels : [],
1948
+ readable: extra.cognitionContract.readable || null,
1949
+ first_principle: extra.cognitionContract.firstPrinciple || null,
1950
+ } : null;
1951
+
1952
+ const doctrine = extra.doctrine ? {
1953
+ blocked: Boolean(extra.doctrine.blocked),
1954
+ hits: Array.isArray(extra.doctrine.hits) ? extra.doctrine.hits.slice(0, 3) : [],
1955
+ } : null;
1956
+
1957
+ const tool_gate = extra.toolGate ? {
1958
+ blocked: Boolean(extra.toolGate.blocked),
1959
+ blockers: Array.isArray(extra.toolGate.blockers) ? extra.toolGate.blockers.slice(0, 5) : [],
1960
+ intents: Array.isArray(extra.toolGate.intents)
1961
+ ? extra.toolGate.intents.map((intent) => ({
1962
+ tool: intent.toolName,
1963
+ action: intent.action,
1964
+ target: intent.target,
1965
+ }))
1966
+ : [],
1967
+ } : null;
1968
+
1114
1969
  const envelope = {
1115
1970
  blocked: Boolean(extra.blocked),
1116
1971
  control_plane: 'aria-mounted-runtime',
@@ -1128,6 +1983,9 @@ function buildReadableAriaEnvelope(extra = {}, debug = false) {
1128
1983
  receipts,
1129
1984
  validation,
1130
1985
  layer3,
1986
+ cognition,
1987
+ doctrine,
1988
+ tool_gate,
1131
1989
  };
1132
1990
 
1133
1991
  if (debug) {
@@ -1141,6 +1999,34 @@ function coerceNonEmptyString(value) {
1141
1999
  return typeof value === 'string' && value.trim() ? value.trim() : '';
1142
2000
  }
1143
2001
 
2002
+ function analyzeToolIntentContract(intent, assistantText) {
2003
+ const target = String(intent?.target || '');
2004
+ const action = String(intent?.action || 'tool');
2005
+ const blockers = [];
2006
+ const isDeploy = action === 'deploy' || TOOL_DEPLOY_PATTERNS.some((rx) => rx.test(target));
2007
+ const isDestructive = action === 'delete' || TOOL_DESTRUCTIVE_PATTERNS.some((rx) => rx.test(target));
2008
+ const isMutation = isDeploy || isDestructive || action === 'edit' || action === 'write' || action === 'bash' || action === 'build';
2009
+
2010
+ if (!assistantText.match(COGNITION_BLOCK_RX)) {
2011
+ blockers.push(`${intent.toolName}: missing readable <cognition> block before tool request`);
2012
+ }
2013
+ if (isMutation && !assistantText.match(EXPECTED_BLOCK_RX)) {
2014
+ blockers.push(`${intent.toolName}: missing <expected> block before non-trivial tool request`);
2015
+ } else if (isMutation && !hasMeasurableExpectedBlock(assistantText)) {
2016
+ blockers.push(`${intent.toolName}: <expected> block lacks a measurable predicate`);
2017
+ }
2018
+ if ((isDeploy || isDestructive) && !VERIFY_BLOCK_RX.test(assistantText)) {
2019
+ blockers.push(`${intent.toolName}: missing <verify> block before deploy/destructive tool request`);
2020
+ }
2021
+
2022
+ return {
2023
+ isMutation,
2024
+ isDeploy,
2025
+ isDestructive,
2026
+ blockers,
2027
+ };
2028
+ }
2029
+
1144
2030
  function extractJsonObject(text) {
1145
2031
  const raw = String(text || '').trim();
1146
2032
  if (!raw) {
@@ -1349,6 +2235,10 @@ function computeForgeContractIssues(manifest, forgeResult) {
1349
2235
  }
1350
2236
 
1351
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
+
1352
2242
  const evolutionPrinciples = extractEvolutionPrinciples(turn.packet, [turn.preResult, turn.midResult, turn.postResult]);
1353
2243
  const aegisPatterns = [
1354
2244
  ...extractAegisPatterns(turn.midResult || {}),
@@ -1488,39 +2378,93 @@ async function persistTurnArtifacts(req, body, client, apiKey, turn) {
1488
2378
  await pushTelemetryUpstream(client, apiKey, telemetryPayload);
1489
2379
  } catch {}
1490
2380
 
1491
- if (findVerifiedState(turn.finalText) || /recommend|should|propose|choose/i.test(turn.finalText)) {
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';
1492
2404
  const decisionPayload = {
1493
2405
  decision_type: body?.metadata?.decision_type || 'runtime-turn',
1494
2406
  category: body?.metadata?.decision_category || 'cognition-control-plane',
1495
- context: turn.userMessage.slice(0, 2000) || 'mounted runtime turn',
1496
- decision: turn.finalText.slice(0, 2000),
1497
- reasoning: [
1498
- ...(turn.preResult?.notes || []),
1499
- ...(turn.midResult?.notes || []),
1500
- ...(turn.postResult?.notes || []),
1501
- ].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,
1502
2412
  outcome: 'pending',
1503
- expected_outcome: {
1504
- predicate: 'response survives runtime validation and compounds into central telemetry',
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',
1505
2431
  measurable_type: 'boolean',
1506
2432
  threshold: true,
1507
2433
  },
1508
- source: 'aria-mounted-runtime',
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`,
1509
2448
  model_used: turn.providerMeta.model,
1510
2449
  code_links: body?.metadata?.code_links || null,
1511
2450
  };
2451
+ let decisionResult = null;
2452
+ let decisionError = null;
1512
2453
  try {
1513
- const decisionResult = await pushDecisionUpstream(client, apiKey, decisionPayload);
1514
- mutateCognitionState(apiKey, (state) => ({
1515
- ...state,
1516
- decisions: appendBounded(state.decisions, {
1517
- at: new Date().toISOString(),
1518
- sessionId: turn.sessionId,
1519
- decision: decisionPayload,
1520
- result: decisionResult,
1521
- }, DECISION_LIMIT),
1522
- }));
1523
- } catch {}
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
+ }));
1524
2468
  }
1525
2469
 
1526
2470
  if (body.ariaGarden !== false && turn.userMessage && turn.finalText) {
@@ -1546,44 +2490,94 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1546
2490
  ? await callProviderForAnthropic(body, turn.ariaSystemPrompt)
1547
2491
  : await callProviderForOpenAI(body, turn.ariaSystemPrompt);
1548
2492
 
1549
- let candidateText = providerMeta.text || '';
1550
- let validation;
1551
2493
  try {
1552
- validation = await client.validateOutput(candidateText, turn.sessionId);
1553
- } catch (error) {
1554
- if (!turn.packetBypassed) throw error;
1555
- validation = {
1556
- passed: true,
1557
- severity: 'warn',
1558
- violations: [
1559
- `remote validate unavailable: ${error instanceof Error ? error.message : String(error)}`,
1560
- ],
1561
- gateTriggers: ['owner-local-bypass'],
1562
- };
1563
- }
1564
- if (validation?.severity === 'block' && validation?.rewritten) {
1565
- candidateText = validation.rewritten;
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
+
2507
+ let candidateText = providerMeta.text || '';
2508
+ const toolIntents = extractProviderToolIntents(providerStyle, providerMeta);
2509
+ const requiresReadableCognition = isNonTrivialAssistantTurn(candidateText, toolIntents);
2510
+ const cognitionContract = extractVisibleCognitionContract(candidateText);
2511
+
2512
+ let validation = {
2513
+ passed: true,
2514
+ severity: 'pass',
2515
+ violations: [],
2516
+ gateTriggers: [],
2517
+ };
2518
+ if (candidateText.trim()) {
1566
2519
  try {
1567
2520
  validation = await client.validateOutput(candidateText, turn.sessionId);
1568
2521
  } catch (error) {
1569
- if (!turn.packetBypassed) throw error;
2522
+ if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
1570
2523
  validation = {
1571
2524
  passed: true,
1572
2525
  severity: 'warn',
1573
2526
  violations: [
1574
- `remote validate unavailable after rewrite: ${error instanceof Error ? error.message : String(error)}`,
2527
+ `remote validate unavailable: ${error instanceof Error ? error.message : String(error)}`,
1575
2528
  ],
1576
2529
  gateTriggers: ['owner-local-bypass'],
1577
2530
  };
1578
2531
  }
2532
+ if (validation?.severity === 'block' && validation?.rewritten) {
2533
+ candidateText = validation.rewritten;
2534
+ try {
2535
+ validation = await client.validateOutput(candidateText, turn.sessionId);
2536
+ } catch (error) {
2537
+ if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
2538
+ validation = {
2539
+ passed: true,
2540
+ severity: 'warn',
2541
+ violations: [
2542
+ `remote validate unavailable after rewrite: ${error instanceof Error ? error.message : String(error)}`,
2543
+ ],
2544
+ gateTriggers: ['owner-local-bypass'],
2545
+ };
2546
+ }
2547
+ }
1579
2548
  }
1580
2549
 
1581
2550
  const layer3 = await runLayer3(req, {
1582
- text: candidateText,
2551
+ text: candidateText || (toolIntents.length > 0 ? `<cognition>\n</cognition>` : ''),
1583
2552
  packet: turn.packet,
1584
2553
  fetchPacket: false,
1585
- requireCognitionBlock: body.requireCognitionBlock ?? false,
2554
+ requireCognitionBlock: body.requireCognitionBlock ?? requiresReadableCognition,
1586
2555
  }, client);
2556
+ const doctrineHits = candidateText.trim() ? collectDoctrineTriggerHits(candidateText) : [];
2557
+ const doctrineBlockers = doctrineHits
2558
+ .filter((hit) => String(hit.severity || 'block').toLowerCase() === 'block')
2559
+ .map((hit) => hit.message || hit.teaching || hit.trigger);
2560
+
2561
+ const toolGateBlockers = [];
2562
+ for (const intent of toolIntents) {
2563
+ const contract = analyzeToolIntentContract(intent, candidateText);
2564
+ toolGateBlockers.push(...contract.blockers);
2565
+ try {
2566
+ const runtimeCheck = await client.checkAction(
2567
+ contract.isDeploy ? 'deploy'
2568
+ : contract.isDestructive ? 'delete'
2569
+ : intent.action === 'build' ? 'build'
2570
+ : 'write',
2571
+ intent.target || intent.toolName,
2572
+ );
2573
+ if (!runtimeCheck.allowed) {
2574
+ toolGateBlockers.push(`${intent.toolName}: ${runtimeCheck.reason || 'runtime action gate blocked this tool request'}`);
2575
+ }
2576
+ } catch (error) {
2577
+ if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
2578
+ toolGateBlockers.push(`${intent.toolName}: runtime action gate unavailable during owner-local-bypass`);
2579
+ }
2580
+ }
1587
2581
  const postBundle = evaluateMizanPost(candidateText, {
1588
2582
  hasVerifiedState: findVerifiedState(candidateText),
1589
2583
  layer3Pass: layer3.pass,
@@ -1601,8 +2595,17 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1601
2595
  });
1602
2596
  const postResult = postBundle.result;
1603
2597
 
1604
- const blocked = validation.severity === 'block' || !layer3.pass || postResult.reAuthorSignal;
1605
- const finalText = blocked ? buildPhaseBlockMessage(postResult, 'post') : candidateText;
2598
+ const blocked =
2599
+ validation.severity === 'block' ||
2600
+ !layer3.pass ||
2601
+ postResult.reAuthorSignal ||
2602
+ doctrineBlockers.length > 0 ||
2603
+ toolGateBlockers.length > 0;
2604
+ const finalText = toolGateBlockers.length > 0
2605
+ ? buildToolGateBlockMessage(toolGateBlockers)
2606
+ : blocked
2607
+ ? buildPhaseBlockMessage(postResult, 'post')
2608
+ : candidateText;
1606
2609
  await persistTurnArtifacts(req, body, client, apiKey, {
1607
2610
  ...turn,
1608
2611
  postResult,
@@ -1632,6 +2635,16 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1632
2635
  operatorPlan: turn.operatorPlan,
1633
2636
  validation,
1634
2637
  layer3,
2638
+ cognitionContract,
2639
+ doctrine: {
2640
+ blocked: doctrineBlockers.length > 0,
2641
+ hits: doctrineHits,
2642
+ },
2643
+ toolGate: {
2644
+ blocked: toolGateBlockers.length > 0,
2645
+ blockers: toolGateBlockers,
2646
+ intents: toolIntents,
2647
+ },
1635
2648
  };
1636
2649
  return providerStyle === 'anthropic'
1637
2650
  ? anthropicResponseEnvelope(finalText, providerMeta, extra, body?.ariaDebug === true)
@@ -1910,6 +2923,8 @@ function packetToSubstrateSet(packet) {
1910
2923
  function runtimeManifest() {
1911
2924
  const runtimeMeta = ensureRuntimeMeta();
1912
2925
  const state = sweepAutonomyState(loadAutonomyState());
2926
+ const ownerToken = readOwnerToken();
2927
+ const offlineBundleStatus = ownerToken ? computeOfflineBundleStatus(loadEncryptedOfflineBundle(ownerToken)) : computeOfflineBundleStatus(null);
1913
2928
  return {
1914
2929
  ok: true,
1915
2930
  runtime: 'aria-mounted-runtime',
@@ -1956,6 +2971,8 @@ function runtimeManifest() {
1956
2971
  'POST /forge/synthesize',
1957
2972
  'POST /codebase/state',
1958
2973
  'POST /v1/chat/completions',
2974
+ 'POST /v1/responses',
2975
+ 'POST /responses',
1959
2976
  'POST /v1/messages',
1960
2977
  ],
1961
2978
  mount: {
@@ -1973,9 +2990,13 @@ function runtimeManifest() {
1973
2990
  },
1974
2991
  security: {
1975
2992
  encrypted_local_lease: LEASE_PATH,
2993
+ encrypted_offline_bundle: OFFLINE_BUNDLE_PATH,
1976
2994
  encrypted_cognition_state: COGNITION_STATE_PATH,
1977
2995
  revocation_lock: REVOCATION_LOCK_PATH,
1978
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,
1979
3000
  },
1980
3001
  memory: {
1981
3002
  qdrant_url: DEFAULT_QDRANT_URL,
@@ -2004,12 +3025,27 @@ async function runLayer3(req, body, client) {
2004
3025
  substrate: packetToSubstrateSet(packet),
2005
3026
  requireCognitionBlock: body.requireCognitionBlock ?? false,
2006
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');
2007
3036
 
2008
3037
  return {
2009
- pass: result.pass,
2010
- summary: result.summary,
2011
- failures: result.failures,
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,
2012
3044
  packetTimestamp: packet.timestamp || null,
3045
+ doctrine: {
3046
+ sourcePath: doctrineTriggerMapCache.sourcePath,
3047
+ hits: doctrineHits,
3048
+ },
2013
3049
  };
2014
3050
  }
2015
3051
 
@@ -2348,6 +3384,12 @@ async function handleRoute(req, res) {
2348
3384
  return json(res, 200, response);
2349
3385
  }
2350
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
+
2351
3393
  if (url.pathname === '/v1/messages') {
2352
3394
  const response = await handleProviderProxy(req, body, client, 'anthropic');
2353
3395
  return json(res, 200, response);
@@ -2378,16 +3420,55 @@ async function handleRoute(req, res) {
2378
3420
 
2379
3421
  if (url.pathname === '/decision/log') {
2380
3422
  const apiKey = resolveApiKey(req, body);
2381
- const result = await pushDecisionUpstream(client, apiKey, body);
2382
- mutateCognitionState(apiKey, (state) => ({
2383
- ...state,
2384
- decisions: appendBounded(state.decisions, {
2385
- at: new Date().toISOString(),
2386
- decision: body,
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,
2387
3438
  result,
2388
- }, DECISION_LIMIT),
2389
- }));
2390
- return json(res, 200, { ok: true, result });
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
+ }
2391
3472
  }
2392
3473
 
2393
3474
  if (url.pathname === '/aegis/patterns') {
@@ -2449,12 +3530,12 @@ async function handleRoute(req, res) {
2449
3530
  }
2450
3531
  }
2451
3532
 
2452
- if (url.pathname === '/packet') {
3533
+ if (url.pathname === '/packet' || url.pathname === '/api/harness/codex') {
2453
3534
  const packet = await loadRuntimePacket(req, body, client, body.packetRequest || body, body.message || '');
2454
3535
  return json(res, 200, { ok: true, packet });
2455
3536
  }
2456
3537
 
2457
- if (url.pathname === '/consult') {
3538
+ if (url.pathname === '/consult' || url.pathname === '/api/harness/delegate') {
2458
3539
  const result = await client.consult(body);
2459
3540
  return json(res, 200, { ok: true, ...result });
2460
3541
  }
@@ -2474,7 +3555,7 @@ async function handleRoute(req, res) {
2474
3555
  return json(res, 200, { ok: true, ...result });
2475
3556
  }
2476
3557
 
2477
- if (url.pathname === '/validate-output') {
3558
+ if (url.pathname === '/validate-output' || url.pathname === '/api/harness/validate') {
2478
3559
  if (typeof body.text !== 'string' || typeof body.sessionId !== 'string') {
2479
3560
  throw new Error('validate-output requires text and sessionId');
2480
3561
  }
@@ -2653,6 +3734,313 @@ async function handleRoute(req, res) {
2653
3734
  return json(res, 200, { ok: true, ...result });
2654
3735
  }
2655
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
+
2656
4044
  return json(res, 404, { ok: false, error: `No route for POST ${url.pathname}` });
2657
4045
  } catch (error) {
2658
4046
  return json(res, 500, {
@@ -2678,6 +4066,15 @@ export async function startRuntimeServer(options = {}) {
2678
4066
  });
2679
4067
  });
2680
4068
  });
4069
+ server.on('upgrade', (req, socket) => {
4070
+ try {
4071
+ handleWebSocketUpgrade(req, socket);
4072
+ } catch {
4073
+ try {
4074
+ socket.destroy();
4075
+ } catch {}
4076
+ }
4077
+ });
2681
4078
 
2682
4079
  await new Promise((resolve, reject) => {
2683
4080
  server.once('error', reject);