build: derive coordination receiver from staged owner sources
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a15374-f59f-7a31-86d2-d23b5d2ed181
This commit is contained in:
parent
4c7f32adc7
commit
bde143e030
9 changed files with 284 additions and 22 deletions
|
|
@ -3,6 +3,8 @@ CUSTODIAN_SOURCE ?= /home/worsch/the-custodian
|
|||
GENERATED_REPO_SLUG ?= net-kingdom
|
||||
.PHONY: generated-work-records generated-work-records-check
|
||||
generated-work-records:
|
||||
python3 tools/coordination-frame/staged.py --repo .
|
||||
python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)"
|
||||
generated-work-records-check:
|
||||
python3 tools/coordination-frame/staged.py --repo . --check
|
||||
python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)" --check
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -401,3 +401,9 @@ generated-work-records:
|
|||
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records
|
||||
generated-work-records-check:
|
||||
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records-check
|
||||
|
||||
.PHONY: coordination-frame-staged coordination-frame-staged-check
|
||||
coordination-frame-staged:
|
||||
python3 tools/coordination-frame/staged.py --repo .
|
||||
coordination-frame-staged-check:
|
||||
python3 tools/coordination-frame/staged.py --repo . --check
|
||||
|
|
|
|||
10
SCOPE.md
10
SCOPE.md
|
|
@ -244,3 +244,13 @@ WORK-RECORDS.md with the selected source paths. Existing generated-only guards
|
|||
and exact-scope secret scans remain in place. This is repository maintenance,
|
||||
not a runtime service, authorization decision, deployment or publication.
|
||||
Other index generators and hub-derived briefs retain their owner boundaries.
|
||||
|
||||
## Coordination receiver generation — 2026-10-19
|
||||
|
||||
The shared opt-in commit recipe now compiles the local proposed Approval Engine
|
||||
receiver from staged NetKingdom/coordination-engine/approval-engine sources,
|
||||
then renders WORK-RECORDS. Dedicated Make generation/check targets preserve
|
||||
unstaged authored work and refuse receiver conflicts or alternate-index writes.
|
||||
Only the local receiver is staged; sibling sources and runtime registries are
|
||||
untouched. Missing owner source checkouts fail closed. Multi-owner binding
|
||||
publication and actual receiver activation retain their existing owner gates.
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@
|
|||
| task | NK-WP-0035-T04 | progress | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||
| task | NK-WP-0035-T05 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||
| task | NK-WP-0035-T06 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||
| task | NK-WP-0035-T07 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md |
|
||||
| task | NK-WP-0036-T01 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||
| task | NK-WP-0036-T02 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||
| task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |
|
||||
|
|
|
|||
|
|
@ -40,3 +40,25 @@ This is the first rollout of a shared coordination setting. Other ecosystem
|
|||
coordination consumers must adopt explicit frame multipliers through their own
|
||||
configuration. Transport polling, security validity and credential leases are
|
||||
not silently rescaled by a coordination setting.
|
||||
|
||||
## Staged local receiver generation
|
||||
|
||||
`make coordination-frame-staged` compiles and stages only the NetKingdom receiver
|
||||
JSON from Git-index snapshots of its renderer, coordination-engine's frame/API
|
||||
and approval-engine's cadence. It never edits those sibling checkouts. Unstaged
|
||||
owner source edits are excluded, and unstaged receiver edits, missing/conflicted
|
||||
or symlink inputs and alternate-index writes are refused. Stage the reviewed
|
||||
adapter before its first run. `make coordination-frame-staged-check` is read-only.
|
||||
|
||||
The existing opt-in pre-commit recipe runs this adapter before WORK-RECORDS
|
||||
rendering and the existing configured scanner. Missing sibling source checkouts
|
||||
fail closed. Full multi-owner `render.py --write` remains an explicit preparation
|
||||
command and is never run against real sibling worktrees by the commit hook.
|
||||
`render.py --receiver-only` limits rendering to the one local proposed receiver;
|
||||
none of these commands activate the receiver or publish runtime changes.
|
||||
|
||||
For a partial commit, generate from the selected staged sources first and include
|
||||
the receiver with those sources; `git commit --only` cannot generate missing
|
||||
changes into its temporary index. Cross-repo owner commits still need their own
|
||||
review and publication; local index snapshots are source evidence, not release
|
||||
or operational acceptance.
|
||||
|
|
|
|||
|
|
@ -69,7 +69,27 @@ def bind_frame_annotation(text, digest, metadata_prefix):
|
|||
return updated
|
||||
|
||||
|
||||
def compile_bindings(workspace: Path):
|
||||
def receiver_binding(frame, provenance, approval_text):
|
||||
receiver = {
|
||||
"status": "proposed-not-applied",
|
||||
"coordination_frame": provenance,
|
||||
"sender": "approval-engine",
|
||||
"source": "approval-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"may_read": False,
|
||||
"emission_cadence_ref": "approval-engine/cadence.yaml",
|
||||
"emission_cadence_sha256": hashlib.sha256(approval_text.encode()).hexdigest(),
|
||||
"heartbeat_classes": {
|
||||
name: frame.seconds(2) for name in ("issuance", "use", "supersession", "revocation")
|
||||
},
|
||||
"receiver_grace_factor": 1.5,
|
||||
"effective_deadline_seconds": frame.seconds(3),
|
||||
"preserve_existing_credentials_and_other_registry_fields": True,
|
||||
}
|
||||
return json.dumps(receiver, indent=2) + "\n"
|
||||
|
||||
|
||||
def compile_bindings(workspace: Path, *, receiver_only=False):
|
||||
frame = owner_frame(workspace)
|
||||
digest = hashlib.sha256((workspace / FRAME_REF).read_bytes()).hexdigest()
|
||||
provenance = {
|
||||
|
|
@ -97,6 +117,9 @@ def compile_bindings(workspace: Path):
|
|||
ext["effective_deadline_seconds"] = frame.seconds(3)
|
||||
approval["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance
|
||||
outputs[approval_path] = json.dumps(approval, indent=2) + "\n"
|
||||
if receiver_only:
|
||||
path = workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json"
|
||||
return frame, {path: receiver_binding(frame, provenance, outputs[approval_path])}
|
||||
|
||||
qonto_path = workspace / "qonto-assistant/specs/audit-emission-cadence.yaml"
|
||||
qonto = yaml.safe_load(qonto_path.read_text())
|
||||
|
|
@ -160,37 +183,20 @@ def compile_bindings(workspace: Path):
|
|||
fixture["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance
|
||||
outputs[fixture_path] = json.dumps(fixture, indent=2) + "\n"
|
||||
|
||||
receiver = {
|
||||
"status": "proposed-not-applied",
|
||||
"coordination_frame": provenance,
|
||||
"sender": "approval-engine",
|
||||
"source": "approval-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"may_read": False,
|
||||
"emission_cadence_ref": "approval-engine/cadence.yaml",
|
||||
"emission_cadence_sha256": hashlib.sha256(outputs[approval_path].encode()).hexdigest(),
|
||||
"heartbeat_classes": {
|
||||
name: frame.seconds(2) for name in ("issuance", "use", "supersession", "revocation")
|
||||
},
|
||||
"receiver_grace_factor": 1.5,
|
||||
"effective_deadline_seconds": frame.seconds(3),
|
||||
"preserve_existing_credentials_and_other_registry_fields": True,
|
||||
}
|
||||
outputs[
|
||||
workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json"
|
||||
] = json.dumps(receiver, indent=2) + "\n"
|
||||
outputs[workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json"] = receiver_binding(frame, provenance, outputs[approval_path])
|
||||
return frame, outputs
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--workspace", type=Path, default=ROOT.parent)
|
||||
parser.add_argument("--receiver-only", action="store_true")
|
||||
mode = parser.add_mutually_exclusive_group()
|
||||
mode.add_argument("--check", action="store_true")
|
||||
mode.add_argument("--write", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
frame, outputs = compile_bindings(args.workspace)
|
||||
frame, outputs = compile_bindings(args.workspace, receiver_only=args.receiver_only)
|
||||
changed = {
|
||||
p: text for p, text in outputs.items() if not p.exists() or p.read_text() != text
|
||||
}
|
||||
|
|
|
|||
100
tools/coordination-frame/staged.py
Normal file
100
tools/coordination-frame/staged.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Stage only the local receiver from indexed owner sources; never mutate siblings."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
OUTPUT = 'generated/coordination-frame/approval-engine-receiver.json'
|
||||
SELF = 'tools/coordination-frame/staged.py'
|
||||
INPUTS = {
|
||||
'net-kingdom': ['tools/coordination-frame/render.py'],
|
||||
'coordination-engine': ['config/coordination-frame.json', 'src/coordination_engine/timing.py'],
|
||||
'approval-engine': ['cadence.yaml'],
|
||||
}
|
||||
|
||||
|
||||
def git(repo, *args, default_index=False):
|
||||
env = dict(os.environ)
|
||||
if default_index:
|
||||
env.pop("GIT_INDEX_FILE", None)
|
||||
return subprocess.check_output(['git', '-C', str(repo), *args], stderr=subprocess.PIPE, env=env)
|
||||
|
||||
|
||||
def indexed(repo, path, *, default_index=False):
|
||||
rows = git(repo, 'ls-files', '--stage', '--', path, default_index=default_index).decode().splitlines()
|
||||
if len(rows) != 1:
|
||||
raise ValueError('missing or conflicted source: ' + path)
|
||||
metadata, name = rows[0].split('\t', 1)
|
||||
mode, oid, stage = metadata.split()
|
||||
if stage != '0' or mode not in ('100644', '100755') or name != path:
|
||||
raise ValueError('nonregular or conflicted source: ' + path)
|
||||
return git(repo, 'cat-file', 'blob', oid, default_index=default_index)
|
||||
|
||||
|
||||
def generate(repo, *, check=False):
|
||||
repo = Path(repo).resolve()
|
||||
workspace = repo.parent
|
||||
if Path(git(repo, 'rev-parse', '--show-toplevel').decode().strip()).resolve() != repo:
|
||||
raise ValueError('repository root required')
|
||||
if (repo / SELF).is_symlink() or (repo / SELF).read_bytes() != indexed(repo, SELF):
|
||||
raise ValueError('stage the reviewed adapter before executing it')
|
||||
target = repo / OUTPUT
|
||||
if any(p.is_symlink() for p in (target, *target.parents)):
|
||||
raise ValueError('redirected receiver output refused')
|
||||
prior = indexed(repo, OUTPUT)
|
||||
if target.read_bytes() != prior:
|
||||
raise ValueError('unstaged receiver edits; preserve or reconcile first')
|
||||
sources = []
|
||||
with tempfile.TemporaryDirectory(prefix='coordination-staged-') as directory:
|
||||
snapshot = Path(directory)
|
||||
for slug, paths in INPUTS.items():
|
||||
source_repo = repo if slug == 'net-kingdom' else workspace / slug
|
||||
if Path(git(source_repo, 'rev-parse', '--show-toplevel').decode().strip()).resolve() != source_repo.resolve():
|
||||
raise ValueError('owner repository root required')
|
||||
for path in paths:
|
||||
dest = snapshot / slug / path
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = indexed(source_repo, path, default_index=source_repo != repo)
|
||||
sources.append((source_repo, path, content))
|
||||
dest.write_bytes(content)
|
||||
subprocess.run([sys.executable, str(snapshot/'net-kingdom/tools/coordination-frame/render.py'),
|
||||
'--workspace', str(snapshot), '--receiver-only', '--write'],
|
||||
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
result = (snapshot/'net-kingdom'/OUTPUT).read_bytes()
|
||||
if any(indexed(owner, name, default_index=owner != repo) != content
|
||||
for owner, name, content in sources):
|
||||
raise ValueError("owner index changed during compilation")
|
||||
if result == prior:
|
||||
return False
|
||||
if check:
|
||||
raise ValueError('staged receiver drift; run make coordination-frame-staged')
|
||||
default_index = Path(git(repo, 'rev-parse', '--git-path', 'index', default_index=True).decode().strip())
|
||||
if not default_index.is_absolute():
|
||||
default_index = repo/default_index
|
||||
active_index = Path(os.environ.get('GIT_INDEX_FILE', str(default_index)))
|
||||
if not active_index.is_absolute():
|
||||
active_index = repo/active_index
|
||||
if active_index.resolve() != default_index.resolve():
|
||||
raise ValueError('alternate-index mutation refused; generate before commit --only')
|
||||
# Optimistic check: refuse if a source/index update raced compilation.
|
||||
if indexed(repo, OUTPUT) != prior or target.read_bytes() != prior:
|
||||
raise ValueError('receiver changed during compilation')
|
||||
target.write_bytes(result)
|
||||
git(repo, 'add', '--', OUTPUT)
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--repo', type=Path, default=Path.cwd())
|
||||
parser.add_argument('--check', action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
changed = generate(args.repo, check=args.check)
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as exc:
|
||||
parser.exit(1, 'Coordination generation refused: ' + str(exc) + '\n')
|
||||
print(json.dumps({'receiver_changed': changed, 'sibling_writes': False, 'live_operations': False}))
|
||||
85
tools/coordination-frame/tests/test_staged.py
Normal file
85
tools/coordination-frame/tests/test_staged.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SPEC = importlib.util.spec_from_file_location('staged_frame', ROOT/'tools/coordination-frame/staged.py')
|
||||
staged = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(staged)
|
||||
|
||||
|
||||
def git(repo, *args):
|
||||
return subprocess.check_output(['git','-C',str(repo),*args])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repos(tmp_path):
|
||||
workspace = ROOT.parent
|
||||
for slug, names in staged.INPUTS.items():
|
||||
repo=tmp_path/slug;repo.mkdir()
|
||||
git(repo,'init','-q')
|
||||
for name in names:
|
||||
dest=repo/name;dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
dest.write_bytes((workspace/slug/name).read_bytes())
|
||||
repo=tmp_path/'net-kingdom'
|
||||
for name in (staged.SELF,staged.OUTPUT):
|
||||
dest=repo/name;dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
dest.write_bytes((ROOT/name).read_bytes())
|
||||
for slug in staged.INPUTS:git(tmp_path/slug,'add','.')
|
||||
return tmp_path,repo
|
||||
|
||||
|
||||
def test_receiver_only_ignores_unstaged_sibling_source(repos):
|
||||
workspace,repo=repos
|
||||
path=workspace/'coordination-engine/config/coordination-frame.json'
|
||||
original=path.read_bytes();path.write_text('uncommitted invalid source')
|
||||
assert staged.generate(repo) is False
|
||||
assert path.read_text()=='uncommitted invalid source'
|
||||
assert git(workspace/'coordination-engine','show',':config/coordination-frame.json')==original
|
||||
|
||||
|
||||
def test_staged_frame_change_only_updates_local_receiver(repos):
|
||||
workspace,repo=repos
|
||||
path=workspace/'coordination-engine/config/coordination-frame.json'
|
||||
data=json.loads(path.read_text());data['frame_seconds']=600;path.write_text(json.dumps(data))
|
||||
git(path.parents[1],'add','config/coordination-frame.json')
|
||||
approval=workspace/'approval-engine/cadence.yaml';before=approval.read_bytes()
|
||||
with pytest.raises(ValueError,match='drift'):staged.generate(repo,check=True)
|
||||
assert staged.generate(repo)
|
||||
result=json.loads((repo/staged.OUTPUT).read_text())
|
||||
assert result['effective_deadline_seconds']==1800
|
||||
assert approval.read_bytes()==before
|
||||
assert staged.generate(repo,check=True) is False
|
||||
|
||||
|
||||
def test_unstaged_receiver_preserved(repos):
|
||||
_,repo=repos;p=repo/staged.OUTPUT;p.write_text('local edit')
|
||||
with pytest.raises(ValueError,match='unstaged receiver'):staged.generate(repo)
|
||||
assert p.read_text()=='local edit'
|
||||
|
||||
|
||||
def test_unstaged_adapter_refused(repos):
|
||||
_,repo=repos;p=repo/staged.SELF;p.write_text(p.read_text()+'\n# local edit\n')
|
||||
with pytest.raises(ValueError,match='stage the reviewed'):staged.generate(repo)
|
||||
|
||||
|
||||
def test_symlink_source_refused(repos):
|
||||
workspace,repo=repos;p=workspace/'approval-engine/cadence.yaml';p.unlink();p.symlink_to('/etc/passwd')
|
||||
git(p.parent,'add','cadence.yaml')
|
||||
with pytest.raises(ValueError,match='nonregular'):staged.generate(repo)
|
||||
|
||||
|
||||
def test_alternate_index_write_refused_and_sibling_default_index_used(repos,monkeypatch):
|
||||
workspace,repo=repos
|
||||
path=workspace/'coordination-engine/config/coordination-frame.json'
|
||||
data=json.loads(path.read_text());data['frame_seconds']=600;path.write_text(json.dumps(data))
|
||||
git(path.parents[1],'add','config/coordination-frame.json')
|
||||
alternate=repo/'.git/partial-index';alternate.write_bytes((repo/'.git/index').read_bytes())
|
||||
before=(repo/staged.OUTPUT).read_bytes()
|
||||
monkeypatch.setenv('GIT_INDEX_FILE',str(alternate))
|
||||
with pytest.raises(ValueError,match='alternate-index mutation'):staged.generate(repo)
|
||||
assert (repo/staged.OUTPUT).read_bytes()==before
|
||||
|
|
@ -10,7 +10,7 @@ owner: codex
|
|||
topic_slug: netkingdom
|
||||
planning_priority: P1
|
||||
created: "2026-09-04"
|
||||
updated: "2026-10-15"
|
||||
updated: "2026-10-19"
|
||||
related:
|
||||
- GH-DEC-2026-004
|
||||
- canon/standards/security-layer-model_v0.7.md
|
||||
|
|
@ -417,3 +417,33 @@ and collector implementation are no longer missing. Remaining work is actual
|
|||
elapsed-time capture, independent source/receiver correlation and observer
|
||||
acceptance. Local-identity remains explicitly incompatible, with no completeness
|
||||
claim. Detailed state: [owner acceptance plan](../docs/evidence/2026-10-15-cadence-adoption/owner-acceptance-plan.json).
|
||||
|
||||
## Automate staged receiver derivation without sibling mutations
|
||||
|
||||
```task
|
||||
id: NK-WP-0035-T07
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Completed source/fixture work 2026-10-19 for CUST-WP-0079-T05. The existing
|
||||
coordination-frame compiler now has a receiver-only mode, and its staged
|
||||
adapter compiles indexed NetKingdom renderer, coordination-engine frame/API and
|
||||
approval-engine cadence inputs in an isolated temporary snapshot. Only the
|
||||
local proposed receiver is written/staged. Dedicated Make targets and the
|
||||
existing shared opt-in pre-commit recipe invoke this path; authored sibling
|
||||
files and unstaged source edits are untouched. Missing/conflicted/nonregular
|
||||
inputs, an unstaged receiver/adapter, changed input snapshots and required
|
||||
alternate-index mutations refuse. The existing guard/scanner remain intact.
|
||||
|
||||
Eleven focused tests pass, including partial source staging, namespaced sibling
|
||||
index isolation, receiver edit preservation and alternate-index refusal.
|
||||
The first alternate-index test found that Git's default index query itself
|
||||
honored GIT_INDEX_FILE; corrected the query and retained its regression. This
|
||||
is a refined implementation, not an unchanged execution-success claim.
|
||||
|
||||
T06 retains ownership of the full multi-consumer frame compiler. T07 owns only
|
||||
the staged local receiver automation introduced here. T04 retains registry,
|
||||
collector and operational lifecycle/observer acceptance; neither this task nor
|
||||
its tests claim runtime activation or a clean week of fleet commit history.
|
||||
See tools/coordination-frame/README.md for commands and source boundaries.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue