Files
Agent-System-Dev/scripts/workspace/common.py
T
weichen 93ac63fb3d Compose immutable dependency sources and preserve branch transitions
Why:
Replace repeated submodule source pointers with consumer-owned language versions and fixed application sources, keeping independent repository edits and existing branches intact.

Evidence:
SELF task receipts contain startup/sync evidence, real Go and npm consumption, Native overlay/unlink, safe transition/ref preservation, credential boundary and historical build regressions. Exact committed full composition and RDM checks follow this checkpoint; formal acceptance and GATE CI remain pending.

Impact:
Add isolated build/test composition, source provenance and safe owner refresh, adapt governance and service build callers, remove replaced index links while retaining original Git metadata, and adopt reusable dependency standards.

Refs:
Documents approved dependency-workspace r1/R1-R15 and U1-U7 implementation plan; feature/dependency-workspace task card.
2026-10-13 17:18:07 +00:00

76 lines
2.5 KiB
Python

import hashlib
import json
import os
from pathlib import PurePosixPath
import subprocess
import tempfile
class WorkspaceError(Exception):
pass
def run(args, cwd, env=None, check=True):
result = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True,
timeout=180)
if check and result.returncode:
raise WorkspaceError(f"{args[0]} failed ({result.returncode}): {result.stderr.strip() or result.stdout.strip()}")
return result
def relative_path(value, allow_dot=False):
if not isinstance(value, str) or not value or "\\" in value or any(ord(c) < 32 for c in value):
raise WorkspaceError("invalid relative path")
path = PurePosixPath(value)
if path.is_absolute() or ".." in path.parts or (value == "." and not allow_dot):
raise WorkspaceError(f"unsafe relative path: {value}")
return path
def safe_path(root, value, allow_dot=False):
relative_path(value, allow_dot)
path = root / value
# Refuse symlink path components even when a link currently points inside root.
# This avoids creating a second apparent owner and later retargeting escapes.
current = root
for part in PurePosixPath(value).parts:
current /= part
if current.is_symlink():
raise WorkspaceError(f"symlink path is not owned by this workspace: {value}")
if not path.resolve().is_relative_to(root.resolve()):
raise WorkspaceError(f"path escapes workspace: {value}")
return path
def read_json(path):
if path.is_symlink():
raise WorkspaceError(f"refusing symlink metadata: {path}")
try:
return json.loads(path.read_text())
except (OSError, ValueError) as error:
raise WorkspaceError(f"cannot read {path.name}: {error}") from error
def atomic_write(path, content):
if path.is_symlink():
raise WorkspaceError(f"refusing symlink output: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary = tempfile.mkstemp(prefix="." + path.name + "-", dir=path.parent)
try:
with os.fdopen(descriptor, "w") as stream:
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def write_json(path, value):
atomic_write(path, json.dumps(value, indent=2, sort_keys=True) + "\n")
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()