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:
tegwick 2026-10-19 13:44:36 +02:00
parent 4c7f32adc7
commit bde143e030
9 changed files with 284 additions and 22 deletions

View file

@ -3,6 +3,8 @@ CUSTODIAN_SOURCE ?= /home/worsch/the-custodian
GENERATED_REPO_SLUG ?= net-kingdom GENERATED_REPO_SLUG ?= net-kingdom
.PHONY: generated-work-records generated-work-records-check .PHONY: generated-work-records generated-work-records-check
generated-work-records: 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)" python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)"
generated-work-records-check: 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 python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)" --check

View file

@ -401,3 +401,9 @@ generated-work-records:
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records $(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records
generated-work-records-check: generated-work-records-check:
$(MAKE) --no-print-directory -f .generated-work-records.mk 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

View file

@ -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, and exact-scope secret scans remain in place. This is repository maintenance,
not a runtime service, authorization decision, deployment or publication. not a runtime service, authorization decision, deployment or publication.
Other index generators and hub-derived briefs retain their owner boundaries. 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.

View file

@ -160,6 +160,7 @@
| task | NK-WP-0035-T04 | progress | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | | 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-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-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-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-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 | | task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md |

View file

@ -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 coordination consumers must adopt explicit frame multipliers through their own
configuration. Transport polling, security validity and credential leases are configuration. Transport polling, security validity and credential leases are
not silently rescaled by a coordination setting. 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.

View file

@ -69,7 +69,27 @@ def bind_frame_annotation(text, digest, metadata_prefix):
return updated 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) frame = owner_frame(workspace)
digest = hashlib.sha256((workspace / FRAME_REF).read_bytes()).hexdigest() digest = hashlib.sha256((workspace / FRAME_REF).read_bytes()).hexdigest()
provenance = { provenance = {
@ -97,6 +117,9 @@ def compile_bindings(workspace: Path):
ext["effective_deadline_seconds"] = frame.seconds(3) ext["effective_deadline_seconds"] = frame.seconds(3)
approval["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance approval["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance
outputs[approval_path] = json.dumps(approval, indent=2) + "\n" 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_path = workspace / "qonto-assistant/specs/audit-emission-cadence.yaml"
qonto = yaml.safe_load(qonto_path.read_text()) qonto = yaml.safe_load(qonto_path.read_text())
@ -160,37 +183,20 @@ def compile_bindings(workspace: Path):
fixture["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance fixture["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance
outputs[fixture_path] = json.dumps(fixture, indent=2) + "\n" outputs[fixture_path] = json.dumps(fixture, indent=2) + "\n"
receiver = { outputs[workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json"] = receiver_binding(frame, provenance, outputs[approval_path])
"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"
return frame, outputs return frame, outputs
def main(argv=None): def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--workspace", type=Path, default=ROOT.parent) parser.add_argument("--workspace", type=Path, default=ROOT.parent)
parser.add_argument("--receiver-only", action="store_true")
mode = parser.add_mutually_exclusive_group() mode = parser.add_mutually_exclusive_group()
mode.add_argument("--check", action="store_true") mode.add_argument("--check", action="store_true")
mode.add_argument("--write", action="store_true") mode.add_argument("--write", action="store_true")
args = parser.parse_args(argv) args = parser.parse_args(argv)
try: try:
frame, outputs = compile_bindings(args.workspace) frame, outputs = compile_bindings(args.workspace, receiver_only=args.receiver_only)
changed = { changed = {
p: text for p, text in outputs.items() if not p.exists() or p.read_text() != text p: text for p, text in outputs.items() if not p.exists() or p.read_text() != text
} }

View 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}))

View 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

View file

@ -10,7 +10,7 @@ owner: codex
topic_slug: netkingdom topic_slug: netkingdom
planning_priority: P1 planning_priority: P1
created: "2026-09-04" created: "2026-09-04"
updated: "2026-10-15" updated: "2026-10-19"
related: related:
- GH-DEC-2026-004 - GH-DEC-2026-004
- canon/standards/security-layer-model_v0.7.md - 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 elapsed-time capture, independent source/receiver correlation and observer
acceptance. Local-identity remains explicitly incompatible, with no completeness 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). 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.