delimit-cli 4.1.43 → 4.1.47

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +46 -5
  3. package/bin/delimit-cli.js +1987 -337
  4. package/bin/delimit-setup.js +108 -66
  5. package/gateway/ai/activate_helpers.py +253 -7
  6. package/gateway/ai/agent_dispatch.py +34 -2
  7. package/gateway/ai/backends/deploy_bridge.py +167 -12
  8. package/gateway/ai/backends/gateway_core.py +236 -13
  9. package/gateway/ai/backends/repo_bridge.py +80 -16
  10. package/gateway/ai/backends/tools_infra.py +49 -32
  11. package/gateway/ai/checksums.sha256 +6 -0
  12. package/gateway/ai/content_engine.py +1276 -2
  13. package/gateway/ai/continuity.py +462 -0
  14. package/gateway/ai/deliberation.pyi +53 -0
  15. package/gateway/ai/github_scanner.py +1 -1
  16. package/gateway/ai/governance.py +58 -0
  17. package/gateway/ai/governance.pyi +32 -0
  18. package/gateway/ai/governance_hardening.py +569 -0
  19. package/gateway/ai/inbox_daemon_runner.py +217 -0
  20. package/gateway/ai/key_resolver.py +95 -2
  21. package/gateway/ai/ledger_manager.py +53 -3
  22. package/gateway/ai/license.py +104 -3
  23. package/gateway/ai/license_core.py +177 -36
  24. package/gateway/ai/license_core.pyi +50 -0
  25. package/gateway/ai/loop_engine.py +929 -294
  26. package/gateway/ai/notify.py +1786 -2
  27. package/gateway/ai/reddit_scanner.py +190 -1
  28. package/gateway/ai/screen_record.py +1 -1
  29. package/gateway/ai/secrets_broker.py +5 -1
  30. package/gateway/ai/server.py +254 -19
  31. package/gateway/ai/social_cache.py +341 -0
  32. package/gateway/ai/social_daemon.py +41 -10
  33. package/gateway/ai/supabase_sync.py +190 -2
  34. package/gateway/ai/swarm.py +86 -0
  35. package/gateway/ai/swarm_infra.py +656 -0
  36. package/gateway/ai/tui.py +594 -36
  37. package/gateway/ai/tweet_corpus_schema.sql +76 -0
  38. package/gateway/core/diff_engine_v2.py +6 -2
  39. package/gateway/core/generator_drift.py +242 -0
  40. package/gateway/core/json_schema_diff.py +375 -0
  41. package/gateway/core/openapi_version.py +124 -0
  42. package/gateway/core/spec_detector.py +47 -7
  43. package/gateway/core/spec_health.py +5 -2
  44. package/gateway/core/zero_spec/express_extractor.py +2 -2
  45. package/gateway/core/zero_spec/nestjs_extractor.py +40 -9
  46. package/gateway/requirements.txt +3 -6
  47. package/lib/cross-model-hooks.js +4 -12
  48. package/package.json +11 -3
  49. package/scripts/demo-v420-clean.sh +267 -0
  50. package/scripts/demo-v420-deliberation.sh +217 -0
  51. package/scripts/demo-v420.sh +55 -0
  52. package/scripts/postinstall.js +4 -3
  53. package/scripts/publish-ci-guard.sh +30 -0
  54. package/scripts/record-and-upload.sh +132 -0
  55. package/scripts/release.sh +126 -0
  56. package/scripts/sync-gateway.sh +112 -0
  57. package/scripts/youtube-upload.py +141 -0
