diff --git a/.repo-manager/index.json b/.repo-manager/index.json index 9dfe14a..5fbb1c7 100644 --- a/.repo-manager/index.json +++ b/.repo-manager/index.json @@ -2,9 +2,9 @@ "schema": "repo_manager.index.v1", "slug": "flex-auth", "repo_root": "/home/worsch/flex-auth", - "head_sha": "56940727bfb36aec705318b0128599f367256c23", - "observed_at": "2026-09-05T23:08:53.160637Z", - "source_fingerprint": "727c449ea8eb418a198b97d34244e377936060c84024b65b2af950ea71714259", + "head_sha": "d144feb2e9b74df39a085ce752335c1169ffac9a", + "observed_at": "2026-09-05T23:29:23.679186Z", + "source_fingerprint": "8708283bc20e40a97e9193fcf6de5b81cb3174f0cac5af6665ecc62d6c716c7e", "source_files": [ ".repo-classification.yaml", "INTENT.md", @@ -29,7 +29,8 @@ "workplans/FLEX-WP-0017-action-bound-authorization-contract.md", "workplans/FLEX-WP-0018-inbound-auth-corrections.md", "workplans/FLEX-WP-0019-layer-model-conformance.md", - "workplans/FLEX-WP-0020-repository-identity-migration.md" + "workplans/FLEX-WP-0020-repository-identity-migration.md", + "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md" ], "work_records": [ { @@ -1216,6 +1217,66 @@ "parent_id": "FLEX-WP-0020", "extra": {} }, + { + "kind": "workplan", + "id": "FLEX-WP-0021", + "status": "proposed", + "title": "secrets-engine consumer policy package and cluster-local pin", + "source_path": "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md", + "uuid": "b01f655e-f71a-50ae-b110-178557f07c63", + "parent_id": null, + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0021-T01", + "status": "todo", + "title": "1. Obtain the real action vocabulary from secrets-engine", + "source_path": "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md", + "uuid": "3c191808-6fe5-5a9b-9a68-a4ce4a672dbd", + "parent_id": "FLEX-WP-0021", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0021-T02", + "status": "todo", + "title": "2. Publish `secrets-engine.catalog-lane.lifecycle` v1", + "source_path": "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md", + "uuid": "38770813-dfcc-5711-bba3-0db50d3af135", + "parent_id": "FLEX-WP-0021", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0021-T03", + "status": "todo", + "title": "3. Confirm the digest join against a real decision record", + "source_path": "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md", + "uuid": "8f7e5cdd-777e-5f54-9b92-c72b79f65672", + "parent_id": "FLEX-WP-0021", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0021-T04", + "status": "wait", + "title": "4. Stand up the `flex-auth-secrets-engine` pin", + "source_path": "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md", + "uuid": "f4e8709a-65dd-5172-97ae-e7c3432afb22", + "parent_id": "FLEX-WP-0021", + "extra": {} + }, + { + "kind": "task", + "id": "FLEX-WP-0021-T05", + "status": "wait", + "title": "5. Hand the pin coordinates back and close", + "source_path": "workplans/FLEX-WP-0021-secrets-engine-consumer-policy-gate.md", + "uuid": "f0828871-fd65-5d8c-adfd-28b13fedd2b0", + "parent_id": "FLEX-WP-0021", + "extra": {} + }, { "kind": "decision", "id": "FLEX-DEC-2026-001", @@ -1344,7 +1405,7 @@ "status": "resolved", "title": "Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05", "source_path": "decisions/decisions.md", - "uuid": null, + "uuid": "f6bfb02a-bbf3-4488-acf9-0e1cb57ce19d", "parent_id": null, "extra": { "record": { @@ -1366,7 +1427,9 @@ "decided_by": "flex-auth (access-engine / PDP)", "rationale": "Answered, not declined. A decision lifetime shorter than the SSH certificate TTL is meaningful, but only as an authority-to-issue window, never as an authority-to-use window over an already-issued certificate. flex-auth lifetime.expires_at bounds how long that one allow may be relied on to authorise a sign; it cannot bound an artifact ops-warden issued under it, and flex-auth does not claim it does. The downstream contract that consumes the shorter lifetime is the pre-sign gate itself: no replay of an allow past expires_at, and a fresh Check per sign. The lever that actually shortens effective access is the requested TTL as a policy input, which is already deployed as the ttl_out_of_bounds deny; ops-warden section 9.7.2 window through cert TTL is correctly stated and correctly owned by the PEP.", "created": "2026-09-05T23:08:38.877787Z", - "updated": "2026-09-05T23:08:38.877787Z" + "updated": "2026-09-06T00:00:00.000000Z", + "decided_at": "2026-09-06T00:00:00.000000Z", + "state_hub_decision_id": "f6bfb02a-bbf3-4488-acf9-0e1cb57ce19d" } } }, @@ -1376,7 +1439,7 @@ "status": "resolved", "title": "secrets-engine policy package is expected but unpublished; /v1/check has no estate-wide endpoint by design", "source_path": "decisions/decisions.md", - "uuid": null, + "uuid": "f318e4e2-6195-4db8-b8c0-c71eeecad766", "parent_id": null, "extra": { "record": { @@ -1398,7 +1461,43 @@ "decided_by": "flex-auth (access-engine / PDP)", "rationale": "Two answers. (1) Yes, a real package is expected, and flex-auth authors it in this repo as it did for every other consumer; the reserved coordinate is secrets-engine.catalog-lane.lifecycle at version v1, and it does not exist yet. Until it is published and pinned, secrets-engine keeping SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE and _VERSION as required configuration with no fallback is the correct shape and flex-auth endorses it. (2) POST /v1/check is deployed, but there is no estate-wide PDP address and there is not meant to be one: each consumer gets its own cluster-local pin whose NetworkPolicy default-denies ingress except from that one approved workload. The 2026-09-06 probe finding no reachable PDP is the design working, not an outage. A reachable endpoint for secrets-engine is a per-consumer pin that follows its policy package.", "created": "2026-09-05T23:08:51.144287Z", - "updated": "2026-09-05T23:08:51.144287Z" + "updated": "2026-09-06T00:00:00.000000Z", + "decided_at": "2026-09-06T00:00:00.000000Z", + "state_hub_decision_id": "f318e4e2-6195-4db8-b8c0-c71eeecad766" + } + } + }, + { + "kind": "decision", + "id": "FLEX-DEC-2026-006", + "status": "resolved", + "title": "ActionAuthorization deferral accepted; G3 is closed and does not argue for ratifying it", + "source_path": "decisions/decisions.md", + "uuid": null, + "parent_id": null, + "extra": { + "record": { + "id": "FLEX-DEC-2026-006", + "kind": "decision", + "title": "ActionAuthorization deferral accepted; G3 is closed and does not argue for ratifying it", + "status": "resolved", + "origin": "cross-repo", + "origin_ref": "APPROVAL-IN-0002 / GH-DEC-2026-003", + "owner": "flex-auth", + "affects": [ + "flex-auth", + "approval-engine", + "gate-house", + "secrets-engine" + ], + "requested_dispositions": [ + "accept", + "contest" + ], + "decided_by": "flex-auth (access-engine / PDP)", + "rationale": "Accepted, and flex-auth argues against its own proposal. The composed ActionAuthorization object was never ratified; the approval-claim plus DecisionEnvelope split lands each check on the layer that owns it and drops none. approval-engine asked whether the open G3 finding (DecisionEnvelope carries no lifetime) argues for ratifying the composed object now. It does not, because G3 is already closed the other way: FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required on every allow by schema conditional, published 2026-09-02. The revisit trigger is spent, and it resolved by adding a field rather than by composition, so the decision now stands alone and needs no bundle. Separately, the provenance.authority == state-hub constant is flex-auth defect: it came from examples/caring/action_authorization.json, which contradicted our own ownership section. Corrected at source, with the schema and contract doc marked deferred-not-withdrawn so no other consumer builds a validator against them.", + "created": "2026-09-05T23:29:20.156770Z", + "updated": "2026-09-05T23:29:20.156770Z" } } }, @@ -1479,15 +1578,15 @@ "type": "repo.command.applied", "command": "repo.work.create_decision", "operation": "create", - "correlation_id": "ff360be5-e7bf-4914-95c3-1ea6df9f6957", + "correlation_id": "ead04d14-441f-4ecd-8ee8-12ae0cd23362", "kind": "decision", - "id": "FLEX-DEC-2026-005", + "id": "FLEX-DEC-2026-006", "git_sha": null, "files_touched": [ "decisions/decisions.md" ], "source": "repo-manager", - "emitted_at": "2026-09-05T23:08:53.160770Z" + "emitted_at": "2026-09-05T23:29:23.679390Z" } ] } diff --git a/decisions/decisions.md b/decisions/decisions.md index 48e9d2d..03730f9 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -744,3 +744,137 @@ that order: publish the package, then pin it. published. Nothing in this record authorises a fallback value. - No change to `flex-auth.decision-record.v1`. The digest join secrets-engine built against it is correct as described. + +## FLEX-DEC-2026-006 — ActionAuthorization deferral accepted; G3 is closed and does not argue for ratifying it + +```yaml +id: FLEX-DEC-2026-006 +kind: decision +title: ActionAuthorization deferral accepted; G3 is closed and does not argue for + ratifying it +status: resolved +origin: cross-repo +origin_ref: APPROVAL-IN-0002 / GH-DEC-2026-003 +owner: flex-auth +affects: +- flex-auth +- approval-engine +- gate-house +- secrets-engine +requested_dispositions: +- accept +- contest +decided_by: flex-auth (access-engine / PDP) +rationale: 'Accepted, and flex-auth argues against its own proposal. The composed + ActionAuthorization object was never ratified; the approval-claim plus DecisionEnvelope + split lands each check on the layer that owns it and drops none. approval-engine + asked whether the open G3 finding (DecisionEnvelope carries no lifetime) argues + for ratifying the composed object now. It does not, because G3 is already closed + the other way: FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required + on every allow by schema conditional, published 2026-09-02. The revisit trigger + is spent, and it resolved by adding a field rather than by composition, so the decision + now stands alone and needs no bundle. Separately, the provenance.authority == state-hub + constant is flex-auth defect: it came from examples/caring/action_authorization.json, + which contradicted our own ownership section. Corrected at source, with the schema + and contract doc marked deferred-not-withdrawn so no other consumer builds a validator + against them.' +created: '2026-09-05T23:29:20.156770Z' +updated: '2026-09-05T23:29:20.156770Z' +``` + +## Context + +`approval-engine` filed `APPROVAL-IN-0002` with gate-house and raised it here +directly, because the request touches flex-auth's artifact and accepting the +shelving is flex-auth's call. + +The trigger was concrete damage: secrets-engine built its PEP validator against +the `ActionAuthorization` schema in +`docs/action-bound-authorization-contract.md`, then pointed it at +`GET /v1/approvals/{id}/claim`. It rejects every response. Both envelopes +declare `schema_version: 0.1`, so the mismatch surfaces late as a field or +authority error that reads like an `approval-engine` outage rather than a +contract mismatch. + +## Disposition + +**Accepted.** The approval-claim is the step-1 artifact; `ActionAuthorization` +is not required on that path; a PEP validates across the claim (approval fact) +and flex-auth's step-2 `DecisionEnvelope` (exact `CheckRequest` match and policy +pin). + +flex-auth argues **against its own proposal** here. The composed object had the +PIP republish flex-auth's decision, which is the part that was wrong: it moves an +artifact across a layer boundary that the estate's own §2 rule says it should not +cross. flex-auth used that rule to decline authentication evidence in +`FLEX-DEC-2026-002` and to win §17's decision-record schema in +`FLEX-DEC-2026-003`. It applies the same way when it costs us the object. + +The split drops no check. Each of the five verifications in the contract's +"Required verification" section lands on the layer that owns it — status, +validity window, supersession, and distinct authenticated approvers on the +approval-claim; effect, binding match, digest, and policy pin on the +`DecisionEnvelope`. + +### Answer to the question asked before the decision: G3 does not argue for it + +`approval-engine` named the open G3 finding — `DecisionEnvelope` carries no +lifetime — as a revisit trigger, *if* it is settled by composition rather than +by adding a lifetime field, and asked flex-auth to say so **before** the +decision if G3 is the strongest case for ratifying now. + +It is not, and the reason is that **G3 is already closed, the other way.** + +`FLEX-WP-0019` closed it by adding the field. `schemas/decision_envelope.schema.json` +now carries a conditional requiring `lifetime` whenever `effect` is `allow`; the +duration comes from the policy package's `allow_ttl` with a `15m` engine default, +and a package declaring `allow_ttl: none` produces a deny with reason +`allow_lifetime_unstated` rather than a standing grant. This was published in +`docs/decision-record-contract.md` on 2026-09-02. + +So the trigger is spent, and it resolved on the branch that removes the argument +rather than the one that supports it. A `DecisionEnvelope` now states its own +end without borrowing `ActionAuthorizationValidity`. The bundle is not needed to +give an allow a lifetime, which was the only structural thing it did that the +split does not. + +The 2026-08-29 alignment review's table still shows G3 `open`. That file is a +dated review record and is not rewritten; this record supersedes its status. + +### The `provenance.authority == "state-hub"` constant is flex-auth's defect + +Correct on the merits, and correctable regardless of how the deferral landed. +`approval-engine` located a real contradiction inside one flex-auth document +set, and the source is narrower than the report suggested — it is a fixture, not +prose: + +```json +"provenance": { "authority": "state-hub" } +``` + +That was the tail of `examples/caring/action_authorization.json`, directly +contradicting the same contract's ownership section: *"State Hub decision +records are coordination and provenance evidence; they are not the runtime +approval authority."* A consumer reading the example rather than the paragraph +gets the wrong constant, which is what happened. + +Corrected at source. The example now names `approval-engine` as the approval +fact's authority and flex-auth as the decision's, and says State Hub is neither. +Its stale `secrets-engine.lifecycle` policy pin is also corrected to the +reserved-but-unpublished coordinate from `FLEX-DEC-2026-005`. + +## Consequences + +- `docs/action-bound-authorization-contract.md` status is now **deferred, not + withdrawn**, with the two-step split stated up front and an explicit "do not + build a validator against it" warning. The `PROPOSED` material is retained as + the record of what was proposed. +- `schemas/action_authorization.schema.json` carries the same deferral in its + `description`, so a machine reader sees it too. `schema_version` stays `0.1`; + bumping it would imply a ratified successor that does not exist. +- The execute-time half is untouched and stays published: `POST /v1/check`, + `binding`, the canonical request digest, and `flex-auth.decision-record.v1`. + Nothing secrets-engine built against the digest join is invalidated. +- No revisit trigger remains open on flex-auth's side. If the composed object is + ever revisited, it needs a fresh argument, not G3. + diff --git a/docs/action-bound-authorization-contract.md b/docs/action-bound-authorization-contract.md index 67b6800..aaa1808 100644 --- a/docs/action-bound-authorization-contract.md +++ b/docs/action-bound-authorization-contract.md @@ -1,9 +1,11 @@ # Action-bound authorization contract -Status: implemented for execute-time request binding; durable approval storage -and lifecycle require `approval-engine` support. +Status: **execute-time request binding is implemented and published.** The +composed `ActionAuthorization` object in this document is **deferred, not +withdrawn** — see "Deferral of the composed object" below. Do not build a +validator against it. -Date: 2026-08-23 +Date: 2026-08-23 (deferral recorded 2026-09-06, `FLEX-DEC-2026-006`) ## Ownership and endpoints @@ -30,7 +32,28 @@ decision records are coordination and provenance evidence; they are not the runtime approval authority. Production consumers must not infer action binding or dual control from State Hub prose or a free-form `decided_by` value. -## Durable object +## Deferral of the composed object + +`ActionAuthorization` was never ratified. `GH-DEC-2026-003` (2026-08-29) named +`approval-engine`'s **approval-claim** as the step-1 artifact, and flex-auth +accepts that split (`FLEX-DEC-2026-006`): + +- **Step 1 — approval fact:** `approval-engine`'s approval-claim, served from + `GET /v1/approvals/{id}/claim`. `approval-engine` is its authority. +- **Step 2 — decision:** flex-auth's `DecisionEnvelope`, with exact + `CheckRequest` match via `binding.request_digest` and the policy pin. + +A PEP validates **across both**. No check from the composed object is dropped; +each one lands on the layer that owns it. The composed object would have had +the PIP republish flex-auth's decision, which is the part that was wrong. + +The `PROPOSED` framing below is retained as the record of what was proposed. +It is not a contract, and a consumer that validates a `GET /v1/approvals/{id}/claim` +response against it will reject every response — both envelopes declare +`schema_version: 0.1`, so the mismatch surfaces late and reads like an +`approval-engine` outage. That is exactly what happened to secrets-engine. + +## Durable object (deferred — proposed, never ratified) [`../schemas/action_authorization.schema.json`](../schemas/action_authorization.schema.json) defines the proposed `ActionAuthorization` storage and transport object. It @@ -80,6 +103,14 @@ A production consumer may execute only when all of the following hold: Local fixtures, CCR labels, workplan ids, prose, and a caller-supplied flex-auth decision id are provenance only. They cannot unlock a production action. +**State Hub is never the authority here.** A validator must not require +`provenance.authority == "state-hub"`, or accept it as evidence. State Hub +decision records are coordination and provenance evidence, as the Ownership +section above states. The authority of the approval fact is `approval-engine`; +the authority of the decision is flex-auth. The stale `"authority": "state-hub"` +value in `examples/caring/action_authorization.json` was the source of that +constant and is corrected. + ## Outage and supersession semantics - `approval-engine` unreachable: privileged live diff --git a/examples/caring/action_authorization.json b/examples/caring/action_authorization.json index 50835f0..b32103c 100644 --- a/examples/caring/action_authorization.json +++ b/examples/caring/action_authorization.json @@ -15,7 +15,9 @@ "system": "secrets-engine", "attributes": { "stage": "build", - "fields": ["token"], + "fields": [ + "token" + ], "policy_targets": [], "auth_targets": [] } @@ -59,7 +61,9 @@ "system": "secrets-engine", "attributes": { "stage": "build", - "fields": ["token"], + "fields": [ + "token" + ], "policy_targets": [], "auth_targets": [] } @@ -80,7 +84,9 @@ "system": "secrets-engine", "attributes": { "stage": "build", - "fields": ["token"], + "fields": [ + "token" + ], "policy_targets": [], "auth_targets": [] } @@ -99,12 +105,15 @@ "provenance": { "evaluator": "flex-auth/local", "mode": "standalone", - "policy_package": "secrets-engine.lifecycle", + "policy_package": "secrets-engine.catalog-lane.lifecycle", "policy_version": "v1", - "decision_time": "2026-08-23T10:00:30Z" + "decision_time": "2026-08-23T10:00:30Z", + "policy_package_note": "Reserved coordinate, not yet published (FLEX-DEC-2026-005). Illustrative only." } }, "provenance": { - "authority": "state-hub" + "note": "DEFERRED EXAMPLE (FLEX-DEC-2026-006). Not a contract. The approval fact's authority is approval-engine; the decision's authority is flex-auth. State Hub is never the authority for either -- do not derive a provenance.authority constant from this file.", + "approval_authority": "approval-engine", + "decision_authority": "flex-auth" } } diff --git a/schemas/action_authorization.schema.json b/schemas/action_authorization.schema.json index 40c4739..c68e17d 100644 --- a/schemas/action_authorization.schema.json +++ b/schemas/action_authorization.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://flex-auth.netkingdom/schemas/action_authorization.schema.json", "title": "ActionAuthorization", - "description": "Durable approval envelope for one exact authorization request. This is a storage/transport contract; live evaluation remains POST /v1/check.", + "description": "DEFERRED, NOT WITHDRAWN (FLEX-DEC-2026-006, 2026-09-06). Proposed durable approval envelope for one exact authorization request; never ratified. GH-DEC-2026-003 named approval-engine's approval-claim as the step-1 artifact instead, and a PEP validates across that claim and flex-auth's DecisionEnvelope. Do not validate an approval-claim response against this schema. Live evaluation remains POST /v1/check.", "type": "object", "additionalProperties": false, "required": ["schema_version", "id", "status", "request", "validity", "approvals", "decision"],