@aria_asi/cli 0.2.32 → 0.2.34

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 (93) hide show
  1. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +8 -1
  2. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -1
  3. package/dist/aria-connector/src/connectors/codebase-awareness.js +126 -71
  4. package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  6. package/dist/aria-connector/src/connectors/codex.js +98 -0
  7. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  8. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
  9. package/dist/aria-connector/src/setup-wizard.js +91 -24
  10. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  11. package/dist/assets/hooks/aria-harness-via-sdk.mjs +26 -8
  12. package/dist/assets/hooks/aria-pre-tool-gate.mjs +60 -1
  13. package/dist/assets/hooks/aria-stop-gate.mjs +69 -3
  14. package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
  15. package/dist/assets/hooks/lib/domain-output-quality.mjs +103 -0
  16. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -0
  17. package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
  18. package/dist/assets/opencode-plugins/harness-gate/index.js +114 -10
  19. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
  20. package/dist/assets/opencode-plugins/harness-outcome/index.js +39 -0
  21. package/dist/assets/opencode-plugins/harness-stop/index.js +234 -139
  22. package/dist/assets/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  23. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
  24. package/dist/runtime/codex-bridge.mjs +71 -8
  25. package/dist/runtime/discipline/CLAUDE.md +2 -2
  26. package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
  27. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
  28. package/dist/runtime/doctrine_trigger_map.json +43 -0
  29. package/dist/runtime/harness-daemon.mjs +50 -2
  30. package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
  31. package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
  32. package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
  33. package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
  34. package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
  35. package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
  36. package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
  37. package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
  38. package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
  39. package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
  40. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
  41. package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
  42. package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
  43. package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
  44. package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
  45. package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
  46. package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
  47. package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
  48. package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
  49. package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
  50. package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
  51. package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
  52. package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
  53. package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
  54. package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
  55. package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
  56. package/dist/runtime/manifest.json +2 -2
  57. package/dist/runtime/sdk/BUNDLED.json +2 -2
  58. package/dist/runtime/sdk/index.d.ts +48 -0
  59. package/dist/runtime/sdk/index.js +140 -1
  60. package/dist/runtime/sdk/index.js.map +1 -1
  61. package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
  62. package/dist/runtime/sdk/runWithGovernance.js +54 -0
  63. package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
  64. package/dist/runtime/service.mjs +339 -10
  65. package/dist/sdk/BUNDLED.json +2 -2
  66. package/dist/sdk/index.d.ts +48 -0
  67. package/dist/sdk/index.js +140 -1
  68. package/dist/sdk/index.js.map +1 -1
  69. package/dist/sdk/runWithGovernance.d.ts +16 -0
  70. package/dist/sdk/runWithGovernance.js +54 -0
  71. package/dist/sdk/runWithGovernance.js.map +1 -0
  72. package/hooks/aria-harness-via-sdk.mjs +26 -8
  73. package/hooks/aria-pre-tool-gate.mjs +60 -1
  74. package/hooks/aria-stop-gate.mjs +69 -3
  75. package/hooks/doctrine_trigger_map.json +43 -0
  76. package/hooks/lib/domain-output-quality.mjs +103 -0
  77. package/hooks/lib/skill-autoload-gate.mjs +14 -0
  78. package/opencode-plugins/harness-context/index.js +1 -1
  79. package/opencode-plugins/harness-gate/index.js +114 -10
  80. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
  81. package/opencode-plugins/harness-outcome/index.js +39 -0
  82. package/opencode-plugins/harness-stop/index.js +234 -139
  83. package/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  84. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
  85. package/package.json +12 -5
  86. package/runtime-src/codex-bridge.mjs +71 -8
  87. package/runtime-src/harness-daemon.mjs +50 -2
  88. package/runtime-src/service.mjs +339 -10
  89. package/scripts/bundle-sdk.mjs +2 -0
  90. package/scripts/self-test-harness-gates.mjs +79 -0
  91. package/src/connectors/codebase-awareness.ts +141 -77
  92. package/src/connectors/codex.ts +98 -0
  93. package/src/setup-wizard.ts +105 -25
