diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 35ed1a2..179988e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -18,7 +18,7 @@ | workplan | SECRETS-WP-0007 | active | — | workplans/SECRETS-WP-0007-production-lifecycle-hardening.md | | workplan | SECRETS-WP-0008 | active | — | workplans/SECRETS-WP-0008-layer-model-lifecycle-conformance.md | | workplan | SECRETS-WP-0009 | blocked | — | workplans/SECRETS-WP-0009-glas-claude-native-delivery.md | -| workplan | SECRETS-WP-0010 | blocked | — | workplans/SECRETS-WP-0010-openrouter-native-access.md | +| workplan | SECRETS-WP-0010 | active | — | workplans/SECRETS-WP-0010-openrouter-native-access.md | | task | SECRETS-WP-ADHOC-2026-08-21-T01 | done | — | workplans/ADHOC-2026-08-21.md | | task | SECRETS-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md | | task | SECRETS-WP-0001-T01 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md | diff --git a/docs/approval-consumption.md b/docs/approval-consumption.md index 687c6f0..849599f 100644 --- a/docs/approval-consumption.md +++ b/docs/approval-consumption.md @@ -199,3 +199,11 @@ The catalog CCR reference remains provenance; the native path does not demand a second hub decision or local fixture with that CCR identifier. Unserved legacy build/test paths retain their existing review gate. Claim, PDP, human-control, stance, freshness and consume refusals still prevent backend access. + + +A future `not_before` is now awaited for at most two seconds before full decision +validation. Larger skew refuses. Expiry and claim freshness are checked afterward; +the consumer never shifts its validation clock or treats a future decision as +already valid. Native preflight exposed workstation clock drift; the existing +NTP service was restarted. All three owner-store/native-PDP diagnostic joins +passed afterward, without consuming approvals. Full regression suite: 420 passed. diff --git a/docs/evidence/2026-09-14-t03-join-preflight.json b/docs/evidence/2026-09-14-t03-join-preflight.json new file mode 100644 index 0000000..76c204d --- /dev/null +++ b/docs/evidence/2026-09-14-t03-join-preflight.json @@ -0,0 +1,557 @@ +[ + { + "action": "apply", + "scope": "read-only owner-store claim and native PDP diagnosis; not authenticated claim/consume execution", + "claim_valid_now": true, + "claim_reason": "ok", + "effect": "allow", + "reason": "catalog_lane_policy_matched", + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "apply", + "resource": { + "id": "catalog:openrouter-llm-connect", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "auth_targets": [ + "se-prod-openrouter-llm-connect" + ], + "fields": [], + "policy_targets": [ + "se-prod-openrouter-llm-connect" + ], + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "09592588-ab15-53e7-89b8-c4e9f29aaacf", + "binding": { + "action": "apply", + "actor": "secrets-engine", + "digest": "sha256:03cc5b37437f14e86b686ce4054f976556334eff3fa260b03e8014ed3d9217e5", + "human_control": true, + "pdp_digest": "sha256:933427642c58c54f65dd4781b8e4c8e2f358eb96497ae35870fff4bd593d2b84", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference", + "target": { + "attributes": { + "auth_targets": [ + "se-prod-openrouter-llm-connect" + ], + "fields": [], + "policy_targets": [ + "se-prod-openrouter-llm-connect" + ], + "stage": "prod" + }, + "id": "catalog:openrouter-llm-connect", + "system": "secrets-engine", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-09-14T01:43:41+00:00", + "observed_at": "2026-09-14T01:43:11+00:00", + "ttl_seconds": 30 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-09-15T00:33:06.559689+00:00", + "not_before": "2026-09-14T00:33:06.559689+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "auth_capability": {}, + "consumers": [ + { + "auth": "kubernetes", + "claim": "serviceaccount:external-secrets/external-secrets", + "name": "llm-connect-activity-core", + "purpose": "sync llm-connect-provider-secrets for the activity-core deployment" + }, + { + "auth": "approle", + "claim": "catalog:openrouter-llm-connect", + "name": "intelligence-radar-key-check", + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference" + } + ], + "delivery_auth": { + "management": "engine", + "method": "approle", + "policy_name": "se-prod-openrouter-llm-connect", + "role_name": "se-prod-openrouter-llm-connect", + "secret_id_num_uses": 1, + "secret_id_ttl": "15m", + "token_max_ttl": "30m", + "token_num_uses": 8, + "token_ttl": "15m" + }, + "delivery_config": { + "exec_owner": { + "command": [ + "/usr/bin/python3.12", + "-I", + "-B", + "/home/worsch/.local/share/secrets-engine/recipients/openrouter-key-check-dc1569f63ef7/openrouter_key_check.py" + ], + "cwd": "/run/user/1000/secrets-engine-openrouter-check", + "environment": { + "LANG": "C.UTF-8" + }, + "files": { + "/home/worsch/.local/share/secrets-engine/recipients/openrouter-key-check-dc1569f63ef7/openrouter_key_check.py": { + "private": true, + "sha256": "dc1569f63ef74d73f0a735548c0573db2a1493f7ffc86945f52d0be9b27a3253" + }, + "/usr/bin/python3.12": { + "private": false, + "sha256": "e50d468e8b0adfb05733f5b87b3cff34829c4a8c1aea50c865aa8bdfe4bb150f" + } + }, + "owner": "intelligence-radar-key-check", + "status": "configured" + } + }, + "delivery_modes": [ + "exec-env", + "read-check" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "kind": "kv", + "mount": "platform", + "mount_management": "existing", + "org": "coulomb", + "path": "workloads/activity-core/llm-connect/llm-connect-provider-secrets", + "repo": "llm-connect", + "workload_delivery": [ + { + "mode": "external-secrets", + "owner": "railiance-platform + llm-connect" + } + ] + }, + "human_control": true, + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference" + }, + "request_digest": "sha256:1e2a0b721dca7a8e719a5837fd4e298be11be4c66535a0f40e11f2fa0639f9df", + "approval_binding_digest": "sha256:933427642c58c54f65dd4781b8e4c8e2f358eb96497ae35870fff4bd593d2b84", + "submitted_request_digest": "sha256:9241fdc373759d67b8db7c356730714e784804465f1a18d7c12a91bd200f3517" + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.catalog-lane.lifecycle", + "policy_version": "v2", + "policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:eb41f86373ada6e123049610c1722116b1638d597069b460b2d0b3538b868a60" + }, + "decision_time": "2026-09-14T01:43:11Z" + }, + "validated": true + }, + { + "action": "verify", + "scope": "read-only owner-store claim and native PDP diagnosis; not authenticated claim/consume execution", + "claim_valid_now": true, + "claim_reason": "ok", + "effect": "allow", + "reason": "catalog_lane_policy_matched", + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "verify", + "resource": { + "id": "catalog:openrouter-llm-connect", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "auth_targets": [ + "se-prod-openrouter-llm-connect" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "policy_targets": [ + "se-prod-openrouter-llm-connect" + ], + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "9416fa31-fa9e-5603-8289-f35bc9625409", + "binding": { + "action": "verify", + "actor": "secrets-engine", + "digest": "sha256:72d9267d038c17107c880ffc9009793ce9c70defbddfe2219628854b811a04b2", + "human_control": true, + "pdp_digest": "sha256:9e36cdbf3890cc5a7e550fd57191f9c0f2a6b2180909aac302aa3b999e7e6b25", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference", + "target": { + "attributes": { + "auth_targets": [ + "se-prod-openrouter-llm-connect" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "policy_targets": [ + "se-prod-openrouter-llm-connect" + ], + "stage": "prod" + }, + "id": "catalog:openrouter-llm-connect", + "system": "secrets-engine", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-09-14T01:43:43+00:00", + "observed_at": "2026-09-14T01:43:13+00:00", + "ttl_seconds": 30 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-09-15T00:33:06.722887+00:00", + "not_before": "2026-09-14T00:33:06.722887+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "auth_capability": {}, + "consumers": [ + { + "auth": "kubernetes", + "claim": "serviceaccount:external-secrets/external-secrets", + "name": "llm-connect-activity-core", + "purpose": "sync llm-connect-provider-secrets for the activity-core deployment" + }, + { + "auth": "approle", + "claim": "catalog:openrouter-llm-connect", + "name": "intelligence-radar-key-check", + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference" + } + ], + "delivery_auth": { + "management": "engine", + "method": "approle", + "policy_name": "se-prod-openrouter-llm-connect", + "role_name": "se-prod-openrouter-llm-connect", + "secret_id_num_uses": 1, + "secret_id_ttl": "15m", + "token_max_ttl": "30m", + "token_num_uses": 8, + "token_ttl": "15m" + }, + "delivery_config": { + "exec_owner": { + "command": [ + "/usr/bin/python3.12", + "-I", + "-B", + "/home/worsch/.local/share/secrets-engine/recipients/openrouter-key-check-dc1569f63ef7/openrouter_key_check.py" + ], + "cwd": "/run/user/1000/secrets-engine-openrouter-check", + "environment": { + "LANG": "C.UTF-8" + }, + "files": { + "/home/worsch/.local/share/secrets-engine/recipients/openrouter-key-check-dc1569f63ef7/openrouter_key_check.py": { + "private": true, + "sha256": "dc1569f63ef74d73f0a735548c0573db2a1493f7ffc86945f52d0be9b27a3253" + }, + "/usr/bin/python3.12": { + "private": false, + "sha256": "e50d468e8b0adfb05733f5b87b3cff34829c4a8c1aea50c865aa8bdfe4bb150f" + } + }, + "owner": "intelligence-radar-key-check", + "status": "configured" + } + }, + "delivery_modes": [ + "exec-env", + "read-check" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "kind": "kv", + "mount": "platform", + "mount_management": "existing", + "org": "coulomb", + "path": "workloads/activity-core/llm-connect/llm-connect-provider-secrets", + "repo": "llm-connect", + "workload_delivery": [ + { + "mode": "external-secrets", + "owner": "railiance-platform + llm-connect" + } + ] + }, + "human_control": true, + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference" + }, + "request_digest": "sha256:9e9f8b506b94fb3d0351b2129f6804f09bc8f207230b636da2112ff722005ab9", + "approval_binding_digest": "sha256:9e36cdbf3890cc5a7e550fd57191f9c0f2a6b2180909aac302aa3b999e7e6b25", + "submitted_request_digest": "sha256:ae7fdd0a04e2d1f131cbe8e384357aa0f994ab325032b8ff2322aba4532111c7" + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.catalog-lane.lifecycle", + "policy_version": "v2", + "policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:418548cb749db472a28010fdd9dbb2a20a1c6fdd35b3d756fac6c7a7abc306fe" + }, + "decision_time": "2026-09-14T01:43:13Z" + }, + "validated": true + }, + { + "action": "exec", + "scope": "read-only owner-store claim and native PDP diagnosis; not authenticated claim/consume execution", + "claim_valid_now": true, + "claim_reason": "ok", + "effect": "allow", + "reason": "catalog_lane_policy_matched", + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "exec", + "resource": { + "id": "catalog:openrouter-llm-connect", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "auth_targets": [ + "se-prod-openrouter-llm-connect" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "policy_targets": [ + "se-prod-openrouter-llm-connect" + ], + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "da678b61-35be-598e-8d95-a7aefa2fdc73", + "binding": { + "action": "exec", + "actor": "secrets-engine", + "digest": "sha256:f2cf0fb53b740900756ccde5587c3578fcff44beef2f1edab17b9a198a4033d8", + "human_control": true, + "pdp_digest": "sha256:9f17812750fc8962b0fc58fc09df136850c9935a864fafa998a1c030dfb75bd9", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference", + "target": { + "attributes": { + "auth_targets": [ + "se-prod-openrouter-llm-connect" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "policy_targets": [ + "se-prod-openrouter-llm-connect" + ], + "stage": "prod" + }, + "id": "catalog:openrouter-llm-connect", + "system": "secrets-engine", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-09-14T01:43:46+00:00", + "observed_at": "2026-09-14T01:43:16+00:00", + "ttl_seconds": 30 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-09-15T00:33:06.955168+00:00", + "not_before": "2026-09-14T00:33:06.955168+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "auth_capability": {}, + "consumers": [ + { + "auth": "kubernetes", + "claim": "serviceaccount:external-secrets/external-secrets", + "name": "llm-connect-activity-core", + "purpose": "sync llm-connect-provider-secrets for the activity-core deployment" + }, + { + "auth": "approle", + "claim": "catalog:openrouter-llm-connect", + "name": "intelligence-radar-key-check", + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference" + } + ], + "delivery_auth": { + "management": "engine", + "method": "approle", + "policy_name": "se-prod-openrouter-llm-connect", + "role_name": "se-prod-openrouter-llm-connect", + "secret_id_num_uses": 1, + "secret_id_ttl": "15m", + "token_max_ttl": "30m", + "token_num_uses": 8, + "token_ttl": "15m" + }, + "delivery_config": { + "exec_owner": { + "command": [ + "/usr/bin/python3.12", + "-I", + "-B", + "/home/worsch/.local/share/secrets-engine/recipients/openrouter-key-check-dc1569f63ef7/openrouter_key_check.py" + ], + "cwd": "/run/user/1000/secrets-engine-openrouter-check", + "environment": { + "LANG": "C.UTF-8" + }, + "files": { + "/home/worsch/.local/share/secrets-engine/recipients/openrouter-key-check-dc1569f63ef7/openrouter_key_check.py": { + "private": true, + "sha256": "dc1569f63ef74d73f0a735548c0573db2a1493f7ffc86945f52d0be9b27a3253" + }, + "/usr/bin/python3.12": { + "private": false, + "sha256": "e50d468e8b0adfb05733f5b87b3cff34829c4a8c1aea50c865aa8bdfe4bb150f" + } + }, + "owner": "intelligence-radar-key-check", + "status": "configured" + } + }, + "delivery_modes": [ + "exec-env", + "read-check" + ], + "fields": [ + "OPENROUTER_API_KEY" + ], + "kind": "kv", + "mount": "platform", + "mount_management": "existing", + "org": "coulomb", + "path": "workloads/activity-core/llm-connect/llm-connect-provider-secrets", + "repo": "llm-connect", + "workload_delivery": [ + { + "mode": "external-secrets", + "owner": "railiance-platform + llm-connect" + } + ] + }, + "exec_owner_sha256": "22393c7e751e458fa64bb24305ee66a26bbe3993c5aba03ae1fc1a488e777b42", + "human_control": true, + "purpose": "IR-WP-0004 read-only OpenRouter key authentication check; no inference" + }, + "request_digest": "sha256:0d18ec993f609e9fcaa0d0b18314d1cbfd10e5e16176748f0c21abbd3bf27c51", + "approval_binding_digest": "sha256:9f17812750fc8962b0fc58fc09df136850c9935a864fafa998a1c030dfb75bd9", + "submitted_request_digest": "sha256:1da3ba876912a80bf0df729ab58325d4ec8da2ff3b77f25bd4157a874f6faa06" + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.catalog-lane.lifecycle", + "policy_version": "v2", + "policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:da1e2a9704eb5aae6a6cc2d24154246221668d2bd293c0e204f5d0c9d4fb8047" + }, + "decision_time": "2026-09-14T01:43:16Z" + }, + "validated": true + } +] diff --git a/docs/evidence/2026-09-14-t03-native-execution-attempt-01.json b/docs/evidence/2026-09-14-t03-native-execution-attempt-01.json new file mode 100644 index 0000000..71319f0 --- /dev/null +++ b/docs/evidence/2026-09-14-t03-native-execution-attempt-01.json @@ -0,0 +1,24 @@ +{ + "status": "failed", + "phase": "preflight", + "observed_at": "2026-09-14T01:32:29.742409+00:00", + "actions": [], + "health_before": { + "deployment": [ + { + "namespace": "activity-core", + "name": "llm-connect", + "ready": 1 + } + ], + "externalsecret": [ + { + "namespace": "activity-core", + "name": "llm-connect-provider-secrets", + "ready": true + } + ] + }, + "failure_type": "DecisionError", + "owner_forwards_closed": true +} diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py index 47407ba..8258146 100644 --- a/src/secrets_engine/approval_consume.py +++ b/src/secrets_engine/approval_consume.py @@ -25,6 +25,7 @@ from secrets_engine.decision_check import check_decision from secrets_engine.authorization import ( build_action_request, validate_decision_envelope, + wait_for_decision_start, ) from secrets_engine.errors import DecisionError from secrets_engine.openbao import read_strict_token_file @@ -312,6 +313,7 @@ def authorize_action( request=expected_request, opener=pdp_opener or urlopen, ) + wait_for_decision_start(envelope) validated = validate_decision_envelope( envelope, expected_request, diff --git a/src/secrets_engine/authorization.py b/src/secrets_engine/authorization.py index 664f942..f96e1ca 100644 --- a/src/secrets_engine/authorization.py +++ b/src/secrets_engine/authorization.py @@ -10,6 +10,7 @@ import copy import hashlib import json import re +import time import uuid from dataclasses import dataclass from datetime import datetime, timezone @@ -399,3 +400,24 @@ def validate_decision_envelope( request_digest=evaluated, submitted_request_digest=submitted, ) + + +def wait_for_decision_start(envelope: object, *, clock=None, sleeper=None) -> None: + """Wait at most two seconds for an owner's future start, never waive it. + + Full decision/claim validation still runs afterward against the real clock. + Longer skew refuses rather than holding an approval claim until it is stale. + """ + current_time = clock or (lambda: datetime.now(timezone.utc)) + sleep = sleeper or time.sleep + if not isinstance(envelope, dict): + return + lifetime = _required_dict(envelope, "lifetime") + if lifetime.get("not_before") is None: + return + start = _parse_time(lifetime["not_before"], "not_before") + delay = (start - current_time()).total_seconds() + if delay > 2: + raise DecisionError("flex-auth decision start exceeds bounded wait") + if delay > 0: + sleep(delay) diff --git a/tests/test_decision_start_wait.py b/tests/test_decision_start_wait.py new file mode 100644 index 0000000..c61ba55 --- /dev/null +++ b/tests/test_decision_start_wait.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta, timezone +import pytest +from secrets_engine.authorization import wait_for_decision_start +from secrets_engine.errors import DecisionError + +@pytest.mark.parametrize("offset,expected", [(-1, []), (0, []), (0.25, [0.25]), (2, [2])]) +def test_waits_until_start_without_changing_the_envelope(offset, expected): + now = datetime(2026, 9, 14, tzinfo=timezone.utc) + envelope = {"lifetime": {"not_before": (now + timedelta(seconds=offset)).isoformat()}} + calls = [] + wait_for_decision_start(envelope, clock=lambda: now, sleeper=calls.append) + assert calls == expected + assert envelope["lifetime"]["not_before"] == (now + timedelta(seconds=offset)).isoformat() + +def test_large_skew_refuses_without_sleep(): + now = datetime(2026, 9, 14, tzinfo=timezone.utc) + with pytest.raises(DecisionError, match="bounded wait"): + wait_for_decision_start({"lifetime": {"not_before": (now + timedelta(seconds=3)).isoformat()}}, + clock=lambda: now, sleeper=lambda _: pytest.fail("must not sleep")) + +def test_wait_does_not_waive_expiration_or_start_validation(): + from secrets_engine.authorization import validate_decision_envelope + from tests.test_action_authorization import _envelope, _request + request = _request() + envelope = _envelope(request) + now = datetime(2026, 9, 14, tzinfo=timezone.utc) + envelope["lifetime"] = {"not_before": (now + timedelta(seconds=1)).isoformat(), + "expires_at": (now + timedelta(seconds=2)).isoformat()} + wait_for_decision_start(envelope, clock=lambda: now, sleeper=lambda _: None) + with pytest.raises(DecisionError, match="has not started"): + validate_decision_envelope(envelope, request, accepted_policy_packages={"secrets-engine.catalog-lane.lifecycle"}, accepted_policy_versions={"v2"}, now=now) + with pytest.raises(DecisionError, match="expired"): + validate_decision_envelope(envelope, request, accepted_policy_packages={"secrets-engine.catalog-lane.lifecycle"}, accepted_policy_versions={"v2"}, now=now + timedelta(seconds=3))