net-kingdom/docs/bounded-execution-contract-table.md
tegwick 1798425be5
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
NK-WP-0058-T01 done: contract table part 1 (executor and its services, rapp packaging)
11 rows with sources at named revisions; 6 keep, 5 merge, 4 owner decisions routed to
NK-WP-0057. CI tests net-kingdom alone and rapp has no CI, so every cross-repo check is local or manual today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 943911@bnt-lap001
Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
2026-11-05 22:55:31 +01:00

16 KiB

Bounded execution: cross-repo contract table

NK-WP-0058-T01 (part 1), as of 2026-11-05. Parts 2 (T09: approval, case and custody chain) and 3 (T10: operations and the founder) add rows to this file. NK-WP-0057-T01 takes the rows marked owner decision as inputs and does not inventory them again.

Each row is one interface on the bounded-execution path. Every claim cites the file that defines it, at these revisions: net-kingdom 23a99c1, rapp-bounded-executor 3ecd462, approval-engine f41a9ca, informed-decision 77161fe, audit-core 3157454, key-cape 82b471f, railiance-platform 31cf74d.

Marks.

  • keep: one owner, one source, so it only needs a contract test (T02).
  • merge: the contract is written down in more than one place. Keep the named source and derive or check the copies.
  • delete: the piece is redundant.
  • owner decision: the owner or the boundary is unclear. It goes to NK-WP-0057 or to the named owner.

Tested today. The levels are:

  • CI: runs in a Forgejo workflow on every change;
  • local: a test that needs a sibling checkout and skips without one;
  • manual: an exercise script someone has to run;
  • live: proven only by a founder-run step;
  • none.

Two facts frame this column:

  • net-kingdom CI (.forgejo/workflows/identity-journeys.yaml) tests a source tarball of net-kingdom alone, so every sibling-dependent test skips there.
  • rapp-bounded-executor has no CI workflow.

Part 1: the executor and its services

