ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
work_workspace.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Read-only client for canonical ``agent_workspace.sh`` ownership metadata."""
4
5from __future__ import annotations
6
7import os
8import re
9import shlex
10import stat
11from dataclasses import dataclass
12from pathlib import Path
13from typing import NoReturn
14
15from work_git import WorkError, discover_repo, resolve_commit, run_git_readonly
16
17OWNER = "work"
18SCHEMA = "2"
19KEY_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
20ISSUE_RE = re.compile(r"^[1-9][0-9]{0,9}$")
21COMMIT_RE = re.compile(r"^[0-9a-f]{40,64}$")
22EXPECTED_FIELDS = {
23 "schema",
24 "name",
25 "created",
26 "by",
27 "ref",
28 "base_commit",
29 "path",
30 "branch",
31 "owner",
32}
33
34READY = "READY"
35STALE = "STALE"
36FOREIGN = "FOREIGN"
37FORGED = "FORGED"
38
39
40class ClaimError(WorkError):
41 """Canonical workspace metadata is unreadable or contradictory."""
42
43
44def fail(message: str) -> NoReturn:
45 """Raise one metadata error."""
46 raise ClaimError(message)
47
48
49def is_identifier(value: str) -> bool:
50 """Return whether ``value`` is an issue number or safe plan key."""
51 return ISSUE_RE.fullmatch(value) is not None or KEY_RE.fullmatch(value) is not None
52
53
54def workspace_name(identifier: str) -> str:
55 """Return the canonical workspace name for one work identifier."""
56 if not is_identifier(identifier):
57 fail("invalid work identifier")
58 return f"work-{identifier}"
59
60
61def branch_name(identifier: str) -> str:
62 """Return the canonical branch for one work identifier."""
63 if not is_identifier(identifier):
64 fail("invalid work identifier")
65 return f"work/{identifier}"
66
67
68def metadata_dir(ws_root: Path) -> Path:
69 """Return the canonical workspace metadata directory."""
70 return ws_root / ".meta"
71
72
73def metadata_path(ws_root: Path, identifier: str) -> Path:
74 """Return one canonical metadata path."""
75 return metadata_dir(ws_root) / workspace_name(identifier)
76
77
78def lexical_absolute(path: Path) -> Path:
79 """Return an absolute normalized path without following any symlink.
80
81 Args:
82 path: Path supplied by configuration or metadata.
83
84 Returns:
85 An absolute lexical path. ``..`` components are normalized, while a
86 symlinked child remains visible to the later ``lstat`` check.
87 """
88 return Path(
89 os.path.abspath( # noqa: PTH100 -- preserve symlink evidence for lstat
90 os.fspath(path.expanduser())
91 )
92 )
93
94
95@dataclass(frozen=True)
96class Claim:
97 """One canonical workspace record owned by the work client."""
98
99 identifier: str
100 name: str
101 worktree: Path
102 branch: str
103 base_ref: str
104 base_commit: str
105 created: str
106 creator: str
107
108
109def _single_line(value: str, field: str) -> str:
110 """Require safe single-line metadata text."""
111 if not value or not value.isascii() or not value.isprintable():
112 fail(f"metadata field {field} must be non-empty printable single-line ASCII")
113 return value
114
115
116def _read_fields(path: Path) -> dict[str, str]:
117 """Read one private, regular canonical metadata record."""
118 try:
119 info = path.lstat()
120 except FileNotFoundError:
121 fail(f"canonical metadata is absent: {path}")
122 if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
123 fail(f"canonical metadata is not a regular file: {path}")
124 try:
125 lines = path.read_text(encoding="ascii").splitlines()
126 except (OSError, UnicodeError):
127 fail(f"canonical metadata could not be read: {path}")
128 fields: dict[str, str] = {}
129 for line in lines:
130 key, separator, value = line.partition("=")
131 if not separator or key in fields:
132 fail(f"canonical metadata has an invalid or duplicate field: {path}")
133 fields[key] = _single_line(value, key)
134 if set(fields) != EXPECTED_FIELDS or fields.get("schema") != SCHEMA:
135 fail(f"canonical metadata schema is not supported: {path}")
136 return fields
137
138
139def load_claim(path: Path, ws_root: Path) -> Claim:
140 """Load and internally validate one work-owned canonical record."""
141 fields = _read_fields(path)
142 name = fields["name"]
143 if fields["owner"] != OWNER or path.name != name or not name.startswith("work-"):
144 fail(f"metadata is not a canonical work-owned claim: {path}")
145 identifier = name.removeprefix("work-")
146 root = lexical_absolute(ws_root)
147 expected_path = root / name
148 recorded_path = lexical_absolute(Path(fields["path"]))
149 checks = (
150 is_identifier(identifier),
151 fields["branch"] == branch_name(identifier),
152 recorded_path.is_absolute(),
153 recorded_path == expected_path,
154 COMMIT_RE.fullmatch(fields["base_commit"]) is not None,
155 )
156 if not all(checks):
157 fail(f"canonical work claim contradicts its derived identity: {path}")
158 try:
159 worktree_info = expected_path.lstat()
160 except FileNotFoundError:
161 worktree_info = None
162 if worktree_info is not None and (
163 stat.S_ISLNK(worktree_info.st_mode) or not stat.S_ISDIR(worktree_info.st_mode)
164 ):
165 fail(f"canonical worktree path is not a real directory: {expected_path}")
166 return Claim(
167 identifier=identifier,
168 name=name,
169 worktree=recorded_path,
170 branch=fields["branch"],
171 base_ref=fields["ref"],
172 base_commit=fields["base_commit"],
173 created=fields["created"],
174 creator=fields["by"],
175 )
176
177
178def list_claims(ws_root: Path) -> list[tuple[Path, Claim | ClaimError]]:
179 """List canonical work claims without interpreting agent-owned metadata."""
180 directory = metadata_dir(ws_root)
181 if not directory.exists():
182 return []
183 if not directory.is_dir() or directory.is_symlink():
184 fail(f"canonical metadata directory is unsafe: {directory}")
185 found: list[tuple[Path, Claim | ClaimError]] = []
186 for path in sorted(directory.iterdir(), key=lambda item: item.name):
187 try:
188 if path.name.startswith("work-"):
189 found.append((path, load_claim(path, ws_root)))
190 else:
191 fields = _read_fields(path)
192 if fields.get("owner") == OWNER:
193 found.append((path, ClaimError(f"work owner used a nonreserved name: {path}")))
194 except ClaimError as exc:
195 # A malformed record using the reserved work- prefix is visible;
196 # unrelated legacy/agent records remain the canonical tool's concern.
197 if path.name.startswith("work-"):
198 found.append((path, exc))
199 return found
200
201
202def _worktree_bindings(cwd: Path) -> dict[Path, tuple[str | None, str | None]]:
203 """Return registered path -> (branch, HEAD) bindings from Git porcelain."""
204 done = run_git_readonly(["worktree", "list", "--porcelain"], cwd=cwd)
205 if not done.ok:
206 fail("git worktree inventory failed")
207 result: dict[Path, tuple[str | None, str | None]] = {}
208 path: Path | None = None
209 head: str | None = None
210 branch: str | None = None
211 for line in [*done.stdout.splitlines(), ""]:
212 if not line:
213 if path is not None:
214 result[lexical_absolute(path)] = (branch, head)
215 path, head, branch = None, None, None
216 elif line.startswith("worktree "):
217 path = Path(line.removeprefix("worktree "))
218 elif line.startswith("HEAD "):
219 head = line.removeprefix("HEAD ")
220 elif line.startswith("branch refs/heads/"):
221 branch = line.removeprefix("branch refs/heads/")
222 return result
223
224
225def classify(claim: Claim, cwd: Path) -> str:
226 """Verify that the registered worktree is attached to the claimed branch."""
227 try:
228 info = claim.worktree.lstat()
229 except FileNotFoundError:
230 return STALE
231 if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
232 return FORGED
233 binding = _worktree_bindings(cwd).get(lexical_absolute(claim.worktree))
234 if binding is None:
235 return FOREIGN
236 branch, registered_head = binding
237 if branch != claim.branch or registered_head is None:
238 return FOREIGN
239 return _classify_repository(claim, cwd, registered_head)
240
241
242def _classify_repository(claim: Claim, cwd: Path, registered_head: str) -> str:
243 """Verify repository identity and immutable object bindings for one claim."""
244 try:
245 caller_repo = discover_repo(cwd)
246 claim_repo = discover_repo(claim.worktree)
247 except WorkError:
248 return FORGED
249 if claim_repo.common_dir != caller_repo.common_dir:
250 return FORGED
251 head = resolve_commit("HEAD", cwd=claim.worktree)
252 base = resolve_commit(claim.base_commit, cwd=claim.worktree)
253 if head != registered_head or base != claim.base_commit:
254 return FORGED
255 return READY
256
257
258def recovery_command(claim: Claim, repo_root: Path, *, stale: bool) -> str:
259 """Return one shell-quoted canonical recovery command for human review."""
260 action = "forget" if stale else "release"
261 return shlex.join(
262 ["/bin/bash", "-p", str(repo_root / "scripts/dev/agent_workspace.sh"), action, claim.name]
263 )