@@ -0,0 +1,14 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ const RECEIPT_ROOT = process.env.ARIA_SKILL_RECEIPT_DIR || join(tmpdir(), 'aria-skill-receipts');
5
+ const ALIASES = new Map([['deploy', 'aria-harness-deploy'], ['output', 'aria-harness-output-discipline'], ['repo', 'aria-repo-doctrine'], ['forge', 'aria-forge-guardrails']]);
6
+ const RX = { deploy: /deploy-service\.sh|kubectl\s+(?:apply|set|rollout|delete|scale)|helm\s+upgrade|terraform\s+apply|docker\s+push/i, mutationTool: /^(?:edit|write|notebookedit|patch|apply_patch)$/i, mutation: /apply_patch|write file|edit file|modify|delete file|migration|handler|route|runtime|hook|plugin|\btest\b|smoke script/i, strip: /remove|delete|strip|drop|omit|disable|bypass|skip|stub|mock|fake|placeholder|temporary|quick scaffold|band-aid/i, readiness: /production-ready|ready for production|works in general|general readiness|client packages?|npm packages?|SDKs?|runtimes?|harnesses?|release-ready|ship-ready/i, narrow: /single flow|one flow|narrow e2e|covered flow|specific path|widget flow/i, completion: /done|complete|completed|ready|verified|fixed|shipped|implemented|production-ready/i, badProof: /but|except|caveat|remaining|not yet|still|separate|later|blocked|skipped|unresolved|follow-up|failed|failing|error|red|not run|could not verify|untested|no proof|missing proof|without proof/i, advisory: /non-blocking|warning only|warn only|advisory|fall through|falls through|fail open|soft fail|logged and continue|quality gate warning/i, success: /(?:verified|passed|success|successful|green|ok)\s*[:=\-].{0,120}(?:npm|node|playwright|jest|vitest|build|test|lint|typecheck|curl|kubectl|self-test|e2e|probe|smoke)|(?:npm|node|playwright|jest|vitest|build|test|lint|typecheck|curl|kubectl).{0,120}(?:passed|success|successful|green|exit\s*0)/i, resubmit: /re-?submission|resubmit/i, rewrite: /re-?write|rewrite|fix/i, retest: /re-?test|retest|rerun/i, aria: /ARIA console|Aria console|\/chat|aria-pipeline-mcp|aria_chat|escalat(?:e|ion).{0,80}ARIA/i };
7
+ function normalizeSkillName(skill) { return ALIASES.get(String(skill || '').trim()) || String(skill || '').trim(); }
8
+ function sessionDir(sessionId) { return join(RECEIPT_ROOT, encodeURIComponent(String(sessionId || 'unknown'))); }
9
+ function readReceiptSkills(sessionId) { const dir = sessionDir(sessionId); if (!existsSync(dir)) return new Set(); const skills = new Set(); for (const name of readdirSync(dir)) { if (!name.endsWith('.json')) continue; try { const receipt = JSON.parse(readFileSync(join(dir, name), 'utf8')); if (receipt?.skill) skills.add(normalizeSkillName(receipt.skill)); } catch {} } return skills; }
10
+ function readInlineSkills(text) { const skills = new Set(); const value = String(text || ''); for (const match of value.matchAll(/<skill_content\s+name=["']([^"']+)["']/gi)) skills.add(normalizeSkillName(match[1])); return skills; }
11
+ export function recordSkillLoaded({ sessionId, skill, surface = 'unknown', metadata = {} } = {}) { const normalized = normalizeSkillName(skill); if (!normalized) throw new Error('recordSkillLoaded requires a skill name'); const dir = sessionDir(sessionId); mkdirSync(dir, { recursive: true }); const receipt = { skill: normalized, surface, metadata, recordedAt: new Date().toISOString() }; writeFileSync(join(dir, `${encodeURIComponent(normalized)}.json`), `${JSON.stringify(receipt, null, 2)}\n`); return receipt; }
12
+ export function classifyRequiredSkills({ text = '', action = '', toolName = '', filePath = '', isDeploy = false, isMutation = false, isOutputCloseout = false } = {}) { const combined = [text, action, toolName, filePath].filter(Boolean).join('\n'); const required = new Set(); const reasons = []; const recoveryMissing = []; if (isDeploy || RX.deploy.test(combined)) { required.add('aria-harness-deploy'); required.add('aria-forge-guardrails'); reasons.push('deploy/shared-infrastructure action requires fail-closed deploy and forge guardrails'); } if (isMutation || RX.mutationTool.test(toolName)) { required.add('aria-repo-doctrine'); reasons.push('repository/runtime mutation requires repo doctrine'); } if (RX.strip.test(combined)) { required.add('aria-harness-no-stripping'); reasons.push('strip/remove/bypass language requires no-stripping gate'); } if (isOutputCloseout && RX.completion.test(combined)) { required.add('aria-harness-output-discipline'); reasons.push('owner-facing completion/readiness claim requires output discipline'); if (!RX.success.test(combined)) recoveryMissing.push('successful proof from a concrete command/probe'); } if (RX.readiness.test(combined)) { required.add('architecture-decision'); required.add('testing-strategy'); required.add('aria-forge-guardrails'); required.add('aria-harness-output-discipline'); reasons.push('broad production/package/SDK/runtime readiness claim requires architecture, testing, and forge guardrails'); } if (RX.readiness.test(combined) && RX.narrow.test(combined)) { required.add('testing-strategy'); required.add('aria-forge-guardrails'); reasons.push('narrow e2e proof cannot support broad readiness claim without readiness-matrix discipline'); } if (RX.completion.test(combined) && RX.badProof.test(combined)) { required.add('aria-harness-output-discipline'); required.add('aria-forge-guardrails'); reasons.push('completion claim with unresolved or failed proof requires recovery cycle'); if (!RX.resubmit.test(combined)) recoveryMissing.push('re-submission'); if (!RX.rewrite.test(combined)) recoveryMissing.push('re-write'); if (!RX.retest.test(combined)) recoveryMissing.push('re-test'); if (!RX.aria.test(combined)) recoveryMissing.push('ARIA console escalation'); } if (RX.advisory.test(combined)) { required.add('aria-forge-guardrails'); required.add('aria-harness-output-discipline'); reasons.push('advisory/fail-open gate language requires fail-closed hardening discipline'); } return { requiredSkills: [...required].sort(), reasons, recoveryMissing }; }
13
+ export function evaluateSkillGate(options = {}) { const classified = classifyRequiredSkills(options); const text = [options.text, options.action].filter(Boolean).join('\n'); const loaded = new Set([...readReceiptSkills(options.sessionId), ...readInlineSkills(text)]); const missingSkills = classified.requiredSkills.filter((skill) => !loaded.has(skill)); const recoveryMissing = classified.recoveryMissing || []; return { ok: missingSkills.length === 0 && recoveryMissing.length === 0, surface: options.surface || 'unknown', sessionId: options.sessionId || 'unknown', requiredSkills: classified.requiredSkills, loadedSkills: [...loaded].sort(), missingSkills, recoveryMissing, reasons: classified.reasons, autoLoadAvailable: options.autoLoadAvailable === true }; }
14
+ export function formatSkillGateBlock(result = {}) { const missing = Array.isArray(result.missingSkills) ? result.missingSkills : []; const recovery = Array.isArray(result.recoveryMissing) ? result.recoveryMissing : []; const reasons = Array.isArray(result.reasons) ? result.reasons : []; return ['=== ARIA SKILL AUTOLOAD GATE BLOCK ===', `surface: ${result.surface || 'unknown'}`, `missing_skills: ${missing.length ? missing.join(', ') : '(none)'}`, `missing_recovery_cycle: ${recovery.length ? recovery.join(', ') : '(none)'}`, `required_skills: ${(result.requiredSkills || []).join(', ') || '(none)'}`, reasons.length ? `reasons: ${reasons.join(' | ')}` : 'reasons: no classifier reason recorded', 'counter_action: re-submit, re-write, re-test, and escalate through ARIA console until successful proof exists. Do not downgrade this to an advisory warning.'].join('\n'); }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@aria_asi/cli",
3
- "version": "0.2.32",
4
- "description": "Aria Smart CLI \u2014 the world's first harness-powered terminal companion",
3
+ "version": "0.2.34",
4
+ "description": "Aria Smart CLI the world's first harness-powered terminal companion",
5
5
  "bin": {
6
- "aria": "./bin/aria.js"
6
+ "aria": "bin/aria.js"
7
7
  },
