docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
e3fad670a1
commit
ccf5a95ce3
50 changed files with 12259 additions and 0 deletions
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"exit_code": 0,
|
||||
"stdout": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 7, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n",
|
||||
"stderr": ""
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from pathlib import Path
|
||||
import subprocess,json,sys
|
||||
base=Path(__file__).resolve().parent;owner=Path('/home/worsch/net-kingdom');label=sys.argv[1]
|
||||
r=subprocess.run(['statehub','fix-consistency','--repo','net-kingdom','--json'],cwd=owner,capture_output=True,text=True)
|
||||
(base/(label+'.stdout.json')).write_text(r.stdout)
|
||||
(base/(label+'.stderr.txt')).write_text(r.stderr)
|
||||
(base/(label+'.exit.json')).write_text(json.dumps({'exit_code':r.returncode})+'\n')
|
||||
backup=base/'publication-backup'
|
||||
(owner/'.custodian-brief.md').write_bytes((backup/'brief.worktree').read_bytes())
|
||||
mode,oid,_,name=(backup/'index-record.txt').read_text().split()
|
||||
subprocess.run(['git','-C',str(owner),'update-index','--cacheinfo',mode,oid,name],check=True)
|
||||
assert subprocess.check_output(['git','-C',str(owner),'show',':.custodian-brief.md'])==(backup/'brief.index').read_bytes()
|
||||
print('Projection exit',r.returncode,'; original staged/working brief restored; exact stdout/stderr retained.')
|
||||
print(r.stderr)
|
||||
raise SystemExit(r.returncode)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
"""Disposable adapter fixtures using exact compiled checker, no checker mocks."""
|
||||
from dataclasses import asdict,replace
|
||||
import base64,copy,importlib.util,json,os
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from contextlib import chdir
|
||||
|
||||
ROOT=Path(os.environ.get('NETKINGDOM_FIXTURE_ROOT', '/home/worsch/net-kingdom'))
|
||||
spec=importlib.util.spec_from_file_location('owner_registration_tests',ROOT/'sso-mfa/k8s/keycape/test_keycape_registration_adapter.py')
|
||||
t=importlib.util.module_from_spec(spec);spec.loader.exec_module(t)
|
||||
BINARY=Path(os.environ.get('KEYCAPE_TEST_CHECKER_BINARY', '/tmp/nk-checker-artifact-next-r3/keycape'))
|
||||
|
||||
class ArtifactAdapterChecks(t.RegistrationTests):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.check_patch.stop() # The real maintained subprocess checker runs.
|
||||
self.binary.write_bytes(BINARY.read_bytes())
|
||||
self.binary.chmod(0o700)
|
||||
desc=t.a.descriptor(t.digest(self.binary.read_bytes()))
|
||||
self.desc_bytes=json.dumps(asdict(desc),sort_keys=True,separators=(',',':')).encode()
|
||||
self.admitted=replace(self.admitted,pins=replace(self.admitted.pins,adapter=t.digest(self.desc_bytes)))
|
||||
self.inv=replace(self.admitted,attempt_id='attempt-one',operation='run')
|
||||
self.kw.update(admitted=self.admitted,check_config_digest=t.digest(self.binary.read_bytes()))
|
||||
self.missing_env=False
|
||||
clients=copy.deepcopy(t.f.EXISTING)
|
||||
clients[1].update(displayName='Synthetic legacy approval',
|
||||
redirectUris=['https://approval.example.invalid/cb'],allowedScopes=['openid'],grantTypes=['authorization_code'])
|
||||
self.secret,self.raw=t.f.fixture(clients,tail='port: 8080\nprivateKeyPem: /nonexistent/synthetic-key.pem\ntokenLifetime: 15m\n')
|
||||
|
||||
def kube(self,args,payload=None):
|
||||
value=super().kube(args,payload)
|
||||
if args[:5]==['-n','sso','get','deployment','keycape'] and not self.missing_env:
|
||||
value['spec']['template']['spec']['containers'][0]['env'].append({
|
||||
'name':'KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET',
|
||||
'valueFrom':{'secretKeyRef':{'name':'synthetic-legacy','key':'client-secret'}}})
|
||||
return value
|
||||
|
||||
def test_real_checker_rejects_invalid_startup_config_before_cas(self):
|
||||
raw=base64.b64decode(self.secret['data']['config.yaml']).decode().replace('port: 8080','port: 0')
|
||||
self.secret['data']['config.yaml']=base64.b64encode(raw.encode()).decode()
|
||||
with self.assertRaises(t.Refused):t.a.build(**self.kw).invoke(self.inv)
|
||||
self.assertFalse(self.effects)
|
||||
self.assertEqual(self.revocations,[self.inv])
|
||||
|
||||
def test_real_checker_rejects_missing_declared_secret_env_before_cas(self):
|
||||
self.missing_env=True
|
||||
with self.assertRaises(t.Refused):t.a.build(**self.kw).invoke(self.inv)
|
||||
self.assertFalse(self.effects)
|
||||
self.assertEqual(self.revocations,[self.inv])
|
||||
|
||||
def test_ambient_urls_cannot_reject_supplied_valid_config(self):
|
||||
with patch.dict(os.environ,{'KEYCAPE_ACCOUNT_PORTAL_URL':'http://polluted.example.invalid',
|
||||
'KEYCAPE_BROWSER_LOGOUT_URL':'http://polluted.example.invalid/logout'}):
|
||||
result=t.a.build(**self.kw).invoke(self.inv)
|
||||
self.assertEqual(result.outcome,t.Outcome.SUCCESS)
|
||||
self.assertEqual(len(self.effects),1)
|
||||
|
||||
def test_ambient_urls_cannot_admit_supplied_invalid_config(self):
|
||||
raw=base64.b64decode(self.secret['data']['config.yaml']).decode()
|
||||
raw+='accountPortalURL: http://invalid.example.invalid\nbrowserLogoutURL: http://invalid.example.invalid/logout\n'
|
||||
self.secret['data']['config.yaml']=base64.b64encode(raw.encode()).decode()
|
||||
with patch.dict(os.environ,{'KEYCAPE_ACCOUNT_PORTAL_URL':'https://polluted.example.invalid',
|
||||
'KEYCAPE_BROWSER_LOGOUT_URL':'https://polluted.example.invalid/logout'}):
|
||||
with self.assertRaises(t.Refused):t.a.build(**self.kw).invoke(self.inv)
|
||||
self.assertFalse(self.effects)
|
||||
self.assertEqual(self.revocations,[self.inv])
|
||||
|
||||
def test_relative_selected_real_binary_is_not_replaced_by_path_decoy(self):
|
||||
decoy=Path(self.temp.name)/'decoy';decoy.mkdir()
|
||||
trap=decoy/'keycape';trap.write_text('#!/bin/sh\nexit 27\n');trap.chmod(0o700)
|
||||
with chdir(self.temp.name),patch.dict(os.environ,{'PATH':str(decoy)}):
|
||||
result=t.a.build(**dict(self.kw,check_config=Path('keycape'))).invoke(self.inv)
|
||||
self.assertEqual(result.outcome,t.Outcome.SUCCESS)
|
||||
self.assertEqual(len(self.effects),1)
|
||||
|
||||
def test_selected_path_replacement_after_snapshot_cannot_change_executed_checker(self):
|
||||
original = t.a.helper.check_config
|
||||
def replace_path(*args, **kwargs):
|
||||
trap = self.binary.with_name('replacement-checker')
|
||||
trap.write_text('#!/bin/sh\nexit 27\n')
|
||||
trap.chmod(0o700)
|
||||
trap.replace(self.binary)
|
||||
return original(*args, **kwargs)
|
||||
with patch.object(t.a.helper, 'check_config', side_effect=replace_path):
|
||||
result = t.a.build(**self.kw).invoke(self.inv)
|
||||
self.assertEqual(result.outcome, t.Outcome.SUCCESS)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
self.assertEqual(self.revocations, [self.inv])
|
||||
|
||||
if __name__=='__main__':
|
||||
selected=[
|
||||
'test_selected_path_replacement_after_snapshot_cannot_change_executed_checker',
|
||||
'test_actual_add_and_created_only_rollback_preserve_original_bytes',
|
||||
'test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust',
|
||||
'test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo',
|
||||
'test_kernel_external_revision_change_retains_uncertain_rollback',
|
||||
'test_stale_revision_refuses_mutation',
|
||||
'test_substituting_caller_selected_checker_under_same_descriptor_is_refused',
|
||||
'test_real_checker_rejects_invalid_startup_config_before_cas',
|
||||
'test_real_checker_rejects_missing_declared_secret_env_before_cas',
|
||||
'test_ambient_urls_cannot_reject_supplied_valid_config',
|
||||
'test_ambient_urls_cannot_admit_supplied_invalid_config',
|
||||
'test_relative_selected_real_binary_is_not_replaced_by_path_decoy']
|
||||
suite=unittest.TestSuite(ArtifactAdapterChecks(name) for name in selected)
|
||||
result=unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
raise SystemExit(not result.wasSuccessful())
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"path": "workplans/NK-WP-0048-security-case-types.md",
|
||||
"before": "6765312428c4bb78380d7736de1132f0d005e6059dc17008792d0b3f2999d441",
|
||||
"after": "4b14db289cee2ff598484c050814efee1de743a316ac7ba7cb549efc08e73ecc"
|
||||
}
|
||||
|
|
@ -0,0 +1,871 @@
|
|||
---
|
||||
id: NK-WP-0048
|
||||
type: workplan
|
||||
title: "Establish SecurityCaseTypes so that known security cases are light"
|
||||
domain: infotech
|
||||
repo: net-kingdom
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: claude-code
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-08"
|
||||
updated: "2026-10-16"
|
||||
depends_on: []
|
||||
related_repos:
|
||||
- ops-mason
|
||||
- ops-warden
|
||||
- key-cape
|
||||
- railiance-platform
|
||||
- informed-decision
|
||||
state_hub_workstream_id: "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
---
|
||||
|
||||
# NK-WP-0048 - Establish SecurityCaseTypes so that known security cases are light
|
||||
|
||||
## Why
|
||||
|
||||
The founder's direction (2026-10-08): setting up a security integration of a
|
||||
kind that has been done before should be easy, meaning accept the case, then
|
||||
ops-mason arranges it and ops-warden follows through. Setting up a new kind
|
||||
needs ample documentation and tests and an informed-decision, because its shape
|
||||
is part of the security architecture.
|
||||
|
||||
Today every integration is hand-built: per-client helper scripts, per-lane
|
||||
custody, founder checklists. The trigger was KEY-WP-0038 (Forgejo as an OIDC
|
||||
client), whose next step was going to be yet another hand-written rollout
|
||||
script.
|
||||
|
||||
This plan continues the pattern work of NK-WP-0008 (pattern infospace),
|
||||
NK-WP-0009 (tutorials) and NK-WP-0030 (scenario composition). It turns patterns
|
||||
into **SecurityCaseTypes** that ops-mason and ops-warden can execute. Canon:
|
||||
`canon/standards/security-case-types_v0.1.md` (proposed).
|
||||
|
||||
Companion plans: ops-mason `MASON-WP-0010` (materialization) and ops-warden
|
||||
`WARDEN-WP-0044` (follow-through).
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "930ac3e9-20ec-5473-98cd-3fe8fddb730f"
|
||||
```
|
||||
|
||||
### T01 — Accept the terminology and the canon
|
||||
|
||||
Founder review of `canon/standards/security-case-types_v0.1.md`, including its
|
||||
section 10 open questions. On acceptance the canon status goes from `proposed`
|
||||
to `accepted`.
|
||||
|
||||
2026-10-08: accepted by the founder (in session). Canon status `accepted`;
|
||||
section 10's open questions are carried forward to the first type (T04, T05).
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "19c25d96-a55c-58f0-9168-3fdbc4955fc2"
|
||||
```
|
||||
|
||||
### T02 — Inventory the de-facto types already built by hand
|
||||
|
||||
Read-only. List every security integration built so far and group them by shape.
|
||||
Known starting points:
|
||||
|
||||
- browser OIDC relying parties on key-cape: informed-decision-approver,
|
||||
coulomb-social, user-engine portal, openbao-admin, and Forgejo (pending)
|
||||
- confidential service clients with `client_credentials`: secrets-engine
|
||||
approval, requester and openbao clients, approval-engine operator
|
||||
- workload KV read lanes on OpenBao: the railiance-platform credential lane
|
||||
designs and CCRs, for example `railiance-backup-offsite-lane`
|
||||
- short-lived SSH certificates through ops-warden (`warden sign`)
|
||||
- tunnelled operator access (ops-bridge)
|
||||
|
||||
For each: the repositories involved, the hand-built steps, attended human steps,
|
||||
custody paths, verification evidence, and rollback (if any). Output
|
||||
`docs/security-case-type-inventory.md`, ranking candidate types by how often the
|
||||
shape recurs and how much manual work each instance costs today.
|
||||
|
||||
2026-10-08: done, `docs/security-case-type-inventory.md`. 36 of 37 CCRs are
|
||||
already `request_type: workload-kv-read`, a de-facto type reviewed each time as
|
||||
if it were new. Browser clients: 5 hand-built plus Forgejo, each with its own
|
||||
rollout script. Confidential service clients need a registration plus two or
|
||||
three KV-read CCRs each. A forge read identity is needed in bulk by
|
||||
ORGREF-WP-0002-T06. Recommendation: oidc-browser-relying-party first (unblocks
|
||||
the forge), workload-kv-read second (largest payoff), then forge-read-identity.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T03
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T01]
|
||||
priority: high
|
||||
state_hub_task_id: "e487975b-73b1-516c-971d-da4cbbb93063"
|
||||
```
|
||||
|
||||
### T03 — Schema, template and validator
|
||||
|
||||
- `canon/schemas/security-case-type_v0.1.schema.json` and
|
||||
`security-case_v0.1.schema.json`
|
||||
- `security-case-types/_template/` with `type.yaml`, `README.md` and tests to copy
|
||||
- a validator, extending `tools/security-scenario-composer/`, that checks a type,
|
||||
checks a case against its type, and renders the case into the composer's
|
||||
deterministic plan (still `execution.permitted: false`)
|
||||
|
||||
Acceptance: the validator rejects missing sections, secret-shaped parameter
|
||||
values, unpinned providers and cases against non-`implemented` types, with tests
|
||||
for each.
|
||||
|
||||
2026-10-08: done. `canon/schemas/security-case-type_v0.1.schema.json` and
|
||||
`security-case_v0.1.schema.json`; `tools/security-case-types/security_case_validator.py`
|
||||
(`validate-type`, `validate-case`, `render`, reusing the scenario composer);
|
||||
`security-case-types/_template/` (a valid, runnable example type over
|
||||
`net-kingdom.local-identity`, with README sections, an example case and tests that
|
||||
travel with every copy). 31 tests pass, including the composer's existing suite.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T04
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T02, NK-WP-0048-T03]
|
||||
priority: high
|
||||
state_hub_task_id: "2fffe112-7d55-5680-9952-78d74f44f87f"
|
||||
```
|
||||
|
||||
### T04 — Define the first type: browser OIDC relying party
|
||||
|
||||
`security-case-types/oidc-browser-relying-party/`, version 1.0:
|
||||
|
||||
- parameters: client id, display name, exact redirect URIs, scopes, client type
|
||||
(confidential or public), MFA pin, the groups the consuming service maps,
|
||||
custody path for the secret (confidential only), consuming service and its
|
||||
owner
|
||||
- composition: key-cape client registration (key-cape declares it as a playbook
|
||||
capability), secret minting into custody (railiance-platform), LLDAP group
|
||||
existence and membership (net-kingdom identity-provisioner), handover to the
|
||||
consuming service
|
||||
- materialization (ops-mason) and follow-through (ops-warden) sections
|
||||
- verification: discovery lists the redirect URI; a test sign-in returns the
|
||||
expected claims for one human; anonymous access to the custody path is denied
|
||||
- rollback: remove exactly this client and its custody path
|
||||
- README: threat model, MFA rationale, why redirect URIs are exact, which steps
|
||||
still need an attended login and how that is minimised
|
||||
- reference instances: informed-decision-approver, coulomb-social, user-engine
|
||||
portal, openbao-admin
|
||||
|
||||
Acceptance: complete per canon section 3, tests green, status `proposed`.
|
||||
|
||||
2026-10-08: done. `security-case-types/oidc-browser-relying-party/` 1.0 (proposed):
|
||||
`type.yaml` with 12 parameters (the custody reference is required only for
|
||||
confidential clients), composition over key-cape's new
|
||||
`key-cape.oidc-client-registration` declaration (contract PASS, `maturity:
|
||||
planned`), materialization and follow-through, verification and rollback, all
|
||||
conditional on client type where it matters. One attended step,
|
||||
`register-client`, justified by the live registry being a Kubernetes Secret.
|
||||
README with threat model, alternatives, an honest fit table (portal and vergabe
|
||||
fit; Forgejo is the first confidential case; openbao-admin, coulomb.social and
|
||||
informed-decision-approver do not fit 1.0), and known limits. Examples: Forgejo
|
||||
(checked against the accepted request) and a public portal. The validator gained
|
||||
`when` / `required_when` conditions and provider discovery. 43 tests pass.
|
||||
ops-mason's and ops-warden's sections are drafted in the type. Their owners
|
||||
review them in MASON-WP-0010-T01 and WARDEN-WP-0044-T01 before acknowledgment
|
||||
(T05).
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T05
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T04, MASON-WP-0010-T01, WARDEN-WP-0044-T01]
|
||||
priority: high
|
||||
state_hub_task_id: "3d987d3d-0797-538b-86e4-51185a327ae2"
|
||||
```
|
||||
|
||||
### T05 — Acknowledge the type through informed-decision
|
||||
|
||||
Prepare the Decision Memo from the type's README and `type.yaml` and submit it
|
||||
through informed-decision's production intake. On acknowledgment the type
|
||||
moves to `acknowledged`.
|
||||
|
||||
2026-10-08: Decision Memo prepared. Hub decision `e418263e-a71f-4f01-a970-c87c9e187c09`.
|
||||
`security-case-types/oidc-browser-relying-party/decision/`: `packet.md` (question,
|
||||
one act, what acknowledging does not do, the shape, alternatives, attended
|
||||
steps, limits, evidence), `build_intake.py` and the built `intake.json` (memo
|
||||
class `decision-record`, review group `net-kingdom-admins`, resolves the hub
|
||||
decision on answer). The envelope passes informed-decision's own
|
||||
`validate_submission`. Queuing it is an attended operator step, a write inside
|
||||
the production dispatcher pod:
|
||||
|
||||
kubectl -n state-hub exec -i deploy/state-hub-memo-dispatch -- \
|
||||
python -m api.services.memo_dispatch --queue /data/private enqueue --envelope /dev/stdin \
|
||||
< security-case-types/oidc-browser-relying-party/decision/intake.json
|
||||
|
||||
The answer then appears for review in informed-decision.
|
||||
|
||||
2026-10-08: queued by the founder (`pending`, `queued`) after one refusal: the
|
||||
first envelope's idempotency key was not State Hub's stable
|
||||
`state-hub.<decision>.<memo>` form, and the builder now validates against both
|
||||
sides. The dispatcher delivered it to informed-decision (ledger 7 → 8
|
||||
delivered). T05 waits for the founder's answer in informed-decision, which
|
||||
resolves hub decision `e418263e`.
|
||||
|
||||
2026-10-08: ops-mason's review (MASON-WP-0010-T01) folded into the type, still
|
||||
1.0 `proposed`: `secret_ref` is no longer a parameter, the type derives
|
||||
`platform/workloads/keycape/{client_id}`; policies are
|
||||
`workload-kv-read-keycape-{client_id}-eso` and
|
||||
`workload-kv-read-keycape-{client_id}-{consuming_service}`, each with a templated
|
||||
body (`read` on that one data path); new `openbao_role`
|
||||
`external-secrets-keycape-{client_id}` (Kubernetes auth, ESO service account,
|
||||
15m, no default policy); catalog entry `{consuming_service}-keycape-client`;
|
||||
the two rollback steps on the KV path collapse into one metadata delete. The
|
||||
schema and validator now require a `body` on policy and role items. 105 tests
|
||||
pass. Two consequences: the delivered memo describes the type before these
|
||||
changes (a case-set custody reference, two differently named policies, no ESO
|
||||
role), so the founder answers on a superseded shape unless the memo is
|
||||
re-sent; and Forgejo's custody path moves from the accepted request's proposed
|
||||
`platform/workloads/forgejo/netkingdom-oidc-client` to
|
||||
`platform/workloads/keycape/forgejo`, which key-cape (KEY-WP-0038) and
|
||||
railiance-apps (RAPPS-WP-0016-T02) need to adopt in T06. ops-warden's review
|
||||
(WARDEN-WP-0044-T01) is still in progress.
|
||||
|
||||
2026-10-08: revised memo `nk-wp-0048-t05-oidc-browser-relying-party-r2` built
|
||||
(`packet.md`, `intake.json`, packet `sha256:af6435e2…`); it states that it
|
||||
supersedes the first memo and passes informed-decision's and the dispatcher's
|
||||
validation. It needs a new memo id because the dispatcher refuses new content
|
||||
under a delivered idempotency key (`idempotency_conflict`). Queuing it is the
|
||||
same attended step as above. Both memos serve hub decision `e418263e`; the
|
||||
first answer resolves it, so the first memo must not stay open: an accept on it
|
||||
would acknowledge the superseded shape, and a decline would resolve the
|
||||
decision against r2. Close it with `return` (a reviewer verb that requests a
|
||||
new revision and does not resolve the hub decision; only accept and decline
|
||||
do), commenting that r2 supersedes it, then answer r2. `withdraw` exists in
|
||||
the vocabulary but is not offered to reviewers, and lifecycle `revise` only
|
||||
applies to lifecycle-registered memos, not intake-delivered ones.
|
||||
|
||||
2026-10-09: the founder reports r2 enqueued and the first memo returned. At
|
||||
close, hub decision `e418263e` still showed `open` (not verified whether r2
|
||||
was answered or the answer is still in transit; the dispatcher ledger was not
|
||||
read). ops-warden's review (WARDEN-WP-0044-T01, ops-warden
|
||||
`history/2026-10-08-oidc-rp-follow-through-review.md`, d470f2e) arrived at
|
||||
21:25, before r2 was built, and is not in r2. Required before WARDEN-WP-0044-T02:
|
||||
R1 a per-step `lane` (missing: identity-provisioner caller credential,
|
||||
railiance-platform broker grant), R2 canon meaning of `warden_executes: true`
|
||||
(caller proxy, caller identity, no standing ops-warden token; ADR-0002), R3 a
|
||||
per-step `value_handling`. Refinements R4 (one type-level catalog lane instead
|
||||
of a per-case entry, which conflicts with ops-mason's item 5), R5 (ESO Ready gate
|
||||
before bind-env), R6 (exact accepted memberships), R7 (mfa-pinned and
|
||||
secret-delivered checks). Next: if r2 was accepted, fold R1-R3 and R5-R7 in as
|
||||
a follow-up and settle R4 between ops-warden and ops-mason before T06; if not,
|
||||
hold r2, fold them in and build r3.
|
||||
key-cape and railiance-apps were told about Forgejo's new custody
|
||||
path.
|
||||
|
||||
2026-10-09: r2 was not answered (hub decision `e418263e` open; dispatcher 9
|
||||
delivered), so r2 is held and r3 replaces it. ops-warden withdrew R4 for R4′
|
||||
(message a66a9204): one catalog entry per case with its body templated in the
|
||||
type. Folded in:
|
||||
|
||||
- Canon section 3: a narrow meaning of `warden_executes: true` (caller proxy,
|
||||
caller identity, no standing credential; ops-warden ADR-0002), plus per-step
|
||||
`lane`, `value_handling` and an optional `ready_check`. A catalog entry body is
|
||||
now required, like policy and role bodies.
|
||||
- Schema and validator enforce all of these.
|
||||
- The type: lanes `identity-provisioner-groups` and
|
||||
`keycape-client-secret-generate` (both new) and `key-cape-oidc-login`;
|
||||
`mint-secret` is `generated_in_custody`; a Ready gate on `deliver-secret`;
|
||||
`ensure-groups` adds exactly the accepted case's memberships; new checks
|
||||
`secret-delivered` and `mfa-pinned`; the catalog-entry body follows R4′.
|
||||
- 110 tests pass.
|
||||
|
||||
Memo r3 (`nk-wp-0048-t05-oidc-browser-relying-party-r3`, packet
|
||||
`sha256:726dfc01…`) passes informed-decision's and the dispatcher's validation.
|
||||
The founder queues it with the same command, returns r2 with a comment that r3
|
||||
supersedes it, then answers r3.
|
||||
|
||||
Prerequisites for T06, not for T05:
|
||||
- the two new lanes, each through its owner;
|
||||
- a founder-approved rollout of an identity-provisioner image containing
|
||||
630ebfd (the deployed one is main-6c4fcaf from 2026-09-24, without
|
||||
`/v1/groups/ensure`).
|
||||
|
||||
ops-mason's request to create LLDAP group `fluid-telegram-operators` (message
|
||||
259ec26d) was answered: not possible from here by that path; the founder creates
|
||||
it in the LLDAP admin UI.
|
||||
|
||||
2026-10-09: ops-warden finished its side of WARDEN-WP-0044-T01 (message
|
||||
`58d33f40`, commit 0e7ceb8): the revised type conforms to `warden case
|
||||
check-type`, with no further changes asked of the type. This acknowledges that
|
||||
review. Its later addendum R8 (message `af274783`, ops-warden
|
||||
`history/2026-10-08-oidc-rp-follow-through-review.md`) showed that lane
|
||||
`identity-provisioner-groups` cannot exist as declared: the provisioner's only
|
||||
HTTP credential is user-engine's runtime token, and its NetworkPolicy admits
|
||||
only user-engine pods, so a caller-proxy lane would act as user-engine. Taken
|
||||
option 1 (ops-warden's recommendation): `ensure-groups` and its rollback are
|
||||
attended, in the `register-client` session on `key-cape-oidc-login`, run inside
|
||||
the provisioner pod; no token is read. Option 2 (an operator credential of its
|
||||
own on the provisioner) stays open for a later version. `warden case
|
||||
check-type` now reports only `keycape-client-secret-generate`
|
||||
(railiance-platform) as missing. 89 tests pass.
|
||||
|
||||
Hub decision `e418263e` was still open, so memo r4
|
||||
(`nk-wp-0048-t05-oidc-browser-relying-party-r4`, packet `sha256:452ea421…`)
|
||||
replaces r3; it passes informed-decision's and the dispatcher's validation. The
|
||||
founder queues it with the same command, returns r2/r3 if still open with a
|
||||
comment that r4 supersedes them, then answers r4.
|
||||
|
||||
Prerequisites for T06 are now: lane `keycape-client-secret-generate`
|
||||
(railiance-platform), and a founder-approved rollout of an identity-provisioner
|
||||
image containing 630ebfd.
|
||||
|
||||
2026-10-09: done. The founder queued r4 (`pending`, `queued`), returned r3 as
|
||||
superseded and accepted r4 in informed-decision. informed-decision's hub
|
||||
write-back is off in production (`INFD_HUB_WRITEBACK` unset), so net-kingdom
|
||||
resolved hub decision `e418263e` from the founder's report, naming memo r4 in
|
||||
the rationale. The type is `acknowledged`; it becomes `implemented` with T06.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T06
|
||||
status: wait
|
||||
depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, NK-WP-0048-T17, NK-WP-0048-T18, MASON-WP-0010-T03, WARDEN-WP-0044-T03]
|
||||
blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status."
|
||||
priority: high
|
||||
state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c"
|
||||
```
|
||||
|
||||
### T06 — First KnownSecurityCase: Forgejo
|
||||
|
||||
Write `security-cases/forgejo-netkingdom-oidc.yaml` from the parameters the
|
||||
founder accepted in KEY-WP-0038-T01 (`key-cape/docs/forgejo-oidc-client-request.yaml`),
|
||||
so that acceptance carries over as the case acceptance. ops-mason materializes it;
|
||||
ops-warden follows through and verifies it. The type becomes `implemented` once
|
||||
this reference case is live.
|
||||
|
||||
This replaces the hand-built path proposed for KEY-WP-0038-T02.
|
||||
|
||||
2026-10-09: case written, `security-cases/forgejo-netkingdom-oidc.yaml`, status
|
||||
`accepted`: the example's parameters plus the founder's 2026-10-08 acceptance of
|
||||
the key-cape request (owner and founder), with the custody-path derivation
|
||||
covered by the type's acknowledgment (memo r4). It validates and renders (plan
|
||||
`execution.permitted: false`; three attended steps on `key-cape-oidc-login`);
|
||||
it needs `--allow-unimplemented` until it is live, since it is the case that
|
||||
makes the type `implemented`. A test keeps it equal to the type's example.
|
||||
Still open before it runs: ops-mason renders and builds it (MASON-WP-0010-T02,
|
||||
T03, unblocked by T05), ops-warden plans and runs it (WARDEN-WP-0044-T02, T03),
|
||||
railiance-platform's `keycape-client-secret-generate` lane (asked by ops-mason
|
||||
`789feb39` and ops-warden `d9f889b1`, both unread), and a founder-approved
|
||||
identity-provisioner rollout containing 630ebfd.
|
||||
|
||||
2026-10-09: identity-provisioner rolled out, founder-approved in session:
|
||||
`main-630ebfd` (`sha256:bafb4860…`, built by the image workflow) replaces
|
||||
`main-6c4fcaf` (`sha256:ffacd5d7…`; only change: 630ebfd, additive). Applied
|
||||
with `kubectl set image` on the Deployment only (generation 17 → 18, 1/1 Ready);
|
||||
the file's Service, NetworkPolicies and Ingress were not reapplied. Verified in
|
||||
the pod: `ensure_service_groups` and `revoke_service_groups` present, `/readyz`
|
||||
200; no token used, no group changed. Rollback: `kubectl -n sso set image
|
||||
deploy/identity-provisioner provisioner=forgejo.coulomb.social/coulomb/identity-provisioner@sha256:ffacd5d790a5b7800e0535f3aa95b7de52c7a68e4f575b9192c3e1622fa672c2`.
|
||||
The pin in `sso-mfa/k8s/identity-provisioner/deployment.yaml` follows.
|
||||
|
||||
2026-10-09: ops-warden closed WARDEN-WP-0044-T01 and shipped `warden case plan`
|
||||
(T02, message `06303d1d`); the accepted Forgejo case is refused until ops-mason
|
||||
materializes it, and a materialized copy plans cleanly once
|
||||
`keycape-client-secret-generate` exists. Its observation that `bind-env` carries
|
||||
a literal `KEYCAPE_<CLIENT_ID>_CLIENT_SECRET`: the validator now renders a
|
||||
`{param:env}` format spec (upper case, `-` as `_`, as `keycape-client.py`
|
||||
derives the name) and rejects unknown specs, and follow-through and rollback
|
||||
text is now checked for unknown placeholders too (patch-level, canon section 6).
|
||||
The type keeps the literal until ops-warden's planner (`text.format`) renders the
|
||||
spec as well; asked of ops-warden. 53 tests pass.
|
||||
|
||||
2026-10-10: ops-warden rendered `{param:env}` in `warden case plan`
|
||||
(23ba584, message `c1df14b7`; other specs and `!conversion` refused). The type
|
||||
adopts it as a 1.0 patch (canon section 6): `bind-env` and its rollback now read
|
||||
`KEYCAPE_{client_id:env}_CLIENT_SECRET`. ops-mason reports the Forgejo case
|
||||
rendered and built (message `1ebddae9`; CCR drafts handed to railiance-platform),
|
||||
so T06 now waits on the platform apply, the mint-secret lane and the attended
|
||||
register-client session.
|
||||
|
||||
2026-10-10: key-cape adopted the type's custody path (key-cape ab5cccd, message
|
||||
`88a4559c`): `docs/forgejo-oidc-client-request.yaml` and KEY-WP-0038-T02 name
|
||||
`platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, delivered as
|
||||
`sso/keycape-forgejo-client` through `external-secrets-keycape-forgejo`, and the
|
||||
`forgejo/netkingdom-oidc-client` proposal is withdrawn. This matches the type and
|
||||
the Forgejo example; no change needed here.
|
||||
|
||||
2026-10-10: key-cape's type-composition review (key-cape 6038483, message
|
||||
`8e5ed5f6`) found the composition sound with two blocking findings, taken as a
|
||||
1.0 patch (stricter validation and documentation, canon section 6):
|
||||
R1, redirect URIs now refuse `?` and `#` in the type pattern and in
|
||||
`keycape-client.py`'s `REDIRECT` (KeyCape refuses `?` at startup and the single
|
||||
replica would stop); R2, `allowed_scopes` always includes `groups`, the renderer
|
||||
refuses `access_groups` without it, and `access_groups` now says the consumer
|
||||
enforces the gate because KeyCape has none; R3, `mfa_required` "false" means the
|
||||
per-user privacyIDEA policy; R4, register-client's justification records the
|
||||
restart for every client, the quiet window, and that the rendered registry must
|
||||
pass KeyCape's client validation before it. The accepted Forgejo case and both
|
||||
examples still validate; 116 tests pass.
|
||||
Open, needs a decision: R2's verification step that the consumer enforces the
|
||||
group gate is a new verification step, hence 1.1 (minor), and the validator pins
|
||||
cases to the exact type version, so the accepted 1.0 Forgejo case would need
|
||||
re-pinning. Also open with key-cape: a KeyCape entry point that validates a
|
||||
rendered client registry without starting the server; until then
|
||||
`keycape-client.py` mirrors the redirect rule.
|
||||
|
||||
2026-10-10: founder go for 1.1, so no 1.0 run overlaps. The type adds
|
||||
verification `consumer-enforces-groups`, and hand-over now requires the consumer
|
||||
to enforce `access_groups` (minor, canon section 6; risk class unchanged, no new
|
||||
acknowledgment; history in the type README). The accepted Forgejo case and both
|
||||
examples are re-pinned to `oidc-browser-relying-party@1.1` before any apply; the
|
||||
acknowledgment intake keeps `@1.0` as the record of what was acknowledged.
|
||||
ops-mason's build and ops-warden's case-ref digest predate this and need a
|
||||
re-render.
|
||||
|
||||
2026-10-10: ops-mason re-rendered the Forgejo case from `@1.1` (message
|
||||
`22c6bba6`; plan digest `af1f4f81…`, no collisions) and rebuilt the bundle. The
|
||||
rendered OpenBao objects are identical to 1.0, so the already applied
|
||||
CCR-2026-0041 stands; 1.1 changes only consumer-side steps. key-cape shipped
|
||||
`keycape check-config` (c528daa, message `a7b0aeff`): KeyCape's startup checks
|
||||
without secret values, including the new duplicate-clientId rule.
|
||||
`keycape-client.py --check-config <binary>` now runs it on the live and the
|
||||
proposed registry, fed on stdin, and apply refuses a proposed registry that
|
||||
fails; register-client's entry point names the option (1.1 patch). The deployed
|
||||
image predates c528daa, so the binary is built from key-cape source for now.
|
||||
Before the next KeyCape rollout (Forgejo register-client or the CCR-2026-0018
|
||||
window), the attended operator runs `inspect --check-config` so the live
|
||||
registry is checked too.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T07
|
||||
status: wait
|
||||
depends_on: [NK-WP-0048-T06]
|
||||
blocking_reason: "The next types follow once the first one is proven end to end."
|
||||
priority: medium
|
||||
state_hub_task_id: "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7"
|
||||
```
|
||||
|
||||
### T07 — Queue the next types
|
||||
|
||||
From the T02 ranking, open the next type drafts (likely the confidential service
|
||||
client and the workload KV read lane), each to be acknowledged on its own.
|
||||
Record which existing hand-built instances get adopted as cases, without
|
||||
rewriting them in place.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T08
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8e519082-dc8b-5e46-b660-03d03e55dcf6"
|
||||
```
|
||||
|
||||
### T08 — identity-provisioner: ensure service-access groups
|
||||
|
||||
The type's `ensure-groups` step calls `identity-provisioner groups ensure --groups
|
||||
<list> --members <group=user+user;...>`. The provisioner already has idempotent
|
||||
`_ensure_group` and `_add_group`, but exposes only tenant-scoped operations. Add
|
||||
the general operation (create if absent, add the listed memberships, never
|
||||
remove others), its rollback (remove only this case's memberships), tests, and
|
||||
the ops-warden route for its service identity.
|
||||
|
||||
2026-10-08: done. `identity-provisioner/provisioner.py` gained
|
||||
`ensure_service_groups` (`POST /v1/groups/ensure`) and `revoke_service_groups`
|
||||
(`POST /v1/groups/revoke`), behind the existing service-token check. Names
|
||||
matching `tenant*`, `netkingdom-*` and `lldap_*` are reserved; members must
|
||||
already exist; everything is validated before any change; memberships are read
|
||||
back before success; nothing else is ever removed; groups are deleted only when
|
||||
asked and empty. 7 new tests (39 in the provisioner suite). The type's
|
||||
`ensure-groups` step and its rollback now name these endpoints. The ops-warden
|
||||
route for the provisioner's service token belongs to WARDEN-WP-0044-T01.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T09
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "0ee12dfc-8260-5038-bd39-cc7fb4113be0"
|
||||
```
|
||||
|
||||
### T09 — The `keycape-client` entry point and its declaration
|
||||
|
||||
Planned first as key-cape KEY-WP-0038-T04. Moved here because the machinery a
|
||||
live registration needs already lives in net-kingdom, where KeyCape is deployed
|
||||
(`sso-mfa/k8s/keycape/`): the contained kubectl, the cluster-identity pin and the
|
||||
issuer pin. A copy in key-cape would drift.
|
||||
|
||||
2026-10-08: done. `sso-mfa/k8s/keycape/keycape-client.py` derives the KeyCape
|
||||
client record from a case (`client-from-case`). It adds or removes exactly one
|
||||
client with the existing safeguards (byte-preserving merge, exact match or
|
||||
refuse, shared-reference refusal, compare-and-swap, read-back, contained errors),
|
||||
requires the Deployment env binding first for confidential clients, and requires
|
||||
the case acceptance record for `apply` instead of a per-client founder decision
|
||||
id. 15 tests, including a regression showing that the vergabe case reproduces
|
||||
its hand-built registration exactly; all 71 helper tests pass. Declaration
|
||||
`capabilities/playbooks/net-kingdom.keycape-client-registration.yaml` (contract
|
||||
PASS, `maturity: implemented`) replaces key-cape's draft. The type now pins it,
|
||||
and for confidential clients adds `deliver-secret` (delivery from custody into
|
||||
the sso namespace) and `bind-env` (Deployment env), attended in the same session
|
||||
as `register-client`. The per-client helpers stay until their clients are
|
||||
adopted as cases.
|
||||
|
||||
2026-10-10 — NetKingdom owner review of the execution seam, source only:
|
||||
ops-warden/docs/security-case-execution-proposal.md SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
|
||||
was reviewed against INTENT, SecurityCase canon, acknowledged type1.1, current
|
||||
case and owner source. docs/security-case-execution-owner-review.md supports
|
||||
the closed adapter/receipt direction with explicit versioned schema, structural
|
||||
materialization provenance, unchanged caller/session boundaries, safe CAS
|
||||
reconciliation and exact-created-object rollback requirements. This is owner
|
||||
review, not approval. Supporting file digests are in the adjacent sources JSON.
|
||||
|
||||
Current source exposes a circular wait: ops-mason T03 waits on mint while Warden
|
||||
and the broker refuse a non-materialized case. Structural acceptance therefore
|
||||
needs its own native pinned receipt before mint, not a successful mint before
|
||||
materialized. The platform broker additionally pins type1.0 while Forgejo is
|
||||
accepted at1.1; platform-owner alignment and tests are required, not an unchecked
|
||||
version bypass. T06 remains wait. No case/type lifecycle status, grant, authority
|
||||
or live runtime changed by this review. A proposed informed-decision would
|
||||
adopt the source contract only; native materialization and execution acceptance
|
||||
stay distinct.
|
||||
|
||||
Final proposal re-review: staged Warden proposal SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
|
||||
adds only the explicitly local reducer appendix, not an accepted wire protocol
|
||||
or executor. The exact-final review and sources JSON supersede the earlier
|
||||
1035f049... proposal review. Platform staged the exact type1.1 broker correction
|
||||
with ten fake-backend subprocess checks (script SHA256
|
||||
9576cd211cfc06526ec4cc9bd0a5b06f0f59287a68931bb2d6e3592a951ddc7d).
|
||||
The next source work is the versioned closed adapter/receipt/materialization
|
||||
contract, not another relaxation of the broker's status gate. Native work still
|
||||
requires installed broker provenance, structural reader receipts, exact new
|
||||
materialized-case bytes, caller grant/session revocation and verified execution.
|
||||
No installed/native acceptance is inferred from that staged correction.
|
||||
|
||||
2026-10-11 — new source-only type1.2 architecture acknowledgment:
|
||||
Confirmed native founder ACCEPT of infd-20261011-forgejo-type12-source-r1@1
|
||||
at06:47:39Z, approval8e9332c8-754a-420c-b8b7-445cb4fe6765. Exact frozen packet
|
||||
0b377d647ca748cd03c92f9792396412048055d4bde9f7bb06ff6d413f7acd68 and submission
|
||||
585cee731de30d2b37883a57abfd9eb32f023ee694e8d911989109d5caf80569; native receipt
|
||||
retained in docs/evidence/2026-10-11-forgejo-type12-source-acknowledgment/.
|
||||
The complete acknowledged1.2 source package is isolated under
|
||||
security-case-type-revisions/1.2/oidc-browser-relying-party; default loader and
|
||||
current accepted1.1 case remain untouched because load_type is single-version.
|
||||
The new proposed1.2 case is NOT accepted/materialized/live and has no acceptance
|
||||
fields. Actual reference-migration design approval is a separate Apps owner
|
||||
receipt; no1.1 approval is inherited. Source-recording review/test proof and exact
|
||||
next gates are in docs/forgejo-type12-source-acknowledgment-followthrough.md.
|
||||
T06 remains wait and T07 remains its existing dependent task; type is NOT
|
||||
implemented and no first-case/native grant or broader workplan finish is asserted.
|
||||
|
||||
2026-10-11 — bounded explicit version selection source preparation for T06:
|
||||
The validator now offers optional exact version_roots/--version-root VERSION=PATH
|
||||
selection with duplicate/mismatch/traversal/symlink refusal and no fallback. The
|
||||
default1.1 source and historicalacceptedcase hashes/ordinaryrefusal/preparation
|
||||
render stayunchanged; actual1.1 is acknowledged, not implemented. Acknowledged1.2
|
||||
ordinaryadmission also refuses; optionalpreparation remains nonexecuting.43local
|
||||
validatorchecks pass and original1.1 CLIrenderbytes match exactly. Makerproof:
|
||||
docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json.
|
||||
FormalNetownerreview/reference-source integration and all existing native adapter,
|
||||
provenance/broker/materialization/attendedproof gates remain; T06 stayswait, no
|
||||
new residual or firstcase/typeimplementation/native grant is inferred.
|
||||
|
||||
|
||||
2026-10-16 — isolated source preparation for T06 (canonical integration pending):
|
||||
Added `check-evidence` to the existing validator CLI. It reuses exact version
|
||||
selection and ordinary type/case/render admission, checks actual Mason structural
|
||||
request source/inventory/projection pins and reads actual Warden journal rows
|
||||
read-only. Successful rows require exact type-declared descriptors and attempt/
|
||||
revocation fingerprints. Existing acknowledged types without machine descriptors
|
||||
remain plan-only. The compatibility result never claims authenticated native
|
||||
completion: Request and journal fingerprints are not portable native evidence.
|
||||
Owner authentication/status binding and actual native gates remain unresolved;
|
||||
T06/T07 statuses and both type/case lifecycles remain unchanged. No dispatch,
|
||||
credential access or authority expansion is introduced. See tool README for the
|
||||
existing owner protocol boundaries and source integration tests.
|
||||
|
||||
Concrete next Warden owner integration: current `Executor.admit` refuses every
|
||||
case parameter whose sensitivity is not public/operational, so browser
|
||||
`redirect_uris`, `allowed_scopes`, `client_type`, `mfa_required` (and 1.2
|
||||
`consumer_namespace`) fail after otherwise prepared admission. The sensitive
|
||||
classification is a security-change review level, not secret content. The
|
||||
Warden owner must validate through NetKingdom's existing exact-version case/type
|
||||
validator, then project only descriptor-declared safe target metadata; do not
|
||||
remove the credential/value refusal or copy complete parameters into journals.
|
||||
Actual owner descriptor acknowledgment and independent evidence admission remain
|
||||
prerequisites, with normal admission still refusing unimplemented types.
|
||||
|
||||
|
||||
## T10 — Source compatibility checker for Mason and Warden evidence
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T10
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4b06ca97-c1e4-5680-bf22-ab75ab1646a4"
|
||||
```
|
||||
|
||||
Completed 2026-10-16 in published source `c31d21ba`. The read-only
|
||||
`check-evidence` command checks the actual Mason structural request and Warden
|
||||
journal source protocols against an exact version/render. Independent source
|
||||
review passed 103 tests at that checkpoint. It always reports native completion
|
||||
false and execution prohibited. Fixtures and local journal fingerprints do not
|
||||
provide authenticated owner/native evidence. This records completed source scope
|
||||
previously described under T06; it does not duplicate native acceptance credit.
|
||||
|
||||
## T11 — Validate the optional machine-descriptor pin contract
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T11
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ef2a0119-f59e-5e45-9519-115da9df7b85"
|
||||
```
|
||||
|
||||
Completed 2026-10-16. Reproduced the JSON schema rejecting a valid
|
||||
`execution_descriptors` map while the Python validator accepted malformed maps.
|
||||
The schema now supports the existing Warden optional exact-byte pin map; semantic
|
||||
validation binds keys to declared steps, bounds the map and checks digest shape.
|
||||
The direct evidence API refuses malformed maps before accessing them. Twenty-one
|
||||
new checks and all 127 affected source checks passed. No current type/case bytes,
|
||||
status, owner descriptors or native acceptance changed. Source evidence and
|
||||
publication receipt: Custodian
|
||||
`docs/changes/netkingdom-target-2026-10-16/browser-acceptance-next/`.
|
||||
|
||||
## T12 — Prepare NetKingdom-owned attended step adapters
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T12
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd"
|
||||
```
|
||||
|
||||
Owner: NetKingdom. Retained scope: prepare bounded attended Deployment env,
|
||||
config verification and restart/readiness steps around the existing helpers.
|
||||
Next action: identify exact current entry points and their revision/readback
|
||||
contracts, then implement fixed source-pinned callbacks or receipt-only attended
|
||||
steps. Completion requires positive/refusal checks, exact target/source pins and
|
||||
uncertainty behavior without acquiring user-engine runtime identity or exposing
|
||||
registry/client secrets. Existing caller/session, cluster/issuer, CAS/readback
|
||||
and status-only output boundaries remain mandatory.
|
||||
|
||||
2026-10-16 task refinement: the original broad adapter scope is split into T13
|
||||
(completed registration source candidate), T14 (group creation provenance), T15
|
||||
(durable registration recovery) and T16 (descriptor/artifact selection and owner
|
||||
review). This preserves T12's task ID and Hub UUID while giving each remaining
|
||||
outcome an owner and completion gate. T06 now names every source dependency;
|
||||
its native/reference lifecycle acceptance scope remains unchanged. Completion
|
||||
of any source task supplies no Warden, Platform or Apps assent and no native
|
||||
execution authority.
|
||||
|
||||
## T13 — Prepare the fixed registration adapter source candidate
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T13
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b264c10b-565c-5d29-b788-03cf5fbd2883"
|
||||
```
|
||||
|
||||
Completed 2026-10-16, source implementation and synthetic fixture proof only.
|
||||
Added an injection-only adapter around actual `keycape-client.run`: fixed
|
||||
case/principal/human/lane/source/acceptance binding, existing CAS/readback,
|
||||
metadata-only owner evidence and per-attempt parent-session revocation boundary.
|
||||
The descriptor source composition pins the exact checker executable digest
|
||||
alongside adapter/helper source. Creation-only rollback authenticates its
|
||||
creation receipt and refuses changed postcreation UID/resourceVersion;
|
||||
pre-existing clients confer no rollback authority. Missing readback, evidence,
|
||||
revocation or process-local creation provenance cannot become retry-safe success.
|
||||
|
||||
Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/`
|
||||
R3 candidate, explicit patch and postimages, retained R1 rejected custody finding
|
||||
and R2 revision, and 36 passing source checks (16 new, 20 existing). Three checks
|
||||
use actual Warden kernel admission/dispatch/rollback with published Warden
|
||||
`d97652a3bb5fa733b063d71a1b74068efcccf572` producer pins unchanged. Owner trust,
|
||||
descriptor/type/case and Kubernetes objects remain synthetic fixtures. This
|
||||
records a completed source deliverable, not independent review, publication,
|
||||
type acknowledgment, exercised procedure, identity admission or native browser
|
||||
acceptance. Independent root source review: Custodian browser-adapter-next/coordinator-review-r3.json. Source integration and normal publication/synchronization receipts: Custodian browser-adapter-next/canonical-integration.json. These receipts supply no other-owner assent or native authority.
|
||||
|
||||
Source publication: `0775c2bfa41af236301155bb5a69bbf088bdf823`, independently
|
||||
reviewed by the Custodian coordinator before integration. The required initial
|
||||
projection and ordinary UUID writeback both completed with zero failures and
|
||||
zero automation errors. Generated-only brief commits were refused by the normal
|
||||
hook; exact pre-existing staged/working brief bytes were restored. The separate
|
||||
canonical integration receipt retains those refusals and projection evidence.
|
||||
Native admission and all other-owner acceptance remain open.
|
||||
|
||||
## T14 — Preserve created-only provenance for group ensure and revoke
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T14
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "fce95440-a25b-5f27-8ef3-a3d182099113"
|
||||
```
|
||||
|
||||
Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner
|
||||
ensure/revoke contract with independently retained prestate and exact newly
|
||||
created groups/memberships before exposing reversible owner callbacks. Current
|
||||
ensure returns no membership-creation inventory; current revoke may remove a
|
||||
membership that predated this case. Completion requires actual helper-backed
|
||||
positive/refusal fixtures, all validation before mutation, per-attempt evidence
|
||||
and revocation, readback, ambiguity handling, and rollback that preserves every
|
||||
pre-existing/shared group and membership. No existing endpoint or service token
|
||||
is treated as new authorization.
|
||||
|
||||
## T15 — Define authenticated recovery for lost registration process state
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T15
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "1b825705-4848-5edc-832f-b1622cd9bdb7"
|
||||
```
|
||||
|
||||
Owner: NetKingdom for registration state, with Warden owning kernel evidence
|
||||
admission. Next action: select the exact existing owner evidence holder/readback
|
||||
contract and define authenticated lookup for the original creation attempt.
|
||||
Completion requires restart/lost-receipt fixtures that bind case/source/principal,
|
||||
original created-only provenance and current native revision before any undo;
|
||||
uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses
|
||||
post-restart rollback and installs no reconciliation callback. A new broker or
|
||||
unverified JSON receipt is not a recovery contract. Actual evidence-holder
|
||||
acceptance and attended recovery remain separately authorized native work.
|
||||
|
||||
## T16 — Select and review the actual machine descriptor composition
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T16
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "200bf500-5084-5ae7-be26-3033fe1f296e"
|
||||
```
|
||||
|
||||
Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden
|
||||
independently owns global provenance/admission. Next action: obtain review of the
|
||||
exact checker source/artifact proposed by T17 and identify native owner evidence/
|
||||
session callback contracts, then assemble the complete step descriptor set and
|
||||
a proposed explicit type version selection. T13's historical descriptor retains
|
||||
its synthetic checker; T17 supplies an artifact-bound proposal, not admission.
|
||||
Completion requires separate owner source review of exact machine descriptors,
|
||||
artifact/source/config pins, normal type/version admission and independent
|
||||
evidence-verification boundaries. Warden d97652a refreshes producer validator
|
||||
pins for NetKingdom 015694ee; it does not attest T13's new adapter. Pinning any
|
||||
machine descriptor into an acknowledged type is a separately reviewed source
|
||||
change. Platform owns structural/mint/broker/delivery evidence; Apps owns actual
|
||||
consumer login/group enforcement. Neither their assent nor native acceptance is
|
||||
supplied by this task.
|
||||
|
||||
2026-10-16 source checkpoint: T19 below extracts the completed per-attempt
|
||||
executable snapshot implementation and reviewed fixture proof. T16 retains
|
||||
native executable storage/custody, exact owner evidence/session callbacks,
|
||||
complete descriptor composition and ordinary type/version admission. The
|
||||
successor descriptor/source pins are proposed only; the earlier descriptor
|
||||
history is preserved. T14 group provenance and T15 durable restart/recovery
|
||||
remain independent open requirements. No native acceptance is counted here.
|
||||
|
||||
## T17 — Build and exercise the exact offline config-checker artifact
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T17
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8c3e4baa-a218-5996-af9e-fe1153a46dac"
|
||||
```
|
||||
|
||||
Completed 2026-10-16, isolated artifact/source fixture proof only. Built the
|
||||
existing `key-cape/src/cmd/keycape` command from published clean source revision
|
||||
`3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`, source tree
|
||||
`f0010b9cab17dbc3a787628a29657d98b900320f`, with cached Go 1.23.0,
|
||||
CGO disabled, Linux/amd64 v1, trimmed paths, disabled VCS stamping and offline
|
||||
read-only module selection. The full retained closure pins 108 source files,
|
||||
9876 toolchain files and ten used module source directories. Three isolated
|
||||
build locations produced identical 13633409-byte artifacts:
|
||||
`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`.
|
||||
|
||||
Eight disposable checks use the actual executable and maintained helper/kernel,
|
||||
with complete synthetic configs and a minimal environment: add/created-only
|
||||
removal, normal pinned producer admission, rollback, lost-readback uncertainty,
|
||||
stale revisions, substituted checker refusal, invalid startup config and missing
|
||||
secret-env refusal. No checker stub or producer-pin override remains. The
|
||||
artifact-bound descriptor proposal is
|
||||
`sha256:76b1153d188062ece53fcd5e8d35290fdbfd02a69dd79d4b001fc42781afcdc3`.
|
||||
Evidence: Custodian
|
||||
`docs/changes/netkingdom-target-2026-10-16/browser-checker-artifact-next/`.
|
||||
The first two build attempts succeeded but their closure collection failed;
|
||||
both failures are retained, as is refusal of the older incomplete synthetic
|
||||
fixture. No production config, credentials or cluster mutation was used.
|
||||
|
||||
This extracts completed build/fixture scope from T16 without claiming KeyCape
|
||||
owner endorsement, native checker selection/storage custody, acknowledged type
|
||||
integration, native evidence/session callbacks or attended browser acceptance.
|
||||
T16 retains exact owner review and composition/admission scope. The attended
|
||||
registration procedure remains unexercised.
|
||||
|
||||
## T18 — Make checker input and executable selection deterministic
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T18
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9c45da71-fb4c-5f20-9065-90f448f37c64"
|
||||
```
|
||||
|
||||
Completed 2026-10-16, isolated source correction and real-artifact fixture proof
|
||||
only; independent integration/publication remain separately recorded. KeyCape
|
||||
source review identified non-secret `KEYCAPE_ACCOUNT_PORTAL_URL` and
|
||||
`KEYCAPE_BROWSER_LOGOUT_URL` overrides in config.Load. The original helper
|
||||
inherited ambient environment, allowing valid supplied configuration to be
|
||||
rejected or invalid supplied URL configuration to be admitted. A bare relative
|
||||
checker filename could also be replaced by PATH lookup after the adapter had
|
||||
hashed the selected local file. All three counterexamples are retained against
|
||||
the unchanged predecessor.
|
||||
|
||||
The existing helper now resolves the selected checker file to an absolute path
|
||||
and passes an explicit empty subprocess environment. Three opt-in real-checker
|
||||
regressions verify both URL-verdict directions and relative-file selection under
|
||||
a non-checker PATH decoy. All 39 affected checks pass with the exact supplied
|
||||
artifact, as do 11 real-artifact helper/kernel scenarios. Source evidence:
|
||||
Custodian `docs/changes/netkingdom-target-2026-10-16/browser-checker-environment-r2/`.
|
||||
Historical descriptors remain retained; the helper correction changes successor
|
||||
source-composition pins and requires their separate review/admission. An absolute
|
||||
path is not immutable custody and does not eliminate file replacement races;
|
||||
T16 retains native executable storage/custody and callback/type admission.
|
||||
No production configuration, credentials or cluster mutation was used.
|
||||
|
||||
Source integration and publication: `916ba4b33e03179274d03b78bd898a4843e8653f`, following independent
|
||||
Custodian source review in browser-checker-environment-r2/coordinator-review.json.
|
||||
The ordinary protected projection pass assigned T17/T18 UUIDs with zero failures
|
||||
and zero automation errors; generated-only brief commit refusal is retained.
|
||||
The exact prior staged/working brief bytes were restored. Final source/publication/
|
||||
projection evidence is retained in Custodian browser-checker-environment-r2/
|
||||
canonical-integration.json. These source receipts do not admit native executable
|
||||
custody, owner callbacks, acknowledged type changes or browser identities.
|
||||
|
||||
## T19 — Bind checker execution to verified immutable attempt bytes
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T19
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Completed 2026-10-16, independently reviewed source implementation and synthetic
|
||||
fixture proof only. The registration adapter now opens a bounded regular checker
|
||||
artifact without following its terminal symlink, verifies the reviewed digest,
|
||||
and snapshots the exact bytes into a write/grow/shrink/seal-removal-sealed Linux
|
||||
memfd before helper access. Live and proposed config checks execute that same fd
|
||||
through procfs with the existing empty environment; the fd closes on completion
|
||||
or failure. Unsupported snapshot/seal/exec policy refuses through the existing
|
||||
parent-session revocation and uncertainty boundaries. The earlier unbounded
|
||||
pre-snapshot path read was removed during independent review; actual adapter
|
||||
symlink/FIFO/oversize refusal tests verify helper access never starts.
|
||||
|
||||
Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-resume-r1/`,
|
||||
including exact base/preimages/postimages/patch, proposed descriptor, coordinator
|
||||
source review and canonical integration receipt. Independent Custodian checks
|
||||
passed 49 affected source tests and 12 actual Go checker/helper/kernel scenarios;
|
||||
the sets overlap and are not separate acceptance counts. Existing published
|
||||
Warden d97652a producer pins remain unchanged. The exact KeyCape artifact remains
|
||||
`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`.
|
||||
The predecessor path-replacement refusal and the first expanded reopened-fd test
|
||||
failure are retained; the latter fixture was corrected to restore write mode
|
||||
before independently proving seals block reopened-fd writes and shared mmap.
|
||||
|
||||
This source task adds no native owner custody, callback, evidence trust, type
|
||||
acknowledgment, credential access, consumer sign-in or rollback acceptance.
|
||||
Executable memfds, Linux seals and usable procfs are explicit runtime dependencies
|
||||
and still require native admission. This is an extracted source outcome from
|
||||
T16, not completion of T16; T12/T14/T15 and all external-owner gates remain open.
|
||||
|
|
@ -0,0 +1,827 @@
|
|||
---
|
||||
id: NK-WP-0048
|
||||
type: workplan
|
||||
title: "Establish SecurityCaseTypes so that known security cases are light"
|
||||
domain: infotech
|
||||
repo: net-kingdom
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: claude-code
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-08"
|
||||
updated: "2026-10-16"
|
||||
depends_on: []
|
||||
related_repos:
|
||||
- ops-mason
|
||||
- ops-warden
|
||||
- key-cape
|
||||
- railiance-platform
|
||||
- informed-decision
|
||||
state_hub_workstream_id: "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
---
|
||||
|
||||
# NK-WP-0048 - Establish SecurityCaseTypes so that known security cases are light
|
||||
|
||||
## Why
|
||||
|
||||
The founder's direction (2026-10-08): setting up a security integration of a
|
||||
kind that has been done before should be easy, meaning accept the case, then
|
||||
ops-mason arranges it and ops-warden follows through. Setting up a new kind
|
||||
needs ample documentation and tests and an informed-decision, because its shape
|
||||
is part of the security architecture.
|
||||
|
||||
Today every integration is hand-built: per-client helper scripts, per-lane
|
||||
custody, founder checklists. The trigger was KEY-WP-0038 (Forgejo as an OIDC
|
||||
client), whose next step was going to be yet another hand-written rollout
|
||||
script.
|
||||
|
||||
This plan continues the pattern work of NK-WP-0008 (pattern infospace),
|
||||
NK-WP-0009 (tutorials) and NK-WP-0030 (scenario composition). It turns patterns
|
||||
into **SecurityCaseTypes** that ops-mason and ops-warden can execute. Canon:
|
||||
`canon/standards/security-case-types_v0.1.md` (proposed).
|
||||
|
||||
Companion plans: ops-mason `MASON-WP-0010` (materialization) and ops-warden
|
||||
`WARDEN-WP-0044` (follow-through).
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "930ac3e9-20ec-5473-98cd-3fe8fddb730f"
|
||||
```
|
||||
|
||||
### T01 — Accept the terminology and the canon
|
||||
|
||||
Founder review of `canon/standards/security-case-types_v0.1.md`, including its
|
||||
section 10 open questions. On acceptance the canon status goes from `proposed`
|
||||
to `accepted`.
|
||||
|
||||
2026-10-08: accepted by the founder (in session). Canon status `accepted`;
|
||||
section 10's open questions are carried forward to the first type (T04, T05).
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "19c25d96-a55c-58f0-9168-3fdbc4955fc2"
|
||||
```
|
||||
|
||||
### T02 — Inventory the de-facto types already built by hand
|
||||
|
||||
Read-only. List every security integration built so far and group them by shape.
|
||||
Known starting points:
|
||||
|
||||
- browser OIDC relying parties on key-cape: informed-decision-approver,
|
||||
coulomb-social, user-engine portal, openbao-admin, and Forgejo (pending)
|
||||
- confidential service clients with `client_credentials`: secrets-engine
|
||||
approval, requester and openbao clients, approval-engine operator
|
||||
- workload KV read lanes on OpenBao: the railiance-platform credential lane
|
||||
designs and CCRs, for example `railiance-backup-offsite-lane`
|
||||
- short-lived SSH certificates through ops-warden (`warden sign`)
|
||||
- tunnelled operator access (ops-bridge)
|
||||
|
||||
For each: the repositories involved, the hand-built steps, attended human steps,
|
||||
custody paths, verification evidence, and rollback (if any). Output
|
||||
`docs/security-case-type-inventory.md`, ranking candidate types by how often the
|
||||
shape recurs and how much manual work each instance costs today.
|
||||
|
||||
2026-10-08: done, `docs/security-case-type-inventory.md`. 36 of 37 CCRs are
|
||||
already `request_type: workload-kv-read`, a de-facto type reviewed each time as
|
||||
if it were new. Browser clients: 5 hand-built plus Forgejo, each with its own
|
||||
rollout script. Confidential service clients need a registration plus two or
|
||||
three KV-read CCRs each. A forge read identity is needed in bulk by
|
||||
ORGREF-WP-0002-T06. Recommendation: oidc-browser-relying-party first (unblocks
|
||||
the forge), workload-kv-read second (largest payoff), then forge-read-identity.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T03
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T01]
|
||||
priority: high
|
||||
state_hub_task_id: "e487975b-73b1-516c-971d-da4cbbb93063"
|
||||
```
|
||||
|
||||
### T03 — Schema, template and validator
|
||||
|
||||
- `canon/schemas/security-case-type_v0.1.schema.json` and
|
||||
`security-case_v0.1.schema.json`
|
||||
- `security-case-types/_template/` with `type.yaml`, `README.md` and tests to copy
|
||||
- a validator, extending `tools/security-scenario-composer/`, that checks a type,
|
||||
checks a case against its type, and renders the case into the composer's
|
||||
deterministic plan (still `execution.permitted: false`)
|
||||
|
||||
Acceptance: the validator rejects missing sections, secret-shaped parameter
|
||||
values, unpinned providers and cases against non-`implemented` types, with tests
|
||||
for each.
|
||||
|
||||
2026-10-08: done. `canon/schemas/security-case-type_v0.1.schema.json` and
|
||||
`security-case_v0.1.schema.json`; `tools/security-case-types/security_case_validator.py`
|
||||
(`validate-type`, `validate-case`, `render`, reusing the scenario composer);
|
||||
`security-case-types/_template/` (a valid, runnable example type over
|
||||
`net-kingdom.local-identity`, with README sections, an example case and tests that
|
||||
travel with every copy). 31 tests pass, including the composer's existing suite.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T04
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T02, NK-WP-0048-T03]
|
||||
priority: high
|
||||
state_hub_task_id: "2fffe112-7d55-5680-9952-78d74f44f87f"
|
||||
```
|
||||
|
||||
### T04 — Define the first type: browser OIDC relying party
|
||||
|
||||
`security-case-types/oidc-browser-relying-party/`, version 1.0:
|
||||
|
||||
- parameters: client id, display name, exact redirect URIs, scopes, client type
|
||||
(confidential or public), MFA pin, the groups the consuming service maps,
|
||||
custody path for the secret (confidential only), consuming service and its
|
||||
owner
|
||||
- composition: key-cape client registration (key-cape declares it as a playbook
|
||||
capability), secret minting into custody (railiance-platform), LLDAP group
|
||||
existence and membership (net-kingdom identity-provisioner), handover to the
|
||||
consuming service
|
||||
- materialization (ops-mason) and follow-through (ops-warden) sections
|
||||
- verification: discovery lists the redirect URI; a test sign-in returns the
|
||||
expected claims for one human; anonymous access to the custody path is denied
|
||||
- rollback: remove exactly this client and its custody path
|
||||
- README: threat model, MFA rationale, why redirect URIs are exact, which steps
|
||||
still need an attended login and how that is minimised
|
||||
- reference instances: informed-decision-approver, coulomb-social, user-engine
|
||||
portal, openbao-admin
|
||||
|
||||
Acceptance: complete per canon section 3, tests green, status `proposed`.
|
||||
|
||||
2026-10-08: done. `security-case-types/oidc-browser-relying-party/` 1.0 (proposed):
|
||||
`type.yaml` with 12 parameters (the custody reference is required only for
|
||||
confidential clients), composition over key-cape's new
|
||||
`key-cape.oidc-client-registration` declaration (contract PASS, `maturity:
|
||||
planned`), materialization and follow-through, verification and rollback, all
|
||||
conditional on client type where it matters. One attended step,
|
||||
`register-client`, justified by the live registry being a Kubernetes Secret.
|
||||
README with threat model, alternatives, an honest fit table (portal and vergabe
|
||||
fit; Forgejo is the first confidential case; openbao-admin, coulomb.social and
|
||||
informed-decision-approver do not fit 1.0), and known limits. Examples: Forgejo
|
||||
(checked against the accepted request) and a public portal. The validator gained
|
||||
`when` / `required_when` conditions and provider discovery. 43 tests pass.
|
||||
ops-mason's and ops-warden's sections are drafted in the type. Their owners
|
||||
review them in MASON-WP-0010-T01 and WARDEN-WP-0044-T01 before acknowledgment
|
||||
(T05).
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T05
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T04, MASON-WP-0010-T01, WARDEN-WP-0044-T01]
|
||||
priority: high
|
||||
state_hub_task_id: "3d987d3d-0797-538b-86e4-51185a327ae2"
|
||||
```
|
||||
|
||||
### T05 — Acknowledge the type through informed-decision
|
||||
|
||||
Prepare the Decision Memo from the type's README and `type.yaml` and submit it
|
||||
through informed-decision's production intake. On acknowledgment the type
|
||||
moves to `acknowledged`.
|
||||
|
||||
2026-10-08: Decision Memo prepared. Hub decision `e418263e-a71f-4f01-a970-c87c9e187c09`.
|
||||
`security-case-types/oidc-browser-relying-party/decision/`: `packet.md` (question,
|
||||
one act, what acknowledging does not do, the shape, alternatives, attended
|
||||
steps, limits, evidence), `build_intake.py` and the built `intake.json` (memo
|
||||
class `decision-record`, review group `net-kingdom-admins`, resolves the hub
|
||||
decision on answer). The envelope passes informed-decision's own
|
||||
`validate_submission`. Queuing it is an attended operator step, a write inside
|
||||
the production dispatcher pod:
|
||||
|
||||
kubectl -n state-hub exec -i deploy/state-hub-memo-dispatch -- \
|
||||
python -m api.services.memo_dispatch --queue /data/private enqueue --envelope /dev/stdin \
|
||||
< security-case-types/oidc-browser-relying-party/decision/intake.json
|
||||
|
||||
The answer then appears for review in informed-decision.
|
||||
|
||||
2026-10-08: queued by the founder (`pending`, `queued`) after one refusal: the
|
||||
first envelope's idempotency key was not State Hub's stable
|
||||
`state-hub.<decision>.<memo>` form, and the builder now validates against both
|
||||
sides. The dispatcher delivered it to informed-decision (ledger 7 → 8
|
||||
delivered). T05 waits for the founder's answer in informed-decision, which
|
||||
resolves hub decision `e418263e`.
|
||||
|
||||
2026-10-08: ops-mason's review (MASON-WP-0010-T01) folded into the type, still
|
||||
1.0 `proposed`: `secret_ref` is no longer a parameter, the type derives
|
||||
`platform/workloads/keycape/{client_id}`; policies are
|
||||
`workload-kv-read-keycape-{client_id}-eso` and
|
||||
`workload-kv-read-keycape-{client_id}-{consuming_service}`, each with a templated
|
||||
body (`read` on that one data path); new `openbao_role`
|
||||
`external-secrets-keycape-{client_id}` (Kubernetes auth, ESO service account,
|
||||
15m, no default policy); catalog entry `{consuming_service}-keycape-client`;
|
||||
the two rollback steps on the KV path collapse into one metadata delete. The
|
||||
schema and validator now require a `body` on policy and role items. 105 tests
|
||||
pass. Two consequences: the delivered memo describes the type before these
|
||||
changes (a case-set custody reference, two differently named policies, no ESO
|
||||
role), so the founder answers on a superseded shape unless the memo is
|
||||
re-sent; and Forgejo's custody path moves from the accepted request's proposed
|
||||
`platform/workloads/forgejo/netkingdom-oidc-client` to
|
||||
`platform/workloads/keycape/forgejo`, which key-cape (KEY-WP-0038) and
|
||||
railiance-apps (RAPPS-WP-0016-T02) need to adopt in T06. ops-warden's review
|
||||
(WARDEN-WP-0044-T01) is still in progress.
|
||||
|
||||
2026-10-08: revised memo `nk-wp-0048-t05-oidc-browser-relying-party-r2` built
|
||||
(`packet.md`, `intake.json`, packet `sha256:af6435e2…`); it states that it
|
||||
supersedes the first memo and passes informed-decision's and the dispatcher's
|
||||
validation. It needs a new memo id because the dispatcher refuses new content
|
||||
under a delivered idempotency key (`idempotency_conflict`). Queuing it is the
|
||||
same attended step as above. Both memos serve hub decision `e418263e`; the
|
||||
first answer resolves it, so the first memo must not stay open: an accept on it
|
||||
would acknowledge the superseded shape, and a decline would resolve the
|
||||
decision against r2. Close it with `return` (a reviewer verb that requests a
|
||||
new revision and does not resolve the hub decision; only accept and decline
|
||||
do), commenting that r2 supersedes it, then answer r2. `withdraw` exists in
|
||||
the vocabulary but is not offered to reviewers, and lifecycle `revise` only
|
||||
applies to lifecycle-registered memos, not intake-delivered ones.
|
||||
|
||||
2026-10-09: the founder reports r2 enqueued and the first memo returned. At
|
||||
close, hub decision `e418263e` still showed `open` (not verified whether r2
|
||||
was answered or the answer is still in transit; the dispatcher ledger was not
|
||||
read). ops-warden's review (WARDEN-WP-0044-T01, ops-warden
|
||||
`history/2026-10-08-oidc-rp-follow-through-review.md`, d470f2e) arrived at
|
||||
21:25, before r2 was built, and is not in r2. Required before WARDEN-WP-0044-T02:
|
||||
R1 a per-step `lane` (missing: identity-provisioner caller credential,
|
||||
railiance-platform broker grant), R2 canon meaning of `warden_executes: true`
|
||||
(caller proxy, caller identity, no standing ops-warden token; ADR-0002), R3 a
|
||||
per-step `value_handling`. Refinements R4 (one type-level catalog lane instead
|
||||
of a per-case entry, which conflicts with ops-mason's item 5), R5 (ESO Ready gate
|
||||
before bind-env), R6 (exact accepted memberships), R7 (mfa-pinned and
|
||||
secret-delivered checks). Next: if r2 was accepted, fold R1-R3 and R5-R7 in as
|
||||
a follow-up and settle R4 between ops-warden and ops-mason before T06; if not,
|
||||
hold r2, fold them in and build r3.
|
||||
key-cape and railiance-apps were told about Forgejo's new custody
|
||||
path.
|
||||
|
||||
2026-10-09: r2 was not answered (hub decision `e418263e` open; dispatcher 9
|
||||
delivered), so r2 is held and r3 replaces it. ops-warden withdrew R4 for R4′
|
||||
(message a66a9204): one catalog entry per case with its body templated in the
|
||||
type. Folded in:
|
||||
|
||||
- Canon section 3: a narrow meaning of `warden_executes: true` (caller proxy,
|
||||
caller identity, no standing credential; ops-warden ADR-0002), plus per-step
|
||||
`lane`, `value_handling` and an optional `ready_check`. A catalog entry body is
|
||||
now required, like policy and role bodies.
|
||||
- Schema and validator enforce all of these.
|
||||
- The type: lanes `identity-provisioner-groups` and
|
||||
`keycape-client-secret-generate` (both new) and `key-cape-oidc-login`;
|
||||
`mint-secret` is `generated_in_custody`; a Ready gate on `deliver-secret`;
|
||||
`ensure-groups` adds exactly the accepted case's memberships; new checks
|
||||
`secret-delivered` and `mfa-pinned`; the catalog-entry body follows R4′.
|
||||
- 110 tests pass.
|
||||
|
||||
Memo r3 (`nk-wp-0048-t05-oidc-browser-relying-party-r3`, packet
|
||||
`sha256:726dfc01…`) passes informed-decision's and the dispatcher's validation.
|
||||
The founder queues it with the same command, returns r2 with a comment that r3
|
||||
supersedes it, then answers r3.
|
||||
|
||||
Prerequisites for T06, not for T05:
|
||||
- the two new lanes, each through its owner;
|
||||
- a founder-approved rollout of an identity-provisioner image containing
|
||||
630ebfd (the deployed one is main-6c4fcaf from 2026-09-24, without
|
||||
`/v1/groups/ensure`).
|
||||
|
||||
ops-mason's request to create LLDAP group `fluid-telegram-operators` (message
|
||||
259ec26d) was answered: not possible from here by that path; the founder creates
|
||||
it in the LLDAP admin UI.
|
||||
|
||||
2026-10-09: ops-warden finished its side of WARDEN-WP-0044-T01 (message
|
||||
`58d33f40`, commit 0e7ceb8): the revised type conforms to `warden case
|
||||
check-type`, with no further changes asked of the type. This acknowledges that
|
||||
review. Its later addendum R8 (message `af274783`, ops-warden
|
||||
`history/2026-10-08-oidc-rp-follow-through-review.md`) showed that lane
|
||||
`identity-provisioner-groups` cannot exist as declared: the provisioner's only
|
||||
HTTP credential is user-engine's runtime token, and its NetworkPolicy admits
|
||||
only user-engine pods, so a caller-proxy lane would act as user-engine. Taken
|
||||
option 1 (ops-warden's recommendation): `ensure-groups` and its rollback are
|
||||
attended, in the `register-client` session on `key-cape-oidc-login`, run inside
|
||||
the provisioner pod; no token is read. Option 2 (an operator credential of its
|
||||
own on the provisioner) stays open for a later version. `warden case
|
||||
check-type` now reports only `keycape-client-secret-generate`
|
||||
(railiance-platform) as missing. 89 tests pass.
|
||||
|
||||
Hub decision `e418263e` was still open, so memo r4
|
||||
(`nk-wp-0048-t05-oidc-browser-relying-party-r4`, packet `sha256:452ea421…`)
|
||||
replaces r3; it passes informed-decision's and the dispatcher's validation. The
|
||||
founder queues it with the same command, returns r2/r3 if still open with a
|
||||
comment that r4 supersedes them, then answers r4.
|
||||
|
||||
Prerequisites for T06 are now: lane `keycape-client-secret-generate`
|
||||
(railiance-platform), and a founder-approved rollout of an identity-provisioner
|
||||
image containing 630ebfd.
|
||||
|
||||
2026-10-09: done. The founder queued r4 (`pending`, `queued`), returned r3 as
|
||||
superseded and accepted r4 in informed-decision. informed-decision's hub
|
||||
write-back is off in production (`INFD_HUB_WRITEBACK` unset), so net-kingdom
|
||||
resolved hub decision `e418263e` from the founder's report, naming memo r4 in
|
||||
the rationale. The type is `acknowledged`; it becomes `implemented` with T06.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T06
|
||||
status: wait
|
||||
depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, NK-WP-0048-T17, NK-WP-0048-T18, MASON-WP-0010-T03, WARDEN-WP-0044-T03]
|
||||
blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status."
|
||||
priority: high
|
||||
state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c"
|
||||
```
|
||||
|
||||
### T06 — First KnownSecurityCase: Forgejo
|
||||
|
||||
Write `security-cases/forgejo-netkingdom-oidc.yaml` from the parameters the
|
||||
founder accepted in KEY-WP-0038-T01 (`key-cape/docs/forgejo-oidc-client-request.yaml`),
|
||||
so that acceptance carries over as the case acceptance. ops-mason materializes it;
|
||||
ops-warden follows through and verifies it. The type becomes `implemented` once
|
||||
this reference case is live.
|
||||
|
||||
This replaces the hand-built path proposed for KEY-WP-0038-T02.
|
||||
|
||||
2026-10-09: case written, `security-cases/forgejo-netkingdom-oidc.yaml`, status
|
||||
`accepted`: the example's parameters plus the founder's 2026-10-08 acceptance of
|
||||
the key-cape request (owner and founder), with the custody-path derivation
|
||||
covered by the type's acknowledgment (memo r4). It validates and renders (plan
|
||||
`execution.permitted: false`; three attended steps on `key-cape-oidc-login`);
|
||||
it needs `--allow-unimplemented` until it is live, since it is the case that
|
||||
makes the type `implemented`. A test keeps it equal to the type's example.
|
||||
Still open before it runs: ops-mason renders and builds it (MASON-WP-0010-T02,
|
||||
T03, unblocked by T05), ops-warden plans and runs it (WARDEN-WP-0044-T02, T03),
|
||||
railiance-platform's `keycape-client-secret-generate` lane (asked by ops-mason
|
||||
`789feb39` and ops-warden `d9f889b1`, both unread), and a founder-approved
|
||||
identity-provisioner rollout containing 630ebfd.
|
||||
|
||||
2026-10-09: identity-provisioner rolled out, founder-approved in session:
|
||||
`main-630ebfd` (`sha256:bafb4860…`, built by the image workflow) replaces
|
||||
`main-6c4fcaf` (`sha256:ffacd5d7…`; only change: 630ebfd, additive). Applied
|
||||
with `kubectl set image` on the Deployment only (generation 17 → 18, 1/1 Ready);
|
||||
the file's Service, NetworkPolicies and Ingress were not reapplied. Verified in
|
||||
the pod: `ensure_service_groups` and `revoke_service_groups` present, `/readyz`
|
||||
200; no token used, no group changed. Rollback: `kubectl -n sso set image
|
||||
deploy/identity-provisioner provisioner=forgejo.coulomb.social/coulomb/identity-provisioner@sha256:ffacd5d790a5b7800e0535f3aa95b7de52c7a68e4f575b9192c3e1622fa672c2`.
|
||||
The pin in `sso-mfa/k8s/identity-provisioner/deployment.yaml` follows.
|
||||
|
||||
2026-10-09: ops-warden closed WARDEN-WP-0044-T01 and shipped `warden case plan`
|
||||
(T02, message `06303d1d`); the accepted Forgejo case is refused until ops-mason
|
||||
materializes it, and a materialized copy plans cleanly once
|
||||
`keycape-client-secret-generate` exists. Its observation that `bind-env` carries
|
||||
a literal `KEYCAPE_<CLIENT_ID>_CLIENT_SECRET`: the validator now renders a
|
||||
`{param:env}` format spec (upper case, `-` as `_`, as `keycape-client.py`
|
||||
derives the name) and rejects unknown specs, and follow-through and rollback
|
||||
text is now checked for unknown placeholders too (patch-level, canon section 6).
|
||||
The type keeps the literal until ops-warden's planner (`text.format`) renders the
|
||||
spec as well; asked of ops-warden. 53 tests pass.
|
||||
|
||||
2026-10-10: ops-warden rendered `{param:env}` in `warden case plan`
|
||||
(23ba584, message `c1df14b7`; other specs and `!conversion` refused). The type
|
||||
adopts it as a 1.0 patch (canon section 6): `bind-env` and its rollback now read
|
||||
`KEYCAPE_{client_id:env}_CLIENT_SECRET`. ops-mason reports the Forgejo case
|
||||
rendered and built (message `1ebddae9`; CCR drafts handed to railiance-platform),
|
||||
so T06 now waits on the platform apply, the mint-secret lane and the attended
|
||||
register-client session.
|
||||
|
||||
2026-10-10: key-cape adopted the type's custody path (key-cape ab5cccd, message
|
||||
`88a4559c`): `docs/forgejo-oidc-client-request.yaml` and KEY-WP-0038-T02 name
|
||||
`platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, delivered as
|
||||
`sso/keycape-forgejo-client` through `external-secrets-keycape-forgejo`, and the
|
||||
`forgejo/netkingdom-oidc-client` proposal is withdrawn. This matches the type and
|
||||
the Forgejo example; no change needed here.
|
||||
|
||||
2026-10-10: key-cape's type-composition review (key-cape 6038483, message
|
||||
`8e5ed5f6`) found the composition sound with two blocking findings, taken as a
|
||||
1.0 patch (stricter validation and documentation, canon section 6):
|
||||
R1, redirect URIs now refuse `?` and `#` in the type pattern and in
|
||||
`keycape-client.py`'s `REDIRECT` (KeyCape refuses `?` at startup and the single
|
||||
replica would stop); R2, `allowed_scopes` always includes `groups`, the renderer
|
||||
refuses `access_groups` without it, and `access_groups` now says the consumer
|
||||
enforces the gate because KeyCape has none; R3, `mfa_required` "false" means the
|
||||
per-user privacyIDEA policy; R4, register-client's justification records the
|
||||
restart for every client, the quiet window, and that the rendered registry must
|
||||
pass KeyCape's client validation before it. The accepted Forgejo case and both
|
||||
examples still validate; 116 tests pass.
|
||||
Open, needs a decision: R2's verification step that the consumer enforces the
|
||||
group gate is a new verification step, hence 1.1 (minor), and the validator pins
|
||||
cases to the exact type version, so the accepted 1.0 Forgejo case would need
|
||||
re-pinning. Also open with key-cape: a KeyCape entry point that validates a
|
||||
rendered client registry without starting the server; until then
|
||||
`keycape-client.py` mirrors the redirect rule.
|
||||
|
||||
2026-10-10: founder go for 1.1, so no 1.0 run overlaps. The type adds
|
||||
verification `consumer-enforces-groups`, and hand-over now requires the consumer
|
||||
to enforce `access_groups` (minor, canon section 6; risk class unchanged, no new
|
||||
acknowledgment; history in the type README). The accepted Forgejo case and both
|
||||
examples are re-pinned to `oidc-browser-relying-party@1.1` before any apply; the
|
||||
acknowledgment intake keeps `@1.0` as the record of what was acknowledged.
|
||||
ops-mason's build and ops-warden's case-ref digest predate this and need a
|
||||
re-render.
|
||||
|
||||
2026-10-10: ops-mason re-rendered the Forgejo case from `@1.1` (message
|
||||
`22c6bba6`; plan digest `af1f4f81…`, no collisions) and rebuilt the bundle. The
|
||||
rendered OpenBao objects are identical to 1.0, so the already applied
|
||||
CCR-2026-0041 stands; 1.1 changes only consumer-side steps. key-cape shipped
|
||||
`keycape check-config` (c528daa, message `a7b0aeff`): KeyCape's startup checks
|
||||
without secret values, including the new duplicate-clientId rule.
|
||||
`keycape-client.py --check-config <binary>` now runs it on the live and the
|
||||
proposed registry, fed on stdin, and apply refuses a proposed registry that
|
||||
fails; register-client's entry point names the option (1.1 patch). The deployed
|
||||
image predates c528daa, so the binary is built from key-cape source for now.
|
||||
Before the next KeyCape rollout (Forgejo register-client or the CCR-2026-0018
|
||||
window), the attended operator runs `inspect --check-config` so the live
|
||||
registry is checked too.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T07
|
||||
status: wait
|
||||
depends_on: [NK-WP-0048-T06]
|
||||
blocking_reason: "The next types follow once the first one is proven end to end."
|
||||
priority: medium
|
||||
state_hub_task_id: "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7"
|
||||
```
|
||||
|
||||
### T07 — Queue the next types
|
||||
|
||||
From the T02 ranking, open the next type drafts (likely the confidential service
|
||||
client and the workload KV read lane), each to be acknowledged on its own.
|
||||
Record which existing hand-built instances get adopted as cases, without
|
||||
rewriting them in place.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T08
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8e519082-dc8b-5e46-b660-03d03e55dcf6"
|
||||
```
|
||||
|
||||
### T08 — identity-provisioner: ensure service-access groups
|
||||
|
||||
The type's `ensure-groups` step calls `identity-provisioner groups ensure --groups
|
||||
<list> --members <group=user+user;...>`. The provisioner already has idempotent
|
||||
`_ensure_group` and `_add_group`, but exposes only tenant-scoped operations. Add
|
||||
the general operation (create if absent, add the listed memberships, never
|
||||
remove others), its rollback (remove only this case's memberships), tests, and
|
||||
the ops-warden route for its service identity.
|
||||
|
||||
2026-10-08: done. `identity-provisioner/provisioner.py` gained
|
||||
`ensure_service_groups` (`POST /v1/groups/ensure`) and `revoke_service_groups`
|
||||
(`POST /v1/groups/revoke`), behind the existing service-token check. Names
|
||||
matching `tenant*`, `netkingdom-*` and `lldap_*` are reserved; members must
|
||||
already exist; everything is validated before any change; memberships are read
|
||||
back before success; nothing else is ever removed; groups are deleted only when
|
||||
asked and empty. 7 new tests (39 in the provisioner suite). The type's
|
||||
`ensure-groups` step and its rollback now name these endpoints. The ops-warden
|
||||
route for the provisioner's service token belongs to WARDEN-WP-0044-T01.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T09
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "0ee12dfc-8260-5038-bd39-cc7fb4113be0"
|
||||
```
|
||||
|
||||
### T09 — The `keycape-client` entry point and its declaration
|
||||
|
||||
Planned first as key-cape KEY-WP-0038-T04. Moved here because the machinery a
|
||||
live registration needs already lives in net-kingdom, where KeyCape is deployed
|
||||
(`sso-mfa/k8s/keycape/`): the contained kubectl, the cluster-identity pin and the
|
||||
issuer pin. A copy in key-cape would drift.
|
||||
|
||||
2026-10-08: done. `sso-mfa/k8s/keycape/keycape-client.py` derives the KeyCape
|
||||
client record from a case (`client-from-case`). It adds or removes exactly one
|
||||
client with the existing safeguards (byte-preserving merge, exact match or
|
||||
refuse, shared-reference refusal, compare-and-swap, read-back, contained errors),
|
||||
requires the Deployment env binding first for confidential clients, and requires
|
||||
the case acceptance record for `apply` instead of a per-client founder decision
|
||||
id. 15 tests, including a regression showing that the vergabe case reproduces
|
||||
its hand-built registration exactly; all 71 helper tests pass. Declaration
|
||||
`capabilities/playbooks/net-kingdom.keycape-client-registration.yaml` (contract
|
||||
PASS, `maturity: implemented`) replaces key-cape's draft. The type now pins it,
|
||||
and for confidential clients adds `deliver-secret` (delivery from custody into
|
||||
the sso namespace) and `bind-env` (Deployment env), attended in the same session
|
||||
as `register-client`. The per-client helpers stay until their clients are
|
||||
adopted as cases.
|
||||
|
||||
2026-10-10 — NetKingdom owner review of the execution seam, source only:
|
||||
ops-warden/docs/security-case-execution-proposal.md SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
|
||||
was reviewed against INTENT, SecurityCase canon, acknowledged type1.1, current
|
||||
case and owner source. docs/security-case-execution-owner-review.md supports
|
||||
the closed adapter/receipt direction with explicit versioned schema, structural
|
||||
materialization provenance, unchanged caller/session boundaries, safe CAS
|
||||
reconciliation and exact-created-object rollback requirements. This is owner
|
||||
review, not approval. Supporting file digests are in the adjacent sources JSON.
|
||||
|
||||
Current source exposes a circular wait: ops-mason T03 waits on mint while Warden
|
||||
and the broker refuse a non-materialized case. Structural acceptance therefore
|
||||
needs its own native pinned receipt before mint, not a successful mint before
|
||||
materialized. The platform broker additionally pins type1.0 while Forgejo is
|
||||
accepted at1.1; platform-owner alignment and tests are required, not an unchecked
|
||||
version bypass. T06 remains wait. No case/type lifecycle status, grant, authority
|
||||
or live runtime changed by this review. A proposed informed-decision would
|
||||
adopt the source contract only; native materialization and execution acceptance
|
||||
stay distinct.
|
||||
|
||||
Final proposal re-review: staged Warden proposal SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
|
||||
adds only the explicitly local reducer appendix, not an accepted wire protocol
|
||||
or executor. The exact-final review and sources JSON supersede the earlier
|
||||
1035f049... proposal review. Platform staged the exact type1.1 broker correction
|
||||
with ten fake-backend subprocess checks (script SHA256
|
||||
9576cd211cfc06526ec4cc9bd0a5b06f0f59287a68931bb2d6e3592a951ddc7d).
|
||||
The next source work is the versioned closed adapter/receipt/materialization
|
||||
contract, not another relaxation of the broker's status gate. Native work still
|
||||
requires installed broker provenance, structural reader receipts, exact new
|
||||
materialized-case bytes, caller grant/session revocation and verified execution.
|
||||
No installed/native acceptance is inferred from that staged correction.
|
||||
|
||||
2026-10-11 — new source-only type1.2 architecture acknowledgment:
|
||||
Confirmed native founder ACCEPT of infd-20261011-forgejo-type12-source-r1@1
|
||||
at06:47:39Z, approval8e9332c8-754a-420c-b8b7-445cb4fe6765. Exact frozen packet
|
||||
0b377d647ca748cd03c92f9792396412048055d4bde9f7bb06ff6d413f7acd68 and submission
|
||||
585cee731de30d2b37883a57abfd9eb32f023ee694e8d911989109d5caf80569; native receipt
|
||||
retained in docs/evidence/2026-10-11-forgejo-type12-source-acknowledgment/.
|
||||
The complete acknowledged1.2 source package is isolated under
|
||||
security-case-type-revisions/1.2/oidc-browser-relying-party; default loader and
|
||||
current accepted1.1 case remain untouched because load_type is single-version.
|
||||
The new proposed1.2 case is NOT accepted/materialized/live and has no acceptance
|
||||
fields. Actual reference-migration design approval is a separate Apps owner
|
||||
receipt; no1.1 approval is inherited. Source-recording review/test proof and exact
|
||||
next gates are in docs/forgejo-type12-source-acknowledgment-followthrough.md.
|
||||
T06 remains wait and T07 remains its existing dependent task; type is NOT
|
||||
implemented and no first-case/native grant or broader workplan finish is asserted.
|
||||
|
||||
2026-10-11 — bounded explicit version selection source preparation for T06:
|
||||
The validator now offers optional exact version_roots/--version-root VERSION=PATH
|
||||
selection with duplicate/mismatch/traversal/symlink refusal and no fallback. The
|
||||
default1.1 source and historicalacceptedcase hashes/ordinaryrefusal/preparation
|
||||
render stayunchanged; actual1.1 is acknowledged, not implemented. Acknowledged1.2
|
||||
ordinaryadmission also refuses; optionalpreparation remains nonexecuting.43local
|
||||
validatorchecks pass and original1.1 CLIrenderbytes match exactly. Makerproof:
|
||||
docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json.
|
||||
FormalNetownerreview/reference-source integration and all existing native adapter,
|
||||
provenance/broker/materialization/attendedproof gates remain; T06 stayswait, no
|
||||
new residual or firstcase/typeimplementation/native grant is inferred.
|
||||
|
||||
|
||||
2026-10-16 — isolated source preparation for T06 (canonical integration pending):
|
||||
Added `check-evidence` to the existing validator CLI. It reuses exact version
|
||||
selection and ordinary type/case/render admission, checks actual Mason structural
|
||||
request source/inventory/projection pins and reads actual Warden journal rows
|
||||
read-only. Successful rows require exact type-declared descriptors and attempt/
|
||||
revocation fingerprints. Existing acknowledged types without machine descriptors
|
||||
remain plan-only. The compatibility result never claims authenticated native
|
||||
completion: Request and journal fingerprints are not portable native evidence.
|
||||
Owner authentication/status binding and actual native gates remain unresolved;
|
||||
T06/T07 statuses and both type/case lifecycles remain unchanged. No dispatch,
|
||||
credential access or authority expansion is introduced. See tool README for the
|
||||
existing owner protocol boundaries and source integration tests.
|
||||
|
||||
Concrete next Warden owner integration: current `Executor.admit` refuses every
|
||||
case parameter whose sensitivity is not public/operational, so browser
|
||||
`redirect_uris`, `allowed_scopes`, `client_type`, `mfa_required` (and 1.2
|
||||
`consumer_namespace`) fail after otherwise prepared admission. The sensitive
|
||||
classification is a security-change review level, not secret content. The
|
||||
Warden owner must validate through NetKingdom's existing exact-version case/type
|
||||
validator, then project only descriptor-declared safe target metadata; do not
|
||||
remove the credential/value refusal or copy complete parameters into journals.
|
||||
Actual owner descriptor acknowledgment and independent evidence admission remain
|
||||
prerequisites, with normal admission still refusing unimplemented types.
|
||||
|
||||
|
||||
## T10 — Source compatibility checker for Mason and Warden evidence
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T10
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4b06ca97-c1e4-5680-bf22-ab75ab1646a4"
|
||||
```
|
||||
|
||||
Completed 2026-10-16 in published source `c31d21ba`. The read-only
|
||||
`check-evidence` command checks the actual Mason structural request and Warden
|
||||
journal source protocols against an exact version/render. Independent source
|
||||
review passed 103 tests at that checkpoint. It always reports native completion
|
||||
false and execution prohibited. Fixtures and local journal fingerprints do not
|
||||
provide authenticated owner/native evidence. This records completed source scope
|
||||
previously described under T06; it does not duplicate native acceptance credit.
|
||||
|
||||
## T11 — Validate the optional machine-descriptor pin contract
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T11
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ef2a0119-f59e-5e45-9519-115da9df7b85"
|
||||
```
|
||||
|
||||
Completed 2026-10-16. Reproduced the JSON schema rejecting a valid
|
||||
`execution_descriptors` map while the Python validator accepted malformed maps.
|
||||
The schema now supports the existing Warden optional exact-byte pin map; semantic
|
||||
validation binds keys to declared steps, bounds the map and checks digest shape.
|
||||
The direct evidence API refuses malformed maps before accessing them. Twenty-one
|
||||
new checks and all 127 affected source checks passed. No current type/case bytes,
|
||||
status, owner descriptors or native acceptance changed. Source evidence and
|
||||
publication receipt: Custodian
|
||||
`docs/changes/netkingdom-target-2026-10-16/browser-acceptance-next/`.
|
||||
|
||||
## T12 — Prepare NetKingdom-owned attended step adapters
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T12
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd"
|
||||
```
|
||||
|
||||
Owner: NetKingdom. Retained scope: prepare bounded attended Deployment env,
|
||||
config verification and restart/readiness steps around the existing helpers.
|
||||
Next action: identify exact current entry points and their revision/readback
|
||||
contracts, then implement fixed source-pinned callbacks or receipt-only attended
|
||||
steps. Completion requires positive/refusal checks, exact target/source pins and
|
||||
uncertainty behavior without acquiring user-engine runtime identity or exposing
|
||||
registry/client secrets. Existing caller/session, cluster/issuer, CAS/readback
|
||||
and status-only output boundaries remain mandatory.
|
||||
|
||||
2026-10-16 task refinement: the original broad adapter scope is split into T13
|
||||
(completed registration source candidate), T14 (group creation provenance), T15
|
||||
(durable registration recovery) and T16 (descriptor/artifact selection and owner
|
||||
review). This preserves T12's task ID and Hub UUID while giving each remaining
|
||||
outcome an owner and completion gate. T06 now names every source dependency;
|
||||
its native/reference lifecycle acceptance scope remains unchanged. Completion
|
||||
of any source task supplies no Warden, Platform or Apps assent and no native
|
||||
execution authority.
|
||||
|
||||
## T13 — Prepare the fixed registration adapter source candidate
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T13
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b264c10b-565c-5d29-b788-03cf5fbd2883"
|
||||
```
|
||||
|
||||
Completed 2026-10-16, source implementation and synthetic fixture proof only.
|
||||
Added an injection-only adapter around actual `keycape-client.run`: fixed
|
||||
case/principal/human/lane/source/acceptance binding, existing CAS/readback,
|
||||
metadata-only owner evidence and per-attempt parent-session revocation boundary.
|
||||
The descriptor source composition pins the exact checker executable digest
|
||||
alongside adapter/helper source. Creation-only rollback authenticates its
|
||||
creation receipt and refuses changed postcreation UID/resourceVersion;
|
||||
pre-existing clients confer no rollback authority. Missing readback, evidence,
|
||||
revocation or process-local creation provenance cannot become retry-safe success.
|
||||
|
||||
Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/`
|
||||
R3 candidate, explicit patch and postimages, retained R1 rejected custody finding
|
||||
and R2 revision, and 36 passing source checks (16 new, 20 existing). Three checks
|
||||
use actual Warden kernel admission/dispatch/rollback with published Warden
|
||||
`d97652a3bb5fa733b063d71a1b74068efcccf572` producer pins unchanged. Owner trust,
|
||||
descriptor/type/case and Kubernetes objects remain synthetic fixtures. This
|
||||
records a completed source deliverable, not independent review, publication,
|
||||
type acknowledgment, exercised procedure, identity admission or native browser
|
||||
acceptance. Independent root source review: Custodian browser-adapter-next/coordinator-review-r3.json. Source integration and normal publication/synchronization receipts: Custodian browser-adapter-next/canonical-integration.json. These receipts supply no other-owner assent or native authority.
|
||||
|
||||
Source publication: `0775c2bfa41af236301155bb5a69bbf088bdf823`, independently
|
||||
reviewed by the Custodian coordinator before integration. The required initial
|
||||
projection and ordinary UUID writeback both completed with zero failures and
|
||||
zero automation errors. Generated-only brief commits were refused by the normal
|
||||
hook; exact pre-existing staged/working brief bytes were restored. The separate
|
||||
canonical integration receipt retains those refusals and projection evidence.
|
||||
Native admission and all other-owner acceptance remain open.
|
||||
|
||||
## T14 — Preserve created-only provenance for group ensure and revoke
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T14
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "fce95440-a25b-5f27-8ef3-a3d182099113"
|
||||
```
|
||||
|
||||
Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner
|
||||
ensure/revoke contract with independently retained prestate and exact newly
|
||||
created groups/memberships before exposing reversible owner callbacks. Current
|
||||
ensure returns no membership-creation inventory; current revoke may remove a
|
||||
membership that predated this case. Completion requires actual helper-backed
|
||||
positive/refusal fixtures, all validation before mutation, per-attempt evidence
|
||||
and revocation, readback, ambiguity handling, and rollback that preserves every
|
||||
pre-existing/shared group and membership. No existing endpoint or service token
|
||||
is treated as new authorization.
|
||||
|
||||
## T15 — Define authenticated recovery for lost registration process state
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T15
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "1b825705-4848-5edc-832f-b1622cd9bdb7"
|
||||
```
|
||||
|
||||
Owner: NetKingdom for registration state, with Warden owning kernel evidence
|
||||
admission. Next action: select the exact existing owner evidence holder/readback
|
||||
contract and define authenticated lookup for the original creation attempt.
|
||||
Completion requires restart/lost-receipt fixtures that bind case/source/principal,
|
||||
original created-only provenance and current native revision before any undo;
|
||||
uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses
|
||||
post-restart rollback and installs no reconciliation callback. A new broker or
|
||||
unverified JSON receipt is not a recovery contract. Actual evidence-holder
|
||||
acceptance and attended recovery remain separately authorized native work.
|
||||
|
||||
## T16 — Select and review the actual machine descriptor composition
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T16
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "200bf500-5084-5ae7-be26-3033fe1f296e"
|
||||
```
|
||||
|
||||
Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden
|
||||
independently owns global provenance/admission. Next action: obtain review of the
|
||||
exact checker source/artifact proposed by T17 and identify native owner evidence/
|
||||
session callback contracts, then assemble the complete step descriptor set and
|
||||
a proposed explicit type version selection. T13's historical descriptor retains
|
||||
its synthetic checker; T17 supplies an artifact-bound proposal, not admission.
|
||||
Completion requires separate owner source review of exact machine descriptors,
|
||||
artifact/source/config pins, normal type/version admission and independent
|
||||
evidence-verification boundaries. Warden d97652a refreshes producer validator
|
||||
pins for NetKingdom 015694ee; it does not attest T13's new adapter. Pinning any
|
||||
machine descriptor into an acknowledged type is a separately reviewed source
|
||||
change. Platform owns structural/mint/broker/delivery evidence; Apps owns actual
|
||||
consumer login/group enforcement. Neither their assent nor native acceptance is
|
||||
supplied by this task.
|
||||
|
||||
## T17 — Build and exercise the exact offline config-checker artifact
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T17
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8c3e4baa-a218-5996-af9e-fe1153a46dac"
|
||||
```
|
||||
|
||||
Completed 2026-10-16, isolated artifact/source fixture proof only. Built the
|
||||
existing `key-cape/src/cmd/keycape` command from published clean source revision
|
||||
`3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`, source tree
|
||||
`f0010b9cab17dbc3a787628a29657d98b900320f`, with cached Go 1.23.0,
|
||||
CGO disabled, Linux/amd64 v1, trimmed paths, disabled VCS stamping and offline
|
||||
read-only module selection. The full retained closure pins 108 source files,
|
||||
9876 toolchain files and ten used module source directories. Three isolated
|
||||
build locations produced identical 13633409-byte artifacts:
|
||||
`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`.
|
||||
|
||||
Eight disposable checks use the actual executable and maintained helper/kernel,
|
||||
with complete synthetic configs and a minimal environment: add/created-only
|
||||
removal, normal pinned producer admission, rollback, lost-readback uncertainty,
|
||||
stale revisions, substituted checker refusal, invalid startup config and missing
|
||||
secret-env refusal. No checker stub or producer-pin override remains. The
|
||||
artifact-bound descriptor proposal is
|
||||
`sha256:76b1153d188062ece53fcd5e8d35290fdbfd02a69dd79d4b001fc42781afcdc3`.
|
||||
Evidence: Custodian
|
||||
`docs/changes/netkingdom-target-2026-10-16/browser-checker-artifact-next/`.
|
||||
The first two build attempts succeeded but their closure collection failed;
|
||||
both failures are retained, as is refusal of the older incomplete synthetic
|
||||
fixture. No production config, credentials or cluster mutation was used.
|
||||
|
||||
This extracts completed build/fixture scope from T16 without claiming KeyCape
|
||||
owner endorsement, native checker selection/storage custody, acknowledged type
|
||||
integration, native evidence/session callbacks or attended browser acceptance.
|
||||
T16 retains exact owner review and composition/admission scope. The attended
|
||||
registration procedure remains unexercised.
|
||||
|
||||
## T18 — Make checker input and executable selection deterministic
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T18
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "9c45da71-fb4c-5f20-9065-90f448f37c64"
|
||||
```
|
||||
|
||||
Completed 2026-10-16, isolated source correction and real-artifact fixture proof
|
||||
only; independent integration/publication remain separately recorded. KeyCape
|
||||
source review identified non-secret `KEYCAPE_ACCOUNT_PORTAL_URL` and
|
||||
`KEYCAPE_BROWSER_LOGOUT_URL` overrides in config.Load. The original helper
|
||||
inherited ambient environment, allowing valid supplied configuration to be
|
||||
rejected or invalid supplied URL configuration to be admitted. A bare relative
|
||||
checker filename could also be replaced by PATH lookup after the adapter had
|
||||
hashed the selected local file. All three counterexamples are retained against
|
||||
the unchanged predecessor.
|
||||
|
||||
The existing helper now resolves the selected checker file to an absolute path
|
||||
and passes an explicit empty subprocess environment. Three opt-in real-checker
|
||||
regressions verify both URL-verdict directions and relative-file selection under
|
||||
a non-checker PATH decoy. All 39 affected checks pass with the exact supplied
|
||||
artifact, as do 11 real-artifact helper/kernel scenarios. Source evidence:
|
||||
Custodian `docs/changes/netkingdom-target-2026-10-16/browser-checker-environment-r2/`.
|
||||
Historical descriptors remain retained; the helper correction changes successor
|
||||
source-composition pins and requires their separate review/admission. An absolute
|
||||
path is not immutable custody and does not eliminate file replacement races;
|
||||
T16 retains native executable storage/custody and callback/type admission.
|
||||
No production configuration, credentials or cluster mutation was used.
|
||||
|
||||
Source integration and publication: `916ba4b33e03179274d03b78bd898a4843e8653f`, following independent
|
||||
Custodian source review in browser-checker-environment-r2/coordinator-review.json.
|
||||
The ordinary protected projection pass assigned T17/T18 UUIDs with zero failures
|
||||
and zero automation errors; generated-only brief commit refusal is retained.
|
||||
The exact prior staged/working brief bytes were restored. Final source/publication/
|
||||
projection evidence is retained in Custodian browser-checker-environment-r2/
|
||||
canonical-integration.json. These source receipts do not admit native executable
|
||||
custody, owner callbacks, acknowledged type changes or browser identities.
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
--- a/workplans/NK-WP-0048-security-case-types.md
|
||||
+++ b/workplans/NK-WP-0048-security-case-types.md
|
||||
@@ -748,6 +748,14 @@
|
||||
consumer login/group enforcement. Neither their assent nor native acceptance is
|
||||
supplied by this task.
|
||||
|
||||
+2026-10-16 source checkpoint: T19 below extracts the completed per-attempt
|
||||
+executable snapshot implementation and reviewed fixture proof. T16 retains
|
||||
+native executable storage/custody, exact owner evidence/session callbacks,
|
||||
+complete descriptor composition and ordinary type/version admission. The
|
||||
+successor descriptor/source pins are proposed only; the earlier descriptor
|
||||
+history is preserved. T14 group provenance and T15 durable restart/recovery
|
||||
+remain independent open requirements. No native acceptance is counted here.
|
||||
+
|
||||
## T17 — Build and exercise the exact offline config-checker artifact
|
||||
|
||||
```task
|
||||
@@ -825,3 +833,39 @@
|
||||
projection evidence is retained in Custodian browser-checker-environment-r2/
|
||||
canonical-integration.json. These source receipts do not admit native executable
|
||||
custody, owner callbacks, acknowledged type changes or browser identities.
|
||||
+
|
||||
+## T19 — Bind checker execution to verified immutable attempt bytes
|
||||
+
|
||||
+```task
|
||||
+id: NK-WP-0048-T19
|
||||
+status: done
|
||||
+priority: high
|
||||
+```
|
||||
+
|
||||
+Completed 2026-10-16, independently reviewed source implementation and synthetic
|
||||
+fixture proof only. The registration adapter now opens a bounded regular checker
|
||||
+artifact without following its terminal symlink, verifies the reviewed digest,
|
||||
+and snapshots the exact bytes into a write/grow/shrink/seal-removal-sealed Linux
|
||||
+memfd before helper access. Live and proposed config checks execute that same fd
|
||||
+through procfs with the existing empty environment; the fd closes on completion
|
||||
+or failure. Unsupported snapshot/seal/exec policy refuses through the existing
|
||||
+parent-session revocation and uncertainty boundaries. The earlier unbounded
|
||||
+pre-snapshot path read was removed during independent review; actual adapter
|
||||
+symlink/FIFO/oversize refusal tests verify helper access never starts.
|
||||
+
|
||||
+Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-resume-r1/`,
|
||||
+including exact base/preimages/postimages/patch, proposed descriptor, coordinator
|
||||
+source review and canonical integration receipt. Independent Custodian checks
|
||||
+passed 49 affected source tests and 12 actual Go checker/helper/kernel scenarios;
|
||||
+the sets overlap and are not separate acceptance counts. Existing published
|
||||
+Warden d97652a producer pins remain unchanged. The exact KeyCape artifact remains
|
||||
+`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`.
|
||||
+The predecessor path-replacement refusal and the first expanded reopened-fd test
|
||||
+failure are retained; the latter fixture was corrected to restore write mode
|
||||
+before independently proving seals block reopened-fd writes and shared mmap.
|
||||
+
|
||||
+This source task adds no native owner custody, callback, evidence trust, type
|
||||
+acknowledgment, credential access, consumer sign-in or rollback acceptance.
|
||||
+Executable memfds, Linux seals and usable procfs are explicit runtime dependencies
|
||||
+and still require native admission. This is an extracted source outcome from
|
||||
+T16, not completion of T16; T12/T14/T15 and all external-owner gates remain open.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"session_id": 77536,
|
||||
"exit_code": 0,
|
||||
"source_revision": "3291a660ae5170d368bdc325b87d9bd5c551e7ff",
|
||||
"summary": {
|
||||
"fail": 0,
|
||||
"automation_error": 0,
|
||||
"warn": 7,
|
||||
"info": 4
|
||||
},
|
||||
"bindings": "89/89",
|
||||
"pushed": true,
|
||||
"generated_only_commit_refused": true,
|
||||
"unrelated_staging_preserved": true,
|
||||
"native_acceptance": false
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"commit": "bc86ef1cf45aae9e6d534de70ad44ba8b617bc69",
|
||||
"published": true,
|
||||
"verified_at": "2026-10-16T15:40:45.359016+00:00",
|
||||
"command": "statehub fix-consistency --repo the-custodian --no-writeback --json",
|
||||
"bindings_synced": 89,
|
||||
"bindings_total": 89,
|
||||
"fail": 0,
|
||||
"automation_error": 0,
|
||||
"warnings": 7,
|
||||
"info": 4,
|
||||
"task_description_synced": "CUST-WP-0080-T06",
|
||||
"retained_caveat": "Generated-only working-memory commit refused; unrelated existing changes preserved. Deferred CUST73T05 rotation remains deferred despite generic C40 warning.",
|
||||
"native_gates_open": 16,
|
||||
"progress_id": "23a4bd92-3406-4c39-9267-a9bc9a1503c5"
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
{
|
||||
"status": "candidate_read_only_allocator_preview_not_admission",
|
||||
"assignments": [
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "RCLUSTER-WP-ADHOC-2026-07-27-T01",
|
||||
"uuid": "3e3f39d3-5070-5ba3-97db-19f5f361f264",
|
||||
"path": "workplans/ADHOC-2026-07-27.md"
|
||||
},
|
||||
{
|
||||
"kind": "workplan",
|
||||
"record_id": "RCLUSTER-WP-0018",
|
||||
"uuid": "e0a7813d-c916-5e49-84b2-5e5b30441891",
|
||||
"path": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "RCLUSTER-WP-0018-T01",
|
||||
"uuid": "7280193d-0d59-5b78-a2d3-96ebd127127f",
|
||||
"path": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "RCLUSTER-WP-0018-T02",
|
||||
"uuid": "90836435-917f-504c-a711-3f29ece348e3",
|
||||
"path": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "RCLUSTER-WP-0018-T03",
|
||||
"uuid": "5dfa9a3a-2639-5cf6-b0a4-13eef417d47a",
|
||||
"path": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "RCLUSTER-WP-0018-T04",
|
||||
"uuid": "021b07bb-65bc-5743-bac5-4294b4be74f6",
|
||||
"path": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "RCLUSTER-WP-0018-T05",
|
||||
"uuid": "5bc67a22-0991-5625-86a6-f749560d8a0e",
|
||||
"path": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
}
|
||||
],
|
||||
"files_changed": [
|
||||
"workplans/ADHOC-2026-07-27.md",
|
||||
"workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md"
|
||||
],
|
||||
"historical_invalid_identifiers": [
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T01",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-1"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T02",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-2"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T03",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-3"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T04",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-4"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T05",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-5"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T01",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-1"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T02",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-2"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T03",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-3"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T04",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-4"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T01",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-1"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T02",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-2"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T03",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-3"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T04",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-4"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T05",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-5"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T06",
|
||||
"source": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-6"
|
||||
}
|
||||
],
|
||||
"historical_identity_collisions": [
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T01",
|
||||
"uuid": null,
|
||||
"sources": [
|
||||
"workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-1",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-1",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-1"
|
||||
],
|
||||
"parent_id": "RAIL-BS-WP-0002",
|
||||
"uuids": [
|
||||
"3f042630-eab0-4c6a-9167-e2b28ff20e40",
|
||||
"4526a842-ea31-4874-9231-92ab556cfe7b",
|
||||
"6841c93a-f146-47eb-9f7c-8fa0e02c1bbc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T02",
|
||||
"uuid": null,
|
||||
"sources": [
|
||||
"workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-2",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-2",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-2"
|
||||
],
|
||||
"parent_id": "RAIL-BS-WP-0002",
|
||||
"uuids": [
|
||||
"a6313e06-1976-46a7-8e31-df4eb2eca880",
|
||||
"ab166073-30a7-4702-a037-4091e8706e20",
|
||||
"e8510646-46ed-4697-a345-f3d3009eea78"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T03",
|
||||
"uuid": null,
|
||||
"sources": [
|
||||
"workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-3",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-3",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-3"
|
||||
],
|
||||
"parent_id": "RAIL-BS-WP-0002",
|
||||
"uuids": [
|
||||
"05d42a55-921f-4aa7-bb76-e8af9c7e0ac3",
|
||||
"140da396-8e30-4f4d-b88c-c42c0cd46c01",
|
||||
"dab2c07f-8aa0-4635-8df6-857e87e93fc5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T04",
|
||||
"uuid": null,
|
||||
"sources": [
|
||||
"workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-4",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md#task-block-4",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-4"
|
||||
],
|
||||
"parent_id": "RAIL-BS-WP-0002",
|
||||
"uuids": [
|
||||
"08233868-d522-4117-bc4e-6c0f52545665",
|
||||
"5c3d40e4-239b-488e-9519-6f7a38d2325f",
|
||||
"d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"id": "T05",
|
||||
"uuid": null,
|
||||
"sources": [
|
||||
"workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md#task-block-5",
|
||||
"workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md#task-block-5"
|
||||
],
|
||||
"parent_id": "RAIL-BS-WP-0002",
|
||||
"uuids": [
|
||||
"2d5acff7-4a4e-4ddd-ad06-08237ad3dac8",
|
||||
"7f9e0e58-a130-467a-a2d0-b3f2564e496f"
|
||||
]
|
||||
}
|
||||
],
|
||||
"strict_scan_ok": false,
|
||||
"all_source_bytes_preserved": true
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"schema": "coordination-derived-cases.v1",
|
||||
"status": "local_observed_waiting_not_hub_published",
|
||||
"worker_injection": false,
|
||||
"cases": [
|
||||
{
|
||||
"scenario_type": "project_driver",
|
||||
"case_id": "cluster-frozen-receiver-before-registrar-r1",
|
||||
"driver_repo": "prj-state-hub-retirement",
|
||||
"driver_workplan_id": "SHR-WP-0001",
|
||||
"beneficiary_workplan_id": "RMGR-WP-0019",
|
||||
"beneficiary_task_id": "RMGR-WP-0019-T05",
|
||||
"obligor_workplan_id": "STATE-WP-0095",
|
||||
"obligor_task_id": "STATE-WP-0095-T06",
|
||||
"flavor": "implementation",
|
||||
"state": "waiting",
|
||||
"reason": "receiver-first reviewed rollout is a human gate; no worker wake",
|
||||
"indirect_beneficiaries": [
|
||||
"RCLUSTER-WP-0018",
|
||||
"NK-WP-0049-T35"
|
||||
],
|
||||
"next_action": "independent source review, canonical integration and exact CI image proposal before scoped rollout review"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"schema": "cluster-historical-receiver-deployment-metadata.v1",
|
||||
"status": "metadata_observed",
|
||||
"mutations": false,
|
||||
"target_node_ip_confirmed": "92.205.62.239",
|
||||
"deployments": [
|
||||
{
|
||||
"name": "state-hub",
|
||||
"generation": 76,
|
||||
"observed_generation": 76,
|
||||
"ready_replicas": 1,
|
||||
"images": [
|
||||
"forgejo.coulomb.social/coulomb/state-hub:main-7d99282"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "state-hub-mcp",
|
||||
"generation": 48,
|
||||
"observed_generation": 48,
|
||||
"ready_replicas": 1,
|
||||
"images": [
|
||||
"forgejo.coulomb.social/coulomb/state-hub:main-7d99282"
|
||||
]
|
||||
}
|
||||
],
|
||||
"helm_history": [
|
||||
{
|
||||
"revision": 73,
|
||||
"status": "superseded",
|
||||
"chart": "state-hub-0.1.0",
|
||||
"app_version": "main-5dd3df9",
|
||||
"updated": "2026-10-04T15:39:41.98613624+02:00"
|
||||
},
|
||||
{
|
||||
"revision": 74,
|
||||
"status": "superseded",
|
||||
"chart": "state-hub-0.1.0",
|
||||
"app_version": "main-5dd3df9",
|
||||
"updated": "2026-10-06T10:27:00.194272456+02:00"
|
||||
},
|
||||
{
|
||||
"revision": 75,
|
||||
"status": "deployed",
|
||||
"chart": "state-hub-0.1.0",
|
||||
"app_version": "main-5dd3df9",
|
||||
"updated": "2026-10-06T12:02:21.111126728+02:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"status": "preview_only_apply_via_normal_allocator_after_receiving_acceptance",
|
||||
"path": "workplans/ADHOC-2026-07-27.md",
|
||||
"record_id": "RCLUSTER-WP-ADHOC-2026-07-27-T01",
|
||||
"uuid": "3e3f39d3-5070-5ba3-97db-19f5f361f264",
|
||||
"before_sha256": "f480e729a820367109d0b58cbbf367f6ac4137408f5caadbc2708fa181a80c67",
|
||||
"expected_after_sha256": "6d3b25ded22ae68835a2a2c97d517194b06fedf98a29dbcc252f99505fbe0ace",
|
||||
"added_lines": 1,
|
||||
"scope": "one existing task UUID field only; no ID rename or content changes"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
--- a/workplans/ADHOC-2026-07-27.md
|
||||
+++ b/workplans/ADHOC-2026-07-27.md
|
||||
@@ -20,6 +20,7 @@
|
||||
id: RCLUSTER-WP-ADHOC-2026-07-27-T01
|
||||
status: done
|
||||
priority: high
|
||||
+state_hub_task_id: "3e3f39d3-5070-5ba3-97db-19f5f361f264"
|
||||
```
|
||||
|
||||
Enable only `kubernetes.podspec-init-containers` in Knative
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Disposable inbox migration compatibility candidate
|
||||
|
||||
Prepared, not executed or approved. This is a bounded source follow-up to the
|
||||
State Hub receiver rollout proposal; it does not authorize deploying it.
|
||||
|
||||
The offline builder used the exact deployed7d99282 ORM source for the two inbox
|
||||
tables, compiled their real PostgreSQL DDL and old-source SQLAlchemy DML, then
|
||||
invoked the actual c4d5e6f7a8b9 migration through Alembic offline Operations.
|
||||
It imported no application config, database module or pytest conftest and opened
|
||||
no database/network connection. Rebuilding requires PYTHONHASHSEED=0 to
|
||||
provide a repeatable Python process baseline. SQLAlchemy also orders independent
|
||||
FK/index objects by identity, so the builder canonicalizes only independent FK
|
||||
clause order and emits sorted indexes after dependency-ordered compiled tables.
|
||||
Two failed repeat-byte checks are retained before that normalization fix. Source and generated SQL hashes are bound by
|
||||
manifest.json. No dependency install or workflow change was introduced.
|
||||
|
||||
The proposed driver reuses the frozen current group fixture utility source and
|
||||
canonical isolated-keycape containment. It pins the same cached PostgreSQL and
|
||||
UserEngine helper images, fixed daemon ID/name/socket, nonroot users, readonly
|
||||
rootfs, dropped capabilities, no privilege gain, none/shared loopback network,
|
||||
no host ports/mounts/retained volumes, private bounded tmpfs, no logs/restart,
|
||||
finite lifetimes/deadline and exact-name/label cleanup. PostgreSQL declared
|
||||
volumes are explicitly masked. Generated password is private stdin/tmpfs only.
|
||||
The helper must be exact /usr/local/bin/python3 with psycopg3.3.6. No image
|
||||
pull/build/push, package download, real credential or production API is allowed.
|
||||
|
||||
The phase would create only the old inbox table schema, apply exact generated
|
||||
migration SQL, then submit old compiled insert/receipt/update/delete shapes.
|
||||
It checks revision backfill/write triggers, transaction-shared revisions, no-op
|
||||
stability and tombstones while leaving the additive schema in place. Receipt
|
||||
contains closed booleans, synthetic timing, safe error class/SQLSTATE and exact
|
||||
session/cleanup metadata; no raw Docker/PostgreSQL output. The driver defaults
|
||||
to plan and has not been invoked with its execution flag. Root must review the
|
||||
exact manifest and code before any disposable Docker call.
|
||||
|
||||
This is a narrow SQL compatibility fixture. It does not prove complete old/new
|
||||
HTTP/ORM behavior, concurrent production feed behavior, real schema state,
|
||||
large-data lock/runtime, service ownership or backup/PITR/recovery authority.
|
||||
The existing full feed tests must only run inside separately reviewed exact
|
||||
disposable database containment, never their default destructive local URL.
|
||||
Helm rollback leaves the migration installed; no automatic schema downgrade is
|
||||
proposed. A hard parent-loss absence/reaper proof is outside this fixture; normal
|
||||
success or failure must retain exact cleanup receipts before claiming absence.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate SQL from exact old ORM and new migration; no config/DB connection."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
from sqlalchemy import create_mock_engine, insert, update, delete, func
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
HERE=Path(__file__).resolve().parent
|
||||
|
||||
def load(name,path):
|
||||
spec=importlib.util.spec_from_file_location(name,path);mod=importlib.util.module_from_spec(spec)
|
||||
sys.modules[name]=mod;spec.loader.exec_module(mod);return mod
|
||||
|
||||
def build():
|
||||
if os.environ.get("PYTHONHASHSEED") != "0":
|
||||
raise RuntimeError("offline_builder_requires_fixed_python_hash_seed_0")
|
||||
# Isolated source namespace: never import api.config/database or conftest.
|
||||
for name in ('api','api.models'):
|
||||
module=types.ModuleType(name);module.__path__=[];sys.modules[name]=module
|
||||
oldbase=load('api.models.base',HERE/'old-base.py')
|
||||
old=load('api.models.agent_message',HERE/'old-agent-message.py')
|
||||
ddl=[]
|
||||
engine=create_mock_engine('postgresql://',lambda sql,*args,**kwargs:ddl.append(str(sql.compile(dialect=engine.dialect))))
|
||||
oldbase.Base.metadata.create_all(engine)
|
||||
migration=load('reviewed_inbox_migration',HERE/'migration.py')
|
||||
output=io.StringIO()
|
||||
migration.op=Operations(MigrationContext.configure(dialect_name='postgresql',opts={'as_sql':True,'output_buffer':output}))
|
||||
migration.upgrade()
|
||||
# SQLAlchemy orders otherwise equivalent FK objects/index sets by identity.
|
||||
# Normalize only independent FK clause order and place sorted indexes after
|
||||
# the original dependency-ordered tables; every clause remains compiled ORM SQL.
|
||||
tables=[];indexes=[]
|
||||
for statement in ddl:
|
||||
lines=statement.splitlines()
|
||||
positions=[i for i,line in enumerate(lines) if line.lstrip().startswith('FOREIGN KEY(')]
|
||||
values=sorted(lines[i].rstrip().rstrip(',') for i in positions)
|
||||
for i,value in zip(positions,values):
|
||||
ending=', ' if lines[i].endswith(', ') else ',' if lines[i].endswith(',') else ''
|
||||
lines[i]=value+ending
|
||||
normalized='\n'.join(lines)
|
||||
(tables if statement.lstrip().startswith('CREATE TABLE') else indexes).append(normalized)
|
||||
(HERE/'old-schema.sql').write_text(';\n'.join(tables+sorted(indexes))+';\n')
|
||||
(HERE/'upgrade.sql').write_text(output.getvalue())
|
||||
# Compile real old-source SQLAlchemy write shapes; values are synthetic.
|
||||
msg='11111111-1111-4111-8111-111111111111'
|
||||
statements={
|
||||
'insert_message':insert(old.AgentMessage.__table__).values(id=msg,from_agent='fixture-sender',to_agent='fixture-reader',subject='synthetic',body='synthetic',kind='message'),
|
||||
'insert_receipt':insert(old.MessageReceipt.__table__).values(message_id=msg,agent='fixture-reader',delivered_at=func.now()),
|
||||
'update_message':update(old.AgentMessage.__table__).where(old.AgentMessage.id==msg).values(read_at=func.now()),
|
||||
'delete_message':delete(old.AgentMessage.__table__).where(old.AgentMessage.id==msg),
|
||||
}
|
||||
writes={}
|
||||
for name,statement in statements.items():
|
||||
compiled=statement.compile(dialect=postgresql.dialect())
|
||||
writes[name]={'sql':str(compiled),'params':compiled.params}
|
||||
(HERE/'old-writes.json').write_text(json.dumps(writes,indent=2)+'\n')
|
||||
return {name:'sha256:'+hashlib.sha256((HERE/name).read_bytes()).hexdigest() for name in ['old-schema.sql','upgrade.sql','old-writes.json']}
|
||||
if __name__=='__main__':print(json.dumps(build(),indent=2))
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
"""Closed public fixture diagnostics. Never serialize exception text or SQL values."""
|
||||
import json
|
||||
import re
|
||||
|
||||
DRIVER_STAGES = ('session_registered', 'anchor_launch', 'anchor_containment', 'postgres_private_import',
|
||||
'postgres_initdb', 'postgres_launch', 'anchor_identity', 'helper_launch', 'full_containment',
|
||||
'helper_interpreter', 'candidate_import', 'bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack',
|
||||
'restored', 'database_dump', 'database_restore', 'cleanup', 'complete')
|
||||
PROBE_STAGES = ('probe_entry', 'loopback_check', 'phase_dispatch', 'bootstrap_connect_wait',
|
||||
'bootstrap_connected', 'bootstrap_schema', 'bootstrap_migration', 'bootstrap_epoch_insert',
|
||||
'bootstrap_role_create', 'bootstrap_runtime_grants', 'bootstrap_checkpoint_init',
|
||||
'bootstrap_directory_init', 'bootstrap_ensure', 'bootstrap_ensure_readback',
|
||||
'bootstrap_runtime_ddl_refusal', 'bootstrap_complete', 'phase_complete')
|
||||
ERROR_CLASSES = ('GroupProvenanceUnavailable', 'RuntimeError', 'ValueError', 'TypeError', 'KeyError',
|
||||
'FileNotFoundError', 'PermissionError', 'OperationalError', 'ProgrammingError', 'IntegrityError',
|
||||
'InsufficientPrivilege', 'InvalidPassword', 'UndefinedTable', 'UndefinedColumn', 'SyntaxError',
|
||||
'DuplicateTable', 'DuplicateObject', 'InvalidSchemaName', 'DeadlockDetected', 'ConnectionException',
|
||||
'InvalidAuthorizationSpecification', 'OSError', 'TimeoutExpired', 'ImportError',
|
||||
'ModuleNotFoundError', 'JSONDecodeError', 'OtherError')
|
||||
|
||||
|
||||
def error_metadata(error, stage):
|
||||
if stage not in DRIVER_STAGES + PROBE_STAGES:
|
||||
stage = 'phase_dispatch'
|
||||
name = type(error).__name__
|
||||
name = name if name in ERROR_CLASSES else 'OtherError'
|
||||
state = None
|
||||
current = error
|
||||
for _ in range(8):
|
||||
if current is None:
|
||||
break
|
||||
try:
|
||||
candidate = getattr(current, 'sqlstate', None)
|
||||
if isinstance(candidate, str) and re.fullmatch('[0-9A-Z]{5}', candidate):
|
||||
state = candidate
|
||||
break
|
||||
current = current.__cause__ or current.__context__
|
||||
except Exception:
|
||||
break
|
||||
return {'stage': stage, 'exception_class': name, 'sqlstate': state}
|
||||
|
||||
|
||||
def emit_stage(stage):
|
||||
if stage not in PROBE_STAGES:
|
||||
raise RuntimeError('invalid_public_fixture_stage')
|
||||
print(json.dumps({'event': 'stage', 'stage': stage}), flush=True)
|
||||
|
||||
|
||||
def parse_frames(raw, *, require_final=True):
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode('utf-8')
|
||||
if not isinstance(raw, str) or len(raw) > 32768:
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
stages, final = [], None
|
||||
lines = raw.splitlines()
|
||||
if len(lines) > 96:
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
for line in lines:
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict) or final is not None:
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
if value.get('event') == 'stage':
|
||||
if set(value) != {'event', 'stage'} or value['stage'] not in PROBE_STAGES:
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
stages.append(value['stage'])
|
||||
elif value.get('status') in {'passed', 'failed'} and final is None:
|
||||
if value['status'] == 'failed':
|
||||
if set(value) != {'status', 'failure', 'diagnostic'} or value['failure'] != 'synthetic_pg_oracle_failed':
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
diagnostic = value['diagnostic']
|
||||
if set(diagnostic) != {'stage', 'exception_class', 'sqlstate'} or diagnostic['stage'] not in PROBE_STAGES or diagnostic['exception_class'] not in ERROR_CLASSES:
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
if diagnostic['sqlstate'] is not None and not re.fullmatch('[0-9A-Z]{5}', str(diagnostic['sqlstate'])):
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
elif (set(value) != {'status', 'checks', 'psycopg_version', 'active_database'}
|
||||
or not isinstance(value['checks'], dict)
|
||||
or not isinstance(value['psycopg_version'], str)
|
||||
or not re.fullmatch(r'3\.[0-9]+\.[0-9]+(?:[a-z]+[0-9]+)?', value['psycopg_version'])
|
||||
or value['active_database'] not in {'postgres', 'fixture_fork'}):
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
final = value
|
||||
else:
|
||||
raise RuntimeError('invalid_public_fixture_diagnostics')
|
||||
if require_final and final is None:
|
||||
raise RuntimeError('missing_public_fixture_result')
|
||||
return stages, final
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Default plan only; exact independent review required before disposable Docker."""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
HERE=Path(__file__).resolve().parent
|
||||
MANIFEST=HERE/'manifest.json'
|
||||
spec=importlib.util.spec_from_file_location('reviewed_pg_containment_driver',HERE/'reviewed-driver-base.py')
|
||||
base=importlib.util.module_from_spec(spec);spec.loader.exec_module(base)
|
||||
require=base.require
|
||||
CHECKS={'only_loopback_no_external_routes','actual_old_model_schema_created','actual_new_migration_sql_applied','legacy_rows_backfilled_one_revision',
|
||||
'old_compiled_insert_accepted_on_new_schema','old_compiled_receipt_same_transaction_revision',
|
||||
'old_compiled_update_stamped_revision','old_noop_update_revision_unchanged',
|
||||
'old_compiled_delete_writes_tombstone','no_application_schema_downgrade'}
|
||||
IMPORT="""
|
||||
import base64,json,os,sys
|
||||
from pathlib import Path
|
||||
os.umask(0o077)
|
||||
root=Path('/fixture')
|
||||
allowed={'probe.py','old-schema.sql','upgrade.sql','old-writes.json','private.json'}
|
||||
files=json.load(sys.stdin)
|
||||
if set(files)!=allowed:raise ValueError()
|
||||
for name,encoded in files.items():
|
||||
fd=os.open(root/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
with os.fdopen(fd,'wb') as target:target.write(base64.b64decode(encoded,validate=True))
|
||||
"""
|
||||
|
||||
def execute(manifest,adapter,result,checkpoint):
|
||||
def publish():
|
||||
try:checkpoint()
|
||||
except OSError:result['receipt_checkpoint_failed']=True
|
||||
session=base.session_plan(manifest,uuid.uuid4().hex)
|
||||
result['session']={'run_id':session['run_id'],'containers':session['containers'],'label':'net-kingdom.recovery-run='+session['run_id']}
|
||||
original=adapter.command;deadline=time.monotonic()+180
|
||||
def run(argv,**kwargs):
|
||||
remaining=deadline-time.monotonic();require(remaining>0)
|
||||
try:return original(argv,timeout=min(40,remaining),**kwargs)
|
||||
except BaseException:
|
||||
result['daemon_request_uncertain']=True;publish();raise
|
||||
def stage(name):result['stage']=name;publish()
|
||||
adapter.command=run
|
||||
try:
|
||||
anchor=session['worker'];helper=session['issuer'];python=base.helper_python(manifest)
|
||||
stage('postgres_launch')
|
||||
args=base.base_args(session,anchor,'none',user='999:999')+[
|
||||
'--tmpfs','/var/lib/postgresql:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700',
|
||||
'--tmpfs','/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=134217728,uid=999,gid=999,mode=0700',
|
||||
'--tmpfs','/run/fixture:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700',
|
||||
'--env','PGDATA=/var/lib/postgresql/data','--entrypoint','/bin/sh',manifest['postgres_image'],'-c','sleep 240']
|
||||
run(args);adapter.inspect_session({**session,'containers':[anchor],'issuer':anchor},worker_only=True)
|
||||
password=secrets.token_hex(24)
|
||||
run(['docker','exec','-i',anchor,'tar','-xf','-','-C','/run/fixture'],input=base.tar_bytes({'password':password.encode()}))
|
||||
stage('initdb')
|
||||
raw=run(['docker','exec',anchor,'/bin/sh','-c',"if initdb -D /var/lib/postgresql/data --username=fixture_admin --auth-local=trust --auth-host=scram-sha-256 --pwfile=/run/fixture/password > /dev/null 2>&1; then echo '{\"ready\":true}'; else echo '{\"ready\":false}'; fi"],max_output=256)
|
||||
require(json.loads(raw)=={'ready':True})
|
||||
run(['docker','exec','--detach',anchor,'/bin/sh','-c','exec postgres -D /var/lib/postgresql/data -c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/fixture > /dev/null 2>&1'])
|
||||
ready=False
|
||||
for _ in range(15):
|
||||
raw=run(['docker','exec',anchor,'/bin/sh','-c',"if pg_isready --host /run/fixture -U fixture_admin > /dev/null 2>&1; then echo '{\"ready\":true}'; else echo '{\"ready\":false}'; fi"],max_output=256)
|
||||
if json.loads(raw)=={'ready':True}:ready=True;break
|
||||
time.sleep(0.2)
|
||||
require(ready)
|
||||
anchor_id=run(['docker','container','inspect',anchor,'--format','{{.Id}}']).decode().strip();require(bool(re.fullmatch('[a-f0-9]{64}',anchor_id)))
|
||||
stage('helper_launch')
|
||||
args=base.base_args(session,helper,'container:'+anchor_id)+['--tmpfs','/fixture:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700','--entrypoint',python,manifest['python_image'],'-c','import time;time.sleep(240)']
|
||||
run(args);adapter.inspect_session(session,worker_only=True)
|
||||
interpreter=json.loads(run(['docker','exec',helper,python,'-c',base.INTERPRETER_PROBE],max_output=4096))
|
||||
require(interpreter.get('helper_ready') is True and interpreter.get('executable')==python and interpreter.get('psycopg_version')=='3.3.6')
|
||||
result['containment_verified_before_private_import']=True;publish()
|
||||
files={name:(HERE/name).read_bytes() for name in ('probe.py','old-schema.sql','upgrade.sql','old-writes.json')}
|
||||
files['private.json']=json.dumps({'admin_password':password}).encode()
|
||||
stage('private_import')
|
||||
run(['docker','exec','-i',helper,python,'-c',IMPORT],input=json.dumps({k:base64.b64encode(v).decode() for k,v in files.items()}).encode())
|
||||
stage('migration_compatibility')
|
||||
value=json.loads(run(['docker','exec',helper,python,'/fixture/probe.py'],max_output=4096))
|
||||
if value.get('status')!='passed':
|
||||
allowed_stages={'network_boundary','old_schema','new_migration','old_insert_receipt','old_update','old_delete'}
|
||||
allowed_errors={'AssertionError','OperationalError','ProgrammingError','UniqueViolation','NotNullViolation','UndefinedColumn','UndefinedTable','SyntaxError','InsufficientPrivilege','unclassified'}
|
||||
code=value.get('sqlstate')
|
||||
result['diagnostic']={'stage':value.get('stage') if value.get('stage') in allowed_stages else 'unclassified',
|
||||
'error_class':value.get('error_class') if value.get('error_class') in allowed_errors else 'unclassified',
|
||||
'sqlstate':code if isinstance(code,str) and re.fullmatch('[0-9A-Z]{5}',code) else None}
|
||||
publish();require(False)
|
||||
require(set(value.get('checks',{}))==CHECKS and all(v is True for v in value['checks'].values()))
|
||||
result['checks']=value['checks'];result['migration_seconds_synthetic_only']=value['migration_seconds_synthetic_only'];result['status']='synthetic_migration_old_write_shapes_passed'
|
||||
finally:
|
||||
stage('cleanup');adapter.command=lambda argv,**kwargs:original(argv,timeout=15,**kwargs)
|
||||
try:adapter.cleanup(session,result);result['container_cleanup_verified']=True
|
||||
except BaseException:result['container_cleanup_verified']=False;result['status']='failed'
|
||||
adapter.command=original
|
||||
if result.get('daemon_request_uncertain') or result.get('receipt_checkpoint_failed'):result['status']='failed'
|
||||
publish()
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument('--execute-reviewed-fixture',action='store_true');ap.add_argument('--expected-manifest-sha256');args=ap.parse_args()
|
||||
if not args.execute_reviewed_fixture:print(MANIFEST.read_text());return 0
|
||||
manifest=base.reviewed_manifest(args.expected_manifest_sha256)
|
||||
adapter=base.load_containment(manifest);base.preflight(adapter,manifest)
|
||||
os.umask(0o077);output=Path(tempfile.mkdtemp(prefix='nk-statehub-migration-compat-',dir='/tmp'))/'result.json'
|
||||
result={'schema':'statehub.synthetic-migration-compat.v1','status':'failed','production_contacted':False,'actual_credentials_used':False,'native_acceptance_claimed':False,'manifest_sha256':args.expected_manifest_sha256}
|
||||
def publish():output.write_text(json.dumps(result,indent=2)+'\n')
|
||||
try:execute(manifest,adapter,result,publish)
|
||||
except BaseException:result['status']='failed'
|
||||
finally:publish()
|
||||
print(json.dumps({'status':result['status'],'safe_result':str(output)}));return int(result['status']!='synthetic_migration_old_write_shapes_passed')
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"schema": "statehub.synthetic-migration-compat-proposal.v1",
|
||||
"stable_proposal_id": "STATE-WP-0095-T06-cluster-frozen-receiving-r1",
|
||||
"revision": "migration-compatibility-1",
|
||||
"status": "prepared_not_executed_not_approved",
|
||||
"containment_helper": "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py",
|
||||
"fixed_endpoint": "unix:///var/run/docker.sock",
|
||||
"engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1",
|
||||
"engine_name": "bnt-lap001",
|
||||
"postgres_image": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70",
|
||||
"python_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b",
|
||||
"helper_interpreter": "/usr/local/bin/python3",
|
||||
"source_sha256": {
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/README.md": "sha256:159c12b98fd58a0d066e39f36ff2a100c5f0fc3596a700207957d6ff6d0f00c1",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/build-offline-sql.py": "sha256:cb49052b6836e7b67807aaade1b1f26f7471d6e1bd8be2333281ab8a9d0cafc9",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/diagnostic_protocol.py": "sha256:d141fc00e18906bcdbf6cf5542e219580668daaaced6c57cbab179132f7c6a61",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/driver.py": "sha256:9e53c74e0c4bc74378113f97fca8a190026100418ea6cf89335a5df5b2170970",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/migration.py": "sha256:48f1730037be99b5623e878d7f20a06bec243ce27317fde81c82e16006d7b3ce",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/old-agent-message.py": "sha256:4bbbed06d4ae17bcbf86c34d02a684bd94b98d15bd9c2205700a524923e76e08",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/old-base.py": "sha256:7a9bf19021a2ea53abe40adc06b478ebbba0249e73e202f2c7a30b2d0e5c73d7",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/old-schema.sql": "sha256:4f95abdbe3942e7dbb8ad056cd6717de86f7e03d2d08a912f0a460b188727259",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/old-writes.json": "sha256:67910bf4341fd98cfcddf0cf84f1d93663daf5d04f054140e53f21326b293a88",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/probe.py": "sha256:d830a0cea7cafb66d7be97f83e7adca18b8f2b0694b572b11182cc3950f576f1",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/reviewed-driver-base.py": "sha256:04237939df1887df8a201181d87ebfacfa25855401b64f4998a3921111bd91eb",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/test_driver.py": "sha256:399326de64527b0006668ea841f8f41bd1d747e027cea3adec2581ebf7e7d994",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/upgrade.sql": "sha256:8336c253ec951fb93ed03db39b8da7f958daecdfaa83afa2454f8be7645fe6c1",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/validation.json": "sha256:0e0f061c68be85fb50589d0815a5da5ca64ea372ba380115560e8198adf693a8",
|
||||
"/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2"
|
||||
},
|
||||
"limits": {
|
||||
"containers": 2,
|
||||
"host_ports": 0,
|
||||
"host_mounts": 0,
|
||||
"named_or_anonymous_volumes": 0,
|
||||
"memory_each_bytes": 402653184,
|
||||
"cpus_each": 0.5,
|
||||
"pids_each": 64,
|
||||
"postgres_data_tmpfs_bytes": 134217728,
|
||||
"helper_tmpfs_bytes": 16777216,
|
||||
"container_lifetime_seconds": 240,
|
||||
"driver_deadline_seconds": 180,
|
||||
"rpc_timeout_seconds": 40,
|
||||
"cleanup_rpc_timeout_seconds": 15,
|
||||
"image_pull": "never",
|
||||
"cleanup_total_bound_seconds": 120
|
||||
},
|
||||
"scope": "Actual new migration SQL from Alembic offline generation on actual old-model DDL and compiled old-source SQLAlchemy DML in disposable PostgreSQL. No full old/new app HTTP/ORM flush or production timing proof.",
|
||||
"not_claimed": [
|
||||
"actual production schema/data compatibility",
|
||||
"full old/new application or inbox-feed contract acceptance",
|
||||
"real production migration runtime/lock budget",
|
||||
"durable backup/PITR/rollback authority",
|
||||
"native original TLS/KeyCape recovery acceptance",
|
||||
"hard-parent-loss cleanup proof"
|
||||
],
|
||||
"execution_command": [
|
||||
"python3",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/cluster-historical-publication-r1/migration-compat-r1/driver.py",
|
||||
"--execute-reviewed-fixture",
|
||||
"--expected-manifest-sha256",
|
||||
"<independently-reviewed-manifest-sha256>"
|
||||
],
|
||||
"offline_builder_precondition": "PYTHONHASHSEED=0; exact generated SQL/writes bound; no regeneration during Docker execution"
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
"""inbox change-feed revisions and tombstones (STATE-WP-0098-T05)
|
||||
|
||||
Implements §2 of docs/inbox-change-feed-contract.md (accepted 2026-10-13, D4):
|
||||
|
||||
- sequence ``inbox_revision_seq``;
|
||||
- ``revision bigint NOT NULL`` on ``agent_messages`` and ``message_receipts``,
|
||||
backfilled with ONE sequence value (one snapshot = one revision), indexed;
|
||||
- ``message_tombstones``;
|
||||
- a BEFORE INSERT OR UPDATE trigger on both tables that skips no-op updates,
|
||||
takes a transaction-scoped advisory lock (commit order = revision order) and
|
||||
assigns one revision per transaction (cached per txid in a txn-local GUC);
|
||||
- a BEFORE DELETE trigger on ``agent_messages`` that writes a tombstone.
|
||||
|
||||
The SQL is a frozen copy of ``api.models.agent_message.INBOX_FEED_FUNCTIONS_SQL``;
|
||||
tests/test_message_feed.py checks the two agree.
|
||||
|
||||
Revision ID: c4d5e6f7a8b9
|
||||
Revises: b3c4d5e6f7a8
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "c4d5e6f7a8b9"
|
||||
down_revision = "b3c4d5e6f7a8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
INBOX_ADVISORY_LOCK_KEY = 5_348_490_098
|
||||
|
||||
FUNCTIONS_SQL = f"""
|
||||
CREATE OR REPLACE FUNCTION inbox_feed_revision() RETURNS bigint
|
||||
LANGUAGE plpgsql AS $fn$
|
||||
DECLARE
|
||||
tx text := txid_current()::text;
|
||||
cached text := coalesce(current_setting('statehub.inbox_rev', true), '');
|
||||
rev bigint;
|
||||
BEGIN
|
||||
PERFORM pg_advisory_xact_lock({INBOX_ADVISORY_LOCK_KEY});
|
||||
IF split_part(cached, ':', 1) = tx THEN
|
||||
RETURN split_part(cached, ':', 2)::bigint;
|
||||
END IF;
|
||||
rev := nextval('inbox_revision_seq');
|
||||
PERFORM set_config('statehub.inbox_rev', tx || ':' || rev::text, true);
|
||||
RETURN rev;
|
||||
END
|
||||
$fn$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION inbox_feed_stamp_revision() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $fn$
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
IF NEW IS NOT DISTINCT FROM OLD THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END IF;
|
||||
NEW.revision := inbox_feed_revision();
|
||||
RETURN NEW;
|
||||
END
|
||||
$fn$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION inbox_feed_tombstone() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $fn$
|
||||
BEGIN
|
||||
INSERT INTO message_tombstones (message_id, revision, deleted_at, reason)
|
||||
VALUES (
|
||||
OLD.id,
|
||||
inbox_feed_revision(),
|
||||
now(),
|
||||
nullif(current_setting('statehub.delete_reason', true), '')
|
||||
)
|
||||
ON CONFLICT (message_id) DO UPDATE
|
||||
SET revision = EXCLUDED.revision,
|
||||
deleted_at = EXCLUDED.deleted_at,
|
||||
reason = EXCLUDED.reason;
|
||||
RETURN OLD;
|
||||
END
|
||||
$fn$;
|
||||
"""
|
||||
|
||||
TRIGGERS_SQL = """
|
||||
CREATE TRIGGER agent_messages_inbox_revision
|
||||
BEFORE INSERT OR UPDATE ON agent_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION inbox_feed_stamp_revision();
|
||||
CREATE TRIGGER agent_messages_inbox_tombstone
|
||||
BEFORE DELETE ON agent_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION inbox_feed_tombstone();
|
||||
CREATE TRIGGER message_receipts_inbox_revision
|
||||
BEFORE INSERT OR UPDATE ON message_receipts
|
||||
FOR EACH ROW EXECUTE FUNCTION inbox_feed_stamp_revision();
|
||||
"""
|
||||
|
||||
_TABLES = ("agent_messages", "message_receipts")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(sa.text("CREATE SEQUENCE inbox_revision_seq AS bigint"))
|
||||
for table in _TABLES:
|
||||
op.add_column(table, sa.Column("revision", sa.BigInteger(), nullable=True))
|
||||
# One sequence value for all existing rows: the pre-feed state is one revision.
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DO $$
|
||||
DECLARE base bigint := nextval('inbox_revision_seq');
|
||||
BEGIN
|
||||
UPDATE agent_messages SET revision = base;
|
||||
UPDATE message_receipts SET revision = base;
|
||||
END
|
||||
$$
|
||||
"""
|
||||
)
|
||||
)
|
||||
for table in _TABLES:
|
||||
op.alter_column(table, "revision", nullable=False)
|
||||
op.create_index(f"ix_{table}_revision", table, ["revision"])
|
||||
op.create_table(
|
||||
"message_tombstones",
|
||||
sa.Column("message_id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("revision", sa.BigInteger(), nullable=False),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("reason", sa.Text(), nullable=True),
|
||||
)
|
||||
op.create_index("ix_message_tombstones_revision", "message_tombstones", ["revision"])
|
||||
op.execute(sa.text(FUNCTIONS_SQL))
|
||||
op.execute(sa.text(TRIGGERS_SQL))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DROP TRIGGER IF EXISTS message_receipts_inbox_revision ON message_receipts;
|
||||
DROP TRIGGER IF EXISTS agent_messages_inbox_tombstone ON agent_messages;
|
||||
DROP TRIGGER IF EXISTS agent_messages_inbox_revision ON agent_messages;
|
||||
DROP FUNCTION IF EXISTS inbox_feed_tombstone();
|
||||
DROP FUNCTION IF EXISTS inbox_feed_stamp_revision();
|
||||
DROP FUNCTION IF EXISTS inbox_feed_revision();
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.drop_index("ix_message_tombstones_revision", table_name="message_tombstones")
|
||||
op.drop_table("message_tombstones")
|
||||
for table in reversed(_TABLES):
|
||||
op.drop_index(f"ix_{table}_revision", table_name=table)
|
||||
op.drop_column(table, "revision")
|
||||
op.execute(sa.text("DROP SEQUENCE IF EXISTS inbox_revision_seq"))
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String, Text, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from api.models.base import Base, new_uuid
|
||||
|
||||
|
||||
BROADCAST = "broadcast"
|
||||
MESSAGE_KINDS = ("message", "news", "standing")
|
||||
|
||||
|
||||
class AgentMessage(Base):
|
||||
__tablename__ = "agent_messages"
|
||||
__table_args__ = (
|
||||
Index("ix_agent_messages_to_kind_archived", "to_agent", "kind", "archived_at"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=new_uuid
|
||||
)
|
||||
from_agent: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
to_agent: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
subject: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
thread_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("agent_messages.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
read_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
archived_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
# STATE-WP-0093: kind is only meaningful for broadcasts
|
||||
# (message | news | standing); direct messages stay "message".
|
||||
kind: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="message", server_default="message"
|
||||
)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
supersedes_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("agent_messages.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=text("now()"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
thread_root: Mapped["AgentMessage | None"] = relationship(
|
||||
"AgentMessage",
|
||||
remote_side="AgentMessage.id",
|
||||
foreign_keys=[thread_id],
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
|
||||
class MessageReceipt(Base):
|
||||
"""Per-reader state for broadcast messages (STATE-WP-0093).
|
||||
|
||||
Written only for broadcasts; direct messages keep the global
|
||||
``AgentMessage.read_at``.
|
||||
"""
|
||||
|
||||
__tablename__ = "message_receipts"
|
||||
|
||||
message_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("agent_messages.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
agent: Mapped[str] = mapped_column(String(100), primary_key=True, index=True)
|
||||
delivered_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
read_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
acknowledged_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
def new_uuid() -> uuid.UUID:
|
||||
return uuid.uuid4()
|
||||
|
||||
|
||||
def new_uuid7() -> uuid.UUID:
|
||||
"""Generate a UUIDv7 (RFC 9562): 48-bit big-endian ms timestamp, version
|
||||
and variant bits, remaining bits random. Time-sortable, so primary keys
|
||||
generated with this helper order chronologically without a separate
|
||||
created_at index lookup — the identity layering canon
|
||||
(work-record-types_v0.1.md) calls this out as the primary internal key
|
||||
for new work-record entities.
|
||||
|
||||
Dependency-free (no uuid7 in stdlib before Python 3.14, no third-party
|
||||
lib added for a ~15-line, non-cryptographic layout).
|
||||
"""
|
||||
unix_ts_ms = int(time.time() * 1000)
|
||||
rand = int.from_bytes(os.urandom(10), "big")
|
||||
rand_a = (rand >> 62) & 0x0FFF # top 12 bits of the 80 random bits
|
||||
rand_b = rand & 0x3FFFFFFFFFFFFFFF # bottom 62 bits
|
||||
value = (
|
||||
(unix_ts_ms << 80)
|
||||
| (0x7 << 76) # version 7
|
||||
| (rand_a << 64)
|
||||
| (0x2 << 62) # variant 10
|
||||
| rand_b
|
||||
)
|
||||
return uuid.UUID(int=value)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
CREATE TABLE agent_messages (
|
||||
id UUID NOT NULL,
|
||||
from_agent VARCHAR(100) NOT NULL,
|
||||
to_agent VARCHAR(100) NOT NULL,
|
||||
subject VARCHAR(500) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
thread_id UUID,
|
||||
read_at TIMESTAMP WITH TIME ZONE,
|
||||
archived_at TIMESTAMP WITH TIME ZONE,
|
||||
kind VARCHAR(20) DEFAULT 'message' NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
supersedes_id UUID,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY(supersedes_id) REFERENCES agent_messages (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY(thread_id) REFERENCES agent_messages (id) ON DELETE SET NULL
|
||||
)
|
||||
;
|
||||
|
||||
CREATE TABLE message_receipts (
|
||||
message_id UUID NOT NULL,
|
||||
agent VARCHAR(100) NOT NULL,
|
||||
delivered_at TIMESTAMP WITH TIME ZONE,
|
||||
read_at TIMESTAMP WITH TIME ZONE,
|
||||
acknowledged_at TIMESTAMP WITH TIME ZONE,
|
||||
PRIMARY KEY (message_id, agent),
|
||||
FOREIGN KEY(message_id) REFERENCES agent_messages (id) ON DELETE CASCADE
|
||||
)
|
||||
;
|
||||
CREATE INDEX ix_agent_messages_thread_id ON agent_messages (thread_id);
|
||||
CREATE INDEX ix_agent_messages_to_agent ON agent_messages (to_agent);
|
||||
CREATE INDEX ix_agent_messages_to_kind_archived ON agent_messages (to_agent, kind, archived_at);
|
||||
CREATE INDEX ix_message_receipts_agent ON message_receipts (agent);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"insert_message": {
|
||||
"sql": "INSERT INTO agent_messages (id, from_agent, to_agent, subject, body, kind) VALUES (%(id)s::UUID, %(from_agent)s, %(to_agent)s, %(subject)s, %(body)s, %(kind)s)",
|
||||
"params": {
|
||||
"id": "11111111-1111-4111-8111-111111111111",
|
||||
"from_agent": "fixture-sender",
|
||||
"to_agent": "fixture-reader",
|
||||
"subject": "synthetic",
|
||||
"body": "synthetic",
|
||||
"kind": "message"
|
||||
}
|
||||
},
|
||||
"insert_receipt": {
|
||||
"sql": "INSERT INTO message_receipts (message_id, agent, delivered_at) VALUES (%(message_id)s::UUID, %(agent)s, now())",
|
||||
"params": {
|
||||
"message_id": "11111111-1111-4111-8111-111111111111",
|
||||
"agent": "fixture-reader"
|
||||
}
|
||||
},
|
||||
"update_message": {
|
||||
"sql": "UPDATE agent_messages SET read_at=now() WHERE agent_messages.id = %(id_1)s::UUID",
|
||||
"params": {
|
||||
"id_1": "11111111-1111-4111-8111-111111111111"
|
||||
}
|
||||
},
|
||||
"delete_message": {
|
||||
"sql": "DELETE FROM agent_messages WHERE agent_messages.id = %(id_1)s::UUID",
|
||||
"params": {
|
||||
"id_1": "11111111-1111-4111-8111-111111111111"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Only disposable fixture DSN generated by contained driver; no env fallback."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
import psycopg
|
||||
ROOT=Path('/fixture')
|
||||
CHECKS=('only_loopback_no_external_routes','actual_old_model_schema_created','actual_new_migration_sql_applied','legacy_rows_backfilled_one_revision',
|
||||
'old_compiled_insert_accepted_on_new_schema','old_compiled_receipt_same_transaction_revision',
|
||||
'old_compiled_update_stamped_revision','old_noop_update_revision_unchanged',
|
||||
'old_compiled_delete_writes_tombstone','no_application_schema_downgrade')
|
||||
|
||||
def main():
|
||||
stage='network_boundary';checks={}
|
||||
try:
|
||||
routes=Path('/proc/net/route').read_text().splitlines()[1:]
|
||||
assert all(line.split()[0]=='lo' for line in routes)
|
||||
checks[CHECKS[0]]=True
|
||||
private=json.loads((ROOT/'private.json').read_text())
|
||||
assert set(private)=={'admin_password'} and re.fullmatch('[0-9a-f]{48}',private['admin_password'])
|
||||
writes=json.loads((ROOT/'old-writes.json').read_text())
|
||||
with psycopg.connect(host='127.0.0.1',port=5432,dbname='postgres',user='fixture_admin',password=private['admin_password'],connect_timeout=10) as conn:
|
||||
stage='old_schema'
|
||||
conn.execute((ROOT/'old-schema.sql').read_text());conn.commit()
|
||||
checks[CHECKS[1]]=True
|
||||
legacy='22222222-2222-4222-8222-222222222222'
|
||||
current_id='11111111-1111-4111-8111-111111111111'
|
||||
for name,key in (('insert_message','id'),('insert_receipt','message_id')):
|
||||
value=writes[name];params=dict(value['params']);params[key]=legacy;conn.execute(value['sql'],params)
|
||||
conn.commit()
|
||||
stage='new_migration';start=time.monotonic()
|
||||
conn.execute((ROOT/'upgrade.sql').read_text());conn.commit()
|
||||
elapsed=round(time.monotonic()-start,6);checks[CHECKS[2]]=True
|
||||
backfill=conn.execute('SELECT revision FROM agent_messages UNION ALL SELECT revision FROM message_receipts').fetchall()
|
||||
assert len(backfill)==2 and backfill[0][0]==backfill[1][0] and backfill[0][0]>0
|
||||
checks[CHECKS[3]]=True;conn.commit()
|
||||
stage='old_insert_receipt'
|
||||
for name in ('insert_message','insert_receipt'):
|
||||
value=writes[name];conn.execute(value['sql'],value['params'])
|
||||
revisions=conn.execute('SELECT revision FROM agent_messages WHERE id=%s UNION ALL SELECT revision FROM message_receipts WHERE message_id=%s',(current_id,current_id)).fetchall()
|
||||
assert len(revisions)==2 and revisions[0][0]>0
|
||||
checks[CHECKS[4]]=True;assert revisions[0][0]==revisions[1][0];checks[CHECKS[5]]=True
|
||||
conn.commit();prior=revisions[0][0]
|
||||
stage='old_update';value=writes['update_message'];conn.execute(value['sql'],value['params'])
|
||||
current=conn.execute('SELECT revision FROM agent_messages WHERE id=%s',(current_id,)).fetchone()[0]
|
||||
assert current>prior;conn.commit();checks[CHECKS[6]]=True
|
||||
conn.execute('UPDATE agent_messages SET subject=subject')
|
||||
assert conn.execute('SELECT revision FROM agent_messages WHERE id=%s',(current_id,)).fetchone()[0]==current
|
||||
conn.commit();checks[CHECKS[7]]=True
|
||||
stage='old_delete';value=writes['delete_message'];conn.execute(value['sql'],value['params'])
|
||||
assert conn.execute('SELECT count(*) FROM agent_messages WHERE id=%s',(current_id,)).fetchone()[0]==0
|
||||
tombstone=conn.execute('SELECT revision FROM message_tombstones WHERE message_id=%s',(current_id,)).fetchone()[0]
|
||||
assert tombstone>current;conn.commit();checks[CHECKS[8]]=True
|
||||
assert conn.execute("SELECT count(*) FROM information_schema.columns WHERE table_name='agent_messages' AND column_name='revision'").fetchone()[0]==1
|
||||
checks[CHECKS[9]]=True
|
||||
print(json.dumps({'status':'passed','checks':checks,'migration_seconds_synthetic_only':elapsed,'psycopg_version':psycopg.__version__}))
|
||||
return 0
|
||||
except BaseException as error:
|
||||
allowed={'AssertionError','OperationalError','ProgrammingError','UniqueViolation','NotNullViolation','UndefinedColumn','UndefinedTable','SyntaxError','InsufficientPrivilege'}
|
||||
code=getattr(error,'sqlstate',None)
|
||||
print(json.dumps({'status':'failed','stage':stage,'error_class':type(error).__name__ if type(error).__name__ in allowed else 'unclassified','sqlstate':code if isinstance(code,str) and re.fullmatch('[0-9A-Z]{5}',code) else None}))
|
||||
return 0 # transport succeeds; closed status still refuses acceptance
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Default is a read-only plan. Docker execution awaits independent exact review."""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
MANIFEST = HERE / 'manifest.json'
|
||||
|
||||
# Fixed local diagnostic parser; no dependency or credential discovery.
|
||||
_spec = importlib.util.spec_from_file_location('fixture_diagnostic_protocol', HERE / 'diagnostic_protocol.py')
|
||||
diagnostics = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(diagnostics)
|
||||
|
||||
|
||||
def require(condition):
|
||||
if not condition:
|
||||
raise RuntimeError('bounded_synthetic_fixture_refused')
|
||||
|
||||
|
||||
def reviewed_manifest(expected):
|
||||
raw = MANIFEST.read_bytes()
|
||||
require(expected == hashlib.sha256(raw).hexdigest())
|
||||
manifest = json.loads(raw)
|
||||
for path, digest in manifest['source_sha256'].items():
|
||||
require(bool(re.fullmatch(r'sha256:[0-9a-f]{64}', digest)))
|
||||
require('sha256:' + hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest)
|
||||
return manifest
|
||||
|
||||
|
||||
def load_containment(manifest):
|
||||
spec = importlib.util.spec_from_file_location('reviewed_existing_containment', manifest['containment_helper'])
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def session_plan(manifest, run_id):
|
||||
require(bool(re.fullmatch(r'[a-f0-9]{32}', run_id)))
|
||||
anchor = 'nk49-kc-' + run_id + '-journal-postgres'
|
||||
probe = 'nk49-kc-' + run_id + '-journal-probe'
|
||||
return {'run_id': run_id, 'containers': [anchor, probe], 'worker': anchor, 'issuer': probe,
|
||||
'images': {anchor: manifest['postgres_image'], probe: manifest['python_image']}, 'volumes': {}}
|
||||
|
||||
|
||||
def base_args(session, name, network, user="1000:1000"):
|
||||
uid, gid = user.split(":")
|
||||
return ['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', network,
|
||||
'--label', 'net-kingdom.recovery-run=' + session['run_id'], '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--user', user, '--memory', '384m', '--cpus', '0.5',
|
||||
'--pids-limit', '64', '--log-driver', 'none', '--restart', 'no',
|
||||
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=' + uid + ',gid=' + gid + ',mode=0700']
|
||||
|
||||
|
||||
def tar_bytes(files):
|
||||
out = io.BytesIO()
|
||||
with tarfile.open(fileobj=out, mode='w') as archive:
|
||||
for name, value in files.items():
|
||||
require(bool(re.fullmatch(r'[a-zA-Z0-9._/-]+', name)) and not name.startswith('/') and '..' not in Path(name).parts)
|
||||
entry = tarfile.TarInfo(name)
|
||||
entry.size = len(value)
|
||||
entry.mode = 0o600
|
||||
entry.uid = entry.gid = 1000
|
||||
archive.addfile(entry, io.BytesIO(value))
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
IMPORT = r'''
|
||||
import base64,json,os,sys
|
||||
from pathlib import Path
|
||||
os.umask(0o077)
|
||||
root=Path('/fixture')
|
||||
(root/'source').mkdir(mode=0o700)
|
||||
(root/'source/migrations').mkdir(mode=0o700)
|
||||
(root/'source/tests').mkdir(mode=0o700)
|
||||
for name,encoded in json.load(sys.stdin).items():
|
||||
if name not in ('source/provisioner.py','source/group_provenance.py','source/group_provenance_postgres.py','source/migrations/group_journal_postgres_v1.sql','source/tests/test_service_groups.py','probe.py','private.json','diagnostic_protocol.py'):raise ValueError()
|
||||
fd=os.open(root/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
||||
with os.fdopen(fd,'wb') as output:output.write(base64.b64decode(encoded,validate=True))
|
||||
'''
|
||||
|
||||
|
||||
def helper_python(manifest):
|
||||
# Exact producer source installs into official Python's system prefix.
|
||||
# Never fall back to PATH or another interpreter/virtual environment.
|
||||
require(manifest.get('helper_interpreter') == '/usr/local/bin/python3')
|
||||
return manifest['helper_interpreter']
|
||||
|
||||
|
||||
INTERPRETER_PROBE = r"""
|
||||
import json,sys
|
||||
try:
|
||||
import psycopg
|
||||
valid=sys.executable=='/usr/local/bin/python3' and psycopg.__version__.split('.')[0]=='3'
|
||||
print(json.dumps({'helper_ready':valid,'executable':sys.executable,'psycopg_version':psycopg.__version__}))
|
||||
except Exception:
|
||||
print(json.dumps({'helper_ready':False}))
|
||||
"""
|
||||
|
||||
|
||||
def preflight(adapter, manifest):
|
||||
helper_python(manifest)
|
||||
adapter.docker_environment() # Refuse ambient alternate routing before calls.
|
||||
contexts = json.loads(adapter.command(['docker', 'context', 'inspect'], timeout=15))
|
||||
require(len(contexts) == 1 and contexts[0]['Endpoints']['docker']['Host'] == manifest['fixed_endpoint'])
|
||||
facts = json.loads(adapter.command(['docker', 'info', '--format',
|
||||
'{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'], timeout=15))
|
||||
require(facts == {'ID': manifest['engine_id'], 'Name': manifest['engine_name'], 'OSType': 'linux'})
|
||||
for image in (manifest['postgres_image'], manifest['python_image']):
|
||||
value = adapter.command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image], timeout=15).decode().strip()
|
||||
require(bool(re.fullmatch(r'sha256:[a-f0-9]{64}', value)))
|
||||
|
||||
|
||||
def retain_helper_result(result, phase, raw, publish):
|
||||
stages, value = diagnostics.parse_frames(raw)
|
||||
safe = {'status': value['status'], 'stages': stages}
|
||||
if value['status'] == 'failed':
|
||||
safe['diagnostic'] = value['diagnostic']
|
||||
result.setdefault('helper_diagnostics', {})[phase] = safe
|
||||
publish() # Persist failure evidence before the phase oracle can raise.
|
||||
return value
|
||||
|
||||
|
||||
def execute(manifest, adapter, result, checkpoint=None):
|
||||
session = session_plan(manifest, uuid.uuid4().hex)
|
||||
result['session'] = {'run_id': session['run_id'], 'containers': session['containers'],
|
||||
'label': 'net-kingdom.recovery-run=' + session['run_id']}
|
||||
python = helper_python(manifest)
|
||||
deadline = time.monotonic() + 210
|
||||
original_command = adapter.command
|
||||
def publish():
|
||||
if checkpoint is not None:
|
||||
try: checkpoint()
|
||||
except OSError: result['receipt_checkpoint_failed'] = True
|
||||
def mark(stage):
|
||||
require(stage in diagnostics.DRIVER_STAGES)
|
||||
result['driver_stage'] = stage
|
||||
publish()
|
||||
def event(argv):
|
||||
require(len(result.setdefault('rpc_trace', [])) < 192)
|
||||
operation = argv[1] if len(argv) > 1 and argv[1] in {'run','exec','rm','container','context','image','info','volume'} else 'other'
|
||||
row = {'stage': result['driver_stage'], 'operation': operation, 'status': 'started'}
|
||||
result['rpc_trace'].append(row)
|
||||
publish()
|
||||
return row
|
||||
def run(argv, **kwargs):
|
||||
row = event(argv)
|
||||
remaining = deadline - time.monotonic()
|
||||
require(remaining > 0)
|
||||
try:
|
||||
value = original_command(argv, timeout=min(50, remaining), **kwargs)
|
||||
row['status'] = 'completed'
|
||||
publish()
|
||||
return value
|
||||
except BaseException as error:
|
||||
row['status'] = 'failed'
|
||||
result['daemon_request_uncertain'] = True
|
||||
result.setdefault('driver_failure', diagnostics.error_metadata(error, result['driver_stage']))
|
||||
# Timeout output can contain complete public progress frames. Never
|
||||
# retain raw stdout/stderr or command/exception text.
|
||||
if result['driver_stage'] in manifest['phase_oracles']:
|
||||
partial = getattr(error, 'stdout', None)
|
||||
if partial:
|
||||
try:
|
||||
stages, _ = diagnostics.parse_frames(partial, require_final=False)
|
||||
result.setdefault('helper_diagnostics', {})[result['driver_stage']] = {'status': 'interrupted', 'stages': stages}
|
||||
except Exception:
|
||||
pass
|
||||
publish()
|
||||
raise
|
||||
mark('session_registered')
|
||||
adapter.command = run
|
||||
try:
|
||||
anchor = session['worker']
|
||||
args = base_args(session, anchor, 'none', user='999:999') + [
|
||||
'--tmpfs', '/var/lib/postgresql:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700',
|
||||
'--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=134217728,uid=999,gid=999,mode=0700',
|
||||
'--tmpfs', '/run/fixture:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700',
|
||||
'--env', 'PGDATA=/var/lib/postgresql/data', '--entrypoint', '/bin/sh',
|
||||
manifest['postgres_image'], '-c', 'sleep 240']
|
||||
mark('anchor_launch')
|
||||
run(args)
|
||||
mark('anchor_containment')
|
||||
adapter.inspect_session({**session, 'containers': [anchor], 'issuer': anchor}, worker_only=True)
|
||||
admin_password, runtime_password = secrets.token_hex(24), secrets.token_hex(24)
|
||||
mark('postgres_private_import')
|
||||
run(['docker', 'exec', '-i', anchor, 'tar', '-xf', '-', '-C', '/run/fixture'],
|
||||
input=tar_bytes({'password': admin_password.encode()}))
|
||||
mark('postgres_initdb')
|
||||
initialized = json.loads(run(['docker', 'exec', anchor, '/bin/sh', '-c',
|
||||
"if initdb -D /var/lib/postgresql/data --username=fixture_admin --auth-local=trust --auth-host=scram-sha-256 --pwfile=/run/fixture/password > /dev/null 2>&1; then echo '{\"postgres_initdb\":true}'; else echo '{\"postgres_initdb\":false}'; fi"], max_output=256))
|
||||
result['postgres_initdb_complete'] = initialized == {'postgres_initdb': True}
|
||||
publish()
|
||||
require(result['postgres_initdb_complete'])
|
||||
mark('postgres_launch')
|
||||
run(['docker', 'exec', '--detach', anchor, '/bin/sh', '-c',
|
||||
'exec postgres -D /var/lib/postgresql/data -c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/fixture > /dev/null 2>&1'])
|
||||
mark('anchor_identity')
|
||||
anchor_id = run(['docker', 'container', 'inspect', anchor, '--format', '{{.Id}}']).decode().strip()
|
||||
require(bool(re.fullmatch(r'[a-f0-9]{64}', anchor_id)))
|
||||
args = base_args(session, session['issuer'], 'container:' + anchor_id) + [
|
||||
'--tmpfs', '/fixture:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700',
|
||||
'--entrypoint', python, manifest['python_image'], '-c', 'import time;time.sleep(240)']
|
||||
mark('helper_launch')
|
||||
run(args)
|
||||
mark('full_containment')
|
||||
adapter.inspect_session(session, worker_only=True)
|
||||
result['containment_verified_before_generated_private_import'] = True
|
||||
mark('helper_interpreter')
|
||||
interpreter = json.loads(run(['docker', 'exec', session['issuer'], python, '-c', INTERPRETER_PROBE], max_output=4096))
|
||||
require(interpreter.get('helper_ready') is True and interpreter.get('executable') == python)
|
||||
require(isinstance(interpreter.get('psycopg_version'), str) and re.fullmatch(r'3\.[0-9]+\.[0-9]+(?:[a-z]+[0-9]+)?', interpreter['psycopg_version']))
|
||||
result['helper_interpreter_verified_before_candidate_import'] = True
|
||||
result['helper_psycopg_version'] = interpreter['psycopg_version']
|
||||
files = {'source/' + name: (HERE.parent / 'postimage/identity-provisioner' / name).read_bytes()
|
||||
for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py',
|
||||
'migrations/group_journal_postgres_v1.sql')}
|
||||
files['source/tests/test_service_groups.py'] = (HERE / 'test_service_groups.py').read_bytes()
|
||||
files.update({'probe.py': (HERE / 'probe.py').read_bytes(), 'diagnostic_protocol.py': (HERE / 'diagnostic_protocol.py').read_bytes(), 'private.json': json.dumps({
|
||||
'admin_password': admin_password, 'runtime_password': runtime_password}).encode()})
|
||||
mark('candidate_import')
|
||||
run(['docker', 'exec', '-i', session['issuer'], python, '-c', IMPORT],
|
||||
input=json.dumps({k: base64.b64encode(v).decode() for k, v in files.items()}).encode())
|
||||
result['checks'] = {}
|
||||
backup = None
|
||||
active_database = 'postgres'
|
||||
for phase in ('bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', 'restored'):
|
||||
if phase == 'restored':
|
||||
require(backup is not None)
|
||||
mark('database_restore')
|
||||
run(['docker', 'exec', '-i', anchor, 'pg_restore', '--host', '/run/fixture', '-U', 'fixture_admin',
|
||||
'--dbname', active_database, '--clean', '--if-exists', '--single-transaction', '--exit-on-error', '--no-owner'], input=backup)
|
||||
mark(phase)
|
||||
raw = run(['docker', 'exec', session['issuer'], python, '/fixture/probe.py', phase], max_output=32768)
|
||||
value = retain_helper_result(result, phase, raw, publish)
|
||||
expected = set(manifest['phase_oracles'][phase])
|
||||
require(value.get('status') == 'passed' and set(value.get('checks', {})) == expected)
|
||||
require(all(v is True for v in value['checks'].values()))
|
||||
result['checks'].update(value['checks'])
|
||||
result['psycopg_version'] = value['psycopg_version']
|
||||
require(value.get('active_database') in {'postgres', 'fixture_fork'})
|
||||
active_database = value['active_database']
|
||||
if phase == 'bootstrap':
|
||||
mark('database_dump')
|
||||
backup = run(['docker', 'exec', anchor, 'pg_dump', '--host', '/run/fixture', '-U', 'fixture_admin',
|
||||
'--dbname', 'postgres', '--format=custom', '--no-owner'], max_output=4194304)
|
||||
result['status'] = 'synthetic_postgres_journal_compatibility_passed'
|
||||
mark('complete')
|
||||
except BaseException as error:
|
||||
result.setdefault('driver_failure', diagnostics.error_metadata(error, result['driver_stage']))
|
||||
publish()
|
||||
raise
|
||||
finally:
|
||||
mark('cleanup')
|
||||
def cleanup_command(argv, **kwargs):
|
||||
row = event(argv)
|
||||
try:
|
||||
value = original_command(argv, timeout=15, **kwargs)
|
||||
row['status'] = 'completed'
|
||||
publish()
|
||||
return value
|
||||
except BaseException:
|
||||
result['daemon_request_uncertain'] = True
|
||||
row['status'] = 'failed'
|
||||
publish()
|
||||
raise
|
||||
adapter.command = cleanup_command
|
||||
try:
|
||||
adapter.cleanup(session, result)
|
||||
result['container_cleanup_verified'] = True
|
||||
except BaseException:
|
||||
result['status'] = 'failed'
|
||||
result['container_cleanup_verified'] = False
|
||||
adapter.command = original_command
|
||||
publish()
|
||||
if result.get('daemon_request_uncertain') or result.get('receipt_checkpoint_failed'):
|
||||
result['status'] = 'failed'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--plan', action='store_true')
|
||||
parser.add_argument('--execute-reviewed-fixture', action='store_true')
|
||||
parser.add_argument('--expected-manifest-sha256')
|
||||
args = parser.parse_args()
|
||||
if not args.execute_reviewed_fixture:
|
||||
print(MANIFEST.read_text())
|
||||
return 0
|
||||
require(not args.plan)
|
||||
manifest = reviewed_manifest(args.expected_manifest_sha256)
|
||||
adapter = load_containment(manifest)
|
||||
preflight(adapter, manifest)
|
||||
os.umask(0o077)
|
||||
output = Path(tempfile.mkdtemp(prefix='nk48-postgres-native-review-', dir='/tmp')) / 'result.json'
|
||||
result = {'schema': 'nk48.synthetic-postgres-journal-result.v1', 'status': 'failed',
|
||||
'manifest_sha256': args.expected_manifest_sha256, 'production_contacted': False,
|
||||
'actual_credentials_used': False, 'native_acceptance_claimed': False}
|
||||
try:
|
||||
execute(manifest, adapter, result, checkpoint=lambda: output.write_text(json.dumps(result, indent=2) + '\n'))
|
||||
except BaseException:
|
||||
result['status'] = 'failed'
|
||||
finally:
|
||||
output.write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps({'status': result['status'], 'safe_result': str(output)}))
|
||||
return int(result['status'] != 'synthetic_postgres_journal_compatibility_passed')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
HERE=Path(__file__).parent
|
||||
spec=importlib.util.spec_from_file_location('migration_compat_driver',HERE/'driver.py')
|
||||
driver=importlib.util.module_from_spec(spec);spec.loader.exec_module(driver)
|
||||
|
||||
class FakeAdapter:
|
||||
def __init__(self,refuse_containment=False,probe_failure=False):
|
||||
self.calls=[];self.refuse_containment=refuse_containment;self.probe_failure=probe_failure;self.cleaned=False
|
||||
def command(self,argv,**kwargs):
|
||||
self.calls.append((argv,kwargs))
|
||||
if argv[1:3]==['container','inspect']:return ('a'*64).encode()
|
||||
if 'INTERPRETER_PROBE' in argv:raise AssertionError('literal placeholder')
|
||||
if driver.base.INTERPRETER_PROBE in argv:return json.dumps({'helper_ready':True,'executable':'/usr/local/bin/python3','psycopg_version':'3.3.6'}).encode()
|
||||
if '/fixture/probe.py' in argv:
|
||||
value={'status':'failed','stage':'new_migration','error_class':'NotNullViolation','sqlstate':'23502'} if self.probe_failure else {'status':'passed','checks':dict.fromkeys(driver.CHECKS,True),'migration_seconds_synthetic_only':0.01}
|
||||
return json.dumps(value).encode()
|
||||
if any('initdb -D' in a or 'pg_isready ' in a for a in argv):return b'{"ready":true}'
|
||||
return b'fixture-command-ok'
|
||||
def inspect_session(self,*args,**kwargs):
|
||||
if self.refuse_containment:raise RuntimeError('synthetic containment refusal')
|
||||
def cleanup(self,session,result):
|
||||
self.cleaned=True;result['cleanup']={n:'absent' for n in session['containers']}
|
||||
|
||||
@pytest.fixture
|
||||
def manifest():return json.loads((HERE/'manifest.json').read_text())
|
||||
|
||||
def test_exact_bounded_argv_and_cleanup(manifest):
|
||||
adapter=FakeAdapter();original=adapter.command;result={'status':'failed'}
|
||||
driver.execute(manifest,adapter,result,lambda:None)
|
||||
assert result['status']=='synthetic_migration_old_write_shapes_passed'
|
||||
assert adapter.cleaned and result['container_cleanup_verified'] and adapter.command==original
|
||||
runs=[a for a,_ in adapter.calls if a[:2]==['docker','run']]
|
||||
assert len(runs)==2
|
||||
for args in runs:
|
||||
assert '--pull=never' in args and '--read-only' in args
|
||||
assert '--privileged' not in args and '--volume' not in args and '-v' not in args and '-p' not in args
|
||||
assert args[args.index('--log-driver')+1]=='none'
|
||||
assert runs[0][runs[0].index('--network')+1]=='none'
|
||||
assert runs[1][runs[1].index('--network')+1]=='container:'+'a'*64
|
||||
assert any(a.startswith('/var/lib/postgresql/data:rw,nosuid,nodev,noexec,') for a in runs[0])
|
||||
assert any(a.startswith('/var/lib/postgresql:rw,nosuid,nodev,noexec,') for a in runs[0])
|
||||
imports=[a for a,k in adapter.calls if 'input' in k]
|
||||
assert len(imports)==2
|
||||
|
||||
def test_containment_refusal_cleans_before_any_private_import(manifest):
|
||||
adapter=FakeAdapter(refuse_containment=True);result={'status':'failed'}
|
||||
with pytest.raises(RuntimeError):driver.execute(manifest,adapter,result,lambda:None)
|
||||
assert adapter.cleaned and result['container_cleanup_verified']
|
||||
assert not any('input' in kwargs for _,kwargs in adapter.calls)
|
||||
|
||||
def test_closed_probe_failure_is_retained_and_never_accepts(manifest):
|
||||
adapter=FakeAdapter(probe_failure=True);result={'status':'failed'}
|
||||
with pytest.raises(RuntimeError):driver.execute(manifest,adapter,result,lambda:None)
|
||||
assert result['status']=='failed' and result['diagnostic']['sqlstate']=='23502'
|
||||
assert adapter.cleaned and result['container_cleanup_verified']
|
||||
|
||||
def test_manifest_hash_mismatch_refuses_before_adapter_loading(monkeypatch):
|
||||
with pytest.raises(RuntimeError):driver.base.reviewed_manifest('0'*64)
|
||||
|
||||
def test_offline_real_orm_and_migration_compilation_without_network(tmp_path):
|
||||
for name in ['build-offline-sql.py','old-base.py','old-agent-message.py','migration.py']:
|
||||
shutil.copyfile(HERE/name,tmp_path/name)
|
||||
script="import socket,runpy; socket.socket.connect=lambda *a: (_ for _ in ()).throw(AssertionError('network forbidden')); runpy.run_path('build-offline-sql.py',run_name='__main__')"
|
||||
env=dict(os.environ);env['PYTHONHASHSEED']='0'
|
||||
p=subprocess.run([sys.executable,'-c',script],cwd=tmp_path,capture_output=True,env=env)
|
||||
assert p.returncode==0
|
||||
for name in ['old-schema.sql','upgrade.sql','old-writes.json']:
|
||||
assert (tmp_path/name).read_bytes()==(HERE/name).read_bytes()
|
||||
assert b'CREATE TRIGGER agent_messages_inbox_revision' in (tmp_path/'upgrade.sql').read_bytes()
|
||||
assert b'revision' not in (tmp_path/'old-schema.sql').read_bytes()
|
||||
|
||||
|
||||
def test_receipt_failure_still_cleans_and_cannot_report_success(manifest):
|
||||
adapter=FakeAdapter();result={'status':'failed'}
|
||||
def unavailable():raise OSError('synthetic private receipt unavailable')
|
||||
driver.execute(manifest,adapter,result,unavailable)
|
||||
assert adapter.cleaned and result['container_cleanup_verified']
|
||||
assert result['receipt_checkpoint_failed'] and result['status']=='failed'
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
CREATE SEQUENCE inbox_revision_seq AS bigint;
|
||||
|
||||
ALTER TABLE agent_messages ADD COLUMN revision BIGINT;
|
||||
|
||||
ALTER TABLE message_receipts ADD COLUMN revision BIGINT;
|
||||
|
||||
DO $$
|
||||
DECLARE base bigint := nextval('inbox_revision_seq');
|
||||
BEGIN
|
||||
UPDATE agent_messages SET revision = base;
|
||||
UPDATE message_receipts SET revision = base;
|
||||
END
|
||||
$$;
|
||||
|
||||
ALTER TABLE agent_messages ALTER COLUMN revision SET NOT NULL;
|
||||
|
||||
CREATE INDEX ix_agent_messages_revision ON agent_messages (revision);
|
||||
|
||||
ALTER TABLE message_receipts ALTER COLUMN revision SET NOT NULL;
|
||||
|
||||
CREATE INDEX ix_message_receipts_revision ON message_receipts (revision);
|
||||
|
||||
CREATE TABLE message_tombstones (
|
||||
message_id UUID NOT NULL,
|
||||
revision BIGINT NOT NULL,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
reason TEXT,
|
||||
PRIMARY KEY (message_id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_message_tombstones_revision ON message_tombstones (revision);
|
||||
|
||||
CREATE OR REPLACE FUNCTION inbox_feed_revision() RETURNS bigint
|
||||
LANGUAGE plpgsql AS $fn$
|
||||
DECLARE
|
||||
tx text := txid_current()::text;
|
||||
cached text := coalesce(current_setting('statehub.inbox_rev', true), '');
|
||||
rev bigint;
|
||||
BEGIN
|
||||
PERFORM pg_advisory_xact_lock(5348490098);
|
||||
IF split_part(cached, ':', 1) = tx THEN
|
||||
RETURN split_part(cached, ':', 2)::bigint;
|
||||
END IF;
|
||||
rev := nextval('inbox_revision_seq');
|
||||
PERFORM set_config('statehub.inbox_rev', tx || ':' || rev::text, true);
|
||||
RETURN rev;
|
||||
END
|
||||
$fn$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION inbox_feed_stamp_revision() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $fn$
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
IF NEW IS NOT DISTINCT FROM OLD THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END IF;
|
||||
NEW.revision := inbox_feed_revision();
|
||||
RETURN NEW;
|
||||
END
|
||||
$fn$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION inbox_feed_tombstone() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $fn$
|
||||
BEGIN
|
||||
INSERT INTO message_tombstones (message_id, revision, deleted_at, reason)
|
||||
VALUES (
|
||||
OLD.id,
|
||||
inbox_feed_revision(),
|
||||
now(),
|
||||
nullif(current_setting('statehub.delete_reason', true), '')
|
||||
)
|
||||
ON CONFLICT (message_id) DO UPDATE
|
||||
SET revision = EXCLUDED.revision,
|
||||
deleted_at = EXCLUDED.deleted_at,
|
||||
reason = EXCLUDED.reason;
|
||||
RETURN OLD;
|
||||
END
|
||||
$fn$;;
|
||||
|
||||
CREATE TRIGGER agent_messages_inbox_revision
|
||||
BEFORE INSERT OR UPDATE ON agent_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION inbox_feed_stamp_revision();
|
||||
CREATE TRIGGER agent_messages_inbox_tombstone
|
||||
BEFORE DELETE ON agent_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION inbox_feed_tombstone();
|
||||
CREATE TRIGGER message_receipts_inbox_revision
|
||||
BEFORE INSERT OR UPDATE ON message_receipts
|
||||
FOR EACH ROW EXECUTE FUNCTION inbox_feed_stamp_revision();;
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"schema": "statehub-migration-compat-source-validation.v1",
|
||||
"status": "source_prepared_not_docker_executed",
|
||||
"production_mutations": false,
|
||||
"actual_database_tests_executed": false,
|
||||
"tests": {
|
||||
"passed": 6,
|
||||
"command": "/home/worsch/state-hub/.venv/bin/python -m pytest -q --tb=short test_driver.py",
|
||||
"duration_seconds": 0.64
|
||||
},
|
||||
"retained_failure": {
|
||||
"test": "test_offline_real_orm_and_migration_compilation_without_network",
|
||||
"result": "failed_on_repeat_generation",
|
||||
"cause": "SQLAlchemy independent foreign-key constraint/index object iteration order varied; fixed Python hash seed alone was insufficient",
|
||||
"fix": "Require PYTHONHASHSEED=0 and normalize only independent compiled FK clause order plus sorted compiled indexes after dependency-ordered compiled tables; isolated child compilation with socket.connect disabled now byte-compares all three artifacts",
|
||||
"attempts": 2
|
||||
},
|
||||
"scope_limits": [
|
||||
"Fake adapter tests verify containment argv/private import order/cleanup and refusal only",
|
||||
"Real PostgreSQL compatibility fixture is prepared, not executed",
|
||||
"No full application or production schema acceptance claim"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
.PHONY: start install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard dashboard-local sync check-primary primary-port-clear check check-local test test-python clean register-project register-codex-project register-mcp configure-codex bootstrap-env dev-hub edge-relay mcp-profile validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run railiance-state-hub-headroom
|
||||
|
||||
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
|
||||
PYTHON ?= python3
|
||||
HELM ?= $(shell command -v helm 2>/dev/null || if [ -x "$$HOME/.local/bin/helm" ]; then printf "%s" "$$HOME/.local/bin/helm"; else printf "%s" "helm"; fi)
|
||||
KUBECTL ?= $(shell command -v kubectl 2>/dev/null || if [ -x "$$HOME/.local/bin/kubectl" ]; then printf "%s" "$$HOME/.local/bin/kubectl"; else printf "%s" "kubectl"; fi)
|
||||
|
||||
RAILIANCE_STATE_HUB_RELEASE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_NAMESPACE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_CHART ?= deploy/railiance/apps/charts/state-hub
|
||||
RAILIANCE_STATE_HUB_VALUES ?= deploy/railiance/apps/helm/state-hub-values.yaml
|
||||
RAILIANCE_STATE_HUB_IMAGE_TAG ?= main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015
|
||||
RAILIANCE_STATE_HUB_PLATFORM_DIR ?= deploy/railiance/platform
|
||||
RAILIANCE_STATE_HUB_APP_MANIFESTS ?= deploy/railiance/apps/manifests
|
||||
# Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable.
|
||||
UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi)
|
||||
RMGR ?= $(shell command -v rmgr 2>/dev/null || if [ -x "$$HOME/repo-manager/.venv/bin/rmgr" ]; then printf "%s" "$$HOME/repo-manager/.venv/bin/rmgr"; else printf "%s" "rmgr"; fi)
|
||||
STATE_HUB_API_BASE ?= http://127.0.0.1:8000
|
||||
SYNC_PATH ?= .
|
||||
SYNC_PUSH ?= 1
|
||||
|
||||
start:
|
||||
@echo "# Normal production UI access (run in order)"
|
||||
@echo "make bridges # connect local :8000 to the railiance01 primary"
|
||||
@echo "make dashboard # verify the primary, then serve the UI on :3000"
|
||||
@echo "make sync # push file-backed records and reconcile the exact forge commit"
|
||||
@echo ""
|
||||
@echo "# Optional local MCP adapter"
|
||||
@echo "make mcp-http # local SSE adapter on :8001; not needed by the UI"
|
||||
@echo ""
|
||||
@echo "# Local fallback/development only"
|
||||
@echo "make api # local Postgres + API; conflicts with the production :8000 tunnel"
|
||||
@echo "make dashboard-local # dashboard against the deliberate local API"
|
||||
|
||||
install:
|
||||
$(UV) sync
|
||||
|
||||
dashboard/node_modules/.bin/observable: dashboard/package.json dashboard/package-lock.json
|
||||
cd dashboard && npm ci
|
||||
|
||||
dashboard-install: dashboard/node_modules/.bin/observable
|
||||
|
||||
dashboard-check: dashboard-install
|
||||
cd dashboard && npm run build
|
||||
|
||||
## Symlink the custodian CLI into ~/.local/bin so it's on PATH system-wide
|
||||
install-cli: install
|
||||
mkdir -p ~/.local/bin
|
||||
ln -sf "$(shell pwd)/.venv/bin/custodian" ~/.local/bin/custodian
|
||||
ln -sf "$(shell pwd)/.venv/bin/statehub" ~/.local/bin/statehub
|
||||
@echo "Installed: custodian → $$(readlink -f ~/.local/bin/custodian)"
|
||||
@echo "Installed: statehub → $$(readlink -f ~/.local/bin/statehub)"
|
||||
@echo "Make sure ~/.local/bin is on your PATH:"
|
||||
@echo " echo 'export PATH=\"\$$HOME/.local/bin:\$$PATH\"' >> ~/.bashrc && source ~/.bashrc"
|
||||
|
||||
db:
|
||||
$(COMPOSE) up -d postgres
|
||||
|
||||
db-tools:
|
||||
$(COMPOSE) --profile tools up -d
|
||||
|
||||
migrate:
|
||||
$(UV) run alembic upgrade head
|
||||
|
||||
seed:
|
||||
$(UV) run python scripts/seed.py
|
||||
|
||||
## Start (or restart) the MCP SSE server on :8001 — primary transport for Claude Code.
|
||||
## Remote clients (e.g. COULOMBCORE) connect via the ops-bridge tunnel (port 18001).
|
||||
## Registration: claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}'
|
||||
mcp-http:
|
||||
@fuser -k 8001/tcp 2>/dev/null && echo "Stopped running MCP server" || true
|
||||
MCP_TRANSPORT=sse MCP_PORT=8001 $(UV) run python mcp_server/server.py
|
||||
|
||||
## Require the production identity before serving the normal dashboard. This
|
||||
## prevents an accidental local `make api` from presenting an empty fallback DB
|
||||
## as the live State Hub.
|
||||
check-primary:
|
||||
@health="$$(curl -fsS --max-time 5 http://127.0.0.1:8000/state/health 2>/dev/null)" || { \
|
||||
echo "ERROR: State Hub primary is not reachable on 127.0.0.1:8000." >&2; \
|
||||
echo "Run 'make bridges' first." >&2; \
|
||||
exit 1; \
|
||||
}; \
|
||||
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
|
||||
if [ "$$identity" != "primary/railiance01" ]; then \
|
||||
echo "ERROR: 127.0.0.1:8000 is '$$identity', not the primary/railiance01 State Hub." >&2; \
|
||||
echo "Stop the local API, then run 'make bridges'. Use 'make dashboard-local' only for intentional local development." >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## Refuse to start the production tunnel when a non-primary API already owns
|
||||
## port 8000. With no listener, bridge is free to establish the tunnel.
|
||||
primary-port-clear:
|
||||
@if health="$$(curl -fsS --max-time 2 http://127.0.0.1:8000/state/health 2>/dev/null)"; then \
|
||||
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
|
||||
if [ "$$identity" != "primary/railiance01" ]; then \
|
||||
echo "ERROR: port 8000 is occupied by '$$identity'." >&2; \
|
||||
echo "Stop the local 'make api' process before starting the production tunnel." >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
dashboard: check-primary
|
||||
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
|
||||
$(MAKE) dashboard-install
|
||||
cd dashboard && npm run dev
|
||||
|
||||
## Deliberate local-development dashboard; bypasses the production identity gate.
|
||||
dashboard-local:
|
||||
@echo "WARNING: serving the dashboard against the local/fallback API on :8000."
|
||||
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
|
||||
$(MAKE) dashboard-install
|
||||
cd dashboard && npm run dev
|
||||
|
||||
## Fast work-record path. Repo Manager assigns only missing deterministic IDs,
|
||||
## verifies the primary/railliance01 bridge, and asks central to derive the
|
||||
## exact pushed Forgejo commit in one transactional request.
|
||||
sync:
|
||||
$(RMGR) sync --path "$(SYNC_PATH)" --api-base "$(STATE_HUB_API_BASE)" $(if $(filter 1 true yes,$(SYNC_PUSH)),--push,)
|
||||
|
||||
check: check-primary
|
||||
@echo "State Hub primary/railiance01 is healthy."
|
||||
|
||||
check-local:
|
||||
curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool
|
||||
|
||||
# CUST-WP-0067-T09. The chart ships a copy of the-custodian canon allowed-values
|
||||
# because a container has no checkout to read it from. Regenerate after any
|
||||
# canon vocabulary change; the -check target fails on drift so a release cannot
|
||||
# validate classification against a stale vocabulary.
|
||||
sync-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py
|
||||
|
||||
check-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py --check
|
||||
|
||||
# STATE-WP-0091: read-only surge-headroom check. Does not start Helm.
|
||||
# Live: make railiance-state-hub-headroom
|
||||
# Fixtures: make railiance-state-hub-headroom HEADROOM_FIXTURE=tests/fixtures/release_headroom/rev63-105m.json
|
||||
HEADROOM_FIXTURE ?=
|
||||
railiance-state-hub-headroom:
|
||||
@if [ -n "$(HEADROOM_FIXTURE)" ]; then \
|
||||
$(PYTHON) scripts/release_headroom_preflight.py --fixture "$(HEADROOM_FIXTURE)"; \
|
||||
else \
|
||||
$(PYTHON) scripts/release_headroom_preflight.py --kubectl "$(KUBECTL)"; \
|
||||
fi
|
||||
|
||||
railiance-state-hub-render: check-classification-allowed
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG)
|
||||
|
||||
railiance-state-hub-client-dry-run:
|
||||
@set -e; \
|
||||
tmpdir="$$(mktemp -d)"; \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"
|
||||
|
||||
railiance-state-hub-server-dry-run:
|
||||
@set -e; \
|
||||
tmpdir="$$(mktemp -d)"; \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
|
||||
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
if $(KUBECTL) get namespace $(RAILIANCE_STATE_HUB_NAMESPACE) >/dev/null 2>&1; then \
|
||||
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=server -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
|
||||
else \
|
||||
echo "Namespace $(RAILIANCE_STATE_HUB_NAMESPACE) does not exist; validating namespaced app manifests with client dry-run."; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
|
||||
fi
|
||||
|
||||
test: test-python dashboard-check
|
||||
|
||||
test-python:
|
||||
TEST_DATABASE_URL=postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test \
|
||||
$(UV) run pytest -x -q
|
||||
|
||||
## Benchmark /state/summary revision cache (API must be running on :8000)
|
||||
benchmark-summary-cache:
|
||||
$(UV) run python scripts/benchmark_summary_cache.py
|
||||
|
||||
## ops-bridge managed tunnels
|
||||
## Requires ops-bridge: bridge is at /home/worsch/.local/bin/bridge
|
||||
tunnels-up: primary-port-clear
|
||||
bridge up
|
||||
|
||||
tunnels-status:
|
||||
bridge status
|
||||
|
||||
## End-to-end check: verifies SSH process alive + remote port listening on COULOMBCORE.
|
||||
## Exits non-zero if any tunnel is not fully operational.
|
||||
tunnels-check:
|
||||
bridge check
|
||||
|
||||
## Ensure all ops-bridge tunnels are up and healthy.
|
||||
## Brings up any stopped/stale tunnels, shows final status, exits non-zero if anything is still down.
|
||||
bridges: primary-port-clear
|
||||
@echo "==> Bringing up all tunnels..."
|
||||
bridge up
|
||||
@echo ""
|
||||
@echo "==> Tunnel status:"
|
||||
bridge status
|
||||
@echo ""
|
||||
@echo "==> Checking tunnel health..."
|
||||
bridge check
|
||||
|
||||
## Start (or restart) the LOCAL FALLBACK backend — db + migrate + uvicorn.
|
||||
## This replaces anything on :8000, including the production State Hub tunnel.
|
||||
api: db
|
||||
@echo "WARNING: starting the local fallback API; this is not the railiance01 primary."
|
||||
@echo "Waiting for postgres..."; \
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do \
|
||||
nc -z 127.0.0.1 5432 2>/dev/null && break; \
|
||||
sleep 1; \
|
||||
done
|
||||
$(MAKE) migrate
|
||||
@fuser -k 8000/tcp 2>/dev/null && echo "Stopped running API" || true
|
||||
$(UV) run uvicorn api.main:app --reload --reload-dir api --reload-dir mcp_server --reload-dir task_flow_engine --host 127.0.0.1 --port 8000
|
||||
|
||||
## Register a project (Claude Code): make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance
|
||||
register-project:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)"
|
||||
|
||||
## Register a Codex project (AGENTS.md + HTTP API): make register-codex-project DOMAIN=capabilities PROJECT_PATH=/home/worsch/my-repo
|
||||
register-codex-project:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-codex-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" --codex
|
||||
|
||||
## Register State Hub MCP for Claude Code. Optional: make register-mcp MCP_URL=http://127.0.0.1:18001/sse
|
||||
register-mcp:
|
||||
scripts/register-mcp.sh \
|
||||
$(if $(MCP_URL),--url "$(MCP_URL)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Configure Codex workspace-write networking for local State Hub access.
|
||||
configure-codex:
|
||||
scripts/configure-codex.sh \
|
||||
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(WITH_MCP),--with-mcp,) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Bootstrap a new operator/collaborator environment. Optional: make bootstrap-env ARGS="--install-missing"
|
||||
bootstrap-env:
|
||||
scripts/bootstrap-env.sh $(ARGS)
|
||||
|
||||
## Files-first local dev hub (CUST-WP-0054-T07): postgres + migrate + seed + API + repo registration.
|
||||
## Optional: WITH_EDGE=1 WITH_MCP=1 make dev-hub
|
||||
## Fleet tunnel when reachable: make dev-hub PROFILE=fleet
|
||||
dev-hub:
|
||||
bash scripts/dev_hub_up.sh $(if $(PROFILE),$(PROFILE),dev)
|
||||
|
||||
## Local edge relay for offline write buffering (STATE-WP-0068).
|
||||
edge-relay:
|
||||
@fuser -k 18080/tcp 2>/dev/null && echo "Stopped running edge relay" || true
|
||||
STATEHUB_UPSTREAM_URL=$${STATEHUB_UPSTREAM_URL:-http://127.0.0.1:8000} \
|
||||
$(UV) run uvicorn api.edge.relay:app --host 127.0.0.1 --port 18080
|
||||
|
||||
## Switch MCP/API profile: make mcp-profile PROFILE=dev|fleet|status
|
||||
mcp-profile:
|
||||
@test -n "$(PROFILE)" || (echo "ERROR: PROFILE is required (dev|fleet|status)."; exit 1)
|
||||
bash scripts/mcp_hub_profile.sh "$(PROFILE)"
|
||||
|
||||
## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra
|
||||
add-repo:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||
@test -n "$(REPO_PATH)" || (echo "ERROR: REPO_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(REPO_PATH)" --additional
|
||||
|
||||
## Create a new domain: make add-domain DOMAIN=my_domain NAME="My Domain"
|
||||
add-domain:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required (slug)."; exit 1)
|
||||
@test -n "$(NAME)" || (echo "ERROR: NAME is required (display name)."; exit 1)
|
||||
curl -sf -X POST http://127.0.0.1:8000/domains/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"slug\": \"$(DOMAIN)\", \"name\": \"$(NAME)\"}" | python3 -m json.tool
|
||||
|
||||
## Rename a domain: make rename-domain DOMAIN=old_slug NEW_SLUG=new_slug NEW_NAME="New Name"
|
||||
rename-domain:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN (old slug) is required."; exit 1)
|
||||
@test -n "$(NEW_SLUG)" || (echo "ERROR: NEW_SLUG is required."; exit 1)
|
||||
@test -n "$(NEW_NAME)" || (echo "ERROR: NEW_NAME is required."; exit 1)
|
||||
curl -sf -X PATCH http://127.0.0.1:8000/domains/$(DOMAIN)/rename \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_slug\": \"$(NEW_SLUG)\", \"new_name\": \"$(NEW_NAME)\"}" | python3 -m json.tool
|
||||
|
||||
## Register this machine's local path for a repo: make register-path REPO=marki-docx PATH=/home/tegwick/marki-docx
|
||||
register-path:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
|
||||
@test -n "$(PATH)" || (echo "ERROR: PATH is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
|
||||
curl -sf -X POST "http://127.0.0.1:8000/repos/$(REPO)/paths" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"host\": \"$$(hostname)\", \"path\": \"$(PATH)\"}" | python3 -m json.tool
|
||||
|
||||
## List repos for a domain: make list-repos DOMAIN=railiance
|
||||
list-repos:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||
curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool
|
||||
|
||||
## Ingest SBOM data for a repo (all mechanisms: lockfiles + ansible + sbom-tools.yaml).
|
||||
## Auto-detect all sources: make ingest-sbom REPO=the-custodian REPO_PATH=/home/worsch/the-custodian
|
||||
## Single lockfile (explicit): make ingest-sbom REPO=the-custodian LOCKFILE=/path/to/uv.lock
|
||||
## Dry-run (no submit): make ingest-sbom REPO=the-custodian REPO_PATH=... DRY_RUN=1
|
||||
## Tip: run capture-tools first for repos with system-level tool dependencies.
|
||||
ingest-sbom:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_sbom.py --repo "$(REPO)" \
|
||||
$(if $(LOCKFILE),--lockfile "$(LOCKFILE)") \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Ingest capability declarations from SCOPE.md into the catalog.
|
||||
## Usage: make ingest-capabilities REPO=the-custodian [REPO_PATH=/home/worsch/the-custodian]
|
||||
## Or: make ingest-capabilities-all
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
ingest-capabilities:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_capabilities.py --repo "$(REPO)" \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
ingest-capabilities-all:
|
||||
$(UV) run python scripts/ingest_capabilities.py --all \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Check Repository Definition of Integrated (DoI) criteria for a repo.
|
||||
## Usage: make check-doi REPO=llm-connect
|
||||
## Or: make check-doi-all
|
||||
## Add JSON=1 for machine-readable output.
|
||||
check-doi:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/check_doi.py --repo "$(REPO)" $(if $(JSON),--json)
|
||||
|
||||
check-doi-all:
|
||||
$(UV) run python scripts/check_doi.py --all $(if $(JSON),--json)
|
||||
|
||||
## Ingest tpsc.yaml service declarations from a repo into the TPSC catalog.
|
||||
## Usage: make ingest-tpsc REPO=llm-connect
|
||||
## Or: make ingest-tpsc-all
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
ingest-tpsc:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_tpsc.py --repo "$(REPO)" \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
ingest-tpsc-all:
|
||||
$(UV) run python scripts/ingest_tpsc.py --all \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Run SBOM capture agent for a repo — generates/updates sbom-tools.yaml.
|
||||
## Usage: make capture-tools REPO=railiance-infra [REPO_PATH=/home/worsch/railiance-infra]
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
capture-tools:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/capture_sbom_tools.py --repo "$(REPO)" \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian]
|
||||
validate-adr:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO=<path> [DOMAIN=<slug>]"; exit 1)
|
||||
$(UV) run python scripts/validate_repo_adr.py "$(REPO)" $(if $(DOMAIN),--domain "$(DOMAIN)",)
|
||||
|
||||
## Consistency exit contract:
|
||||
## - Direct scripts/consistency_check.py: 0 clean, 2 warnings-only, 1 failures.
|
||||
## - Agent/operator Make wrappers below normalize warning-only 2 to shell success
|
||||
## while preserving visible WARN output and keeping real failures non-zero.
|
||||
## Check a single repo for ADR-001 consistency: make check-consistency REPO=the-custodian [REPO_PATH=/override]
|
||||
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
|
||||
check-consistency:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make check-consistency REPO=<slug>"; exit 1)
|
||||
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check and auto-fix a single repo: make fix-consistency REPO=the-custodian [REPO_PATH=/override]
|
||||
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
|
||||
fix-consistency:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make fix-consistency REPO=<slug>"; exit 1)
|
||||
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" --fix \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Normalize workplan frontmatter and task status literals in attached repos.
|
||||
## Usage: make normalize-attached-workplans REPO=artifact-store
|
||||
## make normalize-attached-workplans DIRTY=1
|
||||
normalize-attached-workplans:
|
||||
$(UV) run python scripts/normalize_attached_repo_workplans.py \
|
||||
$(if $(REPO),--repo "$(REPO)",) \
|
||||
$(if $(DIRTY),--dirty,) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
|
||||
|
||||
## Regenerate AGENTS.md / CLAUDE.md / .claude/rules from templates.
|
||||
## Usage: make update-agent-instructions REPO=artifact-store
|
||||
## make update-agent-instructions DIRTY=1
|
||||
update-agent-instructions:
|
||||
$(UV) run python scripts/update_agent_instruction_files.py \
|
||||
$(if $(REPO),--repo "$(REPO)",) \
|
||||
$(if $(DIRTY),--dirty,)
|
||||
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
|
||||
|
||||
## Reconcile measured token sources against State Hub.
|
||||
## Usage: make token-reconcile [SINCE=2026-05-19] [APPLY=1] [ZERO_FALLBACKS=1]
|
||||
token-reconcile:
|
||||
$(PYTHON) scripts/token_reconcile.py \
|
||||
$(if $(SINCE),--since "$(SINCE)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
|
||||
$(if $(CLAUDE_HOME),--claude-home "$(CLAUDE_HOME)",) \
|
||||
$(if $(APPLY),--apply,) \
|
||||
$(if $(ZERO_FALLBACKS),--zero-superseded-fallbacks,)
|
||||
|
||||
## Pull then fix: single repo or all repos if REPO omitted
|
||||
## make fix-consistency-remote — smart pull+fix all repos that need it
|
||||
## make fix-consistency-remote REPO=slug — pull+fix one repo
|
||||
fix-consistency-remote:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
$(if $(REPO),--repo "$(REPO)",--all) \
|
||||
--remote \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(NO_WRITEBACK),--no-writeback,); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Infer repo slug from git remote URL and check: make check-consistency-here [REPO_PATH=/path/to/repo]
|
||||
## Omit REPO_PATH to use the Python script's CWD (i.e. pass an empty --here flag).
|
||||
check-consistency-here:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Infer repo slug from git remote URL and fix: make fix-consistency-here [REPO_PATH=/path/to/repo]
|
||||
fix-consistency-here:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
|
||||
--fix \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check all registered repos for ADR-001 consistency
|
||||
check-consistency-all:
|
||||
$(UV) run python scripts/consistency_check.py --all $(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check and auto-fix all registered repos
|
||||
fix-consistency-all:
|
||||
$(UV) run python scripts/consistency_check.py --all --fix $(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Cancel open tasks belonging to completed/archived workstreams.
|
||||
## Safe to run at any time; also suitable for a daily cron job.
|
||||
## Cron example: 0 3 * * * cd ~/state-hub && make cleanup-stale
|
||||
cleanup-stale:
|
||||
$(UV) run python scripts/cleanup_stale_tasks.py
|
||||
|
||||
## Install custodian post-commit sync hook into one repo: make install-hooks REPO=marki-docx
|
||||
install-hooks:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make install-hooks REPO=<slug>"; exit 1)
|
||||
bash scripts/install_hooks.sh --repo "$(REPO)"
|
||||
|
||||
## Install custodian post-commit sync hook into all active registered repos
|
||||
install-hooks-all:
|
||||
bash scripts/install_hooks.sh --all
|
||||
|
||||
## Remove custodian post-commit sync hook from one repo: make remove-hooks REPO=marki-docx
|
||||
remove-hooks:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make remove-hooks REPO=<slug>"; exit 1)
|
||||
bash scripts/install_hooks.sh --repo "$(REPO)" --remove
|
||||
|
||||
## Compare Gitea coulomb org repos against state-hub registered repos
|
||||
## Requires GITEA_TOKEN in env or .env: make gitea-inventory GITEA_TOKEN=<token>
|
||||
gitea-inventory:
|
||||
$(UV) run python scripts/gitea_inventory.py $(if $(JSON),--json)
|
||||
|
||||
## Register/update one repo from .repo-classification.yaml:
|
||||
## make register-from-classification REPO=state-hub
|
||||
## make register-from-classification PATH=/path/to/repo
|
||||
## Optional: DRY_RUN=1
|
||||
register-from-classification:
|
||||
@test -n "$(REPO)" -o -n "$(PATH)" || (echo "ERROR: REPO or PATH is required."; exit 1)
|
||||
$(UV) run python scripts/register_from_classification.py \
|
||||
$(if $(PATH),--repo-path "$(PATH)",--slug "$(REPO)") \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Bulk register/update all active repos with accessible local paths
|
||||
register-from-classification-all:
|
||||
$(UV) run python scripts/register_from_classification.py --bulk \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
clean:
|
||||
$(COMPOSE) down -v
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v2
|
||||
name: state-hub
|
||||
description: State Hub API service for private Railiance operation
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "main-51bc70d"
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Production values for the State Hub Railiance chart handoff.
|
||||
# Non-secret values only. DATABASE_URL comes from the Secret `state-hub-env`.
|
||||
|
||||
namespace:
|
||||
create: false
|
||||
|
||||
image:
|
||||
repository: forgejo.coulomb.social/coulomb/state-hub
|
||||
tag: "main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
# MCP layer on central (CUST-WP-0067-T08). Enabled together with the image tag
|
||||
# above: MCP_HOST landed in main-6c1262e, and enabling this on an older image
|
||||
# would deploy a pod that binds loopback and never becomes reachable.
|
||||
# ClusterIP only — no ingress, same reason the API's is disabled.
|
||||
mcp:
|
||||
enabled: true
|
||||
resources:
|
||||
requests:
|
||||
# 2026-09-10: 1,078 observed 5-minute samples, p99 2.35m, peak 30.45m.
|
||||
# Preserve the 500m limit for bursts; free 40m for the API rollout surge.
|
||||
cpu: 10m
|
||||
|
||||
config:
|
||||
# This deployment is the authoritative hub. Callers verify this rather than
|
||||
# trusting that whatever answered on a port is central (CUST-WP-0067-T03).
|
||||
instanceRole: primary
|
||||
instanceLabel: railiance01
|
||||
sbomNexusUrl: "http://sbom-nexus.sbom-nexus.svc.cluster.local:8010"
|
||||
# Reversible T04 read cutover; set back to `legacy` to roll back.
|
||||
sbomNexusReadMode: nexus
|
||||
# Reversible T04 write cutover; set back to `legacy` to roll back future writes.
|
||||
sbomNexusWriteMode: nexus
|
||||
|
||||
resources:
|
||||
# Restored to the chart's 250m on 2026-09-21. The earlier 100m was a workaround
|
||||
# for a node with <250m unallocated; ~830m of Knative/Kourier requests were
|
||||
# released that day (the-custodian docs/kubernetes-change-gate-decision.md).
|
||||
# State Hub idles near 5m but bursts past 400m under concurrent agent load.
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
sweep:
|
||||
# RMGR-WP-0005-T11: disabled while railiance01 checkouts still target the
|
||||
# stale gitea-remote lineage. Re-enable only after the governed remote
|
||||
# reconciliation and registrar preflight are complete.
|
||||
enabled: false
|
||||
# Present in the live release but previously missing here, so deploying from
|
||||
# this file alone silently dropped it. Inert while enabled is false.
|
||||
hostname: 239.62.205.92.host.secureserver.net
|
||||
hostPath: /home/tegwick
|
||||
sshHostPath: /home/tegwick/.ssh
|
||||
|
||||
# Forge read credential (STATE-WP-0084-T02). Coordinates only — the token lives
|
||||
# in OpenBao at platform/workloads/state-hub/forge-derivation and reaches the
|
||||
# pod through Kubernetes auth, never through this file or a Secret.
|
||||
forgeRead:
|
||||
enabled: true
|
||||
|
||||
# RPF-WP-0035-T04: dedicated ESO signing key, API-only explicit reference.
|
||||
renamePreflight:
|
||||
enabled: true
|
||||
secretName: state-hub-rename-preflight
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"schema": "statehub.cluster-historical-receiving-delivered.v1",
|
||||
"status": "verified",
|
||||
"proposal_id": "STATE-WP-0095-T06-cluster-frozen-receiving-r1",
|
||||
"approved_revision": 4,
|
||||
"approved_digest_sha256": "sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32",
|
||||
"approval_decision_id": "e3798419-8aa9-5425-8c08-20df6bd82a19",
|
||||
"source_revision": "51bc70d1ff8f25450a8650d8e50caf053093a02e",
|
||||
"readiness_source_revision": "086d79a2736e34b916bd7de3c8a65e2ea38a3d64",
|
||||
"helm_revision": 76,
|
||||
"image_reference": "forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015",
|
||||
"expected_schema": "c4d5e6f7a8b9",
|
||||
"health": {
|
||||
"status": "ok",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"schema": {
|
||||
"status": "ok",
|
||||
"applied": "c4d5e6f7a8b9",
|
||||
"expected": "c4d5e6f7a8b9"
|
||||
}
|
||||
},
|
||||
"pods_ready": true,
|
||||
"historical_module_sha256": "sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2",
|
||||
"historical_preservation_verified": true,
|
||||
"parents": 3,
|
||||
"tasks": 15,
|
||||
"full_historical_rows_equal": true,
|
||||
"native_rollback_probes": {
|
||||
"foreign-holder": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"missing-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-parent": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"preserve": {
|
||||
"full_historical_rows_equal": true,
|
||||
"full_historical_rows_restored": true,
|
||||
"native_outcome_status": "applied",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
}
|
||||
},
|
||||
"native_probe_commits": 0,
|
||||
"fresh_transaction_full_rows_equal": true,
|
||||
"source_receiving_gate_delivered": true,
|
||||
"normal_cluster_publication_verified": false,
|
||||
"registrar_admission_or_cluster_publication_not_inferred": true,
|
||||
"authority_limits": [
|
||||
"Rollback probe SQL effects rolled back; foreign-holder case is historical-binding refusal, not separate global holder branch.",
|
||||
"Actual historical native receiving is verified; owner-native TLS/private admission remains unresolved.",
|
||||
"Migration schema remains additive; Helm rollback does not downgrade schema or stop an active hook Job."
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/approval-decision.json",
|
||||
"sha256": "sha256:b51554c004daa882f3709f67bdae7f2e939d611120383e61be04ec6969878134"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/helm-return.json",
|
||||
"sha256": "sha256:61727deacf22d37da2d28cbaf04f5a3ddee0f9ff7571c77f1e85dba97963b572"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/delivered-runtime.json",
|
||||
"sha256": "sha256:89681066e8fe139456163ebfafa3efe77ffae8534ea167219230b0a74367f129"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/native-refusal-probes.json",
|
||||
"sha256": "sha256:673ccef965c820704dc310a1bf7832b583c30c3edfeb814528cf892555199172"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/before-full-row-hashes.json",
|
||||
"sha256": "sha256:75cdd87eef79059ff3f484ed24ca69e285899cdf8d851cc4e472653c417eb0ee"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/after-full-row-hashes.json",
|
||||
"sha256": "sha256:2156aad18f190384838c27e792706a156a7423391408afbc6107e6c28c5872d8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
---
|
||||
id: STATE-WP-0095
|
||||
type: workplan
|
||||
title: "Preserve exact closed historical source bindings in Forge receiving"
|
||||
domain: infotech
|
||||
repo: state-hub
|
||||
status: active
|
||||
owner: codex
|
||||
created: "2026-10-04"
|
||||
updated: "2026-10-17"
|
||||
flavor: implementation
|
||||
depends_on: []
|
||||
state_hub_workstream_id: "79579572-8120-5d3b-aca5-dd4aff60d058"
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Normal Forge receiving must preserve the seven bound Activity legacy source
|
||||
views and the Custodian retired ADHOC source without new identities, source
|
||||
rewrites, task movement, deletion, relabeling or unretirement. This implements
|
||||
receiving support before RMGR-WP-0019 registrar admission. CUST-WP-0077 owns
|
||||
cross-owner coordination; deployment and replay remain explicit owner returns.
|
||||
|
||||
## Verify source bindings and shared views
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7"
|
||||
```
|
||||
|
||||
Reviewed seven exact Activity files: four existing workplans and 64 UUID-bound
|
||||
closed tasks. The archived custodian-WP-0003 parent and three split_from views
|
||||
share one existing parent UUID; every repeated task binding/content agrees.
|
||||
Custodian ADHOC-2026-08-25-retired retains parent
|
||||
1c53d3db-6633-5e2d-987f-10706d73be9e and task
|
||||
d8af3e6b-26ae-580c-bbeb-caa8ad419bec. No identity migration is authorized.
|
||||
|
||||
## Implement preservation receiving and global collision refusal
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ebb24194-e4cc-5130-aaa6-663f2ea2c822"
|
||||
```
|
||||
|
||||
Prepared exact source digest manifest, existing-parent/task verification,
|
||||
immutable receiving and UUID-based historical diff. Ordinary canonical sources
|
||||
continue deriving their identities. Missing/changed/symlinked source, absent
|
||||
bound rows or nonterminal hub histories refuse. Check task UUID holders globally,
|
||||
including retired and acknowledged-stale parent workplans, before INSERT.
|
||||
|
||||
Offline preview tests and snapshot proof are evidence of source behavior only;
|
||||
finish after applying reviewed source and its focused tests to the owning repo.
|
||||
|
||||
## Deliver receiving through normal deployment and verify owner return
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T03
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T02]
|
||||
state_hub_task_id: "e8566341-cf6e-5b9c-ad80-8564d14cd52a"
|
||||
decision_id: "36772026-f43d-4331-b466-5f4b1afe4eca"
|
||||
```
|
||||
|
||||
No direct live code patch. This task is a deployment return, not policy authority
|
||||
or permission to resolve identity decisions. Exact receiving fixture acceptance
|
||||
and global collision rejection must hold in the delivered version.
|
||||
|
||||
## Verify normal historical replay preserves existing identities
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T04
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T03, RMGR-WP-0019-T03]
|
||||
state_hub_task_id: "3bbb311f-194e-5862-8aed-358a580f22b4"
|
||||
```
|
||||
|
||||
Verify Activity four historical parent UUIDs/64 task UUIDs and Custodian retired
|
||||
parent/task unchanged; no historical row creation, movement, deletion,
|
||||
relabeling or unretirement. Unrelated stale projections remain separate decisions.
|
||||
|
||||
2026-10-05: Source exact-set retirement support now exists. Repository projection
|
||||
reconcile accepts `expected_retirement_uuids`; that set must equal the stale
|
||||
projections, and any task or dependency edge on a stale row refuses the call.
|
||||
Live apply still needs a reviewed rollout. On 2026-10-05 the founder accepted
|
||||
review `review-1ece4f3e-d0e3-4fd6-abc8-272f1b1876f4`, which names only the three
|
||||
empty projections and preserves parent `b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`.
|
||||
That acceptance is not a live retirement and does not resolve
|
||||
`91b7c633-a66f-5d43-9f4b-6565aefb9356`.
|
||||
|
||||
## Offline evidence, 2026-10-04
|
||||
|
||||
Prepared receiver suite: Forge projection regression plus historical preservation,
|
||||
missing/changed source, terminality, immutable UUID diff, and global holder query.
|
||||
Real-source offline snapshot proof preserves four Activity parents/64 tasks and
|
||||
one Custodian parent/task without mutations; no API writes or deployment occurred.
|
||||
Exact original bindings are maintained in api/services/historical_projection.py.
|
||||
|
||||
2026-10-04 actual owning-source validation: 155 focused receiver/qualifier/planner
|
||||
cases passed; the independent offline current-source/current-hub-snapshot proof
|
||||
retained five existing parents and 65 task UUIDs, no mutations, clean historical
|
||||
diff. T02 is source-complete; T03/T04 remain wait for reviewed deployment and
|
||||
normal registrar/replay receiving, respectively. Evidence: docs/evidence/2026-10-04-historical-receiving-source.json.
|
||||
|
||||
|
||||
## Verified receiving rollout and remaining replay gate, 2026-10-04
|
||||
|
||||
T03 is done on actual founder approval of proposal
|
||||
STATE-WP-0095-T03-historical-receiving revision 1, SHA256
|
||||
`d6789359b3fe7988a6201fff6f367cb46948aa197e1cc0ca66c31f45e1ea9981`,
|
||||
approval receipt reference `user_reply:call_o87uEfFs3AtXfxO5YqUnSpcg:0`.
|
||||
The normal owner Helm path delivered `main-5dd3df9`, commit
|
||||
`5dd3df9c69eb564af10ea5ea01e349786212501a`, revision 72; API and MCP are Ready,
|
||||
primary/railiance01 health and schema a0b1c2d3e4f5 verified, running source
|
||||
hashes match. Rollback remains revision 71; no receipt-table downgrade is needed.
|
||||
Actual historical preservation and global task-holder refusal were verified in
|
||||
the delivered version. Durable receipt:
|
||||
[verified historical receiving rollout](../docs/evidence/2026-10-04-historical-receiving-rollout.json).
|
||||
|
||||
Custodian immutable replay applied while preserving the retired historical
|
||||
parent/task UUIDs. Activity immutable replay retained all four historical parent
|
||||
and 64 task UUIDs and historical row contents, but refused transactionally on
|
||||
three unrelated stale projections: custodian-wp-0003 (archived),
|
||||
custodian-wp-0003a (done), custodian-wp-0003b (done). No retirement acknowledgement
|
||||
was issued and that refusal is not represented as normal replay success.
|
||||
|
||||
Registrar source `c41d36c2` has 38 actual passing tests and Repo Manager's own
|
||||
normal sync succeeded; registrar admission does not authorize retiring Activity
|
||||
projections. T04 therefore remains wait, retaining RMGR-WP-0019-T03 and awaiting
|
||||
Activity Core's precise reviewed disposition of those three stale rows. No
|
||||
rename, identity migration, new paid native trial or further live action is
|
||||
included in this source status update.
|
||||
|
||||
## Activity Core return, 2026-10-06
|
||||
|
||||
Activity Core returned the exact disposition (message
|
||||
`cf2d5847-8b3f-4653-9d9f-d5fcfbe3f1e4`, ACTIVITY-WP-0045, activity-core commit
|
||||
`941fac0`). `expected_retirement_uuids`:
|
||||
`683724d9-4249-5e0e-88e0-7baff4330d58` (custodian-wp-0003),
|
||||
`644017c7-c5f2-5a64-81c3-d8ee806f8251` (custodian-wp-0003a),
|
||||
`a5806df9-5b9a-50da-81c1-7e606076bed9` (custodian-wp-0003b). Each has zero
|
||||
tasks and zero dependency edges. None is preserved parent
|
||||
`b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`. Repo Manager independently verified the
|
||||
set (message `916841f6`, repo-manager commit `498682c`). Nothing was retired.
|
||||
T04 now waits on decision `91b7c633` and a reviewed live rollout. After that,
|
||||
Repo Manager re-runs the Activity owner sync and closes RMGR-WP-0019-T03.
|
||||
|
||||
## Retirement rollout and T04 verification, 2026-10-06
|
||||
|
||||
The founder resolved decision `91b7c633` (retire exactly the three projections)
|
||||
and approved the rollout. The founder ran the normal Helm upgrade to
|
||||
`main-968a811` (revision 74; rollback revision 73, `main-fad9b3f`). API and MCP
|
||||
are Ready, health is ok on primary/railiance01, and schema `b3c4d5e6f7a8` is
|
||||
unchanged; no migrations. The activity-core reconcile at commit
|
||||
`67a92d2ff8e033baf2c36998d24187fe83edec84` with `expected_retirement_uuids` set
|
||||
to the three UUIDs applied: 0 created, 3 retired (tombstoned with an
|
||||
`@retired-20261006` slug; UUIDs and statuses kept), 0 refused, 0 tasks
|
||||
cancelled. All 7 bound Activity source files still match their digests. The four
|
||||
historical parents and 64 task UUIDs are present under their bound parents with
|
||||
unchanged fields. The Custodian retired parent and task are unchanged. Activity
|
||||
had 54 workplans and 288 tasks before and after. Evidence:
|
||||
[activity stale retirement rollout](../docs/evidence/2026-10-06-activity-stale-retirement-rollout.json).
|
||||
Repo Manager's re-run of the Activity owner sync (RMGR-WP-0019-T03) and
|
||||
ACTIVITY-WP-0045-T02 remain with their owners.
|
||||
|
||||
|
||||
## Cluster exact historical receiving successor, 2026-10-17
|
||||
|
||||
This scoped extension retains the completed Activity/Custodian outcomes above.
|
||||
Three frozen cluster archives contain three existing parent UUIDs and 15 existing
|
||||
closed task UUIDs. Extend the existing exact historical receiving path before
|
||||
registrar admission; never derive replacement identities or modify those
|
||||
sources. Cross-owner observation attaches to existing SHR-WP-0001, with
|
||||
RMGR-WP-0019 as obligor/beneficiary and NK-WP-0049-T35 benefiting indirectly.
|
||||
The local derived case is waiting; no live worker injection is authorized.
|
||||
|
||||
## Prepare exact cluster source preservation and refusal cases
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T05
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "339b9a37-181c-5599-9a89-0393e199bd4b"
|
||||
```
|
||||
|
||||
Prepared the same full-file SHA256 and 15 task binding manifest as Repo Manager.
|
||||
Candidate receiving tests pass 26: existing rows remain entirely unchanged;
|
||||
missing/foreign/nonterminal rows and unknown, changed, missing or symlinked
|
||||
source refuse. Read-only public metadata confirms all 18 existing rows have the
|
||||
expected ownership and terminal status. These are source and metadata checks,
|
||||
not delivered receiving or a committed native trial.
|
||||
|
||||
## Deliver reviewed cluster receiving successor before registrar admission
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T06
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T05]
|
||||
needs_human: false
|
||||
intervention_note: "Exact revision 4 approved and verified; no pending rollout intervention."
|
||||
state_hub_task_id: "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4"
|
||||
```
|
||||
|
||||
Completed 2026-10-17 on the founder's approval of exact revision 4 of stable
|
||||
proposal STATE-WP-0095-T06-cluster-frozen-receiving-r1 (decision
|
||||
e3798419-8aa9-5425-8c08-20df6bd82a19; approved digest
|
||||
sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32).
|
||||
The ordinary owner Helm path delivered revision 76, runtime source 51bc70d and
|
||||
exact image manifest sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015.
|
||||
API/MCP Ready and primary/railiance01 health report expected schema
|
||||
c4d5e6f7a8b9; delivered historical module sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2 matches.
|
||||
All three parent and 15 task full DB row hashes/columns/UUIDs remained unchanged.
|
||||
Five actual native probes passed: preserve, actual foreign-parent holder move,
|
||||
nonterminal task, nonterminal parent and missing task. Every probe rolled back;
|
||||
a fresh transaction confirmed complete row restoration. The foreign-holder
|
||||
case precisely verified historical binding refusal, not the separate global
|
||||
UUID-holder creation branch. No committed probe writes were made.
|
||||
|
||||
Evidence: [verified receiving rollout](../docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json).
|
||||
This actual receiving return satisfies the prerequisite of RMGR-WP-0019-T05.
|
||||
Normal cluster publication and its immutable-source/row return remain T07;
|
||||
source readiness and this deployment do not count that later acceptance twice.
|
||||
The additive inbox migration is retained; Helm rollback does not downgrade
|
||||
schema or stop an active migration Job. Original failures and exact-UID recovery
|
||||
limits remain in the referenced approved packet. No TLS recovery/native private
|
||||
admission or new autonomy/cost grant is inferred.
|
||||
|
||||
## Verify ordinary cluster receiving and immutable historical rows
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T07
|
||||
status: wait
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T06, RMGR-WP-0019-T06]
|
||||
state_hub_task_id: "cfd1ea99-e75b-542e-a434-acefb62f3c7d"
|
||||
```
|
||||
|
||||
Capture normal cluster owner publication/receiving receipts and compare entire
|
||||
existing row snapshots and exact source bytes. No creation, movement, relabel,
|
||||
unretirement or retirement is authorized for these histories. A rollback first
|
||||
fences cluster sync and restores registrar refusal before removing receiving
|
||||
support; it never rewrites archived IDs or UUIDs. This work does not establish
|
||||
original TLS recovery or owner-native admission.
|
||||
|
||||
## Fix policy-compatible Secret-template server dry-run preparation
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T08
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2dd9b04f-2250-5a8a-b5bc-c8a009872858"
|
||||
```
|
||||
|
||||
Prepared the narrow Makefile successor after the existing readiness command
|
||||
refused its first Secret-template phase. Ordinary client-side apply adds the
|
||||
last-applied annotation, correctly denied by the existing Platform
|
||||
reject-secret-last-applied policy. Only the two Secret-template server
|
||||
dry-run requests now use server-side apply with dry-run; no force-conflicts,
|
||||
Secret value change, policy relaxation or actual Secret write is introduced.
|
||||
Command-fixture tests verify both requests and preserve other dry-run phases.
|
||||
Evidence and original refusal remain in the Custodian cluster historical
|
||||
publication packet, revisions/r2 and r3-readiness. This is completed source
|
||||
preparation; T06 separately records the verified actual rollout. The runtime image
|
||||
continues to be main-51bc70d; Makefile/workplan changes do not create a new
|
||||
application image or native acceptance.
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
.PHONY: start install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard dashboard-local sync check-primary primary-port-clear check check-local test test-python clean register-project register-codex-project register-mcp configure-codex bootstrap-env dev-hub edge-relay mcp-profile validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run railiance-state-hub-headroom
|
||||
|
||||
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
|
||||
PYTHON ?= python3
|
||||
HELM ?= $(shell command -v helm 2>/dev/null || if [ -x "$$HOME/.local/bin/helm" ]; then printf "%s" "$$HOME/.local/bin/helm"; else printf "%s" "helm"; fi)
|
||||
KUBECTL ?= $(shell command -v kubectl 2>/dev/null || if [ -x "$$HOME/.local/bin/kubectl" ]; then printf "%s" "$$HOME/.local/bin/kubectl"; else printf "%s" "kubectl"; fi)
|
||||
|
||||
RAILIANCE_STATE_HUB_RELEASE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_NAMESPACE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_CHART ?= deploy/railiance/apps/charts/state-hub
|
||||
RAILIANCE_STATE_HUB_VALUES ?= deploy/railiance/apps/helm/state-hub-values.yaml
|
||||
RAILIANCE_STATE_HUB_IMAGE_TAG ?= main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015
|
||||
RAILIANCE_STATE_HUB_PLATFORM_DIR ?= deploy/railiance/platform
|
||||
RAILIANCE_STATE_HUB_APP_MANIFESTS ?= deploy/railiance/apps/manifests
|
||||
# Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable.
|
||||
UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi)
|
||||
RMGR ?= $(shell command -v rmgr 2>/dev/null || if [ -x "$$HOME/repo-manager/.venv/bin/rmgr" ]; then printf "%s" "$$HOME/repo-manager/.venv/bin/rmgr"; else printf "%s" "rmgr"; fi)
|
||||
STATE_HUB_API_BASE ?= http://127.0.0.1:8000
|
||||
SYNC_PATH ?= .
|
||||
SYNC_PUSH ?= 1
|
||||
|
||||
start:
|
||||
@echo "# Normal production UI access (run in order)"
|
||||
@echo "make bridges # connect local :8000 to the railiance01 primary"
|
||||
@echo "make dashboard # verify the primary, then serve the UI on :3000"
|
||||
@echo "make sync # push file-backed records and reconcile the exact forge commit"
|
||||
@echo ""
|
||||
@echo "# Optional local MCP adapter"
|
||||
@echo "make mcp-http # local SSE adapter on :8001; not needed by the UI"
|
||||
@echo ""
|
||||
@echo "# Local fallback/development only"
|
||||
@echo "make api # local Postgres + API; conflicts with the production :8000 tunnel"
|
||||
@echo "make dashboard-local # dashboard against the deliberate local API"
|
||||
|
||||
install:
|
||||
$(UV) sync
|
||||
|
||||
dashboard/node_modules/.bin/observable: dashboard/package.json dashboard/package-lock.json
|
||||
cd dashboard && npm ci
|
||||
|
||||
dashboard-install: dashboard/node_modules/.bin/observable
|
||||
|
||||
dashboard-check: dashboard-install
|
||||
cd dashboard && npm run build
|
||||
|
||||
## Symlink the custodian CLI into ~/.local/bin so it's on PATH system-wide
|
||||
install-cli: install
|
||||
mkdir -p ~/.local/bin
|
||||
ln -sf "$(shell pwd)/.venv/bin/custodian" ~/.local/bin/custodian
|
||||
ln -sf "$(shell pwd)/.venv/bin/statehub" ~/.local/bin/statehub
|
||||
@echo "Installed: custodian → $$(readlink -f ~/.local/bin/custodian)"
|
||||
@echo "Installed: statehub → $$(readlink -f ~/.local/bin/statehub)"
|
||||
@echo "Make sure ~/.local/bin is on your PATH:"
|
||||
@echo " echo 'export PATH=\"\$$HOME/.local/bin:\$$PATH\"' >> ~/.bashrc && source ~/.bashrc"
|
||||
|
||||
db:
|
||||
$(COMPOSE) up -d postgres
|
||||
|
||||
db-tools:
|
||||
$(COMPOSE) --profile tools up -d
|
||||
|
||||
migrate:
|
||||
$(UV) run alembic upgrade head
|
||||
|
||||
seed:
|
||||
$(UV) run python scripts/seed.py
|
||||
|
||||
## Start (or restart) the MCP SSE server on :8001 — primary transport for Claude Code.
|
||||
## Remote clients (e.g. COULOMBCORE) connect via the ops-bridge tunnel (port 18001).
|
||||
## Registration: claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}'
|
||||
mcp-http:
|
||||
@fuser -k 8001/tcp 2>/dev/null && echo "Stopped running MCP server" || true
|
||||
MCP_TRANSPORT=sse MCP_PORT=8001 $(UV) run python mcp_server/server.py
|
||||
|
||||
## Require the production identity before serving the normal dashboard. This
|
||||
## prevents an accidental local `make api` from presenting an empty fallback DB
|
||||
## as the live State Hub.
|
||||
check-primary:
|
||||
@health="$$(curl -fsS --max-time 5 http://127.0.0.1:8000/state/health 2>/dev/null)" || { \
|
||||
echo "ERROR: State Hub primary is not reachable on 127.0.0.1:8000." >&2; \
|
||||
echo "Run 'make bridges' first." >&2; \
|
||||
exit 1; \
|
||||
}; \
|
||||
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
|
||||
if [ "$$identity" != "primary/railiance01" ]; then \
|
||||
echo "ERROR: 127.0.0.1:8000 is '$$identity', not the primary/railiance01 State Hub." >&2; \
|
||||
echo "Stop the local API, then run 'make bridges'. Use 'make dashboard-local' only for intentional local development." >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## Refuse to start the production tunnel when a non-primary API already owns
|
||||
## port 8000. With no listener, bridge is free to establish the tunnel.
|
||||
primary-port-clear:
|
||||
@if health="$$(curl -fsS --max-time 2 http://127.0.0.1:8000/state/health 2>/dev/null)"; then \
|
||||
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
|
||||
if [ "$$identity" != "primary/railiance01" ]; then \
|
||||
echo "ERROR: port 8000 is occupied by '$$identity'." >&2; \
|
||||
echo "Stop the local 'make api' process before starting the production tunnel." >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
dashboard: check-primary
|
||||
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
|
||||
$(MAKE) dashboard-install
|
||||
cd dashboard && npm run dev
|
||||
|
||||
## Deliberate local-development dashboard; bypasses the production identity gate.
|
||||
dashboard-local:
|
||||
@echo "WARNING: serving the dashboard against the local/fallback API on :8000."
|
||||
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
|
||||
$(MAKE) dashboard-install
|
||||
cd dashboard && npm run dev
|
||||
|
||||
## Fast work-record path. Repo Manager assigns only missing deterministic IDs,
|
||||
## verifies the primary/railliance01 bridge, and asks central to derive the
|
||||
## exact pushed Forgejo commit in one transactional request.
|
||||
sync:
|
||||
$(RMGR) sync --path "$(SYNC_PATH)" --api-base "$(STATE_HUB_API_BASE)" $(if $(filter 1 true yes,$(SYNC_PUSH)),--push,)
|
||||
|
||||
check: check-primary
|
||||
@echo "State Hub primary/railiance01 is healthy."
|
||||
|
||||
check-local:
|
||||
curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool
|
||||
|
||||
# CUST-WP-0067-T09. The chart ships a copy of the-custodian canon allowed-values
|
||||
# because a container has no checkout to read it from. Regenerate after any
|
||||
# canon vocabulary change; the -check target fails on drift so a release cannot
|
||||
# validate classification against a stale vocabulary.
|
||||
sync-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py
|
||||
|
||||
check-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py --check
|
||||
|
||||
# STATE-WP-0091: read-only surge-headroom check. Does not start Helm.
|
||||
# Live: make railiance-state-hub-headroom
|
||||
# Fixtures: make railiance-state-hub-headroom HEADROOM_FIXTURE=tests/fixtures/release_headroom/rev63-105m.json
|
||||
HEADROOM_FIXTURE ?=
|
||||
railiance-state-hub-headroom:
|
||||
@if [ -n "$(HEADROOM_FIXTURE)" ]; then \
|
||||
$(PYTHON) scripts/release_headroom_preflight.py --fixture "$(HEADROOM_FIXTURE)"; \
|
||||
else \
|
||||
$(PYTHON) scripts/release_headroom_preflight.py --kubectl "$(KUBECTL)"; \
|
||||
fi
|
||||
|
||||
railiance-state-hub-render: check-classification-allowed
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG)
|
||||
|
||||
railiance-state-hub-client-dry-run:
|
||||
@set -e; \
|
||||
tmpdir="$$(mktemp -d)"; \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"
|
||||
|
||||
railiance-state-hub-server-dry-run:
|
||||
@set -e; \
|
||||
tmpdir="$$(mktemp -d)"; \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
|
||||
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
if $(KUBECTL) get namespace $(RAILIANCE_STATE_HUB_NAMESPACE) >/dev/null 2>&1; then \
|
||||
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=server -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
|
||||
else \
|
||||
echo "Namespace $(RAILIANCE_STATE_HUB_NAMESPACE) does not exist; validating namespaced app manifests with client dry-run."; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
|
||||
fi
|
||||
|
||||
test: test-python dashboard-check
|
||||
|
||||
test-python:
|
||||
TEST_DATABASE_URL=postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test \
|
||||
$(UV) run pytest -x -q
|
||||
|
||||
## Benchmark /state/summary revision cache (API must be running on :8000)
|
||||
benchmark-summary-cache:
|
||||
$(UV) run python scripts/benchmark_summary_cache.py
|
||||
|
||||
## ops-bridge managed tunnels
|
||||
## Requires ops-bridge: bridge is at /home/worsch/.local/bin/bridge
|
||||
tunnels-up: primary-port-clear
|
||||
bridge up
|
||||
|
||||
tunnels-status:
|
||||
bridge status
|
||||
|
||||
## End-to-end check: verifies SSH process alive + remote port listening on COULOMBCORE.
|
||||
## Exits non-zero if any tunnel is not fully operational.
|
||||
tunnels-check:
|
||||
bridge check
|
||||
|
||||
## Ensure all ops-bridge tunnels are up and healthy.
|
||||
## Brings up any stopped/stale tunnels, shows final status, exits non-zero if anything is still down.
|
||||
bridges: primary-port-clear
|
||||
@echo "==> Bringing up all tunnels..."
|
||||
bridge up
|
||||
@echo ""
|
||||
@echo "==> Tunnel status:"
|
||||
bridge status
|
||||
@echo ""
|
||||
@echo "==> Checking tunnel health..."
|
||||
bridge check
|
||||
|
||||
## Start (or restart) the LOCAL FALLBACK backend — db + migrate + uvicorn.
|
||||
## This replaces anything on :8000, including the production State Hub tunnel.
|
||||
api: db
|
||||
@echo "WARNING: starting the local fallback API; this is not the railiance01 primary."
|
||||
@echo "Waiting for postgres..."; \
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do \
|
||||
nc -z 127.0.0.1 5432 2>/dev/null && break; \
|
||||
sleep 1; \
|
||||
done
|
||||
$(MAKE) migrate
|
||||
@fuser -k 8000/tcp 2>/dev/null && echo "Stopped running API" || true
|
||||
$(UV) run uvicorn api.main:app --reload --reload-dir api --reload-dir mcp_server --reload-dir task_flow_engine --host 127.0.0.1 --port 8000
|
||||
|
||||
## Register a project (Claude Code): make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance
|
||||
register-project:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)"
|
||||
|
||||
## Register a Codex project (AGENTS.md + HTTP API): make register-codex-project DOMAIN=capabilities PROJECT_PATH=/home/worsch/my-repo
|
||||
register-codex-project:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-codex-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" --codex
|
||||
|
||||
## Register State Hub MCP for Claude Code. Optional: make register-mcp MCP_URL=http://127.0.0.1:18001/sse
|
||||
register-mcp:
|
||||
scripts/register-mcp.sh \
|
||||
$(if $(MCP_URL),--url "$(MCP_URL)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Configure Codex workspace-write networking for local State Hub access.
|
||||
configure-codex:
|
||||
scripts/configure-codex.sh \
|
||||
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(WITH_MCP),--with-mcp,) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Bootstrap a new operator/collaborator environment. Optional: make bootstrap-env ARGS="--install-missing"
|
||||
bootstrap-env:
|
||||
scripts/bootstrap-env.sh $(ARGS)
|
||||
|
||||
## Files-first local dev hub (CUST-WP-0054-T07): postgres + migrate + seed + API + repo registration.
|
||||
## Optional: WITH_EDGE=1 WITH_MCP=1 make dev-hub
|
||||
## Fleet tunnel when reachable: make dev-hub PROFILE=fleet
|
||||
dev-hub:
|
||||
bash scripts/dev_hub_up.sh $(if $(PROFILE),$(PROFILE),dev)
|
||||
|
||||
## Local edge relay for offline write buffering (STATE-WP-0068).
|
||||
edge-relay:
|
||||
@fuser -k 18080/tcp 2>/dev/null && echo "Stopped running edge relay" || true
|
||||
STATEHUB_UPSTREAM_URL=$${STATEHUB_UPSTREAM_URL:-http://127.0.0.1:8000} \
|
||||
$(UV) run uvicorn api.edge.relay:app --host 127.0.0.1 --port 18080
|
||||
|
||||
## Switch MCP/API profile: make mcp-profile PROFILE=dev|fleet|status
|
||||
mcp-profile:
|
||||
@test -n "$(PROFILE)" || (echo "ERROR: PROFILE is required (dev|fleet|status)."; exit 1)
|
||||
bash scripts/mcp_hub_profile.sh "$(PROFILE)"
|
||||
|
||||
## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra
|
||||
add-repo:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||
@test -n "$(REPO_PATH)" || (echo "ERROR: REPO_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(REPO_PATH)" --additional
|
||||
|
||||
## Create a new domain: make add-domain DOMAIN=my_domain NAME="My Domain"
|
||||
add-domain:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required (slug)."; exit 1)
|
||||
@test -n "$(NAME)" || (echo "ERROR: NAME is required (display name)."; exit 1)
|
||||
curl -sf -X POST http://127.0.0.1:8000/domains/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"slug\": \"$(DOMAIN)\", \"name\": \"$(NAME)\"}" | python3 -m json.tool
|
||||
|
||||
## Rename a domain: make rename-domain DOMAIN=old_slug NEW_SLUG=new_slug NEW_NAME="New Name"
|
||||
rename-domain:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN (old slug) is required."; exit 1)
|
||||
@test -n "$(NEW_SLUG)" || (echo "ERROR: NEW_SLUG is required."; exit 1)
|
||||
@test -n "$(NEW_NAME)" || (echo "ERROR: NEW_NAME is required."; exit 1)
|
||||
curl -sf -X PATCH http://127.0.0.1:8000/domains/$(DOMAIN)/rename \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_slug\": \"$(NEW_SLUG)\", \"new_name\": \"$(NEW_NAME)\"}" | python3 -m json.tool
|
||||
|
||||
## Register this machine's local path for a repo: make register-path REPO=marki-docx PATH=/home/tegwick/marki-docx
|
||||
register-path:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
|
||||
@test -n "$(PATH)" || (echo "ERROR: PATH is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
|
||||
curl -sf -X POST "http://127.0.0.1:8000/repos/$(REPO)/paths" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"host\": \"$$(hostname)\", \"path\": \"$(PATH)\"}" | python3 -m json.tool
|
||||
|
||||
## List repos for a domain: make list-repos DOMAIN=railiance
|
||||
list-repos:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||
curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool
|
||||
|
||||
## Ingest SBOM data for a repo (all mechanisms: lockfiles + ansible + sbom-tools.yaml).
|
||||
## Auto-detect all sources: make ingest-sbom REPO=the-custodian REPO_PATH=/home/worsch/the-custodian
|
||||
## Single lockfile (explicit): make ingest-sbom REPO=the-custodian LOCKFILE=/path/to/uv.lock
|
||||
## Dry-run (no submit): make ingest-sbom REPO=the-custodian REPO_PATH=... DRY_RUN=1
|
||||
## Tip: run capture-tools first for repos with system-level tool dependencies.
|
||||
ingest-sbom:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_sbom.py --repo "$(REPO)" \
|
||||
$(if $(LOCKFILE),--lockfile "$(LOCKFILE)") \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Ingest capability declarations from SCOPE.md into the catalog.
|
||||
## Usage: make ingest-capabilities REPO=the-custodian [REPO_PATH=/home/worsch/the-custodian]
|
||||
## Or: make ingest-capabilities-all
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
ingest-capabilities:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_capabilities.py --repo "$(REPO)" \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
ingest-capabilities-all:
|
||||
$(UV) run python scripts/ingest_capabilities.py --all \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Check Repository Definition of Integrated (DoI) criteria for a repo.
|
||||
## Usage: make check-doi REPO=llm-connect
|
||||
## Or: make check-doi-all
|
||||
## Add JSON=1 for machine-readable output.
|
||||
check-doi:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/check_doi.py --repo "$(REPO)" $(if $(JSON),--json)
|
||||
|
||||
check-doi-all:
|
||||
$(UV) run python scripts/check_doi.py --all $(if $(JSON),--json)
|
||||
|
||||
## Ingest tpsc.yaml service declarations from a repo into the TPSC catalog.
|
||||
## Usage: make ingest-tpsc REPO=llm-connect
|
||||
## Or: make ingest-tpsc-all
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
ingest-tpsc:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_tpsc.py --repo "$(REPO)" \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
ingest-tpsc-all:
|
||||
$(UV) run python scripts/ingest_tpsc.py --all \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Run SBOM capture agent for a repo — generates/updates sbom-tools.yaml.
|
||||
## Usage: make capture-tools REPO=railiance-infra [REPO_PATH=/home/worsch/railiance-infra]
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
capture-tools:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/capture_sbom_tools.py --repo "$(REPO)" \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian]
|
||||
validate-adr:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO=<path> [DOMAIN=<slug>]"; exit 1)
|
||||
$(UV) run python scripts/validate_repo_adr.py "$(REPO)" $(if $(DOMAIN),--domain "$(DOMAIN)",)
|
||||
|
||||
## Consistency exit contract:
|
||||
## - Direct scripts/consistency_check.py: 0 clean, 2 warnings-only, 1 failures.
|
||||
## - Agent/operator Make wrappers below normalize warning-only 2 to shell success
|
||||
## while preserving visible WARN output and keeping real failures non-zero.
|
||||
## Check a single repo for ADR-001 consistency: make check-consistency REPO=the-custodian [REPO_PATH=/override]
|
||||
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
|
||||
check-consistency:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make check-consistency REPO=<slug>"; exit 1)
|
||||
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check and auto-fix a single repo: make fix-consistency REPO=the-custodian [REPO_PATH=/override]
|
||||
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
|
||||
fix-consistency:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make fix-consistency REPO=<slug>"; exit 1)
|
||||
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" --fix \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Normalize workplan frontmatter and task status literals in attached repos.
|
||||
## Usage: make normalize-attached-workplans REPO=artifact-store
|
||||
## make normalize-attached-workplans DIRTY=1
|
||||
normalize-attached-workplans:
|
||||
$(UV) run python scripts/normalize_attached_repo_workplans.py \
|
||||
$(if $(REPO),--repo "$(REPO)",) \
|
||||
$(if $(DIRTY),--dirty,) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
|
||||
|
||||
## Regenerate AGENTS.md / CLAUDE.md / .claude/rules from templates.
|
||||
## Usage: make update-agent-instructions REPO=artifact-store
|
||||
## make update-agent-instructions DIRTY=1
|
||||
update-agent-instructions:
|
||||
$(UV) run python scripts/update_agent_instruction_files.py \
|
||||
$(if $(REPO),--repo "$(REPO)",) \
|
||||
$(if $(DIRTY),--dirty,)
|
||||
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
|
||||
|
||||
## Reconcile measured token sources against State Hub.
|
||||
## Usage: make token-reconcile [SINCE=2026-05-19] [APPLY=1] [ZERO_FALLBACKS=1]
|
||||
token-reconcile:
|
||||
$(PYTHON) scripts/token_reconcile.py \
|
||||
$(if $(SINCE),--since "$(SINCE)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
|
||||
$(if $(CLAUDE_HOME),--claude-home "$(CLAUDE_HOME)",) \
|
||||
$(if $(APPLY),--apply,) \
|
||||
$(if $(ZERO_FALLBACKS),--zero-superseded-fallbacks,)
|
||||
|
||||
## Pull then fix: single repo or all repos if REPO omitted
|
||||
## make fix-consistency-remote — smart pull+fix all repos that need it
|
||||
## make fix-consistency-remote REPO=slug — pull+fix one repo
|
||||
fix-consistency-remote:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
$(if $(REPO),--repo "$(REPO)",--all) \
|
||||
--remote \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(NO_WRITEBACK),--no-writeback,); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Infer repo slug from git remote URL and check: make check-consistency-here [REPO_PATH=/path/to/repo]
|
||||
## Omit REPO_PATH to use the Python script's CWD (i.e. pass an empty --here flag).
|
||||
check-consistency-here:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Infer repo slug from git remote URL and fix: make fix-consistency-here [REPO_PATH=/path/to/repo]
|
||||
fix-consistency-here:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
|
||||
--fix \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check all registered repos for ADR-001 consistency
|
||||
check-consistency-all:
|
||||
$(UV) run python scripts/consistency_check.py --all $(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check and auto-fix all registered repos
|
||||
fix-consistency-all:
|
||||
$(UV) run python scripts/consistency_check.py --all --fix $(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Cancel open tasks belonging to completed/archived workstreams.
|
||||
## Safe to run at any time; also suitable for a daily cron job.
|
||||
## Cron example: 0 3 * * * cd ~/state-hub && make cleanup-stale
|
||||
cleanup-stale:
|
||||
$(UV) run python scripts/cleanup_stale_tasks.py
|
||||
|
||||
## Install custodian post-commit sync hook into one repo: make install-hooks REPO=marki-docx
|
||||
install-hooks:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make install-hooks REPO=<slug>"; exit 1)
|
||||
bash scripts/install_hooks.sh --repo "$(REPO)"
|
||||
|
||||
## Install custodian post-commit sync hook into all active registered repos
|
||||
install-hooks-all:
|
||||
bash scripts/install_hooks.sh --all
|
||||
|
||||
## Remove custodian post-commit sync hook from one repo: make remove-hooks REPO=marki-docx
|
||||
remove-hooks:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make remove-hooks REPO=<slug>"; exit 1)
|
||||
bash scripts/install_hooks.sh --repo "$(REPO)" --remove
|
||||
|
||||
## Compare Gitea coulomb org repos against state-hub registered repos
|
||||
## Requires GITEA_TOKEN in env or .env: make gitea-inventory GITEA_TOKEN=<token>
|
||||
gitea-inventory:
|
||||
$(UV) run python scripts/gitea_inventory.py $(if $(JSON),--json)
|
||||
|
||||
## Register/update one repo from .repo-classification.yaml:
|
||||
## make register-from-classification REPO=state-hub
|
||||
## make register-from-classification PATH=/path/to/repo
|
||||
## Optional: DRY_RUN=1
|
||||
register-from-classification:
|
||||
@test -n "$(REPO)" -o -n "$(PATH)" || (echo "ERROR: REPO or PATH is required."; exit 1)
|
||||
$(UV) run python scripts/register_from_classification.py \
|
||||
$(if $(PATH),--repo-path "$(PATH)",--slug "$(REPO)") \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Bulk register/update all active repos with accessible local paths
|
||||
register-from-classification-all:
|
||||
$(UV) run python scripts/register_from_classification.py --bulk \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
clean:
|
||||
$(COMPOSE) down -v
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v2
|
||||
name: state-hub
|
||||
description: State Hub API service for private Railiance operation
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "main-51bc70d"
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# GENERATED — do not edit.
|
||||
# Synced from the-custodian/canon/standards/repo-classification.allowed.yaml
|
||||
# by scripts/sync_classification_allowed.py (CUST-WP-0067-T09).
|
||||
# Canon is authoritative; this copy exists only so the container has one.
|
||||
# Machine-readable allowed-values for the Repo Classification Standard.
|
||||
#
|
||||
# Single source of truth for the standard's controlled vocabularies, derived
|
||||
# from canon/standards/repo-classification-standard_v1.0.md. Consumed by:
|
||||
# - the per-repo .repo-classification.yaml linter (tools/validate_repo_classification.py)
|
||||
# - the State Hub registration validator (CUST-WP-0050 T04)
|
||||
#
|
||||
# When the standard's vocabularies change, update this file and bump `version`
|
||||
# to match the standard version. CUST-WP-0050 T01.
|
||||
|
||||
standard: "Repo Classification Standard"
|
||||
version: "1.1"
|
||||
canon_id: "canon-repo-classification"
|
||||
|
||||
# category — exactly 1 required (§5)
|
||||
categories:
|
||||
- experimental
|
||||
- research
|
||||
- project
|
||||
- tooling
|
||||
- product
|
||||
- business
|
||||
- publication
|
||||
|
||||
# domain / secondary_domains — primary exactly 1; secondaries 0..n (§6)
|
||||
domains:
|
||||
- infotech
|
||||
- financials
|
||||
- communication
|
||||
- consumer
|
||||
- health
|
||||
- industrials
|
||||
- energy
|
||||
- utilities
|
||||
- materials
|
||||
- realestate
|
||||
- crypto
|
||||
- agents
|
||||
- space
|
||||
- government
|
||||
|
||||
# business_stake — 0..n; 2..6 recommended (§8)
|
||||
business_stake:
|
||||
- execution
|
||||
- intelligence
|
||||
- finance
|
||||
- legal
|
||||
- sales
|
||||
- experience
|
||||
- technology
|
||||
- operations
|
||||
- product
|
||||
- people
|
||||
- procurement
|
||||
- sustainability
|
||||
- automation
|
||||
|
||||
# business_mechanics — 0..n, optional (§9)
|
||||
business_mechanics:
|
||||
- intention
|
||||
- control
|
||||
- coordination
|
||||
- operation
|
||||
- adaptation
|
||||
|
||||
# capability_tags are intentionally OPEN-ENDED (§7): lowercase kebab-case, not
|
||||
# restricted to this set. The families below are the standard's recommended
|
||||
# canonical tags — used to warn on likely synonyms/typos, never to reject.
|
||||
capability_families:
|
||||
identity_and_access:
|
||||
- identity
|
||||
- authentication
|
||||
- authorization
|
||||
- access-control
|
||||
- user-management
|
||||
- tenancy
|
||||
knowledge_and_evidence:
|
||||
- knowledge
|
||||
- citations
|
||||
- evidence
|
||||
- source-management
|
||||
- traceability
|
||||
- documentation
|
||||
- decision-support
|
||||
- history
|
||||
- participation
|
||||
platform_and_operations:
|
||||
- platform
|
||||
- deployment
|
||||
- operations
|
||||
- observability
|
||||
- feature-control
|
||||
- configuration
|
||||
- orchestration
|
||||
market_and_coordination:
|
||||
- marketplace
|
||||
- pricing
|
||||
- monetization
|
||||
- reputation
|
||||
- challenges
|
||||
- bounties
|
||||
- collaboration
|
||||
- coordination
|
||||
product_and_lifecycle:
|
||||
- product-development
|
||||
- lifecycle
|
||||
governance_and_control:
|
||||
- governance
|
||||
- policy
|
||||
- compliance
|
||||
- risk
|
||||
- audit
|
||||
- control
|
||||
games_and_entertainment:
|
||||
- game
|
||||
- entertainment
|
||||
|
||||
# Validation guidance (advisory bounds the linter applies as warnings)
|
||||
guidance:
|
||||
secondary_domains_max: 3
|
||||
business_stake_recommended_min: 2
|
||||
business_stake_recommended_max: 6
|
||||
capability_tag_pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Production values for the State Hub Railiance chart handoff.
|
||||
# Non-secret values only. DATABASE_URL comes from the Secret `state-hub-env`.
|
||||
|
||||
namespace:
|
||||
create: false
|
||||
|
||||
image:
|
||||
repository: forgejo.coulomb.social/coulomb/state-hub
|
||||
tag: "main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
# MCP layer on central (CUST-WP-0067-T08). Enabled together with the image tag
|
||||
# above: MCP_HOST landed in main-6c1262e, and enabling this on an older image
|
||||
# would deploy a pod that binds loopback and never becomes reachable.
|
||||
# ClusterIP only — no ingress, same reason the API's is disabled.
|
||||
mcp:
|
||||
enabled: true
|
||||
resources:
|
||||
requests:
|
||||
# 2026-09-10: 1,078 observed 5-minute samples, p99 2.35m, peak 30.45m.
|
||||
# Preserve the 500m limit for bursts; free 40m for the API rollout surge.
|
||||
cpu: 10m
|
||||
|
||||
config:
|
||||
# This deployment is the authoritative hub. Callers verify this rather than
|
||||
# trusting that whatever answered on a port is central (CUST-WP-0067-T03).
|
||||
instanceRole: primary
|
||||
instanceLabel: railiance01
|
||||
sbomNexusUrl: "http://sbom-nexus.sbom-nexus.svc.cluster.local:8010"
|
||||
# Reversible T04 read cutover; set back to `legacy` to roll back.
|
||||
sbomNexusReadMode: nexus
|
||||
# Reversible T04 write cutover; set back to `legacy` to roll back future writes.
|
||||
sbomNexusWriteMode: nexus
|
||||
|
||||
resources:
|
||||
# Restored to the chart's 250m on 2026-09-21. The earlier 100m was a workaround
|
||||
# for a node with <250m unallocated; ~830m of Knative/Kourier requests were
|
||||
# released that day (the-custodian docs/kubernetes-change-gate-decision.md).
|
||||
# State Hub idles near 5m but bursts past 400m under concurrent agent load.
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
sweep:
|
||||
# RMGR-WP-0005-T11: disabled while railiance01 checkouts still target the
|
||||
# stale gitea-remote lineage. Re-enable only after the governed remote
|
||||
# reconciliation and registrar preflight are complete.
|
||||
enabled: false
|
||||
# Present in the live release but previously missing here, so deploying from
|
||||
# this file alone silently dropped it. Inert while enabled is false.
|
||||
hostname: 239.62.205.92.host.secureserver.net
|
||||
hostPath: /home/tegwick
|
||||
sshHostPath: /home/tegwick/.ssh
|
||||
|
||||
# Forge read credential (STATE-WP-0084-T02). Coordinates only — the token lives
|
||||
# in OpenBao at platform/workloads/state-hub/forge-derivation and reaches the
|
||||
# pod through Kubernetes auth, never through this file or a Secret.
|
||||
forgeRead:
|
||||
enabled: true
|
||||
|
||||
# RPF-WP-0035-T04: dedicated ESO signing key, API-only explicit reference.
|
||||
renamePreflight:
|
||||
enabled: true
|
||||
secretName: state-hub-rename-preflight
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"schema": "statehub.cluster-historical-receiving-delivered.v1",
|
||||
"status": "verified",
|
||||
"proposal_id": "STATE-WP-0095-T06-cluster-frozen-receiving-r1",
|
||||
"approved_revision": 4,
|
||||
"approved_digest_sha256": "sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32",
|
||||
"approval_decision_id": "e3798419-8aa9-5425-8c08-20df6bd82a19",
|
||||
"source_revision": "51bc70d1ff8f25450a8650d8e50caf053093a02e",
|
||||
"readiness_source_revision": "086d79a2736e34b916bd7de3c8a65e2ea38a3d64",
|
||||
"helm_revision": 76,
|
||||
"image_reference": "forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015",
|
||||
"expected_schema": "c4d5e6f7a8b9",
|
||||
"health": {
|
||||
"status": "ok",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"schema": {
|
||||
"status": "ok",
|
||||
"applied": "c4d5e6f7a8b9",
|
||||
"expected": "c4d5e6f7a8b9"
|
||||
}
|
||||
},
|
||||
"pods_ready": true,
|
||||
"historical_module_sha256": "sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2",
|
||||
"historical_preservation_verified": true,
|
||||
"parents": 3,
|
||||
"tasks": 15,
|
||||
"full_historical_rows_equal": true,
|
||||
"native_rollback_probes": {
|
||||
"foreign-holder": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"missing-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-parent": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"preserve": {
|
||||
"full_historical_rows_equal": true,
|
||||
"full_historical_rows_restored": true,
|
||||
"native_outcome_status": "applied",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
}
|
||||
},
|
||||
"native_probe_commits": 0,
|
||||
"fresh_transaction_full_rows_equal": true,
|
||||
"source_receiving_gate_delivered": true,
|
||||
"normal_cluster_publication_verified": false,
|
||||
"registrar_admission_or_cluster_publication_not_inferred": true,
|
||||
"authority_limits": [
|
||||
"Rollback probe SQL effects rolled back; foreign-holder case is historical-binding refusal, not separate global holder branch.",
|
||||
"Actual historical native receiving is verified; owner-native TLS/private admission remains unresolved.",
|
||||
"Migration schema remains additive; Helm rollback does not downgrade schema or stop an active hook Job."
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/approval-decision.json",
|
||||
"sha256": "sha256:b51554c004daa882f3709f67bdae7f2e939d611120383e61be04ec6969878134"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/helm-return.json",
|
||||
"sha256": "sha256:61727deacf22d37da2d28cbaf04f5a3ddee0f9ff7571c77f1e85dba97963b572"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/delivered-runtime.json",
|
||||
"sha256": "sha256:89681066e8fe139456163ebfafa3efe77ffae8534ea167219230b0a74367f129"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/native-refusal-probes.json",
|
||||
"sha256": "sha256:673ccef965c820704dc310a1bf7832b583c30c3edfeb814528cf892555199172"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/before-full-row-hashes.json",
|
||||
"sha256": "sha256:75cdd87eef79059ff3f484ed24ca69e285899cdf8d851cc4e472653c417eb0ee"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/after-full-row-hashes.json",
|
||||
"sha256": "sha256:2156aad18f190384838c27e792706a156a7423391408afbc6107e6c28c5872d8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sync the canon classification allowed-values into the chart (CUST-WP-0067-T09).
|
||||
|
||||
The State Hub validates repository classification against
|
||||
``the-custodian/canon/standards/repo-classification.allowed.yaml``. That file is
|
||||
canon and must stay authoritative, but a container has no the-custodian
|
||||
checkout, so a copy has to travel with the release.
|
||||
|
||||
A copy that nobody checks is a copy that silently drifts. This script owns the
|
||||
copy: ``--check`` fails when it diverges from canon, so the release can refuse
|
||||
to ship a stale vocabulary rather than validate against yesterday's rules.
|
||||
|
||||
Usage:
|
||||
python scripts/sync_classification_allowed.py # write the copy
|
||||
python scripts/sync_classification_allowed.py --check # verify, exit 1 on drift
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CHART_COPY = (
|
||||
REPO_ROOT
|
||||
/ "deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml"
|
||||
)
|
||||
CANON_CANDIDATES = (
|
||||
Path("/home/worsch/the-custodian/canon/standards/repo-classification.allowed.yaml"),
|
||||
Path("/home/tegwick/the-custodian/canon/standards/repo-classification.allowed.yaml"),
|
||||
REPO_ROOT.parent / "the-custodian/canon/standards/repo-classification.allowed.yaml",
|
||||
)
|
||||
|
||||
HEADER = (
|
||||
"# GENERATED — do not edit.\n"
|
||||
"# Synced from the-custodian/canon/standards/repo-classification.allowed.yaml\n"
|
||||
"# by scripts/sync_classification_allowed.py (CUST-WP-0067-T09).\n"
|
||||
"# Canon is authoritative; this copy exists only so the container has one.\n"
|
||||
)
|
||||
|
||||
|
||||
def find_canon() -> Path:
|
||||
for candidate in CANON_CANDIDATES:
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
raise SystemExit(
|
||||
"ERROR: canon allowed-values not found. Looked in:\n "
|
||||
+ "\n ".join(str(c) for c in CANON_CANDIDATES)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Verify the chart copy matches canon; exit 1 on drift",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
canon = find_canon()
|
||||
desired = HEADER + canon.read_text(encoding="utf-8")
|
||||
|
||||
if args.check:
|
||||
if not CHART_COPY.is_file():
|
||||
print(f"DRIFT: {CHART_COPY} is missing; run without --check to create it")
|
||||
return 1
|
||||
current = CHART_COPY.read_text(encoding="utf-8")
|
||||
if current != desired:
|
||||
print(f"DRIFT: chart copy differs from canon ({canon})")
|
||||
sys.stdout.writelines(
|
||||
difflib.unified_diff(
|
||||
current.splitlines(keepends=True),
|
||||
desired.splitlines(keepends=True),
|
||||
fromfile="chart copy",
|
||||
tofile="canon",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
print(f"OK: chart copy matches canon ({canon})")
|
||||
return 0
|
||||
|
||||
CHART_COPY.parent.mkdir(parents=True, exist_ok=True)
|
||||
CHART_COPY.write_text(desired, encoding="utf-8")
|
||||
print(f"Synced {canon} -> {CHART_COPY}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
---
|
||||
id: STATE-WP-0095
|
||||
type: workplan
|
||||
title: "Preserve exact closed historical source bindings in Forge receiving"
|
||||
domain: infotech
|
||||
repo: state-hub
|
||||
status: active
|
||||
owner: codex
|
||||
created: "2026-10-04"
|
||||
updated: "2026-10-17"
|
||||
flavor: implementation
|
||||
depends_on: []
|
||||
state_hub_workstream_id: "79579572-8120-5d3b-aca5-dd4aff60d058"
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Normal Forge receiving must preserve the seven bound Activity legacy source
|
||||
views and the Custodian retired ADHOC source without new identities, source
|
||||
rewrites, task movement, deletion, relabeling or unretirement. This implements
|
||||
receiving support before RMGR-WP-0019 registrar admission. CUST-WP-0077 owns
|
||||
cross-owner coordination; deployment and replay remain explicit owner returns.
|
||||
|
||||
## Verify source bindings and shared views
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7"
|
||||
```
|
||||
|
||||
Reviewed seven exact Activity files: four existing workplans and 64 UUID-bound
|
||||
closed tasks. The archived custodian-WP-0003 parent and three split_from views
|
||||
share one existing parent UUID; every repeated task binding/content agrees.
|
||||
Custodian ADHOC-2026-08-25-retired retains parent
|
||||
1c53d3db-6633-5e2d-987f-10706d73be9e and task
|
||||
d8af3e6b-26ae-580c-bbeb-caa8ad419bec. No identity migration is authorized.
|
||||
|
||||
## Implement preservation receiving and global collision refusal
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ebb24194-e4cc-5130-aaa6-663f2ea2c822"
|
||||
```
|
||||
|
||||
Prepared exact source digest manifest, existing-parent/task verification,
|
||||
immutable receiving and UUID-based historical diff. Ordinary canonical sources
|
||||
continue deriving their identities. Missing/changed/symlinked source, absent
|
||||
bound rows or nonterminal hub histories refuse. Check task UUID holders globally,
|
||||
including retired and acknowledged-stale parent workplans, before INSERT.
|
||||
|
||||
Offline preview tests and snapshot proof are evidence of source behavior only;
|
||||
finish after applying reviewed source and its focused tests to the owning repo.
|
||||
|
||||
## Deliver receiving through normal deployment and verify owner return
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T03
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T02]
|
||||
state_hub_task_id: "e8566341-cf6e-5b9c-ad80-8564d14cd52a"
|
||||
decision_id: "36772026-f43d-4331-b466-5f4b1afe4eca"
|
||||
```
|
||||
|
||||
No direct live code patch. This task is a deployment return, not policy authority
|
||||
or permission to resolve identity decisions. Exact receiving fixture acceptance
|
||||
and global collision rejection must hold in the delivered version.
|
||||
|
||||
## Verify normal historical replay preserves existing identities
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T04
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T03, RMGR-WP-0019-T03]
|
||||
state_hub_task_id: "3bbb311f-194e-5862-8aed-358a580f22b4"
|
||||
```
|
||||
|
||||
Verify Activity four historical parent UUIDs/64 task UUIDs and Custodian retired
|
||||
parent/task unchanged; no historical row creation, movement, deletion,
|
||||
relabeling or unretirement. Unrelated stale projections remain separate decisions.
|
||||
|
||||
2026-10-05: Source exact-set retirement support now exists. Repository projection
|
||||
reconcile accepts `expected_retirement_uuids`; that set must equal the stale
|
||||
projections, and any task or dependency edge on a stale row refuses the call.
|
||||
Live apply still needs a reviewed rollout. On 2026-10-05 the founder accepted
|
||||
review `review-1ece4f3e-d0e3-4fd6-abc8-272f1b1876f4`, which names only the three
|
||||
empty projections and preserves parent `b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`.
|
||||
That acceptance is not a live retirement and does not resolve
|
||||
`91b7c633-a66f-5d43-9f4b-6565aefb9356`.
|
||||
|
||||
## Offline evidence, 2026-10-04
|
||||
|
||||
Prepared receiver suite: Forge projection regression plus historical preservation,
|
||||
missing/changed source, terminality, immutable UUID diff, and global holder query.
|
||||
Real-source offline snapshot proof preserves four Activity parents/64 tasks and
|
||||
one Custodian parent/task without mutations; no API writes or deployment occurred.
|
||||
Exact original bindings are maintained in api/services/historical_projection.py.
|
||||
|
||||
2026-10-04 actual owning-source validation: 155 focused receiver/qualifier/planner
|
||||
cases passed; the independent offline current-source/current-hub-snapshot proof
|
||||
retained five existing parents and 65 task UUIDs, no mutations, clean historical
|
||||
diff. T02 is source-complete; T03/T04 remain wait for reviewed deployment and
|
||||
normal registrar/replay receiving, respectively. Evidence: docs/evidence/2026-10-04-historical-receiving-source.json.
|
||||
|
||||
|
||||
## Verified receiving rollout and remaining replay gate, 2026-10-04
|
||||
|
||||
T03 is done on actual founder approval of proposal
|
||||
STATE-WP-0095-T03-historical-receiving revision 1, SHA256
|
||||
`d6789359b3fe7988a6201fff6f367cb46948aa197e1cc0ca66c31f45e1ea9981`,
|
||||
approval receipt reference `user_reply:call_o87uEfFs3AtXfxO5YqUnSpcg:0`.
|
||||
The normal owner Helm path delivered `main-5dd3df9`, commit
|
||||
`5dd3df9c69eb564af10ea5ea01e349786212501a`, revision 72; API and MCP are Ready,
|
||||
primary/railiance01 health and schema a0b1c2d3e4f5 verified, running source
|
||||
hashes match. Rollback remains revision 71; no receipt-table downgrade is needed.
|
||||
Actual historical preservation and global task-holder refusal were verified in
|
||||
the delivered version. Durable receipt:
|
||||
[verified historical receiving rollout](../docs/evidence/2026-10-04-historical-receiving-rollout.json).
|
||||
|
||||
Custodian immutable replay applied while preserving the retired historical
|
||||
parent/task UUIDs. Activity immutable replay retained all four historical parent
|
||||
and 64 task UUIDs and historical row contents, but refused transactionally on
|
||||
three unrelated stale projections: custodian-wp-0003 (archived),
|
||||
custodian-wp-0003a (done), custodian-wp-0003b (done). No retirement acknowledgement
|
||||
was issued and that refusal is not represented as normal replay success.
|
||||
|
||||
Registrar source `c41d36c2` has 38 actual passing tests and Repo Manager's own
|
||||
normal sync succeeded; registrar admission does not authorize retiring Activity
|
||||
projections. T04 therefore remains wait, retaining RMGR-WP-0019-T03 and awaiting
|
||||
Activity Core's precise reviewed disposition of those three stale rows. No
|
||||
rename, identity migration, new paid native trial or further live action is
|
||||
included in this source status update.
|
||||
|
||||
## Activity Core return, 2026-10-06
|
||||
|
||||
Activity Core returned the exact disposition (message
|
||||
`cf2d5847-8b3f-4653-9d9f-d5fcfbe3f1e4`, ACTIVITY-WP-0045, activity-core commit
|
||||
`941fac0`). `expected_retirement_uuids`:
|
||||
`683724d9-4249-5e0e-88e0-7baff4330d58` (custodian-wp-0003),
|
||||
`644017c7-c5f2-5a64-81c3-d8ee806f8251` (custodian-wp-0003a),
|
||||
`a5806df9-5b9a-50da-81c1-7e606076bed9` (custodian-wp-0003b). Each has zero
|
||||
tasks and zero dependency edges. None is preserved parent
|
||||
`b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`. Repo Manager independently verified the
|
||||
set (message `916841f6`, repo-manager commit `498682c`). Nothing was retired.
|
||||
T04 now waits on decision `91b7c633` and a reviewed live rollout. After that,
|
||||
Repo Manager re-runs the Activity owner sync and closes RMGR-WP-0019-T03.
|
||||
|
||||
## Retirement rollout and T04 verification, 2026-10-06
|
||||
|
||||
The founder resolved decision `91b7c633` (retire exactly the three projections)
|
||||
and approved the rollout. The founder ran the normal Helm upgrade to
|
||||
`main-968a811` (revision 74; rollback revision 73, `main-fad9b3f`). API and MCP
|
||||
are Ready, health is ok on primary/railiance01, and schema `b3c4d5e6f7a8` is
|
||||
unchanged; no migrations. The activity-core reconcile at commit
|
||||
`67a92d2ff8e033baf2c36998d24187fe83edec84` with `expected_retirement_uuids` set
|
||||
to the three UUIDs applied: 0 created, 3 retired (tombstoned with an
|
||||
`@retired-20261006` slug; UUIDs and statuses kept), 0 refused, 0 tasks
|
||||
cancelled. All 7 bound Activity source files still match their digests. The four
|
||||
historical parents and 64 task UUIDs are present under their bound parents with
|
||||
unchanged fields. The Custodian retired parent and task are unchanged. Activity
|
||||
had 54 workplans and 288 tasks before and after. Evidence:
|
||||
[activity stale retirement rollout](../docs/evidence/2026-10-06-activity-stale-retirement-rollout.json).
|
||||
Repo Manager's re-run of the Activity owner sync (RMGR-WP-0019-T03) and
|
||||
ACTIVITY-WP-0045-T02 remain with their owners.
|
||||
|
||||
|
||||
## Cluster exact historical receiving successor, 2026-10-17
|
||||
|
||||
This scoped extension retains the completed Activity/Custodian outcomes above.
|
||||
Three frozen cluster archives contain three existing parent UUIDs and 15 existing
|
||||
closed task UUIDs. Extend the existing exact historical receiving path before
|
||||
registrar admission; never derive replacement identities or modify those
|
||||
sources. Cross-owner observation attaches to existing SHR-WP-0001, with
|
||||
RMGR-WP-0019 as obligor/beneficiary and NK-WP-0049-T35 benefiting indirectly.
|
||||
The local derived case is waiting; no live worker injection is authorized.
|
||||
|
||||
## Prepare exact cluster source preservation and refusal cases
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T05
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "339b9a37-181c-5599-9a89-0393e199bd4b"
|
||||
```
|
||||
|
||||
Prepared the same full-file SHA256 and 15 task binding manifest as Repo Manager.
|
||||
Candidate receiving tests pass 26: existing rows remain entirely unchanged;
|
||||
missing/foreign/nonterminal rows and unknown, changed, missing or symlinked
|
||||
source refuse. Read-only public metadata confirms all 18 existing rows have the
|
||||
expected ownership and terminal status. These are source and metadata checks,
|
||||
not delivered receiving or a committed native trial.
|
||||
|
||||
## Deliver reviewed cluster receiving successor before registrar admission
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T06
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T05]
|
||||
needs_human: false
|
||||
intervention_note: "Exact revision 4 approved and verified; no pending rollout intervention."
|
||||
state_hub_task_id: "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4"
|
||||
```
|
||||
|
||||
Completed 2026-10-17 on the founder's approval of exact revision 4 of stable
|
||||
proposal STATE-WP-0095-T06-cluster-frozen-receiving-r1 (decision
|
||||
e3798419-8aa9-5425-8c08-20df6bd82a19; approved digest
|
||||
sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32).
|
||||
The ordinary owner Helm path delivered revision 76, runtime source 51bc70d and
|
||||
exact image manifest sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015.
|
||||
API/MCP Ready and primary/railiance01 health report expected schema
|
||||
c4d5e6f7a8b9; delivered historical module sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2 matches.
|
||||
All three parent and 15 task full DB row hashes/columns/UUIDs remained unchanged.
|
||||
Five actual native probes passed: preserve, actual foreign-parent holder move,
|
||||
nonterminal task, nonterminal parent and missing task. Every probe rolled back;
|
||||
a fresh transaction confirmed complete row restoration. The foreign-holder
|
||||
case precisely verified historical binding refusal, not the separate global
|
||||
UUID-holder creation branch. No committed probe writes were made.
|
||||
|
||||
Evidence: [verified receiving rollout](../docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json).
|
||||
This actual receiving return satisfies the prerequisite of RMGR-WP-0019-T05.
|
||||
Normal cluster publication and its immutable-source/row return remain T07;
|
||||
source readiness and this deployment do not count that later acceptance twice.
|
||||
The additive inbox migration is retained; Helm rollback does not downgrade
|
||||
schema or stop an active migration Job. Original failures and exact-UID recovery
|
||||
limits remain in the referenced approved packet. No TLS recovery/native private
|
||||
admission or new autonomy/cost grant is inferred.
|
||||
|
||||
## Verify ordinary cluster receiving and immutable historical rows
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T07
|
||||
status: wait
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T06, RMGR-WP-0019-T06]
|
||||
state_hub_task_id: "cfd1ea99-e75b-542e-a434-acefb62f3c7d"
|
||||
```
|
||||
|
||||
Capture normal cluster owner publication/receiving receipts and compare entire
|
||||
existing row snapshots and exact source bytes. No creation, movement, relabel,
|
||||
unretirement or retirement is authorized for these histories. A rollback first
|
||||
fences cluster sync and restores registrar refusal before removing receiving
|
||||
support; it never rewrites archived IDs or UUIDs. This work does not establish
|
||||
original TLS recovery or owner-native admission.
|
||||
|
||||
## Fix policy-compatible Secret-template server dry-run preparation
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T08
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2dd9b04f-2250-5a8a-b5bc-c8a009872858"
|
||||
```
|
||||
|
||||
Prepared the narrow Makefile successor after the existing readiness command
|
||||
refused its first Secret-template phase. Ordinary client-side apply adds the
|
||||
last-applied annotation, correctly denied by the existing Platform
|
||||
reject-secret-last-applied policy. Only the two Secret-template server
|
||||
dry-run requests now use server-side apply with dry-run; no force-conflicts,
|
||||
Secret value change, policy relaxation or actual Secret write is introduced.
|
||||
Command-fixture tests verify both requests and preserve other dry-run phases.
|
||||
Evidence and original refusal remain in the Custodian cluster historical
|
||||
publication packet, revisions/r2 and r3-readiness. This is completed source
|
||||
preparation; T06 separately records the verified actual rollout. The runtime image
|
||||
continues to be main-51bc70d; Makefile/workplan changes do not create a new
|
||||
application image or native acceptance.
|
||||
|
||||
|
||||
T08 source repair refinement, 2026-10-17: the required normal Make render
|
||||
correctly refused a pre-existing generated chart classification copy at v1.0
|
||||
after authoritative canon reached v1.1. Regenerated only through the existing
|
||||
scripts/sync_classification_allowed.py, preserving canon and strict check.
|
||||
The exact source diff is version 1.0→1.1 plus publication/history/participation.
|
||||
Normal Make rendering and the focused classification check passed in an
|
||||
isolated owner-layout copy. The original refusal is retained in the Custodian
|
||||
owner-followthrough-r1 validation receipt. This is source repair only: no live
|
||||
ConfigMap apply, new runtime image, policy relaxation or T07 completion.
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
.PHONY: start install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard dashboard-local sync check-primary primary-port-clear check check-local test test-python clean register-project register-codex-project register-mcp configure-codex bootstrap-env dev-hub edge-relay mcp-profile validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run railiance-state-hub-headroom
|
||||
|
||||
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
|
||||
PYTHON ?= python3
|
||||
HELM ?= $(shell command -v helm 2>/dev/null || if [ -x "$$HOME/.local/bin/helm" ]; then printf "%s" "$$HOME/.local/bin/helm"; else printf "%s" "helm"; fi)
|
||||
KUBECTL ?= $(shell command -v kubectl 2>/dev/null || if [ -x "$$HOME/.local/bin/kubectl" ]; then printf "%s" "$$HOME/.local/bin/kubectl"; else printf "%s" "kubectl"; fi)
|
||||
|
||||
RAILIANCE_STATE_HUB_RELEASE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_NAMESPACE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_CHART ?= deploy/railiance/apps/charts/state-hub
|
||||
RAILIANCE_STATE_HUB_VALUES ?= deploy/railiance/apps/helm/state-hub-values.yaml
|
||||
RAILIANCE_STATE_HUB_IMAGE_TAG ?= main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015
|
||||
RAILIANCE_STATE_HUB_PLATFORM_DIR ?= deploy/railiance/platform
|
||||
RAILIANCE_STATE_HUB_APP_MANIFESTS ?= deploy/railiance/apps/manifests
|
||||
# Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable.
|
||||
UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi)
|
||||
RMGR ?= $(shell command -v rmgr 2>/dev/null || if [ -x "$$HOME/repo-manager/.venv/bin/rmgr" ]; then printf "%s" "$$HOME/repo-manager/.venv/bin/rmgr"; else printf "%s" "rmgr"; fi)
|
||||
STATE_HUB_API_BASE ?= http://127.0.0.1:8000
|
||||
SYNC_PATH ?= .
|
||||
SYNC_PUSH ?= 1
|
||||
|
||||
start:
|
||||
@echo "# Normal production UI access (run in order)"
|
||||
@echo "make bridges # connect local :8000 to the railiance01 primary"
|
||||
@echo "make dashboard # verify the primary, then serve the UI on :3000"
|
||||
@echo "make sync # push file-backed records and reconcile the exact forge commit"
|
||||
@echo ""
|
||||
@echo "# Optional local MCP adapter"
|
||||
@echo "make mcp-http # local SSE adapter on :8001; not needed by the UI"
|
||||
@echo ""
|
||||
@echo "# Local fallback/development only"
|
||||
@echo "make api # local Postgres + API; conflicts with the production :8000 tunnel"
|
||||
@echo "make dashboard-local # dashboard against the deliberate local API"
|
||||
|
||||
install:
|
||||
$(UV) sync
|
||||
|
||||
dashboard/node_modules/.bin/observable: dashboard/package.json dashboard/package-lock.json
|
||||
cd dashboard && npm ci
|
||||
|
||||
dashboard-install: dashboard/node_modules/.bin/observable
|
||||
|
||||
dashboard-check: dashboard-install
|
||||
cd dashboard && npm run build
|
||||
|
||||
## Symlink the custodian CLI into ~/.local/bin so it's on PATH system-wide
|
||||
install-cli: install
|
||||
mkdir -p ~/.local/bin
|
||||
ln -sf "$(shell pwd)/.venv/bin/custodian" ~/.local/bin/custodian
|
||||
ln -sf "$(shell pwd)/.venv/bin/statehub" ~/.local/bin/statehub
|
||||
@echo "Installed: custodian → $$(readlink -f ~/.local/bin/custodian)"
|
||||
@echo "Installed: statehub → $$(readlink -f ~/.local/bin/statehub)"
|
||||
@echo "Make sure ~/.local/bin is on your PATH:"
|
||||
@echo " echo 'export PATH=\"\$$HOME/.local/bin:\$$PATH\"' >> ~/.bashrc && source ~/.bashrc"
|
||||
|
||||
db:
|
||||
$(COMPOSE) up -d postgres
|
||||
|
||||
db-tools:
|
||||
$(COMPOSE) --profile tools up -d
|
||||
|
||||
migrate:
|
||||
$(UV) run alembic upgrade head
|
||||
|
||||
seed:
|
||||
$(UV) run python scripts/seed.py
|
||||
|
||||
## Start (or restart) the MCP SSE server on :8001 — primary transport for Claude Code.
|
||||
## Remote clients (e.g. COULOMBCORE) connect via the ops-bridge tunnel (port 18001).
|
||||
## Registration: claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}'
|
||||
mcp-http:
|
||||
@fuser -k 8001/tcp 2>/dev/null && echo "Stopped running MCP server" || true
|
||||
MCP_TRANSPORT=sse MCP_PORT=8001 $(UV) run python mcp_server/server.py
|
||||
|
||||
## Require the production identity before serving the normal dashboard. This
|
||||
## prevents an accidental local `make api` from presenting an empty fallback DB
|
||||
## as the live State Hub.
|
||||
check-primary:
|
||||
@health="$$(curl -fsS --max-time 5 http://127.0.0.1:8000/state/health 2>/dev/null)" || { \
|
||||
echo "ERROR: State Hub primary is not reachable on 127.0.0.1:8000." >&2; \
|
||||
echo "Run 'make bridges' first." >&2; \
|
||||
exit 1; \
|
||||
}; \
|
||||
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
|
||||
if [ "$$identity" != "primary/railiance01" ]; then \
|
||||
echo "ERROR: 127.0.0.1:8000 is '$$identity', not the primary/railiance01 State Hub." >&2; \
|
||||
echo "Stop the local API, then run 'make bridges'. Use 'make dashboard-local' only for intentional local development." >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## Refuse to start the production tunnel when a non-primary API already owns
|
||||
## port 8000. With no listener, bridge is free to establish the tunnel.
|
||||
primary-port-clear:
|
||||
@if health="$$(curl -fsS --max-time 2 http://127.0.0.1:8000/state/health 2>/dev/null)"; then \
|
||||
identity="$$(printf '%s' "$$health" | $(PYTHON) -c 'import json, sys; d=json.load(sys.stdin); print("{}/{}".format(d.get("instance_role", ""), d.get("instance_label", "")))')"; \
|
||||
if [ "$$identity" != "primary/railiance01" ]; then \
|
||||
echo "ERROR: port 8000 is occupied by '$$identity'." >&2; \
|
||||
echo "Stop the local 'make api' process before starting the production tunnel." >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
dashboard: check-primary
|
||||
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
|
||||
$(MAKE) dashboard-install
|
||||
cd dashboard && npm run dev
|
||||
|
||||
## Deliberate local-development dashboard; bypasses the production identity gate.
|
||||
dashboard-local:
|
||||
@echo "WARNING: serving the dashboard against the local/fallback API on :8000."
|
||||
@fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true
|
||||
$(MAKE) dashboard-install
|
||||
cd dashboard && npm run dev
|
||||
|
||||
## Fast work-record path. Repo Manager assigns only missing deterministic IDs,
|
||||
## verifies the primary/railliance01 bridge, and asks central to derive the
|
||||
## exact pushed Forgejo commit in one transactional request.
|
||||
sync:
|
||||
$(RMGR) sync --path "$(SYNC_PATH)" --api-base "$(STATE_HUB_API_BASE)" $(if $(filter 1 true yes,$(SYNC_PUSH)),--push,)
|
||||
|
||||
check: check-primary
|
||||
@echo "State Hub primary/railiance01 is healthy."
|
||||
|
||||
check-local:
|
||||
curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool
|
||||
|
||||
# CUST-WP-0067-T09. The chart ships a copy of the-custodian canon allowed-values
|
||||
# because a container has no checkout to read it from. Regenerate after any
|
||||
# canon vocabulary change; the -check target fails on drift so a release cannot
|
||||
# validate classification against a stale vocabulary.
|
||||
sync-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py
|
||||
|
||||
check-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py --check
|
||||
|
||||
# STATE-WP-0091: read-only surge-headroom check. Does not start Helm.
|
||||
# Live: make railiance-state-hub-headroom
|
||||
# Fixtures: make railiance-state-hub-headroom HEADROOM_FIXTURE=tests/fixtures/release_headroom/rev63-105m.json
|
||||
HEADROOM_FIXTURE ?=
|
||||
railiance-state-hub-headroom:
|
||||
@if [ -n "$(HEADROOM_FIXTURE)" ]; then \
|
||||
$(PYTHON) scripts/release_headroom_preflight.py --fixture "$(HEADROOM_FIXTURE)"; \
|
||||
else \
|
||||
$(PYTHON) scripts/release_headroom_preflight.py --kubectl "$(KUBECTL)"; \
|
||||
fi
|
||||
|
||||
railiance-state-hub-render: check-classification-allowed
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG)
|
||||
|
||||
railiance-state-hub-client-dry-run:
|
||||
@set -e; \
|
||||
tmpdir="$$(mktemp -d)"; \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"
|
||||
|
||||
railiance-state-hub-server-dry-run:
|
||||
@set -e; \
|
||||
tmpdir="$$(mktemp -d)"; \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
--set image.tag=$(RAILIANCE_STATE_HUB_IMAGE_TAG) > "$$tmpdir/state-hub.yaml"; \
|
||||
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-credentials.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-cluster.yaml; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_PLATFORM_DIR)/state-hub-db-networkpolicies.yaml; \
|
||||
$(KUBECTL) apply --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
if $(KUBECTL) get namespace $(RAILIANCE_STATE_HUB_NAMESPACE) >/dev/null 2>&1; then \
|
||||
$(KUBECTL) apply --server-side --dry-run=server -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=server -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
|
||||
else \
|
||||
echo "Namespace $(RAILIANCE_STATE_HUB_NAMESPACE) does not exist; validating namespaced app manifests with client dry-run."; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-namespace.yaml; \
|
||||
$(KUBECTL) apply --dry-run=client -f $(RAILIANCE_STATE_HUB_APP_MANIFESTS)/state-hub-env.secret.sops.yaml.template; \
|
||||
$(KUBECTL) apply --dry-run=client -n $(RAILIANCE_STATE_HUB_NAMESPACE) -f "$$tmpdir/state-hub.yaml"; \
|
||||
fi
|
||||
|
||||
test: test-python dashboard-check
|
||||
|
||||
test-python:
|
||||
TEST_DATABASE_URL=postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test \
|
||||
$(UV) run pytest -x -q
|
||||
|
||||
## Benchmark /state/summary revision cache (API must be running on :8000)
|
||||
benchmark-summary-cache:
|
||||
$(UV) run python scripts/benchmark_summary_cache.py
|
||||
|
||||
## ops-bridge managed tunnels
|
||||
## Requires ops-bridge: bridge is at /home/worsch/.local/bin/bridge
|
||||
tunnels-up: primary-port-clear
|
||||
bridge up
|
||||
|
||||
tunnels-status:
|
||||
bridge status
|
||||
|
||||
## End-to-end check: verifies SSH process alive + remote port listening on COULOMBCORE.
|
||||
## Exits non-zero if any tunnel is not fully operational.
|
||||
tunnels-check:
|
||||
bridge check
|
||||
|
||||
## Ensure all ops-bridge tunnels are up and healthy.
|
||||
## Brings up any stopped/stale tunnels, shows final status, exits non-zero if anything is still down.
|
||||
bridges: primary-port-clear
|
||||
@echo "==> Bringing up all tunnels..."
|
||||
bridge up
|
||||
@echo ""
|
||||
@echo "==> Tunnel status:"
|
||||
bridge status
|
||||
@echo ""
|
||||
@echo "==> Checking tunnel health..."
|
||||
bridge check
|
||||
|
||||
## Start (or restart) the LOCAL FALLBACK backend — db + migrate + uvicorn.
|
||||
## This replaces anything on :8000, including the production State Hub tunnel.
|
||||
api: db
|
||||
@echo "WARNING: starting the local fallback API; this is not the railiance01 primary."
|
||||
@echo "Waiting for postgres..."; \
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do \
|
||||
nc -z 127.0.0.1 5432 2>/dev/null && break; \
|
||||
sleep 1; \
|
||||
done
|
||||
$(MAKE) migrate
|
||||
@fuser -k 8000/tcp 2>/dev/null && echo "Stopped running API" || true
|
||||
$(UV) run uvicorn api.main:app --reload --reload-dir api --reload-dir mcp_server --reload-dir task_flow_engine --host 127.0.0.1 --port 8000
|
||||
|
||||
## Register a project (Claude Code): make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance
|
||||
register-project:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)"
|
||||
|
||||
## Register a Codex project (AGENTS.md + HTTP API): make register-codex-project DOMAIN=capabilities PROJECT_PATH=/home/worsch/my-repo
|
||||
register-codex-project:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-codex-project DOMAIN=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" --codex
|
||||
|
||||
## Register State Hub MCP for Claude Code. Optional: make register-mcp MCP_URL=http://127.0.0.1:18001/sse
|
||||
register-mcp:
|
||||
scripts/register-mcp.sh \
|
||||
$(if $(MCP_URL),--url "$(MCP_URL)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Configure Codex workspace-write networking for local State Hub access.
|
||||
configure-codex:
|
||||
scripts/configure-codex.sh \
|
||||
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(WITH_MCP),--with-mcp,) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Bootstrap a new operator/collaborator environment. Optional: make bootstrap-env ARGS="--install-missing"
|
||||
bootstrap-env:
|
||||
scripts/bootstrap-env.sh $(ARGS)
|
||||
|
||||
## Files-first local dev hub (CUST-WP-0054-T07): postgres + migrate + seed + API + repo registration.
|
||||
## Optional: WITH_EDGE=1 WITH_MCP=1 make dev-hub
|
||||
## Fleet tunnel when reachable: make dev-hub PROFILE=fleet
|
||||
dev-hub:
|
||||
bash scripts/dev_hub_up.sh $(if $(PROFILE),$(PROFILE),dev)
|
||||
|
||||
## Local edge relay for offline write buffering (STATE-WP-0068).
|
||||
edge-relay:
|
||||
@fuser -k 18080/tcp 2>/dev/null && echo "Stopped running edge relay" || true
|
||||
STATEHUB_UPSTREAM_URL=$${STATEHUB_UPSTREAM_URL:-http://127.0.0.1:8000} \
|
||||
$(UV) run uvicorn api.edge.relay:app --host 127.0.0.1 --port 18080
|
||||
|
||||
## Switch MCP/API profile: make mcp-profile PROFILE=dev|fleet|status
|
||||
mcp-profile:
|
||||
@test -n "$(PROFILE)" || (echo "ERROR: PROFILE is required (dev|fleet|status)."; exit 1)
|
||||
bash scripts/mcp_hub_profile.sh "$(PROFILE)"
|
||||
|
||||
## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra
|
||||
add-repo:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||
@test -n "$(REPO_PATH)" || (echo "ERROR: REPO_PATH is required."; exit 1)
|
||||
scripts/register_project.sh "$(DOMAIN)" "$(REPO_PATH)" --additional
|
||||
|
||||
## Create a new domain: make add-domain DOMAIN=my_domain NAME="My Domain"
|
||||
add-domain:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required (slug)."; exit 1)
|
||||
@test -n "$(NAME)" || (echo "ERROR: NAME is required (display name)."; exit 1)
|
||||
curl -sf -X POST http://127.0.0.1:8000/domains/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"slug\": \"$(DOMAIN)\", \"name\": \"$(NAME)\"}" | python3 -m json.tool
|
||||
|
||||
## Rename a domain: make rename-domain DOMAIN=old_slug NEW_SLUG=new_slug NEW_NAME="New Name"
|
||||
rename-domain:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN (old slug) is required."; exit 1)
|
||||
@test -n "$(NEW_SLUG)" || (echo "ERROR: NEW_SLUG is required."; exit 1)
|
||||
@test -n "$(NEW_NAME)" || (echo "ERROR: NEW_NAME is required."; exit 1)
|
||||
curl -sf -X PATCH http://127.0.0.1:8000/domains/$(DOMAIN)/rename \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_slug\": \"$(NEW_SLUG)\", \"new_name\": \"$(NEW_NAME)\"}" | python3 -m json.tool
|
||||
|
||||
## Register this machine's local path for a repo: make register-path REPO=marki-docx PATH=/home/tegwick/marki-docx
|
||||
register-path:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
|
||||
@test -n "$(PATH)" || (echo "ERROR: PATH is required. Usage: make register-path REPO=<slug> PATH=<path>"; exit 1)
|
||||
curl -sf -X POST "http://127.0.0.1:8000/repos/$(REPO)/paths" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"host\": \"$$(hostname)\", \"path\": \"$(PATH)\"}" | python3 -m json.tool
|
||||
|
||||
## List repos for a domain: make list-repos DOMAIN=railiance
|
||||
list-repos:
|
||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||
curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool
|
||||
|
||||
## Ingest SBOM data for a repo (all mechanisms: lockfiles + ansible + sbom-tools.yaml).
|
||||
## Auto-detect all sources: make ingest-sbom REPO=the-custodian REPO_PATH=/home/worsch/the-custodian
|
||||
## Single lockfile (explicit): make ingest-sbom REPO=the-custodian LOCKFILE=/path/to/uv.lock
|
||||
## Dry-run (no submit): make ingest-sbom REPO=the-custodian REPO_PATH=... DRY_RUN=1
|
||||
## Tip: run capture-tools first for repos with system-level tool dependencies.
|
||||
ingest-sbom:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_sbom.py --repo "$(REPO)" \
|
||||
$(if $(LOCKFILE),--lockfile "$(LOCKFILE)") \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Ingest capability declarations from SCOPE.md into the catalog.
|
||||
## Usage: make ingest-capabilities REPO=the-custodian [REPO_PATH=/home/worsch/the-custodian]
|
||||
## Or: make ingest-capabilities-all
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
ingest-capabilities:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_capabilities.py --repo "$(REPO)" \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
ingest-capabilities-all:
|
||||
$(UV) run python scripts/ingest_capabilities.py --all \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Check Repository Definition of Integrated (DoI) criteria for a repo.
|
||||
## Usage: make check-doi REPO=llm-connect
|
||||
## Or: make check-doi-all
|
||||
## Add JSON=1 for machine-readable output.
|
||||
check-doi:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/check_doi.py --repo "$(REPO)" $(if $(JSON),--json)
|
||||
|
||||
check-doi-all:
|
||||
$(UV) run python scripts/check_doi.py --all $(if $(JSON),--json)
|
||||
|
||||
## Ingest tpsc.yaml service declarations from a repo into the TPSC catalog.
|
||||
## Usage: make ingest-tpsc REPO=llm-connect
|
||||
## Or: make ingest-tpsc-all
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
ingest-tpsc:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/ingest_tpsc.py --repo "$(REPO)" \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
ingest-tpsc-all:
|
||||
$(UV) run python scripts/ingest_tpsc.py --all \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Run SBOM capture agent for a repo — generates/updates sbom-tools.yaml.
|
||||
## Usage: make capture-tools REPO=railiance-infra [REPO_PATH=/home/worsch/railiance-infra]
|
||||
## Add DRY_RUN=1 to preview without writing.
|
||||
capture-tools:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1)
|
||||
$(UV) run python scripts/capture_sbom_tools.py --repo "$(REPO)" \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \
|
||||
$(if $(DRY_RUN),--dry-run)
|
||||
|
||||
## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian]
|
||||
validate-adr:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO=<path> [DOMAIN=<slug>]"; exit 1)
|
||||
$(UV) run python scripts/validate_repo_adr.py "$(REPO)" $(if $(DOMAIN),--domain "$(DOMAIN)",)
|
||||
|
||||
## Consistency exit contract:
|
||||
## - Direct scripts/consistency_check.py: 0 clean, 2 warnings-only, 1 failures.
|
||||
## - Agent/operator Make wrappers below normalize warning-only 2 to shell success
|
||||
## while preserving visible WARN output and keeping real failures non-zero.
|
||||
## Check a single repo for ADR-001 consistency: make check-consistency REPO=the-custodian [REPO_PATH=/override]
|
||||
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
|
||||
check-consistency:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make check-consistency REPO=<slug>"; exit 1)
|
||||
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check and auto-fix a single repo: make fix-consistency REPO=the-custodian [REPO_PATH=/override]
|
||||
## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures
|
||||
fix-consistency:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make fix-consistency REPO=<slug>"; exit 1)
|
||||
$(UV) run python scripts/consistency_check.py --repo "$(REPO)" --fix \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Normalize workplan frontmatter and task status literals in attached repos.
|
||||
## Usage: make normalize-attached-workplans REPO=artifact-store
|
||||
## make normalize-attached-workplans DIRTY=1
|
||||
normalize-attached-workplans:
|
||||
$(UV) run python scripts/normalize_attached_repo_workplans.py \
|
||||
$(if $(REPO),--repo "$(REPO)",) \
|
||||
$(if $(DIRTY),--dirty,) \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
|
||||
|
||||
## Regenerate AGENTS.md / CLAUDE.md / .claude/rules from templates.
|
||||
## Usage: make update-agent-instructions REPO=artifact-store
|
||||
## make update-agent-instructions DIRTY=1
|
||||
update-agent-instructions:
|
||||
$(UV) run python scripts/update_agent_instruction_files.py \
|
||||
$(if $(REPO),--repo "$(REPO)",) \
|
||||
$(if $(DIRTY),--dirty,)
|
||||
@test -n "$(REPO)$(DIRTY)" || (echo "ERROR: set REPO=<slug> or DIRTY=1"; exit 1)
|
||||
|
||||
## Reconcile measured token sources against State Hub.
|
||||
## Usage: make token-reconcile [SINCE=2026-05-19] [APPLY=1] [ZERO_FALLBACKS=1]
|
||||
token-reconcile:
|
||||
$(PYTHON) scripts/token_reconcile.py \
|
||||
$(if $(SINCE),--since "$(SINCE)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \
|
||||
$(if $(CLAUDE_HOME),--claude-home "$(CLAUDE_HOME)",) \
|
||||
$(if $(APPLY),--apply,) \
|
||||
$(if $(ZERO_FALLBACKS),--zero-superseded-fallbacks,)
|
||||
|
||||
## Pull then fix: single repo or all repos if REPO omitted
|
||||
## make fix-consistency-remote — smart pull+fix all repos that need it
|
||||
## make fix-consistency-remote REPO=slug — pull+fix one repo
|
||||
fix-consistency-remote:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
$(if $(REPO),--repo "$(REPO)",--all) \
|
||||
--remote \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",) \
|
||||
$(if $(NO_WRITEBACK),--no-writeback,); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Infer repo slug from git remote URL and check: make check-consistency-here [REPO_PATH=/path/to/repo]
|
||||
## Omit REPO_PATH to use the Python script's CWD (i.e. pass an empty --here flag).
|
||||
check-consistency-here:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Infer repo slug from git remote URL and fix: make fix-consistency-here [REPO_PATH=/path/to/repo]
|
||||
fix-consistency-here:
|
||||
$(UV) run python scripts/consistency_check.py \
|
||||
--here $(if $(REPO_PATH),"$(REPO_PATH)",) \
|
||||
--fix \
|
||||
$(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check all registered repos for ADR-001 consistency
|
||||
check-consistency-all:
|
||||
$(UV) run python scripts/consistency_check.py --all $(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Check and auto-fix all registered repos
|
||||
fix-consistency-all:
|
||||
$(UV) run python scripts/consistency_check.py --all --fix $(if $(API_BASE),--api-base "$(API_BASE)",); \
|
||||
e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e
|
||||
|
||||
## Cancel open tasks belonging to completed/archived workstreams.
|
||||
## Safe to run at any time; also suitable for a daily cron job.
|
||||
## Cron example: 0 3 * * * cd ~/state-hub && make cleanup-stale
|
||||
cleanup-stale:
|
||||
$(UV) run python scripts/cleanup_stale_tasks.py
|
||||
|
||||
## Install custodian post-commit sync hook into one repo: make install-hooks REPO=marki-docx
|
||||
install-hooks:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make install-hooks REPO=<slug>"; exit 1)
|
||||
bash scripts/install_hooks.sh --repo "$(REPO)"
|
||||
|
||||
## Install custodian post-commit sync hook into all active registered repos
|
||||
install-hooks-all:
|
||||
bash scripts/install_hooks.sh --all
|
||||
|
||||
## Remove custodian post-commit sync hook from one repo: make remove-hooks REPO=marki-docx
|
||||
remove-hooks:
|
||||
@test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make remove-hooks REPO=<slug>"; exit 1)
|
||||
bash scripts/install_hooks.sh --repo "$(REPO)" --remove
|
||||
|
||||
## Compare Gitea coulomb org repos against state-hub registered repos
|
||||
## Requires GITEA_TOKEN in env or .env: make gitea-inventory GITEA_TOKEN=<token>
|
||||
gitea-inventory:
|
||||
$(UV) run python scripts/gitea_inventory.py $(if $(JSON),--json)
|
||||
|
||||
## Register/update one repo from .repo-classification.yaml:
|
||||
## make register-from-classification REPO=state-hub
|
||||
## make register-from-classification PATH=/path/to/repo
|
||||
## Optional: DRY_RUN=1
|
||||
register-from-classification:
|
||||
@test -n "$(REPO)" -o -n "$(PATH)" || (echo "ERROR: REPO or PATH is required."; exit 1)
|
||||
$(UV) run python scripts/register_from_classification.py \
|
||||
$(if $(PATH),--repo-path "$(PATH)",--slug "$(REPO)") \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
## Bulk register/update all active repos with accessible local paths
|
||||
register-from-classification-all:
|
||||
$(UV) run python scripts/register_from_classification.py --bulk \
|
||||
$(if $(DRY_RUN),--dry-run,)
|
||||
|
||||
clean:
|
||||
$(COMPOSE) down -v
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.database import get_session
|
||||
from api.events import EventEnvelope, publish_event
|
||||
from api.models.managed_repo import ManagedRepo
|
||||
from api.models.workplan import Workplan
|
||||
from api.models.task import Task
|
||||
from api.services.historical_projection import frozen_cluster_binding, validate_frozen_cluster_rows
|
||||
from api.schemas.workplan import (
|
||||
WorkplanBindingsSync,
|
||||
WorkplanCreate,
|
||||
WorkplanRead,
|
||||
WorkplanUpdate,
|
||||
)
|
||||
from api.services.lifecycle import transition_workplan_status
|
||||
from api.services.legacy_compat import retire_legacy_route
|
||||
from api.services.wait_kind import annotate_workplans
|
||||
from api.work_record_flavor import (
|
||||
RESIDUAL_FLAVOR,
|
||||
WORK_RECORD_FLAVORS,
|
||||
normalize_flavor,
|
||||
)
|
||||
from api.workplan_status import (
|
||||
is_supported_workplan_status,
|
||||
normalize_workplan_status,
|
||||
ready_review_status,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/workstreams", tags=["workstreams"])
|
||||
workplan_router = APIRouter(prefix="/workplans", tags=["workplans"])
|
||||
|
||||
_INDEX_CACHE: dict[str, Any] | None = None
|
||||
_INDEX_CACHE_AT: float = 0.0
|
||||
_INDEX_TTL = 30.0
|
||||
_INDEX_REFRESH_TASK: asyncio.Task | None = None
|
||||
_INDEX_LAST_ERROR: str | None = None
|
||||
|
||||
_COMPLETED_WORKPLAN_EVENT = "org.statehub.workplan.completed"
|
||||
|
||||
|
||||
def _repo_path(repo: ManagedRepo) -> Path | None:
|
||||
hostname = socket.gethostname()
|
||||
candidates = []
|
||||
host_paths = repo.host_paths or {}
|
||||
if host_paths.get(hostname):
|
||||
candidates.append(host_paths[hostname])
|
||||
if repo.local_path:
|
||||
candidates.append(repo.local_path)
|
||||
for raw in candidates:
|
||||
path = Path(raw).expanduser()
|
||||
if path.is_dir():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def _frontmatter(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return {}
|
||||
if not text.startswith("---\n"):
|
||||
return {}
|
||||
end = text.find("\n---", 4)
|
||||
if end == -1:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return yaml.safe_load(text[4:end].strip()) or {}
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
|
||||
|
||||
def _legacy_key(method: str, route: str) -> str:
|
||||
return f"rest_api:{method} {route}"
|
||||
|
||||
|
||||
async def _list_workplans(
|
||||
*,
|
||||
topic_id: uuid.UUID | None,
|
||||
repo_id: uuid.UUID | None,
|
||||
repo_goal_id: uuid.UUID | None,
|
||||
status_filter: str | None,
|
||||
owner: str | None,
|
||||
slug: str | None,
|
||||
flavor: str | None = None,
|
||||
include_residuals: bool = True,
|
||||
session: AsyncSession,
|
||||
) -> list[Workplan]:
|
||||
q = select(Workplan)
|
||||
if topic_id:
|
||||
q = q.where(Workplan.topic_id == topic_id)
|
||||
if repo_id:
|
||||
q = q.where(Workplan.repo_id == repo_id)
|
||||
if repo_goal_id:
|
||||
q = q.where(Workplan.repo_goal_id == repo_goal_id)
|
||||
if status_filter:
|
||||
normalised_status = normalize_workplan_status(status_filter)
|
||||
if not is_supported_workplan_status(status_filter):
|
||||
raise HTTPException(status_code=422, detail=f"Unsupported workplan status '{status_filter}'")
|
||||
q = q.where(Workplan.status == normalised_status)
|
||||
if owner:
|
||||
q = q.where(Workplan.owner == owner)
|
||||
if slug:
|
||||
q = q.where(Workplan.slug == slug)
|
||||
wanted = normalize_flavor(flavor)
|
||||
if wanted is not None:
|
||||
if wanted not in WORK_RECORD_FLAVORS:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=(
|
||||
f"Unknown work-record flavor {wanted!r}; "
|
||||
f"expected one of {', '.join(WORK_RECORD_FLAVORS)}"
|
||||
),
|
||||
)
|
||||
q = q.where(Workplan.flavor == wanted)
|
||||
elif not include_residuals:
|
||||
q = q.where(or_(Workplan.flavor.is_(None), Workplan.flavor != RESIDUAL_FLAVOR))
|
||||
q = q.order_by(
|
||||
Workplan.planning_priority.asc().nullslast(),
|
||||
Workplan.planning_order.asc().nullslast(),
|
||||
Workplan.updated_at.desc(),
|
||||
)
|
||||
result = await session.execute(q)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def _build_workplan_index(session: AsyncSession) -> dict[str, Any]:
|
||||
result = await session.execute(
|
||||
select(ManagedRepo).where(ManagedRepo.status == "active").order_by(ManagedRepo.slug)
|
||||
)
|
||||
index: dict[str, Any] = {}
|
||||
for repo in result.scalars().all():
|
||||
root = _repo_path(repo)
|
||||
if root is None:
|
||||
continue
|
||||
for directory, archived in (
|
||||
(root / "workplans", False),
|
||||
(root / "workplans" / "archived", True),
|
||||
):
|
||||
if not directory.is_dir():
|
||||
continue
|
||||
for path in sorted(directory.glob("*.md")):
|
||||
data = _frontmatter(path)
|
||||
workplan_id = data.get("state_hub_workstream_id") or data.get("state_hub_workplan_id")
|
||||
if not workplan_id:
|
||||
continue
|
||||
file_status = normalize_workplan_status(data.get("status", ""))
|
||||
review = (
|
||||
ready_review_status(
|
||||
root,
|
||||
data.get("reviewed_against_commit"),
|
||||
data.get("context_paths"),
|
||||
)
|
||||
if file_status == "ready"
|
||||
else None
|
||||
)
|
||||
index[str(workplan_id)] = {
|
||||
"filename": path.name,
|
||||
"relative_path": str(path.relative_to(root)),
|
||||
"repo_slug": repo.slug,
|
||||
"archived": archived,
|
||||
"status": file_status or None,
|
||||
"needs_review": bool(review and review.needs_review),
|
||||
"health_labels": ["needs_review"] if review and review.needs_review else [],
|
||||
}
|
||||
await _merge_db_backing_index(session, index)
|
||||
return {"workplans": index, "workstreams": index}
|
||||
|
||||
|
||||
async def _merge_db_backing_index(session: AsyncSession, index: dict[str, Any]) -> None:
|
||||
"""Fill index gaps from DB-backed file bindings synced by fix-consistency."""
|
||||
result = await session.execute(
|
||||
select(Workplan, ManagedRepo.slug)
|
||||
.join(ManagedRepo, Workplan.repo_id == ManagedRepo.id)
|
||||
.where(Workplan.backing_filename.isnot(None))
|
||||
)
|
||||
for wp, repo_slug in result.all():
|
||||
key = str(wp.id)
|
||||
if key in index:
|
||||
continue
|
||||
index[key] = {
|
||||
"filename": wp.backing_filename,
|
||||
"relative_path": wp.backing_relative_path,
|
||||
"repo_slug": repo_slug,
|
||||
"archived": bool(wp.backing_archived),
|
||||
"status": normalize_workplan_status(wp.status) if wp.status else None,
|
||||
"needs_review": False,
|
||||
"health_labels": [],
|
||||
}
|
||||
|
||||
|
||||
def _invalidate_workplan_index_cache() -> None:
|
||||
global _INDEX_CACHE, _INDEX_CACHE_AT
|
||||
_INDEX_CACHE = None
|
||||
_INDEX_CACHE_AT = 0.0
|
||||
|
||||
|
||||
def _index_with_meta(*, stale: bool, refresh_in_progress: bool) -> dict[str, Any]:
|
||||
age = time.monotonic() - _INDEX_CACHE_AT if _INDEX_CACHE_AT else None
|
||||
return {
|
||||
**(_INDEX_CACHE or {"workplans": {}, "workstreams": {}}),
|
||||
"_meta": {
|
||||
"generated_at": _INDEX_CACHE.get("_meta", {}).get("generated_at") if _INDEX_CACHE else None,
|
||||
"stale": stale,
|
||||
"cache_age_seconds": round(age, 3) if age is not None else None,
|
||||
"refresh_in_progress": refresh_in_progress,
|
||||
"last_error": _INDEX_LAST_ERROR,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _refresh_workplan_index_background() -> None:
|
||||
global _INDEX_CACHE, _INDEX_CACHE_AT, _INDEX_LAST_ERROR
|
||||
from api.database import async_session_factory
|
||||
|
||||
try:
|
||||
async with async_session_factory() as session:
|
||||
index = await _build_workplan_index(session)
|
||||
index["_meta"] = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"stale": False,
|
||||
"cache_age_seconds": 0.0,
|
||||
"refresh_in_progress": False,
|
||||
"last_error": None,
|
||||
}
|
||||
_INDEX_CACHE = index
|
||||
_INDEX_CACHE_AT = time.monotonic()
|
||||
_INDEX_LAST_ERROR = None
|
||||
except Exception as exc:
|
||||
_INDEX_LAST_ERROR = str(exc)
|
||||
|
||||
|
||||
def _ensure_index_refresh_started() -> None:
|
||||
global _INDEX_REFRESH_TASK
|
||||
if _INDEX_REFRESH_TASK is not None and not _INDEX_REFRESH_TASK.done():
|
||||
return
|
||||
_INDEX_REFRESH_TASK = asyncio.create_task(_refresh_workplan_index_background())
|
||||
|
||||
|
||||
async def _workplan_index(
|
||||
*,
|
||||
refresh: bool,
|
||||
session: AsyncSession,
|
||||
) -> dict[str, Any]:
|
||||
"""Map file-backed workplan ids to their local workplan filenames."""
|
||||
global _INDEX_CACHE, _INDEX_CACHE_AT, _INDEX_LAST_ERROR
|
||||
cache_age = time.monotonic() - _INDEX_CACHE_AT if _INDEX_CACHE_AT else None
|
||||
if not refresh and _INDEX_CACHE is not None and cache_age is not None and cache_age < _INDEX_TTL:
|
||||
refresh_running = _INDEX_REFRESH_TASK is not None and not _INDEX_REFRESH_TASK.done()
|
||||
return _index_with_meta(stale=False, refresh_in_progress=refresh_running)
|
||||
|
||||
if not refresh and _INDEX_CACHE is not None:
|
||||
_ensure_index_refresh_started()
|
||||
return _index_with_meta(stale=True, refresh_in_progress=True)
|
||||
|
||||
index = await _build_workplan_index(session)
|
||||
index["_meta"] = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"stale": False,
|
||||
"cache_age_seconds": 0.0,
|
||||
"refresh_in_progress": False,
|
||||
"last_error": None,
|
||||
}
|
||||
_INDEX_CACHE = index
|
||||
_INDEX_CACHE_AT = time.monotonic()
|
||||
_INDEX_LAST_ERROR = None
|
||||
return _INDEX_CACHE
|
||||
|
||||
|
||||
async def _create_workplan(
|
||||
*,
|
||||
body: WorkplanCreate,
|
||||
session: AsyncSession,
|
||||
) -> Workplan:
|
||||
wp = Workplan(**body.model_dump())
|
||||
session.add(wp)
|
||||
await session.commit()
|
||||
await session.refresh(wp)
|
||||
return wp
|
||||
|
||||
|
||||
async def _get_workplan(
|
||||
*,
|
||||
workplan_id: uuid.UUID,
|
||||
session: AsyncSession,
|
||||
) -> Workplan:
|
||||
wp = await session.get(Workplan, workplan_id)
|
||||
if wp is None:
|
||||
raise HTTPException(status_code=404, detail="Workplan not found")
|
||||
return wp
|
||||
|
||||
|
||||
async def _update_workplan(
|
||||
*,
|
||||
workplan_id: uuid.UUID,
|
||||
body: WorkplanUpdate,
|
||||
session: AsyncSession,
|
||||
) -> Workplan:
|
||||
wp = await session.get(Workplan, workplan_id)
|
||||
if wp is None:
|
||||
raise HTTPException(status_code=404, detail="Workplan not found")
|
||||
update_data = body.model_dump(exclude_unset=True)
|
||||
status_update = update_data.pop("status", None)
|
||||
prev_status = wp.status
|
||||
for field, value in update_data.items():
|
||||
setattr(wp, field, value)
|
||||
if status_update is not None:
|
||||
transition_workplan_status(wp, status_update)
|
||||
await session.commit()
|
||||
await session.refresh(wp)
|
||||
|
||||
if normalize_workplan_status(prev_status) != "finished" and wp.status == "finished":
|
||||
await _publish_completion_events(wp, session)
|
||||
|
||||
return wp
|
||||
|
||||
|
||||
async def _archive_workplan(
|
||||
*,
|
||||
workplan_id: uuid.UUID,
|
||||
session: AsyncSession,
|
||||
) -> Workplan:
|
||||
wp = await session.get(Workplan, workplan_id)
|
||||
if wp is None:
|
||||
raise HTTPException(status_code=404, detail="Workplan not found")
|
||||
transition_workplan_status(wp, "archived")
|
||||
await session.commit()
|
||||
await session.refresh(wp)
|
||||
return wp
|
||||
|
||||
|
||||
async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> None:
|
||||
workplan_envelope = EventEnvelope.new(
|
||||
_COMPLETED_WORKPLAN_EVENT,
|
||||
attributes={
|
||||
"workplan_id": str(wp.id),
|
||||
"slug": wp.slug,
|
||||
"title": wp.title,
|
||||
"topic_id": str(wp.topic_id) if wp.topic_id else None,
|
||||
"repo_id": str(wp.repo_id) if wp.repo_id else None,
|
||||
"repo_goal_id": str(wp.repo_goal_id) if wp.repo_goal_id else None,
|
||||
},
|
||||
)
|
||||
asyncio.create_task(publish_event(_COMPLETED_WORKPLAN_EVENT, workplan_envelope))
|
||||
|
||||
|
||||
@router.get("/", status_code=status.HTTP_410_GONE)
|
||||
async def list_workstreams(
|
||||
request: Request,
|
||||
response: Response,
|
||||
topic_id: uuid.UUID | None = None,
|
||||
repo_id: uuid.UUID | None = None,
|
||||
repo_goal_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
slug: str | None = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/"),
|
||||
replacement_ref="/workplans/",
|
||||
detail="Legacy GET /workstreams/ retired; use GET /workplans/",
|
||||
)
|
||||
|
||||
|
||||
@workplan_router.get("/", response_model=list[WorkplanRead])
|
||||
async def list_workplans(
|
||||
topic_id: uuid.UUID | None = None,
|
||||
repo_id: uuid.UUID | None = None,
|
||||
repo_goal_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
slug: str | None = None,
|
||||
flavor: str | None = Query(None),
|
||||
include_residuals: bool = Query(True),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[Workplan]:
|
||||
workplans = await _list_workplans(
|
||||
topic_id=topic_id,
|
||||
repo_id=repo_id,
|
||||
repo_goal_id=repo_goal_id,
|
||||
status_filter=status,
|
||||
owner=owner,
|
||||
slug=slug,
|
||||
flavor=flavor,
|
||||
include_residuals=include_residuals,
|
||||
session=session,
|
||||
)
|
||||
return list(await annotate_workplans(session, workplans))
|
||||
|
||||
|
||||
@router.get("/workplan-index", status_code=status.HTTP_410_GONE)
|
||||
async def workplan_index(
|
||||
request: Request,
|
||||
response: Response,
|
||||
refresh: bool = Query(False, description="Force cache invalidation"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/workplan-index"),
|
||||
replacement_ref="/workplans/index",
|
||||
detail="Legacy GET /workstreams/workplan-index retired; use GET /workplans/index",
|
||||
)
|
||||
|
||||
|
||||
@workplan_router.get("/index")
|
||||
async def workplan_index_preferred(
|
||||
refresh: bool = Query(False, description="Force cache invalidation"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
return await _workplan_index(refresh=refresh, session=session)
|
||||
|
||||
|
||||
@workplan_router.put("/index/bindings")
|
||||
async def sync_workplan_bindings(
|
||||
body: WorkplanBindingsSync,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, int]:
|
||||
"""Upsert workstation workplan file bindings for remote API index fallback."""
|
||||
# Preflight the complete batch before assigning even an ordinary row.
|
||||
try:
|
||||
frozen = {entry.workplan_id: bound for entry in body.bindings
|
||||
if (bound := frozen_cluster_binding(entry.model_dump())) is not None}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
requested_ids = {entry.workplan_id for entry in body.bindings}
|
||||
rows = await session.execute(
|
||||
select(Workplan).where(Workplan.id.in_(requested_ids))
|
||||
.order_by(Workplan.id).with_for_update().execution_options(populate_existing=True)
|
||||
)
|
||||
workplans = {workplan.id: workplan for workplan in rows.scalars().all()}
|
||||
if frozen:
|
||||
repo_rows = await session.execute(
|
||||
select(ManagedRepo).where(ManagedRepo.slug == "railiance-cluster")
|
||||
)
|
||||
repo = repo_rows.scalar_one_or_none()
|
||||
task_rows = await session.execute(
|
||||
select(Task).where(Task.workplan_id.in_(frozen))
|
||||
.order_by(Task.id).with_for_update().execution_options(populate_existing=True)
|
||||
)
|
||||
tasks = task_rows.scalars().all()
|
||||
try:
|
||||
for workplan_id, bound in frozen.items():
|
||||
validate_frozen_cluster_rows(
|
||||
bound, workplans.get(workplan_id), tasks, repo.id if repo else None
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
synced_at = datetime.now(timezone.utc)
|
||||
updated = 0
|
||||
preserved = 0
|
||||
for entry in body.bindings:
|
||||
if entry.workplan_id in frozen:
|
||||
preserved += 1
|
||||
continue # No setters, including backing_synced_at/updated_at.
|
||||
wp = workplans.get(entry.workplan_id)
|
||||
if wp is None:
|
||||
continue
|
||||
wp.backing_filename = entry.filename
|
||||
wp.backing_relative_path = entry.relative_path
|
||||
wp.backing_archived = entry.archived
|
||||
wp.backing_synced_at = synced_at
|
||||
updated += 1
|
||||
if updated:
|
||||
await session.commit()
|
||||
_invalidate_workplan_index_cache()
|
||||
return {"updated": updated, "received": len(body.bindings), "preserved": preserved}
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_410_GONE)
|
||||
async def create_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
body: WorkplanCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("POST", "/workstreams/"),
|
||||
replacement_ref="/workplans/",
|
||||
detail="Legacy POST /workstreams/ retired; use POST /workplans/",
|
||||
)
|
||||
|
||||
|
||||
@workplan_router.post("/", response_model=WorkplanRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_workplan(
|
||||
body: WorkplanCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
return await _create_workplan(body=body, session=session)
|
||||
|
||||
|
||||
@router.get("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
||||
async def get_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/{workstream_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}",
|
||||
detail="Legacy GET /workstreams/{workstream_id} retired; use GET /workplans/{workplan_id}",
|
||||
)
|
||||
|
||||
|
||||
@workplan_router.get("/{workplan_id}", response_model=WorkplanRead)
|
||||
async def get_workplan(
|
||||
workplan_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
wp = await _get_workplan(workplan_id=workplan_id, session=session)
|
||||
await annotate_workplans(session, [wp])
|
||||
return wp
|
||||
|
||||
|
||||
@router.patch("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
||||
async def update_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
body: WorkplanUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("PATCH", "/workstreams/{workstream_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}",
|
||||
detail="Legacy PATCH /workstreams/{workstream_id} retired; use PATCH /workplans/{workplan_id}",
|
||||
)
|
||||
|
||||
|
||||
@workplan_router.patch("/{workplan_id}", response_model=WorkplanRead)
|
||||
async def update_workplan(
|
||||
workplan_id: uuid.UUID,
|
||||
body: WorkplanUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
return await _update_workplan(workplan_id=workplan_id, body=body, session=session)
|
||||
|
||||
|
||||
@router.delete("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
||||
async def archive_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}"),
|
||||
replacement_ref="DELETE /workplans/{workplan_id}",
|
||||
detail="Legacy DELETE /workstreams/{workstream_id} retired; use DELETE /workplans/{workplan_id}",
|
||||
)
|
||||
|
||||
|
||||
@workplan_router.delete("/{workplan_id}", response_model=WorkplanRead)
|
||||
async def archive_workplan(
|
||||
workplan_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
return await _archive_workplan(workplan_id=workplan_id, session=session)
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
"""Exact closed source bindings predating canonical Forge identity.
|
||||
|
||||
These are frozen historical source views, not aliases or UUID allocation rules.
|
||||
An altered/missing source requires explicit owner review; generic source UUIDs
|
||||
remain untrusted. Shared Activity views represent the one archived parent.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from hashlib import sha256
|
||||
import json
|
||||
|
||||
BINDINGS = json.loads(r'''[
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/archived/260707-custodian-WP-0003-event-bridge.md",
|
||||
"sha256": "83ea8bcb151e003419b73c59dbd028f4a1ce3d1120a82e4a128a772503ff3b76",
|
||||
"record_id": "custodian-WP-0003",
|
||||
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
|
||||
"parent_record_id": "custodian-WP-0003"
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/custodian-WP-0001-temporal-backbone.md",
|
||||
"sha256": "020e2eac973f2daa41c1687f8e867da9d521f945547e6aa82c87993b3ff6fc64",
|
||||
"record_id": "custodian-WP-0001",
|
||||
"uuid": "e22516cc-4fdf-4dad-a928-a9e04a9fa7c7",
|
||||
"parent_record_id": "custodian-WP-0001"
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/custodian-WP-0002-triggers-ops.md",
|
||||
"sha256": "d8cbc61631c01b6f11a03a63f2fb6275fc853ff7f7459ce65973ed1a233daa01",
|
||||
"record_id": "custodian-WP-0002",
|
||||
"uuid": "3a4f47d9-8bc1-434e-acb4-bed5d4dacda0",
|
||||
"parent_record_id": "custodian-WP-0002"
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/custodian-WP-0003a-event-bridge-model-rules-registry.md",
|
||||
"sha256": "ffa6682e9ba3aaf559bd44203d561c90dea69bcf1b1e778209a7b209e6e5cdd3",
|
||||
"record_id": "custodian-WP-0003a",
|
||||
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
|
||||
"parent_record_id": "custodian-WP-0003"
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/custodian-WP-0003b-event-bridge-parser-workflow-triggers.md",
|
||||
"sha256": "4fc34e0f821e2df51e5c08c7ab34da35b65690381d01f0cfdf55d00c970b414c",
|
||||
"record_id": "custodian-WP-0003b",
|
||||
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
|
||||
"parent_record_id": "custodian-WP-0003"
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/custodian-WP-0003c-event-bridge-context-integration.md",
|
||||
"sha256": "b6cd62b26515e509670ca39736b6bcec9633beedd46c9bd8f7598c06bac7276b",
|
||||
"record_id": "custodian-WP-0003c",
|
||||
"uuid": "b4eb45a9-69e3-4ab0-b00c-67a53c3117c5",
|
||||
"parent_record_id": "custodian-WP-0003"
|
||||
},
|
||||
{
|
||||
"repo": "activity-core",
|
||||
"path": "workplans/custodian-WP-0004-railiance-ops.md",
|
||||
"sha256": "20df02eea740e109c937130e7f8801a636fc0990ed8c6d2ced55659f8957adb1",
|
||||
"record_id": "custodian-WP-0004",
|
||||
"uuid": "759b1255-aa78-42b5-8fab-c5fcf168f5f4",
|
||||
"parent_record_id": "custodian-WP-0004"
|
||||
},
|
||||
{
|
||||
"repo": "the-custodian",
|
||||
"path": "workplans/ADHOC-2026-08-25-retired.md",
|
||||
"sha256": "344a1293dbb3a67a944eb34ca4a0337487d8f047226027a2107922346fbab2dd",
|
||||
"record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED",
|
||||
"uuid": "1c53d3db-6633-5e2d-987f-10706d73be9e",
|
||||
"parent_record_id": "CUST-WP-ADHOC-2026-08-25-RETIRED"
|
||||
},
|
||||
{
|
||||
"repo": "railiance-cluster",
|
||||
"path": "workplans/archived/260622-RAIL-BS-WP-0002-k3s-baseline.md",
|
||||
"sha256": "8d6f8533c119117686bc0d5bb9ed5349726aa6c3189ff98b53409c68ac685ca9",
|
||||
"record_id": "RAIL-BS-WP-0002",
|
||||
"uuid": "4c63dfc6-9eac-4e79-9f77-8f644ad7147d",
|
||||
"parent_record_id": "RAIL-BS-WP-0002",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "T01",
|
||||
"uuid": "3f042630-eab0-4c6a-9167-e2b28ff20e40",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T02",
|
||||
"uuid": "e8510646-46ed-4697-a345-f3d3009eea78",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T03",
|
||||
"uuid": "dab2c07f-8aa0-4635-8df6-857e87e93fc5",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T04",
|
||||
"uuid": "5c3d40e4-239b-488e-9519-6f7a38d2325f",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T05",
|
||||
"uuid": "7f9e0e58-a130-467a-a2d0-b3f2564e496f",
|
||||
"status": "done"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "railiance-cluster",
|
||||
"path": "workplans/archived/260622-RAIL-BS-WP-0003-pgpool-ha-failover-fix.md",
|
||||
"sha256": "4dc2f61e15adc3c5c81f9ecf8b933512275b95e8f829fac52ae7cde1385cea4a",
|
||||
"record_id": "RAIL-BS-WP-0003",
|
||||
"uuid": "7ee9ee22-1fae-4567-9194-8d70a9e0f45b",
|
||||
"parent_record_id": "RAIL-BS-WP-0003",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "T01",
|
||||
"uuid": "6841c93a-f146-47eb-9f7c-8fa0e02c1bbc",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T02",
|
||||
"uuid": "ab166073-30a7-4702-a037-4091e8706e20",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T03",
|
||||
"uuid": "140da396-8e30-4f4d-b88c-c42c0cd46c01",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T04",
|
||||
"uuid": "d8a3ba40-fda0-4c1f-a9f1-ffcd621a5b3d",
|
||||
"status": "done"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"repo": "railiance-cluster",
|
||||
"path": "workplans/archived/260622-RAIL-BS-WP-0004-safety-net.md",
|
||||
"sha256": "04f787078232395bd2d43861de16c521848559fa286f06e1caa9cb779aa71ac3",
|
||||
"record_id": "RAIL-BS-WP-0004",
|
||||
"uuid": "7e8b0c20-51eb-40c9-9e3b-85dd380d7625",
|
||||
"parent_record_id": "RAIL-BS-WP-0004",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "T01",
|
||||
"uuid": "4526a842-ea31-4874-9231-92ab556cfe7b",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T02",
|
||||
"uuid": "a6313e06-1976-46a7-8e31-df4eb2eca880",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T03",
|
||||
"uuid": "05d42a55-921f-4aa7-bb76-e8af9c7e0ac3",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T04",
|
||||
"uuid": "08233868-d522-4117-bc4e-6c0f52545665",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T05",
|
||||
"uuid": "2d5acff7-4a4e-4ddd-ad06-08237ad3dac8",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"label": "T06",
|
||||
"uuid": "f8e4a094-c367-40eb-b895-da17bc144b07",
|
||||
"status": "done"
|
||||
}
|
||||
]
|
||||
}
|
||||
]''')
|
||||
|
||||
|
||||
def bound_sources(repo_root: Path, repo_slug: str):
|
||||
entries = [entry for entry in BINDINGS if entry["repo"] == repo_slug]
|
||||
result = []
|
||||
for entry in entries:
|
||||
path = repo_root / entry["path"]
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("Historical source binding missing or symlinked: " + entry["path"])
|
||||
data = path.read_bytes()
|
||||
if sha256(data).hexdigest() != entry["sha256"]:
|
||||
raise ValueError("Historical source binding changed: " + entry["path"])
|
||||
result.append((entry, data.decode("utf-8")))
|
||||
return result
|
||||
|
||||
|
||||
# Only the three existing full-task Cluster bindings have immutable DB rows.
|
||||
# Other historical views retain their existing compatibility behavior.
|
||||
FROZEN_CLUSTER_BINDINGS = tuple(
|
||||
entry for entry in BINDINGS
|
||||
if entry["repo"] == "railiance-cluster" and "tasks" in entry
|
||||
)
|
||||
|
||||
|
||||
def frozen_cluster_binding(binding):
|
||||
"""Validate a claimed frozen binding; unrelated requests remain ordinary."""
|
||||
matches = [entry for entry in FROZEN_CLUSTER_BINDINGS
|
||||
if str(binding["workplan_id"]) == entry["uuid"]
|
||||
or (binding["repo_slug"] == "railiance-cluster"
|
||||
and (binding["relative_path"] == entry["path"]
|
||||
or binding["filename"] == Path(entry["path"]).name))]
|
||||
if not matches:
|
||||
return None
|
||||
if len(matches) != 1:
|
||||
raise ValueError("frozen historical binding is ambiguous")
|
||||
entry = matches[0]
|
||||
expected = {
|
||||
"workplan_id": entry["uuid"], "repo_slug": entry["repo"],
|
||||
"filename": Path(entry["path"]).name, "relative_path": entry["path"],
|
||||
"archived": True, "status": "finished",
|
||||
}
|
||||
if any((str(binding[key]) if key == "workplan_id" else binding[key]) != value
|
||||
for key, value in expected.items()):
|
||||
raise ValueError("frozen historical binding metadata changed")
|
||||
return entry
|
||||
|
||||
|
||||
def validate_frozen_cluster_rows(entry, parent, tasks, repo_id):
|
||||
"""Receiving requires the exact existing terminal closure, never repair."""
|
||||
if (parent is None or str(parent.id) != entry["uuid"]
|
||||
or repo_id is None or parent.repo_id != repo_id
|
||||
or parent.status not in {"done", "finished", "archived", "completed"}):
|
||||
raise ValueError("frozen historical parent missing, foreign or nonterminal")
|
||||
local = {str(task.id): task for task in tasks if task.workplan_id == parent.id}
|
||||
if any(task["uuid"] not in local or local[task["uuid"]].status not in {"done", "cancel"}
|
||||
for task in entry["tasks"]):
|
||||
raise ValueError("frozen historical task missing, foreign or nonterminal")
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v2
|
||||
name: state-hub
|
||||
description: State Hub API service for private Railiance operation
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "main-51bc70d"
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# GENERATED — do not edit.
|
||||
# Synced from the-custodian/canon/standards/repo-classification.allowed.yaml
|
||||
# by scripts/sync_classification_allowed.py (CUST-WP-0067-T09).
|
||||
# Canon is authoritative; this copy exists only so the container has one.
|
||||
# Machine-readable allowed-values for the Repo Classification Standard.
|
||||
#
|
||||
# Single source of truth for the standard's controlled vocabularies, derived
|
||||
# from canon/standards/repo-classification-standard_v1.0.md. Consumed by:
|
||||
# - the per-repo .repo-classification.yaml linter (tools/validate_repo_classification.py)
|
||||
# - the State Hub registration validator (CUST-WP-0050 T04)
|
||||
#
|
||||
# When the standard's vocabularies change, update this file and bump `version`
|
||||
# to match the standard version. CUST-WP-0050 T01.
|
||||
|
||||
standard: "Repo Classification Standard"
|
||||
version: "1.1"
|
||||
canon_id: "canon-repo-classification"
|
||||
|
||||
# category — exactly 1 required (§5)
|
||||
categories:
|
||||
- experimental
|
||||
- research
|
||||
- project
|
||||
- tooling
|
||||
- product
|
||||
- business
|
||||
- publication
|
||||
|
||||
# domain / secondary_domains — primary exactly 1; secondaries 0..n (§6)
|
||||
domains:
|
||||
- infotech
|
||||
- financials
|
||||
- communication
|
||||
- consumer
|
||||
- health
|
||||
- industrials
|
||||
- energy
|
||||
- utilities
|
||||
- materials
|
||||
- realestate
|
||||
- crypto
|
||||
- agents
|
||||
- space
|
||||
- government
|
||||
|
||||
# business_stake — 0..n; 2..6 recommended (§8)
|
||||
business_stake:
|
||||
- execution
|
||||
- intelligence
|
||||
- finance
|
||||
- legal
|
||||
- sales
|
||||
- experience
|
||||
- technology
|
||||
- operations
|
||||
- product
|
||||
- people
|
||||
- procurement
|
||||
- sustainability
|
||||
- automation
|
||||
|
||||
# business_mechanics — 0..n, optional (§9)
|
||||
business_mechanics:
|
||||
- intention
|
||||
- control
|
||||
- coordination
|
||||
- operation
|
||||
- adaptation
|
||||
|
||||
# capability_tags are intentionally OPEN-ENDED (§7): lowercase kebab-case, not
|
||||
# restricted to this set. The families below are the standard's recommended
|
||||
# canonical tags — used to warn on likely synonyms/typos, never to reject.
|
||||
capability_families:
|
||||
identity_and_access:
|
||||
- identity
|
||||
- authentication
|
||||
- authorization
|
||||
- access-control
|
||||
- user-management
|
||||
- tenancy
|
||||
knowledge_and_evidence:
|
||||
- knowledge
|
||||
- citations
|
||||
- evidence
|
||||
- source-management
|
||||
- traceability
|
||||
- documentation
|
||||
- decision-support
|
||||
- history
|
||||
- participation
|
||||
platform_and_operations:
|
||||
- platform
|
||||
- deployment
|
||||
- operations
|
||||
- observability
|
||||
- feature-control
|
||||
- configuration
|
||||
- orchestration
|
||||
market_and_coordination:
|
||||
- marketplace
|
||||
- pricing
|
||||
- monetization
|
||||
- reputation
|
||||
- challenges
|
||||
- bounties
|
||||
- collaboration
|
||||
- coordination
|
||||
product_and_lifecycle:
|
||||
- product-development
|
||||
- lifecycle
|
||||
governance_and_control:
|
||||
- governance
|
||||
- policy
|
||||
- compliance
|
||||
- risk
|
||||
- audit
|
||||
- control
|
||||
games_and_entertainment:
|
||||
- game
|
||||
- entertainment
|
||||
|
||||
# Validation guidance (advisory bounds the linter applies as warnings)
|
||||
guidance:
|
||||
secondary_domains_max: 3
|
||||
business_stake_recommended_min: 2
|
||||
business_stake_recommended_max: 6
|
||||
capability_tag_pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Production values for the State Hub Railiance chart handoff.
|
||||
# Non-secret values only. DATABASE_URL comes from the Secret `state-hub-env`.
|
||||
|
||||
namespace:
|
||||
create: false
|
||||
|
||||
image:
|
||||
repository: forgejo.coulomb.social/coulomb/state-hub
|
||||
tag: "main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
# MCP layer on central (CUST-WP-0067-T08). Enabled together with the image tag
|
||||
# above: MCP_HOST landed in main-6c1262e, and enabling this on an older image
|
||||
# would deploy a pod that binds loopback and never becomes reachable.
|
||||
# ClusterIP only — no ingress, same reason the API's is disabled.
|
||||
mcp:
|
||||
enabled: true
|
||||
resources:
|
||||
requests:
|
||||
# 2026-09-10: 1,078 observed 5-minute samples, p99 2.35m, peak 30.45m.
|
||||
# Preserve the 500m limit for bursts; free 40m for the API rollout surge.
|
||||
cpu: 10m
|
||||
|
||||
config:
|
||||
# This deployment is the authoritative hub. Callers verify this rather than
|
||||
# trusting that whatever answered on a port is central (CUST-WP-0067-T03).
|
||||
instanceRole: primary
|
||||
instanceLabel: railiance01
|
||||
sbomNexusUrl: "http://sbom-nexus.sbom-nexus.svc.cluster.local:8010"
|
||||
# Reversible T04 read cutover; set back to `legacy` to roll back.
|
||||
sbomNexusReadMode: nexus
|
||||
# Reversible T04 write cutover; set back to `legacy` to roll back future writes.
|
||||
sbomNexusWriteMode: nexus
|
||||
|
||||
resources:
|
||||
# Restored to the chart's 250m on 2026-09-21. The earlier 100m was a workaround
|
||||
# for a node with <250m unallocated; ~830m of Knative/Kourier requests were
|
||||
# released that day (the-custodian docs/kubernetes-change-gate-decision.md).
|
||||
# State Hub idles near 5m but bursts past 400m under concurrent agent load.
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
|
||||
sweep:
|
||||
# RMGR-WP-0005-T11: disabled while railiance01 checkouts still target the
|
||||
# stale gitea-remote lineage. Re-enable only after the governed remote
|
||||
# reconciliation and registrar preflight are complete.
|
||||
enabled: false
|
||||
# Present in the live release but previously missing here, so deploying from
|
||||
# this file alone silently dropped it. Inert while enabled is false.
|
||||
hostname: 239.62.205.92.host.secureserver.net
|
||||
hostPath: /home/tegwick
|
||||
sshHostPath: /home/tegwick/.ssh
|
||||
|
||||
# Forge read credential (STATE-WP-0084-T02). Coordinates only — the token lives
|
||||
# in OpenBao at platform/workloads/state-hub/forge-derivation and reaches the
|
||||
# pod through Kubernetes auth, never through this file or a Secret.
|
||||
forgeRead:
|
||||
enabled: true
|
||||
|
||||
# RPF-WP-0035-T04: dedicated ESO signing key, API-only explicit reference.
|
||||
renamePreflight:
|
||||
enabled: true
|
||||
secretName: state-hub-rename-preflight
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"schema": "statehub.cluster-historical-receiving-delivered.v1",
|
||||
"status": "verified",
|
||||
"proposal_id": "STATE-WP-0095-T06-cluster-frozen-receiving-r1",
|
||||
"approved_revision": 4,
|
||||
"approved_digest_sha256": "sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32",
|
||||
"approval_decision_id": "e3798419-8aa9-5425-8c08-20df6bd82a19",
|
||||
"source_revision": "51bc70d1ff8f25450a8650d8e50caf053093a02e",
|
||||
"readiness_source_revision": "086d79a2736e34b916bd7de3c8a65e2ea38a3d64",
|
||||
"helm_revision": 76,
|
||||
"image_reference": "forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015",
|
||||
"expected_schema": "c4d5e6f7a8b9",
|
||||
"health": {
|
||||
"status": "ok",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"schema": {
|
||||
"status": "ok",
|
||||
"applied": "c4d5e6f7a8b9",
|
||||
"expected": "c4d5e6f7a8b9"
|
||||
}
|
||||
},
|
||||
"pods_ready": true,
|
||||
"historical_module_sha256": "sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2",
|
||||
"historical_preservation_verified": true,
|
||||
"parents": 3,
|
||||
"tasks": 15,
|
||||
"full_historical_rows_equal": true,
|
||||
"native_rollback_probes": {
|
||||
"foreign-holder": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"missing-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-parent": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"preserve": {
|
||||
"full_historical_rows_equal": true,
|
||||
"full_historical_rows_restored": true,
|
||||
"native_outcome_status": "applied",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
}
|
||||
},
|
||||
"native_probe_commits": 0,
|
||||
"fresh_transaction_full_rows_equal": true,
|
||||
"source_receiving_gate_delivered": true,
|
||||
"normal_cluster_publication_verified": false,
|
||||
"registrar_admission_or_cluster_publication_not_inferred": true,
|
||||
"authority_limits": [
|
||||
"Rollback probe SQL effects rolled back; foreign-holder case is historical-binding refusal, not separate global holder branch.",
|
||||
"Actual historical native receiving is verified; owner-native TLS/private admission remains unresolved.",
|
||||
"Migration schema remains additive; Helm rollback does not downgrade schema or stop an active hook Job."
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/approval-decision.json",
|
||||
"sha256": "sha256:b51554c004daa882f3709f67bdae7f2e939d611120383e61be04ec6969878134"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/helm-return.json",
|
||||
"sha256": "sha256:61727deacf22d37da2d28cbaf04f5a3ddee0f9ff7571c77f1e85dba97963b572"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/delivered-runtime.json",
|
||||
"sha256": "sha256:89681066e8fe139456163ebfafa3efe77ffae8534ea167219230b0a74367f129"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/native-refusal-probes.json",
|
||||
"sha256": "sha256:673ccef965c820704dc310a1bf7832b583c30c3edfeb814528cf892555199172"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/before-full-row-hashes.json",
|
||||
"sha256": "sha256:75cdd87eef79059ff3f484ed24ca69e285899cdf8d851cc4e472653c417eb0ee"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/after-full-row-hashes.json",
|
||||
"sha256": "sha256:2156aad18f190384838c27e792706a156a7423391408afbc6107e6c28c5872d8"
|
||||
}
|
||||
],
|
||||
"later_ordinary_cluster_publication": {
|
||||
"status": "failed_full_parent_row_preservation",
|
||||
"changed_parent_hashes": 3,
|
||||
"unchanged_task_hashes": 15,
|
||||
"exact_changed_columns": "unknown_hash_only_baseline",
|
||||
"original_parent_preimages": "unavailable_no_guessed_restoration",
|
||||
"receipt": "the-custodian/docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/post-publication-full-row-hashes-FAILED.json",
|
||||
"T07": "wait",
|
||||
"RMGR_WP_0019_T06": "wait",
|
||||
"followup": [
|
||||
"STATE-WP-0095-T09",
|
||||
"STATE-WP-0095-T10"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sync the canon classification allowed-values into the chart (CUST-WP-0067-T09).
|
||||
|
||||
The State Hub validates repository classification against
|
||||
``the-custodian/canon/standards/repo-classification.allowed.yaml``. That file is
|
||||
canon and must stay authoritative, but a container has no the-custodian
|
||||
checkout, so a copy has to travel with the release.
|
||||
|
||||
A copy that nobody checks is a copy that silently drifts. This script owns the
|
||||
copy: ``--check`` fails when it diverges from canon, so the release can refuse
|
||||
to ship a stale vocabulary rather than validate against yesterday's rules.
|
||||
|
||||
Usage:
|
||||
python scripts/sync_classification_allowed.py # write the copy
|
||||
python scripts/sync_classification_allowed.py --check # verify, exit 1 on drift
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CHART_COPY = (
|
||||
REPO_ROOT
|
||||
/ "deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml"
|
||||
)
|
||||
CANON_CANDIDATES = (
|
||||
Path("/home/worsch/the-custodian/canon/standards/repo-classification.allowed.yaml"),
|
||||
Path("/home/tegwick/the-custodian/canon/standards/repo-classification.allowed.yaml"),
|
||||
REPO_ROOT.parent / "the-custodian/canon/standards/repo-classification.allowed.yaml",
|
||||
)
|
||||
|
||||
HEADER = (
|
||||
"# GENERATED — do not edit.\n"
|
||||
"# Synced from the-custodian/canon/standards/repo-classification.allowed.yaml\n"
|
||||
"# by scripts/sync_classification_allowed.py (CUST-WP-0067-T09).\n"
|
||||
"# Canon is authoritative; this copy exists only so the container has one.\n"
|
||||
)
|
||||
|
||||
|
||||
def find_canon() -> Path:
|
||||
for candidate in CANON_CANDIDATES:
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
raise SystemExit(
|
||||
"ERROR: canon allowed-values not found. Looked in:\n "
|
||||
+ "\n ".join(str(c) for c in CANON_CANDIDATES)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Verify the chart copy matches canon; exit 1 on drift",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
canon = find_canon()
|
||||
desired = HEADER + canon.read_text(encoding="utf-8")
|
||||
|
||||
if args.check:
|
||||
if not CHART_COPY.is_file():
|
||||
print(f"DRIFT: {CHART_COPY} is missing; run without --check to create it")
|
||||
return 1
|
||||
current = CHART_COPY.read_text(encoding="utf-8")
|
||||
if current != desired:
|
||||
print(f"DRIFT: chart copy differs from canon ({canon})")
|
||||
sys.stdout.writelines(
|
||||
difflib.unified_diff(
|
||||
current.splitlines(keepends=True),
|
||||
desired.splitlines(keepends=True),
|
||||
fromfile="chart copy",
|
||||
tofile="canon",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
print(f"OK: chart copy matches canon ({canon})")
|
||||
return 0
|
||||
|
||||
CHART_COPY.parent.mkdir(parents=True, exist_ok=True)
|
||||
CHART_COPY.write_text(desired, encoding="utf-8")
|
||||
print(f"Synced {canon} -> {CHART_COPY}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue