@aria_asi/cli 0.2.38 → 0.2.40

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 (35) hide show
  1. package/README.md +140 -0
  2. package/bin/aria.js +8 -4
  3. package/dist/aria-connector/src/auth.d.ts +1 -0
  4. package/dist/aria-connector/src/auth.d.ts.map +1 -1
  5. package/dist/aria-connector/src/auth.js +26 -1
  6. package/dist/aria-connector/src/auth.js.map +1 -1
  7. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  8. package/dist/aria-connector/src/connectors/codex.js +52 -3
  9. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  10. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
  11. package/dist/aria-connector/src/setup-wizard.js +41 -1
  12. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  13. package/dist/aria-connector/src/types.d.ts +6 -0
  14. package/dist/aria-connector/src/types.d.ts.map +1 -1
  15. package/dist/assets/hooks/aria-pre-tool-gate.mjs +40 -12
  16. package/dist/cli-0.2.38.tgz +0 -0
  17. package/dist/runtime/coach-kernel.mjs +59 -4
  18. package/dist/runtime/gated-ledger.mjs +237 -0
  19. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +40 -12
  20. package/dist/runtime/manifest.json +1 -1
  21. package/dist/runtime/quality-enforcer.mjs +257 -0
  22. package/dist/runtime/sdk/BUNDLED.json +1 -1
  23. package/dist/runtime/service.mjs +119 -0
  24. package/dist/sdk/BUNDLED.json +1 -1
  25. package/hooks/aria-pre-tool-gate.mjs +40 -12
  26. package/package.json +1 -1
  27. package/runtime-src/coach-kernel.mjs +59 -4
  28. package/runtime-src/gated-ledger.mjs +237 -0
  29. package/runtime-src/quality-enforcer.mjs +257 -0
  30. package/runtime-src/service.mjs +119 -0
  31. package/scripts/install-client.sh +32 -2
  32. package/src/auth.ts +25 -1
  33. package/src/connectors/codex.ts +52 -3
  34. package/src/setup-wizard.ts +43 -1
  35. package/src/types.ts +6 -0
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runtime Quality Enforcer — First-Class Doctrine Rails
4
+ *
5
+ * Hard-blocks any output that contains internal gate labels, placeholders,
6
+ * or collapse-text. No gate label ever reaches a user surface again.
7
+ *
8
+ * Invariants:
9
+ * 1. HARD regex blocks — catch-all for any internal machinery leaking
10
+ * 2. Minimum substance check — no empty/trivial responses
11
+ * 3. Recovery contract — blocked → rewrite prompt → retry (max 2) → safe fallback
12
+ * 4. Coach kernel notified of every violation for pattern learning
13
+ * 5. Quality violation ledger records every enforcement action
14
+ * 6. Safe fallbacks guaranteed per kernel — deterministic, never empty
15
+ */
16
+
17
+ import { createHash, randomUUID } from 'node:crypto';
18
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+
22
+ // ── Paths ──────────────────────────────────────────────────────────────────
23
+
24
+ const HOME = homedir();
25
+ const STATE_DIR = join(HOME, '.aria', 'runtime', 'state');
26
+ const QUALITY_LEDGER_PATH = join(STATE_DIR, 'quality-violations.jsonl');
27
+ const COACH_STATE_PATH = join(STATE_DIR, 'coach-state.json');
28
+
29
+ // ── Hard Doctrine Rails ────────────────────────────────────────────────────
30
+
31
+ const HARD_BLOCK_PATTERNS = [
32
+ { pattern: /\bpersonal_mouth_[a-z_]+\b/i, label: 'gate_label:personal_mouth' },
33
+ { pattern: /\bcode_no_tests\b/i, label: 'gate_label:code_no_tests' },
34
+ { pattern: /\bcode_fake_implementation\b/i, label: 'gate_label:fake_impl' },
35
+ { pattern: /\bcode_type_safety\b/i, label: 'gate_label:type_safety' },
36
+ { pattern: /\bip_infrastructure\b/i, label: 'gate_label:ip_leak' },
37
+ { pattern: /\b8lens_[a-z_]+\b/i, label: 'gate_label:8lens' },
38
+ { pattern: /\bvoice_cold_[a-z_]+\b/i, label: 'gate_label:voice_cold' },
39
+ { pattern: /\bharness_output_gate_block\b/i, label: 'gate_label:output_block' },
40
+ { pattern: /\bauto_fix:\s/i, label: 'gate_label:auto_fix' },
41
+ { pattern: /I need to pause and reconsider\.?/i, label: 'gate_label:collapse_placeholder' },
42
+ { pattern: /\bpersonal_mouth_harness_shallow_[a-z_]+\b/i, label: 'gate_label:shallow' },
43
+ { pattern: /\bpersonal_mouth_unsupported_internal_[a-z_]+\b/i, label: 'gate_label:internal_claim' },
44
+ ];
45
+
46
+ const MINIMUM_CHARS = 40;
47
+
48
+ const SAFE_FALLBACKS = {
49
+ emotional_presence: "I'm here. Tell me what's with you right now.",
50
+ architect: "I need more context to give a proper architecture answer. What specific system or decision are you working on?",
51
+ repair: "I can see the issue — let me trace the root cause. Can you share the specific error or surface that's broken?",
52
+ action: "Action kernel received — confirmation required before proceeding. What would you like to execute?",
53
+ research: "Let me gather the relevant information. What specific topic or question should I research?",
54
+ default: "Let me try again — that last response wasn't right. What were you asking about?",
55
+ };
56
+
57
+ // ── Violation Ledger ──────────────────────────────────────────────────────
58
+
59
+ function ensureStateDir() {
60
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
61
+ }
62
+
63
+ function logViolation(violation) {
64
+ ensureStateDir();
65
+ try {
66
+ appendFileSync(QUALITY_LEDGER_PATH, `${JSON.stringify(violation)}\n`, { encoding: 'utf8' });
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function notifyCoach(violation) {
74
+ ensureStateDir();
75
+ try {
76
+ const event = {
77
+ at: new Date().toISOString(),
78
+ type: 'quality_violation',
79
+ violationId: violation.violationId,
80
+ kernel: violation.kernel,
81
+ violation: violation.violation,
82
+ textPreview: violation.textPreview,
83
+ recoveryAttempts: violation.recoveryAttempts,
84
+ finalOutcome: violation.finalOutcome,
85
+ };
86
+ if (existsSync(COACH_STATE_PATH)) {
87
+ // Append to coach state for offline learning
88
+ appendFileSync(COACH_STATE_PATH, `${JSON.stringify(event)}\n`, { encoding: 'utf8' });
89
+ }
90
+ } catch {
91
+ // Non-fatal — coach learning is best-effort
92
+ }
93
+ }
94
+
95
+ function violationHash(text) {
96
+ return createHash('sha256').update(String(text)).digest('hex').slice(0, 16);
97
+ }
98
+
99
+ // ── Core Enforcement ──────────────────────────────────────────────────────
100
+
101
+ export function checkQuality(text) {
102
+ if (typeof text !== 'string' || text.length === 0) {
103
+ return { allowed: false, reasons: ['empty_output'] };
104
+ }
105
+
106
+ const reasons = [];
107
+ for (const { pattern, label } of HARD_BLOCK_PATTERNS) {
108
+ if (pattern.test(text)) {
109
+ reasons.push(label);
110
+ }
111
+ }
112
+
113
+ if (text.trim().length < MINIMUM_CHARS) {
114
+ reasons.push('below_minimum_chars');
115
+ }
116
+
117
+ return { allowed: reasons.length === 0, reasons };
118
+ }
119
+
120
+ export async function enforceQualityWithRecovery(
121
+ text,
122
+ kernel = 'default',
123
+ options = {},
124
+ ) {
125
+ const sessionId = options.sessionId || 'runtime';
126
+ const rewriteFn = options.rewriteFn || null;
127
+ const maxAttempts = Math.min(2, Math.max(0, Number(options.maxRecoveryAttempts || 2)));
128
+
129
+ const initial = checkQuality(text);
130
+ if (initial.allowed) {
131
+ return {
132
+ finalText: text,
133
+ enforced: false,
134
+ attempts: 0,
135
+ violations: [],
136
+ logged: false,
137
+ };
138
+ }
139
+
140
+ const violations = [...initial.reasons];
141
+ let currentText = text;
142
+ let attempts = 0;
143
+ let repaired = false;
144
+
145
+ // Recovery loop: ask the model to repair its own output
146
+ while (attempts < maxAttempts && rewriteFn && !repaired) {
147
+ attempts += 1;
148
+ try {
149
+ const repairedText = await rewriteFn(
150
+ `Your previous response was blocked by quality enforcement for these reasons: ${initial.reasons.join(', ')}. ` +
151
+ `Rewrite the answer to remove all internal labels, gate phrases, and placeholder text. ` +
152
+ `Original context: ${text.slice(0, 200)}`
153
+ );
154
+ const repairCheck = checkQuality(repairedText);
155
+ if (repairCheck.allowed) {
156
+ currentText = repairedText;
157
+ repaired = true;
158
+ break;
159
+ }
160
+ violations.push(...repairCheck.reasons);
161
+ } catch {
162
+ // Recovery attempt failed — continue to next attempt or fallback
163
+ }
164
+ }
165
+
166
+ // Safe fallback if all recovery attempts failed
167
+ const finalText = repaired
168
+ ? currentText
169
+ : (SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default);
170
+
171
+ // Log the violation
172
+ const violation = {
173
+ violationId: randomUUID(),
174
+ sessionId,
175
+ kernel,
176
+ violations,
177
+ textPreview: String(text).slice(0, 200),
178
+ textHash: violationHash(text),
179
+ recoveryAttempts: attempts,
180
+ finalOutcome: repaired ? 'repaired' : 'safe_fallback',
181
+ at: new Date().toISOString(),
182
+ };
183
+
184
+ const logged = logViolation(violation);
185
+ notifyCoach(violation);
186
+
187
+ return {
188
+ finalText,
189
+ enforced: true,
190
+ attempts,
191
+ violations,
192
+ logged,
193
+ };
194
+ }
195
+
196
+ // ── Safe Mouth Proxy ──────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Drop-in guard for mounted-runtime provider pipelines.
200
+ * Accepts a providerMeta text and guarantees the finalText is safe.
201
+ */
202
+ export async function guardProviderOutput(providerMeta, kernel, sessionId) {
203
+ const result = await enforceQualityWithRecovery(
204
+ providerMeta.text,
205
+ kernel,
206
+ { sessionId },
207
+ );
208
+ return {
209
+ ...providerMeta,
210
+ text: result.finalText,
211
+ quality: {
212
+ enforced: result.enforced,
213
+ attempts: result.attempts,
214
+ violations: result.violations,
215
+ logged: result.logged,
216
+ },
217
+ };
218
+ }
219
+
220
+ // ── Coach Notification Bridge ─────────────────────────────────────────────
221
+
222
+ /**
223
+ * Notifies the coach kernel of a new quality violation pattern.
224
+ * Called by the quality enforcer after every enforcement action.
225
+ */
226
+ export function getCoachQualitySummary() {
227
+ ensureStateDir();
228
+ try {
229
+ if (!existsSync(QUALITY_LEDGER_PATH)) {
230
+ return { ok: true, violationCount: 0, recentPatterns: [] };
231
+ }
232
+ const lines = require('node:fs').readFileSync(QUALITY_LEDGER_PATH, 'utf8').trim().split('\n').filter(Boolean);
233
+ const violations = lines.map((line) => {
234
+ try { return JSON.parse(line); } catch { return null; }
235
+ }).filter(Boolean);
236
+
237
+ // Count by violation type
238
+ const byType = {};
239
+ for (const v of violations) {
240
+ for (const reason of (v.violations || [])) {
241
+ byType[reason] = (byType[reason] || 0) + 1;
242
+ }
243
+ }
244
+
245
+ return {
246
+ ok: true,
247
+ violationCount: violations.length,
248
+ recentPatterns: Object.entries(byType)
249
+ .sort(([, a], [, b]) => b - a)
250
+ .slice(0, 10)
251
+ .map(([pattern, count]) => ({ pattern, count })),
252
+ lastViolation: violations[violations.length - 1] || null,
253
+ };
254
+ } catch {
255
+ return { ok: false, violationCount: 0, recentPatterns: [] };
256
+ }
257
+ }
@@ -42,8 +42,11 @@ import {
42
42
  formatCoachClientBlock,
43
43
  readCoachState,
44
44
  recordCoachPhase,
45
+ triggerMissingSkills,
45
46
  } from './coach-kernel.mjs';
46
47
  import { resolveAriaAuthToken } from './auth-token.mjs';
48
+ import { check, enforceWithRecovery as enforceQualityWithRecovery } from './quality-enforcer.mjs';
49
+ import { enforceGates } from './gated-ledger.mjs';
47
50
 
48
51
  const require = createRequire(import.meta.url);
49
52
  const { runFullChain } = require('./vendor/aria-gate-runtime/index.js');
@@ -3726,6 +3729,15 @@ async function evaluateProviderCandidate(req, body, client, apiKey, turn, provid
3726
3729
  };
3727
3730
  }
3728
3731
 
3732
+ function deriveEffectiveKernel(turn, body) {
3733
+ const msg = body?.message || body?.prompt || body?.input || '';
3734
+ if (turn?.turnClass === 'repair' || /repair|fix|debug|broken|bug|error|failing|crash|recover/i.test(msg)) return 'repair';
3735
+ if (turn?.turnClass === 'architect' || /architecture|design|system|pipeline|tradeoff|ADR/i.test(msg)) return 'architect';
3736
+ if (turn?.turnClass === 'action' || /deploy|execute|run|build|ship|rollout|restart|push/i.test(msg)) return 'action';
3737
+ if (turn?.turnClass === 'research' || /research|search|find|recall|retrieve|look.up|investigate/i.test(msg)) return 'research';
3738
+ return 'emotional_presence';
3739
+ }
3740
+
3729
3741
  async function handleProviderProxy(req, body, client, providerStyle, options = {}) {
3730
3742
  const apiKey = resolveApiKey(req, body, options);
3731
3743
  const startedAt = Date.now();
@@ -3819,6 +3831,44 @@ async function handleProviderProxy(req, body, client, providerStyle, options = {
3819
3831
  ],
3820
3832
  });
3821
3833
  coachRecords.push(preGenerationCoach);
3834
+ // ── Coach auto-trigger: load missing skills instead of blocking ──
3835
+ if (preGenerationCoach.decision === 'auto_trigger_skills' && turn.missingSkillIds?.length > 0) {
3836
+ const skillResult = await triggerMissingSkills(
3837
+ turn.missingSkillIds,
3838
+ SKILL_SEARCH_ROOTS,
3839
+ );
3840
+ const loadedCoach = recordRuntimeCoachPhase({
3841
+ phase: 'pre_generation',
3842
+ body,
3843
+ turn: { ...turn, loadedSkillIds: [...(turn.loadedSkillIds || []), ...skillResult.loaded], missingSkillIds: skillResult.stillMissing },
3844
+ providerStyle,
3845
+ providerPlan,
3846
+ text: turn.userMessage,
3847
+ evidenceRefs: [`skills_loaded:${skillResult.loaded.length}`, `skills_still_missing:${skillResult.stillMissing.length}`],
3848
+ metadata: { autoLoadedSkills: skillResult.loaded },
3849
+ });
3850
+ coachRecords.push(loadedCoach);
3851
+ if (loadedCoach.decision === 'hard_block') {
3852
+ const refusal = formatCoachClientBlock(loadedCoach);
3853
+ const autoBlockRecord = appendManagedRuntimeLedger(buildManagedRuntimeLedgerRecord({
3854
+ phase: 'pre_generation_skills_block',
3855
+ body, turn, providerStyle, providerPlan,
3856
+ releaseDecision: 'hard_block',
3857
+ blockers: loadedCoach.reasons,
3858
+ evidenceRefs: coachRecordRefs(coachRecords),
3859
+ }));
3860
+ ledgerRecords.push({ phase: 'pre_generation_skills_block', ...autoBlockRecord });
3861
+ return providerStyle === 'anthropic'
3862
+ ? anthropicResponseEnvelope(refusal, { model: body.model || 'aria-runtime', finishReason: 'end_turn' }, {}, body?.ariaDebug === true)
3863
+ : openAiResponseEnvelope(body, refusal, { model: body.model || 'aria-runtime', finishReason: 'stop', usage: null }, {});
3864
+ }
3865
+ // Skills loaded — update turn for the rest of the pipeline
3866
+ turn.loadedSkillIds = [...(turn.loadedSkillIds || []), ...skillResult.loaded];
3867
+ turn.missingSkillIds = skillResult.stillMissing;
3868
+ if (skillResult.loadedBodies.length > 0) {
3869
+ turn.skillBodies = [...(turn.skillBodies || []), ...skillResult.loadedBodies.map((s) => s.body)];
3870
+ }
3871
+ }
3822
3872
  if (preGenerationCoach.decision === 'hard_block') {
3823
3873
  const refusal = formatCoachClientBlock(preGenerationCoach);
3824
3874
  const preBlockRecord = appendManagedRuntimeLedger(buildManagedRuntimeLedgerRecord({
@@ -3877,6 +3927,17 @@ async function handleProviderProxy(req, body, client, providerStyle, options = {
3877
3927
  ledgerRecords.push({ phase: 'provider_call_failed', ...failureRecord });
3878
3928
  throw error;
3879
3929
  }
3930
+
3931
+ const providerQualityResult = await enforceQualityWithRecovery(
3932
+ providerMeta.text || '',
3933
+ deriveEffectiveKernel(turn, body),
3934
+ { sessionId: turn.sessionId || body.sessionId },
3935
+ );
3936
+ if (providerQualityResult.enforced) {
3937
+ providerMeta.text = providerQualityResult.finalText;
3938
+ providerMeta.qualityEnforced = true;
3939
+ }
3940
+
3880
3941
  recordProviderUsage(body, turn, providerMeta);
3881
3942
  let hardCoachBlock = null;
3882
3943
  let evaluation = await evaluateProviderCandidate(req, body, client, apiKey, turn, providerStyle, providerMeta, providerMeta.text || '');
@@ -4125,11 +4186,55 @@ async function handleProviderProxy(req, body, client, providerStyle, options = {
4125
4186
  records: coachRecords,
4126
4187
  },
4127
4188
  };
4189
+ const finalQualityResult = await enforceQualityWithRecovery(
4190
+ finalText,
4191
+ deriveEffectiveKernel(turn, body),
4192
+ { sessionId: turn.sessionId || body.sessionId },
4193
+ );
4194
+ if (finalQualityResult.enforced) {
4195
+ finalText = finalQualityResult.finalText;
4196
+ }
4197
+
4198
+ // ── Gated Ledger: final enforcement before release ──
4199
+ const gated = await enforceGates(finalText, {
4200
+ kernel: deriveEffectiveKernel(turn, body),
4201
+ sessionId: turn.sessionId || body.sessionId,
4202
+ });
4203
+ if (gated.enforced) {
4204
+ extra.gatedLedger = {
4205
+ enforced: true,
4206
+ gates: gated.gates,
4207
+ doctrineTriggers: gated.doctrineTriggers,
4208
+ recordId: gated.record.recordId,
4209
+ };
4210
+ }
4211
+ finalText = gated.finalText;
4212
+
4128
4213
  return providerStyle === 'anthropic'
4129
4214
  ? anthropicResponseEnvelope(finalText, providerMeta, extra, body?.ariaDebug === true)
4130
4215
  : openAiResponseEnvelope(body, finalText, providerMeta, extra);
4131
4216
  }
4132
4217
 
4218
+ function extractProviderResponseText(response) {
4219
+ if (!response || typeof response !== 'object') return null;
4220
+ const choices = response.choices;
4221
+ if (Array.isArray(choices) && choices.length > 0) {
4222
+ const content = choices[0]?.message?.content;
4223
+ return typeof content === 'string' ? content : null;
4224
+ }
4225
+ return null;
4226
+ }
4227
+
4228
+ function extractAnthropicResponseText(response) {
4229
+ if (!response || typeof response !== 'object') return null;
4230
+ const content = response.content;
4231
+ if (Array.isArray(content)) {
4232
+ return content.map(c => (c && c.text ? c.text : '')).join(' ').trim() || null;
4233
+ }
4234
+ if (typeof content === 'string') return content;
4235
+ return null;
4236
+ }
4237
+
4133
4238
  async function handleForgeSynthesis(req, body, client) {
4134
4239
  const apiKey = resolveApiKey(req, body);
4135
4240
  const startedAt = Date.now();
@@ -5338,6 +5443,13 @@ async function handleRoute(req, res) {
5338
5443
 
5339
5444
  if (providerPath === '/v1/chat/completions') {
5340
5445
  const response = await handleProviderProxy(req, body, client, 'openai', ariaAuthOptions);
5446
+ const responseText = extractProviderResponseText(response);
5447
+ if (responseText) {
5448
+ const qScan = check(responseText);
5449
+ if (!qScan.allowed) {
5450
+ console.warn(`[quality-enforcer] Gate labels detected in OpenAI response after handleProviderProxy. ${qScan.reasons.join(', ')}`);
5451
+ }
5452
+ }
5341
5453
  return json(res, 200, response);
5342
5454
  }
5343
5455
 
@@ -5349,6 +5461,13 @@ async function handleRoute(req, res) {
5349
5461
 
5350
5462
  if (providerPath === '/v1/messages') {
5351
5463
  const response = await handleProviderProxy(req, body, client, 'anthropic', ariaAuthOptions);
5464
+ const responseText = extractAnthropicResponseText(response);
5465
+ if (responseText) {
5466
+ const qScan = check(responseText);
5467
+ if (!qScan.allowed) {
5468
+ console.warn(`[quality-enforcer] Gate labels detected in Anthropic response after handleProviderProxy. ${qScan.reasons.join(', ')}`);
5469
+ }
5470
+ }
5352
5471
  return json(res, 200, response);
5353
5472
  }
5354
5473
 
@@ -159,8 +159,38 @@ case "${SOURCE}" in
159
159
  ;;
160
160
  esac
161
161
 
162
- aria login "${TOKEN}"
163
- aria connect --force
162
+ resolve_aria_bin() {
163
+ if command -v aria >/dev/null 2>&1; then
164
+ echo "aria"
165
+ return 0
166
+ fi
167
+ local npm_prefix
168
+ npm_prefix="$(npm config get prefix 2>/dev/null || echo '')"
169
+ local candidate="${npm_prefix}/bin/aria"
170
+ if [[ -n "${npm_prefix}" && -x "${candidate}" ]]; then
171
+ echo "${candidate}"
172
+ return 0
173
+ fi
174
+ candidate="${HOME}/.npm-global/bin/aria"
175
+ if [[ -x "${candidate}" ]]; then
176
+ echo "${candidate}"
177
+ return 0
178
+ fi
179
+ return 1
180
+ }
181
+
182
+ ARIA_BIN="$(resolve_aria_bin)" || true
183
+ if [[ -z "${ARIA_BIN}" ]]; then
184
+ echo "I couldn't find the aria binary after install." >&2
185
+ echo "Restart your shell (exec \$SHELL -l) and run: aria login ${TOKEN}" >&2
186
+ else
187
+ "${ARIA_BIN}" login "${TOKEN}"
188
+ if "${ARIA_BIN}" connect --force 2>&1 | grep -qi 'server\|unreachable\|backend\|unavailable'; then
189
+ echo ""
190
+ echo "Aria Soul backend is unreachable — running local-only connect."
191
+ "${ARIA_BIN}" connect --local
192
+ fi
193
+ fi
164
194
  prompt_github_connect
165
195
 
166
196
  cat <<'EOF'
package/src/auth.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { createHash } from 'node:crypto';
3
- import { homedir } from 'node:os';
3
+ import { homedir, hostname } from 'node:os';
4
4
  import { join, dirname } from 'node:path';
5
5
  import type { AuthConfig } from './types.js';
6
6
 
@@ -127,6 +127,30 @@ export async function saveLicense(config: AuthConfig): Promise<void> {
127
127
  });
128
128
  }
129
129
 
130
+ export async function generateLocalLicense(): Promise<AuthConfig> {
131
+ const existing = await loadLicense();
132
+ if (existing?.token && existing?.sub) return existing;
133
+ const now = new Date();
134
+ const localToken = `local_${createHash('sha256').update(`${now.toISOString()}-${process.pid || 0}-${Math.random()}`).digest('hex').slice(0, 32)}`;
135
+ const ownerToken = `aria_${createHash('sha256').update(localToken).digest('hex').slice(0, 24)}`;
136
+ const license: AuthConfig = {
137
+ sub: `local-${createHash('sha256').update(hostname()).digest('hex').slice(0, 12)}`,
138
+ token: localToken,
139
+ harnessToken: ownerToken,
140
+ jti: `local-${Date.now()}`,
141
+ iat: Math.floor(now.getTime() / 1000),
142
+ iss: 'aria-local-runtime',
143
+ licenseVersion: 1,
144
+ features: { offline: true, localRuntime: true, coachGovernance: true },
145
+ };
146
+ await saveLicense(license);
147
+ try {
148
+ await mkdir(dirname(OWNER_TOKEN_PATH), { recursive: true, mode: 0o700 });
149
+ await writeFile(OWNER_TOKEN_PATH, ownerToken + '\n', { mode: 0o600, encoding: 'utf-8' });
150
+ } catch {}
151
+ return license;
152
+ }
153
+
130
154
  export async function resolveHarnessToken(
131
155
  options: ResolveHarnessTokenOptions = {},
132
156
  ): Promise<ResolvedHarnessToken> {
@@ -524,7 +524,7 @@ try {
524
524
 
525
525
  function buildCodexPreToolHook(): string {
526
526
  return `#!/usr/bin/env node
527
- import {
527
+ import {
528
528
  inferSessionId,
529
529
  classifyAction,
530
530
  summarizeTarget,
@@ -532,6 +532,7 @@ import {
532
532
  loadTurnState,
533
533
  makeEvidenceRef,
534
534
  recordCoachPhase,
535
+ runGovernanceGate,
535
536
  saveTurnState,
536
537
  formatCodexRecoveryBlock,
537
538
  emitJson,
@@ -575,6 +576,42 @@ try {
575
576
  }),
576
577
  });
577
578
  }
579
+ let gateEvidence = null;
580
+ try {
581
+ gateEvidence = runGovernanceGate({
582
+ sessionId,
583
+ sourceRuntime: 'codex',
584
+ surface: 'codex-pre-tool-use',
585
+ text: JSON.stringify(event).slice(0, 8000),
586
+ action,
587
+ toolName,
588
+ isDeploy: action === 'deploy',
589
+ isMutation: action === 'write' || action === 'delete',
590
+ evidence: requestRef,
591
+ });
592
+ } catch {}
593
+ if (gateEvidence) {
594
+ const gateRef = makeEvidenceRef('governance_gate', gateEvidence, { sessionId, action, toolName });
595
+ const gateCoach = await recordCoachPhase('pre_tool', {
596
+ requestId: state?.traceId || sessionId,
597
+ sessionId,
598
+ text: target,
599
+ action,
600
+ target,
601
+ evidenceRefs: [requestRef, gateRef],
602
+ metadata: { source: 'codex-pre-tool-hook', toolName, governanceGate: gateEvidence },
603
+ });
604
+ if (gateCoach?.permitted === false) {
605
+ emitJson({
606
+ decision: 'block',
607
+ reason: formatCodexRecoveryBlock({
608
+ surface: 'codex-pre-tool-gate-coach',
609
+ reason: gateCoach.clientMessage || 'Coach Kernel denied after governance gate signal.',
610
+ next: '6. Repair the condition flagged by the governance gate, then request the tool again.',
611
+ }),
612
+ });
613
+ }
614
+ }
578
615
  const tools = Array.isArray(state?.tools) ? state.tools.slice(-24) : [];
579
616
  tools.push({
580
617
  at: new Date().toISOString(),
@@ -679,6 +716,7 @@ import {
679
716
  formatValidationFailure,
680
717
  formatCodexRecoveryBlock,
681
718
  isAriaControlBlock,
719
+ runGovernanceGate,
682
720
  updateTaskProjectLedger,
683
721
  evaluateTaskProjectClaim,
684
722
  recordBlockedTaskProjectClaim,
@@ -722,6 +760,17 @@ try {
722
760
  }),
723
761
  });
724
762
  }
763
+ let gateEvidence = null;
764
+ try {
765
+ gateEvidence = runGovernanceGate({
766
+ sessionId,
767
+ sourceRuntime: 'codex',
768
+ surface: 'codex-stop',
769
+ text: text.slice(0, 8000),
770
+ isOutputCloseout: true,
771
+ evidence: outputRef,
772
+ });
773
+ } catch {}
725
774
  const ledgerClaim = evaluateTaskProjectClaim({ text, ledger: ledgerResult.ledger });
726
775
  if (!ledgerClaim.ok) {
727
776
  recordBlockedTaskProjectClaim({
@@ -763,8 +812,8 @@ try {
763
812
  text,
764
813
  validation: validation?.validation || null,
765
814
  layer3: validation?.layer3 || null,
766
- evidenceRefs: [outputRef, makeEvidenceRef('runtime_validation', validation, { sessionId, traceId: state?.traceId || null })],
767
- metadata: { source: 'codex-stop-hook', requireCognitionBlock: false, requireAppliedCognition: false },
815
+ evidenceRefs: [outputRef, makeEvidenceRef('runtime_validation', validation, { sessionId, traceId: state?.traceId || null }), ...(gateEvidence ? [makeEvidenceRef('governance_gate', gateEvidence, { sessionId })] : [])],
816
+ metadata: { source: 'codex-stop-hook', requireCognitionBlock: false, requireAppliedCognition: false, governanceGate: gateEvidence || null },
768
817
  });
769
818
  if (preOutputCoach?.permitted === false) {
770
819
  emitJson({
@@ -113,7 +113,17 @@ export async function runSetupWizard(): Promise<void> {
113
113
 
114
114
  const resp = await callConverse({ sessionId, action, userMessage });
115
115
  if (!resp.ok) {
116
- console.error(`\n❌ Onboarding error: ${resp.error || 'unknown'}`);
116
+ const errMsg = resp.error || 'unknown';
117
+ console.error(`\n⚠ Aria Soul backend is unreachable: ${errMsg}`);
118
+ console.log('\nYou can continue with local-only mode.');
119
+ console.log('Local mode gives you Coach governance, hooks, and runtime.');
120
+ console.log('Full features (hive sync, garden memory) activate when the server is reachable.\n');
121
+ const useLocal = await ask('Continue with local-only setup? [Y/n]: ');
122
+ if (useLocal.toLowerCase().startsWith('n')) {
123
+ console.log('\nRun `aria` again when the server is available, or use `aria login <token>`.\n');
124
+ return;
125
+ }
126
+ await runLocalOnlySetup(ask);
117
127
  return;
118
128
  }
119
129
 
@@ -345,6 +355,38 @@ async function applyConfigWrites(writes: ConfigWrite[]): Promise<void> {
345
355
  }
346
356
  }
347
357
 
358
+ async function runLocalOnlySetup(ask: (q: string) => Promise<string>): Promise<void> {
359
+ const { generateLocalLicense } = await import('./auth.js');
360
+ const { connectClaudeCode } = await import('./connectors/claude-code.js');
361
+ const { connectCodex } = await import('./connectors/codex.js');
362
+ const { installSharedRuntime } = await import('./connectors/runtime.js');
363
+ const provider = (await ask('LLM provider (openai/anthropic/deepseek/xai/google/openrouter): ')).trim().toLowerCase() || 'openai';
364
+ const apiKey = await ask(`${provider} API key: `);
365
+ const license = await generateLocalLicense();
366
+ console.log(`\n Local license generated (${license.sub}).`);
367
+ const config = loadConfig();
368
+ const validProvider = (['openai', 'anthropic', 'deepseek', 'xai', 'google', 'openrouter', 'ollama'] as const).find(
369
+ (p) => provider === p,
370
+ ) || 'openai';
371
+ config.model = {
372
+ provider: validProvider,
373
+ model: defaultModelForProvider(validProvider),
374
+ apiKey,
375
+ };
376
+ const { saveConfig: sc } = await import('./config.js');
377
+ sc(config as Parameters<typeof sc>[0]);
378
+ console.log(' Installing runtime and hooks...');
379
+ const logs = [
380
+ ...(await installSharedRuntime()),
381
+ ...(await connectClaudeCode(config, { force: true })),
382
+ ...(await connectCodex(config)),
383
+ ];
384
+ for (const line of logs) console.log(` ${line}`);
385
+ console.log('\n✅ Local setup complete.');
386
+ console.log(' Your CLIs are wired with Coach governance.');
387
+ console.log(' Run `aria login <token>` when the server becomes available for full features.\n');
388
+ }
389
+
348
390
  function defaultModelForProvider(provider: string): string {
349
391
  const defaults: Record<string, string> = {
350
392
  anthropic: 'claude-sonnet-4-20250514',
package/src/types.ts CHANGED
@@ -9,8 +9,14 @@ export interface AuthConfig {
9
9
  token?: string;
10
10
  /** License JWT identifier */
11
11
  jti?: string;
12
+ /** License issuer */
13
+ iss?: string;
12
14
  /** License tier (e.g. 'standard', 'pro') */
13
15
  tier?: string;
16
+ /** License version */
17
+ licenseVersion?: number;
18
+ /** License features map */
19
+ features?: Record<string, boolean>;
14
20
  /** License expiry unix timestamp */
15
21
  exp?: number;
16
22
  /** License issued-at unix timestamp */