8
8
  "main": "./dist/aria-connector/src/index.js",
9
9
  "types": "./dist/aria-connector/src/index.d.ts",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "scripts": {
20
20
  "build": "tsc && node scripts/bundle-sdk.mjs",
21
- "check:hooks": "node scripts/validate-hook-contracts.mjs",
21
+ "check:hooks": "node scripts/validate-hook-contracts.mjs && node scripts/self-test-harness-gates.mjs",
22
+ "self-test:gates": "node scripts/self-test-harness-gates.mjs",
22
23
  "check:skills": "node scripts/validate-skill-prompts.mjs",
23
24
  "prepare": "npm run build",
24
25
  "dev": "tsc --watch",
@@ -50,5 +51,11 @@
50
51
  "node": ">=20.0.0"
51
52
  },
52
53
  "license": "UNLICENSED",
53
- "private": false
54
+ "private": false,
55
+ "keywords": [],
56
+ "author": "",
57
+ "bugs": {
58
+ "url": "https://github.com/REI-Nationwide/cowork-sandbox/issues"
59
+ },
60
+ "homepage": "https://github.com/REI-Nationwide/cowork-sandbox#readme"
54
61
  }
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
4
+ import { createHash, randomUUID } from 'node:crypto';
4
5
  import { homedir } from 'node:os';
