Wl6adams's picture
Organize private Nexum release into Lite, Universal, and Expanded profiles
9a70a84
Raw
History Blame Contribute Delete
17.8 kB
"""Receipt-backed draft, comparison, and selection workflows."""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import secrets
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Mapping
from .artifacts import ArtifactStore
from .contracts import (
ToolCall,
ToolExecutionContext,
ToolExecutionResult,
ToolParameter,
ToolSpec,
)
from .sandbox import control_root, ensure_control_root
from .security import SecretRedactor
_DRAFT_PREFIX = "draft_"
_HEX = frozenset("0123456789abcdef")
DRAFTING_TOOL_SPECS: tuple[ToolSpec, ...] = (
ToolSpec(
"DraftCreate",
"drafting",
"Persist model-selected candidate actions or answers before any external effect is taken.",
"DraftCreate(objective='Repair failing test', candidates=[{'name':'minimal','content':'...'}], criteria=['passes current test'])",
(
ToolParameter("objective", "string", "Current objective being drafted."),
ToolParameter(
"candidates",
"array",
"Candidate actions or answers. Each row must include name and content.",
),
ToolParameter(
"criteria",
"array",
"Evidence criteria the model will use to compare candidates.",
),
),
risk="workspace_write",
parallel_safe=False,
idempotent=False,
),
ToolSpec(
"DraftCompare",
"drafting",
"Attach evidence and observations to a draft without choosing for the model.",
"DraftCompare(draft_id='draft_...', evidence_refs=['rep_...'], observations=['candidate 0 preserves API'])",
(
ToolParameter("draft_id", "string", "Draft receipt identifier."),
ToolParameter(
"evidence_refs",
"array",
"Optional receipt or artifact identifiers considered by the model.",
required=False,
),
ToolParameter(
"observations",
"array",
"Model-observed comparison facts grounded in current evidence.",
required=False,
),
),
risk="workspace_write",
parallel_safe=False,
idempotent=False,
),
ToolSpec(
"DraftSelect",
"drafting",
"Persist the model's chosen draft index with cited evidence before execution or final answer.",
"DraftSelect(draft_id='draft_...', selected_index=0, decision='smallest verified change', evidence_refs=['rep_...'])",
(
ToolParameter("draft_id", "string", "Draft receipt identifier."),
ToolParameter("selected_index", "integer", "Zero-based selected candidate index."),
ToolParameter("decision", "string", "Why this candidate was selected."),
ToolParameter(
"evidence_refs",
"array",
"Optional receipt or artifact identifiers supporting the selection.",
required=False,
),
),
risk="workspace_write",
parallel_safe=False,
idempotent=False,
),
ToolSpec(
"DraftStatus",
"drafting",
"Read a session-scoped draft receipt, comparisons, and final selection state.",
"DraftStatus(draft_id='draft_...')",
(ToolParameter("draft_id", "string", "Draft receipt identifier."),),
),
)
DRAFTING_TOOL_NAMES = frozenset(spec.name for spec in DRAFTING_TOOL_SPECS)
_DRAFTING_TOOL_BY_NAME = {spec.name: spec for spec in DRAFTING_TOOL_SPECS}
@dataclass(frozen=True)
class DraftCandidate:
index: int
name: str
content_sha256: str
artifact_id: str
metadata_sha256: str
@dataclass(frozen=True)
class DraftComparison:
comparison_id: str
evidence_refs: tuple[str, ...]
observations: tuple[str, ...]
evidence_sha256: str
created_unix_ms: int
@dataclass(frozen=True)
class DraftSelection:
selected_index: int
decision: str
evidence_refs: tuple[str, ...]
evidence_sha256: str
created_unix_ms: int
@dataclass(frozen=True)
class DraftRecord:
draft_id: str
session_sha256: str
objective: str
criteria: tuple[str, ...]
candidates: tuple[DraftCandidate, ...]
comparisons: tuple[DraftComparison, ...]
selection: DraftSelection | None
created_unix_ms: int
updated_unix_ms: int
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def _session_sha256(session_id: str) -> str:
return hashlib.sha256((session_id or "direct").encode("utf-8")).hexdigest()
def _require_session(session_sha256: str, session_id: str) -> None:
if not hmac.compare_digest(session_sha256, _session_sha256(session_id)):
raise PermissionError("draft receipt does not belong to this session")
def _sanitize_text(value: object, workspace: Path) -> str:
redacted = SecretRedactor().redact(str(value or ""))
return redacted.replace(str(workspace), "[WORKSPACE]")
def _string_rows(value: object, name: str, *, allow_empty: bool) -> tuple[str, ...]:
if value is None and allow_empty:
return ()
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
raise ValueError(f"{name} must be an array of strings")
rows = tuple(item.strip() for item in value)
if any(not item for item in rows) or (not rows and not allow_empty):
raise ValueError(f"{name} must contain non-empty strings")
return rows
def _draft_path(root: Path, draft_id: str) -> Path:
suffix = draft_id.removeprefix(_DRAFT_PREFIX)
if (
not draft_id.startswith(_DRAFT_PREFIX)
or len(suffix) != 32
or any(char not in _HEX for char in suffix)
):
raise ValueError("draft id is invalid")
return root / f"{draft_id}.json"
def _atomic_json(path: Path, payload: Mapping[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(path.parent, 0o700)
temporary = path.with_name(f".{path.name}.{secrets.token_hex(8)}.tmp")
temporary.write_text(
json.dumps(payload, sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
temporary.chmod(0o600)
with temporary.open("rb") as handle:
os.fsync(handle.fileno())
os.replace(temporary, path)
directory_fd = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
def _load_record(path: Path) -> DraftRecord:
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise RuntimeError("draft receipt is invalid")
candidates = tuple(DraftCandidate(**row) for row in payload.pop("candidates", []))
comparisons = tuple(
DraftComparison(
**{
**row,
"evidence_refs": tuple(row.get("evidence_refs", [])),
"observations": tuple(row.get("observations", [])),
}
)
for row in payload.pop("comparisons", [])
)
raw_selection = payload.pop("selection", None)
selection = (
DraftSelection(
**{
**raw_selection,
"evidence_refs": tuple(raw_selection.get("evidence_refs", [])),
}
)
if isinstance(raw_selection, dict)
else None
)
return DraftRecord(
**{
**payload,
"criteria": tuple(payload.get("criteria", [])),
"candidates": candidates,
"comparisons": comparisons,
"selection": selection,
}
)
class DraftStore:
def __init__(self, workspace: str | Path) -> None:
self.workspace = ensure_control_root(workspace)
self.root = control_root(self.workspace) / "drafting"
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(self.root, 0o700)
self.artifacts = ArtifactStore(self.workspace)
def create(
self,
*,
objective: str,
candidates: object,
criteria: tuple[str, ...],
session_id: str,
) -> DraftRecord:
objective_text = _sanitize_text(objective, self.workspace).strip()
if not objective_text:
raise ValueError("draft objective is required")
if not isinstance(candidates, list) or not candidates:
raise ValueError("draft candidates must be a non-empty array")
rows: list[DraftCandidate] = []
for index, raw_candidate in enumerate(candidates):
if not isinstance(raw_candidate, dict):
raise ValueError("each draft candidate must be an object")
name = _sanitize_text(raw_candidate.get("name"), self.workspace).strip()
content = _sanitize_text(raw_candidate.get("content"), self.workspace)
if not name or not content.strip():
raise ValueError("each draft candidate requires name and content")
metadata = {
key: _sanitize_text(value, self.workspace)
for key, value in raw_candidate.items()
if key not in {"name", "content"}
}
artifact = self.artifacts.put_text(
content,
media_type="text/plain; charset=utf-8",
source="draft_candidate",
session_id=session_id or "direct",
)
rows.append(
DraftCandidate(
index=index,
name=name,
content_sha256=hashlib.sha256(content.encode("utf-8")).hexdigest(),
artifact_id=artifact.artifact_id,
metadata_sha256=hashlib.sha256(
json.dumps(
metadata,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
).hexdigest(),
)
)
if not criteria:
raise ValueError("draft criteria must contain at least one entry")
now = int(time.time() * 1000)
record = DraftRecord(
draft_id=_DRAFT_PREFIX + secrets.token_hex(16),
session_sha256=_session_sha256(session_id),
objective=objective_text,
criteria=tuple(_sanitize_text(item, self.workspace) for item in criteria),
candidates=tuple(rows),
comparisons=(),
selection=None,
created_unix_ms=now,
updated_unix_ms=now,
)
_atomic_json(_draft_path(self.root, record.draft_id), record.to_dict())
return record
def get(self, draft_id: str, *, session_id: str) -> DraftRecord:
record = _load_record(_draft_path(self.root, draft_id))
_require_session(record.session_sha256, session_id)
return record
def compare(
self,
draft_id: str,
*,
evidence_refs: tuple[str, ...],
observations: tuple[str, ...],
session_id: str,
) -> DraftRecord:
record = self.get(draft_id, session_id=session_id)
if not evidence_refs and not observations:
raise ValueError("draft comparison requires evidence refs or observations")
safe_evidence = tuple(_sanitize_text(item, self.workspace) for item in evidence_refs)
safe_observations = tuple(_sanitize_text(item, self.workspace) for item in observations)
evidence_sha256 = hashlib.sha256(
json.dumps(
{"evidence_refs": safe_evidence, "observations": safe_observations},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
).hexdigest()
comparison = DraftComparison(
comparison_id="cmp_" + secrets.token_hex(16),
evidence_refs=safe_evidence,
observations=safe_observations,
evidence_sha256=evidence_sha256,
created_unix_ms=int(time.time() * 1000),
)
updated = DraftRecord(
**{
**record.to_dict(),
"comparisons": (*record.comparisons, comparison),
"updated_unix_ms": int(time.time() * 1000),
}
)
_atomic_json(_draft_path(self.root, draft_id), updated.to_dict())
return updated
def select(
self,
draft_id: str,
*,
selected_index: int,
decision: str,
evidence_refs: tuple[str, ...],
session_id: str,
) -> DraftRecord:
record = self.get(draft_id, session_id=session_id)
if selected_index < 0 or selected_index >= len(record.candidates):
raise ValueError("selected draft index is outside the candidate set")
decision_text = _sanitize_text(decision, self.workspace).strip()
if not decision_text:
raise ValueError("draft selection decision is required")
safe_evidence = tuple(_sanitize_text(item, self.workspace) for item in evidence_refs)
evidence_sha256 = hashlib.sha256(
json.dumps(
{
"selected_index": selected_index,
"decision": decision_text,
"evidence_refs": safe_evidence,
},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
).hexdigest()
selection = DraftSelection(
selected_index=selected_index,
decision=decision_text,
evidence_refs=safe_evidence,
evidence_sha256=evidence_sha256,
created_unix_ms=int(time.time() * 1000),
)
updated = DraftRecord(
**{
**record.to_dict(),
"selection": selection,
"updated_unix_ms": int(time.time() * 1000),
}
)
_atomic_json(_draft_path(self.root, draft_id), updated.to_dict())
return updated
def _tool_result(
call: ToolCall,
*,
started: float,
ok: bool,
output: str = "",
error: str = "",
) -> ToolExecutionResult:
rendered = output or error
return ToolExecutionResult(
name=call.name,
args=call.args,
ok=ok,
tool_call_id=call.call_id,
output=output,
error=error,
executed=True,
elapsed_s=round(time.perf_counter() - started, 4),
source_trust="trusted_execution",
output_sha256=hashlib.sha256(rendered.encode("utf-8")).hexdigest(),
)
def execute_drafting_tool(
call: ToolCall,
context: ToolExecutionContext,
) -> ToolExecutionResult:
started = time.perf_counter()
spec = _DRAFTING_TOOL_BY_NAME.get(call.name)
if spec is None:
return _tool_result(
call,
started=started,
ok=False,
error=f"unsupported drafting tool: {call.name}",
)
try:
spec.validate_arguments(call.args)
store = DraftStore(context.workspace)
if call.name == "DraftCreate":
record = store.create(
objective=str(call.args["objective"]),
candidates=call.args["candidates"],
criteria=_string_rows(call.args["criteria"], "criteria", allow_empty=False),
session_id=context.session_id,
)
return _tool_result(
call,
started=started,
ok=True,
output=json.dumps(record.to_dict(), sort_keys=True),
)
if call.name == "DraftCompare":
record = store.compare(
str(call.args["draft_id"]),
evidence_refs=_string_rows(
call.args.get("evidence_refs"), "evidence_refs", allow_empty=True
),
observations=_string_rows(
call.args.get("observations"), "observations", allow_empty=True
),
session_id=context.session_id,
)
return _tool_result(
call,
started=started,
ok=True,
output=json.dumps(record.to_dict(), sort_keys=True),
)
if call.name == "DraftSelect":
record = store.select(
str(call.args["draft_id"]),
selected_index=int(call.args["selected_index"]),
decision=str(call.args["decision"]),
evidence_refs=_string_rows(
call.args.get("evidence_refs"), "evidence_refs", allow_empty=True
),
session_id=context.session_id,
)
return _tool_result(
call,
started=started,
ok=True,
output=json.dumps(record.to_dict(), sort_keys=True),
)
if call.name == "DraftStatus":
record = store.get(str(call.args["draft_id"]), session_id=context.session_id)
return _tool_result(
call,
started=started,
ok=True,
output=json.dumps(record.to_dict(), sort_keys=True),
)
raise RuntimeError("drafting tool dispatch fell through")
except (OSError, PermissionError, RuntimeError, ValueError) as exc:
return _tool_result(
call,
started=started,
ok=False,
error=f"{type(exc).__name__}: {exc}",
)
__all__ = [
"DRAFTING_TOOL_NAMES",
"DRAFTING_TOOL_SPECS",
"DraftStore",
"execute_drafting_tool",
]