feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
Executable
+232
@@ -0,0 +1,232 @@
|
||||
"""Bounded local records and predictable output; no network or AI dependency."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[3]
|
||||
MAX_JSON_BYTES = 8 * 1024 * 1024
|
||||
IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\Z")
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def canonical(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False
|
||||
).encode()
|
||||
|
||||
|
||||
def digest(value: object) -> str:
|
||||
return hashlib.sha256(canonical(value)).hexdigest()
|
||||
|
||||
|
||||
def identifier(value: str) -> str:
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or not IDENTIFIER.fullmatch(value)
|
||||
or value in {".", ".."}
|
||||
):
|
||||
raise ValueError("Invalid record identifier")
|
||||
return value
|
||||
|
||||
|
||||
def state_root(workspace_root: Path, state_dir: Path | None = None) -> Path:
|
||||
base = (
|
||||
state_dir
|
||||
or Path(os.environ.get("XDG_STATE_HOME", str(Path.home() / ".local/state")))
|
||||
/ "govoplan/devkit"
|
||||
)
|
||||
# Preserve spelling until symlink validation; resolving here would hide an unsafe alias.
|
||||
base = Path(os.path.abspath(os.fspath(base.expanduser())))
|
||||
return base / ("workspace-" + digest(str(workspace_root.resolve()))[:24])
|
||||
|
||||
|
||||
def reject_symlinks(path: Path) -> None:
|
||||
for item in (path, *path.parents):
|
||||
if item.is_symlink():
|
||||
raise ValueError("State and evidence paths must not contain symlinks")
|
||||
|
||||
|
||||
def private_directory(path: Path) -> None:
|
||||
reject_symlinks(path)
|
||||
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
reject_symlinks(path)
|
||||
metadata = path.stat()
|
||||
if not stat.S_ISDIR(metadata.st_mode) or (
|
||||
hasattr(os, "getuid") and metadata.st_uid != os.getuid()
|
||||
):
|
||||
raise ValueError("State directory must be owned by the current user")
|
||||
path.chmod(0o700)
|
||||
|
||||
|
||||
def read_bounded_bytes(path: Path, max_bytes: int = MAX_JSON_BYTES) -> bytes:
|
||||
reject_symlinks(path)
|
||||
descriptor = os.open(
|
||||
path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
||||
)
|
||||
with os.fdopen(descriptor, "rb") as handle:
|
||||
metadata = os.fstat(handle.fileno())
|
||||
if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > max_bytes:
|
||||
raise ValueError("Evidence must be a bounded regular file")
|
||||
encoded = handle.read(max_bytes + 1)
|
||||
if len(encoded) > max_bytes:
|
||||
raise ValueError("Evidence file exceeds its size bound")
|
||||
return encoded
|
||||
|
||||
|
||||
def read_json(path: Path, max_bytes: int = MAX_JSON_BYTES) -> object:
|
||||
encoded = read_bounded_bytes(path, max_bytes)
|
||||
|
||||
def unique(pairs):
|
||||
result = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise ValueError("Duplicate JSON keys are not accepted")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
try:
|
||||
return json.loads(
|
||||
encoded,
|
||||
object_pairs_hook=unique,
|
||||
parse_constant=lambda _: (_ for _ in ()).throw(
|
||||
ValueError("Non-finite JSON number")
|
||||
),
|
||||
)
|
||||
except (RecursionError, UnicodeError) as exc:
|
||||
raise ValueError("JSON nesting or encoding is unsupported") from exc
|
||||
|
||||
|
||||
def atomic_text(path: Path, value: str, max_bytes: int = MAX_JSON_BYTES) -> None:
|
||||
encoded = value.encode("utf-8")
|
||||
if len(encoded) > max_bytes:
|
||||
raise ValueError("Output exceeds its size bound")
|
||||
reject_symlinks(path.parent)
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
reject_symlinks(path.parent)
|
||||
reject_symlinks(path)
|
||||
if path.exists() and not path.is_file():
|
||||
raise ValueError("Output target must be a regular file")
|
||||
descriptor, temporary = tempfile.mkstemp(prefix=".devkit-", dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
os.fchmod(handle.fileno(), 0o600)
|
||||
handle.write(encoded)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, path)
|
||||
directory = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
def atomic_json(path: Path, payload: object) -> None:
|
||||
atomic_text(
|
||||
path,
|
||||
json.dumps(
|
||||
payload, indent=2, sort_keys=True, ensure_ascii=True, allow_nan=False
|
||||
)
|
||||
+ "\n",
|
||||
)
|
||||
|
||||
|
||||
def redact(text: str) -> str:
|
||||
"""Best-effort display hygiene, not permission to include secrets in commands."""
|
||||
for key, value in os.environ.items():
|
||||
if (
|
||||
re.search(r"TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY", key, re.I)
|
||||
and len(value) >= 4
|
||||
):
|
||||
text = text.replace(value, "[redacted]")
|
||||
text = re.sub(r"(?im)(authorization\s*[:=]\s*)([^\r\n]+)", r"\1[redacted]", text)
|
||||
text = re.sub(r"(?i)(https?://)[^/\s:@]+:[^/\s@]+@", r"\1[redacted]@", text)
|
||||
text = re.sub(
|
||||
r"(?i)((?:token|password|secret|api[_-]?key)\s*[=:]\s*)[^\s,;]+",
|
||||
r"\1[redacted]",
|
||||
text,
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def redact_argv(argv: list[str]) -> list[str]:
|
||||
result, hide_next = [], False
|
||||
for argument in argv:
|
||||
if hide_next:
|
||||
result.append("[redacted]")
|
||||
hide_next = False
|
||||
continue
|
||||
if re.fullmatch(
|
||||
r"--?(?:password|passwd|token|secret|api[-_]key|access[-_]token|authorization)",
|
||||
argument,
|
||||
re.I,
|
||||
):
|
||||
hide_next = True
|
||||
result.append(redact(argument))
|
||||
return result
|
||||
|
||||
|
||||
def safe_output(value):
|
||||
"""Redact presentation, not immutable identity hashes or execution inputs."""
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: redact_argv(item)
|
||||
if key == "argv"
|
||||
and isinstance(item, list)
|
||||
and all(isinstance(arg, str) for arg in item)
|
||||
else "[redacted]"
|
||||
if re.fullmatch(
|
||||
r"password|passwd|token|secret|api[_-]?key|authorization",
|
||||
str(key),
|
||||
re.I,
|
||||
)
|
||||
else safe_output(item)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [safe_output(item) for item in value]
|
||||
return redact(value) if isinstance(value, str) else value
|
||||
|
||||
|
||||
@contextmanager
|
||||
def resource_lock(directory: Path, name: str, timeout: float = 0):
|
||||
"""Host-local advisory lock, released by the OS even after a process crash."""
|
||||
import fcntl
|
||||
|
||||
private_directory(directory)
|
||||
path = directory / (hashlib.sha256(name.encode()).hexdigest() + ".lock")
|
||||
reject_symlinks(path)
|
||||
descriptor = os.open(
|
||||
path, os.O_CREAT | os.O_RDWR | getattr(os, "O_NOFOLLOW", 0), 0o600
|
||||
)
|
||||
try:
|
||||
if not stat.S_ISREG(os.fstat(descriptor).st_mode):
|
||||
raise ValueError("Lock target must be a regular file")
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
try:
|
||||
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
break
|
||||
except BlockingIOError:
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError(f"Resource is busy: {name}") from None
|
||||
time.sleep(min(0.1, max(0, deadline - time.monotonic())))
|
||||
yield
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
Reference in New Issue
Block a user