← Files note Workspace|ROGNALIAARCHIVED FILE
skills/note-workspace-setup/scripts/record_role_task_binding.py
7.04 KB · Oct 3, 2026 · 06:35 UTC
#!/usr/bin/env python3
"""Record one ready user-visible task binding in a local workspace."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Optional
from task_binding_common import (
SCHEMA_VERSION,
TaskBindingError,
atomic_write_events,
canonical_hash,
consume_verified_challenge,
exclusive_lock,
load_events,
load_json,
normalize_input,
parse_timestamp,
expected_replacement,
require_verified_challenge,
resolve_workspace,
)
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Record one ready role/task binding after external task setup."
)
parser.add_argument("workspace", help="Absolute path to the user workspace")
parser.add_argument("--config", required=True, help="Binding JSON path, or - for stdin")
parser.add_argument("--timestamp", help="ISO 8601 binding timestamp override")
return parser
def main(argv: Optional[list[str]] = None) -> int:
args = _parser().parse_args(argv)
try:
workspace, manifest, settings = resolve_workspace(args.workspace)
config = normalize_input(
load_json(args.config), settings, workspace, manifest
)
bound_at, moment = parse_timestamp(args.timestamp)
registry = workspace / "strategy/role-task-bindings.jsonl"
mode = settings["task_topology"]["mode"]
generation = settings["task_topology"]["binding_generation"]
_, ready_moment = parse_timestamp(
config["ready_receipt"]["observed_at"]
)
_, verified_moment = parse_timestamp(
config["ready_receipt"]["verified_at"]
)
if ready_moment > moment:
raise TaskBindingError(
"ready receipt must be observed before the binding is recorded"
)
if verified_moment > moment:
raise TaskBindingError(
"ready receipt must be verified before the binding is recorded"
)
payload_sha256 = canonical_hash(
{
"task_mode": mode,
"binding_generation": generation,
"binding": config,
}
)
with exclusive_lock(workspace / "strategy/.task-binding.lock", bound_at):
events = load_events(workspace, manifest, settings)
if any(
event["binding_generation"] == generation
and event["task_mode"] != mode
for event in events
):
raise TaskBindingError(
"task mode changed without incrementing binding_generation"
)
for event in events:
if event["request_id"] != config["request_id"]:
continue
if event["payload_sha256"] != payload_sha256:
raise TaskBindingError(
"request_id is already used for a different binding"
)
challenge = require_verified_challenge(
workspace,
manifest,
settings,
config,
allow_consumed_event_id=event["event_id"],
)
if challenge["status"] == "verified":
consume_verified_challenge(
workspace,
challenge,
event_id=event["event_id"],
consumed_at=event["bound_at"],
)
print(
json.dumps(
{
"status": "duplicate",
"event_id": event["event_id"],
"role": event["role"],
"slot": event["slot"],
"validation": "pass",
"external_actions": [],
},
ensure_ascii=False,
indent=2,
)
)
return 0
challenge_proof = config["ready_receipt"]["challenge_proof_sha256"]
if any(
event["ready_receipt"]["challenge_proof_sha256"]
== challenge_proof
for event in events
):
raise TaskBindingError(
"ready challenge proof is already used by another binding"
)
challenge = require_verified_challenge(
workspace, manifest, settings, config
)
current = expected_replacement(events, config, mode, generation)
if current is None and config["replaces_event_id"] is not None:
raise TaskBindingError("replaces_event_id refers to no active binding")
if current is not None:
if config["replaces_event_id"] != current["event_id"]:
raise TaskBindingError(
"replacing an active slot or reusing an active task requires its event_id"
)
event_id = (
"task-binding-"
+ moment.strftime("%Y%m%d")
+ "-"
+ canonical_hash(
{
"workspace_id": manifest["workspace_id"],
"bound_at": bound_at,
"task_mode": mode,
"binding_generation": generation,
"config": config,
}
)[:12]
)
event = {
**config,
"schema_version": SCHEMA_VERSION,
"event_id": event_id,
"workspace_id": manifest["workspace_id"],
"task_mode": mode,
"binding_generation": generation,
"bound_at": bound_at,
"payload_sha256": payload_sha256,
}
candidate = events + [event]
atomic_write_events(registry, candidate)
load_events(workspace, manifest, settings)
consume_verified_challenge(
workspace,
challenge,
event_id=event_id,
consumed_at=bound_at,
)
print(
json.dumps(
{
"status": "created" if current is None else "replaced",
"event_id": event_id,
"role": config["role"],
"slot": config["slot"],
"binding_generation": generation,
"validation": "pass",
"external_actions": [],
},
ensure_ascii=False,
indent=2,
)
)
return 0
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(
json.dumps({"status": "error", "error": str(exc)}, ensure_ascii=False),
file=sys.stderr,
)
return 2
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 7b10b882006a6dcde8dc8c63ebfea4db688d47eec88a2b3e6f1ef90e160918d0