mxguru1 commited on
Commit
d5f7354
·
verified ·
1 Parent(s): a56cb6b

Add PermissionGate — capability-tier model (T0-T6) per Sovereign Hive spec, audit log with severity flagging, permissive() for test code

Browse files
Files changed (1) hide show
  1. permission_gate.py +359 -0
permission_gate.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sovereign Hive HSAQ — PermissionGate
3
+ =====================================
4
+
5
+ Capability-tier gate for all privileged operations in the HSAQ stack.
6
+ Implements the seven-tier model documented in the Sovereign Hive
7
+ permission spec (T0 inert → T6 system operator).
8
+
9
+ Design tenets:
10
+ - Capabilities are the unit of authorization, not table names or
11
+ function names. Resource types (Vault, filesystem, network, GPU,
12
+ agent lifecycle) are decomposed into specific capabilities so a
13
+ tier grant is auditable.
14
+ - Tiers are cumulative — each tier inherits everything below — but
15
+ grants are enumerated per tier explicitly so changes show up in
16
+ git history rather than being hidden in inheritance.
17
+ - The gate logs every check (granted or denied) to an in-memory
18
+ audit log, optionally also to an external sink. High-severity
19
+ capabilities (write code, schema, arbitrary egress, unsandboxed
20
+ subprocess) log at WARNING level.
21
+ - The gate never grants tier-6 capabilities by default. Test code
22
+ uses `PermissionGate.permissive()` to opt out of enforcement.
23
+ - The orchestration layer is responsible for spawn-tier bounds
24
+ (e.g., "T4 can only escalate to T5"). The gate just answers
25
+ "does tier N have capability X?" — it does not police lineage.
26
+
27
+ What this module deliberately doesn't do:
28
+ - Doesn't enforce rate limits — that belongs at the inference queue
29
+ gateway, not the auth gate.
30
+ - Doesn't justify spawn requests — `agent.spawn.escalated` is a
31
+ capability that exists; the calling orchestrator records the
32
+ justification in its Vault row, not here.
33
+ - Doesn't suspend itself — there is no "disable gate" capability,
34
+ by design. If the gate's wrong, fix the policy in source.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ from dataclasses import dataclass
40
+ from datetime import datetime, timezone
41
+ from enum import Enum
42
+ from typing import Callable, Optional
43
+
44
+ logger = logging.getLogger("HSAQ.PermissionGate")
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Capability primitives
49
+ # ---------------------------------------------------------------------------
50
+ # Naming convention matches the spec's `domain.action.qualifier` shape so
51
+ # string forms stay human-readable in logs and audit rows.
52
+
53
+
54
+ class Capability(str, Enum):
55
+ # Filesystem
56
+ FS_READ_CACHE = "fs.read.cache"
57
+ FS_READ_CODE = "fs.read.code"
58
+ FS_READ_VAULT = "fs.read.vault"
59
+ FS_WRITE_SCRATCH = "fs.write.scratch"
60
+ FS_WRITE_OUTPUTS = "fs.write.outputs"
61
+ FS_WRITE_CONFIG = "fs.write.config"
62
+ FS_WRITE_CODE = "fs.write.code"
63
+
64
+ # Vault
65
+ VAULT_READ = "vault.read"
66
+ VAULT_APPEND = "vault.append"
67
+ VAULT_AMEND = "vault.amend"
68
+ VAULT_SCHEMA = "vault.schema"
69
+
70
+ # Network
71
+ NET_EGRESS_ALLOWLISTED = "net.egress.allowlisted"
72
+ NET_EGRESS_ARBITRARY = "net.egress.arbitrary"
73
+
74
+ # Compute
75
+ COMPUTE_CPU = "compute.cpu"
76
+ COMPUTE_GPU_INFERENCE = "compute.gpu.inference"
77
+ COMPUTE_GPU_PROFILE = "compute.gpu.profile"
78
+ COMPUTE_GPU_TRAIN = "compute.gpu.train"
79
+ COMPUTE_SUBPROCESS_SANDBOXED = "compute.subprocess.sandboxed"
80
+ COMPUTE_SUBPROCESS_UNSANDBOXED = "compute.subprocess.unsandboxed"
81
+
82
+ # Agent lifecycle
83
+ AGENT_SPAWN_LATERAL = "agent.spawn.lateral"
84
+ AGENT_SPAWN_ESCALATED = "agent.spawn.escalated"
85
+ AGENT_TERMINATE_CHILD = "agent.terminate.child"
86
+ AGENT_TERMINATE_PEER = "agent.terminate.peer"
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Tier grants — explicit, per-tier, cumulative-but-enumerated
91
+ # ---------------------------------------------------------------------------
92
+ # Why enumerated and not union-derived from a lower tier: changes to a tier's
93
+ # capability set should show up as a deliberate edit to that tier's frozenset,
94
+ # not as a side-effect of editing a lower tier. The cost is repeating
95
+ # capabilities across tiers; the benefit is grants being auditable in diffs.
96
+
97
+ _TIER_GRANTS: dict[int, frozenset[Capability]] = {
98
+ # T0 — Inert. Pure compute on own process; nothing privileged.
99
+ 0: frozenset({
100
+ Capability.COMPUTE_CPU,
101
+ }),
102
+
103
+ # T1 — Read-only observer.
104
+ 1: frozenset({
105
+ Capability.COMPUTE_CPU,
106
+ Capability.FS_READ_CACHE,
107
+ Capability.FS_READ_CODE,
108
+ Capability.VAULT_READ,
109
+ Capability.AGENT_SPAWN_LATERAL,
110
+ Capability.AGENT_TERMINATE_CHILD,
111
+ }),
112
+
113
+ # T2 — Local I/O worker. The default tier for new agents.
114
+ 2: frozenset({
115
+ Capability.COMPUTE_CPU,
116
+ Capability.COMPUTE_GPU_INFERENCE,
117
+ Capability.FS_READ_CACHE,
118
+ Capability.FS_READ_CODE,
119
+ Capability.FS_READ_VAULT,
120
+ Capability.FS_WRITE_SCRATCH,
121
+ Capability.VAULT_READ,
122
+ Capability.VAULT_APPEND,
123
+ Capability.AGENT_SPAWN_LATERAL,
124
+ Capability.AGENT_TERMINATE_CHILD,
125
+ }),
126
+
127
+ # T3 — Network fetcher and long-running profiler.
128
+ 3: frozenset({
129
+ Capability.COMPUTE_CPU,
130
+ Capability.COMPUTE_GPU_INFERENCE,
131
+ Capability.COMPUTE_GPU_PROFILE,
132
+ Capability.FS_READ_CACHE,
133
+ Capability.FS_READ_CODE,
134
+ Capability.FS_READ_VAULT,
135
+ Capability.FS_WRITE_SCRATCH,
136
+ Capability.FS_WRITE_OUTPUTS,
137
+ Capability.VAULT_READ,
138
+ Capability.VAULT_APPEND,
139
+ Capability.NET_EGRESS_ALLOWLISTED,
140
+ Capability.AGENT_SPAWN_LATERAL,
141
+ Capability.AGENT_TERMINATE_CHILD,
142
+ }),
143
+
144
+ # T4 — Trainer & quantizer.
145
+ 4: frozenset({
146
+ Capability.COMPUTE_CPU,
147
+ Capability.COMPUTE_GPU_INFERENCE,
148
+ Capability.COMPUTE_GPU_PROFILE,
149
+ Capability.COMPUTE_GPU_TRAIN,
150
+ Capability.COMPUTE_SUBPROCESS_SANDBOXED,
151
+ Capability.FS_READ_CACHE,
152
+ Capability.FS_READ_CODE,
153
+ Capability.FS_READ_VAULT,
154
+ Capability.FS_WRITE_SCRATCH,
155
+ Capability.FS_WRITE_OUTPUTS,
156
+ Capability.VAULT_READ,
157
+ Capability.VAULT_APPEND,
158
+ Capability.NET_EGRESS_ALLOWLISTED,
159
+ Capability.AGENT_SPAWN_LATERAL,
160
+ Capability.AGENT_SPAWN_ESCALATED,
161
+ Capability.AGENT_TERMINATE_CHILD,
162
+ }),
163
+
164
+ # T5 — Orchestrator. Coordinates; does not do its own real work.
165
+ 5: frozenset({
166
+ Capability.COMPUTE_CPU,
167
+ Capability.COMPUTE_GPU_INFERENCE,
168
+ Capability.COMPUTE_GPU_PROFILE,
169
+ Capability.COMPUTE_GPU_TRAIN,
170
+ Capability.COMPUTE_SUBPROCESS_SANDBOXED,
171
+ Capability.FS_READ_CACHE,
172
+ Capability.FS_READ_CODE,
173
+ Capability.FS_READ_VAULT,
174
+ Capability.FS_WRITE_SCRATCH,
175
+ Capability.FS_WRITE_OUTPUTS,
176
+ Capability.FS_WRITE_CONFIG,
177
+ Capability.VAULT_READ,
178
+ Capability.VAULT_APPEND,
179
+ Capability.VAULT_AMEND,
180
+ Capability.NET_EGRESS_ALLOWLISTED,
181
+ Capability.AGENT_SPAWN_LATERAL,
182
+ Capability.AGENT_SPAWN_ESCALATED,
183
+ Capability.AGENT_TERMINATE_CHILD,
184
+ Capability.AGENT_TERMINATE_PEER,
185
+ }),
186
+
187
+ # T6 — System operator. Short-lived, single-purpose. The ceiling.
188
+ 6: frozenset({
189
+ # All T5 capabilities…
190
+ Capability.COMPUTE_CPU,
191
+ Capability.COMPUTE_GPU_INFERENCE,
192
+ Capability.COMPUTE_GPU_PROFILE,
193
+ Capability.COMPUTE_GPU_TRAIN,
194
+ Capability.COMPUTE_SUBPROCESS_SANDBOXED,
195
+ Capability.FS_READ_CACHE,
196
+ Capability.FS_READ_CODE,
197
+ Capability.FS_READ_VAULT,
198
+ Capability.FS_WRITE_SCRATCH,
199
+ Capability.FS_WRITE_OUTPUTS,
200
+ Capability.FS_WRITE_CONFIG,
201
+ Capability.VAULT_READ,
202
+ Capability.VAULT_APPEND,
203
+ Capability.VAULT_AMEND,
204
+ Capability.NET_EGRESS_ALLOWLISTED,
205
+ Capability.AGENT_SPAWN_LATERAL,
206
+ Capability.AGENT_SPAWN_ESCALATED,
207
+ Capability.AGENT_TERMINATE_CHILD,
208
+ Capability.AGENT_TERMINATE_PEER,
209
+ # …plus T6 delta:
210
+ Capability.FS_WRITE_CODE,
211
+ Capability.VAULT_SCHEMA,
212
+ Capability.NET_EGRESS_ARBITRARY,
213
+ Capability.COMPUTE_SUBPROCESS_UNSANDBOXED,
214
+ }),
215
+ }
216
+
217
+
218
+ # Capabilities that log at severity HIGH whenever exercised or attempted.
219
+ # Matches the spec's "logged with severity HIGH" annotations.
220
+ _HIGH_SEVERITY: frozenset[Capability] = frozenset({
221
+ Capability.FS_WRITE_CODE,
222
+ Capability.VAULT_SCHEMA,
223
+ Capability.NET_EGRESS_ARBITRARY,
224
+ Capability.COMPUTE_SUBPROCESS_UNSANDBOXED,
225
+ })
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Audit record + exception
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ @dataclass(frozen=True)
234
+ class AuditRecord:
235
+ """One row in the gate's audit log. Same shape works for an external
236
+ sink (e.g. write to vault.audit_log table)."""
237
+ timestamp: str # ISO 8601 UTC
238
+ capability: str # Capability value (string form)
239
+ agent_id: str
240
+ agent_tier: int
241
+ granted: bool
242
+ severity: str # 'NORMAL' or 'HIGH'
243
+ reason: Optional[str] = None # populated on denial
244
+
245
+
246
+ class PermissionDenied(Exception):
247
+ """Raised when a capability check fails."""
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # PermissionGate
252
+ # ---------------------------------------------------------------------------
253
+
254
+
255
+ class PermissionGate:
256
+ """Capability gate enforcing the seven-tier model.
257
+
258
+ The gate is a pure-logic check + audit log. It never opens
259
+ files, never touches the network, never reaches into a Vault.
260
+ The Vault adapter calls .check() before every privileged operation
261
+ and surfaces the denial to its caller.
262
+ """
263
+
264
+ def __init__(
265
+ self,
266
+ audit_sink: Optional[Callable[[AuditRecord], None]] = None,
267
+ ) -> None:
268
+ self.audit_log: list[AuditRecord] = []
269
+ self.audit_sink = audit_sink
270
+ self._permissive = False
271
+
272
+ @classmethod
273
+ def permissive(cls) -> "PermissionGate":
274
+ """For test code only. Returns a gate that grants every capability.
275
+ Calls still log to audit_log so tests can assert audit behaviour."""
276
+ g = cls()
277
+ g._permissive = True
278
+ return g
279
+
280
+ def check(
281
+ self,
282
+ capability: "Capability | str",
283
+ agent_id: str,
284
+ agent_tier: int,
285
+ ) -> None:
286
+ """Authorize one capability for one agent. Raises PermissionDenied
287
+ if the agent's tier doesn't grant it; always appends an AuditRecord
288
+ and (if configured) calls the audit_sink."""
289
+ # Accept either enum or string for ergonomics
290
+ if isinstance(capability, str):
291
+ try:
292
+ capability = Capability(capability)
293
+ except ValueError as e:
294
+ raise PermissionDenied(
295
+ f"unknown capability: {capability!r}"
296
+ ) from e
297
+
298
+ granted = self._permissive or self._has_capability(agent_tier, capability)
299
+ severity = "HIGH" if capability in _HIGH_SEVERITY else "NORMAL"
300
+ reason = None if granted else (
301
+ f"tier {agent_tier} does not grant {capability.value}"
302
+ )
303
+
304
+ record = AuditRecord(
305
+ timestamp=datetime.now(timezone.utc).isoformat(),
306
+ capability=capability.value,
307
+ agent_id=agent_id,
308
+ agent_tier=agent_tier,
309
+ granted=granted,
310
+ severity=severity,
311
+ reason=reason,
312
+ )
313
+ self.audit_log.append(record)
314
+
315
+ if self.audit_sink is not None:
316
+ try:
317
+ self.audit_sink(record)
318
+ except Exception as e:
319
+ # Never let a sink failure crash the gate path
320
+ logger.warning("audit sink raised: %s", e)
321
+
322
+ if severity == "HIGH":
323
+ logger.warning(
324
+ "[GATE] %s agent=%s tier=%d granted=%s",
325
+ capability.value, agent_id, agent_tier, granted,
326
+ )
327
+ else:
328
+ logger.debug(
329
+ "[GATE] %s agent=%s tier=%d granted=%s",
330
+ capability.value, agent_id, agent_tier, granted,
331
+ )
332
+
333
+ if not granted:
334
+ raise PermissionDenied(reason)
335
+
336
+ def has_capability(
337
+ self,
338
+ agent_tier: int,
339
+ capability: "Capability | str",
340
+ ) -> bool:
341
+ """Boolean check without raising or logging. Useful for dry-run
342
+ UI ("does this agent have permission to X before I show that
343
+ button?")."""
344
+ if isinstance(capability, str):
345
+ try:
346
+ capability = Capability(capability)
347
+ except ValueError:
348
+ return False
349
+ return self._permissive or self._has_capability(agent_tier, capability)
350
+
351
+ def tier_grants(self, agent_tier: int) -> frozenset[Capability]:
352
+ """Return the full capability set for a tier. Empty frozenset for
353
+ unknown tier numbers."""
354
+ return _TIER_GRANTS.get(agent_tier, frozenset())
355
+
356
+ def _has_capability(self, tier: int, capability: Capability) -> bool:
357
+ if tier not in _TIER_GRANTS:
358
+ return False
359
+ return capability in _TIER_GRANTS[tier]