ID Interface Owner and source of truth Consumer Tested today 2026-11-04/05 failure Mark
E1 OpenBao: Kubernetes login and KV, three lanes railiance-platform: CCR-2026-0058, -0059, -0061 and openbao/policies/ executor (operations.py OpenBao); executor-lanes.py creates and deletes the write role CI: fake OpenBao. Local: tests/test_executor_lanes.py against the CCRs Write role left open overnight; window-close failed as bare LaneError merge
E2 KeyCape client_credentials token key-cape: src/internal/server/oidc/token.go. Client registration: net-kingdom sso-mfa/k8s/keycape/ with keycape-client.py executor (clients.py KeyCapeClientCredentials) CI: fakes. Manual: exercise_approval_identity.py (T19) against KeyCape source/image none (the request phase worked) keep; registration ownership is an owner decision
E3 approval-engine: create, get and consume approval-engine: approval_engine/api.py, store.py executor (clients.py ApprovalEngine, executor.py _check_approval) CI: fakes. Manual: T19 against approval-engine source platform-root approval refused as approval_entry; finding which field failed took diagnostic pod 4 keep
E4 informed-decision: memo intake (present_existing) informed-decision: informed_decision/intake_service.py, deploy/lifecycle/intake-submitters.json executor (clients.py InformedDecision, cli.py admission_memo) Local: test_approval_request.py, path hard-coded to /home/worsch/informed-decision Memo reached platform-root only, so one window was wasted (routing, E5) keep
E5 The approver's identity across repos None. It is defined four times (below) executor _check_approval, informed-decision routing, the founder none One wasted window: the admission named admin, routing went to platform-root merge; owner decision (NK-WP-0057)
E6 audit-core: event ingest audit-core: docs/event-envelope.md, audit_core/ingestion.py, sender registry. Receipt schema: net-kingdom canon/schemas/execution-attribution-receipt_v0.1.schema.json executor (clients.py AuditCore) CI: fakes. Manual: exercise_audit_sender.py (T15) against audit-core source. Receipt validator not in a workflow none (receipt accepted 2026-11-05) keep
E7 LLDAP: sign-in and self password change LLDAP upstream. The runtime in sso has no owner in docs/responsibility-map.md; its reference manifests are in net-kingdom sso-mfa/k8s/ executor (operations.py Directory, the image's lldap_set_password) CI: fakes. Nothing pins the image's tool version against the server none keep; runtime ownership is an owner decision
E8 Kubernetes API: ESO force-sync and Deployment restart API: railiance-cluster. Role content: net-kingdom sso-mfa/k8s/bounded-executor/rbac.yaml, copied to rapp manifests/rbac.yaml executor (operations.py KubeDelivery, KubeRestart) Make only: test_rbac.py needs no sibling, but no workflow runs it. Local: rapp test_ceiling_is_the_net_kingdom_source none in the run (ESO server-side apply was a founder step, T10) merge
E9 Network path of the executor pod rapp manifests/networkpolicy.yaml (egress). Ingress belongs to each target: net-kingdom netpol-sso.yaml (lldap, keycape), approval-engine e9e9f93, audit-core 7728fde, informed-decision :8081, OpenBao executor wait_all_reachable, make netcheck Local: rapp test_every_in_namespace_egress_is_admitted_by_net_kingdom. Live: make netcheck Two windows refused setup:URLError; four diagnostic pods; live approval-engine had a policy its file lacked keep; add a cross-owner ingress test
R1 Image build and pin chain net-kingdom builds (.forgejo/workflows/bounded-executor-image.yaml); rapp pins (Makefile IMAGE_DIGEST); the admission pins source files (plan.source.commit and file sha256) renderer (tools/render_job.py), the executor's checks none: commit to digest is recorded only in the rapp commit message A re-pin and a new admission after c2a0f2f, all by hand merge
R2 Runtime config, Job template and exit codes net-kingdom bounded-executor/cli.py (config keys in its docstring, EXIT). Copies: rapp examples/config.pilot.json, docs/runtime-contract.md, render_job.py allow-lists rapp renderer, pilot-run.sh, the founder Local: rapp test_packaging.py reads cli.py (skips without sibling); no rapp CI pilot-run.sh edits trust_satisfied into a temporary copy of the config merge

Row details

E1: OpenBao lanes

  • Contract:
    • address http://openbao.openbao.svc:8200;
    • login POST /v1/auth/kubernetes/login with the projected token, audience openbao, from /var/run/secrets/openbao/token;
    • one role per lane, with leases of at most 15 minutes, each revoked at exit:
      • write: net-kingdom-bind-executor-svc-authelia-write, KV platform/workloads/net-kingdom/directory-binds/svc-authelia, field LDAP_BIND_PASSWORD, cas_required;
      • journey: net-kingdom-bind-executor-syn-journey-read, KV .../synthetic-journey/syn-journey, fields PASSWORD, TOTP_SEED;
      • approval client: keycape-nk-bind-executor-approval-bounded-executor-workload-kv-read, field CLIENT_SECRET, read once and revoked at once.
  • Window: the write role exists only between executor-lanes.py window-open and window-close.
  • Duplicates: role names, KV paths and fields appear in four places: the CCR files, executor-lanes.py LANES, rapp config.pilot.json and the rapp renderer's role allow-list. LANES also carries the write and journey policy text inline.
  • Proposal:
    • LANES reads names, paths and fields from the CCRs. The policy text is compared against railiance-platform's policy file, not kept as a second copy.
    • The renderer's allow-list is checked against the same source.
    • executor-lanes.py names the lane error's cause. On 2026-11-05 phase: identity, LaneError hid "no platform-admin login" (T03).

E2: KeyCape client

  • Contract:
    • POST http://keycape.sso.svc.cluster.local:8080/token with Host kc.coulomb.social, client_secret_basic, client nk-bind-executor-approval;
    • one scope per token (approval:create, approval:read, approval:consume);
    • the answer must be a bearer token with expires_in of at most 900;
    • a scope outside the list must be answered invalid_profile_usage.
  • Owners: KeyCape owns the token behaviour. The client's registration lives in net-kingdom sso-mfa/k8s/keycape/ (deployment env and keycape-client.py), and its case is security-cases/nk-bind-executor-approval.yaml (oidc-service-client).
  • Open:
    • excess-scope-refused is not yet proven against live KeyCape.
    • Owner decision: ADR-0015 moves runtime packages to rapp-*, but there is no KeyCape rapp, so the client registration sits in net-kingdom's reference manifests. Who owns registering a KeyCape client?

E3: approval-engine

  • Contract:
    • create: POST /v1/approvals answers 201. binding.actor must equal the token's sub (api.py); the executor sends human_control: true, pdp_path: false and required_count = the number of approvers.
    • read: GET /v1/approvals/{id}.
    • consume: POST /v1/approvals/{id}/consume with request_digest and decision_id.
    • the executor accepts an entry only if:
      • subject_id is a listed approver;
      • principal_type is human;
      • the assurance is aal2;
      • the subject is not the executor itself.
    • approval-engine also has required_assurance. The executor doesn't send it, and checks aal2 itself ("recorded, not enforced").
  • Proposal:
    • approval_entry names the failed field: subject, principal type or assurance. That alone would have replaced diagnostic pod 4.
    • Whether to send required_assurance is a question for approval-engine, not a local change.

E4: informed-decision intake

  • Contract:
    • POST /v1/memos on informed-decision-workload.informed-decision.svc:8081;
    • the bearer is the projected ServiceAccount token, audience informed-decision-intake, checked by TokenReview;
    • the registry row is system:serviceaccount:sso:nk-bind-executor → submitter nk-bind-executor, memo class bounded-execution, review group net-kingdom-admins, present_existing.approval_actor service:nk-bind-executor, daily limit 10;
    • the receipt must echo memo_id, approval_id and approval_binding_digest;
    • cli.py --request exits 7 when the memo is not accepted. Resubmitting is idempotent.
  • Test gap: bounded-executor/tests/test_approval_request.py:216 hard-codes /home/worsch/informed-decision (T07: no hard-coded paths).

E5: approver identity, defined four times

Where Value on 2026-11-05 Owner
net-kingdom admissions/BEA-2026-0003.yaml spec.approval.approvers [admin] (a name) net-kingdom (prepare_admission.py)
rapp examples/config.pilot.json approver_subjects, approver_issuer admin → uid=admin,ou=people,dc=netkingdom,dc=local, https://kc.coulomb.social rapp-bounded-executor
informed-decision deploy/lifecycle/review-groups.json net-kingdom-admins → uid=admin,… (interim, decision c9bedd9c; it was platform-root until 77161fe) informed-decision
key-cape LLDAP adapter sub = the LDAP DN key-cape

Nothing checks that these four agree, and a mismatch shows only as a refused approval after a window has opened. Until NK-WP-0057 decides who routes: the T03 readiness check compares them. The admission's approver names resolve through the rapp config and must equal the routing recipient for the memo's review group.

E6: audit-core

  • Contract:
    • POST /v1/events with Idempotency-Key equal to the event's id, and all eight envelope fields;
    • answers: 202 accepted, 200 duplicate, 400 dead-lettered with a named reason, and 409 for a reused run id with a different body (a defect, never retried);
    • the bearer is the file /var/run/secrets/bounded-executor/audit-core, from sso/bounded-executor-tokens key audit-core (custody in part 2);
    • the source is checked against the sender registry;
    • receipt schema v0.1 and its EAR rules (tools/execution-attribution-receipt).
  • Proposal: fold the T15 exercise into the T02 suite, and run make execution-attribution-receipt-test in CI.

E7: LLDAP

  • Contract:
    • http://lldap.sso.svc.cluster.local:17170;
    • sign-in checks over its HTTP API;
    • the password change runs the image's /app/lldap_set_password with --base-url, --token, --username (OPAQUE).
  • Gap: nothing checks that the tool in the executor image is compatible with the running LLDAP server.
  • Owner decision: docs/responsibility-map.md names no owner for the LLDAP, Authelia or KeyCape runtimes in sso. SCOPE says the sso-mfa/k8s/ material is "migration inputs, not blanket authority".

E8: Kubernetes API

  • Contract:
    • force-sync: merge-patch metadata.annotations.force-sync on /apis/external-secrets.io/v1/namespaces/sso/externalsecrets/authelia-secrets, then poll its status;
    • restart: merge-patch spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"] on Deployment authelia, then wait for the rollout;
    • Role: get/watch/patch on that Deployment and get/patch on that ExternalSecret. No Secret access.
  • Duplicate: the Role is written in net-kingdom and copied by hand into rapp (make ceiling-sync: "copy, never edit"). The compare test skips without the sibling checkout, and rapp has no CI.
  • Proposal: keep net-kingdom as the source, and make the compare test a T02 contract test that runs in CI.

E9: network path

  • Contract:
    • the executor pod is labelled app.kubernetes.io/name=bounded-executor in namespace sso;
    • egress per the rapp policy;
    • each target admits it on its own port: approval-engine and audit-core on 8080, informed-decision on 8081, KeyCape and LLDAP through netpol-sso.yaml, and OpenBao on 8200;
    • the executor waits up to 30 s for all five endpoints. A setup failure names the unreachable one (since c2a0f2f).
  • Gap: the ingress side lives in five repos, and only the sso part is compared. The live approval-engine policy differed from its file.
  • Proposal: a T02 test that renders every owner's ingress policy for this label, plus a live comparison in the T03 readiness check.

R1: image and pins

  • Chain:
    1. a net-kingdom commit;
    2. the Forgejo image workflow builds a digest;
    3. the digest is pinned by hand in rapp's Makefile IMAGE_DIGEST, with the source commit only in the rapp commit message;
    4. separately, prepare_admission.py --commit pins the admission to source file sha256s (plan.source.files).
  • Gap: no record links the digest to the commit to the admission pins, so "the admission's pins match the pinned image" cannot be checked.
  • Proposal:
    • the image workflow writes a value-free pin record (commit, file sha256s, digest);
    • rapp pins that record;
    • the renderer refuses when the admission's file hashes are not the image's (T03).

R2: config, Job and exit codes

  • Source: the config keys and exit codes are defined in cli.py: 0 completed, 3 reverted, 4 refused, 5 unknown, 6 not in custody, and 7 for a request whose memo was not accepted.
  • Copies:
    • rapp docs/runtime-contract.md (prose);
    • config.pilot.json (values);
    • render_job.py allow-lists (token paths, role names);
    • pilot-run.sh sets trust_satisfied by editing a temporary copy of the config.
  • Proposal:
    • a versioned config schema file in net-kingdom, which the renderer validates against;
    • the exit codes stated once, in that file;
    • trust_satisfied becomes a run input recorded in the state file (T04), not a config edit.

Summary for part 1

  • keep: E2, E3, E4, E6, E7, E9. Each needs its T02 contract test. The T15 and T19 exercises become CI tests.
  • merge:
    • E1: lane names and policy text, from the CCRs;
    • E5: the approver, compared until it has one source;
    • E8: the RBAC copy;
    • R1: the pin record;
    • R2: the config schema.
  • delete: nothing yet. The candidates are the hand copies named under merge, once they are derived or checked.
  • owner decision (to NK-WP-0057-T01 or the named owner):
    • E5: who owns the approver and routing;
    • E2: KeyCape client registration with no KeyCape rapp;
    • E7: the LLDAP, Authelia and KeyCape runtime owner in sso;
    • E3: whether approval-engine should enforce required_assurance.
  • Transparency findings for T03:
    • executor-lanes.py lane errors without a cause;
    • approval_entry without the failed field.
  • Hard-coded path for T07: test_approval_request.py:216.