delimit-cli 4.1.53 → 4.2.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.
@@ -0,0 +1,154 @@
1
+ """Base worker class — defines the bounded capability surface.
2
+
3
+ Every worker:
4
+ - Receives a ledger item as input
5
+ - Has access to READ-ONLY tools only (lint, diff, grep, read)
6
+ - Produces a WorkerResult containing a work-order artifact
7
+ - Cannot call state-changing tools (write, commit, push, notify)
8
+ - Records its work in the audit trail
9
+
10
+ The capability boundary is enforced by the ALLOWED_TOOLS whitelist.
11
+ Workers that try to call tools outside the whitelist get an error,
12
+ not a silent fallback.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ import time
20
+ from abc import ABC, abstractmethod
21
+ from dataclasses import dataclass, field, asdict
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ logger = logging.getLogger("delimit.workers")
26
+
27
+ # Read-only tool whitelist — workers CANNOT call anything else.
28
+ # This is the "sandboxed" property from the swarm charter.
29
+ ALLOWED_TOOLS = frozenset({
30
+ "delimit_lint",
31
+ "delimit_diff",
32
+ "delimit_semver",
33
+ "delimit_spec_health",
34
+ "delimit_repo_analyze",
35
+ "delimit_sense",
36
+ "delimit_ledger_query",
37
+ "delimit_ledger_context",
38
+ "delimit_memory_search",
39
+ "delimit_memory_recent",
40
+ "delimit_intel_query",
41
+ "delimit_gov_health",
42
+ # File system reads (not MCP tools, used via subprocess)
43
+ "grep",
44
+ "read_file",
45
+ "glob",
46
+ })
47
+
48
+ # Explicitly DENIED tools — existence check, not exhaustive.
49
+ # Workers hitting these get a clear error message.
50
+ DENIED_TOOLS = frozenset({
51
+ "delimit_ledger_add",
52
+ "delimit_ledger_update",
53
+ "delimit_ledger_done",
54
+ "delimit_memory_store",
55
+ "delimit_notify",
56
+ "delimit_social_post",
57
+ "delimit_deploy_publish",
58
+ "delimit_deploy_site",
59
+ "delimit_secret_store",
60
+ "write_file",
61
+ "edit_file",
62
+ "bash",
63
+ })
64
+
65
+ AUDIT_DIR = Path.home() / ".delimit" / "workers" / "audit"
66
+
67
+
68
+ @dataclass
69
+ class WorkerResult:
70
+ """The output of a worker execution."""
71
+ worker_type: str
72
+ ledger_item_id: str
73
+ success: bool
74
+ artifact_path: str = ""
75
+ artifact_preview: str = ""
76
+ work_order_id: str = ""
77
+ error: str = ""
78
+ tools_called: List[str] = field(default_factory=list)
79
+ duration_seconds: float = 0.0
80
+ timestamp: str = ""
81
+
82
+ def to_dict(self) -> Dict[str, Any]:
83
+ return asdict(self)
84
+
85
+
86
+ class Worker(ABC):
87
+ """Base class for all read-only workers."""
88
+
89
+ worker_type: str = "base"
90
+ description: str = "Base worker"
91
+
92
+ def __init__(self):
93
+ self._tools_called: List[str] = []
94
+
95
+ def check_tool_allowed(self, tool_name: str) -> bool:
96
+ """Check if a tool is in the worker's allowed set."""
97
+ if tool_name in DENIED_TOOLS:
98
+ logger.warning("Worker %s attempted denied tool: %s", self.worker_type, tool_name)
99
+ return False
100
+ return tool_name in ALLOWED_TOOLS
101
+
102
+ def call_tool(self, tool_name: str, **kwargs) -> Any:
103
+ """Call a tool through the bounded surface. Raises if denied."""
104
+ if not self.check_tool_allowed(tool_name):
105
+ raise PermissionError(
106
+ f"Worker '{self.worker_type}' cannot call '{tool_name}'. "
107
+ f"Allowed: {sorted(ALLOWED_TOOLS)}"
108
+ )
109
+ self._tools_called.append(tool_name)
110
+ # Import and call the tool from the server module
111
+ from ai import server as srv
112
+ fn = getattr(srv, f"delimit_{tool_name}" if not tool_name.startswith("delimit_") else tool_name, None)
113
+ if fn is None:
114
+ raise ValueError(f"Tool '{tool_name}' not found in server module")
115
+ return fn(**kwargs)
116
+
117
+ @abstractmethod
118
+ def execute(self, ledger_item: Dict[str, Any]) -> WorkerResult:
119
+ """Execute the worker's task on a ledger item.
120
+
121
+ Must return a WorkerResult with an artifact (work order).
122
+ Must NOT modify any state — output only.
123
+ """
124
+ ...
125
+
126
+ def run(self, ledger_item: Dict[str, Any]) -> WorkerResult:
127
+ """Run the worker with timing + audit trail."""
128
+ start = time.time()
129
+ try:
130
+ result = self.execute(ledger_item)
131
+ except Exception as e:
132
+ result = WorkerResult(
133
+ worker_type=self.worker_type,
134
+ ledger_item_id=ledger_item.get("id", "?"),
135
+ success=False,
136
+ error=str(e),
137
+ )
138
+ result.duration_seconds = round(time.time() - start, 2)
139
+ result.timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ")
140
+ result.tools_called = self._tools_called.copy()
141
+ self._tools_called.clear()
142
+
143
+ # Audit trail
144
+ self._record_audit(result)
145
+ return result
146
+
147
+ def _record_audit(self, result: WorkerResult):
148
+ AUDIT_DIR.mkdir(parents=True, exist_ok=True)
149
+ audit_file = AUDIT_DIR / f"{self.worker_type}.jsonl"
150
+ try:
151
+ with audit_file.open("a") as f:
152
+ f.write(json.dumps(result.to_dict()) + "\n")
153
+ except Exception as e:
154
+ logger.warning("Failed to write worker audit: %s", e)