5
6
  import { delimiter, dirname, resolve } from 'node:path';
6
7
  import { spawn, spawnSync } from 'node:child_process';
7
8
  import { createRequire } from 'node:module';
9
+ import { evaluateSkillGate, formatSkillGateBlock } from './hooks/lib/skill-autoload-gate.mjs';
8
10
 
9
11
  const require = createRequire(import.meta.url);
10
12
  const { WebSocketServer, WebSocket } = require('ws');
@@ -75,6 +77,24 @@ function sleep(ms) {
75
77
  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
76
78
  }
77
79
 
80
+ function stableEvidenceString(value) {
81
+ if (typeof value === 'string') return value;
82
+ try { return JSON.stringify(value); } catch { return String(value); }
83
+ }
84
+
85
+ function makeEvidenceRef(kind, value, metadata = {}) {
86
+ const raw = stableEvidenceString(value);
87
+ const sha256 = createHash('sha256').update(raw).digest('hex');
88
+ return {
89
+ evidenceId: `ev_${sha256.slice(0, 16)}`,
90
+ kind,
91
+ at: new Date().toISOString(),
92
+ sha256,
93
+ preview: raw.slice(0, 500),
94
+ metadata,
95
+ };
96
+ }
97
+
78
98
  function readRuntimeToken() {
79
99
  const envToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || process.env.OPENAI_API_KEY || process.env.ARIA_MASTER_TOKEN;
80
100
  if (envToken) return envToken;
@@ -123,9 +143,10 @@ function ensureTurnState(threadId, turnId) {
123
143
  userText: '',
124
144
  preReceiptId: null,
125
145
  agentText: '',
126
- bufferedAgentNotifications: [],
127
- firstAgentItemId: null,
128
- };
146
+ bufferedAgentNotifications: [],
147
+ firstAgentItemId: null,
148
+ traceId: `trace_${randomUUID()}`,
149
+ };
129
150
  turnState.set(key, state);
