@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.
Files changed (87) 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/assets/hooks/aria-cognition-substrate-binding.mjs +52 -25
  25. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  26. package/dist/assets/hooks/aria-pre-tool-gate.mjs +185 -76
  27. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +63 -14
  28. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +2 -0
  29. package/dist/assets/hooks/aria-stop-gate.mjs +225 -52
  30. package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
  31. package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
  32. package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
  33. package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
  34. package/dist/assets/opencode-plugins/harness-gate/index.js +23 -1
  35. package/dist/assets/opencode-plugins/harness-stop/index.js +93 -4
  36. package/dist/runtime/auth-middleware.mjs +251 -0
  37. package/dist/runtime/codex-bridge.mjs +644 -0
  38. package/dist/runtime/discipline/CLAUDE.md +12 -0
  39. package/dist/runtime/discipline/doctrine_trigger_map.json +479 -0
  40. package/dist/runtime/doctrine_trigger_map.json +479 -0
  41. package/dist/runtime/fleet-engine.mjs +231 -0
  42. package/dist/runtime/harness-daemon.mjs +433 -0
  43. package/dist/runtime/manifest.json +1 -1
  44. package/dist/runtime/metering.mjs +100 -0
  45. package/dist/runtime/onboarding-engine.mjs +89 -0
  46. package/dist/runtime/plugin-engine.mjs +196 -0
  47. package/dist/runtime/sdk/BUNDLED.json +1 -1
  48. package/dist/runtime/sdk/index.d.ts +7 -0
  49. package/dist/runtime/sdk/index.js +120 -14
  50. package/dist/runtime/sdk/index.js.map +1 -1
  51. package/dist/runtime/service.mjs +1094 -47
  52. package/dist/runtime/workflow-engine.mjs +322 -0
  53. package/dist/sdk/BUNDLED.json +1 -1
  54. package/dist/sdk/index.d.ts +7 -0
  55. package/dist/sdk/index.js +120 -14
  56. package/dist/sdk/index.js.map +1 -1
  57. package/hooks/aria-cognition-substrate-binding.mjs +52 -25
  58. package/hooks/aria-harness-via-sdk.mjs +126 -12
  59. package/hooks/aria-pre-tool-gate.mjs +185 -76
  60. package/hooks/aria-preturn-memory-gate.mjs +63 -14
  61. package/hooks/aria-repo-doctrine-gate.mjs +2 -0
  62. package/hooks/aria-stop-gate.mjs +225 -52
  63. package/hooks/lib/canonical-lenses.mjs +6 -5
  64. package/hooks/lib/gate-loop-state.mjs +50 -0
  65. package/hooks/lib/hook-message-window.mjs +121 -0
  66. package/hooks/test-tier-lens-labeling.mjs +26 -58
  67. package/opencode-plugins/harness-gate/index.js +23 -1
  68. package/opencode-plugins/harness-stop/index.js +93 -4
  69. package/package.json +1 -1
  70. package/runtime-src/auth-middleware.mjs +251 -0
  71. package/runtime-src/codex-bridge.mjs +644 -0
  72. package/runtime-src/fleet-engine.mjs +231 -0
  73. package/runtime-src/harness-daemon.mjs +433 -0
  74. package/runtime-src/metering.mjs +100 -0
  75. package/runtime-src/onboarding-engine.mjs +89 -0
  76. package/runtime-src/plugin-engine.mjs +196 -0
  77. package/runtime-src/service.mjs +1094 -47
  78. package/runtime-src/workflow-engine.mjs +322 -0
  79. package/scripts/bundle-sdk.mjs +5 -0
  80. package/src/connectors/claude-code.ts +98 -20
  81. package/src/connectors/codex.ts +534 -1
  82. package/src/connectors/doctrine-trigger-map.ts +112 -0
  83. package/src/connectors/must-read.ts +113 -0
  84. package/src/connectors/opencode.ts +3 -0
  85. package/src/connectors/runtime.ts +241 -21
  86. package/src/connectors/shell.ts +78 -3
  87. package/dist/cli-0.2.0.tgz +0 -0