@@ -0,0 +1,656 @@
1
+ """Swarm shared infrastructure — auth, logging, namespace isolation, directory jailing.
2
+
3
+ LED-274: Phase 1 shared infrastructure for multi-agent orchestration.
4
+ Provides enforcement primitives that the swarm dispatch system can wire into
5
+ agent operations. All classes are lightweight and stateless where possible;
6
+ persistent state lives in ~/.delimit/swarm/ alongside the existing registry.
7
+
8
+ Design:
9
+ - SwarmAuth: venture-scoped token issue/validate for agent identity
10
+ - SwarmLogger: centralized structured logging with venture+agent context
11
+ - VentureNamespace: resolve venture paths and enforce boundaries
12
+ - DirectoryJail: hard enforcement — raises on out-of-scope writes
13
+ - SharedLibs: read-only access registry for common utilities
14
+ """
15
+
16
+ import hashlib
17
+ import hmac
18
+ import json
19
+ import logging
20
+ import os
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional, Set
24
+
25
+ # Re-use the swarm directory for shared state
26
+ SWARM_DIR = Path.home() / ".delimit" / "swarm"
27
+ AUTH_DIR = SWARM_DIR / "auth"
28
+ INFRA_LOG = SWARM_DIR / "infra_log.jsonl"
29
+
30
+
31
+ # =========================================================================
32
+ # SwarmAuth — venture-scoped tokens for agent identity
33
+ # =========================================================================
34
+
35
+ class SwarmAuth:
36
+ """Issue and validate HMAC-based tokens scoped to a venture+agent pair.
37
+
38
+ Tokens are short-lived (default 1 hour) and tied to a specific venture
39
+ namespace. The signing secret is derived per-venture so a token from
40
+ venture A cannot be used in venture B.
41
+
42
+ Token format (hex): hmac_hex : issued_ts : venture : agent_id
43
+ """
44
+
45
+ TOKEN_FILE = AUTH_DIR / "tokens.json"
46
+ # Default to 1 hour; caller can override
47
+ DEFAULT_TTL = 3600
48
+
49
+ def __init__(self, master_secret: str = ""):
50
+ """Initialize with a master secret. Falls back to a machine-derived key."""
51
+ self._master = master_secret or self._derive_machine_secret()
52
+ AUTH_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ # -- public API -------------------------------------------------------
55
+
56
+ def issue_token(
57
+ self,
58
+ venture: str,
59
+ agent_id: str,
60
+ ttl: int = 0,
61
+ ) -> Dict[str, Any]:
62
+ """Issue a venture-scoped token for an agent."""
63
+ if not venture or not agent_id:
64
+ return {"error": "venture and agent_id are required"}
65
+
66
+ ttl = ttl or self.DEFAULT_TTL
67
+ issued = int(time.time())
68
+ expires = issued + ttl
69
+ payload = f"{venture}:{agent_id}:{issued}:{expires}"
70
+ sig = self._sign(venture, payload)
71
+
72
+ token = f"{sig}:{issued}:{expires}:{venture}:{agent_id}"
73
+
74
+ # Persist for revocation checks
75
+ self._store_token(token, venture, agent_id, expires)
76
+
77
+ return {
78
+ "token": token,
79
+ "venture": venture,
80
+ "agent_id": agent_id,
81
+ "issued_at": issued,
82
+ "expires_at": expires,
83
+ "ttl_seconds": ttl,
84
+ }
85
+
86
+ def validate_token(self, token: str) -> Dict[str, Any]:
87
+ """Validate a token. Returns agent identity or error."""
88
+ parts = token.split(":")
89
+ if len(parts) != 5:
90
+ return {"valid": False, "error": "Malformed token"}
91
+
92
+ sig, issued_str, expires_str, venture, agent_id = parts
93
+
94
+ try:
95
+ issued = int(issued_str)
96
+ expires = int(expires_str)
97
+ except ValueError:
98
+ return {"valid": False, "error": "Invalid timestamp in token"}
99
+
100
+ # Expiry check
101
+ now = int(time.time())
102
+ if now > expires:
103
+ return {
104
+ "valid": False,
105
+ "error": "Token expired",
106
+ "expired_at": expires,
107
+ "venture": venture,
108
+ "agent_id": agent_id,
109
+ }
110
+
111
+ # Signature check
112
+ payload = f"{venture}:{agent_id}:{issued}:{expires}"
113
+ expected_sig = self._sign(venture, payload)
114
+ if not hmac.compare_digest(sig, expected_sig):
115
+ return {"valid": False, "error": "Invalid signature"}
116
+
117
+ # Revocation check
118
+ if self._is_revoked(token):
119
+ return {"valid": False, "error": "Token has been revoked"}
120
+
121
+ return {
122
+ "valid": True,
123
+ "venture": venture,
124
+ "agent_id": agent_id,
125
+ "issued_at": issued,
126
+ "expires_at": expires,
127
+ "remaining_seconds": expires - now,
128
+ }
129
+
130
+ def revoke_token(self, token: str) -> Dict[str, Any]:
131
+ """Revoke a token so it can no longer be used."""
132
+ tokens = self._load_tokens()
133
+ token_hash = hashlib.sha256(token.encode()).hexdigest()[:16]
134
+ if token_hash in tokens:
135
+ tokens[token_hash]["revoked"] = True
136
+ tokens[token_hash]["revoked_at"] = int(time.time())
137
+ self._save_tokens(tokens)
138
+ return {"status": "revoked", "token_hash": token_hash}
139
+ return {"status": "not_found", "token_hash": token_hash}
140
+
141
+ # -- internals --------------------------------------------------------
142
+
143
+ @staticmethod
144
+ def _derive_machine_secret() -> str:
145
+ """Derive a stable secret from machine identity (hostname + uid)."""
146
+ identity = f"{os.uname().nodename}:{os.getuid()}:delimit-swarm-v1"
147
+ return hashlib.sha256(identity.encode()).hexdigest()
148
+
149
+ def _venture_key(self, venture: str) -> bytes:
150
+ """Derive a per-venture signing key from the master secret."""
151
+ return hashlib.sha256(f"{self._master}:{venture}".encode()).digest()
152
+
153
+ def _sign(self, venture: str, payload: str) -> str:
154
+ key = self._venture_key(venture)
155
+ return hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()[:32]
156
+
157
+ def _load_tokens(self) -> Dict[str, Any]:
158
+ if not self.TOKEN_FILE.exists():
159
+ return {}
160
+ try:
161
+ return json.loads(self.TOKEN_FILE.read_text())
162
+ except (json.JSONDecodeError, OSError):
163
+ return {}
164
+
165
+ def _save_tokens(self, tokens: Dict[str, Any]):
166
+ self.TOKEN_FILE.write_text(json.dumps(tokens, indent=2))
167
+
168
+ def _store_token(self, token: str, venture: str, agent_id: str, expires: int):
169
+ tokens = self._load_tokens()
170
+ token_hash = hashlib.sha256(token.encode()).hexdigest()[:16]
171
+ tokens[token_hash] = {
172
+ "venture": venture,
173
+ "agent_id": agent_id,
174
+ "expires": expires,
175
+ "revoked": False,
176
+ }
177
+ # Prune expired tokens while we are here
178
+ now = int(time.time())
179
+ tokens = {k: v for k, v in tokens.items() if v.get("expires", 0) > now or v.get("revoked")}
180
+ self._save_tokens(tokens)
181
+
182
+ def _is_revoked(self, token: str) -> bool:
183
+ tokens = self._load_tokens()
184
+ token_hash = hashlib.sha256(token.encode()).hexdigest()[:16]
185
+ entry = tokens.get(token_hash, {})
186
+ return entry.get("revoked", False)
187
+
188
+
189
+ # =========================================================================
190
+ # SwarmLogger — centralized structured logging with venture/agent context
191
+ # =========================================================================
192
+
193
+ class SwarmLogger:
194
+ """Structured JSON-line logger with venture and agent context tags.
195
+
196
+ All log entries go to a shared JSONL file that the governor can query.
197
+ Also integrates with Python's logging module for standard stderr output.
198
+ """
199
+
200
+ def __init__(self, log_file: Optional[Path] = None):
201
+ self._log_file = log_file or INFRA_LOG
202
+ SWARM_DIR.mkdir(parents=True, exist_ok=True)
203
+ self._py_logger = logging.getLogger("delimit.swarm")
204
+ if not self._py_logger.handlers:
205
+ handler = logging.StreamHandler()
206
+ handler.setFormatter(logging.Formatter(
207
+ "[%(asctime)s] %(levelname)s %(name)s — %(message)s",
208
+ datefmt="%Y-%m-%dT%H:%M:%S",
209
+ ))
210
+ self._py_logger.addHandler(handler)
211
+ self._py_logger.setLevel(logging.DEBUG)
212
+
213
+ def log(
214
+ self,
215
+ level: str,
216
+ message: str,
217
+ venture: str = "",
218
+ agent_id: str = "",
219
+ action: str = "",
220
+ extra: Optional[Dict[str, Any]] = None,
221
+ ) -> Dict[str, Any]:
222
+ """Write a structured log entry."""
223
+ entry = {
224
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
225
+ "epoch": time.time(),
226
+ "level": level.upper(),
227
+ "message": message,
228
+ "venture": venture,
229
+ "agent_id": agent_id,
230
+ "action": action,
231
+ }
232
+ if extra:
233
+ entry["extra"] = extra
234
+
235
+ # Write to JSONL
236
+ try:
237
+ with open(self._log_file, "a") as f:
238
+ f.write(json.dumps(entry) + "\n")
239
+ except OSError:
240
+ pass
241
+
242
+ # Mirror to Python logging
243
+ py_level = getattr(logging, level.upper(), logging.INFO)
244
+ ctx = f"[{venture}/{agent_id}]" if venture else ""
245
+ self._py_logger.log(py_level, f"{ctx} {message}")
246
+
247
+ return entry
248
+
249
+ def info(self, message: str, **kwargs) -> Dict[str, Any]:
250
+ return self.log("INFO", message, **kwargs)
251
+
252
+ def warn(self, message: str, **kwargs) -> Dict[str, Any]:
253
+ return self.log("WARNING", message, **kwargs)
254
+
255
+ def error(self, message: str, **kwargs) -> Dict[str, Any]:
256
+ return self.log("ERROR", message, **kwargs)
257
+
258
+ def audit(self, message: str, **kwargs) -> Dict[str, Any]:
259
+ """Audit-level log entry (always persisted, never filtered)."""
260
+ return self.log("AUDIT", message, **kwargs)
261
+
262
+ def query(
263
+ self,
264
+ venture: str = "",
265
+ agent_id: str = "",
266
+ level: str = "",
267
+ limit: int = 50,
268
+ ) -> List[Dict[str, Any]]:
269
+ """Query recent log entries with optional filters."""
270
+ if not self._log_file.exists():
271
+ return []
272
+
273
+ entries: List[Dict[str, Any]] = []
274
+ try:
275
+ with open(self._log_file) as f:
276
+ for line in f:
277
+ line = line.strip()
278
+ if not line:
279
+ continue
280
+ try:
281
+ entry = json.loads(line)
282
+ except json.JSONDecodeError:
283
+ continue
284
+ if venture and entry.get("venture") != venture:
285
+ continue
286
+ if agent_id and entry.get("agent_id") != agent_id:
287
+ continue
288
+ if level and entry.get("level") != level.upper():
289
+ continue
290
+ entries.append(entry)
291
+ except OSError:
292
+ pass
293
+
294
+ # Return most recent first, capped at limit
295
+ return entries[-limit:][::-1]
296
+
297
+
298
+ # =========================================================================
299
+ # VentureNamespace — resolve venture paths and enforce boundaries
300
+ # =========================================================================
301
+
302
+ class VentureNamespace:
303
+ """Resolve venture-specific paths and enforce namespace boundaries.
304
+
305
+ Each venture gets an isolated directory tree under ~/.delimit/ventures/<ns>/
306
+ plus its registered repo_path. Agents can only operate within their
307
+ venture's resolved paths.
308
+ """
309
+
310
+ VENTURES_DIR = Path.home() / ".delimit" / "ventures"
311
+ VENTURES_FILE = SWARM_DIR / "ventures.json"
312
+
313
+ def __init__(self):
314
+ self.VENTURES_DIR.mkdir(parents=True, exist_ok=True)
315
+
316
+ def resolve(self, venture: str) -> Dict[str, Any]:
317
+ """Resolve all paths for a venture namespace."""
318
+ if not venture:
319
+ return {"error": "venture name is required"}
320
+
321
+ ns = venture.strip().lower().replace(" ", "_").replace("-", "_")
322
+ venture_root = self.VENTURES_DIR / ns
323
+ venture_root.mkdir(parents=True, exist_ok=True)
324
+
325
+ # Also check for a registered repo_path
326
+ repo_path = self._get_repo_path(venture)
327
+
328
+ return {
329
+ "venture": venture,
330
+ "namespace": ns,
331
+ "venture_root": str(venture_root),
332
+ "repo_path": repo_path,
333
+ "data_dir": str(venture_root / "data"),
334
+ "logs_dir": str(venture_root / "logs"),
335
+ "tmp_dir": str(venture_root / "tmp"),
336
+ "allowed_roots": self._allowed_roots(ns, repo_path),
337
+ }
338
+
339
+ def is_within_namespace(self, venture: str, target_path: str) -> bool:
340
+ """Check if a path falls within the venture's namespace."""
341
+ info = self.resolve(venture)
342
+ if "error" in info:
343
+ return False
344
+
345
+ target = Path(target_path).resolve()
346
+ for root in info["allowed_roots"]:
347
+ if str(target).startswith(root):
348
+ return True
349
+ return False
350
+
351
+ def _get_repo_path(self, venture: str) -> str:
352
+ """Look up the repo_path from the swarm ventures registry."""
353
+ if not self.VENTURES_FILE.exists():
354
+ return ""
355
+ try:
356
+ ventures = json.loads(self.VENTURES_FILE.read_text())
357
+ name = venture.strip().lower()
358
+ return ventures.get(name, {}).get("repo_path", "")
359
+ except (json.JSONDecodeError, OSError):
360
+ return ""
361
+
362
+ def _allowed_roots(self, ns: str, repo_path: str) -> List[str]:
363
+ """Build the list of allowed root paths for a namespace."""
364
+ roots = [str(self.VENTURES_DIR / ns)]
365
+ if repo_path:
366
+ roots.append(str(Path(repo_path).resolve()))
367
+ return roots
368
+
369
+
370
+ # =========================================================================
371
+ # DirectoryJail — enforce that agents only write to their venture dirs
372
+ # =========================================================================
373
+
374
+ class DirectoryJailViolation(Exception):
375
+ """Raised when an agent attempts to write outside its venture scope."""
376
+ pass
377
+
378
+
379
+ class DirectoryJail:
380
+ """Enforce write-path restrictions for agents.
381
+
382
+ Validates every write path against the venture's allowed roots.
383
+ Raises DirectoryJailViolation on any violation -- this is hard
384
+ enforcement, not advisory logging.
385
+ """
386
+
387
+ def __init__(self, namespace: Optional[VentureNamespace] = None):
388
+ self._ns = namespace or VentureNamespace()
389
+ self._logger = SwarmLogger()
390
+
391
+ def check_write(self, venture: str, agent_id: str, target_path: str) -> bool:
392
+ """Validate a write path. Returns True if allowed, raises on violation."""
393
+ resolved = Path(target_path).resolve()
394
+
395
+ # Block obvious escapes
396
+ if ".." in str(target_path):
397
+ self._raise_violation(venture, agent_id, target_path, "Path contains '..' traversal")
398
+
399
+ # Check namespace membership
400
+ if not self._ns.is_within_namespace(venture, str(resolved)):
401
+ self._raise_violation(
402
+ venture,
403
+ agent_id,
404
+ target_path,
405
+ f"Path is outside venture '{venture}' namespace",
406
+ )
407
+
408
+ # Block sensitive system paths regardless of namespace
409
+ blocked_prefixes = ["/etc", "/usr", "/bin", "/sbin", "/boot", "/proc", "/sys"]
410
+ for prefix in blocked_prefixes:
411
+ if str(resolved).startswith(prefix):
412
+ self._raise_violation(
413
+ venture,
414
+ agent_id,
415
+ target_path,
416
+ f"Writes to system path '{prefix}' are always blocked",
417
+ )
418
+
419
+ self._logger.info(
420
+ f"Write allowed: {target_path}",
421
+ venture=venture,
422
+ agent_id=agent_id,
423
+ action="jail_check_pass",
424
+ )
425
+ return True
426
+
427
+ def check_read(self, venture: str, agent_id: str, target_path: str) -> bool:
428
+ """Validate a read path. More permissive than writes -- allows shared libs."""
429
+ resolved = Path(target_path).resolve()
430
+
431
+ # Reads within namespace are always fine
432
+ if self._ns.is_within_namespace(venture, str(resolved)):
433
+ return True
434
+
435
+ # Reads from shared libs are fine (handled by SharedLibs)
436
+ # Reads from /tmp are fine
437
+ if str(resolved).startswith("/tmp"):
438
+ return True
439
+
440
+ self._logger.warn(
441
+ f"Read outside namespace: {target_path}",
442
+ venture=venture,
443
+ agent_id=agent_id,
444
+ action="jail_read_warning",
445
+ )
446
+ # Reads are advisory warnings, not hard blocks
447
+ return True
448
+
449
+ def _raise_violation(self, venture: str, agent_id: str, path: str, reason: str):
450
+ self._logger.error(
451
+ f"JAIL VIOLATION: {reason} (path={path})",
452
+ venture=venture,
453
+ agent_id=agent_id,
454
+ action="jail_violation",
455
+ extra={"path": path, "reason": reason},
456
+ )
457
+ raise DirectoryJailViolation(
458
+ f"Agent '{agent_id}' (venture: {venture}) blocked: {reason}. Path: {path}"
459
+ )
460
+
461
+
462
+ # =========================================================================
463
+ # SharedLibs — read-only access to common utilities
464
+ # =========================================================================
465
+
466
+ class SharedLibs:
467
+ """Registry of shared libraries that agents can read but not modify.
468
+
469
+ The governor controls which paths are exposed as shared libs.
470
+ Agents get read-only access; any write attempt through the jail
471
+ will be blocked.
472
+ """
473
+
474
+ SHARED_LIBS_FILE = SWARM_DIR / "shared_libs.json"
475
+
476
+ # Default shared lib paths that all ventures can read
477
+ DEFAULT_LIBS: List[Dict[str, str]] = [
478
+ {
479
+ "name": "delimit_core",
480
+ "path": str(Path.home() / ".delimit" / "server"),
481
+ "description": "Core Delimit MCP server (read-only)",
482
+ },
483
+ {
484
+ "name": "governance_policies",
485
+ "path": str(Path.home() / ".delimit" / "governance"),
486
+ "description": "Governance policy definitions",
487
+ },
488
+ {
489
+ "name": "shared_schemas",
490
+ "path": str(Path.home() / ".delimit" / "schemas"),
491
+ "description": "Shared data schemas across ventures",
492
+ },
493
+ ]
494
+
495
+ def __init__(self):
496
+ SWARM_DIR.mkdir(parents=True, exist_ok=True)
497
+
498
+ def register_lib(
499
+ self,
500
+ name: str,
501
+ path: str,
502
+ description: str = "",
503
+ ventures: Optional[List[str]] = None,
504
+ ) -> Dict[str, Any]:
505
+ """Register a shared library path. Only the governor should call this."""
506
+ if not name or not path:
507
+ return {"error": "name and path are required"}
508
+
509
+ resolved = str(Path(path).resolve())
510
+ libs = self._load_libs()
511
+ libs[name] = {
512
+ "name": name,
513
+ "path": resolved,
514
+ "description": description,
515
+ "ventures": ventures or ["*"], # "*" means all ventures
516
+ "registered_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
517
+ }
518
+ self._save_libs(libs)
519
+
520
+ return {"status": "registered", "name": name, "path": resolved}
521
+
522
+ def unregister_lib(self, name: str) -> Dict[str, Any]:
523
+ """Remove a shared library from the registry."""
524
+ libs = self._load_libs()
525
+ if name not in libs:
526
+ return {"status": "not_found", "name": name}
527
+ del libs[name]
528
+ self._save_libs(libs)
529
+ return {"status": "removed", "name": name}
530
+
531
+ def list_libs(self, venture: str = "") -> List[Dict[str, str]]:
532
+ """List available shared libraries, optionally filtered by venture."""
533
+ libs = self._load_libs()
534
+ result = []
535
+ for lib in libs.values():
536
+ allowed = lib.get("ventures", ["*"])
537
+ if venture and "*" not in allowed and venture not in allowed:
538
+ continue
539
+ result.append({
540
+ "name": lib["name"],
541
+ "path": lib["path"],
542
+ "description": lib.get("description", ""),
543
+ })
544
+ return result
545
+
546
+ def can_access(self, venture: str, path: str) -> bool:
547
+ """Check if a venture can access a path via shared libs."""
548
+ resolved = str(Path(path).resolve())
549
+ libs = self._load_libs()
550
+ for lib in libs.values():
551
+ allowed = lib.get("ventures", ["*"])
552
+ if "*" not in allowed and venture not in allowed:
553
+ continue
554
+ if resolved.startswith(lib["path"]):
555
+ return True
556
+ return False
557
+
558
+ def get_lib_paths(self, venture: str = "") -> Set[str]:
559
+ """Get all shared lib root paths accessible to a venture."""
560
+ paths: Set[str] = set()
561
+ for lib in self.list_libs(venture=venture):
562
+ paths.add(lib["path"])
563
+ return paths
564
+
565
+ def _load_libs(self) -> Dict[str, Any]:
566
+ if not self.SHARED_LIBS_FILE.exists():
567
+ # Bootstrap with defaults
568
+ libs = {}
569
+ for d in self.DEFAULT_LIBS:
570
+ libs[d["name"]] = {**d, "ventures": ["*"], "registered_at": "bootstrap"}
571
+ return libs
572
+ try:
573
+ return json.loads(self.SHARED_LIBS_FILE.read_text())
574
+ except (json.JSONDecodeError, OSError):
575
+ return {}
576
+
577
+ def _save_libs(self, libs: Dict[str, Any]):
578
+ self.SHARED_LIBS_FILE.write_text(json.dumps(libs, indent=2))
579
+
580
+
581
+ # =========================================================================
582
+ # Convenience: single entry point for swarm infra validation
583
+ # =========================================================================
584
+
585
+ def validate_agent_operation(
586
+ venture: str,
587
+ agent_id: str,
588
+ token: str,
589
+ target_path: str,
590
+ operation: str = "write",
591
+ ) -> Dict[str, Any]:
592
+ """Full validation pipeline: auth -> namespace -> jail -> shared libs.
593
+
594
+ Returns a structured result with pass/fail and details.
595
+ Raises DirectoryJailViolation on write violations.
596
+ """
597
+ auth = SwarmAuth()
598
+ jail = DirectoryJail()
599
+ shared = SharedLibs()
600
+ logger = SwarmLogger()
601
+
602
+ # 1. Authenticate
603
+ auth_result = auth.validate_token(token)
604
+ if not auth_result.get("valid"):
605
+ logger.error(
606
+ f"Auth failed: {auth_result.get('error')}",
607
+ venture=venture,
608
+ agent_id=agent_id,
609
+ action="validate_operation",
610
+ )
611
+ return {"allowed": False, "stage": "auth", "error": auth_result.get("error")}
612
+
613
+ # 2. Verify token matches claimed identity
614
+ if auth_result["venture"] != venture or auth_result["agent_id"] != agent_id:
615
+ logger.error(
616
+ f"Identity mismatch: token is for {auth_result['venture']}/{auth_result['agent_id']}",
617
+ venture=venture,
618
+ agent_id=agent_id,
619
+ action="validate_operation",
620
+ )
621
+ return {"allowed": False, "stage": "identity", "error": "Token does not match claimed identity"}
622
+
623
+ # 3. Check operation
624
+ if operation == "write":
625
+ # This raises DirectoryJailViolation on failure
626
+ jail.check_write(venture, agent_id, target_path)
627
+ elif operation == "read":
628
+ # Check shared libs for cross-namespace reads
629
+ ns = VentureNamespace()
630
+ if not ns.is_within_namespace(venture, target_path):
631
+ if not shared.can_access(venture, target_path):
632
+ logger.warn(
633
+ f"Read denied: {target_path} not in namespace or shared libs",
634
+ venture=venture,
635
+ agent_id=agent_id,
636
+ action="validate_operation",
637
+ )
638
+ return {
639
+ "allowed": False,
640
+ "stage": "namespace",
641
+ "error": "Path not in namespace or shared libs",
642
+ }
643
+
644
+ logger.info(
645
+ f"Operation '{operation}' approved for {target_path}",
646
+ venture=venture,
647
+ agent_id=agent_id,
648
+ action="validate_operation",
649
+ )
650
+ return {
651
+ "allowed": True,
652
+ "venture": venture,
653
+ "agent_id": agent_id,
654
+ "operation": operation,
655
+ "target_path": target_path,
656
+ }