Executor-Tyrant-Framework Claude Opus 4.6 (1M context) commited on
Commit
9544d64
·
1 Parent(s): 792eca8

Add risk_authority.py — risk classification + authority map (Phase 4)

Browse files

Content-derived risk levels from spec analysis:
- routine: read-only, no sensitive files
- standard: write operations, cross-module scope
- sensitive: vendored files, auth/security, PolicyEngine
- command: constitutional files, Syl's protected data, new modules

Graph recall density bump: sparse recall = unfamiliar territory = risk +1.
Strategist can escalate risk (never downgrade).

Authority map: role → risk levels they can approve, retry budget,
escalation target, co-signer requirements. QB approves command-level.
Reviewer handles routine/standard. Razor co-signs sensitive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. Dockerfile +1 -0
  2. risk_authority.py +301 -0
Dockerfile CHANGED
@@ -72,6 +72,7 @@ COPY work_block_schema.py .
72
  COPY spec_executor.py .
73
  COPY persona_client.py .
74
  COPY report_evaluator.py .
 
75
 
76
  # Copy tools directory
77
  COPY tools/ ./tools/
 
72
  COPY spec_executor.py .
73
  COPY persona_client.py .
74
  COPY report_evaluator.py .
75
+ COPY risk_authority.py .
76
 
77
  # Copy tools directory
78
  COPY tools/ ./tools/