@@ -0,0 +1,644 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { delimiter, dirname, resolve } from 'node:path';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const { WebSocketServer, WebSocket } = require('ws');
11
+
12
+ const HOME = homedir();
13
+ const LOG_PATH = `${HOME}/.aria/runtime/logs/codex-bridge.log`;
14
+ const RUNTIME_BASE_URL = (process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319').replace(/\/+$/, '');
15
+ const BRIDGE_PORT = Number(process.env.ARIA_CODEX_BRIDGE_PORT || '4320');
16
+ const DOWNSTREAM_PORT = Number(process.env.ARIA_CODEX_DOWNSTREAM_PORT || '4321');
17
+ const BRIDGE_HOST = process.env.ARIA_CODEX_BRIDGE_HOST || '127.0.0.1';
18
+ const DOWNSTREAM_URL = process.env.ARIA_CODEX_DOWNSTREAM_URL || `ws://${BRIDGE_HOST}:${DOWNSTREAM_PORT}`;
19
+ const DOWNSTREAM_STARTUP_GRACE_MS = Number(process.env.ARIA_CODEX_DOWNSTREAM_STARTUP_GRACE_MS || '5000');
20
+ const BRIDGE_WARNING_METHOD = 'guardianWarning';
21
+
22
+ let downstreamProcess = null;
23
+ let downstreamProcessStarting = null;
24
+
25
+ const turnState = new Map();
26
+
27
+ function resolveRealCodexBin() {
28
+ if (process.env.ARIA_CODEX_REAL_BIN && existsSync(process.env.ARIA_CODEX_REAL_BIN)) {
29
+ return process.env.ARIA_CODEX_REAL_BIN;
30
+ }
31
+
32
+ const wrapperDir = resolve(HOME, '.aria', 'wrappers');
33
+ const sanitizedPath = String(process.env.PATH || '')
34
+ .split(delimiter)
35
+ .filter((entry) => entry && resolve(entry) !== wrapperDir)
36
+ .join(delimiter);
37
+
38
+ try {
39
+ const result = spawnSync('bash', ['-lc', 'which -a codex'], {
40
+ encoding: 'utf8',
41
+ env: {
42
+ ...process.env,
43
+ PATH: sanitizedPath,
44
+ },
45
+ });
46
+ const candidates = String(result.stdout || '')
47
+ .split('\n')
48
+ .map((line) => line.trim())
49
+ .filter(Boolean)
50
+ .filter((candidate) => !candidate.startsWith(`${wrapperDir}/`));
51
+ if (candidates.length > 0 && existsSync(candidates[0])) {
52
+ return candidates[0];
53
+ }
54
+ } catch {}
55
+
56
+ const fallbackCandidates = [
57
+ resolve(HOME, '.npm-global', 'bin', 'codex'),
58
+ resolve(HOME, '.local', 'bin', 'codex'),
59
+ '/usr/local/bin/codex',
60
+ '/usr/bin/codex',
61
+ ];
62
+ return fallbackCandidates.find((candidate) => existsSync(candidate)) || resolve(HOME, '.npm-global', 'bin', 'codex');
63
+ }
64
+
65
+ const CODEx_REAL_BIN = resolveRealCodexBin();
66
+
67
+ function log(message) {
68
+ try {
69
+ if (!existsSync(dirname(LOG_PATH))) mkdirSync(dirname(LOG_PATH), { recursive: true });
70
+ appendFileSync(LOG_PATH, `${new Date().toISOString()} ${message}\n`);
71
+ } catch {}
72
+ }
73
+
74
+ function sleep(ms) {
75
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
76
+ }
77
+
78
+ function readRuntimeToken() {
79
+ const envToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || process.env.OPENAI_API_KEY || process.env.ARIA_MASTER_TOKEN;
80
+ if (envToken) return envToken;
81
+ const ownerTokenPath = `${HOME}/.aria/owner-token`;
82
+ if (existsSync(ownerTokenPath)) {
83
+ try {
84
+ return readFileSync(ownerTokenPath, 'utf8').trim();
85
+ } catch {}
86
+ }
87
+ return '';
88
+ }
89
+
90
+ function runtimeHeaders() {
91
+ const token = readRuntimeToken();
92
+ return {
93
+ 'Content-Type': 'application/json',
94
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
95
+ };
96
+ }
97
+
98
+ async function postRuntime(pathname, body) {
99
+ const response = await fetch(`${RUNTIME_BASE_URL}${pathname}`, {
100
+ method: 'POST',
101
+ headers: runtimeHeaders(),
102
+ body: JSON.stringify(body),
103
+ });
104
+ const text = await response.text();
105
+ let parsed = null;
106
+ try {
107
+ parsed = text ? JSON.parse(text) : null;
108
+ } catch {}
109
+ if (!response.ok) {
110
+ const message = parsed?.error || parsed?.message || `${response.status} ${response.statusText}`;
111
+ throw new Error(`${pathname} failed: ${message}`);
112
+ }
113
+ return parsed;
114
+ }
115
+
116
+ function ensureTurnState(threadId, turnId) {
117
+ const key = `${threadId}:${turnId}`;
118
+ let state = turnState.get(key);
119
+ if (!state) {
120
+ state = {
121
+ threadId,
122
+ turnId,
123
+ userText: '',
124
+ preReceiptId: null,
125
+ agentText: '',
126
+ bufferedAgentNotifications: [],
127
+ firstAgentItemId: null,
128
+ };
129
+ turnState.set(key, state);
130
+ }
131
+ return state;
132
+ }
133
+
134
+ function extractInputText(input) {
135
+ if (!Array.isArray(input)) return '';
136
+ const parts = [];
137
+ for (const item of input) {
138
+ if (!item || typeof item !== 'object') continue;
139
+ if (typeof item.text === 'string' && item.text.trim()) {
140
+ parts.push(item.text.trim());
141
+ continue;
142
+ }
143
+ if (typeof item.message === 'string' && item.message.trim()) {
144
+ parts.push(item.message.trim());
145
+ continue;
146
+ }
147
+ if (Array.isArray(item.content)) {
148
+ for (const contentItem of item.content) {
149
+ if (contentItem && typeof contentItem.text === 'string' && contentItem.text.trim()) {
150
+ parts.push(contentItem.text.trim());
151
+ }
152
+ }
153
+ }
154
+ }
155
+ return parts.join('\n\n').trim();
156
+ }
157
+
158
+ function deriveActionFromCommand(command = '') {
159
+ const text = String(command || '').trim();
160
+ if (!text) return 'write';
161
+ if (/\bkubectl\s+(apply|set\s+image|rollout\s+(restart|undo)|create|replace|delete)\b|\bdocker\s+(push|build\b.*--push)|\bdeploy-service\.sh\b/i.test(text)) {
162
+ return 'deploy';
163
+ }
164
+ if (/\brm\b|\bgit\s+reset\s+--hard\b|\bgit\s+checkout\s+--\b|\bDROP\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i.test(text)) {
165
+ return 'delete';
166
+ }
167
+ if (/\b(?:npm|pnpm|yarn)\s+run\s+(?:build|typecheck|test)\b|\btsc\b|\bmake\b|\bcargo\s+(?:build|test)\b/i.test(text)) {
168
+ return 'build';
169
+ }
170
+ return 'write';
171
+ }
172
+
173
+ function summarizeRuntimeFailures(result) {
174
+ const failures = Array.isArray(result?.layer3?.failures) ? result.layer3.failures : [];
175
+ const validationViolations = Array.isArray(result?.validation?.violations) ? result.validation.violations : [];
176
+ const messages = [
177
+ ...validationViolations,
178
+ ...failures.map((failure) => failure?.detail || failure?.message || JSON.stringify(failure)),
179
+ ].filter(Boolean);
180
+ return messages.slice(0, 6).join(' | ');
181
+ }
182
+
183
+ async function validateTurnText(threadId, turnId) {
184
+ const state = ensureTurnState(threadId, turnId);
185
+ const text = String(state.agentText || '').trim();
186
+ if (!text) {
187
+ return { ok: false, reason: 'No assistant text exists for this turn yet. Codex must emit readable cognition before action.' };
188
+ }
189
+ const result = await postRuntime('/validate-output', {
190
+ text,
191
+ sessionId: `codex:${threadId}:${turnId}`,
192
+ runLayer3: true,
193
+ requireCognitionBlock: true,
194
+ packetRequest: {
195
+ sessionId: `codex:${threadId}`,
196
+ platform: 'codex',
197
+ message: state.userText || text,
198
+ stage: 'codex-turn',
199
+ actor: 'codex-bridge',
200
+ system: 'codex-bridge',
201
+ },
202
+ });
203
+ const pass = result?.pass === true && result?.validation?.passed !== false;
204
+ return pass
205
+ ? { ok: true, result }
206
+ : {
207
+ ok: false,
208
+ reason: summarizeRuntimeFailures(result) || 'Runtime validation failed for this turn before action/output release.',
209
+ result,
210
+ };
211
+ }
212
+
213
+ async function checkActionAgainstRuntime(action, target, threadId, turnId) {
214
+ const result = await postRuntime('/check-action', {
215
+ action,
216
+ target,
217
+ sessionId: `codex:${threadId}:${turnId}`,
218
+ });
219
+ if (result?.allowed === false) {
220
+ return {
221
+ ok: false,
222
+ reason: result.reason || `Runtime denied ${action} on ${target}`,
223
+ result,
224
+ };
225
+ }
226
+ return { ok: true, result };
227
+ }
228
+
229
+ async function recordMizanPre(threadId, turnId) {
230
+ const state = ensureTurnState(threadId, turnId);
231
+ try {
232
+ const result = await postRuntime('/mizan/pre', {
233
+ sessionId: `codex:${threadId}:${turnId}`,
234
+ packetRequest: {
235
+ sessionId: `codex:${threadId}`,
236
+ platform: 'codex',
237
+ message: state.userText || 'codex turn start',
238
+ stage: 'codex-pre',
239
+ actor: 'codex-bridge',
240
+ system: 'codex-bridge',
241
+ },
242
+ context: {
243
+ sessionId: `codex:${threadId}:${turnId}`,
244
+ surface: 'codex-bridge',
245
+ platform: 'codex',
246
+ userText: state.userText,
247
+ },
248
+ });
249
+ state.preReceiptId = result?.receipt?.receiptId || null;
250
+ } catch (error) {
251
+ log(`warn mizan/pre thread=${threadId} turn=${turnId} error="${error instanceof Error ? error.message : String(error)}"`);
252
+ }
253
+ }
254
+
255
+ async function recordMizanPost(threadId, turnId, pass, summary) {
256
+ const state = ensureTurnState(threadId, turnId);
257
+ try {
258
+ await postRuntime('/mizan/post', {
259
+ sessionId: `codex:${threadId}:${turnId}`,
260
+ parentReceiptId: state.preReceiptId,
261
+ text: state.agentText || summary,
262
+ evidence: {
263
+ validated_output: pass,
264
+ bridge: 'codex',
265
+ },
266
+ context: {
267
+ sessionId: `codex:${threadId}:${turnId}`,
268
+ surface: 'codex-bridge',
269
+ platform: 'codex',
270
+ userText: state.userText,
271
+ summary,
272
+ },
273
+ });
274
+ } catch (error) {
275
+ log(`warn mizan/post thread=${threadId} turn=${turnId} error="${error instanceof Error ? error.message : String(error)}"`);
276
+ }
277
+ try {
278
+ await postRuntime('/decision/log', {
279
+ session_id: `codex:${threadId}:${turnId}`,
280
+ decision_type: 'codex_turn',
281
+ decision_category: 'runtime-control-plane',
282
+ summary,
283
+ outcome: pass ? 'validated' : 'blocked',
284
+ surface: 'codex-bridge',
285
+ details: {
286
+ threadId,
287
+ turnId,
288
+ },
289
+ });
290
+ } catch (error) {
291
+ log(`warn decision/log thread=${threadId} turn=${turnId} error="${error instanceof Error ? error.message : String(error)}"`);
292
+ }
293
+ }
294
+
295
+ function makeGuardianWarning(threadId, message) {
296
+ return {
297
+ method: BRIDGE_WARNING_METHOD,
298
+ params: {
299
+ threadId,
300
+ message,
301
+ },
302
+ };
303
+ }
304
+
305
+ async function handleApprovalRequest(upstream, downstream, message) {
306
+ const { id, method, params = {} } = message;
307
+ const threadId = params.threadId || params.conversationId || 'unknown-thread';
308
+ const turnId = params.turnId || 'legacy-turn';
309
+ const validation = await validateTurnText(threadId, turnId);
310
+
311
+ if (!validation.ok) {
312
+ const reason = `Aria runtime blocked action before tool approval: ${validation.reason}`;
313
+ upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
314
+ const declineResult =
315
+ method === 'item/commandExecution/requestApproval'
316
+ ? { decision: 'decline' }
317
+ : method === 'item/fileChange/requestApproval'
318
+ ? { decision: 'decline' }
319
+ : method === 'item/permissions/requestApproval'
320
+ ? { permissions: { fileSystem: { entries: [] }, network: { enabled: false } }, scope: 'turn', strictAutoReview: true }
321
+ : method === 'execCommandApproval'
322
+ ? { decision: 'denied' }
323
+ : { decision: 'denied' };
324
+ downstream.send(JSON.stringify({ id, result: declineResult }));
325
+ log(`deny approval method=${method} thread=${threadId} turn=${turnId} reason="${validation.reason}"`);
326
+ return true;
327
+ }
328
+
329
+ if (method === 'item/commandExecution/requestApproval' || method === 'execCommandApproval') {
330
+ const command = method === 'item/commandExecution/requestApproval'
331
+ ? params.command || ''
332
+ : Array.isArray(params.command) ? params.command.join(' ') : '';
333
+ const action = deriveActionFromCommand(command);
334
+ const target = JSON.stringify({
335
+ command,
336
+ cwd: params.cwd || null,
337
+ commandActions: params.commandActions || params.parsedCmd || null,
338
+ }).slice(0, 1500);
339
+ const actionCheck = await checkActionAgainstRuntime(action, target, threadId, turnId);
340
+ if (!actionCheck.ok) {
341
+ const reason = `Aria runtime denied ${action}: ${actionCheck.reason}`;
342
+ upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
343
+ downstream.send(JSON.stringify({
344
+ id,
345
+ result: method === 'item/commandExecution/requestApproval'
346
+ ? { decision: 'decline' }
347
+ : { decision: 'denied' },
348
+ }));
349
+ log(`deny command method=${method} action=${action} thread=${threadId} turn=${turnId} reason="${actionCheck.reason}"`);
350
+ return true;
351
+ }
352
+ downstream.send(JSON.stringify({
353
+ id,
354
+ result: method === 'item/commandExecution/requestApproval'
355
+ ? { decision: 'accept' }
356
+ : { decision: 'approved' },
357
+ }));
358
+ log(`allow command method=${method} action=${action} thread=${threadId} turn=${turnId}`);
359
+ return true;
360
+ }
361
+
362
+ if (method === 'item/fileChange/requestApproval' || method === 'applyPatchApproval') {
363
+ const target = params.grantRoot || params.reason || params.itemId || 'file-change';
364
+ const actionCheck = await checkActionAgainstRuntime('write', String(target), threadId, turnId);
365
+ if (!actionCheck.ok) {
366
+ const reason = `Aria runtime denied file change: ${actionCheck.reason}`;
367
+ upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
368
+ downstream.send(JSON.stringify({
369
+ id,
370
+ result: method === 'item/fileChange/requestApproval'
371
+ ? { decision: 'decline' }
372
+ : { decision: 'denied' },
373
+ }));
374
+ log(`deny file-change method=${method} thread=${threadId} turn=${turnId} reason="${actionCheck.reason}"`);
375
+ return true;
376
+ }
377
+ downstream.send(JSON.stringify({
378
+ id,
379
+ result: method === 'item/fileChange/requestApproval'
380
+ ? { decision: 'accept' }
381
+ : { decision: 'approved' },
382
+ }));
383
+ log(`allow file-change method=${method} thread=${threadId} turn=${turnId}`);
384
+ return true;
385
+ }
386
+
387
+ if (method === 'item/permissions/requestApproval') {
388
+ const target = JSON.stringify({
389
+ cwd: params.cwd || null,
390
+ permissions: params.permissions || null,
391
+ reason: params.reason || null,
392
+ }).slice(0, 1500);
393
+ const actionCheck = await checkActionAgainstRuntime('write', target, threadId, turnId);
394
+ if (!actionCheck.ok) {
395
+ const reason = `Aria runtime denied permissions request: ${actionCheck.reason}`;
396
+ upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
397
+ downstream.send(JSON.stringify({
398
+ id,
399
+ result: {
400
+ permissions: {
401
+ fileSystem: {
402
+ entries: [],
403
+ },
404
+ network: {
405
+ enabled: false,
406
+ },
407
+ },
408
+ scope: 'turn',
409
+ strictAutoReview: true,
410
+ },
411
+ }));
412
+ log(`deny permissions thread=${threadId} turn=${turnId} reason="${actionCheck.reason}"`);
413
+ return true;
414
+ }
415
+ }
416
+
417
+ return false;
418
+ }
419
+
420
+ async function releaseTurnOutput(upstream, notification) {
421
+ const threadId = notification?.params?.threadId;
422
+ const turnId = notification?.params?.turn?.id || notification?.params?.turnId;
423
+ if (!threadId || !turnId) {
424
+ upstream.send(JSON.stringify(notification));
425
+ return;
426
+ }
427
+
428
+ const state = ensureTurnState(threadId, turnId);
429
+ if (state.bufferedAgentNotifications.length === 0) {
430
+ upstream.send(JSON.stringify(notification));
431
+ turnState.delete(`${threadId}:${turnId}`);
432
+ return;
433
+ }
434
+
435
+ const validation = await validateTurnText(threadId, turnId);
436
+ if (validation.ok) {
437
+ for (const buffered of state.bufferedAgentNotifications) {
438
+ upstream.send(JSON.stringify(buffered));
439
+ }
440
+ await recordMizanPost(threadId, turnId, true, 'codex turn validated and released');
441
+ log(`release turn thread=${threadId} turn=${turnId} chars=${state.agentText.length}`);
442
+ } else {
443
+ const refusalText = `Aria runtime blocked final output for this Codex turn.\n\n${validation.reason}`;
444
+ if (state.firstAgentItemId) {
445
+ upstream.send(JSON.stringify({
446
+ method: 'item/agentMessage/delta',
447
+ params: {
448
+ threadId,
449
+ turnId,
450
+ itemId: state.firstAgentItemId,
451
+ delta: refusalText,
452
+ },
453
+ }));
454
+ }
455
+ upstream.send(JSON.stringify(makeGuardianWarning(threadId, refusalText)));
456
+ await recordMizanPost(threadId, turnId, false, validation.reason);
457
+ log(`block turn thread=${threadId} turn=${turnId} reason="${validation.reason}"`);
458
+ }
459
+
460
+ upstream.send(JSON.stringify(notification));
461
+ turnState.delete(`${threadId}:${turnId}`);
462
+ }
463
+
464
+ async function ensureDownstreamReady() {
465
+ if (downstreamProcess && !downstreamProcess.killed) return;
466
+ if (downstreamProcessStarting) {
467
+ await downstreamProcessStarting;
468
+ return;
469
+ }
470
+
471
+ downstreamProcessStarting = (async () => {
472
+ log(`start downstream codex app-server bin=${CODEx_REAL_BIN} port=${DOWNSTREAM_PORT}`);
473
+ const env = {
474
+ ...process.env,
475
+ PATH: (process.env.PATH || '')
476
+ .split(':')
477
+ .filter((entry) => entry && entry !== `${HOME}/.aria/wrappers`)
478
+ .join(':'),
479
+ };
480
+ delete env.ARIA_CODEX_BRIDGE_PORT;
481
+ delete env.ARIA_CODEX_DOWNSTREAM_PORT;
482
+ delete env.ARIA_CODEX_DOWNSTREAM_URL;
483
+ delete env.ARIA_CODEX_REAL_BIN;
484
+
485
+ downstreamProcess = spawn(CODEx_REAL_BIN, ['app-server', '--listen', `ws://${BRIDGE_HOST}:${DOWNSTREAM_PORT}`], {
486
+ env,
487
+ stdio: ['ignore', 'pipe', 'pipe'],
488
+ });
489
+ downstreamProcess.stdout.on('data', (chunk) => log(`[downstream stdout] ${String(chunk).trimEnd()}`));
490
+ downstreamProcess.stderr.on('data', (chunk) => log(`[downstream stderr] ${String(chunk).trimEnd()}`));
491
+ downstreamProcess.on('exit', (code, signal) => {
492
+ log(`downstream exit code=${code} signal=${signal || ''}`);
493
+ downstreamProcess = null;
494
+ });
495
+
496
+ const startedAt = Date.now();
497
+ while (Date.now() - startedAt < DOWNSTREAM_STARTUP_GRACE_MS) {
498
+ try {
499
+ await new Promise((resolvePromise, rejectPromise) => {
500
+ const socket = new WebSocket(DOWNSTREAM_URL);
501
+ socket.once('open', () => {
502
+ socket.terminate();
503
+ resolvePromise();
504
+ });
505
+ socket.once('error', rejectPromise);
506
+ });
507
+ return;
508
+ } catch {
509
+ await sleep(100);
510
+ }
511
+ }
512
+ throw new Error(`downstream codex app-server not reachable at ${DOWNSTREAM_URL}`);
513
+ })();
514
+
515
+ try {
516
+ await downstreamProcessStarting;
517
+ } finally {
518
+ downstreamProcessStarting = null;
519
+ }
520
+ }
521
+
522
+ function maybePrimeTurnState(message) {
523
+ if (!message || typeof message !== 'object') return;
524
+ const method = message.method;
525
+ const params = message.params || {};
526
+ if (method === 'turn/start' || method === 'turn/steer') {
527
+ const threadId = params.threadId;
528
+ if (!threadId) return;
529
+ const userText = extractInputText(params.input);
530
+ const state = ensureTurnState(threadId, `${Date.now()}`);
531
+ state.userText = userText;
532
+ }
533
+ }
534
+
535
+ async function startBridge() {
536
+ await ensureDownstreamReady();
537
+
538
+ const wss = new WebSocketServer({
539
+ host: BRIDGE_HOST,
540
+ port: BRIDGE_PORT,
541
+ perMessageDeflate: false,
542
+ });
543
+
544
+ wss.on('connection', async (upstream) => {
545
+ await ensureDownstreamReady();
546
+ const downstream = new WebSocket(DOWNSTREAM_URL);
547
+
548
+ upstream.once('close', () => {
549
+ try { downstream.close(); } catch {}
550
+ });
551
+ downstream.once('close', () => {
552
+ try { upstream.close(); } catch {}
553
+ });
554
+
555
+ upstream.on('message', async (data) => {
556
+ const raw = typeof data === 'string' ? data : data.toString();
557
+ let message;
558
+ try {
559
+ message = JSON.parse(raw);
560
+ } catch {
561
+ downstream.send(raw);
562
+ return;
563
+ }
564
+
565
+ if (message?.method === 'turn/start' && message?.params?.threadId) {
566
+ const state = ensureTurnState(message.params.threadId, String(Date.now()));
567
+ state.userText = extractInputText(message.params.input);
568
+ if (!message.params.approvalPolicy) {
569
+ message.params.approvalPolicy = 'untrusted';
570
+ }
571
+ void recordMizanPre(message.params.threadId, state.turnId);
572
+ }
573
+
574
+ downstream.send(JSON.stringify(message));
575
+ });
576
+
577
+ downstream.on('message', async (data) => {
578
+ const raw = typeof data === 'string' ? data : data.toString();
579
+ let message;
580
+ try {
581
+ message = JSON.parse(raw);
582
+ } catch {
583
+ upstream.send(raw);
584
+ return;
585
+ }
586
+
587
+ if (message?.id != null && typeof message?.method === 'string') {
588
+ const intercepted = await handleApprovalRequest(upstream, downstream, message).catch((error) => {
589
+ const threadId = message?.params?.threadId || 'unknown-thread';
590
+ const reason = `Aria Codex bridge approval hook failed: ${error instanceof Error ? error.message : String(error)}`;
591
+ upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
592
+ log(`error approval method=${message.method} reason="${reason}"`);
593
+ return false;
594
+ });
595
+ if (intercepted) return;
596
+ }
597
+
598
+ if (message?.method === 'item/agentMessage/delta') {
599
+ const { threadId, turnId, itemId, delta } = message.params || {};
600
+ if (threadId && turnId && typeof delta === 'string') {
601
+ const state = ensureTurnState(threadId, turnId);
602
+ state.agentText += delta;
603
+ state.bufferedAgentNotifications.push(message);
604
+ if (!state.firstAgentItemId && itemId) state.firstAgentItemId = itemId;
605
+ return;
606
+ }
607
+ }
608
+
609
+ if (message?.method === 'turn/completed') {
610
+ await releaseTurnOutput(upstream, message);
611
+ return;
612
+ }
613
+
614
+ if (message?.method === 'turn/started') {
615
+ const threadId = message?.params?.threadId;
616
+ const turnId = message?.params?.turn?.id || message?.params?.turnId;
617
+ if (threadId && turnId) {
618
+ const state = ensureTurnState(threadId, turnId);
619
+ if (!state.userText) {
620
+ state.userText = message?.params?.turn?.input?.map?.((entry) => entry?.text).filter(Boolean).join('\n\n') || '';
621
+ }
622
+ void recordMizanPre(threadId, turnId);
623
+ }
624
+ }
625
+
626
+ upstream.send(JSON.stringify(message));
627
+ });
628
+
629
+ downstream.on('error', (error) => {
630
+ log(`downstream socket error ${error.message}`);
631
+ try {
632
+ upstream.send(JSON.stringify(makeGuardianWarning('codex-bridge', `Codex downstream socket error: ${error.message}`)));
633
+ } catch {}
634
+ });
635
+ upstream.on('error', (error) => log(`upstream socket error ${error.message}`));
636
+ });
637
+
638
+ log(`bridge listening ws://${BRIDGE_HOST}:${BRIDGE_PORT} downstream=${DOWNSTREAM_URL}`);
639
+ }
640
+
641
+ startBridge().catch((error) => {
642
+ log(`fatal ${error instanceof Error ? error.stack || error.message : String(error)}`);
643
+ process.exit(1);
644
+ });