@aria_asi/cli 0.2.37 → 0.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/bin/aria.js +8 -4
- package/dist/aria-connector/src/auth.d.ts +1 -0
- package/dist/aria-connector/src/auth.d.ts.map +1 -1
- package/dist/aria-connector/src/auth.js +26 -1
- package/dist/aria-connector/src/auth.js.map +1 -1
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +0 -42
- package/dist/aria-connector/src/connectors/codex.js.map +1 -1
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
- package/dist/aria-connector/src/setup-wizard.js +41 -1
- package/dist/aria-connector/src/setup-wizard.js.map +1 -1
- package/dist/aria-connector/src/types.d.ts +6 -0
- package/dist/aria-connector/src/types.d.ts.map +1 -1
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +53 -0
- package/dist/assets/hooks/aria-pre-tool-use.mjs +75 -0
- package/dist/assets/hooks/lib/first-class-coach.mjs +3 -3
- package/dist/cli-0.2.38.tgz +0 -0
- package/dist/runtime/coach-kernel.mjs +66 -5
- package/dist/runtime/gated-ledger.mjs +237 -0
- package/dist/runtime/hooks/aria-pre-tool-gate.mjs +53 -0
- package/dist/runtime/hooks/aria-pre-tool-use.mjs +75 -0
- package/dist/runtime/hooks/lib/first-class-coach.mjs +3 -3
- package/dist/runtime/manifest.json +1 -1
- package/dist/runtime/quality-enforcer.mjs +257 -0
- package/dist/runtime/sdk/BUNDLED.json +1 -1
- package/dist/runtime/service.mjs +119 -0
- package/dist/sdk/BUNDLED.json +1 -1
- package/hooks/aria-pre-tool-gate.mjs +53 -0
- package/hooks/aria-pre-tool-use.mjs +75 -0
- package/hooks/lib/first-class-coach.mjs +3 -3
- package/package.json +1 -1
- package/runtime-src/coach-kernel.mjs +66 -5
- package/runtime-src/gated-ledger.mjs +237 -0
- package/runtime-src/quality-enforcer.mjs +257 -0
- package/runtime-src/service.mjs +119 -0
- package/scripts/install-client.sh +32 -2
- package/src/auth.ts +25 -1
- package/src/connectors/codex.ts +0 -42
- package/src/setup-wizard.ts +43 -1
- package/src/types.ts +6 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getHarnessClient,
|
|
4
|
+
inferSessionId,
|
|
5
|
+
classifyAction,
|
|
6
|
+
summarizeTarget,
|
|
7
|
+
readEventFromStdin,
|
|
8
|
+
loadTurnState,
|
|
9
|
+
makeEvidenceRef,
|
|
10
|
+
recordCoachPhase,
|
|
11
|
+
saveTurnState,
|
|
12
|
+
formatCodexRecoveryBlock,
|
|
13
|
+
emitJson,
|
|
14
|
+
} from './lib/runtime-client.mjs';
|
|
15
|
+
|
|
16
|
+
const event = readEventFromStdin();
|
|
17
|
+
const sessionId = inferSessionId(event);
|
|
18
|
+
const action = classifyAction(event);
|
|
19
|
+
const target = summarizeTarget(event);
|
|
20
|
+
const state = loadTurnState(sessionId);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (!state?.preReceiptId && !state?.userText) {
|
|
24
|
+
emitJson({
|
|
25
|
+
decision: 'block',
|
|
26
|
+
reason: formatCodexRecoveryBlock({
|
|
27
|
+
surface: 'codex-pre-tool',
|
|
28
|
+
reason: 'this turn has no pre-turn Mizan receipt',
|
|
29
|
+
next: '6. Re-submit the prompt so cognition is established before tool use, then request the tool again.',
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const toolName = String(event?.tool_name || event?.toolName || '').trim() || null;
|
|
34
|
+
const requestRef = makeEvidenceRef('codex_tool_request', { action, toolName, target }, { sessionId });
|
|
35
|
+
const coach = await recordCoachPhase('pre_tool', {
|
|
36
|
+
requestId: state?.traceId || sessionId,
|
|
37
|
+
sessionId,
|
|
38
|
+
text: target,
|
|
39
|
+
action,
|
|
40
|
+
target,
|
|
41
|
+
evidenceRefs: [requestRef],
|
|
42
|
+
metadata: { source: 'codex-pre-tool-hook', toolName, requireVerify: action === 'deploy' || action === 'delete' },
|
|
43
|
+
});
|
|
44
|
+
if (coach?.permitted === false) {
|
|
45
|
+
emitJson({
|
|
46
|
+
decision: 'block',
|
|
47
|
+
reason: formatCodexRecoveryBlock({
|
|
48
|
+
surface: 'codex-pre-tool-coach',
|
|
49
|
+
reason: coach.clientMessage || 'Coach Kernel denied ' + action,
|
|
50
|
+
next: '6. Add the required evidence/cognition contract, then request the tool again.',
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const tools = Array.isArray(state?.tools) ? state.tools.slice(-24) : [];
|
|
55
|
+
tools.push({
|
|
56
|
+
at: new Date().toISOString(),
|
|
57
|
+
action,
|
|
58
|
+
toolName,
|
|
59
|
+
target,
|
|
60
|
+
evidenceRef: makeEvidenceRef('tool_request', { action, toolName, target }, { sessionId }),
|
|
61
|
+
});
|
|
62
|
+
saveTurnState(sessionId, {
|
|
63
|
+
tools,
|
|
64
|
+
lastEvent: 'PreToolUse',
|
|
65
|
+
});
|
|
66
|
+
process.exit(0);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
emitJson({
|
|
69
|
+
decision: 'block',
|
|
70
|
+
reason: formatCodexRecoveryBlock({
|
|
71
|
+
surface: 'codex-pre-tool-hook',
|
|
72
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -243,10 +243,10 @@ function summarizeRuntimeToolTarget(event = {}) {
|
|
|
243
243
|
|
|
244
244
|
function inferRuntimeAction(event = {}, phase = '') {
|
|
245
245
|
const haystack = `${phase}\n${summarizeRuntimeToolTarget(event)}\n${stableJson(event).slice(0, 6000)}`;
|
|
246
|
-
if (/\b(?:kubectl\s+(?:apply|set|rollout|delete)|helm\s+upgrade|terraform\s+apply|docker\s+push|deploy)\b/i.test(haystack)) return 'deploy';
|
|
247
|
-
if (/\b(?:rm\s+-[rRfF]
|
|
246
|
+
if (/\b(?:kubectl\s+(?:apply|set|rollout|delete|create|replace|scale)|helm\s+(?:upgrade|install|uninstall)|terraform\s+(?:apply|destroy)|docker\s+(?:push|build\s+.*--push)|deploy)\b/i.test(haystack)) return 'deploy';
|
|
247
|
+
if (/\b(?:rm\s+-[rRfF]+\S*|drop\s+(?:table|database|schema|collection|index)|git\s+(?:reset\s+--hard|push\s+--force|push\s+--delete)|sudo\s+|systemctl\s+(?:stop|disable|mask|kill)|kill\s+-[9K]|pkill\s+-[9K]|chmod\s+777|docker\s+rm\s+-f)\b/i.test(haystack)) return 'delete';
|
|
248
|
+
if (/\b(?:--no-verify|--no-gpg-sign)\b/i.test(haystack)) return 'delete';
|
|
248
249
|
if (phase === 'stop' || phase === 'pre_emit') return 'release';
|
|
249
|
-
if (phase === 'pre_tool' || phase === 'post_tool') return 'write';
|
|
250
250
|
return '';
|
|
251
251
|
}
|
|
252
252
|
|
|
Binary file
|
|
@@ -127,7 +127,7 @@ function inferRiskClass(highRisk, repairable) {
|
|
|
127
127
|
return 'low';
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function decisionFromSignals(highRisk, repairable, warnings) {
|
|
130
|
+
function decisionFromSignals(highRisk, repairable, warnings, autoTrigger = []) {
|
|
131
131
|
if (highRisk.length > 0) {
|
|
132
132
|
return {
|
|
133
133
|
decision: 'hard_block',
|
|
@@ -136,6 +136,14 @@ function decisionFromSignals(highRisk, repairable, warnings) {
|
|
|
136
136
|
reasons: highRisk,
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
+
if (autoTrigger.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
decision: 'auto_trigger_skills',
|
|
142
|
+
permitted: false,
|
|
143
|
+
nextAction: 'auto_load_missing_skills_and_retry',
|
|
144
|
+
reasons: autoTrigger,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
139
147
|
if (repairable.length > 0) {
|
|
140
148
|
return {
|
|
141
149
|
decision: 'repair_once',
|
|
@@ -192,12 +200,16 @@ export function normalizeCoachEvent(input = {}) {
|
|
|
192
200
|
return record;
|
|
193
201
|
}
|
|
194
202
|
|
|
203
|
+
const DESTRUCTIVE_RX = /\b(?:rm\s+-[rRfF]+\S*|drop\s+(?:table|database|schema|collection|index)|git\s+(?:reset\s+--hard|push\s+--force|push\s+--delete)|sudo\s+|systemctl\s+(?:stop|disable|mask|kill)|kill\s+-[9K]|pkill\s+-[9K]|--no-verify|--no-gpg-sign|kubectl\s+(?:delete|scale\s+--replicas=0|rollout\s+undo)|docker\s+rm\s+-f|chmod\s+777|wget|curl.*\|\s*(?:ba)?sh)\b/i;
|
|
204
|
+
const DEPLOY_RX = /\b(?:kubectl\s+(?:apply|set|rollout|delete|create|replace|scale)|helm\s+(?:upgrade|install|uninstall)|terraform\s+(?:apply|destroy)|docker\s+(?:push|build\s+.*--push))\b/i;
|
|
205
|
+
|
|
195
206
|
export function evaluateCoachEvent(event = {}) {
|
|
196
207
|
const normalized = event.phase ? event : normalizeCoachEvent(event);
|
|
197
208
|
const text = normalized.rawText || normalized.text_preview || '';
|
|
198
209
|
const highRisk = [];
|
|
199
210
|
const repairable = [];
|
|
200
211
|
const warnings = [];
|
|
212
|
+
const autoTrigger = [];
|
|
201
213
|
const action = String(normalized.action || '').toLowerCase();
|
|
202
214
|
|
|
203
215
|
if (!ALLOWED_PHASES.has(normalized.phase)) {
|
|
@@ -207,13 +219,13 @@ export function evaluateCoachEvent(event = {}) {
|
|
|
207
219
|
highRisk.push('secret_or_credential_exposure');
|
|
208
220
|
}
|
|
209
221
|
if (normalized.phase === 'pre_generation' && normalized.missing_skill_ids.length > 0) {
|
|
210
|
-
|
|
222
|
+
autoTrigger.push('required_skill_auto_load');
|
|
211
223
|
}
|
|
212
224
|
if (TOOL_PHASES.has(normalized.phase) || action) {
|
|
213
|
-
if ((action === 'delete' ||
|
|
225
|
+
if ((action === 'delete' || DESTRUCTIVE_RX.test(text)) && normalized.metadata?.approved !== true) {
|
|
214
226
|
highRisk.push('unapproved_destructive_action');
|
|
215
227
|
}
|
|
216
|
-
if ((action === 'deploy' ||
|
|
228
|
+
if ((action === 'deploy' || DEPLOY_RX.test(text)) && !hasVerifyEvidence(normalized, text)) {
|
|
217
229
|
highRisk.push('unverified_deploy_or_infra_mutation');
|
|
218
230
|
}
|
|
219
231
|
}
|
|
@@ -229,17 +241,21 @@ export function evaluateCoachEvent(event = {}) {
|
|
|
229
241
|
repairable.push('unsupported_completion_or_verification_claim');
|
|
230
242
|
}
|
|
231
243
|
}
|
|
244
|
+
if (normalized.missing_skill_ids.length > 0 && !normalized.metadata?.skillsAdvisoryOnly) {
|
|
245
|
+
autoTrigger.push('required_skills_not_loaded');
|
|
246
|
+
}
|
|
232
247
|
if (normalized.lane.includes('unmanaged') || normalized.metadata?.complianceGuarantee === 'best_effort_only') {
|
|
233
248
|
warnings.push('unmanaged_direct_provider_best_effort_only');
|
|
234
249
|
}
|
|
235
250
|
|
|
236
|
-
const verdict = decisionFromSignals(highRisk, repairable, warnings);
|
|
251
|
+
const verdict = decisionFromSignals(highRisk, repairable, warnings, autoTrigger);
|
|
237
252
|
return {
|
|
238
253
|
...verdict,
|
|
239
254
|
riskClass: inferRiskClass(highRisk, repairable),
|
|
240
255
|
highRisk,
|
|
241
256
|
repairable,
|
|
242
257
|
warnings,
|
|
258
|
+
autoTrigger,
|
|
243
259
|
};
|
|
244
260
|
}
|
|
245
261
|
|
|
@@ -369,3 +385,48 @@ export function formatCoachClientBlock(recordOrResult = {}) {
|
|
|
369
385
|
`Next: ${next}`,
|
|
370
386
|
].join('\n');
|
|
371
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Auto-trigger skill loading when the Coach detects missing skills.
|
|
391
|
+
* Searches the skill registry for each missing skill ID and loads their
|
|
392
|
+
* body text into the turn context so the LLM can apply them.
|
|
393
|
+
*/
|
|
394
|
+
export async function triggerMissingSkills(missingSkillIds = [], skillSearchRoots = []) {
|
|
395
|
+
if (!missingSkillIds.length) return { loaded: [], stillMissing: [], loadedBodies: [] };
|
|
396
|
+
|
|
397
|
+
const loaded = [];
|
|
398
|
+
const stillMissing = [];
|
|
399
|
+
const loadedBodies = [];
|
|
400
|
+
|
|
401
|
+
const roots = skillSearchRoots.length > 0 ? skillSearchRoots : [
|
|
402
|
+
join(dirname(fileURLToPath(import.meta.url)), 'discipline', 'skills'),
|
|
403
|
+
join(process.env.HOME || '', '.aria', 'runtime', 'discipline', 'skills'),
|
|
404
|
+
join(process.env.HOME || '', '.claude', 'skills'),
|
|
405
|
+
join(process.env.HOME || '', '.agents', 'skills'),
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
for (const skillId of missingSkillIds) {
|
|
409
|
+
let found = false;
|
|
410
|
+
for (const root of roots) {
|
|
411
|
+
if (!existsSync(root)) continue;
|
|
412
|
+
try {
|
|
413
|
+
const entries = require('node:fs').readdirSync(root, { recursive: true, withFileTypes: true });
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') && !entry.name.endsWith('SKILL.md')) continue;
|
|
416
|
+
if (entry.name.toLowerCase().includes(skillId.toLowerCase()) ||
|
|
417
|
+
entry.parentPath?.toLowerCase().includes(skillId.toLowerCase())) {
|
|
418
|
+
const body = readFileSync(join(entry.parentPath || root, entry.name), 'utf8');
|
|
419
|
+
loaded.push(skillId);
|
|
420
|
+
loadedBodies.push({ skillId, path: join(entry.parentPath || root, entry.name), body: body.slice(0, 20000) });
|
|
421
|
+
found = true;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch {}
|
|
426
|
+
if (found) break;
|
|
427
|
+
}
|
|
428
|
+
if (!found) stillMissing.push(skillId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { loaded, stillMissing, loadedBodies };
|
|
432
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gated Ledger — End-to-End Runtime Enforcement
|
|
4
|
+
*
|
|
5
|
+
* Every kernel output, coach decision, completion claim, and doctrine
|
|
6
|
+
* trigger flows through this ledger. Nothing bypasses it.
|
|
7
|
+
*
|
|
8
|
+
* Gates (executed in order):
|
|
9
|
+
* 1. Skill Gate — missing skills → auto-trigger load → retry
|
|
10
|
+
* 2. Template Gate — deterministic/templated output → forced regeneration
|
|
11
|
+
* 3. Coach Gate — pre/post cognition, tool directives enforced
|
|
12
|
+
* 4. Quality Gate — gate labels, minimum substance, collapse text blocked
|
|
13
|
+
* 5. Evidence Gate — completion claims without measured evidence blocked
|
|
14
|
+
* 6. Doctrine Gate — doctrine trigger map enforced
|
|
15
|
+
* 7. Final Output Gate — safe fallback if all else fails
|
|
16
|
+
*
|
|
17
|
+
* Ledger records every gate decision, every enforcement action,
|
|
18
|
+
* every repair attempt, and every final outcome.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
const HOME = homedir();
|
|
27
|
+
const STATE_DIR = join(HOME, '.aria', 'runtime', 'state');
|
|
28
|
+
const GATED_LEDGER_PATH = join(STATE_DIR, 'gated-ledger.jsonl');
|
|
29
|
+
const DOCTRINE_TRIGGER_MAP_PATH = join(STATE_DIR, 'doctrine_trigger_map.json');
|
|
30
|
+
|
|
31
|
+
function ensureDir() {
|
|
32
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendRecord(record) {
|
|
36
|
+
ensureDir();
|
|
37
|
+
try {
|
|
38
|
+
appendFileSync(GATED_LEDGER_PATH, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
39
|
+
return true;
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hash(text) {
|
|
44
|
+
return createHash('sha256').update(String(text)).digest('hex').slice(0, 16);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const GATE_LABEL_RX = /\b(?:personal_mouth_[a-z_]+\b|code_no_tests\b|code_fake_implementation\b|code_type_safety\b|ip_infrastructure\b|8lens_[a-z_]+\b|voice_cold_[a-z_]+\b|harness_output_gate_block\b|auto_fix:\s|personal_mouth_harness_[a-z_]+\b|personal_mouth_unsupported_[a-z_]+\b)/i;
|
|
48
|
+
const COLLAPSE_RX = /I need to pause and reconsider\.?/i;
|
|
49
|
+
const COMPLETION_CLAIM_RX = /\b(?:done|complete|completed|ready|verified|fixed|shipped|production-ready|passing|passed|all phases|all done)\b/i;
|
|
50
|
+
const TEMPLATE_RX = /\b(?:Decision: use Owner Runtime kernels|Sequence: contract, Garden Service snapshot|Repair context loaded|Research context loaded|Action kernel engaged|I'm here with you\.\s*No fixing,\s*no task pressure)\b/i;
|
|
51
|
+
const MINIMUM_CHARS = 50;
|
|
52
|
+
|
|
53
|
+
/** Detect deterministic/templated kernel output */
|
|
54
|
+
function isTemplateOutput(text, kernel) {
|
|
55
|
+
if (!text || text.length < MINIMUM_CHARS) return { isTemplate: true, reason: 'below_minimum_substance' };
|
|
56
|
+
if (TEMPLATE_RX.test(text)) return { isTemplate: true, reason: 'deterministic_kernel_template' };
|
|
57
|
+
return { isTemplate: false, reason: '' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Check for completion claims without evidence */
|
|
61
|
+
function hasUnsupportedClaim(text) {
|
|
62
|
+
if (!COMPLETION_CLAIM_RX.test(text)) return { claim: false, reasons: [] };
|
|
63
|
+
const reasons = [];
|
|
64
|
+
if (!/\b(?:exit\s*0|0\s+failures?|passed|status.*ok|200|verified|ledger_record|receiptId|sha256|test.*pass)\b/i.test(text)) {
|
|
65
|
+
reasons.push('completion_claim_without_measured_evidence');
|
|
66
|
+
}
|
|
67
|
+
if (!/<cognition>[\s\S]*?<\/cognition>/i.test(text) && !/<verify>[\s\S]*?<\/verify>/i.test(text)) {
|
|
68
|
+
reasons.push('completion_claim_without_cognition_or_verify');
|
|
69
|
+
}
|
|
70
|
+
return { claim: reasons.length > 0, reasons };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Check quality */
|
|
74
|
+
function checkQuality(text) {
|
|
75
|
+
const reasons = [];
|
|
76
|
+
if (!text || text.length === 0) reasons.push('empty_output');
|
|
77
|
+
if (GATE_LABEL_RX.test(text)) reasons.push('gate_label_leak');
|
|
78
|
+
if (COLLAPSE_RX.test(text)) reasons.push('collapse_placeholder');
|
|
79
|
+
if (text.trim().length < MINIMUM_CHARS) reasons.push('below_minimum_chars');
|
|
80
|
+
return { passed: reasons.length === 0, reasons };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const SAFE_FALLBACKS = {
|
|
84
|
+
emotional_presence: "I'm here. Tell me what's with you right now.",
|
|
85
|
+
architect: "I need more context for a proper architecture answer.",
|
|
86
|
+
repair: "Let me trace the root cause. What's the specific error?",
|
|
87
|
+
action: "Action kernel requires confirmation. What would you like to execute?",
|
|
88
|
+
research: "Let me gather relevant information. What should I research?",
|
|
89
|
+
default: "Let me try again — that last response wasn't right.",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enforce all gates on a kernel output.
|
|
94
|
+
* Returns the final safe text and the full enforcement record.
|
|
95
|
+
*/
|
|
96
|
+
export async function enforceGates(text, context = {}) {
|
|
97
|
+
const kernel = context.kernel || 'default';
|
|
98
|
+
const sessionId = context.sessionId || 'runtime';
|
|
99
|
+
const gateLog = [];
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
let current = text || '';
|
|
102
|
+
let enforced = false;
|
|
103
|
+
let allPassed = true;
|
|
104
|
+
|
|
105
|
+
// ── Gate 1: Template Detection ────────────────────────────────────
|
|
106
|
+
const templateCheck = isTemplateOutput(current, kernel);
|
|
107
|
+
gateLog.push({
|
|
108
|
+
gate: 'template',
|
|
109
|
+
passed: !templateCheck.isTemplate,
|
|
110
|
+
reason: templateCheck.reason,
|
|
111
|
+
});
|
|
112
|
+
if (templateCheck.isTemplate) {
|
|
113
|
+
enforced = true;
|
|
114
|
+
current = SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
115
|
+
allPassed = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Gate 2: Quality ───────────────────────────────────────────────
|
|
119
|
+
const quality = checkQuality(current);
|
|
120
|
+
gateLog.push({
|
|
121
|
+
gate: 'quality',
|
|
122
|
+
passed: quality.passed,
|
|
123
|
+
reasons: quality.reasons,
|
|
124
|
+
});
|
|
125
|
+
if (!quality.passed) {
|
|
126
|
+
enforced = true;
|
|
127
|
+
current = SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
128
|
+
const requality = checkQuality(current);
|
|
129
|
+
if (!requality.passed) {
|
|
130
|
+
current = "I'm here. The pipeline needs attention. Let me recover.";
|
|
131
|
+
}
|
|
132
|
+
allPassed = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Gate 3: Completion Claims ─────────────────────────────────────
|
|
136
|
+
const claimCheck = hasUnsupportedClaim(current);
|
|
137
|
+
gateLog.push({
|
|
138
|
+
gate: 'completion_claim',
|
|
139
|
+
passed: !claimCheck.claim,
|
|
140
|
+
reasons: claimCheck.reasons,
|
|
141
|
+
});
|
|
142
|
+
if (claimCheck.claim) {
|
|
143
|
+
enforced = true;
|
|
144
|
+
// Remove the claim language, keep the substance
|
|
145
|
+
current = current.replace(/\b(?:done|complete|completed|ready|verified|fixed|shipped|all phases|all done)\b/gi, 'in progress');
|
|
146
|
+
allPassed = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Gate 4: Doctrine Trigger ──────────────────────────────────────
|
|
150
|
+
const doctrineHits = checkDoctrineTriggers(current, context);
|
|
151
|
+
gateLog.push({
|
|
152
|
+
gate: 'doctrine',
|
|
153
|
+
passed: doctrineHits.length === 0,
|
|
154
|
+
triggers: doctrineHits,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── Write Ledger Record ───────────────────────────────────────────
|
|
158
|
+
const record = {
|
|
159
|
+
recordId: randomUUID(),
|
|
160
|
+
sessionId,
|
|
161
|
+
kernel,
|
|
162
|
+
inputTextHash: hash(text),
|
|
163
|
+
finalTextHash: hash(current),
|
|
164
|
+
enforced,
|
|
165
|
+
allPassed,
|
|
166
|
+
gates: gateLog,
|
|
167
|
+
doctrineTriggers: doctrineHits,
|
|
168
|
+
durationMs: Date.now() - startedAt,
|
|
169
|
+
at: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
appendRecord(record);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
finalText: current,
|
|
175
|
+
enforced,
|
|
176
|
+
allPassed,
|
|
177
|
+
gates: gateLog,
|
|
178
|
+
doctrineTriggers: doctrineHits,
|
|
179
|
+
record,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Load the doctrine trigger map and check text against it */
|
|
184
|
+
function checkDoctrineTriggers(text, context) {
|
|
185
|
+
const triggers = [];
|
|
186
|
+
let map = null;
|
|
187
|
+
try {
|
|
188
|
+
if (existsSync(DOCTRINE_TRIGGER_MAP_PATH)) {
|
|
189
|
+
map = JSON.parse(readFileSync(DOCTRINE_TRIGGER_MAP_PATH, 'utf8'));
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
if (!map || !Array.isArray(map.triggers)) return triggers;
|
|
193
|
+
|
|
194
|
+
for (const trigger of map.triggers) {
|
|
195
|
+
if (!trigger.pattern) continue;
|
|
196
|
+
try {
|
|
197
|
+
const rx = new RegExp(trigger.pattern, 'i');
|
|
198
|
+
if (rx.test(text)) {
|
|
199
|
+
triggers.push({
|
|
200
|
+
trigger: trigger.name || trigger.pattern,
|
|
201
|
+
doctrine: trigger.doctrine || trigger.name,
|
|
202
|
+
severity: trigger.severity || 'warning',
|
|
203
|
+
action: trigger.action || 'log',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
return triggers;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fast inline enforcement — use in streamConversation or any
|
|
213
|
+
* point where a text is about to reach the user surface.
|
|
214
|
+
*/
|
|
215
|
+
export function enforceFast(text, kernel = 'default') {
|
|
216
|
+
if (!text || text.length === 0) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
217
|
+
if (GATE_LABEL_RX.test(text)) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
218
|
+
if (COLLAPSE_RX.test(text)) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
219
|
+
if (TEMPLATE_RX.test(text)) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read the gated ledger for monitoring/debugging.
|
|
225
|
+
*/
|
|
226
|
+
export function readGatedLedger(limit = 50) {
|
|
227
|
+
ensureDir();
|
|
228
|
+
try {
|
|
229
|
+
if (!existsSync(GATED_LEDGER_PATH)) return [];
|
|
230
|
+
const lines = readFileSync(GATED_LEDGER_PATH, 'utf8').trim().split('\n').filter(Boolean);
|
|
231
|
+
return lines.slice(-limit).map((line) => {
|
|
232
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
233
|
+
}).filter(Boolean);
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -1068,6 +1068,59 @@ if (emergencyGateOff.off) {
|
|
|
1068
1068
|
|
|
1069
1069
|
const toolName = event.tool_name ?? event.toolName ?? '';
|
|
1070
1070
|
const toolInput = event.tool_input ?? event.toolInput ?? {};
|
|
1071
|
+
|
|
1072
|
+
// Coach Kernel routing — single source of truth, run before all hook-native checks.
|
|
1073
|
+
try {
|
|
1074
|
+
const _coachUrl = `${HOME}/.aria/runtime/runtime.env`;
|
|
1075
|
+
const _coachBase = existsSync(_coachUrl)
|
|
1076
|
+
? String(readFileSync(_coachUrl, 'utf8')).match(/ARIA_RUNTIME_URL=(http:\/\/[^ \n]+)/)?.[1] || 'http://127.0.0.1:4319'
|
|
1077
|
+
: 'http://127.0.0.1:4319';
|
|
1078
|
+
const _coachToken = (() => {
|
|
1079
|
+
const tp = `${HOME}/.aria/owner-token`;
|
|
1080
|
+
if (existsSync(tp)) return readFileSync(tp, 'utf8').trim();
|
|
1081
|
+
const lp = `${HOME}/.aria/license.json`;
|
|
1082
|
+
if (existsSync(lp)) {
|
|
1083
|
+
try { const lt = JSON.parse(readFileSync(lp, 'utf8')); return lt.token || lt.harnessToken || ''; } catch { return ''; }
|
|
1084
|
+
}
|
|
1085
|
+
return process.env.ARIA_API_KEY || process.env.ARIA_MASTER_TOKEN || '';
|
|
1086
|
+
})();
|
|
1087
|
+
const _coachHeaders = { 'Content-Type': 'application/json' };
|
|
1088
|
+
if (_coachToken) _coachHeaders.Authorization = `Bearer ${_coachToken}`;
|
|
1089
|
+
const _cmd = String(toolInput?.command || '');
|
|
1090
|
+
const _coachPayload = {
|
|
1091
|
+
phase: 'pre_tool',
|
|
1092
|
+
requestId: `claude-pre-tool:${Date.now()}`,
|
|
1093
|
+
sessionId: String(toolInput?.session_id || process.env.HOOK_SESSION_ID || 'claude-unknown').slice(0, 80),
|
|
1094
|
+
surface: 'claude-hooks',
|
|
1095
|
+
lane: 'claude_native_hooks',
|
|
1096
|
+
action: (() => {
|
|
1097
|
+
const t = _cmd.toLowerCase();
|
|
1098
|
+
if (/\b(?:kubectl\s+(?:apply|set|rollout|delete|create|replace|scale)|helm\s+(?:upgrade|install|uninstall)|terraform\s+(?:apply|destroy)|docker\s+(?:push|build\s+.*--push)|deploy)\b/i.test(t)) return 'deploy';
|
|
1099
|
+
if (/\b(?:rm\s+-[rRfF]+\S*|sudo\s+|systemctl\s+(?:stop|disable|mask|kill)|kill\s+-[9K]|pkill\s+-[9K]|chmod\s+777|git\s+(?:push\s+--force|reset\s+--hard)|docker\s+rm\s+-f)\b/i.test(t)) return 'delete';
|
|
1100
|
+
return '';
|
|
1101
|
+
})(),
|
|
1102
|
+
target: JSON.stringify(toolInput).slice(0, 2000),
|
|
1103
|
+
text: _cmd.slice(0, 1000),
|
|
1104
|
+
metadata: { source: 'claude-pre-tool-gate', toolName },
|
|
1105
|
+
};
|
|
1106
|
+
const _coachResp = await fetch(`${_coachBase}/coach/phase`, {
|
|
1107
|
+
method: 'POST', headers: _coachHeaders, body: JSON.stringify(_coachPayload),
|
|
1108
|
+
signal: AbortSignal.timeout(2000),
|
|
1109
|
+
});
|
|
1110
|
+
if (_coachResp.ok) {
|
|
1111
|
+
const _coachBody = await _coachResp.json();
|
|
1112
|
+
if (_coachBody?.permitted === false && _coachBody?.decision === 'hard_block') {
|
|
1113
|
+
audit('block-coach-authoritative', `reasons=${(_coachBody.reasons||[]).join(',')}`);
|
|
1114
|
+
console.log(JSON.stringify({
|
|
1115
|
+
decision: 'block',
|
|
1116
|
+
reason: ['Aria Coach blocked this action before execution.', '', `Reason: ${(_coachBody.reasons||['coach_policy']).slice(0,3).join('; ')}`, '', _coachBody.clientMessage || 'Remove the high-risk condition and retry.'].join('\n'),
|
|
1117
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', coach_decision: _coachBody.decision, coach_reasons: _coachBody.reasons },
|
|
1118
|
+
}));
|
|
1119
|
+
process.exit(2);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} catch { /* Coach unreachable — fall through to hook-native checks */ }
|
|
1123
|
+
|
|
1071
1124
|
const transcriptPath = event.transcript_path ?? event.transcriptPath;
|
|
1072
1125
|
|
|
1073
1126
|
// Gate every action tool — every tool that mutates state must go through
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getHarnessClient,
|
|
4
|
+
inferSessionId,
|
|
5
|
+
classifyAction,
|
|
6
|
+
summarizeTarget,
|
|
7
|
+
readEventFromStdin,
|
|
8
|
+
loadTurnState,
|
|
9
|
+
makeEvidenceRef,
|
|
10
|
+
recordCoachPhase,
|
|
11
|
+
saveTurnState,
|
|
12
|
+
formatCodexRecoveryBlock,
|
|
13
|
+
emitJson,
|
|
14
|
+
} from './lib/runtime-client.mjs';
|
|
15
|
+
|
|
16
|
+
const event = readEventFromStdin();
|
|
17
|
+
const sessionId = inferSessionId(event);
|
|
18
|
+
const action = classifyAction(event);
|
|
19
|
+
const target = summarizeTarget(event);
|
|
20
|
+
const state = loadTurnState(sessionId);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (!state?.preReceiptId && !state?.userText) {
|
|
24
|
+
emitJson({
|
|
25
|
+
decision: 'block',
|
|
26
|
+
reason: formatCodexRecoveryBlock({
|
|
27
|
+
surface: 'codex-pre-tool',
|
|
28
|
+
reason: 'this turn has no pre-turn Mizan receipt',
|
|
29
|
+
next: '6. Re-submit the prompt so cognition is established before tool use, then request the tool again.',
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const toolName = String(event?.tool_name || event?.toolName || '').trim() || null;
|
|
34
|
+
const requestRef = makeEvidenceRef('codex_tool_request', { action, toolName, target }, { sessionId });
|
|
35
|
+
const coach = await recordCoachPhase('pre_tool', {
|
|
36
|
+
requestId: state?.traceId || sessionId,
|
|
37
|
+
sessionId,
|
|
38
|
+
text: target,
|
|
39
|
+
action,
|
|
40
|
+
target,
|
|
41
|
+
evidenceRefs: [requestRef],
|
|
42
|
+
metadata: { source: 'codex-pre-tool-hook', toolName, requireVerify: action === 'deploy' || action === 'delete' },
|
|
43
|
+
});
|
|
44
|
+
if (coach?.permitted === false) {
|
|
45
|
+
emitJson({
|
|
46
|
+
decision: 'block',
|
|
47
|
+
reason: formatCodexRecoveryBlock({
|
|
48
|
+
surface: 'codex-pre-tool-coach',
|
|
49
|
+
reason: coach.clientMessage || 'Coach Kernel denied ' + action,
|
|
50
|
+
next: '6. Add the required evidence/cognition contract, then request the tool again.',
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const tools = Array.isArray(state?.tools) ? state.tools.slice(-24) : [];
|
|
55
|
+
tools.push({
|
|
56
|
+
at: new Date().toISOString(),
|
|
57
|
+
action,
|
|
58
|
+
toolName,
|
|
59
|
+
target,
|
|
60
|
+
evidenceRef: makeEvidenceRef('tool_request', { action, toolName, target }, { sessionId }),
|
|
61
|
+
});
|
|
62
|
+
saveTurnState(sessionId, {
|
|
63
|
+
tools,
|
|
64
|
+
lastEvent: 'PreToolUse',
|
|
65
|
+
});
|
|
66
|
+
process.exit(0);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
emitJson({
|
|
69
|
+
decision: 'block',
|
|
70
|
+
reason: formatCodexRecoveryBlock({
|
|
71
|
+
surface: 'codex-pre-tool-hook',
|
|
72
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -243,10 +243,10 @@ function summarizeRuntimeToolTarget(event = {}) {
|
|
|
243
243
|
|
|
244
244
|
function inferRuntimeAction(event = {}, phase = '') {
|
|
245
245
|
const haystack = `${phase}\n${summarizeRuntimeToolTarget(event)}\n${stableJson(event).slice(0, 6000)}`;
|
|
246
|
-
if (/\b(?:kubectl\s+(?:apply|set|rollout|delete)|helm\s+upgrade|terraform\s+apply|docker\s+push|deploy)\b/i.test(haystack)) return 'deploy';
|
|
247
|
-
if (/\b(?:rm\s+-[rRfF]
|
|
246
|
+
if (/\b(?:kubectl\s+(?:apply|set|rollout|delete|create|replace|scale)|helm\s+(?:upgrade|install|uninstall)|terraform\s+(?:apply|destroy)|docker\s+(?:push|build\s+.*--push)|deploy)\b/i.test(haystack)) return 'deploy';
|
|
247
|
+
if (/\b(?:rm\s+-[rRfF]+\S*|drop\s+(?:table|database|schema|collection|index)|git\s+(?:reset\s+--hard|push\s+--force|push\s+--delete)|sudo\s+|systemctl\s+(?:stop|disable|mask|kill)|kill\s+-[9K]|pkill\s+-[9K]|chmod\s+777|docker\s+rm\s+-f)\b/i.test(haystack)) return 'delete';
|
|
248
|
+
if (/\b(?:--no-verify|--no-gpg-sign)\b/i.test(haystack)) return 'delete';
|
|
248
249
|
if (phase === 'stop' || phase === 'pre_emit') return 'release';
|
|
249
|
-
if (phase === 'pre_tool' || phase === 'post_tool') return 'write';
|
|
250
250
|
return '';
|
|
251
251
|
}
|
|
252
252
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"bundledAt": "2026-05-
|
|
2
|
+
"bundledAt": "2026-05-04T05:01:08.186Z",
|
|
3
3
|
"sdkFiles": 12,
|
|
4
4
|
"runtimeTemplate": "/home/hamzaibrahim1/rei-ai-brain/packages/aria-connector/runtime-src",
|
|
5
5
|
"gateRuntimeSource": "/home/hamzaibrahim1/rei-ai-brain/packages/aria-gate-runtime/dist",
|