risk_authority.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- Changelog ----
2
+ # [2026-04-07] Josh + Claude — Risk classification + authority map (Phase 4)
3
+ # What: Content-derived risk levels, Strategist escalation-only override, authority chain
4
+ # Why: No single bottleneck — routine work flows through Reviewer, security through Razor,
5
+ # architecture through QB. Risk scales with what the spec touches.
6
+ # How: Content rules + Graph recall density → risk level → authority map → approval routing
7
+ # -------------------
8
+
9
+ """Risk Classification and Authority Map.
10
+
11
+ Determines who needs to approve a spec based on what it touches.
12
+ Content-derived baseline, Strategist can escalate (never downgrade).
13
+ Sparse Graph recall bumps risk up one level (unfamiliar territory = more oversight).
14
+ """
15
+
16
+ import logging
17
+ import re
18
+ from typing import Optional
19
+
20
+ logger = logging.getLogger("risk_authority")
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Risk levels (ordered)
25
+ # ---------------------------------------------------------------------------
26
+
27
+ RISK_LEVELS = ["routine", "standard", "sensitive", "command"]
28
+
29
+
30
+ def _risk_index(level: str) -> int:
31
+ """Get numeric index for a risk level. Higher = more risk."""
32
+ try:
33
+ return RISK_LEVELS.index(level)
34
+ except ValueError:
35
+ return 0
36
+
37
+
38
+ def _max_risk(a: str, b: str) -> str:
39
+ """Return the higher of two risk levels."""
40
+ return a if _risk_index(a) >= _risk_index(b) else b
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Content-based risk classification
45
+ # ---------------------------------------------------------------------------
46
+
47
+ # Patterns that indicate sensitive or command-level work
48
+ _SENSITIVE_PATTERNS = [
49
+ # Vendored files
50
+ r"ng_lite\.py",
51
+ r"ng_tract_bridge\.py",
52
+ r"ng_peer_bridge\.py",
53
+ r"ng_ecosystem\.py",
54
+ r"ng_autonomic\.py",
55
+ r"openclaw_adapter\.py",
56
+ r"ng_embed\.py",
57
+ r"ng_updater\.py",
58
+ # Auth/security files
59
+ r"\.env",
60
+ r"credentials",
61
+ r"api_key",
62
+ r"token",
63
+ r"secret",
64
+ # Public API / cross-module
65
+ r"openclaw_hook\.py",
66
+ r"policy_engine\.py",
67
+ # Protected ecosystem files
68
+ r"neuro_foundation\.py",
69
+ r"constitutional_embeddings",
70
+ ]
71
+
72
+ _COMMAND_PATTERNS = [
73
+ # New module creation
74
+ r"et_module\.json",
75
+ # Constitutional / Laws changes
76
+ r"laws\.md",
77
+ r"ARCHITECTURE\.md",
78
+ r"CLAUDE\.md",
79
+ # Syl's protected files
80
+ r"main\.msgpack",
81
+ r"vectors\.msgpack",
82
+ r"activation.*\.json",
83
+ ]
84
+
85
+
86
+ def _scan_spec_content(spec: dict) -> str:
87
+ """Derive risk level from spec contents.
88
+
89
+ Scans scope, step params, and snap_interface for sensitive patterns.
90
+ """
91
+ risk = "routine"
92
+
93
+ # Collect all text to scan
94
+ text_to_scan = []
95
+
96
+ # Block scope
97
+ text_to_scan.append(spec.get("block", {}).get("scope", ""))
98
+
99
+ # Snap interface paths
100
+ snap = spec.get("snap_interface", {})
101
+ for item in snap.get("inputs", []):
102
+ text_to_scan.append(item.get("path", ""))
103
+ for item in snap.get("outputs", []):
104
+ text_to_scan.append(item.get("path", ""))
105
+
106
+ # Step params — scan tool targets and commands
107
+ def _collect_step_text(steps):
108
+ for step in steps:
109
+ if step.get("type") == "action":
110
+ params = step.get("params", {})
111
+ text_to_scan.append(params.get("path", ""))
112
+ text_to_scan.append(params.get("command", ""))
113
+ text_to_scan.append(params.get("content", "")[:500])
114
+ text_to_scan.append(params.get("old_text", "")[:200])
115
+ text_to_scan.append(params.get("new_text", "")[:200])
116
+ elif step.get("type") == "condition":
117
+ _collect_step_text(step.get("if_true", []))
118
+ _collect_step_text(step.get("if_false", []))
119
+ elif step.get("type") == "loop":
120
+ _collect_step_text(step.get("body", []))
121
+ elif step.get("type") == "group":
122
+ _collect_step_text(step.get("steps", []))
123
+
124
+ _collect_step_text(spec.get("steps", []))
125
+
126
+ combined = " ".join(text_to_scan)
127
+
128
+ # Check for command-level patterns first (highest risk)
129
+ for pattern in _COMMAND_PATTERNS:
130
+ if re.search(pattern, combined, re.IGNORECASE):
131
+ logger.info("Risk: command — matched pattern '%s'", pattern)
132
+ return "command"
133
+
134
+ # Check for sensitive patterns
135
+ for pattern in _SENSITIVE_PATTERNS:
136
+ if re.search(pattern, combined, re.IGNORECASE):
137
+ risk = _max_risk(risk, "sensitive")
138
+ logger.info("Risk: sensitive — matched pattern '%s'", pattern)
139
+
140
+ # Check for write operations (standard minimum for any writes)
141
+ has_writes = False
142
+ def _check_writes(steps):
143
+ nonlocal has_writes
144
+ for step in steps:
145
+ if step.get("type") == "action":
146
+ if step.get("tool") in ("write_file", "edit_file", "push_to_github", "shell_execute"):
147
+ has_writes = True
148
+ elif step.get("type") == "condition":
149
+ _check_writes(step.get("if_true", []))
150
+ _check_writes(step.get("if_false", []))
151
+ elif step.get("type") == "loop":
152
+ _check_writes(step.get("body", []))
153
+ elif step.get("type") == "group":
154
+ _check_writes(step.get("steps", []))
155
+
156
+ _check_writes(spec.get("steps", []))
157
+ if has_writes:
158
+ risk = _max_risk(risk, "standard")
159
+
160
+ # Cross-module work (multiple repos in scope or workspace = /home/josh)
161
+ workspace = spec.get("block", {}).get("workspace", "")
162
+ scope = spec.get("block", {}).get("scope", "")
163
+ if workspace in ("/home/josh", "~") or "ecosystem-wide" in scope.lower() or "cross-module" in scope.lower():
164
+ risk = _max_risk(risk, "standard")
165
+
166
+ return risk
167
+
168
+
169
+ def _graph_recall_density(graph_context: Optional[list]) -> str:
170
+ """Check Graph recall density. Sparse recall = bump risk up one level.
171
+
172
+ The logic: if the Graph has little relevant experience for this type
173
+ of work, we're in unfamiliar territory and need more oversight.
174
+ """
175
+ if graph_context is None:
176
+ return "bump" # No context at all = unfamiliar
177
+
178
+ if len(graph_context) == 0:
179
+ return "bump" # Graph returned nothing relevant
180
+
181
+ # Check average similarity — low similarity means weak matches
182
+ similarities = [r.get("similarity", 0) for r in graph_context]
183
+ avg_similarity = sum(similarities) / len(similarities) if similarities else 0
184
+
185
+ if avg_similarity < 0.3:
186
+ return "bump" # Weak matches = unfamiliar territory
187
+
188
+ if len(graph_context) < 3 and avg_similarity < 0.5:
189
+ return "bump" # Few matches, mediocre relevance
190
+
191
+ return "no_bump" # Sufficient recall density
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Public API
196
+ # ---------------------------------------------------------------------------
197
+
198
+ def classify_risk(
199
+ spec: dict,
200
+ graph_context: Optional[list] = None,
201
+ strategist_override: Optional[str] = None,
202
+ ) -> str:
203
+ """Classify a spec's risk level.
204
+
205
+ Args:
206
+ spec: The WorkBlockSpec to classify
207
+ graph_context: Graph recall results for the spec's domain
208
+ strategist_override: Strategist can escalate (never downgrade)
209
+
210
+ Returns:
211
+ One of: "routine", "standard", "sensitive", "command"
212
+ """
213
+ # Content-derived baseline
214
+ risk = _scan_spec_content(spec)
215
+ logger.info("Content-derived risk: %s", risk)
216
+
217
+ # Graph recall density bump
218
+ density = _graph_recall_density(graph_context)
219
+ if density == "bump" and risk != "command":
220
+ original = risk
221
+ risk = RISK_LEVELS[min(_risk_index(risk) + 1, len(RISK_LEVELS) - 1)]
222
+ logger.info("Graph recall sparse — bumped %s → %s", original, risk)
223
+
224
+ # Strategist override (escalation only, never downgrade)
225
+ if strategist_override:
226
+ override = strategist_override.lower()
227
+ if override in RISK_LEVELS:
228
+ if _risk_index(override) > _risk_index(risk):
229
+ logger.info("Strategist escalated %s → %s", risk, override)
230
+ risk = override
231
+ elif _risk_index(override) < _risk_index(risk):
232
+ logger.warning("Strategist tried to downgrade %s → %s — denied", risk, override)
233
+
234
+ return risk
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Authority map
239
+ # ---------------------------------------------------------------------------
240
+
241
+ # Who can approve at each risk level, and how many retries they get
242
+ AUTHORITY_MAP = {
243
+ "routine": {
244
+ "approver": "reviewer",
245
+ "retries": 3,
246
+ "escalates_to": "strategist",
247
+ "co_signer": None,
248
+ },
249
+ "standard": {
250
+ "approver": "reviewer",
251
+ "retries": 2,
252
+ "escalates_to": "strategist",
253
+ "co_signer": None,
254
+ },
255
+ "sensitive": {
256
+ "approver": "reviewer",
257
+ "retries": 2,
258
+ "escalates_to": "qb",
259
+ "co_signer": "razor", # Security co-sign required
260
+ },
261
+ "command": {
262
+ "approver": "qb",
263
+ "retries": 1,
264
+ "escalates_to": "josh", # Human in the loop
265
+ "co_signer": "strategist", # Architecture co-sign
266
+ },
267
+ }
268
+
269
+
270
+ def get_authority(risk_level: str) -> dict:
271
+ """Get the authority configuration for a risk level.
272
+
273
+ Returns:
274
+ {approver, retries, escalates_to, co_signer}
275
+ """
276
+ return AUTHORITY_MAP.get(risk_level, AUTHORITY_MAP["standard"])
277
+
278
+
279
+ def can_approve(role: str, risk_level: str) -> bool:
280
+ """Check if a role has authority to approve at this risk level."""
281
+ auth = get_authority(risk_level)
282
+ if role == auth["approver"]:
283
+ return True
284
+ if role == "qb":
285
+ return True # QB can approve anything
286
+ return False
287
+
288
+
289
+ def needs_co_signer(risk_level: str) -> Optional[str]:
290
+ """Check if this risk level needs a co-signer. Returns role name or None."""
291
+ return get_authority(risk_level).get("co_signer")
292
+
293
+
294
+ def get_retry_budget(risk_level: str) -> int:
295
+ """Get the number of retries allowed at this risk level before escalation."""
296
+ return get_authority(risk_level).get("retries", 2)
297
+
298
+
299
+ def get_escalation_target(risk_level: str) -> str:
300
+ """Get who to escalate to when retries are exhausted."""
301
+ return get_authority(risk_level).get("escalates_to", "josh")