@cratylus/canon 0.8.0 → 0.10.0
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 +6 -4
- package/dist/agents/architect.js +2 -2
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/assayer.js +14 -10
- package/dist/agents/assayer.js.map +1 -1
- package/dist/agents/implementer.js +7 -3
- package/dist/agents/implementer.js.map +1 -1
- package/dist/agents/integrator.d.ts +2 -0
- package/dist/agents/integrator.js +21 -0
- package/dist/agents/integrator.js.map +1 -0
- package/dist/agents/kino.js +5 -4
- package/dist/agents/kino.js.map +1 -1
- package/dist/agents/mav.js +20 -37
- package/dist/agents/mav.js.map +1 -1
- package/dist/agents/nico.js +13 -31
- package/dist/agents/nico.js.map +1 -1
- package/dist/agents/planner.js +5 -5
- package/dist/agents/planner.js.map +1 -1
- package/dist/dimensions/actions/delegation.js +6 -1
- package/dist/dimensions/actions/delegation.js.map +1 -1
- package/dist/dimensions/engineering-principles/cold-decode-oracle.js +1 -1
- package/dist/dimensions/engineering-principles/cold-decode-oracle.js.map +1 -1
- package/dist/dimensions/engineering-principles/trust-but-verify.js +1 -1
- package/dist/dimensions/engineering-principles/trust-but-verify.js.map +1 -1
- package/dist/dimensions/objective/conceptual-integrity.d.ts +2 -0
- package/dist/dimensions/objective/conceptual-integrity.js +2 -0
- package/dist/dimensions/objective/conceptual-integrity.js.map +1 -0
- package/dist/dimensions/role/architect.js +6 -3
- package/dist/dimensions/role/architect.js.map +1 -1
- package/dist/dimensions/role/assayer.js +32 -13
- package/dist/dimensions/role/assayer.js.map +1 -1
- package/dist/dimensions/role/implementer.js +28 -4
- package/dist/dimensions/role/implementer.js.map +1 -1
- package/dist/dimensions/role/integrator.d.ts +2 -0
- package/dist/dimensions/role/integrator.js +36 -0
- package/dist/dimensions/role/integrator.js.map +1 -0
- package/dist/dimensions/role/planner.js +19 -7
- package/dist/dimensions/role/planner.js.map +1 -1
- package/dist/genus/prime-principle.d.ts +10 -0
- package/dist/genus/prime-principle.js +16 -0
- package/dist/genus/prime-principle.js.map +1 -0
- package/dist/hooks/deploy-drift-notice.js +9 -9
- package/dist/hooks/deploy-drift-notice.js.map +1 -1
- package/dist/hooks/purview-guardrail.js +154 -62
- package/dist/hooks/purview-guardrail.js.map +1 -1
- package/dist/hooks/stance-guardrail-pre.js +105 -42
- package/dist/hooks/stance-guardrail-pre.js.map +1 -1
- package/dist/hooks/stance-guardrail.js +161 -57
- package/dist/hooks/stance-guardrail.js.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +3 -2
- package/dist/manifest.js +6 -6
- package/dist/manifest.js.map +1 -1
- package/dist/roles/architect.js +27 -22
- package/dist/roles/architect.js.map +1 -1
- package/dist/roles/assayer.js +11 -7
- package/dist/roles/assayer.js.map +1 -1
- package/dist/roles/hold.js +1 -1
- package/dist/roles/hold.js.map +1 -1
- package/dist/roles/implementer.js +16 -5
- package/dist/roles/implementer.js.map +1 -1
- package/dist/roles/integrator.d.ts +2 -0
- package/dist/roles/integrator.js +51 -0
- package/dist/roles/integrator.js.map +1 -0
- package/dist/roles/planner.js +6 -5
- package/dist/roles/planner.js.map +1 -1
- package/dist/skills/create-skill/skill.js +2 -0
- package/dist/skills/create-skill/skill.js.map +1 -1
- package/dist/skills/deliver/skill.js +87 -129
- package/dist/skills/deliver/skill.js.map +1 -1
- package/dist/skills/design/skill.js +20 -10
- package/dist/skills/design/skill.js.map +1 -1
- package/dist/skills/exemplify/skill.js +2 -0
- package/dist/skills/exemplify/skill.js.map +1 -1
- package/dist/skills/formalize/skill.js +2 -0
- package/dist/skills/formalize/skill.js.map +1 -1
- package/dist/skills/introspect/skill.js +2 -0
- package/dist/skills/introspect/skill.js.map +1 -1
- package/dist/skills/materialize/skill.js +2 -0
- package/dist/skills/materialize/skill.js.map +1 -1
- package/dist/skills/plan/skill.js +35 -37
- package/dist/skills/plan/skill.js.map +1 -1
- package/dist/skills/probe/skill.js +2 -0
- package/dist/skills/probe/skill.js.map +1 -1
- package/dist/skills/signify/skill.js +2 -0
- package/dist/skills/signify/skill.js.map +1 -1
- package/package.json +4 -4
- package/dist/genus/founding-doctrine.d.ts +0 -7
- package/dist/genus/founding-doctrine.js +0 -13
- package/dist/genus/founding-doctrine.js.map +0 -1
|
@@ -23,16 +23,22 @@
|
|
|
23
23
|
// every agent in the corpus composed the fragment it claimed to enforce.
|
|
24
24
|
//
|
|
25
25
|
// WHAT IT BLOCKS is what the arrow excludes, and nothing else:
|
|
26
|
-
// · a dispatch
|
|
27
|
-
// arrow does not write spec — the architect skipping the
|
|
26
|
+
// · a dispatch carrying a spec the dispatcher wrote for a unit of the loop, from a holder
|
|
27
|
+
// whose arrow does not write spec — the architect skipping the planner, who alone turns
|
|
28
|
+
// a shard into units;
|
|
28
29
|
// · a write to the substrate by a holder whose arrow does not write artifact;
|
|
29
|
-
// · a dispatch whose prompt is the operator's literal words rather than a
|
|
30
|
-
// — the same dispatch-echo the stance guard catches at turn
|
|
31
|
-
// because at this moment the contract that convicts it is the ROLE's.
|
|
32
|
-
// WHAT IT PASSES: a dispatch to a planner, a dispatch to an
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// · a dispatch whose prompt is the operator's literal words rather than a routed name or
|
|
31
|
+
// the holder's own request — the same dispatch-echo the stance guard catches at turn
|
|
32
|
+
// end, refused here because at this moment the contract that convicts it is the ROLE's.
|
|
33
|
+
// WHAT IT PASSES: a dispatch to a planner, a dispatch routing a unit name to an implementer,
|
|
34
|
+
// a dispatch to an assayer, a dispatch to the integrator, a dispatch outside the loop of
|
|
35
|
+
// realizing the design — a comparison, an audit, a question — in the holder's own words,
|
|
36
|
+
// where the holder's own contract admits it, READING anything, and writing whatever the arrow
|
|
37
|
+
// reserves. The operator saw an ad hoc comparison refused twice because a self-worded prompt
|
|
38
|
+
// was read as a spec; a request is not a spec, the routing discipline binds units of the loop
|
|
39
|
+
// and not everything a holder says to a delegate. Reading is never a block — the architect's
|
|
40
|
+
// contract makes an artifact read a descent, but a read is cheap to undo and a gate that
|
|
41
|
+
// fires on one would wedge every legitimate orientation.
|
|
36
42
|
//
|
|
37
43
|
// FAIL-OPEN, EVIDENCE-CHECKED, RE-ENTRY-CAPPED — all three inherited from the sibling
|
|
38
44
|
// pre-hook, which is also the structural model for the worker. A block whose cited span
|
|
@@ -40,8 +46,10 @@
|
|
|
40
46
|
// error than a missed one.
|
|
41
47
|
export const purviewGuardrail = {
|
|
42
48
|
id: 'purview-guardrail',
|
|
43
|
-
residue: 'structural-refusal ↾ mid-turn act ∉ role-arrow · deny-before-fire ⟨descent ⟨write(artifact) ∉ writes⟩ · skipped-rung ⟨dispatch(
|
|
49
|
+
residue: 'structural-refusal ↾ mid-turn act ∉ role-arrow · deny-before-fire ⟨descent ⟨write(artifact) ∉ writes⟩ · skipped-rung ⟨dispatch(spec the dispatcher wrote for a unit of the loop) ∄ planner⟩ · dispatch-echo ⟨literal-words ≠ routed-name ∨ own-words⟩⟩ · pass ⟨read · act ∈ reserves · dispatch ↦ plan ∨ implement(unit-name) ∨ assay ∨ integrate ∨ ad-hoc(own-words ∉ loop ↾ contract admits)⟩ · law = the HOLDER-projected role-section ⟨quoted verbatim · ¬ authored-opinion⟩ · shared judge-backend ⟨sibling⟩ · fail-open ∧ evidence-checked ∧ re-entry-capped',
|
|
44
50
|
substrate: 'harness',
|
|
51
|
+
// Bound by holding a role at all: the arrow it judges against is the role's own.
|
|
52
|
+
binds: { dimension: 'role' },
|
|
45
53
|
order: 1,
|
|
46
54
|
events: ['subagent.dispatch.pre', 'tool.use.pre'],
|
|
47
55
|
entry: 'purview-guardrail.sh',
|
|
@@ -69,19 +77,82 @@ export const purviewGuardrail = {
|
|
|
69
77
|
# - CONTRACT-ENROLLED, one step further. A persona whose Target carries no \`## Role\`
|
|
70
78
|
# section has no arrow to be outside of, so it is allowed. A gate cannot convict
|
|
71
79
|
# against a law that was never declared.
|
|
72
|
-
# - FAILS OPEN. Any error (no jq, no Target, judge failure
|
|
80
|
+
# - FAILS OPEN, NEVER SILENTLY. Any error (no jq, no input, no Target, judge failure,
|
|
81
|
+
# unparseable verdict, unexpected error) -> exit 0, and the call goes through WITH A NOTICE
|
|
82
|
+
# the operator reads (\`say\`: Claude Code's JSON \`systemMessage\`, omp's relayed line) naming
|
|
83
|
+
# this guard and why it could not judge. Silence is reserved for "judged, pass", "not
|
|
84
|
+
# enrolled" and "no arrow declared".
|
|
73
85
|
# - EVIDENCE-CHECKED. A BLOCK must quote a span that is literally present in the
|
|
74
|
-
# payload; a citation that is not there is a fabricated block and is DISCARDED
|
|
75
|
-
#
|
|
86
|
+
# payload; a citation that is not there is a fabricated block and is DISCARDED — and the
|
|
87
|
+
# discard is said, since no verdict then stands.
|
|
88
|
+
# - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice. The second,
|
|
89
|
+
# identical call goes through unjudged, and says so.
|
|
76
90
|
#
|
|
77
91
|
# INPUT : Claude Code PreToolUse hook JSON on stdin.
|
|
78
92
|
# OUTPUT : on a purview breach -> a deny decision on stdout + exit 0.
|
|
79
|
-
#
|
|
93
|
+
# judged, pass -> no stdout + exit 0.
|
|
94
|
+
# let through with no verdict -> the notice \`say\` prints + exit 0.
|
|
80
95
|
#
|
|
81
|
-
# POSIX sh. Depends on: jq. Missing jq ->
|
|
96
|
+
# POSIX sh. Depends on: jq. Missing jq -> the guard says so and allows the call.
|
|
82
97
|
|
|
83
98
|
set -eu
|
|
84
99
|
|
|
100
|
+
# WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
|
|
101
|
+
# command: the first thing that can be missing is \`jq\`, and a worker run with a PATH holding
|
|
102
|
+
# little beyond \`sh\` and \`cat\` must still say it did not judge, so \`esc\`, \`say\` and \`open\`
|
|
103
|
+
# are builtins only and nothing external runs above the jq check.
|
|
104
|
+
#
|
|
105
|
+
# Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
|
|
106
|
+
# exit 0. omp's extension bridge reads the worker's plain stdout, and would relay a JSON object
|
|
107
|
+
# as raw JSON — hence one form per harness, chosen at projection by the harness's own name.
|
|
108
|
+
esc() {
|
|
109
|
+
_in="$1"
|
|
110
|
+
_out=""
|
|
111
|
+
_nl="$(printf '\\nx')"
|
|
112
|
+
_nl="\${_nl%x}"
|
|
113
|
+
_tab="$(printf '\\t')"
|
|
114
|
+
_cr="$(printf '\\r')"
|
|
115
|
+
while [ -n "$_in" ]; do
|
|
116
|
+
_rest="\${_in#?}"
|
|
117
|
+
_c="\${_in%"$_rest"}"
|
|
118
|
+
case "$_c" in
|
|
119
|
+
'\\') _c='\\\\' ;;
|
|
120
|
+
'"') _c='\\"' ;;
|
|
121
|
+
"$_nl") _c='\\n' ;;
|
|
122
|
+
"$_tab") _c='\\t' ;;
|
|
123
|
+
"$_cr") _c='\\r' ;;
|
|
124
|
+
esac
|
|
125
|
+
_out="$_out$_c"
|
|
126
|
+
_in="$_rest"
|
|
127
|
+
done
|
|
128
|
+
printf '%s' "$_out"
|
|
129
|
+
}
|
|
130
|
+
say() {
|
|
131
|
+
case "{{fact:harness-name}}" in
|
|
132
|
+
claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
|
|
133
|
+
*) printf '%s\\n' "$1" ;;
|
|
134
|
+
esac
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
allow() { exit 0; }
|
|
138
|
+
|
|
139
|
+
# A verdict and a failure are different facts and silence carries only one of them: \`allow\`
|
|
140
|
+
# means "judged, pass" or "not this guard's call", \`open\` means "could not judge" — the call
|
|
141
|
+
# still proceeds, and the operator is told.
|
|
142
|
+
open() {
|
|
143
|
+
[ -z "\${LOG:-}" ] || printf '%s\\n' "open: $1 tool=\${tool_name:-?} agent=\${agent_type:-?}" >> "$LOG" 2>/dev/null || true
|
|
144
|
+
say "PURVIEW GUARDRAIL — DARK: $1. This call was NOT judged; the absence of a deny is an absence of a verdict, not a clean one."
|
|
145
|
+
exit 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# A PreToolUse hook must never break a session. An unexpected error (a nonzero status reaching
|
|
149
|
+
# the trap) is let through AND SAID; every deliberate \`exit 0\` arrives with status 0.
|
|
150
|
+
trap 'rc=$?; [ "$rc" -eq 0 ] || open "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
|
|
151
|
+
|
|
152
|
+
input="$(cat)"
|
|
153
|
+
[ -n "$input" ] || open "the hook received no input"
|
|
154
|
+
command -v jq >/dev/null 2>&1 || open "jq is not installed, so the hook payload cannot be read"
|
|
155
|
+
|
|
85
156
|
# The sibling Stop hook's deployed dir owns the SHARED judge backend, resolved from
|
|
86
157
|
# this script's own location exactly as the pre-hook resolves it — every hooks root
|
|
87
158
|
# holds the hook dirs as siblings, so this derivation is true at every site this file
|
|
@@ -94,33 +165,32 @@ RUBRIC="\${PURVIEW_RUBRIC:-$NEUTRAL_ROOT/purview-guardrail/purview-judge-prompt.
|
|
|
94
165
|
JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $JUDGE_DIR/stance-judge.sh}"
|
|
95
166
|
LOG="\${PURVIEW_GUARD_LOG:-$SELF_DIR/misses.log}"
|
|
96
167
|
|
|
97
|
-
trap 'exit 0' EXIT
|
|
98
|
-
|
|
99
|
-
allow() { exit 0; }
|
|
100
|
-
note() { printf '%s\\n' "$1" >> "$LOG" 2>/dev/null || true; }
|
|
101
|
-
|
|
102
|
-
input="$(cat)"
|
|
103
|
-
[ -n "$input" ] || allow
|
|
104
|
-
command -v jq >/dev/null 2>&1 || allow
|
|
105
|
-
|
|
106
168
|
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
107
169
|
[ -n "$cwd" ] && cd "$cwd" 2>/dev/null || true
|
|
108
170
|
|
|
109
171
|
# --- scope gate: the persona's OWN stance manifest -------------------------------------------
|
|
172
|
+
# The scope is the one the harness hands over: \`stance_scope\` where a dispatcher sits in the
|
|
173
|
+
# persona's own scope (omp), else the persona directory of the agent the payload NAMES
|
|
174
|
+
# (\`agent_type\`, Claude Code — none on a bare session) under this harness's home, one hop above
|
|
175
|
+
# the hooks root. Presence of the manifest is enrollment; no agent list lives here.
|
|
110
176
|
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
111
|
-
[ -
|
|
112
|
-
|
|
177
|
+
if [ -z "$stance_scope" ]; then
|
|
178
|
+
named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
|
|
179
|
+
case "$named" in '' | */* | . | ..) allow ;; esac
|
|
180
|
+
stance_scope="$(dirname -- "$HOOKS_ROOT")/{{fact:harness-persona-root}}/$named"
|
|
181
|
+
fi
|
|
182
|
+
manifest="$stance_scope/{{fact:stance-manifest}}"
|
|
113
183
|
[ -f "$manifest" ] || allow
|
|
114
184
|
agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
|
|
115
|
-
[ -n "$agent_type" ] ||
|
|
185
|
+
[ -n "$agent_type" ] || open "the stance manifest at $manifest names no agent, so there is no declared contract to judge against"
|
|
116
186
|
|
|
117
187
|
# --- the LAW: this persona's own projected role contract -------------------------------------
|
|
118
|
-
# A persona scope is \`<root>/
|
|
119
|
-
# from is
|
|
120
|
-
# shape of derivation the judge path above uses. Overridable for test rigs.
|
|
188
|
+
# A persona scope is \`<root>/personas/<name>\` (claude) or \`<root>/agent/personas/<name>\` (omp), and
|
|
189
|
+
# the Target it was projected from is \`agents/<name>.md\` beside \`personas/\` — two levels out and
|
|
190
|
+
# back down, the same shape of derivation the judge path above uses. Overridable for test rigs.
|
|
121
191
|
AGENT_ROOT="$(dirname -- "$(dirname -- "$stance_scope")")"
|
|
122
192
|
AGENT_MD="\${PURVIEW_AGENT_MD:-$AGENT_ROOT/agents/$agent_type.md}"
|
|
123
|
-
[ -f "$AGENT_MD" ] ||
|
|
193
|
+
[ -f "$AGENT_MD" ] || open "the agent's projected Target is not at $AGENT_MD, so its declared contract cannot be read"
|
|
124
194
|
contract="$(awk '/^## Role$/{f=1;next} /^## /{f=0} f' "$AGENT_MD" 2>/dev/null || true)"
|
|
125
195
|
# A contract that is a BARE TOKEN states no arrow, so there is nothing to be outside
|
|
126
196
|
# of. Only a multi-line contract is scoreable, and saying so here is what keeps the
|
|
@@ -129,13 +199,13 @@ contract="$(awk '/^## Role$/{f=1;next} /^## /{f=0} f' "$AGENT_MD" 2>/dev/null ||
|
|
|
129
199
|
|
|
130
200
|
# --- extract the judged payload, branched by act ---------------------------------------------
|
|
131
201
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
132
|
-
[ -n "$tool_name" ] ||
|
|
202
|
+
[ -n "$tool_name" ] || open "the payload names no tool, so there is no call to judge"
|
|
133
203
|
|
|
134
204
|
case "$tool_name" in
|
|
135
205
|
Agent|SendMessage|Task)
|
|
136
206
|
body="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.message // .tool_input.description // ""' 2>/dev/null || true)"
|
|
137
207
|
target="$(printf '%s' "$input" | jq -r '.tool_input.subagent_type // .tool_input.agent // .tool_input.name // ""' 2>/dev/null || true)"
|
|
138
|
-
act="DISPATCH to \\\`\${target:-unnamed}\\\` (codomain:
|
|
208
|
+
act="DISPATCH to \\\`\${target:-unnamed}\\\` (codomain: route when it carries a unit name and address only, spec when it carries instructions the dispatcher wrote for a unit of the loop, request when it carries the dispatcher's own words outside the loop)"
|
|
139
209
|
;;
|
|
140
210
|
Write|Edit|MultiEdit|NotebookEdit)
|
|
141
211
|
body="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .tool_input.notebook_path // ""' 2>/dev/null || true)"
|
|
@@ -160,28 +230,32 @@ $body"
|
|
|
160
230
|
session_id="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
|
|
161
231
|
sig="$(printf '%s' "$input" | jq -c '.tool_input' 2>/dev/null | cksum | cut -d' ' -f1 2>/dev/null || echo 0)"
|
|
162
232
|
seen="\${TMPDIR:-/tmp}/.purview-pre-$session_id-$sig"
|
|
163
|
-
[ -f "$seen" ] &&
|
|
233
|
+
[ -f "$seen" ] && open "this exact call was already denied once, so the re-entry cap lets it through unjudged"
|
|
164
234
|
|
|
165
235
|
# --- judge (SHARED backend, OWN rubric) -----------------------------------------------------
|
|
166
|
-
|
|
236
|
+
# THE SAME SEAM, UNDER THE SAME NAMES, as the two stance workers: a host that holds a model
|
|
237
|
+
# runs this worker twice around a judgment it makes itself (\`STANCE_EMIT_PAYLOAD\` to receive
|
|
238
|
+
# the payload and rubric, \`STANCE_VERDICT_FILE\` naming the answer). This one read
|
|
239
|
+
# \`PURVIEW_\`-prefixed names of its own, so the omp bridge — which speaks the shared names —
|
|
240
|
+
# got no payload back and purview judged nothing there, on every call, while reading as a
|
|
241
|
+
# guard that had found nothing.
|
|
242
|
+
if [ -n "\${STANCE_EMIT_PAYLOAD:-}" ]; then
|
|
167
243
|
jq -cn --arg r "$RUBRIC" --arg p "$payload" '{rubric:$r, payload:$p}'
|
|
168
244
|
exit 0
|
|
169
245
|
fi
|
|
170
|
-
if [ -n "\${
|
|
171
|
-
verdict="$(cat "\${
|
|
172
|
-
[ -n "$verdict" ] ||
|
|
173
|
-
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-empty tool=$tool_name agent=$agent_type"
|
|
174
|
-
allow
|
|
175
|
-
}
|
|
246
|
+
if [ -n "\${STANCE_VERDICT_FILE:-}" ]; then
|
|
247
|
+
verdict="$(cat "\${STANCE_VERDICT_FILE}" 2>/dev/null || true)"
|
|
248
|
+
[ -n "$verdict" ] || open "the judge did not answer"
|
|
176
249
|
else
|
|
177
|
-
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" ||
|
|
178
|
-
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-fail tool=$tool_name agent=$agent_type"
|
|
179
|
-
allow
|
|
180
|
-
}
|
|
250
|
+
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || open "the judge did not answer"
|
|
181
251
|
fi
|
|
182
252
|
|
|
183
|
-
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
|
|
184
|
-
|
|
253
|
+
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
|
|
254
|
+
case "$decision" in
|
|
255
|
+
PASS) allow ;; # judged, pass: the one silent verdict
|
|
256
|
+
BLOCK) ;;
|
|
257
|
+
*) open "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
|
|
258
|
+
esac
|
|
185
259
|
|
|
186
260
|
reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
|
|
187
261
|
[ -n "$reason" ] || reason="This act falls outside the arrow your role contract declares."
|
|
@@ -193,8 +267,9 @@ reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head
|
|
|
193
267
|
cited="$(printf '%s\\n' "$verdict" | sed -n 's/^SPAN:[[:space:]]*//p' | head -1)"
|
|
194
268
|
if [ -n "$cited" ]; then
|
|
195
269
|
printf '%s' "$payload" | grep -qF -- "$cited" || {
|
|
196
|
-
|
|
197
|
-
|
|
270
|
+
[ -z "\${LOG:-}" ] || printf '%s\\n' "block-discarded: span=$cited tool=$tool_name agent=$agent_type" >> "$LOG" 2>/dev/null || true
|
|
271
|
+
say "PURVIEW GUARDRAIL — BLOCK DISCARDED: the judge blocked this call but cited a span that is not in it (fabricated): $cited. No verdict stands; this call was NOT judged clean."
|
|
272
|
+
exit 0
|
|
198
273
|
}
|
|
199
274
|
fi
|
|
200
275
|
|
|
@@ -202,10 +277,13 @@ fi
|
|
|
202
277
|
|
|
203
278
|
feedback="PURVIEW GUARDRAIL — denied this $tool_name call: it falls outside the arrow your own \\
|
|
204
279
|
role contract declares. $reason Delegation is a THEOREM of that arrow, not an option: an act whose \\
|
|
205
|
-
domain or codomain lies outside it goes to the role that owns it — a decomposition
|
|
206
|
-
build to an implementer, an artifact reading to an assayer
|
|
207
|
-
|
|
208
|
-
|
|
280
|
+
domain or codomain lies outside it goes to the role that owns it — a decomposition of a shard to a \\
|
|
281
|
+
planner, a build to an implementer, an artifact reading to an assayer, a merge or a whole check to \\
|
|
282
|
+
the integrator. Hand a unit of the loop over by name and carry on with what the contract reserves. \\
|
|
283
|
+
(Legitimate and NOT blocked here: reading anything, dispatching to a planner, routing a unit name to \\
|
|
284
|
+
an implementer, an assayer or the integrator, dispatching a request outside the loop to whichever role \\
|
|
285
|
+
or agent fits it in your own rectified words where your contract admits it, and performing any act the \\
|
|
286
|
+
contract's own \\\`reserves\\\` clause names.)"
|
|
209
287
|
|
|
210
288
|
jq -cn --arg r "$feedback" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
|
|
211
289
|
exit 0
|
|
@@ -251,7 +329,11 @@ durative concept lattice — the design.
|
|
|
251
329
|
|
|
252
330
|
An act has a **domain** (the layer it consumes) and a **codomain** (the layer it produces):
|
|
253
331
|
|
|
254
|
-
- a DISPATCH
|
|
332
|
+
- a DISPATCH that carries a unit name and its address only produces a **route**; a DISPATCH that
|
|
333
|
+
carries instructions the dispatcher wrote for a unit of the loop the contract names produces a
|
|
334
|
+
**spec** — those instructions are the layer being written; a DISPATCH that carries a request in
|
|
335
|
+
the dispatcher's own words, outside that loop, produces neither: it is a **request**, and it is
|
|
336
|
+
in the arrow exactly when the contract says so;
|
|
255
337
|
- a WRITE to a source or config file produces an **artifact**;
|
|
256
338
|
- reading anything at all produces **nothing**, and is never a breach.
|
|
257
339
|
|
|
@@ -263,20 +345,30 @@ whole test, and it is mechanical.
|
|
|
263
345
|
- **A write whose codomain is \`artifact\` by a holder whose \`writes\` set excludes \`artifact\`.**
|
|
264
346
|
The contract usually names this: \`descent\`. The path in the payload is the evidence.
|
|
265
347
|
- **A dispatch whose codomain is \`spec\` by a holder whose \`writes\` set excludes \`spec\`.** This is
|
|
266
|
-
the skipped rung: a holder that writes only the design has handed a delegate
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
348
|
+
the skipped rung: a holder that writes only the design and its routes has handed a delegate
|
|
349
|
+
instructions it wrote itself for a unit of the loop — the prompt names a unit or a plan, or tells
|
|
350
|
+
the delegate what to build, change or verify in a unit's files — where only a planner turns a shard
|
|
351
|
+
into units. The prompt is the evidence.
|
|
352
|
+
- **A dispatch whose prompt is the operator's literal words** rather than a routed name or the
|
|
353
|
+
holder's own request. The contract that convicts this is the same clause: transcription is not
|
|
354
|
+
authorship. Evidence is the prompt reading as the operator's text relayed — addressed to the
|
|
355
|
+
holder, or in the operator's own voice — rather than as a request the holder rectified and worded
|
|
356
|
+
itself.
|
|
272
357
|
|
|
273
358
|
## What to PASS — and these are the majority
|
|
274
359
|
|
|
275
360
|
- **Any read.** Reading is outside the test by construction. Even where a contract calls
|
|
276
361
|
artifact-reading a descent, that is a standing discipline, not a mid-turn refusal.
|
|
277
362
|
- **A dispatch to a role the contract explicitly routes to.** Contracts name their delegations
|
|
278
|
-
(\`⟨C → spec⟩ ↦
|
|
279
|
-
|
|
363
|
+
(\`⟨C → spec⟩ ↦ planner\`, \`⟨spec → artifact⟩ ↦ implementer\`, \`⟨artifact → C⟩ ↦ assayer\`, the
|
|
364
|
+
integrator for gate, merge and record). A dispatch to a planner, a dispatch routing a unit name
|
|
365
|
+
to an implementer, a dispatch to an assayer and a dispatch to the integrator are the arrow
|
|
366
|
+
WORKING, whether the contract's \`writes\` names \`route\` or not.
|
|
367
|
+
- **A dispatch the contract admits in the holder's own words.** Where the contract states that a
|
|
368
|
+
request outside the loop — a comparison, an audit, a question — may go to any role or agent that
|
|
369
|
+
fits it, a dispatch carrying such a request in the holder's own words is the contract working,
|
|
370
|
+
whichever role it names. It writes no spec: it names no unit of the loop and hands no
|
|
371
|
+
instructions for building one. Do not read a request's being self-worded as a spec.
|
|
280
372
|
- **Any act the \`reserves\` clause names**, whatever it is.
|
|
281
373
|
- **A write by a holder whose \`writes\` set includes \`artifact\`.** Most agents in most corpora
|
|
282
374
|
build. Do not read a contract's other clauses as narrowing an arrow that plainly permits the act.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purview-guardrail.js","sourceRoot":"","sources":["../../src/hooks/purview-guardrail.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,oFAAoF;AACpF,mFAAmF;AACnF,kFAAkF;AAClF,qBAAqB;AACrB,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,mFAAmF;AACnF,wFAAwF;AACxF,EAAE;AACF,sFAAsF;AACtF,oFAAoF;AACpF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,+EAA+E;AAC/E,sFAAsF;AACtF,yEAAyE;AACzE,EAAE;AACF,+DAA+D;AAC/D,
|
|
1
|
+
{"version":3,"file":"purview-guardrail.js","sourceRoot":"","sources":["../../src/hooks/purview-guardrail.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,oFAAoF;AACpF,mFAAmF;AACnF,kFAAkF;AAClF,qBAAqB;AACrB,EAAE;AACF,iFAAiF;AACjF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,mFAAmF;AACnF,wFAAwF;AACxF,EAAE;AACF,sFAAsF;AACtF,oFAAoF;AACpF,uFAAuF;AACvF,uFAAuF;AACvF,uFAAuF;AACvF,+EAA+E;AAC/E,sFAAsF;AACtF,yEAAyE;AACzE,EAAE;AACF,+DAA+D;AAC/D,4FAA4F;AAC5F,4FAA4F;AAC5F,0BAA0B;AAC1B,gFAAgF;AAChF,2FAA2F;AAC3F,yFAAyF;AACzF,4FAA4F;AAC5F,6FAA6F;AAC7F,yFAAyF;AACzF,yFAAyF;AACzF,8FAA8F;AAC9F,6FAA6F;AAC7F,8FAA8F;AAC9F,6FAA6F;AAC7F,yFAAyF;AACzF,yDAAyD;AACzD,EAAE;AACF,sFAAsF;AACtF,wFAAwF;AACxF,mFAAmF;AACnF,2BAA2B;AAE3B,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,EAAE,EAAE,mBAAmB;IACvB,OAAO,EACL,oiBAAoiB;IACtiB,SAAS,EAAE,SAAS;IACpB,iFAAiF;IACjF,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;IAC5B,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,uBAAuB,EAAE,cAAc,CAAC;IACjD,KAAK,EAAE,sBAAsB;IAC7B,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,sBAAsB;YAChC,UAAU,EAAE,uDAAuD;YACnE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmOd;SACI;QACD;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,KAAK;YACjB,iFAAiF;YACjF,2EAA2E;YAC3E,6EAA6E;YAC7E,8EAA8E;YAC9E,6BAA6B;YAC7B,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwFd;SACI;KACF;CACF,CAAC"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { anchorOf } from '@cratylus/schema';
|
|
2
|
+
import { handoff } from '../dimensions/autonomy/handoff.js';
|
|
1
3
|
// stance-guardrail-pre — the BEFORE-THE-CALL twin of `stance-guardrail` (the turn-end
|
|
2
4
|
// cell). It STRUCTURALLY REFUSES, BEFORE the call fires, a mid-turn tool call that
|
|
3
5
|
// collapses out of the intent-driven-expert stance. Two acts, and the acts are what it
|
|
@@ -10,19 +12,23 @@
|
|
|
10
12
|
//
|
|
11
13
|
// IT BINDS TWO ACTS, NOT ONE EVENT PLUS A REGEX. This cell used to read
|
|
12
14
|
// `events: ['tool.use.pre']` + `matcher: 'AskUserQuestion|Agent|SendMessage'` — three
|
|
13
|
-
// claude tool names on a harness-agnostic shape, which
|
|
14
|
-
// without a word, so the hook there fired on EVERY tool
|
|
15
|
-
// own `*) allow ;;` branch kept it correct. The two acts
|
|
16
|
-
// the residue (`permission-menu` · `dispatch-echo`);
|
|
17
|
-
// to the adapter, which alone knows which of its
|
|
18
|
-
// computes its own ⟨native event, native
|
|
19
|
-
// but not narrow it now SAYS SO through
|
|
15
|
+
// claude tool names on a harness-agnostic shape, which an adapter that could not
|
|
16
|
+
// narrow by tool name dropped without a word, so the hook there fired on EVERY tool
|
|
17
|
+
// call and only the worker's own `*) allow ;;` branch kept it correct. The two acts
|
|
18
|
+
// below were already latent in the residue (`permission-menu` · `dispatch-echo`);
|
|
19
|
+
// naming them moves the narrowing to the adapter, which alone knows which of its
|
|
20
|
+
// tools performs the act. Each adapter computes its own ⟨native event, native
|
|
21
|
+
// selector⟩ pair, and one that can fire an act but not narrow it now SAYS SO through
|
|
22
|
+
// the projection's warnings.
|
|
20
23
|
// `test/hook-rule-boundary.test.ts` byte-locks the worker;
|
|
21
24
|
// `test/hook-act-selector.test.ts` holds the seam.
|
|
22
25
|
export const stanceGuardrailPre = {
|
|
23
26
|
id: 'stance-guardrail-pre',
|
|
24
27
|
residue: 'structural-refusal ↾ mid-turn tool-call · deny-before-fire ⟨intent-driven-expert-collapse⟩ ⟨permission-menu · dispatch-echo ⟨literal-transcription ∄ extracted-intent⟩⟩ · pass ⟨reserved · irreversible-outward-consent · substantive-dispatch · intent-ambiguity ↦ elicit⟩ · shared judge-backend ⟨sibling⟩ · loop-safe ⟨re-entry-cap : ¬deny identical twice⟩',
|
|
25
28
|
substrate: 'harness',
|
|
29
|
+
// Bound by the same composition as its turn-end twin: the stance it enforces is the
|
|
30
|
+
// one `handoff` declares.
|
|
31
|
+
binds: { dimension: 'autonomy', value: anchorOf(handoff) },
|
|
26
32
|
order: 1,
|
|
27
33
|
events: ['operator.consult.pre', 'subagent.dispatch.pre'],
|
|
28
34
|
entry: 'stance-guardrail-pre.sh',
|
|
@@ -49,22 +55,85 @@ export const stanceGuardrailPre = {
|
|
|
49
55
|
# - SCOPE-ENROLLED, exactly as its twin: a scope carrying a stance manifest is judged and
|
|
50
56
|
# every other scope is silent. No repo opt-in, no allowlist — the projection that places a
|
|
51
57
|
# persona also enrolls it, and declining the guard means declining the persona.
|
|
52
|
-
# - FAILS OPEN. Any error (no jq, judge failure,
|
|
58
|
+
# - FAILS OPEN, NEVER SILENTLY. Any error (no jq, no input, judge failure, unparseable verdict,
|
|
59
|
+
# unexpected error) -> exit 0 (allow the call), and the call goes through WITH A NOTICE the
|
|
60
|
+
# operator reads (\`say\`: Claude Code's JSON \`systemMessage\`, omp's relayed line) naming this
|
|
61
|
+
# guard and why it could not judge. Silence is reserved for "judged, pass" and "not enrolled".
|
|
53
62
|
# - LOOP-SAFE. A re-entry cap: never deny an identical tool_input twice (there is no
|
|
54
|
-
# stop_hook_active analog for PreToolUse), so it can never wedge a call.
|
|
55
|
-
#
|
|
63
|
+
# stop_hook_active analog for PreToolUse), so it can never wedge a call. The second,
|
|
64
|
+
# identical call goes through unjudged, and says so.
|
|
65
|
+
# - OBSERVABLE. Every notice is also a line in the miss log.
|
|
56
66
|
#
|
|
57
67
|
# INPUT : Claude Code PreToolUse hook JSON on stdin (tool_name, tool_input, agent_type
|
|
58
68
|
# [subagents], session_id, cwd, ...).
|
|
59
69
|
# OUTPUT : on collapse -> stdout
|
|
60
70
|
# {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
|
|
61
71
|
# "permissionDecisionReason":"..."}} + exit 0.
|
|
62
|
-
#
|
|
72
|
+
# judged, pass -> no stdout + exit 0 (allow the call).
|
|
73
|
+
# let through with no verdict -> the notice \`say\` prints + exit 0 (allow the call).
|
|
63
74
|
#
|
|
64
|
-
# POSIX sh. Depends on: jq. Missing jq ->
|
|
75
|
+
# POSIX sh. Depends on: jq. Missing jq -> the guard says so and allows the call.
|
|
65
76
|
|
|
66
77
|
set -eu
|
|
67
78
|
|
|
79
|
+
# WHAT AN OPERATOR READS WHEN THE GUARD CANNOT JUDGE, defined before anything that needs a
|
|
80
|
+
# command: the first thing that can be missing is \`jq\`, and a worker run with a PATH holding
|
|
81
|
+
# little beyond \`sh\` and \`cat\` must still say it did not judge, so \`esc\`, \`say\` and \`open\`
|
|
82
|
+
# are builtins only and nothing external runs above the jq check.
|
|
83
|
+
#
|
|
84
|
+
# Claude Code shows the operator a hook's JSON \`systemMessage\` and NOT its plain stdout on
|
|
85
|
+
# exit 0. omp's extension bridge reads the worker's plain stdout and relays a line naming this
|
|
86
|
+
# guard and DARK to the session, and would relay a JSON object as raw JSON — hence one form per
|
|
87
|
+
# harness, chosen at projection by the harness's own name.
|
|
88
|
+
esc() {
|
|
89
|
+
_in="$1"
|
|
90
|
+
_out=""
|
|
91
|
+
_nl="$(printf '\\nx')"
|
|
92
|
+
_nl="\${_nl%x}"
|
|
93
|
+
_tab="$(printf '\\t')"
|
|
94
|
+
_cr="$(printf '\\r')"
|
|
95
|
+
while [ -n "$_in" ]; do
|
|
96
|
+
_rest="\${_in#?}"
|
|
97
|
+
_c="\${_in%"$_rest"}"
|
|
98
|
+
case "$_c" in
|
|
99
|
+
'\\') _c='\\\\' ;;
|
|
100
|
+
'"') _c='\\"' ;;
|
|
101
|
+
"$_nl") _c='\\n' ;;
|
|
102
|
+
"$_tab") _c='\\t' ;;
|
|
103
|
+
"$_cr") _c='\\r' ;;
|
|
104
|
+
esac
|
|
105
|
+
_out="$_out$_c"
|
|
106
|
+
_in="$_rest"
|
|
107
|
+
done
|
|
108
|
+
printf '%s' "$_out"
|
|
109
|
+
}
|
|
110
|
+
say() {
|
|
111
|
+
case "{{fact:harness-name}}" in
|
|
112
|
+
claude) printf '{"systemMessage":"%s"}\\n' "$(esc "$1")" ;;
|
|
113
|
+
*) printf '%s\\n' "$1" ;;
|
|
114
|
+
esac
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
allow() { exit 0; } # emit nothing; the tool call proceeds.
|
|
118
|
+
|
|
119
|
+
# A verdict and a failure are different facts and silence carries only one of them: \`allow\`
|
|
120
|
+
# means "judged, pass" or "not this guard's call", \`open\` means "could not judge" — the call
|
|
121
|
+
# still proceeds, and the operator is told.
|
|
122
|
+
open() {
|
|
123
|
+
[ -z "\${LOG:-}" ] || printf '%s\\n' "open: $1 tool=\${tool_name:-?} agent=\${agent_type:-?}" >> "$LOG" 2>/dev/null || true
|
|
124
|
+
say "STANCE GUARDRAIL (pre) — DARK: $1. This call was NOT judged; the absence of a deny is an absence of a verdict, not a clean one."
|
|
125
|
+
exit 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# A PreToolUse hook must never break a session. An unexpected error (a nonzero status reaching
|
|
129
|
+
# the trap) is let through AND SAID; every deliberate \`exit 0\` arrives with status 0.
|
|
130
|
+
trap 'rc=$?; [ "$rc" -eq 0 ] || open "an unexpected error stopped it (exit status $rc)"; exit 0' EXIT
|
|
131
|
+
|
|
132
|
+
# --- read hook input ------------------------------------------------------------------------
|
|
133
|
+
input="$(cat)"
|
|
134
|
+
[ -n "$input" ] || open "the hook received no input"
|
|
135
|
+
command -v jq >/dev/null 2>&1 || open "jq is not installed, so the hook payload cannot be read"
|
|
136
|
+
|
|
68
137
|
# The sibling Stop hook's deployed dir owns the SHARED judge + rubric (deployed
|
|
69
138
|
# together, as siblings under the SAME hooks root).
|
|
70
139
|
#
|
|
@@ -87,17 +156,6 @@ RUBRIC="\${STANCE_RUBRIC:-$NEUTRAL_ROOT/stance-guardrail/stance-judge-prompt.md}
|
|
|
87
156
|
JUDGE_CMD="\${STANCE_JUDGE_CMD:-sh $JUDGE_DIR/stance-judge.sh}"
|
|
88
157
|
LOG="\${STANCE_GUARD_LOG:-$SELF_DIR/misses.log}"
|
|
89
158
|
|
|
90
|
-
# A PreToolUse hook must never break a session. Trap any unexpected error -> allow.
|
|
91
|
-
trap 'exit 0' EXIT
|
|
92
|
-
|
|
93
|
-
allow() { exit 0; } # emit nothing; the tool call proceeds.
|
|
94
|
-
note() { printf '%s\\n' "$1" >> "$LOG" 2>/dev/null || true; }
|
|
95
|
-
|
|
96
|
-
# --- read hook input ------------------------------------------------------------------------
|
|
97
|
-
input="$(cat)"
|
|
98
|
-
[ -n "$input" ] || allow
|
|
99
|
-
command -v jq >/dev/null 2>&1 || allow # no jq -> cannot parse -> fail open
|
|
100
|
-
|
|
101
159
|
# The per-repo opt-in is GONE, for the reason its twin states: it asked whether a guard may run in
|
|
102
160
|
# a DIRECTORY, and a stance belongs to the agent, not the checkout. \`cwd\` survives it — the worker
|
|
103
161
|
# still runs where the session runs.
|
|
@@ -108,16 +166,23 @@ cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
|
108
166
|
# The twin of the turn-end guard's gate, and inverted for the same reason: an allowlist here was
|
|
109
167
|
# a runtime self-filter over an enrollment the corpus already derives, and it had drifted the way
|
|
110
168
|
# such a list always drifts. Presence of a manifest in this persona's own scope IS enrollment.
|
|
111
|
-
# Absent → silence, never an error, so an unenrolled launch is untouched.
|
|
169
|
+
# Absent → silence, never an error, so an unenrolled launch is untouched. The scope is the one
|
|
170
|
+
# the harness hands over: \`stance_scope\` where a dispatcher sits in the persona's own scope (omp),
|
|
171
|
+
# else the persona directory of the agent the payload NAMES (\`agent_type\`, Claude Code — none on a
|
|
172
|
+
# bare session) under this harness's home, one hop above the hooks root. No agent list here.
|
|
112
173
|
stance_scope="$(printf '%s' "$input" | jq -r '.stance_scope // empty' 2>/dev/null || true)"
|
|
113
|
-
[ -
|
|
114
|
-
|
|
174
|
+
if [ -z "$stance_scope" ]; then
|
|
175
|
+
named="$(printf '%s' "$input" | jq -r '.agent_type // empty' 2>/dev/null || true)"
|
|
176
|
+
case "$named" in '' | */* | . | ..) allow ;; esac
|
|
177
|
+
stance_scope="$(dirname -- "$HOOKS_ROOT")/{{fact:harness-persona-root}}/$named"
|
|
178
|
+
fi
|
|
179
|
+
manifest="$stance_scope/{{fact:stance-manifest}}"
|
|
115
180
|
[ -f "$manifest" ] || allow
|
|
116
181
|
agent_type="$(jq -r '.agent // empty' "$manifest" 2>/dev/null || true)"
|
|
117
182
|
|
|
118
183
|
# --- extract the judged payload, branched by tool -------------------------------------------
|
|
119
184
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
120
|
-
[ -n "$tool_name" ] ||
|
|
185
|
+
[ -n "$tool_name" ] || open "the payload names no tool, so there is no call to judge"
|
|
121
186
|
|
|
122
187
|
case "$tool_name" in
|
|
123
188
|
AskUserQuestion)
|
|
@@ -134,10 +199,10 @@ case "$tool_name" in
|
|
|
134
199
|
body="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.message // .tool_input.description // ""' 2>/dev/null || true)"
|
|
135
200
|
payload="$tool_name dispatch (the delegate prompt/message): $body" ;;
|
|
136
201
|
*)
|
|
137
|
-
allow ;; # DEFENCE IN DEPTH
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
202
|
+
allow ;; # DEFENCE IN DEPTH: every supported adapter computes a selector from the
|
|
203
|
+
# act, so this never fires there. A harness that could fire the act but
|
|
204
|
+
# not narrow it would WARN in projection, and this branch is what would
|
|
205
|
+
# keep the guard off every other tool call.
|
|
141
206
|
esac
|
|
142
207
|
|
|
143
208
|
[ -n "\${body:-}" ] || allow # nothing judgeable -> allow
|
|
@@ -147,7 +212,7 @@ esac
|
|
|
147
212
|
session_id="$(printf '%s' "$input" | jq -r '.session_id // "nosession"' 2>/dev/null || echo nosession)"
|
|
148
213
|
sig="$(printf '%s' "$input" | jq -c '.tool_input' 2>/dev/null | cksum | cut -d' ' -f1 2>/dev/null || echo 0)"
|
|
149
214
|
seen="\${TMPDIR:-/tmp}/.stance-pre-$session_id-$sig"
|
|
150
|
-
[ -f "$seen" ] &&
|
|
215
|
+
[ -f "$seen" ] && open "this exact call was already denied once, so the re-entry cap lets it through unjudged"
|
|
151
216
|
|
|
152
217
|
# --- judge (SHARED backend + rubric) --------------------------------------------------------
|
|
153
218
|
# The same out-of-process seam the Stop worker carries, and for the same reason: a
|
|
@@ -159,19 +224,17 @@ if [ -n "\${STANCE_EMIT_PAYLOAD:-}" ]; then
|
|
|
159
224
|
fi
|
|
160
225
|
if [ -n "\${STANCE_VERDICT_FILE:-}" ]; then
|
|
161
226
|
verdict="$(cat "\${STANCE_VERDICT_FILE}" 2>/dev/null || true)"
|
|
162
|
-
[ -n "$verdict" ] ||
|
|
163
|
-
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-empty tool=$tool_name agent=\${agent_type:-?}"
|
|
164
|
-
allow
|
|
165
|
-
}
|
|
227
|
+
[ -n "$verdict" ] || open "the judge did not answer"
|
|
166
228
|
else
|
|
167
|
-
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" ||
|
|
168
|
-
note "$(date -u +%Y-%m-%dT%H:%M:%SZ) judge-fail tool=$tool_name agent=\${agent_type:-?}"
|
|
169
|
-
allow
|
|
170
|
-
}
|
|
229
|
+
verdict="$(printf '%s' "$payload" | $JUDGE_CMD "$RUBRIC" 2>/dev/null)" || open "the judge did not answer"
|
|
171
230
|
fi
|
|
172
231
|
|
|
173
|
-
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1)"
|
|
174
|
-
|
|
232
|
+
decision="$(printf '%s\\n' "$verdict" | sed -n 's/^VERDICT:[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//')"
|
|
233
|
+
case "$decision" in
|
|
234
|
+
PASS) allow ;; # judged, pass: the one silent verdict
|
|
235
|
+
BLOCK) ;;
|
|
236
|
+
*) open "the judge's verdict was unparseable (no VERDICT: PASS or VERDICT: BLOCK line)" ;;
|
|
237
|
+
esac
|
|
175
238
|
|
|
176
239
|
reason="$(printf '%s\\n' "$verdict" | sed -n 's/^REASON:[[:space:]]*//p' | head -1)"
|
|
177
240
|
[ -n "$reason" ] || reason="This tool call collapsed out of the intent-driven-expert stance."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stance-guardrail-pre.js","sourceRoot":"","sources":["../../src/hooks/stance-guardrail-pre.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAG5D,sFAAsF;AACtF,mFAAmF;AACnF,uFAAuF;AACvF,SAAS;AACT,wFAAwF;AACxF,6FAA6F;AAC7F,0FAA0F;AAC1F,uFAAuF;AACvF,qEAAqE;AACrE,EAAE;AACF,wEAAwE;AACxE,sFAAsF;AACtF,iFAAiF;AACjF,oFAAoF;AACpF,oFAAoF;AACpF,kFAAkF;AAClF,iFAAiF;AACjF,8EAA8E;AAC9E,qFAAqF;AACrF,6BAA6B;AAC7B,2DAA2D;AAC3D,mDAAmD;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAa;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,OAAO,EACL,iWAAiW;IACnW,SAAS,EAAE,SAAS;IACpB,oFAAoF;IACpF,0BAA0B;IAC1B,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE;IAC1D,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC,sBAAsB,EAAE,uBAAuB,CAAC;IACzD,KAAK,EAAE,yBAAyB;IAChC,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,OAAO,EAAE;QACP;YACE,QAAQ,EAAE,yBAAyB;YACnC,UAAU,EAAE,0DAA0D;YACtE,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqNd;SACI;KACF;CACF,CAAC"}
|