130
151
  }
131
152
  return state;
@@ -186,6 +207,16 @@ async function validateTurnText(threadId, turnId) {
186
207
  if (!text) {
187
208
  return { ok: false, reason: 'No assistant text exists for this turn yet. Codex must emit readable cognition before action.' };
188
209
  }
210
+ const skillGate = evaluateSkillGate({
211
+ sessionId: `codex:${threadId}:${turnId}`,
212
+ surface: 'codex-bridge-output',
213
+ text: [state.userText, text].join('\n'),
214
+ isOutputCloseout: true,
215
+ autoLoadAvailable: false,
216
+ });
217
+ if (!skillGate.ok) {
218
+ return { ok: false, reason: formatSkillGateBlock(skillGate), result: { skillGate } };
219
+ }
189
220
  const result = await postRuntime('/validate-output', {
190
221
  text,
191
222
  sessionId: `codex:${threadId}:${turnId}`,
@@ -198,6 +229,7 @@ async function validateTurnText(threadId, turnId) {
198
229
  stage: 'codex-turn',
199
230
  actor: 'codex-bridge',
200
231
  system: 'codex-bridge',
232
+ traceId: state.traceId,
201
233
  },
202
234
  });
203
235
  const pass = result?.pass === true && result?.validation?.passed !== false;
@@ -210,11 +242,29 @@ async function validateTurnText(threadId, turnId) {
210
242
  };
211
243
  }
212
244
 
213
- async function checkActionAgainstRuntime(action, target, threadId, turnId) {
245
+ async function checkActionAgainstRuntime(action, target, threadId, turnId, metadata = {}) {
246
+ const state = ensureTurnState(threadId, turnId);
247
+ const skillGate = evaluateSkillGate({
248
+ sessionId: `codex:${threadId}:${turnId}`,
249
+ surface: 'codex-bridge-action',
250
+ text: target,
251
+ action: target,
252
+ toolName: action,
253
+ isDeploy: action === 'deploy',
254
+ isMutation: action === 'write',
255
+ autoLoadAvailable: false,
256
+ });
257
+ if (!skillGate.ok) {
258
+ return { ok: false, reason: formatSkillGateBlock(skillGate), result: { skillGate } };
259
+ }
214
260
  const result = await postRuntime('/check-action', {
215
261
  action,
216
262
  target,
217
263
  sessionId: `codex:${threadId}:${turnId}`,
264
+ actor: 'codex',
265
+ roleProfile: process.env.CODEX_ARIA_ROLE_PROFILE || process.env.ARIA_ROLE_PROFILE || null,
266
+ verifyText: state.agentText || '',
267
+ ...metadata,
218
268
  });
219
269
  if (result?.allowed === false) {
220
270
  return {
@@ -244,6 +294,8 @@ async function recordMizanPre(threadId, turnId) {
244
294
  surface: 'codex-bridge',
245
295
  platform: 'codex',
246
296
  userText: state.userText,
297
+ traceId: state.traceId,
298
+ evidenceRefs: [makeEvidenceRef('user_input', state.userText, { threadId, turnId, traceId: state.traceId })],
247
299
  },
248
300
  });
249
301
  state.preReceiptId = result?.receipt?.receiptId || null;
@@ -262,6 +314,8 @@ async function recordMizanPost(threadId, turnId, pass, summary) {
262
314
  evidence: {
263
315
  validated_output: pass,
264
316
  bridge: 'codex',
317
+ trace_id: state.traceId,
318
+ output_ref: makeEvidenceRef('assistant_output', state.agentText || summary, { threadId, turnId, traceId: state.traceId }),
265
319
  },
266
320
  context: {
267
321
  sessionId: `codex:${threadId}:${turnId}`,
@@ -269,6 +323,7 @@ async function recordMizanPost(threadId, turnId, pass, summary) {
269
323
  platform: 'codex',
270
324
  userText: state.userText,
271
325
  summary,
326
+ traceId: state.traceId,
272
327
  },
273
328
  });
274
329
  } catch (error) {
@@ -285,6 +340,8 @@ async function recordMizanPost(threadId, turnId, pass, summary) {
285
340
  details: {
286
341
  threadId,
287
342
  turnId,
343
+ traceId: state.traceId,
344
+ outputRef: makeEvidenceRef('assistant_output', state.agentText || summary, { threadId, turnId, traceId: state.traceId }),
288
345
  },
289
346
  });
290
347
  } catch (error) {
@@ -336,7 +393,9 @@ async function handleApprovalRequest(upstream, downstream, message) {
336
393
  cwd: params.cwd || null,
337
394
  commandActions: params.commandActions || params.parsedCmd || null,
338
395
  }).slice(0, 1500);
