@aria_asi/cli 0.2.23 → 0.2.24
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/dist/sdk/BUNDLED.json +1 -1
- package/hooks/aria-harness-via-sdk.mjs +38 -2
- package/hooks/aria-pre-tool-gate.mjs +210 -10
- package/package.json +1 -1
package/dist/sdk/BUNDLED.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
|
-
import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
21
21
|
import { dirname } from 'node:path';
|
|
22
22
|
import { createConnection } from 'node:net';
|
|
23
23
|
|
|
@@ -110,7 +110,7 @@ function loadCachedPacket() {
|
|
|
110
110
|
try {
|
|
111
111
|
if (existsSync(PACKET_CACHE)) {
|
|
112
112
|
const stat = readFileSync(PACKET_CACHE, 'utf8');
|
|
113
|
-
const ageSec = (Date.now() - (existsSync(PACKET_CACHE) ?
|
|
113
|
+
const ageSec = (Date.now() - (existsSync(PACKET_CACHE) ? statSync(PACKET_CACHE).mtimeMs : 0)) / 1000;
|
|
114
114
|
if (ageSec < PACKET_CACHE_TTL_SEC) {
|
|
115
115
|
return { data: JSON.parse(stat), ageSec };
|
|
116
116
|
}
|
|
@@ -175,6 +175,37 @@ async function loadSdkClass() {
|
|
|
175
175
|
return null;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// Bonus fix #74 — conversation_history_count=0 packet propagation.
|
|
179
|
+
//
|
|
180
|
+
// The codex handler (apps/arias-soul/api/harness/codex.ts) reads req.body.messages
|
|
181
|
+
// to inject conversation history into the harness packet so it can report a real
|
|
182
|
+
// conversation_history_count (non-zero). Previously tryViaSdk never forwarded the
|
|
183
|
+
// messages from the hook event — the packet always had conversation_history_count=0.
|
|
184
|
+
//
|
|
185
|
+
// Fix: read the Claude Code hook event from stdin at startup, extract event.messages
|
|
186
|
+
// (the conversation history array), and forward it in both the SDK bodyOverride and
|
|
187
|
+
// the direct-fetch body shape. The codex handler passes it through to
|
|
188
|
+
// buildAriaExternalHarnessPacket which populates conversation_history_count.
|
|
189
|
+
//
|
|
190
|
+
// Stdin is read once and stored in HOOK_EVENT_MESSAGES. It's capped at 50 entries
|
|
191
|
+
// before forwarding to avoid blowing the POST body size limit; the most recent
|
|
192
|
+
// messages are the most relevant for history-count purposes.
|
|
193
|
+
let HOOK_EVENT_MESSAGES = undefined;
|
|
194
|
+
try {
|
|
195
|
+
// Claude Code hooks receive the event JSON on stdin. We read it synchronously
|
|
196
|
+
// only if data is already available (i.e. piped); we don't block waiting for
|
|
197
|
+
// interactive input. Use a try/catch so the script still works when stdin is
|
|
198
|
+
// a terminal (e.g. manual invocation for testing).
|
|
199
|
+
const stdinBuf = readFileSync('/dev/stdin', { flag: 'r' });
|
|
200
|
+
const hookEvent = JSON.parse(stdinBuf.toString('utf8'));
|
|
201
|
+
if (Array.isArray(hookEvent?.messages) && hookEvent.messages.length > 0) {
|
|
202
|
+
// Cap to last 50 messages. The server-side handler slices further if needed.
|
|
203
|
+
HOOK_EVENT_MESSAGES = hookEvent.messages.slice(-50);
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// stdin not available / not JSON / no messages field — HOOK_EVENT_MESSAGES stays undefined.
|
|
207
|
+
}
|
|
208
|
+
|
|
178
209
|
async function tryViaSdk(baseUrl, apiKey) {
|
|
179
210
|
// Canonical path: HTTPHarnessClient.getHarnessPacket(). The SDK POSTs to
|
|
180
211
|
// /api/harness/codex with the right shape and returns { packet, timestamp,
|
|
@@ -199,6 +230,9 @@ async function tryViaSdk(baseUrl, apiKey) {
|
|
|
199
230
|
actor: 'claude-code',
|
|
200
231
|
system: 'claude-coding-agent',
|
|
201
232
|
platform: 'harness-http-client',
|
|
233
|
+
// Bonus #74: forward conversation history so codex handler can report
|
|
234
|
+
// a non-zero conversation_history_count in the packet.
|
|
235
|
+
...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
|
|
202
236
|
};
|
|
203
237
|
const wrapped = await client.getHarnessPacket(bodyOverride);
|
|
204
238
|
const json = wrapped.packet;
|
|
@@ -220,6 +254,8 @@ async function tryViaSdk(baseUrl, apiKey) {
|
|
|
220
254
|
system: 'claude-coding-agent',
|
|
221
255
|
roleProfile: 'general_worker',
|
|
222
256
|
deliverySurface: 'claude_code_session',
|
|
257
|
+
// Bonus #74: forward conversation history (same field as SDK path above).
|
|
258
|
+
...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
|
|
223
259
|
}),
|
|
224
260
|
});
|
|
225
261
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
@@ -134,11 +134,57 @@ function activePlanPath(sid) {
|
|
|
134
134
|
return `${HOME}/.claude/aria-active-plan-${String(sid || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Defect #5 — plan-completion persists across session boundary.
|
|
138
|
+
//
|
|
139
|
+
// The harness packet at ~/.claude/.aria-harness-last-packet.json retains
|
|
140
|
+
// exhausted plan state. An old plan from a prior session that was "all
|
|
141
|
+
// phases complete" would block every tool call in a NEW session because
|
|
142
|
+
// pickCurrentPhase returns null (all complete) on that stale plan.
|
|
143
|
+
//
|
|
144
|
+
// Fix: at load time, validate the plan file against TWO staleness guards:
|
|
145
|
+
// (a) mtime > 24h → discard (plan is older than a working day; a new
|
|
146
|
+
// session should start fresh)
|
|
147
|
+
// (b) plan.sessionId present AND doesn't match current sid → discard
|
|
148
|
+
// (plan was issued for a different session; current session is unbound)
|
|
149
|
+
//
|
|
150
|
+
// Discarding = returning null → the binding gate treats it as "no plan"
|
|
151
|
+
// and blocks with CONSULT_UNAVAILABLE, which triggers a fresh consult on
|
|
152
|
+
// the next user prompt. The discard is audit-logged.
|
|
153
|
+
const PLAN_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
154
|
+
|
|
137
155
|
function loadActivePlan(sid) {
|
|
138
156
|
const p = activePlanPath(sid);
|
|
139
157
|
if (!existsSync(p)) return null;
|
|
140
158
|
try {
|
|
141
|
-
|
|
159
|
+
const raw = readFileSync(p, 'utf8');
|
|
160
|
+
const plan = JSON.parse(raw);
|
|
161
|
+
// Guard (b) — session mismatch: plan was issued for a different session.
|
|
162
|
+
if (plan.sessionId && sid && plan.sessionId !== String(sid)) {
|
|
163
|
+
bindingAuditAppend({
|
|
164
|
+
event: 'discard_plan_session_mismatch',
|
|
165
|
+
planId: plan.planId,
|
|
166
|
+
planSessionId: plan.sessionId,
|
|
167
|
+
currentSessionId: sid,
|
|
168
|
+
});
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// Guard (a) — plan age via plan.mintedAt ISO timestamp (written by harness
|
|
172
|
+
// when issuing the plan). If mintedAt is present and older than 24h, discard.
|
|
173
|
+
// No mintedAt → conservatively trust the plan (harness version may predate
|
|
174
|
+
// this field; upgrade path: harness always writes mintedAt going forward).
|
|
175
|
+
if (plan.mintedAt) {
|
|
176
|
+
const mintedMs = Date.parse(plan.mintedAt);
|
|
177
|
+
if (Number.isFinite(mintedMs) && (Date.now() - mintedMs) > PLAN_MAX_AGE_MS) {
|
|
178
|
+
bindingAuditAppend({
|
|
179
|
+
event: 'discard_plan_stale_by_mintedAt',
|
|
180
|
+
planId: plan.planId,
|
|
181
|
+
mintedAt: plan.mintedAt,
|
|
182
|
+
ageHours: ((Date.now() - mintedMs) / 3600000).toFixed(1),
|
|
183
|
+
});
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return plan;
|
|
142
188
|
} catch {
|
|
143
189
|
return null;
|
|
144
190
|
}
|
|
@@ -199,15 +245,49 @@ function pickCurrentPhase(plan, transcript) {
|
|
|
199
245
|
return null; // all phases reported complete — needs new consult
|
|
200
246
|
}
|
|
201
247
|
|
|
202
|
-
function actionMatchesPattern(action, pattern) {
|
|
203
|
-
//
|
|
204
|
-
//
|
|
248
|
+
function actionMatchesPattern(action, pattern, target) {
|
|
249
|
+
// Defect #3 fix — edit-pattern regex over-matches.
|
|
250
|
+
//
|
|
251
|
+
// Old behaviour: "edit:/etc/**" matched any path that contained "/etc/" as a
|
|
252
|
+
// substring because the colon-split only checked pAction===action, ignoring the
|
|
253
|
+
// path portion entirely — so edit:/etc/** allowed ALL edit actions regardless
|
|
254
|
+
// of path, not just edits under /etc/.
|
|
255
|
+
//
|
|
256
|
+
// Fix:
|
|
257
|
+
// - Exact action name ("read", "consult", "kubectl_get"): unchanged.
|
|
258
|
+
// - Prefixed pattern ("edit:/etc/**"): split on first colon, compare
|
|
259
|
+
// pAction===action, then glob-match the path portion against `target`
|
|
260
|
+
// using anchored prefix matching (no substring). Supported forms:
|
|
261
|
+
// "edit:/etc/**" → action=edit, path must start with /etc/
|
|
262
|
+
// "edit:apps/.*" → action=edit, path must start with apps/
|
|
263
|
+
// "bash_other:curl .*" → action=bash_other (target substring match for bash)
|
|
264
|
+
// - No colon: exact action match only.
|
|
205
265
|
if (pattern === action) return true;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
266
|
+
const colonIdx = pattern.indexOf(':');
|
|
267
|
+
if (colonIdx < 0) return false;
|
|
268
|
+
const pAction = pattern.slice(0, colonIdx);
|
|
269
|
+
if (pAction !== action) return false;
|
|
270
|
+
// pAction matches — now check the target portion.
|
|
271
|
+
const pathPattern = pattern.slice(colonIdx + 1);
|
|
272
|
+
if (!pathPattern) return true; // bare "edit:" with no path spec → match all edits of that action type
|
|
273
|
+
const t = target || '';
|
|
274
|
+
// Convert simple glob /** suffix to prefix anchor, or treat as a regex if
|
|
275
|
+
// it contains regex meta-chars beyond "." and "*". For the most common
|
|
276
|
+
// harness patterns ("edit:/etc/**", "edit:apps/arias-soul/.*") a prefix-
|
|
277
|
+
// match on the stripped glob is correct and safe.
|
|
278
|
+
// Strip trailing /** or /*, then anchor-prefix-match.
|
|
279
|
+
const strippedGlob = pathPattern.replace(/\/\*+$/, '');
|
|
280
|
+
if (strippedGlob && !pathPattern.includes('(?') && !pathPattern.includes('[')) {
|
|
281
|
+
// Simple prefix match — anchored at start of target path.
|
|
282
|
+
return t.startsWith(strippedGlob);
|
|
283
|
+
}
|
|
284
|
+
// Full regex path pattern (caller used explicit regex syntax).
|
|
285
|
+
try {
|
|
286
|
+
return new RegExp(`^(?:${pathPattern})`).test(t);
|
|
287
|
+
} catch {
|
|
288
|
+
// Malformed regex in plan — fail-open to avoid blocking everything.
|
|
289
|
+
return false;
|
|
209
290
|
}
|
|
210
|
-
return false;
|
|
211
291
|
}
|
|
212
292
|
|
|
213
293
|
function classifyToolForBinding(toolName, command, filePath) {
|
|
@@ -471,6 +551,57 @@ function countCognitionLenses(text) {
|
|
|
471
551
|
return detectCognitionLenses(text).count;
|
|
472
552
|
}
|
|
473
553
|
|
|
554
|
+
// ── arch_facts gate ──────────────────────────────────────────────────────────
|
|
555
|
+
//
|
|
556
|
+
// Inspects actual diff content (tool_input.content / file_path) for
|
|
557
|
+
// architectural violations defined as arch_facts rules in mizan.yaml.
|
|
558
|
+
// Called AFTER cognition + discovery-binding checks pass, for Edit/Write/
|
|
559
|
+
// NotebookEdit only (Bash skipped — arch violations live in code, not commands).
|
|
560
|
+
//
|
|
561
|
+
// Tier-aware: if ~/.aria/license.json exists (client surface), arch_facts
|
|
562
|
+
// check is skipped — only owner gets server-side mizan enforcement here.
|
|
563
|
+
//
|
|
564
|
+
// Fail-open: if aria-telemetry is unreachable, the tool call is allowed.
|
|
565
|
+
// The 3s AbortController is a DETECTION PROBE (is telemetry alive?) not a
|
|
566
|
+
// hard deadline — if mizan is slow but alive the probe will complete; if it
|
|
567
|
+
// is down the catch path fail-opens without blocking the developer.
|
|
568
|
+
function isClientSurface() {
|
|
569
|
+
try {
|
|
570
|
+
return existsSync(`${HOME}/.aria/license.json`);
|
|
571
|
+
} catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function archFactsGate(toolInput) {
|
|
577
|
+
// Skip on client surfaces — arch_facts enforcement is owner-only.
|
|
578
|
+
if (isClientSurface()) return { ok: true, allow: true, note: 'client-surface-skip' };
|
|
579
|
+
try {
|
|
580
|
+
const ariaTelemetry = process.env.ARIA_TELEMETRY_BASE || 'http://aria-telemetry.aria.svc.cluster.local:8088';
|
|
581
|
+
const ctl = new AbortController();
|
|
582
|
+
const probeTimer = setTimeout(() => ctl.abort(), 3000);
|
|
583
|
+
let resp;
|
|
584
|
+
try {
|
|
585
|
+
resp = await fetch(`${ariaTelemetry}/v1/mizan/check`, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: { 'Content-Type': 'application/json' },
|
|
588
|
+
body: JSON.stringify({ draft: String(toolInput).slice(0, 50000), source: 'pre-tool-gate-arch-facts' }),
|
|
589
|
+
signal: ctl.signal,
|
|
590
|
+
});
|
|
591
|
+
} finally {
|
|
592
|
+
clearTimeout(probeTimer);
|
|
593
|
+
}
|
|
594
|
+
if (!resp.ok) return { ok: true, allow: true, note: 'mizan unreachable, fail-open' };
|
|
595
|
+
const result = await resp.json();
|
|
596
|
+
if (result?.hard_block === true && Array.isArray(result?.violations)) {
|
|
597
|
+
return { ok: false, allow: false, violations: result.violations };
|
|
598
|
+
}
|
|
599
|
+
return { ok: true, allow: true };
|
|
600
|
+
} catch {
|
|
601
|
+
return { ok: true, allow: true, note: 'mizan probe error, fail-open' };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
474
605
|
// Hive cognition-logging v1.2 — fire-and-forget HTTP push to
|
|
475
606
|
// /api/cognition/log so every gate decision joins the corpus.
|
|
476
607
|
// Failures are silent: the local audit log is the durable record;
|
|
@@ -873,6 +1004,49 @@ No env-var disable path — gates are unconditional from the gated process per H
|
|
|
873
1004
|
process.exit(2);
|
|
874
1005
|
}
|
|
875
1006
|
|
|
1007
|
+
// ── arch_facts gate (architectural violation scan) ────────────────────────
|
|
1008
|
+
//
|
|
1009
|
+
// Runs after cognition + discovery-binding pass, for Edit/Write/NotebookEdit.
|
|
1010
|
+
// Bash is skipped: architectural violations live in the code being written,
|
|
1011
|
+
// not in shell commands — checking Bash commands would produce false positives
|
|
1012
|
+
// on grep/find invocations that mention forbidden strings without introducing them.
|
|
1013
|
+
//
|
|
1014
|
+
// Content inspected: new_string (Edit), content (Write), source (NotebookEdit).
|
|
1015
|
+
// Fail-open: unreachable mizan → allow. Client surface → skip.
|
|
1016
|
+
if (['Edit', 'Write', 'NotebookEdit'].includes(toolName)) {
|
|
1017
|
+
// Extract the actual content being written — this is what mizan should scan
|
|
1018
|
+
// for architectural patterns, not the file path or surrounding metadata.
|
|
1019
|
+
const contentToScan =
|
|
1020
|
+
toolInput.new_string ?? // Edit: the replacement text
|
|
1021
|
+
toolInput.content ?? // Write: the full file content
|
|
1022
|
+
toolInput.source ?? // NotebookEdit: cell source
|
|
1023
|
+
'';
|
|
1024
|
+
// Also include file_path for context so mizan pattern-matching can be
|
|
1025
|
+
// path-scoped (e.g. R9 only fires for telemetry/cron paths).
|
|
1026
|
+
const scanPayload = `// file: ${filePath}\n${contentToScan}`;
|
|
1027
|
+
const archResult = await archFactsGate(scanPayload);
|
|
1028
|
+
if (!archResult.allow) {
|
|
1029
|
+
const violationText = (archResult.violations || [])
|
|
1030
|
+
.map((v) => ` • [${v.rule ?? v.id ?? 'arch'}] ${v.description ?? v.message ?? JSON.stringify(v)}`)
|
|
1031
|
+
.join('\n');
|
|
1032
|
+
const archReason = `Aria arch_facts gate: architectural violation(s) detected in ${toolName} diff for ${filePath || '(no path)'}.
|
|
1033
|
+
|
|
1034
|
+
${violationText}
|
|
1035
|
+
|
|
1036
|
+
These patterns are forbidden by mizan arch_facts rules (mizan.yaml R7–R11). Fix the structural issue before re-issuing the tool call — the gate does not have a bypass for architectural violations.
|
|
1037
|
+
|
|
1038
|
+
Rule references:
|
|
1039
|
+
R7 no_sidecar_in_main_container — bolt-in fork pattern (project_aria_soul_systemd_migration.md)
|
|
1040
|
+
R8 no_silent_fallback_default — || 'unknown'/'default'/'fallback' masks config failures
|
|
1041
|
+
R9 no_timeout_based_retry — setTimeout+abort in telemetry/chat/cron paths (no-timeouts doctrine)
|
|
1042
|
+
R10 no_kubectl_apply_for_image_drift — kubectl apply on aria-soul-stateful (project_forge_psi_oom_cascade.md)
|
|
1043
|
+
R11 no_console_log_secrets — console.log with TOKEN/PASSWORD/SECRET/API_KEY/JWT/BEARER`;
|
|
1044
|
+
audit(`block-arch-facts ${toolName.toLowerCase()} path=${filePath}`, `violations=${archResult.violations?.length ?? 0}`);
|
|
1045
|
+
console.log(JSON.stringify({ decision: 'block', reason: archReason }));
|
|
1046
|
+
process.exit(2);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
876
1050
|
// Non-trivial action with cognition (inline for Bash, transcript for
|
|
877
1051
|
// Edit/Write/NotebookEdit) — passes cognition gate. Now check Aria-binding.
|
|
878
1052
|
|
|
@@ -896,6 +1070,32 @@ No env-var disable path — gates are unconditional from the gated process per H
|
|
|
896
1070
|
// ReferenceError. No new bypass entries can be created (pre-existing defect
|
|
897
1071
|
// fixed inline per atomic-discovery-rule).
|
|
898
1072
|
const bindingBypassReason = null;
|
|
1073
|
+
|
|
1074
|
+
// Defect #4 fix — Consult-Aria unconditionally allowed (doctrine #50).
|
|
1075
|
+
//
|
|
1076
|
+
// Aria-as-commander session pattern (HARNESS_ARIA_AS_COMMANDER_CONTRACT.md
|
|
1077
|
+
// doctrine #50) makes per-turn consult mandatory. A plan cannot forbid the
|
|
1078
|
+
// consult mechanism that issues plans — that would be an unbootstrappable
|
|
1079
|
+
// circular lock. Previously plan p1 forbade bash_other → consult curl was
|
|
1080
|
+
// classified as bash_other → blocked. Now: any Bash command that is a consult
|
|
1081
|
+
// to a known Aria/harness endpoint passes UNCONDITIONALLY past ALL binding
|
|
1082
|
+
// checks regardless of allowedActions or forbiddenActions in the active phase.
|
|
1083
|
+
// Cognition gate still applies (it ran above and passed to reach this point).
|
|
1084
|
+
//
|
|
1085
|
+
// Covered endpoints (hardcoded carve-out):
|
|
1086
|
+
// curl http(s)://aria-soul<anything>
|
|
1087
|
+
// curl http(s)://aria-telemetry<anything>
|
|
1088
|
+
// curl http(s)://localhost:30080/(chat|api/aria/speak|api/harness/codex|
|
|
1089
|
+
// api/harness/verify-claim|v1/doctrine|v1/mizan)
|
|
1090
|
+
const ARIA_CONSULT_CURL_RX = /curl\s+['"]?https?:\/\/(?:aria-soul[^\s'"]*|aria-telemetry[^\s'"]*|localhost:30080\/(?:chat|api\/aria\/speak|api\/harness\/(?:codex|verify-claim|delegate|army|plan)|v1\/(?:doctrine|mizan))[^\s'"]*)/i;
|
|
1091
|
+
const __isUnconditionalConsult = toolName === 'Bash' && ARIA_CONSULT_CURL_RX.test(cmd);
|
|
1092
|
+
if (__isUnconditionalConsult) {
|
|
1093
|
+
bindingAuditAppend({ event: 'allow_unconditional_consult', sessionId, toolName, cmdPreview, reason: 'doctrine#50-aria-as-commander-consult-carveout' });
|
|
1094
|
+
audit(`allow-unconditional-consult lenses=${lensCount}`, cmdPreview);
|
|
1095
|
+
pushDecision('allow', 'unconditional consult carve-out (doctrine #50)');
|
|
1096
|
+
process.exit(0);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
899
1099
|
const __bindingActionClassification = (BINDING_ENABLED && !bindingBypassReason)
|
|
900
1100
|
? classifyToolForBinding(toolName, cmd, filePath)
|
|
901
1101
|
: null;
|
|
@@ -952,7 +1152,7 @@ What Claude must do: emit [PLAN_BLOCKER reason="<concrete observation>" suggeste
|
|
|
952
1152
|
const phase = phaseInfo.phase;
|
|
953
1153
|
|
|
954
1154
|
// Forbidden takes precedence
|
|
955
|
-
const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p));
|
|
1155
|
+
const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p, target));
|
|
956
1156
|
if (forbidden) {
|
|
957
1157
|
bindingAuditAppend({ event: 'block_forbidden_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target, matchedRule: forbidden });
|
|
958
1158
|
const reason = `Aria binding gate: action "${action}" on target "${target}" matches forbidden pattern "${forbidden}" for current phase ${phase.id} ("${phase.summary}") of plan ${plan.planId}.
|
|
@@ -966,7 +1166,7 @@ Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [P
|
|
|
966
1166
|
process.exit(2);
|
|
967
1167
|
}
|
|
968
1168
|
|
|
969
|
-
const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p));
|
|
1169
|
+
const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p, target));
|
|
970
1170
|
if (!allowed) {
|
|
971
1171
|
bindingAuditAppend({ event: 'block_action_not_in_allowed_list', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
|
|
972
1172
|
const reason = `Aria binding gate: action "${action}" on target "${target}" is NOT in allowedActions for current phase ${phase.id} of plan ${plan.planId}.
|