diff --git a/credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml b/credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml new file mode 100644 index 0000000..42078fe --- /dev/null +++ b/credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml @@ -0,0 +1,155 @@ +id: CCR-2026-0046 +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-read +title: Reuse Surface exact seven private-source repository reader +status: proposed +created: '2026-10-15' +updated: '2026-10-15' +requester: + agent: platform-reader-owner delegated preparation worker + reason: CUST-WP-0081 hosted63/70; founder metadata consent excludes credential provisioning. + Prepare separate seven-source reader. +review: + required: true + required_approvers: + - platform-operator + - railiance-forge-owner + - reuse-surface-owner + - railiance-apps-owner + comments: [] +target: + domain: infotech + tenant: reuse + workload: reuse-surface + environment: production + purpose: Native acquisition of seven owner-consented pinned capability indexes. + Entire private repository code becomes readable by this dedicated reader; publication + remains only approved metadata. +openbao: + mount: platform + kv_path: platform/workloads/reuse/reuse-surface/forgejo-source-read + fields: + - FORGEJO_SOURCE_TOKEN + - API_USER + - API_BASE_URL + - TOKEN_SCOPES + - GENERATED_AT + - CCR_ID + policy_name: workload-kv-read-reuse-surface-runtime-eso + policy_file: openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl + auth: + method: kubernetes + mount: kubernetes + role: reuse-surface-runtime-eso + bound_claims: + service_account_names: + - reuse-surface-eso + service_account_namespaces: + - reuse + bound_claims_confirmed: false + policies: + - workload-kv-read-reuse-surface-runtime-eso + ttl: 15m + audience: openbao +access_frontdoor: + type: external-secrets + catalog_id: reuse-surface-forgejo-source-read + readiness: pending-review + resolvable: false +delivery: + surface: external-secrets + bootstrap_command: warden access openbao-platform-admin-login --exec -- python3 + scripts/provision-reuse-surface-forgejo-source.py --approval + --receipt <0600-durable-receipt> + provisioning_script: scripts/provision-reuse-surface-forgejo-source.py + target: Existing namespace-limited store openbao-reuse. Apps owner adds only proposed + data item into ExternalSecret reuse/reuse-surface-runtime. Shared target reuse-surface-env + remains intact. + forgejo_identity: reuse-surface-source + forgejo_team: reuse-surface-source-readers + repositories: + - coulomb/rapp-openbao + - coulomb/rapp-policy-nexus + - coulomb/rapp-postgres + - coulomb/rapp-sbom-nexus + - coulomb/rapp-telemetry + - coulomb/rapp-tenant-engine + - coulomb/rapp-user-engine + token_scopes: + - read:repository + consumer_gate: Reader must not be delivered until reuse-surface owner supplies verified + image digest containing BOTH authenticated redirect rejection/HTTPS enforcement + AND runtime-controlled exact URL allowlist checked before credential lookup. Apps + owner must deploy REUSE_SURFACE_AUTH_SOURCE_ALLOWLIST mapping the reader environment + to exactly fourteen consented pinned index/entry URLs and inventory existing authenticated + sources for migration. Current image and redirect-only fix are not approved. Native + owner helper must verify all seven pinned indexes and entry hashes after rollout. +risk: + classification: high + cost_estimate_eur: 0 + notes: + - Repository grants disclose entire source of these seven repositories to hub; PAT + cannot restrict access to index files. + - No all-repositories membership, unrelated private grants, admin privileges or + repository writes. + - Existing ESO role expands by one exact data-read path; writer OIDC caller remains + unchanged and denied new path. + - Current consumer accepts arbitrary authenticated source URLs and redirects, allowing + bearer disclosure. Delivery barred until full reviewed redirect/HTTPS and runtime-controlled + exact URL allowlist patch image plus config is verified. + - PAT has no server-side expiry; rotate quarterly and immediately upon incident. + - Bootstrap writes only exact new identity/team/PAT/KV. Policy apply, ESO delivery, + consumer rollout and registrations require their separate reviewed owner steps. + - R1 pagination defect preserved; R2 pages until empty with duplicate/count/bound + checks. Deployed-version live complete-grant inventory remains required; code + review is not live scope proof. + - Quarterly rotation is a proposed manual requirement, not deployed expiry enforcement; + due-date owner task/receipt and separate exact-token recovery approval must be + established before activation. +verification: + positive: + - All seven repositories private; exact team/code-read flags, one dedicated team + member, exact seven memberships, effective permissions read and exactly one read:repository + PAT. + - Each consented index digest matches from authenticated Forgejo read; final hosted + native owner helper verifies pinned entry hashes as well. + - OpenBao stdin CAS=0 field write readback matches without printing or persisting + value. + - ESO binding and namespace condition surveyed and confirmed; native fixed consumer + image verified before delivery; final hosted70/70 exactly once. + negative: + - Known excluded private rapp-core-hub reader request denied (403/404); administrative + endpoint and Actions secrets denied. + - No additional private effective repositories or teams; no write probing that could + itself cause a change. + - Scoped writer and default OpenBao identities denied new data path; ESO role denied + unrelated siblings and wrong SA/namespace/audience login refused. + - Missing/anonymous native authentication refused with no privileged fallback. + activation_conditions: + - Independent platform/Forgejo/reuse/apps owner reviews of exact artifacts and founder + CCR provisioning disposition recorded; no approval inferred from metadata consent. + - Confirm existing ESO Kubernetes binding before any delegated metadata apply. + - Consumer security patch image digest verified and apps rollout artifacts reviewed/approved + before delivery. + - Durable bounded bootstrap and positive/negative receipts pass; owner native fetch + proof passes all seven pinned sources. + - Separate scoped writer registration batch followed by fresh hosted70/70 completion + audit. + evidence: [] +lifecycle: + deactivate: Separate explicitly approved silent scripts/revoke-reuse-surface-forgejo-source.py + validates reviewed single PAT ID/name, admin PATCH only dedicated user to fresh + process-memory password, Basic-auth DELETE exact token, then reconciles complete + inventory absence. Admin PAT/Sudo alone cannot delete. KV/shared runtime Secret + preserved. Apps/platform owners remove only new ESO item/policy stanza through + their reviewed path; preserve writer/webhook/public registrations. Recovery helper + approval is separate from mint/bootstrap approval. + rotate: Attended owner revokes old identified PAT then governed bootstrap remints + and verifies; never overwrite differing/orphan KV through bootstrap. Explicit + scoped CAS update plan required for rotation. + compromised: Revoke all dedicated service-user PATs, remove delivery data item, + roll fixed consumer with owners, retain failed sanitized evidence; investigate + all seven private source exposure. Do not rotate unrelated credentials without + separate scope. +revision: 2 diff --git a/docs/proposals/2026-10-15-reuse-private-reader/README.md b/docs/proposals/2026-10-15-reuse-private-reader/README.md new file mode 100644 index 0000000..9c3615d --- /dev/null +++ b/docs/proposals/2026-10-15-reuse-private-reader/README.md @@ -0,0 +1,83 @@ +# Seven private-source reader preparation + +This bundle is prepared under platform's INTENT custody/Q1 remit. Nothing here +records founder credential approval or another owner's review. The existing +all-repository core-hub provisioner is precedent only and is never executed. + +The exact bounded command, after approved CCR, independent reviews and binding +confirmation, is: + +```sh +warden access openbao-platform-admin-login --exec -- python3 scripts/provision-reuse-surface-forgejo-source.py --approval /absolute/path/approved-artifact-receipt.json --receipt /absolute/path/durable-0600-receipt.jsonl +``` + +An approval receipt must contain `ccr_id: CCR-2026-0046`, `decision: approved`, +nonempty `supervisor` and `decision_ref`, and `approved_artifacts` mapping exactly +these four relative paths to SHA256 digests of their approved bytes: + +- credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml +- scripts/provision-reuse-surface-forgejo-source.py +- docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json +- openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl + +CCR itself must be approved; preparation hashes do not satisfy that gate. This +local check binds revisions and relies on attended platform lane/supervisor +custody for authenticity; it is not a signed approval authority or an autopilot +grant. Metadata apply is separately governed by existing credential-change CLI. +Bootstrap never applies policy or changes cluster delivery. + +`apps-externalsecret-data-item.draft.yaml` is a proposed insertion for apps owner, +not a deployable manifest or an apps review. Existing ESO role reads only one new +exact data path; caller writer policy is unchanged. Claim confirmation remains +false until live owner survey. Consumer delivery must wait for the FULL reviewed +image fixing redirects/HTTPS and runtime-controlled exact source URL allowlist, +with fourteen consented pinned URLs in REUSE_SURFACE_AUTH_SOURCE_ALLOWLIST. + +Failure recovery: inspect sanitized journal and authoritative identity/team/PAT/ +KV state quietly. A missing observation is not proof a request did not apply. +The script refuses orphan PAT/KV state rather than reminting. Revoke only an +identified dedicated PAT through Forgejo owner's attended recovery; if mint +response lost, locate its exact recorded attempt-name prefix before revocation. +Never delete unrelated token or shared runtime Secret. Rotation/recovery of an +existing KV version requires a separately scoped CAS update plan; this create-only +bootstrap does not overwrite or automatically rollback ambiguous writes. + +Known execution gates: provisional IDs must be rechecked against current primary; +Forgejo owner must confirm the API contracts (team organization username, complete +sudo user inventory and token_last_eight metadata) on the deployed server; missing +or differing metadata fails closed. Excluded-private rapp-core-hub must still be +private. Native verification is a separate consumer-owned proof, not this API +bootstrap's index-only custody check. New reader provisioning/production approval, +apps config delta, fixed-image digest, live ESO survey and final70 audit all pending. + +Revision2 refines the same CCR after a confirmed server-capped pagination defect. +R1 patch and failed review evidence remain retained; no new scored trial is claimed. +All list requests now continue until an observed empty page, reject duplicate IDs, +and reconcile supplied total counts. Forgejo v11.0.3 source confirms user/repos +includes effective direct/team grants. Stable ID sort is requested for that list. +Version compatibility and live proof still precede apply. + +Forgejo11.0.3 token DELETE requires Basic authentication, unlike token inventory GET. +Admin PAT/Sudo alone is not a valid recovery implementation. The separate reviewed +recovery must reset only this dedicated user password to fresh process-memory bytes, +Basic-auth DELETE only the reviewed token ID, and verify absence. Recovery +helper approval and manual rotation due-date task remain activation gates; no +deployed quarterly expiry enforcement is claimed. + + +The separately reviewed executable credential rollback is: + +```sh +warden access openbao-platform-admin-login --exec -- python3 scripts/revoke-reuse-surface-forgejo-source.py --approval /absolute/path/approved-exact-token-recovery.json --receipt /absolute/path/durable-0600-recovery.jsonl +``` + +Its independent approval record names `action: revoke_exact_pat`, the CCR, +`decision: approved`, supervisor and decision_ref, `target: {id: , +name: }`, and SHA256s in `approved_artifacts` for +exactly recovery helper, bootstrap helper, CCR and bootstrap contract. This +receipt also relies on attended supervisor custody; it is not signed authority. +Different inventory stops before password mutation. Password stays in memory; +DELETE uses Basic authorization captured only by the quiet client. Uncertain +DELETE is followed only by read-only absence reconciliation, never another +DELETE or mint. KV is deliberately preserved, so rotation needs its separate CAS +plan. Credential rollback and apps delivery cleanup have independent receipts. diff --git a/docs/proposals/2026-10-15-reuse-private-reader/apps-externalsecret-data-item.draft.yaml b/docs/proposals/2026-10-15-reuse-private-reader/apps-externalsecret-data-item.draft.yaml new file mode 100644 index 0000000..12dbafc --- /dev/null +++ b/docs/proposals/2026-10-15-reuse-private-reader/apps-externalsecret-data-item.draft.yaml @@ -0,0 +1,4 @@ +secretKey: REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN +remoteRef: + key: workloads/reuse/reuse-surface/forgejo-source-read + property: FORGEJO_SOURCE_TOKEN diff --git a/docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json b/docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json new file mode 100644 index 0000000..798797b --- /dev/null +++ b/docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json @@ -0,0 +1,211 @@ +{ + "id": "CCR-2026-0046-BOOTSTRAP-R2", + "revision": 2, + "sources": [ + { + "repo": "rapp-openbao", + "capability_ids": [ + "capability.railiance.openbao-package" + ], + "consented_commit": "35672be138036c8433a5ac5de91e068c03c2b124", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-openbao/raw/registry/indexes/capabilities.yaml?ref=35672be138036c8433a5ac5de91e068c03c2b124", + "index_sha256": "044efa3d02b90e1872f2516c581159d388a8080a689f76fcd78bb4d048d6ee64", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + }, + { + "repo": "rapp-policy-nexus", + "capability_ids": [ + "capability.railiance.policy-publication-package" + ], + "consented_commit": "088b78ac03b0cebeec34a69aae3569eb7ec2f9c3", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-policy-nexus/raw/registry/indexes/capabilities.yaml?ref=088b78ac03b0cebeec34a69aae3569eb7ec2f9c3", + "index_sha256": "e68bdedf61eee9ca2026f46a2434ce384c26234616e74f7a5d08a06c75b3bb00", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + }, + { + "repo": "rapp-postgres", + "capability_ids": [ + "capability.railiance.postgres-isolation-recovery" + ], + "consented_commit": "a9f569c96d74aa384ac8d3ac4aa960e4061550b5", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-postgres/raw/registry/indexes/capabilities.yaml?ref=a9f569c96d74aa384ac8d3ac4aa960e4061550b5", + "index_sha256": "d939333eed9d85172e006f526c445fbe2602a721d53f7c0d14bc68286ae08776", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + }, + { + "repo": "rapp-sbom-nexus", + "capability_ids": [ + "capability.railiance.sbom-runtime-package" + ], + "consented_commit": "adbedd349e8df97be43a1f57db7095af45ff29d1", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-sbom-nexus/raw/registry/indexes/capabilities.yaml?ref=adbedd349e8df97be43a1f57db7095af45ff29d1", + "index_sha256": "d2ee0f193d2198bf8f16ce64c90ae7664e59a2bc307ad711426af3fd47deb057", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + }, + { + "repo": "rapp-telemetry", + "capability_ids": [ + "capability.railiance.telemetry-package" + ], + "consented_commit": "f158307e24b86bab88a1af4830ff6b4f85afdeb5", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-telemetry/raw/registry/indexes/capabilities.yaml?ref=f158307e24b86bab88a1af4830ff6b4f85afdeb5", + "index_sha256": "ee8801f143ed5cf064205710a61c2bb24efc6ea8585d5786d6b679426ec4c1d4", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + }, + { + "repo": "rapp-tenant-engine", + "capability_ids": [ + "capability.railiance.tenant-engine-package" + ], + "consented_commit": "9bdc86572728783a5ed94266f04f7b12d414c557", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-tenant-engine/raw/registry/indexes/capabilities.yaml?ref=9bdc86572728783a5ed94266f04f7b12d414c557", + "index_sha256": "d8e9c1282350b5d2fd4f4d69712aa7a5fbcd2367bb1a7fad1b6eb16a97e5d732", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + }, + { + "repo": "rapp-user-engine", + "capability_ids": [ + "capability.railiance.user-engine-package" + ], + "consented_commit": "f35108f9e1b8702fb6c526fe42b2fec5ca032738", + "native_raw_url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/rapp-user-engine/raw/registry/indexes/capabilities.yaml?ref=f35108f9e1b8702fb6c526fe42b2fec5ca032738", + "index_sha256": "95b4ed2d0b189f6847b1c0899f740d1da46933a8f40491e1a02f2d4e57c819fc", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN" + } + ], + "negative_private_probe": "rapp-core-hub", + "identity": { + "forgejo_user": "reuse-surface-source", + "forgejo_org": "coulomb", + "forgejo_team": "reuse-surface-source-readers", + "restricted": true, + "is_admin": false, + "visibility": "private", + "password": "random value retained only in attended process memory; never stored in evidence", + "team_permission": "read", + "includes_all_repositories": false, + "can_create_org_repo": false, + "units_map": { + "repo.code": "read", + "repo.actions": "none", + "repo.packages": "none", + "repo.issues": "none", + "repo.ext_issues": "none", + "repo.wiki": "none", + "repo.pulls": "none", + "repo.releases": "none", + "repo.projects": "none", + "repo.ext_wiki": "none" + }, + "team_repositories": [ + "coulomb/rapp-openbao", + "coulomb/rapp-policy-nexus", + "coulomb/rapp-postgres", + "coulomb/rapp-sbom-nexus", + "coulomb/rapp-telemetry", + "coulomb/rapp-tenant-engine", + "coulomb/rapp-user-engine" + ], + "token_scopes": [ + "read:repository" + ], + "token_name_prefix": "reuse-surface-source-read-", + "reuse_other_workload_token": false, + "fail_on_differing_existing_identity_or_team": true, + "enforcement": "Verify complete team membership and all effective user repository grants; no other team, collaborator or organisation grants may provide extra private read access." + }, + "delivery": { + "kv_mount": "platform", + "kv_path": "platform/workloads/reuse/reuse-surface/forgejo-source-read", + "field": "FORGEJO_SOURCE_TOKEN", + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN", + "eso_role": "reuse-surface-runtime-eso", + "eso_identity": "reuse/reuse-surface-eso", + "audience": "openbao", + "ttl": "15m", + "cluster_secret_store": "openbao-reuse", + "store_namespace_condition": [ + "reuse" + ], + "external_secret": "reuse/reuse-surface-runtime", + "target_secret": "reuse/reuse-surface-env", + "existing_consumption": "Deployment reuse/reuse-surface envFrom secretRef reuse-surface-env; environment variable available only after controlled rollout. Existing image and resource requests retained.", + "policy_delta": { + "file": "railiance-platform/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl", + "add_exact_stanza": "path \"platform/data/workloads/reuse/reuse-surface/forgejo-source-read\" { capabilities = [\"read\"] }", + "retain_existing_paths": true, + "do_not_change": "Caller-facing workload-kv-read-reuse-surface-runtime policy or writer OIDC role; those remain unable to read this new path." + }, + "external_secret_data_delta": { + "secretKey": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN", + "remoteRef": { + "key": "workloads/reuse/reuse-surface/forgejo-source-read", + "property": "FORGEJO_SOURCE_TOKEN" + } + }, + "delivery_design_reason": "Uses the existing namespace-limited ESO store and identity plus the existing envFrom interface. Separate new KV path keeps scoped writer callers from also acquiring private-source access. No new static ESO token or new store is needed.", + "native_source_auth": { + "auth_env": "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN", + "auth_header": "Authorization", + "header_behavior": "Native federation _auth_headers prefixes a bare token with Bearer; owner must verify accepted Forgejo raw API route with this actual native implementation." + }, + "rollout_image": "Pending full consumer fix reviewed image digest; same-image rollout of current consumer is prohibited." + }, + "consumer_gate": "Reader must not be delivered until reuse-surface owner supplies verified image digest containing BOTH authenticated redirect rejection/HTTPS enforcement AND runtime-controlled exact URL allowlist checked before credential lookup. Apps owner must deploy REUSE_SURFACE_AUTH_SOURCE_ALLOWLIST mapping the reader environment to exactly fourteen consented pinned index/entry URLs and inventory existing authenticated sources for migration. Current image and redirect-only fix are not approved. Native owner helper must verify all seven pinned indexes and entry hashes after rollout.", + "approval": "No approval recorded. External supervisor approval receipt must bind exactly CCR, script, contract and ESO policy bytes; CCR must be approved. Additional forge/apps/reuse owner reviews and rollout approval remain required.", + "metadata_consent_sha256": "4a4602ad926e3af855d6ddba4731430d33e4a7432c507ba0a5193b9f43056b52", + "preparation_sha256": "7819eb51d249f5c7aff4df9fb520afcfb76603b05a4104439499102b6c20af99", + "recovery": "On ambiguous mutation stop. Reconcile complete Forgejo identity/team/repository/PAT metadata and exact KV field through suppressed helper before retry. Token without KV or reverse refuses automatically; revoke identified dedicated PAT through attended Forgejo owner recovery before retry. Never remint on observation timeout. KV CAS=0 prevents overwrites. Do not delete shared runtime Secret.", + "consumer_preparation": { + "owner": "reuse-surface", + "allowlist_artifact": "/tmp/cust81-reuse-reader-owner/docs/assessments/cust81-private-reader/runtime-auth-source-allowlist-r1.json", + "allowlist_sha256": "e1948c537e206b30fc72b9e4732a3f600d92e131813e6f7ecfd4bfbb18c27286", + "federation_py_sha256": "dbaa40f25cc0f090143578cd1c7068cb2cb00960fafd17d5239bf8a5bac514dd", + "integration_patch_sha256": "da9634481ce80658dfce815d3539ba023234938dc76163dfd93d7677be95ed51", + "review_state": "changes_requested for current consumer; preparation patch alone is not deployed-image evidence" + }, + "pagination_contract": "All list endpoints page until observed empty, max20pages; reject duplicate/missing IDs, reconcile X-Total-Count when supplied, reject changed/mismatched counts and bound exhaustion. Never infer completion from requestedlimit100 versus server-capped50.", + "effective_grants_contract": "Forgejo11.0.3 /user/repos?order_by=id with Sudo reader uses OwnerID=reader and Collaborate=None; SearchRepositoryCondition defaults collaborations true and includes direct UserAccessRepoCond plus UserOrgTeamRepoCond. It is not owned-only despite Swagger summary. Independent deployed-version compatibility and live inventory proof pending.", + "r1_review_failure": { + "patch_sha256": "a94547b419b87e38dc178b02d1d31af61cc11a71e3734052a190167668db8e30", + "confirmed_defect": "R1 len(batch)<100 stopped after server-capped50, omitting later grants", + "corrected_preliminary_claim": "Owned-only endpoint claim disproved by Forgejo11.0.3 model defaults; no endpoint replacement based on false premise", + "same_proposal": "CCR-2026-0046; revision2 is refinement, not a new successful trial" + }, + "api_source_evidence": [ + { + "upstream_path": "models/repo/repo/list.go", + "url": "https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/models/repo/repo/list.go", + "sha256": "bb7833badf0fe7f6a2c800c2f301cb846b5968ce3f9e9bd29c3f1677db42acd9", + "local_cache": "/tmp/cust81-forge-contract-source/source-models_repo_repo_list.go" + }, + { + "upstream_path": "routers/api/v1/utils/page.go", + "url": "https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/routers/api/v1/utils/page.go", + "sha256": "1d286f60b5571513fb5a26a9243c22eae65f05d7be32dfe5d0f0b15d20550954", + "local_cache": "/tmp/cust81-forge-contract-source/source-routers_api_v1_utils_page.go" + }, + { + "upstream_path": "services/convert/utils.go", + "url": "https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/services/convert/utils.go", + "sha256": "2689ffc579ea1df69457fbe84410fef101ae4a9580f0e56dcc3113a2988626b3", + "local_cache": "/tmp/cust81-forge-contract-source/source-services_convert_utils.go" + }, + { + "upstream_path": "modules/setting/api.go", + "url": "https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/modules/setting/api.go", + "sha256": "1f640ed884afaa6454c4bf427eccd2d6bb9a092fe37ac1aedfdc5eb85de4f7ba", + "local_cache": "/tmp/cust81-forge-contract-source/source-modules_setting_api.go" + }, + { + "upstream_path": "routers/api/v1/user/repo.go", + "url": "https://codeberg.org/forgejo/forgejo/src/tag/v11.0.3/routers/api/v1/user/repo.go", + "sha256": "adee62e6671aadd95b5013a64dbb690f2d81a0b9b4de0350c70b4153074ca6ec", + "local_cache": "/tmp/cust81-forge-contract-source/source-routers_api_v1_user_repo.go" + } + ], + "rollback_helper": { + "path": "scripts/revoke-reuse-surface-forgejo-source.py", + "scope": "Separate approval binds exact PAT id/name and artifact bytes; Basic-auth revocation after dedicated-user memory-only password reset. Preserve KV/sharedruntime; no remint. Reconcile uncertain DELETE by inventory absence." + } +} diff --git a/docs/proposals/2026-10-15-reuse-private-reader/platform-owner-review-r1.json b/docs/proposals/2026-10-15-reuse-private-reader/platform-owner-review-r1.json new file mode 100644 index 0000000..484be01 --- /dev/null +++ b/docs/proposals/2026-10-15-reuse-private-reader/platform-owner-review-r1.json @@ -0,0 +1,35 @@ +{ + "id": "RPF-WP-0058-PLATFORM-OWNER-REVIEW-R1", + "owner_repo": "railiance-platform", + "reviewer": "platform-reader-owner delegated worker", + "basis": [ + "INTENT.md shared stateful service custody, Q1 approval/delegated apply/revocation, stable delivery interface", + "AGENTS.md credential-change owner review and founder-gated attended provisioning", + "No own supervision record exists; supervised preparation only" + ], + "disposition": "prepared_with_conditions_not_execution_approval", + "scope": "Only platform custody/CCR/policy/bootstrap; other repo reviews not authored", + "findings": [ + "Separate exact KV path and exact data-read ESO delta preserve writer-only caller boundary.", + "No all-repo Forgejo team; preflight rejects different existing user/team, unexpected team repos/members, other teams/orgs/private grants, orphan PAT/KV, and unverified PAT/KV binding.", + "Quiet create-only bootstrap with per-user lane lock, durable fsynced intents/outcomes, CAS=0 KV; uncertainty is reconciled before retry, never blindly reminted.", + "Full consumer redirect/HTTPS plus authenticated URL allowlist image and exact config mandatory before credential delivery.", + "Approval receipt is locally bound revision evidence, not authenticated authority; attended founder/platform lane remains authorization source." + ], + "validation": { + "targeted_tests": 17, + "result": "passed", + "ccr_validator": "passed with expected unconfirmed ESO binding/apply blocked warning", + "diff_check": "passed" + }, + "pending": [ + "Allocation recheck against owner primary before integration", + "Independent Forgejo/apps/reuse owner reviews and actual founder credential provisioning decision", + "Live ESO claims and namespace condition confirmation; no token read or live mutations performed", + "Forgejo deployed API metadata contract support proof; sudo inventory and token_last_eight missing metadata fail closed", + "Apps fixedimage digest/config/migration/dryrun/headroom rollout approval and receipt", + "Native seven index+entry verification and final hosted70/70 audit" + ], + "execution_approved": false, + "live_changes_performed": false +} diff --git a/docs/proposals/2026-10-15-reuse-private-reader/platform-owner-review-r2.json b/docs/proposals/2026-10-15-reuse-private-reader/platform-owner-review-r2.json new file mode 100644 index 0000000..7c41582 --- /dev/null +++ b/docs/proposals/2026-10-15-reuse-private-reader/platform-owner-review-r2.json @@ -0,0 +1,54 @@ +{ + "id": "RPF-WP-0058-PLATFORM-OWNER-REVIEW-R2", + "owner_repo": "railiance-platform", + "reviewer": "platform-reader-owner delegated worker", + "basis": [ + "INTENT.md shared stateful service custody, Q1 approval/delegated apply/revocation, stable delivery interface", + "AGENTS.md credential-change owner review and founder-gated attended provisioning", + "No own supervision record exists; supervised preparation only" + ], + "disposition": "approved", + "scope": "Platform custody PREPARATION artifact revision2 only; not founder credential/production authority or another repo review", + "findings": [ + "Separate exact KV path and exact data-read ESO delta preserve writer-only caller boundary.", + "No all-repo Forgejo team; preflight rejects different existing user/team, unexpected team repos/members, other teams/orgs/private grants, orphan PAT/KV, and unverified PAT/KV binding.", + "Quiet create-only bootstrap with per-user lane lock, durable fsynced intents/outcomes, CAS=0 KV; uncertainty is reconciled before retry, never blindly reminted.", + "Full consumer redirect/HTTPS plus authenticated URL allowlist image and exact config mandatory before credential delivery.", + "Approval receipt is locally bound revision evidence, not authenticated authority; attended founder/platform lane remains authorization source.", + "Forgejo version source disproves preliminary owned-only /user/repos claim; unchanged endpoint with id sort includes direct and team grants. R2 pages until observed empty, refuses missing/duplicate IDs and inconsistent total counts." + ], + "validation": { + "targeted_tests": 28, + "result": "passed", + "ccr_validator": "passed with expected unconfirmed ESO binding/apply blocked warning", + "diff_check": "passed" + }, + "pending": [ + "Allocation recheck against owner primary before integration", + "Independent Forgejo/apps/reuse owner reviews and actual founder credential provisioning decision", + "Live ESO claims and namespace condition confirmation; no token read or live mutations performed", + "Forgejo deployed API metadata contract support proof; sudo inventory and token_last_eight missing metadata fail closed", + "Apps fixedimage digest/config/migration/dryrun/headroom rollout approval and receipt", + "Native seven index+entry verification and final hosted70/70 audit", + "Separate explicit exact-token recovery approval and manual rotation due-date task/receipt before activation." + ], + "execution_approved": false, + "live_changes_performed": false, + "revision": 2, + "decision": "approved", + "work_item": "RPF-WP-0058-T01", + "reviewed_artifacts": { + "credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml": "90522912fb78fed72e625382fb7114a0e9aafe76d59e36bffecab83c5bb7d643", + "scripts/provision-reuse-surface-forgejo-source.py": "abf60a754596baa1610d23b1fd367385b0222ac8aeaebba0f2cfb7ec4c62055b", + "scripts/revoke-reuse-surface-forgejo-source.py": "f78d1b26fbff93c6cc481a896d9fb9b9ce93049b8b3267fa41a64bc57dadb162", + "tests/test_reuse_surface_forgejo_source_provision.py": "0d06fd22596a0f17ecd8dd5ff6a12bb1b1782258d00126ad468661099d7d298b", + "tests/test_reuse_surface_forgejo_source_recovery.py": "08247129b220c198d8f7d1322ce63956203b9ffcaa0342e52990bb6520fd138d", + "docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json": "2bb72671e0a5f230497ab8ef2967a20ee68d1f2cf081f07c84f6db3442e18d8b", + "openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl": "6320f6c2d262d924ea16291d301d5f9eeb7efdb6ce7ea141c1a41d277d86b01b", + "docs/proposals/2026-10-15-reuse-private-reader/README.md": "dcc8879d96b85b21b52c6cac9d7a58db78169ed903490ef11561bf8d6aa507e5", + "workplans/RPF-WP-0058-reuse-private-source-reader.md": "9aeb0fbbb5359b7ba9afb4f5f7f2c55f84dade0d6d95fe98b15901e656aed5b5", + "docs/proposals/2026-10-15-reuse-private-reader/apps-externalsecret-data-item.draft.yaml": "e582a7d2eeb68668858ec19a6a32b8b816a4850b5ad1757f7686330b141578b2" + }, + "supersedes_review_artifact_sha256": "0a9746f65bb79982632accd9e20a2a632eafc5ee5410f4eddeadd625e73ca0bd", + "refinement_of_same_proposal": "CCR-2026-0046 R1 confirmed capped-page bug retained; R2 fixes pagination, not a new trial" +} diff --git a/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl b/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl index 93739c6..ebbb1fc 100644 --- a/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl +++ b/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl @@ -9,3 +9,8 @@ path "auth/token/lookup-self" { path "auth/token/revoke-self" { capabilities = ["update"] } + +# CCR-2026-0046 proposed; exact extra data path, no metadata/list grants. +path "platform/data/workloads/reuse/reuse-surface/forgejo-source-read" { + capabilities = ["read"] +} diff --git a/scripts/provision-reuse-surface-forgejo-source.py b/scripts/provision-reuse-surface-forgejo-source.py new file mode 100755 index 0000000..24a83b6 --- /dev/null +++ b/scripts/provision-reuse-surface-forgejo-source.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +"""Silent attended CCR-2026-0046 bootstrap; never runs without exact approval. + +Only this seven-repository identity/PAT/KV path is mutable. No policy, ESO, +consumer rollout, or registration mutations occur here. A receipt is evidence, +not authorization. Uncertain requests are recorded and reconciled, never retried +in the same execution; orphan PAT drift requires attended recovery. +""" +from __future__ import annotations +import argparse +import base64 +import datetime as dt +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import secrets +import subprocess +import sys +import urllib.error +import urllib.request +import yaml + +ROOT = Path(__file__).resolve().parent.parent +CCR = 'CCR-2026-0046' +CCR_FILE = 'credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml' +PLAN_FILE = 'docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json' +SCRIPT_FILE = 'scripts/provision-reuse-surface-forgejo-source.py' +API = 'https://forgejo.coulomb.social/api/v1' +USER = 'reuse-surface-source' +TEAM = 'reuse-surface-source-readers' +ORG = 'coulomb' +KV = 'platform/workloads/reuse/reuse-surface/forgejo-source-read' +FIELD = 'FORGEJO_SOURCE_TOKEN' +PREFIX = 'reuse-surface-source-read-' +REPOS = frozenset('rapp-openbao rapp-policy-nexus rapp-postgres rapp-sbom-nexus rapp-telemetry rapp-tenant-engine rapp-user-engine'.split()) +SCOPES = ['read:repository'] +UNITS = {'repo.code': 'read', **{x: 'none' for x in ('repo.actions', 'repo.packages', 'repo.issues', 'repo.ext_issues', 'repo.wiki', 'repo.pulls', 'repo.releases', 'repo.projects', 'repo.ext_wiki')}} + +class Refused(Exception): + """Messages must be fixed non-secret codes, never provider text.""" + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, *args, **kwargs): + raise Refused('redirect_refused') + +class Journal: + def __init__(self, path): + flags = os.O_RDWR | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW + self.fd = os.open(path, flags, 0o600) + if os.fstat(self.fd).st_mode & 0o077: + os.close(self.fd) + raise Refused('receipt_permissions') + fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + self.attempt = secrets.token_hex(12) + def put(self, event, **fields): + # Only locally authored operation tags, scalar status/IDs are accepted. + allowed = {'operation', 'status', 'http_status', 'token_id', 'team_id', 'outcome', 'error_code'} + if set(fields) - allowed: + raise Refused('receipt_fields') + record = {'at': dt.datetime.now(dt.timezone.utc).isoformat(), 'ccr': CCR, + 'attempt': self.attempt, 'event': event, **fields} + data = (json.dumps(record, sort_keys=True) + '\n').encode() + while data: + written = os.write(self.fd, data) + if written <= 0: + raise Refused('receipt_write_failed') + data = data[written:] + os.fsync(self.fd) + def close(self): + os.close(self.fd) + +class Client: + def __init__(self, admin, journal): + self.admin, self.journal = admin, journal + self.opener = urllib.request.build_opener(NoRedirect()) + def request(self, method, path, payload=None, expected=(200,), *, token=None, sudo=False, basic=None, operation=None): + if not path.startswith('/') or '://' in path: + raise Refused('invalid_api_path') + headers = {'Authorization': basic or 'token ' + (self.admin if token is None else token), 'Accept': 'application/json'} + if sudo: + headers['Sudo'] = USER + if payload is not None: + headers['Content-Type'] = 'application/json' + if method != 'GET': + if not operation: + raise Refused('mutation_without_intent') + self.journal.put('intent', operation=operation) + req = urllib.request.Request(API + path, data=None if payload is None else json.dumps(payload).encode(), headers=headers, method=method) + self.last_total_count = None + try: + with self.opener.open(req, timeout=30) as response: + status, body = response.status, response.read(2_000_001) + raw_total = response.headers.get('X-Total-Count') + if raw_total is not None: + if not re.fullmatch(r'[0-9]+', raw_total): + raise Refused('invalid_total_count') + self.last_total_count = int(raw_total) + except urllib.error.HTTPError as error: + status, body = error.code, b'' + except Exception: + if operation: + self.journal.put('outcome', operation=operation, status='ambiguous') + raise Refused('request_observation_failed') from None + if operation: + self.journal.put('outcome', operation=operation, status='observed', http_status=status) + if status not in expected: + raise Refused('unexpected_http_status') + if len(body) > 2_000_000: + raise Refused('response_bound_exceeded') + if not body: + return status, None + try: + return status, json.loads(body) + except Exception: + raise Refused('invalid_json') from None + def pages(self, path, *, sudo=False): + """Page until observed empty; requested size may be server-clamped. + + Forgejo11.0.3 default MaxResponseItems=50, so a response shorter than + our requested100 proves nothing. Detect unstable/duplicate IDs and + reconcile X-Total-Count whenever available; exceeding bound refuses. + """ + items, seen, expected_total = [], set(), None + for page in range(1, 21): + separator = '&' if '?' in path else '?' + _, batch = self.request('GET', path + separator + f'limit=100&page={page}', sudo=sudo) + if not isinstance(batch, list) or any(not isinstance(x, dict) or type(x.get('id')) is not int or x['id'] <= 0 for x in batch): + raise Refused('invalid_list') + observed_total = getattr(self, 'last_total_count', None) + if observed_total is not None: + if expected_total is not None and observed_total != expected_total: + raise Refused('unstable_total_count') + expected_total = observed_total + if not batch: + if expected_total is not None and len(items) != expected_total: + raise Refused('total_count_mismatch') + return items + for item in batch: + if item['id'] in seen: + raise Refused('duplicate_page_id') + seen.add(item['id']) + items.extend(batch) + if expected_total is not None and len(items) > expected_total: + raise Refused('total_count_mismatch') + raise Refused('pagination_bound_exceeded') + +class Bao: + @staticmethod + def run(args, stdin=None): + try: + return subprocess.run(['bao', *args], input=stdin, capture_output=True, timeout=60, check=False, cwd=ROOT) + except Exception: + raise Refused('bao_observation_failed') from None + def get(self): + result = self.run(['kv', 'get', '-format=json', KV]) + if result.returncode != 0: + # Distinguish an actual not-found response from denied/unavailable. + # Never include captured raw errors in any evidence. + if re.search(rb'Code:\s*404\b', result.stderr or b'') or b'No value found at ' in (result.stderr or b''): + return None + raise Refused('bao_preflight_failed') + try: + data = json.loads(result.stdout)['data']['data'] + except Exception: + raise Refused('bao_invalid_record') from None + if set(data) != {FIELD, 'API_USER', 'API_BASE_URL', 'TOKEN_SCOPES', 'GENERATED_AT', 'CCR_ID'} or data['API_USER'] != USER or data['API_BASE_URL'] != 'https://forgejo.coulomb.social' or data['TOKEN_SCOPES'] != 'read:repository' or data['CCR_ID'] != CCR or not isinstance(data[FIELD], str) or not data[FIELD]: + raise Refused('bao_record_conflict') + return data + def put(self, token, journal): + journal.put('intent', operation='kv_create') + timestamp = dt.datetime.now(dt.timezone.utc).isoformat() + try: + result = self.run(['kv', 'put', '-cas=0', KV, FIELD + '=-', 'API_USER=' + USER, + 'API_BASE_URL=https://forgejo.coulomb.social', 'TOKEN_SCOPES=read:repository', + 'GENERATED_AT=' + timestamp, 'CCR_ID=' + CCR], token.encode()) + except Refused: + journal.put('outcome', operation='kv_create', status='ambiguous') + raise + journal.put('outcome', operation='kv_create', status='observed' if result.returncode == 0 else 'ambiguous') + # Readback reconciles even a response failure after successful write. + stored = self.get() + if stored is None or not secrets.compare_digest(stored[FIELD], token): + raise Refused('bao_write_requires_reconciliation') + journal.put('verified', operation='kv_create', status='matched') + + +def valid_user(user): + return isinstance(user, dict) and user.get('login') == USER and user.get('restricted') is True and user.get('is_admin') is False and user.get('visibility') == 'private' + +def valid_team(team): + return isinstance(team, dict) and isinstance(team.get('id'), int) and team.get('name') == TEAM and team.get('permission') == 'read' and team.get('includes_all_repositories') is False and team.get('can_create_org_repo') is False and team.get('units_map') == UNITS + +def repo_names(items): + names = set() + for repo in items: + if repo.get('owner', {}).get('login') != ORG or repo.get('name') not in REPOS: + raise Refused('unexpected_team_repository') + names.add(repo['name']) + if len(names) != len(items): + raise Refused('duplicate_repository') + return names + +def inspect(client, bao): + """Read-only preflight before mutations. + + Forgejo11.0.3 ListMyRepos uses OwnerID plus Collaborate=None; model + ValueOrDefault(true) includes direct access and team grants (not only owned). + Deployed-server API compatibility and live scope proof remain separate gates. + """ + for repo in sorted(REPOS): + _, meta = client.request('GET', f'/repos/{ORG}/{repo}') + if not isinstance(meta, dict) or meta.get('private') is not True or meta.get('full_name') != ORG + '/' + repo: + raise Refused('source_repository_conflict') + _, probe = client.request('GET', '/repos/coulomb/rapp-core-hub') + if not isinstance(probe, dict) or probe.get('private') is not True: + raise Refused('negative_probe_not_private') + _, user = client.request('GET', '/users/' + USER, expected=(200, 404)) + if user is not None and not valid_user(user): + raise Refused('user_conflict') + teams = [x for x in client.pages(f'/orgs/{ORG}/teams') if x.get('name') == TEAM] + if len(teams) > 1: + raise Refused('duplicate_team') + team = teams[0] if teams else None + members, repos = [], set() + if team is not None: + if not valid_team(team): + raise Refused('team_conflict') + members = client.pages('/teams/' + str(team['id']) + '/members') + if len(members) > 1 or any(x.get('login') != USER for x in members): + raise Refused('team_members_conflict') + repos = repo_names(client.pages('/teams/' + str(team['id']) + '/repos')) + tokens = [] + if user is not None: + orgs = client.pages('/user/orgs', sudo=True) + if any(x.get('username') != ORG for x in orgs): + raise Refused('extra_organization_affiliation') + grants = client.pages('/user/teams', sudo=True) + if any(x.get('name') != TEAM or x.get('organization', {}).get('username') != ORG for x in grants): + raise Refused('extra_team_grant') + for meta in client.pages('/user/repos?order_by=id', sudo=True): + if meta.get('private') is True and meta.get('full_name') not in {ORG + '/' + x for x in REPOS}: + raise Refused('extra_private_repository_grant') + if meta.get('owner', {}).get('login') == USER: + raise Refused('service_user_owned_repository') + tokens = client.pages('/users/' + USER + '/tokens') + if len(tokens) > 1 or any(not str(x.get('name', '')).startswith(PREFIX) or x.get('scopes') != SCOPES or not isinstance(x.get('id'), int) for x in tokens): + raise Refused('token_conflict') + stored = bao.get() + if bool(tokens) != bool(stored): + raise Refused('orphan_token_or_kv_requires_recovery') + if tokens and (not isinstance(tokens[0].get('token_last_eight'), str) or len(tokens[0]['token_last_eight']) != 8 or not secrets.compare_digest(tokens[0]['token_last_eight'], stored[FIELD][-8:])): + raise Refused('pat_kv_binding_unverifiable') + if tokens and (not team or repos != REPOS or len(members) != 1): + raise Refused('active_lane_grant_drift') + return user, team, repos, members, tokens, stored + +def verify(client, source_token, plan): + for source in plan['sources']: + _, repo = client.request('GET', '/repos/' + ORG + '/' + source['repo'], token=source_token) + if repo.get('permissions') != {'admin': False, 'push': False, 'pull': True}: + raise Refused('effective_permission_not_read') + # Exact consented bytes fetched quietly; raw output is never retained. + path = f"/repos/{ORG}/{source['repo']}/contents/registry/indexes/capabilities.yaml?ref={source['consented_commit']}" + _, content = client.request('GET', path, token=source_token) + try: + raw = base64.b64decode(content['content'], validate=False) + except Exception: + raise Refused('invalid_index_encoding') from None + if hashlib.sha256(raw).hexdigest() != source['index_sha256']: + raise Refused('index_digest_mismatch') + client.request('GET', f"/repos/{ORG}/{source['repo']}/actions/secrets?limit=1", token=source_token, expected=(403, 404)) + client.request('GET', '/admin/users', token=source_token, expected=(403, 404)) + # A known private excluded source is selected by reviewed contract. + probe = plan['negative_private_probe'] + _, meta = client.request('GET', '/repos/' + ORG + '/' + probe) + if not isinstance(meta, dict) or meta.get('private') is not True: + raise Refused('negative_probe_not_private') + client.request('GET', '/repos/' + ORG + '/' + probe, token=source_token, expected=(403, 404)) + client.journal.put('verified', operation='reader_scope', status='seven_exact_no_extra_private_grants') + +def provision(client, bao, plan): + user, team, repos, members, tokens, stored = inspect(client, bao) + if tokens: + verify(client, stored[FIELD], plan) + client.journal.put('terminal', outcome='verified_noop') + return 'noop' + if user is None: + _, user = client.request('POST', '/admin/users', {'username': USER, 'email': USER + '@coulomb.social', 'full_name': 'Reuse Surface seven-source reader', 'password': secrets.token_urlsafe(48), 'restricted': True, 'must_change_password': False, 'send_notify': False, 'visibility': 'private'}, expected=(201,), operation='user_create') + if not valid_user(user): + raise Refused('created_user_contract_failed') + if team is None: + _, team = client.request('POST', f'/orgs/{ORG}/teams', {'name': TEAM, 'description': CCR + ' exact seven consented source repos', 'permission': 'read', 'includes_all_repositories': False, 'can_create_org_repo': False, 'units': ['repo.code'], 'units_map': UNITS}, expected=(201,), operation='team_create') + if not valid_team(team): + raise Refused('created_team_contract_failed') + team_id = team['id'] + for repo in sorted(REPOS - repos): + client.request('PUT', f'/teams/{team_id}/repos/{ORG}/{repo}', expected=(204,), operation='team_repo_' + repo) + if not members: + client.request('PUT', f'/teams/{team_id}/members/{USER}', expected=(204,), operation='team_member_add') + # Recheck complete grants before creating any PAT; refuse implicit grants. + _, _, actual_repos, actual_members, _, _ = inspect(client, bao) + if actual_repos != REPOS or len(actual_members) != 1: + raise Refused('incomplete_team_scope') + password = secrets.token_urlsafe(48) + client.request('PATCH', '/admin/users/' + USER, {'password': password, 'restricted': True, 'admin': False, 'allow_create_organization': False, 'allow_git_hook': False, 'allow_import_local': False, 'max_repo_creation': 0, 'must_change_password': False, 'active': True, 'visibility': 'private'}, operation='service_password_randomize') + basic = 'Basic ' + base64.b64encode((USER + ':' + password).encode()).decode() + _, record = client.request('POST', '/users/' + USER + '/tokens', {'name': PREFIX + client.journal.attempt, 'scopes': SCOPES}, expected=(201,), basic=basic, operation='pat_create') + if not isinstance(record, dict) or not isinstance(record.get('id'), int) or record.get('scopes') != SCOPES or not isinstance(record.get('sha1'), str) or not record['sha1']: + raise Refused('created_pat_requires_recovery') + client.journal.put('identified', operation='pat_create', token_id=record['id'], team_id=team_id) + token = record['sha1'] + verify(client, token, plan) + bao.put(token, client.journal) + # Repeat complete grant inventory after custody; verify exactly one PAT. + inspect(client, bao) + client.journal.put('terminal', outcome='provisioned_verified_delivery_pending') + return 'minted' + +def approval(path): + try: + receipt = json.loads(Path(path).read_text()) + ccr = yaml.safe_load((ROOT / CCR_FILE).read_text()) + if receipt['ccr_id'] != CCR or receipt['decision'] != 'approved' or not receipt['supervisor'] or not receipt['decision_ref'] or ccr['status'] != 'approved': + raise Refused('approval_missing') + files = {CCR_FILE, PLAN_FILE, SCRIPT_FILE, 'openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl'} + if set(receipt['approved_artifacts']) != files: + raise Refused('approval_artifact_set') + for rel in files: + if hashlib.sha256((ROOT / rel).read_bytes()).hexdigest() != receipt['approved_artifacts'][rel]: + raise Refused('approval_revision_mismatch') + plan = json.loads((ROOT / PLAN_FILE).read_text()) + if {x['repo'] for x in plan['sources']} != REPOS or len(plan['sources']) != 7 or plan['negative_private_probe'] != 'rapp-core-hub' or any(not re.fullmatch(r'[a-f0-9]{40}', x['consented_commit']) or not re.fullmatch(r'[a-f0-9]{64}', x['index_sha256']) for x in plan['sources']): + raise Refused('plan_scope_conflict') + return plan + except Refused: + raise + except Exception: + raise Refused('approval_invalid') from None + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('--approval', required=True) + parser.add_argument('--receipt', required=True) + parser.add_argument('--inner', action='store_true', help=argparse.SUPPRESS) + args = parser.parse_args(argv) + journal = None + lane_lock = None + try: + plan = approval(args.approval) + if not args.inner: + # Governed OpenBao attended lane surrounds this command. Warden + # supplies only Forgejo API_TOKEN to inner child, not via argv/files. + result = subprocess.run(['warden', 'access', 'forgejo-admin-api-token', '--field', 'API_TOKEN', '--exec', '--', sys.executable, str(ROOT / SCRIPT_FILE), '--inner', '--approval', str(Path(args.approval).resolve()), '--receipt', str(Path(args.receipt).resolve())], capture_output=True, timeout=900, check=False, cwd=ROOT) + return 0 if result.returncode == 0 else 1 + lane_lock = os.open('/tmp/' + CCR + '-reuse-reader-' + str(os.getuid()) + '.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600) + if os.fstat(lane_lock).st_uid != os.getuid() or os.fstat(lane_lock).st_mode & 0o077: + raise Refused('lane_lock_owner_or_permissions') + fcntl.flock(lane_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + journal = Journal(args.receipt) + admin = os.environ.get('API_TOKEN') + if not admin: + raise Refused('admin_route_missing') + provision(Client(admin, journal), Bao(), plan) + return 0 + except Exception as error: + if journal: + journal.put('terminal', outcome='stopped_reconcile_before_retry', error_code=str(error) if isinstance(error, Refused) else 'internal_failure') + return 1 + finally: + if journal: + journal.close() + if lane_lock is not None: + os.close(lane_lock) + +if __name__ == '__main__': + try: + result = main() + except Exception: + result = 1 + raise SystemExit(result) diff --git a/scripts/revoke-reuse-surface-forgejo-source.py b/scripts/revoke-reuse-surface-forgejo-source.py new file mode 100755 index 0000000..9487967 --- /dev/null +++ b/scripts/revoke-reuse-surface-forgejo-source.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Silent separately approved exact-PAT revocation; preserve KV and delivery. + +Forgejo11.0.3 DELETE token requires Basic auth. Reset only the dedicated user +password to process-memory random bytes, revoke the reviewed token and reconcile +inventory absence. Never remint, rotate/write KV, or touch another identity. +""" +import argparse +import base64 +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import secrets +import subprocess +import sys + +ROOT = Path(__file__).resolve().parent.parent +SCRIPT = 'scripts/revoke-reuse-surface-forgejo-source.py' +spec = importlib.util.spec_from_file_location('reuse_reader_bootstrap', ROOT / 'scripts/provision-reuse-surface-forgejo-source.py') +p = importlib.util.module_from_spec(spec) +spec.loader.exec_module(p) + + +def approved_target(path): + try: + record = json.loads(Path(path).read_text()) + expected_files = {SCRIPT, p.SCRIPT_FILE, p.CCR_FILE, p.PLAN_FILE} + if record['ccr_id'] != p.CCR or record['decision'] != 'approved' or not record['supervisor'] or not record['decision_ref'] or record['action'] != 'revoke_exact_pat' or set(record['approved_artifacts']) != expected_files: + raise p.Refused('recovery_approval_invalid') + for rel in expected_files: + if hashlib.sha256((ROOT / rel).read_bytes()).hexdigest() != record['approved_artifacts'][rel]: + raise p.Refused('recovery_approved_revision_mismatch') + target = record['target'] + if set(target) != {'id', 'name'} or type(target['id']) is not int or target['id'] <= 0 or not isinstance(target['name'], str) or not target['name'].startswith(p.PREFIX) or len(target['name']) > 100: + raise p.Refused('recovery_target_invalid') + return target + except p.Refused: + raise + except Exception: + raise p.Refused('recovery_approval_invalid') from None + + +def recover(client, target): + _, user = client.request('GET', '/users/' + p.USER) + if not p.valid_user(user): + raise p.Refused('recovery_user_conflict') + tokens = client.pages('/users/' + p.USER + '/tokens') + if not tokens: + # Absence reconciles a prior ambiguous revocation without new writes. + client.journal.put('terminal', outcome='revoke_verified_absent_noop') + return + if len(tokens) != 1 or tokens[0].get('id') != target['id'] or tokens[0].get('name') != target['name'] or tokens[0].get('scopes') != p.SCOPES: + raise p.Refused('recovery_token_inventory_conflict') + password = secrets.token_urlsafe(48) + client.request('PATCH', '/admin/users/' + p.USER, {'password': password, 'must_change_password': False}, operation='recovery_password_randomize') + basic = 'Basic ' + base64.b64encode((p.USER + ':' + password).encode()).decode() + try: + client.request('DELETE', '/users/' + p.USER + '/tokens/' + str(target['id']), expected=(204, 404), basic=basic, operation='exact_pat_revoke') + except p.Refused: + # Observation failure does not mean DELETE did not apply. Read-only + # reconciliation follows; never issue another DELETE in this execution. + pass + remaining = client.pages('/users/' + p.USER + '/tokens') + if remaining: + raise p.Refused('revoke_unverified_stop') + client.journal.put('verified', operation='exact_pat_revoke', token_id=target['id'], status='inventory_absent') + client.journal.put('terminal', outcome='revoked_verified_kv_preserved') + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('--approval', required=True) + parser.add_argument('--receipt', required=True) + parser.add_argument('--inner', action='store_true', help=argparse.SUPPRESS) + args = parser.parse_args(argv) + journal, lock = None, None + try: + target = approved_target(args.approval) + if not args.inner: + result = subprocess.run(['warden', 'access', 'forgejo-admin-api-token', '--field', 'API_TOKEN', '--exec', '--', sys.executable, str(ROOT / SCRIPT), '--inner', '--approval', str(Path(args.approval).resolve()), '--receipt', str(Path(args.receipt).resolve())], capture_output=True, check=False, timeout=900, cwd=ROOT) + return 0 if result.returncode == 0 else 1 + lock = os.open('/tmp/' + p.CCR + '-reuse-reader-' + str(os.getuid()) + '.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600) + if os.fstat(lock).st_uid != os.getuid() or os.fstat(lock).st_mode & 0o077: + raise p.Refused('lane_lock_owner_or_permissions') + p.fcntl.flock(lock, p.fcntl.LOCK_EX | p.fcntl.LOCK_NB) + journal = p.Journal(args.receipt) + admin = os.environ.get('API_TOKEN') + if not admin: + raise p.Refused('admin_route_missing') + recover(p.Client(admin, journal), target) + return 0 + except Exception as error: + if journal: + journal.put('terminal', outcome='recovery_stopped_reconcile_before_retry', error_code=str(error) if isinstance(error, p.Refused) else 'internal_failure') + return 1 + finally: + if journal: + journal.close() + if lock is not None: + os.close(lock) + +if __name__ == '__main__': + try: + result = main() + except Exception: + result = 1 + raise SystemExit(result) diff --git a/tests/test_reuse_surface_forgejo_source_provision.py b/tests/test_reuse_surface_forgejo_source_provision.py new file mode 100644 index 0000000..4bc1b2e --- /dev/null +++ b/tests/test_reuse_surface_forgejo_source_provision.py @@ -0,0 +1,201 @@ +"""Meaningful fail-closed tests; no credentials or network are used.""" +import importlib.util +import io +import json +import hashlib +import yaml +from pathlib import Path +import tempfile +import unittest +from contextlib import redirect_stdout, redirect_stderr +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location('reuse_provision', Path(__file__).parents[1] / 'scripts/provision-reuse-surface-forgejo-source.py') +p = importlib.util.module_from_spec(spec) +spec.loader.exec_module(p) + +class Journal: + attempt = 'mock-attempt' + def __init__(self): self.rows = [] + def put(self, event, **kw): self.rows.append((event, kw)) + +class Fake: + def __init__(self): + self.journal = Journal() + self.user = None + self.team = None + self.members = [] + self.repos = [] + self.tokens = [] + self.extra = [] + self.mutations = [] + def pages(self, path, sudo=False): + if path == '/orgs/coulomb/teams': return [] if self.team is None else [self.team] + if path.endswith('/members'): return self.members + if path.endswith('/repos') and path.startswith('/teams/'): return self.repos + if path == '/user/orgs': return [] if not self.members else [{'username':'coulomb'}] + if path == '/user/teams': return [] if not self.members else [{**self.team,'organization':{'username':'coulomb'}}] + if path == '/user/repos?order_by=id': return self.repos + self.extra + if path.endswith('/tokens'): return self.tokens + raise AssertionError(path) + def request(self, method, path, payload=None, expected=(200,), **kwargs): + if method != 'GET': + self.mutations.append((method,path)) + raise AssertionError('mutation must not occur in preflight failure test') + if path == '/users/' + p.USER: return (404,None) if self.user is None else (200,self.user) + if path.startswith('/repos/'): return 200, {'private':True,'full_name':'coulomb/'+path.split('/')[3]} + raise AssertionError(path) + +class Bao: + def __init__(self, data=None): self.data = data + def get(self): return self.data + +class Tests(unittest.TestCase): + def complete(self): + c = Fake() + c.user = {'login':p.USER,'restricted':True,'is_admin':False,'visibility':'private'} + c.team = {'id':10,'name':p.TEAM,'permission':'read','includes_all_repositories':False,'can_create_org_repo':False,'units_map':p.UNITS} + c.members = [{'login':p.USER}] + c.repos = [{'name':x,'owner':{'login':p.ORG},'private':True,'full_name':p.ORG+'/'+x} for x in sorted(p.REPOS)] + return c + def test_server_capped_pages_continue_until_empty(self): + c=p.Client('mock-admin',Journal()); pages={1:[{'id':x} for x in range(1,51)],2:[{'id':51}],3:[]} + def get(method,path,**kwargs): + c.last_total_count=51 + page=int(path.split('page=')[1]); return 200,pages[page] + with patch.object(c,'request',side_effect=get) as request: + self.assertEqual(len(c.pages('/user/repos?order_by=id',sudo=True)),51) + self.assertEqual(request.call_count,3) + def test_late_collaborator_private_grant_zero_mutations(self): + c=self.complete() + original_pages=c.pages + def pages(path,sudo=False): + if path.startswith('/user/repos'): + return p.Client.pages(c,path,sudo=sudo) + return original_pages(path,sudo=sudo) + original_request=c.request + def request(method,path,**kwargs): + if path.startswith('/user/repos?'): + page=int(path.split('page=')[1]) + if page==1: return 200,[{'id':x,'private':False,'full_name':'someone/public-'+str(x),'owner':{'login':'someone'}} for x in range(1,51)] + if page==2: return 200,[{'id':51,'private':True,'full_name':'other-org/excluded-collaborator','owner':{'login':'other-org'}}] + return 200,[] + return original_request(method,path,**kwargs) + c.pages=pages; c.request=request + with self.assertRaisesRegex(p.Refused,'extra_private_repository_grant'): p.provision(c,Bao(),{}) + self.assertEqual(c.mutations,[]) + def test_duplicate_page_ids_refuse(self): + c=p.Client('mock-admin',Journal()) + with patch.object(c,'request',return_value=(200,[{'id':1}])): + with self.assertRaisesRegex(p.Refused,'duplicate_page_id'): c.pages('/user/repos') + def test_total_count_mismatch_refuses(self): + c=p.Client('mock-admin',Journal()) + def request(method,path,**kwargs): + c.last_total_count=2 + return 200,[{'id':1}] if 'page=1' in path else [] + with patch.object(c,'request',side_effect=request): + with self.assertRaisesRegex(p.Refused,'total_count_mismatch'): c.pages('/user/repos') + def test_unstable_total_count_refuses(self): + c=p.Client('mock-admin',Journal()) + def request(method,path,**kwargs): + page=int(path.split('page=')[1]); c.last_total_count=2 if page==1 else 3 + return 200,[{'id':page}] + with patch.object(c,'request',side_effect=request): + with self.assertRaisesRegex(p.Refused,'unstable_total_count'): c.pages('/user/repos') + def test_excess_pagination_refuses(self): + c=p.Client('mock-admin',Journal()) + def request(method,path,**kwargs): + return 200,[{'id':int(path.split('page=')[1])}] + with patch.object(c,'request',side_effect=request): + with self.assertRaisesRegex(p.Refused,'pagination_bound_exceeded'): c.pages('/user/repos') + def test_existing_all_repository_team_refused_before_mutation(self): + c=self.complete(); c.team['includes_all_repositories']=True + with self.assertRaisesRegex(p.Refused,'team_conflict'): p.provision(c,Bao(),{}) + self.assertEqual(c.mutations,[]) + def test_existing_excluded_team_repo_refused_before_mutation(self): + c=self.complete(); c.repos.append({'name':'rapp-core-hub','owner':{'login':'coulomb'}}) + with self.assertRaisesRegex(p.Refused,'unexpected_team_repository'): p.provision(c,Bao(),{}) + self.assertEqual(c.mutations,[]) + def test_existing_admin_user_refused_before_mutation(self): + c=self.complete(); c.user['is_admin']=True + with self.assertRaisesRegex(p.Refused,'user_conflict'): p.provision(c,Bao(),{}) + self.assertEqual(c.mutations,[]) + def test_existing_token_kv_binding_mismatch_refused(self): + c=self.complete(); c.tokens=[{'id':20,'name':p.PREFIX+'one','scopes':p.SCOPES,'token_last_eight':'different'}] + with self.assertRaisesRegex(p.Refused,'pat_kv_binding_unverifiable'): p.provision(c,Bao({p.FIELD:'mock-token'}),{}) + self.assertEqual(c.mutations,[]) + def test_extra_private_grants_refused(self): + c=self.complete(); c.extra=[{'private':True,'full_name':'coulomb/secret-outside'}] + with self.assertRaisesRegex(p.Refused,'extra_private'): p.provision(c,Bao(),{}) + self.assertEqual(c.mutations,[]) + def test_orphan_pat_not_reminted(self): + c=self.complete(); c.tokens=[{'id':20,'name':p.PREFIX+'one','scopes':p.SCOPES,'token_last_eight':'ck-token'}] + with self.assertRaisesRegex(p.Refused,'orphan'): p.provision(c,Bao(),{}) + self.assertEqual(c.mutations,[]) + def test_verified_noop_rechecks_secret_and_scope(self): + c=self.complete(); c.tokens=[{'id':20,'name':p.PREFIX+'one','scopes':p.SCOPES,'token_last_eight':'ck-token'}] + with patch.object(p,'verify') as verify: + self.assertEqual(p.provision(c,Bao({p.FIELD:'mock-token'}),{}),'noop') + verify.assert_called_once_with(c,'mock-token',{}) + self.assertEqual(c.mutations,[]) + self.assertEqual(c.journal.rows[-1],('terminal',{'outcome':'verified_noop'})) + def test_existing_write_unit_refused(self): + c=self.complete(); c.team['units_map']={**p.UNITS,'repo.code':'write'} + with self.assertRaisesRegex(p.Refused,'team_conflict'): p.inspect(c,Bao()) + def test_api_ambiguous_mutation_records_intent_without_raw_error(self): + j=Journal(); c=p.Client('fake-secret-admin',j) + with patch.object(c.opener,'open',side_effect=RuntimeError('fake-secret-admin sensitive provider body')): + with self.assertRaisesRegex(p.Refused,'request_observation_failed'): + c.request('POST','/admin/users',{'password':'fake-secret-password'},operation='user_create') + self.assertEqual([x[0] for x in j.rows],['intent','outcome']) + self.assertNotIn('secret',json.dumps(j.rows)) + self.assertEqual(j.rows[-1][1]['status'],'ambiguous') + def test_network_error_never_reaches_stdout_stderr(self): + out,err=io.StringIO(),io.StringIO() + with patch.object(p,'approval',return_value={}), patch.object(p,'Journal',return_value=Journal()) as factory, patch.dict(p.os.environ,{'API_TOKEN':'fake-secret-admin'}), patch.object(p,'provision',side_effect=RuntimeError('fake-secret-admin body')): + factory.return_value.close=lambda:None + with redirect_stdout(out),redirect_stderr(err): + code=p.main(['--inner','--approval','mock','--receipt','mock']) + self.assertEqual(code,1); self.assertEqual(out.getvalue()+err.getvalue(),'') + self.assertNotIn('secret',json.dumps(factory.return_value.rows)) + def test_unapproved_ccr_refuses_before_warden(self): + with tempfile.TemporaryDirectory() as tmp: + f=Path(tmp)/'approval.json'; f.write_text(json.dumps({'ccr_id':p.CCR,'decision':'approved','supervisor':'mock','decision_ref':'mock'})) + with patch.object(p.subprocess,'run') as run: + self.assertEqual(p.main(['--approval',str(f),'--receipt',str(Path(tmp)/'receipt')]),1) + run.assert_not_called() + def test_approved_artifact_revision_mismatch_before_warden(self): + with tempfile.TemporaryDirectory() as tmp: + root=Path(tmp); files={p.CCR_FILE,p.PLAN_FILE,p.SCRIPT_FILE,'openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl'} + for rel in files: + f=root/rel; f.parent.mkdir(parents=True,exist_ok=True); f.write_text('mock') + (root/p.CCR_FILE).write_text(yaml.safe_dump({'status':'approved'})) + receipt={'ccr_id':p.CCR,'decision':'approved','supervisor':'founder','decision_ref':'mock-resolved-decision','approved_artifacts':{rel:hashlib.sha256((root/rel).read_bytes()).hexdigest() for rel in files}} + f=root/'approval.json'; f.write_text(json.dumps(receipt)); (root/p.SCRIPT_FILE).write_text('modified-after-approval') + with patch.object(p,'ROOT',root),patch.object(p.subprocess,'run') as run: + self.assertEqual(p.main(['--approval',str(f),'--receipt',str(root/'receipt')]),1) + run.assert_not_called() + def test_journal_symlink_refused(self): + with tempfile.TemporaryDirectory() as tmp: + f=Path(tmp)/'real'; f.write_text(''); link=Path(tmp)/'link'; link.symlink_to(f) + with self.assertRaises(OSError): p.Journal(link) + def test_redirects_never_follow_auth_header(self): + with self.assertRaisesRegex(p.Refused,'redirect_refused'): + p.NoRedirect().redirect_request(None,None,302,'',{ },'https://other.example') + def test_bao_cas_write_uncertain_response_reconciled(self): + b=p.Bao(); j=Journal() + with patch.object(b,'run',return_value=type('R',(),{'returncode':1})()) as run, patch.object(b,'get',return_value={p.FIELD:'fake-source'}): + b.put('fake-source',j) + self.assertIn('-cas=0',run.call_args.args[0]); self.assertEqual(run.call_args.args[1],b'fake-source') + self.assertEqual(j.rows[-1],('verified',{'operation':'kv_create','status':'matched'})) + def test_bao_timeout_preserves_ambiguous_outcome(self): + b=p.Bao(); j=Journal() + with patch.object(b,'run',side_effect=p.Refused('bao_observation_failed')): + with self.assertRaises(p.Refused): b.put('fake-source',j) + self.assertEqual(j.rows[-1],('outcome',{'operation':'kv_create','status':'ambiguous'})) + def test_kv_denial_not_treated_as_absence(self): + b=p.Bao() + with patch.object(b,'run',return_value=type('R',(),{'returncode':1,'stderr':b'Code: 403 Error: fake-secret'})()): + with self.assertRaisesRegex(p.Refused,'bao_preflight_failed'): b.get() + +if __name__=='__main__': unittest.main() diff --git a/tests/test_reuse_surface_forgejo_source_recovery.py b/tests/test_reuse_surface_forgejo_source_recovery.py new file mode 100644 index 0000000..719626e --- /dev/null +++ b/tests/test_reuse_surface_forgejo_source_recovery.py @@ -0,0 +1,43 @@ +import importlib.util +from pathlib import Path +import unittest + +spec=importlib.util.spec_from_file_location('reuse_recovery',Path(__file__).parents[1]/'scripts/revoke-reuse-surface-forgejo-source.py') +r=importlib.util.module_from_spec(spec); spec.loader.exec_module(r) + +class Journal: + def __init__(self): self.rows=[] + def put(self,event,**kw): self.rows.append((event,kw)) +class Fake: + def __init__(self,ambiguous=False,deleted=True): + self.journal=Journal(); self.mutations=[]; self.token={'id':123,'name':r.p.PREFIX+'attempt','scopes':r.p.SCOPES}; self.ambiguous=ambiguous; self.deleted=deleted; self.basic=None + def pages(self,path): return [] if self.token is None else [self.token] + def request(self,method,path,payload=None,**kw): + if method=='GET': return 200,{'login':r.p.USER,'restricted':True,'is_admin':False,'visibility':'private'} + self.mutations.append((method,path)) + if method=='PATCH': + assert set(payload)=={'password','must_change_password'} + return 200,{} + if method=='DELETE': + self.basic=kw.get('basic'); assert self.basic.startswith('Basic ') + if self.deleted: self.token=None + if self.ambiguous: raise r.p.Refused('request_observation_failed') + return 204,None + raise AssertionError(method) +class Tests(unittest.TestCase): + def target(self): return {'id':123,'name':r.p.PREFIX+'attempt'} + def test_basic_exact_token_revocation(self): + c=Fake(); r.recover(c,self.target()); self.assertEqual(c.mutations[-1],('DELETE','/users/'+r.p.USER+'/tokens/123')); self.assertEqual(c.journal.rows[-1][1]['outcome'],'revoked_verified_kv_preserved') + def test_unexpected_token_refuses_without_password_change(self): + c=Fake(); c.token['id']=124 + with self.assertRaisesRegex(r.p.Refused,'inventory_conflict'): r.recover(c,self.target()) + self.assertEqual(c.mutations,[]) + def test_ambiguous_delete_reconciles_absence_without_retry(self): + c=Fake(ambiguous=True); r.recover(c,self.target()); self.assertEqual(sum(m=='DELETE' for m,p in c.mutations),1) + def test_ambiguous_delete_still_present_stops(self): + c=Fake(ambiguous=True,deleted=False) + with self.assertRaisesRegex(r.p.Refused,'revoke_unverified'): r.recover(c,self.target()) + self.assertEqual(sum(m=='DELETE' for m,p in c.mutations),1) + def test_absent_token_verified_noop_has_no_mutations(self): + c=Fake(); c.token=None; r.recover(c,self.target()); self.assertEqual(c.mutations,[]) +if __name__=='__main__': unittest.main() diff --git a/workplans/RPF-WP-0058-reuse-private-source-reader.md b/workplans/RPF-WP-0058-reuse-private-source-reader.md new file mode 100644 index 0000000..429eb51 --- /dev/null +++ b/workplans/RPF-WP-0058-reuse-private-source-reader.md @@ -0,0 +1,84 @@ +--- +id: RPF-WP-0058 +type: workplan +title: Prepare the exact seven private-source reader for reuse-surface +domain: financials +repo: railiance-platform +status: proposed +owner: codex +topic_slug: platform +created: '2026-10-15' +updated: '2026-10-15' +flavor: implementation +depends_on: [] +--- + +Custody/Q1 work under platform INTENT.md: dependable shared secret custody and +stable delivery with reviewed, delegated apply and revocation. Founder metadata +consent is recorded separately and grants no credential or live rollout authority. +Preparation occurs in isolated clone; no own supervision record means supervised +preparation only. IDs CCR-2026-0046 and RPF-WP-0058 are provisional until integration +rechecks the authoritative owner's latest allocation. + +## Prepare bounded custody change + +```task +id: RPF-WP-0058-T01 +status: done +priority: high +``` + +Prepare CCR-2026-0046, exact seven-source bootstrap contract, silent bounded +provisioner, tests and exact ESO policy delta. No provisioner invocation against +live service, token read, policy apply, or cluster change has occurred. + +## Independent owner review and founder disposition + +```task +id: RPF-WP-0058-T02 +status: wait +priority: high +``` + +Platform prepared-review covers own custody artifacts. Forge, apps and consumer +owners independently review their scopes; their reviews remain pending. Founder +credential provisioning approval must bind exact artifact hashes and confirmed +ESO claims. No approval inferred from metadata disclosure. CCR stays proposed. +Current consumer image cannot receive this credential: full authenticated +redirect/HTTPS plus exact source URL allowlist patch and runtime config required. + +## Execute and verify restricted reader custody + +```task +id: RPF-WP-0058-T03 +status: wait +priority: high +``` + +After review and disposition, attended platform lane executes silent bootstrap. +Read-only preflight checks all seven private repos, restricted non-admin private +user, exact code-read team flags, no unrelated members/teams/private grants and +PAT/KV drift. Mutation intent and observed/ambiguous outcome are fsynced. No +uncertain mint is retried. CAS=0 never overwrites existing KV. Noop still verifies +PAT metadata binding, all seven pinned index digests and excluded-private/admin/ +Actions denial. Raw provider output and credentials remain suppressed. + +## Owner delivery and hosted completion + +```task +id: RPF-WP-0058-T04 +status: wait +priority: high +``` + +Apps owner integrates proposed data item and exact fourteen approved URL config, +performs approved fixed-image rollout after CPU/PVC preflight and safe diff/dryrun. +Confirm existing ESO SA reuse/reuse-surface-eso, role, audience and namespace +condition from live metadata. Consumer owner native verification precedes private +registration; Custodian final fresh hosted audit must prove all70 exactly once. +Removing reader grants and delivery must preserve shared writer/webhook fields. + +Revision2 of the same CCR preserves R1 capped-page failure and the independently +corrected preliminary owned-only endpoint concern. Twenty bounded pages until +empty, duplicate IDs and supplied total-count consistency prevent silent short +page acceptance. No live complete-grant proof is claimed by these mocks.