339
- const actionCheck = await checkActionAgainstRuntime(action, target, threadId, turnId);
396
+ const actionCheck = await checkActionAgainstRuntime(action, target, threadId, turnId, {
397
+ requireVerify: action === 'delete' || action === 'deploy',
398
+ });
340
399
  if (!actionCheck.ok) {
341
400
  const reason = `Aria runtime denied ${action}: ${actionCheck.reason}`;
342
401
  upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
@@ -360,8 +419,9 @@ async function handleApprovalRequest(upstream, downstream, message) {
360
419
  }
361
420
 
362
421
  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);
422
+ const target = params.grantRoot || params.path || params.filePath || params.reason || params.itemId || 'file-change';
423
+ const files = [params.path, params.filePath, params.grantRoot].filter((value) => typeof value === 'string' && value.trim());
424
+ const actionCheck = await checkActionAgainstRuntime('write', String(target), threadId, turnId, { files });
365
425
  if (!actionCheck.ok) {
366
426
  const reason = `Aria runtime denied file change: ${actionCheck.reason}`;
367
427
  upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
@@ -390,7 +450,10 @@ async function handleApprovalRequest(upstream, downstream, message) {
390
450
  permissions: params.permissions || null,
391
451
  reason: params.reason || null,
392
452
  }).slice(0, 1500);
393
- const actionCheck = await checkActionAgainstRuntime('write', target, threadId, turnId);
453
+ const files = Array.isArray(params.permissions?.fileSystem?.entries)
454
+ ? params.permissions.fileSystem.entries.filter((value) => typeof value === 'string' && value.trim())
455
+ : [];
456
+ const actionCheck = await checkActionAgainstRuntime('write', target, threadId, turnId, { files });
394
457
  if (!actionCheck.ok) {
395
458
  const reason = `Aria runtime denied permissions request: ${actionCheck.reason}`;
396
459
  upstream.send(JSON.stringify(makeGuardianWarning(threadId, reason)));
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createServer } from 'node:http';
4
+ import { createHash } from 'node:crypto';
4
5
  import { createRequire } from 'node:module';
5
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
7
  import { fileURLToPath } from 'node:url';
@@ -35,6 +36,7 @@ const LOCAL_FIRST_PRINCIPLE = 'Truth over deception. No harm. Sacred trust. Powe
35
36
 
36
37
  let cachedPacketEnvelope = loadCachedPacketEnvelope();
37
38
  let refreshInFlight = null;
39
+ let refreshInFlightKey = '';
38
40
  let lastRefreshError = null;
39
41
  let lastRefreshStartedAt = null;
40
42
  let lastRefreshCompletedAt = cachedPacketEnvelope?.timestamp || null;
@@ -99,6 +101,46 @@ function sanitizePacketEnvelope(raw) {
99
101
  };
100
102
  }
101
103
 
