← Files note Workspace|ROGNALIAARCHIVED FILE
skills/note-workspace-setup/scripts/standing_instruction_common.py
17.3 KB · Oct 3, 2026 · 06:35 UTC
#!/usr/bin/env python3
"""Validated current state and append-only history for standing instructions."""
from __future__ import annotations
import hashlib
import json
import os
import re
import tempfile
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple
from workspace_common import render_standing_instructions
SCHEMA_VERSION = 1
PRODUCT_ID = "note-workspace"
CATEGORIES = {"workflow", "editorial", "image", "safety", "other"}
REQUEST_ID_PATTERN = re.compile(r"[a-z0-9][a-z0-9._:-]{7,127}")
INSTRUCTION_ID_PATTERN = re.compile(r"rule-[a-z0-9][a-z0-9-]{2,63}")
EVENT_ID_PATTERN = re.compile(r"standing-[0-9]{8}-[0-9a-f]{12}")
SHA256_PATTERN = re.compile(r"[0-9a-f]{64}")
INPUT_KEYS = {
"schema_version",
"request_id",
"reason",
"approved_by_user",
"instructions",
}
INSTRUCTION_KEYS = {"instruction_id", "category", "text"}
STATE_KEYS = {
"schema_version",
"workspace_id",
"revision",
"updated_at",
"latest_event_id",
"instructions",
}
EVENT_KEYS = {
"schema_version",
"event_id",
"request_id",
"revision",
"changed_at",
"reason",
"approved_by_user",
"before",
"after",
"before_sha256",
"after_sha256",
"payload_sha256",
}
class StandingInstructionError(ValueError):
"""Raised when standing instructions cannot be changed safely."""
def _fsync_directory(path: Path) -> None:
"""Persist directory metadata where Python supports opening directories."""
if os.name == "nt":
return
descriptor = os.open(path, os.O_RDONLY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def canonical_hash(value: Any) -> str:
encoded = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def json_text(value: Dict[str, Any]) -> str:
return json.dumps(value, ensure_ascii=False, indent=2) + "\n"
def parse_timestamp(value: Optional[str]) -> Tuple[str, datetime]:
if value is None:
moment = datetime.now().astimezone().replace(microsecond=0)
else:
try:
moment = datetime.fromisoformat(value.replace("Z", "+00:00"))
except (AttributeError, ValueError) as exc:
raise StandingInstructionError(
"timestamp must be ISO 8601 with a timezone"
) from exc
if moment.tzinfo is None:
raise StandingInstructionError("timestamp must include a timezone")
moment = moment.replace(microsecond=0)
normalized = moment.isoformat()
if normalized.endswith("+00:00"):
normalized = normalized[:-6] + "Z"
return normalized, moment
def _object(value: Any, label: str) -> Dict[str, Any]:
if not isinstance(value, dict):
raise StandingInstructionError(f"{label} must be an object")
return dict(value)
def _exact_keys(value: Dict[str, Any], label: str, keys: set[str]) -> None:
unknown = sorted(set(value) - keys)
missing = sorted(keys - set(value))
if unknown:
raise StandingInstructionError(
f"{label} has unknown fields: {', '.join(unknown)}"
)
if missing:
raise StandingInstructionError(
f"{label} is missing fields: {', '.join(missing)}"
)
def _one_line(value: Any, label: str, maximum: int = 2000) -> str:
if not isinstance(value, str):
raise StandingInstructionError(f"{label} must be a string")
cleaned = value.strip()
if not cleaned or "\n" in cleaned or "\r" in cleaned:
raise StandingInstructionError(f"{label} must be non-empty one-line text")
if len(cleaned) > maximum:
raise StandingInstructionError(f"{label} is too long")
return cleaned
def normalize_instructions(value: Any, label: str = "instructions") -> List[Dict[str, str]]:
if not isinstance(value, list):
raise StandingInstructionError(f"{label} must be an array")
if len(value) > 100:
raise StandingInstructionError(f"{label} must contain at most 100 items")
output: List[Dict[str, str]] = []
ids: set[str] = set()
for index, raw in enumerate(value):
item_label = f"{label}[{index}]"
item = _object(raw, item_label)
_exact_keys(item, item_label, INSTRUCTION_KEYS)
instruction_id = item.get("instruction_id")
if (
not isinstance(instruction_id, str)
or not INSTRUCTION_ID_PATTERN.fullmatch(instruction_id)
):
raise StandingInstructionError(
f"{item_label}.instruction_id has an invalid format"
)
if instruction_id in ids:
raise StandingInstructionError(f"{label} has duplicate instruction_id")
ids.add(instruction_id)
category = item.get("category")
if category not in CATEGORIES:
raise StandingInstructionError(f"{item_label}.category is invalid")
output.append(
{
"instruction_id": instruction_id,
"category": category,
"text": _one_line(item.get("text"), f"{item_label}.text"),
}
)
return output
def normalize_config(raw: Any) -> Dict[str, Any]:
config = _object(raw, "config")
_exact_keys(config, "config", INPUT_KEYS)
if config.get("schema_version") != SCHEMA_VERSION:
raise StandingInstructionError(f"schema_version must be {SCHEMA_VERSION}")
request_id = config.get("request_id")
if not isinstance(request_id, str) or not REQUEST_ID_PATTERN.fullmatch(request_id):
raise StandingInstructionError("request_id has an invalid format")
if config.get("approved_by_user") is not True:
raise StandingInstructionError(
"approved_by_user must be true after the standing instruction read-back"
)
return {
"schema_version": SCHEMA_VERSION,
"request_id": request_id,
"reason": _one_line(config.get("reason"), "reason"),
"approved_by_user": True,
"instructions": normalize_instructions(config.get("instructions")),
}
def load_json(path: str) -> Dict[str, Any]:
import sys
if path == "-":
value = json.load(sys.stdin)
else:
with Path(path).open("r", encoding="utf-8") as handle:
value = json.load(handle)
return _object(value, "config")
def resolve_workspace(raw: str) -> Tuple[Path, Dict[str, Any]]:
provided = Path(raw).expanduser()
if provided.is_symlink():
raise StandingInstructionError("workspace root must not be a symlink")
workspace = provided.resolve()
if not workspace.is_dir():
raise StandingInstructionError("workspace does not exist or is not a directory")
manifest_path = workspace / "workspace.json"
for relative in (
"profile/standing-instructions.md",
"profile/standing-instructions.json",
"profile/standing-instructions-history.jsonl",
):
path = workspace / relative
if not path.is_file() or path.is_symlink():
raise StandingInstructionError(f"{relative} is missing or unsafe")
try:
manifest = _object(
json.loads(manifest_path.read_text(encoding="utf-8")),
"workspace.json",
)
except json.JSONDecodeError as exc:
raise StandingInstructionError("workspace.json is invalid JSON") from exc
if manifest.get("schema_version") != SCHEMA_VERSION:
raise StandingInstructionError("workspace schema_version is not supported")
if manifest.get("product_id") != PRODUCT_ID:
raise StandingInstructionError("workspace product_id mismatch")
if manifest.get("data_owner") != "user" or manifest.get("status") != "ready":
raise StandingInstructionError("workspace is not ready or user-owned")
return workspace, manifest
def _event_config(event: Dict[str, Any]) -> Dict[str, Any]:
return {
"schema_version": event.get("schema_version"),
"request_id": event.get("request_id"),
"reason": event.get("reason"),
"approved_by_user": event.get("approved_by_user"),
"instructions": event.get("after"),
}
def load_history(
workspace: Path,
manifest: Dict[str, Any],
) -> List[Dict[str, Any]]:
path = workspace / "profile/standing-instructions-history.jsonl"
events: List[Dict[str, Any]] = []
current: List[Dict[str, str]] = []
requests: set[str] = set()
event_ids: set[str] = set()
previous_moment: Optional[datetime] = None
with path.open("r", encoding="utf-8") as handle:
for number, line in enumerate(handle, start=1):
if not line.strip():
continue
try:
event = _object(json.loads(line), f"{path}:{number}")
except json.JSONDecodeError as exc:
raise StandingInstructionError(f"{path}:{number} is invalid JSON") from exc
_exact_keys(event, f"{path}:{number}", EVENT_KEYS)
if event.get("schema_version") != SCHEMA_VERSION:
raise StandingInstructionError(f"{path}:{number} schema_version mismatch")
revision = event.get("revision")
if (
isinstance(revision, bool)
or not isinstance(revision, int)
or revision != number
):
raise StandingInstructionError(f"{path}:{number} revision mismatch")
event_id = event.get("event_id")
if (
not isinstance(event_id, str)
or not EVENT_ID_PATTERN.fullmatch(event_id)
or event_id in event_ids
):
raise StandingInstructionError(f"{path}:{number} invalid event_id")
event_ids.add(event_id)
config = normalize_config(_event_config(event))
if config["request_id"] in requests:
raise StandingInstructionError(f"{path}:{number} duplicate request_id")
requests.add(config["request_id"])
before = normalize_instructions(event.get("before"), "before")
after = config["instructions"]
if before != current:
raise StandingInstructionError(f"{path}:{number} history chain mismatch")
if event.get("before_sha256") != canonical_hash(before):
raise StandingInstructionError(f"{path}:{number} before_sha256 mismatch")
if event.get("after_sha256") != canonical_hash(after):
raise StandingInstructionError(f"{path}:{number} after_sha256 mismatch")
if event.get("payload_sha256") != canonical_hash(config):
raise StandingInstructionError(f"{path}:{number} payload_sha256 mismatch")
changed_at, moment = parse_timestamp(event.get("changed_at"))
if event.get("changed_at") != changed_at:
raise StandingInstructionError(f"{path}:{number} changed_at is not normalized")
if previous_moment is not None and moment < previous_moment:
raise StandingInstructionError(f"{path}:{number} changed_at goes backwards")
previous_moment = moment
current = after
events.append(event)
return events
def load_state(workspace: Path, manifest: Dict[str, Any]) -> Dict[str, Any]:
path = workspace / "profile/standing-instructions.json"
try:
state = _object(json.loads(path.read_text(encoding="utf-8")), str(path))
except json.JSONDecodeError as exc:
raise StandingInstructionError(f"{path} is invalid JSON") from exc
_exact_keys(state, str(path), STATE_KEYS)
if state.get("schema_version") != SCHEMA_VERSION:
raise StandingInstructionError("standing instruction state schema mismatch")
if state.get("workspace_id") != manifest.get("workspace_id"):
raise StandingInstructionError("standing instruction workspace_id mismatch")
revision = state.get("revision")
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 0:
raise StandingInstructionError("standing instruction revision is invalid")
state["instructions"] = normalize_instructions(state.get("instructions"))
updated_at, _ = parse_timestamp(state.get("updated_at"))
if state.get("updated_at") != updated_at:
raise StandingInstructionError("standing instruction updated_at is not normalized")
latest = state.get("latest_event_id")
if latest is not None and (
not isinstance(latest, str) or not EVENT_ID_PATTERN.fullmatch(latest)
):
raise StandingInstructionError("standing instruction latest_event_id is invalid")
return state
def validate_data(
workspace: Path,
manifest: Dict[str, Any],
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
state = load_state(workspace, manifest)
events = load_history(workspace, manifest)
if events:
latest = events[-1]
if state["revision"] != latest["revision"]:
raise StandingInstructionError("standing instruction state revision mismatch")
if state["updated_at"] != latest["changed_at"]:
raise StandingInstructionError("standing instruction state time mismatch")
if state["latest_event_id"] != latest["event_id"]:
raise StandingInstructionError("standing instruction latest event mismatch")
if state["instructions"] != latest["after"]:
raise StandingInstructionError("standing instruction state history mismatch")
else:
created_at, _ = parse_timestamp(manifest.get("created_at"))
if state != {
"schema_version": SCHEMA_VERSION,
"workspace_id": manifest["workspace_id"],
"revision": 0,
"updated_at": created_at,
"latest_event_id": None,
"instructions": [],
}:
raise StandingInstructionError("initial standing instruction state mismatch")
expected_markdown = render_standing_instructions(state["instructions"]).rstrip() + "\n"
actual_markdown = (
workspace / "profile/standing-instructions.md"
).read_text(encoding="utf-8")
if actual_markdown != expected_markdown:
raise StandingInstructionError("standing instruction Markdown does not match state")
return state, events
@contextmanager
def exclusive_lock(path: Path, created_at: str) -> Iterator[None]:
payload = json.dumps(
{"created_at": created_at, "pid": os.getpid()}, ensure_ascii=False
).encode("utf-8")
try:
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError as exc:
raise StandingInstructionError(
f"another write may be active; inspect the lock before retrying: {path}"
) from exc
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
yield
finally:
try:
path.unlink()
except FileNotFoundError:
pass
def atomic_replace_many(contents: Dict[Path, bytes]) -> None:
originals: Dict[Path, bytes] = {}
modes: Dict[Path, int] = {}
temporaries: Dict[Path, Path] = {}
replaced: List[Path] = []
for path in contents:
if not path.is_file() or path.is_symlink():
raise StandingInstructionError(f"write target is missing or unsafe: {path}")
originals[path] = path.read_bytes()
modes[path] = path.stat().st_mode & 0o777
try:
for path, content in contents.items():
descriptor, temporary = tempfile.mkstemp(
prefix=f".{path.name}.", dir=str(path.parent)
)
temporary_path = Path(temporary)
temporaries[path] = temporary_path
os.fchmod(descriptor, modes[path])
with os.fdopen(descriptor, "wb") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
for path in contents:
os.replace(temporaries[path], path)
replaced.append(path)
for parent in sorted({path.parent for path in contents}, key=str):
_fsync_directory(parent)
except OSError as exc:
rollback_errors: List[str] = []
for path in reversed(replaced):
try:
descriptor, temporary = tempfile.mkstemp(
prefix=f".{path.name}.rollback.", dir=str(path.parent)
)
os.fchmod(descriptor, modes[path])
with os.fdopen(descriptor, "wb") as handle:
handle.write(originals[path])
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
except OSError as rollback_exc:
rollback_errors.append(f"{path}: {rollback_exc}")
detail = (
"; rollback failed for " + ", ".join(rollback_errors)
if rollback_errors
else ""
)
raise StandingInstructionError(
f"standing instruction update failed: {exc}{detail}"
) from exc
finally:
for temporary_path in temporaries.values():
try:
temporary_path.unlink()
except FileNotFoundError:
pass
def render_history(events: List[Dict[str, Any]]) -> bytes:
return "".join(
json.dumps(event, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
for event in events
).encode("utf-8")
SHA-256: 91fa373fe24a013df9c2ccf83bc573096a1593f8caeb9a7da299da990e9ab55b