104
+ function stableIdentityValue(value) {
105
+ return String(value || '').trim().toLowerCase();
106
+ }
107
+
108
+ function requestCacheKey(body = {}, apiKey = '') {
109
+ const identity = {
110
+ stage: stableIdentityValue(body.stage || body.packetRequest?.stage),
111
+ actor: stableIdentityValue(body.actor || body.packetRequest?.actor),
112
+ system: stableIdentityValue(body.system || body.packetRequest?.system),
113
+ platform: stableIdentityValue(body.platform || body.packetRequest?.platform),
114
+ roleProfile: stableIdentityValue(body.roleProfile || body.role_profile || body.packetRequest?.roleProfile || body.packetRequest?.role_profile),
115
+ token: apiKey ? createHash('sha256').update(apiKey).digest('hex').slice(0, 16) : '',
116
+ };
117
+ return createHash('sha256').update(JSON.stringify(identity)).digest('hex');
118
+ }
119
+
120
+ function extractPacketField(packet, field) {
121
+ if (!packet || typeof packet !== 'object') return '';
122
+ const direct = packet[field];
123
+ if (typeof direct === 'string' && direct.trim()) return stableIdentityValue(direct);
124
+ for (const source of [packet.adapter, packet.harness]) {
125
+ if (typeof source !== 'string') continue;
126
+ const match = source.match(new RegExp(`(?:^|\\n)\\s*${field}\\s*=\\s*([^\\n\\s]+)`, 'i'));
127
+ if (match?.[1]) return stableIdentityValue(match[1]);
128
+ }
129
+ return '';
130
+ }
131
+
132
+ function cachedPacketMatchesRequest(envelope, body = {}) {
133
+ const packet = envelope?.packet;
134
+ if (!packet || typeof packet !== 'object') return false;
135
+ for (const field of ['stage', 'actor', 'system']) {
136
+ const expected = stableIdentityValue(body[field] || body.packetRequest?.[field]);
137
+ if (!expected) continue;
138
+ const actual = extractPacketField(packet, field);
139
+ if (actual && actual !== expected) return false;
140
+ }
141
+ return true;
142
+ }
143
+
102
144
  function loadCachedPacketEnvelope() {
103
145
  const candidates = [LOCAL_PACKET_CACHE_PATH, ...LEGACY_PACKET_CACHE_CANDIDATES];
104
146
  for (const candidate of candidates) {
@@ -220,10 +262,15 @@ async function fetchJsonWithRetry(url, init, attempts = 3) {
220
262
  }
221
263
 
222
264
  async function refreshPacket(body = {}, apiKey = '') {
223
- if (refreshInFlight) return refreshInFlight;
265
+ const cacheKey = requestCacheKey(body, apiKey);
266
+ if (refreshInFlight && refreshInFlightKey === cacheKey) return refreshInFlight;
267
+ if (refreshInFlight) {
268
+ await refreshInFlight.catch(() => {});
269
+ }
224
270
  if (isLoopedUpstream(UPSTREAM_HARNESS_URL)) {
225
271
  throw new Error(`upstream harness URL loops back to local daemon: ${UPSTREAM_HARNESS_URL}`);
226
272
  }
273
+ refreshInFlightKey = cacheKey;
227
274
  lastRefreshStartedAt = new Date().toISOString();
228
275
  lastRefreshError = null;
229
276
  refreshInFlight = (async () => {
@@ -258,6 +305,7 @@ async function refreshPacket(body = {}, apiKey = '') {
258
305
  throw error;
259
306
  } finally {
260
307
  refreshInFlight = null;
308
+ refreshInFlightKey = '';
261
309
  }
262
310
  }
263
311
 
@@ -268,7 +316,7 @@ function queuePacketRefresh(body, apiKey) {
268
316
  }
269
317
 
270
318
  async function resolvePacketEnvelope(packetRequest = {}, apiKey = '', message = '') {
271
- if (cachedPacketEnvelope) {
319
+ if (cachedPacketEnvelope && cachedPacketMatchesRequest(cachedPacketEnvelope, packetRequest)) {
272
320
  queuePacketRefresh(packetRequest, apiKey);
273
321
  return cachedPacketEnvelope;
274
322
  }