diff --git a/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md b/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md index 08e5516..5069487 100644 --- a/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md +++ b/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md @@ -5,15 +5,15 @@ title: "ADR-0004 — High-risk lanes refuse raw value streaming to agent session domain: infotech repo: ops-warden status: accepted -version: "1.0" -revision: "1" +version: "1.1" +revision: "2" owner: ops-warden binds: "ops-warden; any agent runtime calling warden access" created: "2026-07-20" -updated: "2026-08-18" -last_reviewed: "2026-08-18" +updated: "2026-09-04" +last_reviewed: "2026-09-04" review_interval: 6m -enforced_by: "src/warden/access.py (exit 7); OpenBao policy agent-high-risk-boundary" +enforced_by: "src/warden/access.py + src/warden/cli.py (exit 7); OpenBao policy agent-high-risk-boundary" supersedes: "" successor: "" --- @@ -48,6 +48,13 @@ refuses to stream the raw value and exits 7.** The agent is not blocked from doi its work — `--out`, `--exec`, `--wrap` and `--fingerprint` all remain available. It is blocked from doing its work *in a way that writes the secret into a transcript*. +**Revision 2 also recognizes the issued coding-agent subject.** When +`WARDEN_POLICY_SUBJECT` is the KeyCape-issued +`service:codex:railiance-platform` identity, the same refusal applies without +`WARDEN_AGENT_ID`. The legacy variable remains a fail-toward-safety fallback. +Ops-warden reads this identity marker; it does not validate the token or decide +whether the principal is authorized. + **The boundary is enforced at the credential store as well as at the CLI.** The OpenBao policy `agent-high-risk-boundary` denies data-read on those paths for agent tokens, allowing metadata and capabilities only. A control that lives solely in our @@ -73,10 +80,12 @@ retry correctly. Changing it is a breaking change to every agent runtime. **`risk: high` becomes a load-bearing catalog field** rather than documentation. Classifying a lane now changes runtime behaviour, so it must be set deliberately. -**We accept that `--unsafe-stdout` still exists for humans.** The boundary keys on -`WARDEN_AGENT_ID`, so an agent that does not set it is not caught. That is a known -limit: this ADR raises the floor for cooperating runtimes and hardens the store -behind them; it does not claim to stop a determined caller. +**We accept that `--unsafe-stdout` still exists for humans.** The CLI recognizes the +issued subject when the runtime presents it and otherwise relies on +`WARDEN_AGENT_ID`; both markers remain caller-visible and therefore advisory. This +ADR raises the floor for cooperating runtimes and hardens the store behind them; it +does not claim to stop a determined caller. The OpenBao +`agent-high-risk-boundary` policy is the enforced control. ## Related diff --git a/layer.yaml b/layer.yaml index be3ec97..ea8e961 100644 --- a/layer.yaml +++ b/layer.yaml @@ -1,6 +1,6 @@ # ops-warden — NetKingdom security layer declaration # -# Framework: net-kingdom/canon/standards/security-layer-model_v0.4.md +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md # Assent: docs/adr/ADR-0010 (ops-warden's own voice, per §11 "who must declare") # Validate: python3 scripts/check_layer_conformance.py # @@ -14,7 +14,7 @@ schema_version: "0.1" framework: netkingdom-security-layer-model -standard_version: "0.4" +standard_version: "0.7" repository: ops-warden layer: staff declared_by: docs/adr/ADR-0010 diff --git a/pep-stance.yaml b/pep-stance.yaml index cf4e83f..6364a80 100644 --- a/pep-stance.yaml +++ b/pep-stance.yaml @@ -1,6 +1,6 @@ # ops-warden — PEP unreachable-engine stance map # -# Framework: net-kingdom/canon/standards/security-layer-model_v0.6.md §6.4, §9.3 +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3, §9.7 # Rule of record: docs/adr/ADR-0009 # Validate: pytest tests/test_layer_conformance.py -k stance # @@ -18,7 +18,7 @@ schema_version: "0.1" framework: netkingdom-security-layer-model -standard_version: "0.6" +standard_version: "0.7" repository: ops-warden pep_shape: true declared_by: docs/adr/ADR-0009 @@ -67,3 +67,38 @@ input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebu reconstructability: bound: "§9.6 attributive — completeness is not claimed" declared_at: wiki/AuditTrail.md + +# §9.7.2 — the certificate-verification PEP has no active revocation channel. +# A previously allowed certificate remains usable until its enforced TTL expires, +# so the visibility deadline is exactly the maximum issued lifetime per actor type. +revocation_visibility: + boundary: "SSH certificate verification at the target host" + mechanism: ttl_expiry + revocation_channel: none + deadline_hours: + adm: 48 + agt: 24 + atm: 8 + note: >- + No CRL or KRL distribution exists. Host-side authorization belongs to + railiance-infra; shortening or actively recalling this window is a joint + design question, not an implied capability of ops-warden. + +# §9.6 — signing evidence is attributive, not load-bearing. Measurement as of +# 2026-09-04 found three signature records across 79 calendar days, concentrated +# on only two active days (1 event on 2026-06-17; 2 within 40 minutes on +# 2026-08-22). That bursty operator-driven sample cannot support a useful rate +# threshold, so cadence is explicitly deferred instead of fabricated. +emission_cadence: + classification: attributive + status: deferred + measured_as_of: "2026-09-04" + observed_window: + first: "2026-06-17T23:18:33Z" + last: "2026-09-04T21:21:30Z" + signature_records: 3 + active_signature_days: 2 + daily_counts: + "2026-06-17": 1 + "2026-08-22": 2 + reason: "operator-driven issuance is too sparse and bursty for a meaningful rate threshold" diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 5811077..3be856d 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -40,6 +40,24 @@ version: 1 entries: + - id: netkingdom-layer-declaration + title: NetKingdom layer declaration and PEP stance-map route + risk: standard + workload_ref: + applicability: not-applicable + reason: "Repository conformance guidance, not a workload credential or runtime action." + need_keywords: [how, declare, layer, declaration, netkingdom, security, companion, pep, pep-shaped, stance, conformance, tooling, staff, engine] + owner_repo: ops-warden + subsystem: NetKingdom layer-conformance routing + warden_executes: false + wiki_ref: wiki/playbooks/netkingdom-layer-declaration.md#worker-checklist + canon_ref: net-kingdom/canon/standards/security-layer-model_v0.7.md + reviewed: "2026-09-04" + status: active + delegation: + mode: permanent + reviewed: "2026-09-04" + - id: ssh-cert-host-access title: Short-lived SSH certificate for host / ops reachability # Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05). diff --git a/src/warden/access.py b/src/warden/access.py index ad3af27..0b60704 100644 --- a/src/warden/access.py +++ b/src/warden/access.py @@ -12,13 +12,21 @@ reuses `expand_handoff` to build the command it runs as the caller. """ from __future__ import annotations +import os from dataclasses import dataclass -from typing import Optional +from typing import Mapping, Optional from warden.config import ConfigError, load_config from warden.routing.models import RouteEntry +# KeyCape KEY-WP-0009-T03 is the issued coding-agent identity available today. +# This is intentionally an exact subject allowlist, not a guess based on every +# ``service:*`` identity. OpenBao validates and enforces the credential; this +# module only recognizes the already-issued subject for the advisory CLI guard. +ISSUED_AGENT_SUBJECTS = frozenset({"service:codex:railiance-platform"}) + + @dataclass class ExpandedHandoff: """Handoff templates with `` substituted when a domain is supplied. @@ -34,6 +42,25 @@ class ExpandedHandoff: exec_capable: bool +def agent_read_boundary_identity( + environ: Mapping[str, str] | None = None, +) -> str | None: + """Return the issued/fallback agent marker used by the advisory read guard. + + ``WARDEN_POLICY_SUBJECT`` carries the principal identity used by the policy + request. When it names an issued coding-agent subject, prefer it over the + self-declared legacy marker. This function does not validate a token or + render an authorization decision; OpenBao's agent policy is the enforced + boundary. ``WARDEN_AGENT_ID`` remains a fail-toward-safety fallback. + """ + env = os.environ if environ is None else environ + issued_subject = str(env.get("WARDEN_POLICY_SUBJECT") or "").strip() + if issued_subject in ISSUED_AGENT_SUBJECTS: + return issued_subject + fallback = str(env.get("WARDEN_AGENT_ID") or "").strip() + return fallback or None + + def _sub_domain(value: Optional[str], domain: Optional[str]) -> Optional[str]: if value and domain: return value.replace("", domain) diff --git a/src/warden/cli.py b/src/warden/cli.py index b28d6da..dcea9c2 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -2,7 +2,6 @@ from __future__ import annotations import json -import os from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Annotated, List, Optional @@ -1309,7 +1308,9 @@ def _access_proxy( # T04 — agent identity on a high-risk lane: never stream raw secret data. # Agents may use sanctioned transports (--out / --exec / --wrap / --fingerprint). - agent_id = os.environ.get("WARDEN_AGENT_ID", "").strip() + from warden.access import agent_read_boundary_identity + + agent_id = agent_read_boundary_identity() raw_value_stream = ( not is_login and not do_exec and not wrap and not out_path and not fingerprint ) diff --git a/tests/test_access.py b/tests/test_access.py index f5765d9..5e10a7d 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -6,7 +6,7 @@ from pathlib import Path from typer.testing import CliRunner -from warden.access import expand_handoff, policy_gate_status +from warden.access import agent_read_boundary_identity, expand_handoff, policy_gate_status from warden.cli import app from warden.routing.models import RouteEntry @@ -64,6 +64,33 @@ def test_policy_gate_status_no_config(monkeypatch, tmp_path): assert "advisory" in policy_gate_status() +def test_issued_coding_agent_subject_drives_read_boundary_without_legacy_marker(): + env = {"WARDEN_POLICY_SUBJECT": "service:codex:railiance-platform"} + assert agent_read_boundary_identity(env) == "service:codex:railiance-platform" + + +def test_legacy_agent_marker_remains_fail_toward_safety_fallback(): + env = {"WARDEN_AGENT_ID": "grok"} + assert agent_read_boundary_identity(env) == "grok" + + +def test_non_agent_policy_subject_does_not_manufacture_agent_identity(): + env = {"WARDEN_POLICY_SUBJECT": "iam:bernd"} + assert agent_read_boundary_identity(env) is None + + +def test_explicit_empty_environment_does_not_inherit_process_identity(monkeypatch): + monkeypatch.setenv("WARDEN_AGENT_ID", "codex") + monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "service:codex:railiance-platform") + assert agent_read_boundary_identity({}) is None + assert agent_read_boundary_identity() == "service:codex:railiance-platform" + + +def test_operator_subject_does_not_override_legacy_agent_marker(): + env = {"WARDEN_POLICY_SUBJECT": "iam:bernd", "WARDEN_AGENT_ID": "codex"} + assert agent_read_boundary_identity(env) == "codex" + + # --- CLI ------------------------------------------------------------------- def test_access_advisory_output(monkeypatch): diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index 59284aa..b10b7f7 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -30,6 +30,7 @@ class TestDeclaration: d = _decl() assert d["repository"] == "ops-warden" assert d["layer"] == "staff" + assert d["standard_version"] == "0.7" # §11: "only the repository's own file, in its own voice, conforms." assert d["declared_by"] == "docs/adr/ADR-0010" @@ -131,3 +132,21 @@ class TestPepStanceMap: """§6.4 obligation 2: caching an input claim is permitted; caching the answer is a second decision point deciding early (§6.1).""" assert self._stance()["verdict_caching"] == "none" + + def test_revocation_visibility_deadline_equals_enforced_ttl_policy(self): + """§9.7.2: a published replay window must not drift from issuance.""" + from warden.models import ActorType, MAX_TTL_HOURS + + published = self._stance()["revocation_visibility"] + expected = {actor.value: MAX_TTL_HOURS[actor] for actor in ActorType} + assert published["deadline_hours"] == expected + assert published["mechanism"] == "ttl_expiry" + assert published["revocation_channel"] == "none" + + def test_attributive_emission_cadence_deferral_carries_measurement(self): + cadence = self._stance()["emission_cadence"] + assert cadence["classification"] == "attributive" + assert cadence["status"] == "deferred" + assert cadence["observed_window"]["signature_records"] == 3 + assert cadence["observed_window"]["active_signature_days"] == 2 + assert cadence["reason"] diff --git a/tests/test_proxy.py b/tests/test_proxy.py index b67b122..9955add 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -590,6 +590,27 @@ def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch): assert "agent read-boundary" in r.output.lower() or "risk=high" in r.output.lower() +def test_access_issued_agent_subject_refuses_raw_stream_without_legacy_marker( + tmp_path, monkeypatch +): + """WP-0034 T02: the issued KeyCape subject is sufficient for exit 7.""" + _proxy_env(monkeypatch, tmp_path) + monkeypatch.setenv("VAULT_TOKEN", "caller-token") + monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) + monkeypatch.setenv( + "WARDEN_POLICY_SUBJECT", "service:codex:railiance-platform" + ) + r = runner.invoke( + app, + [ + "access", "railiance-backup-offsite-lane", + "--fetch", "--unsafe-stdout", + ], + ) + assert r.exit_code == 7, r.output + assert "service:codex:railiance-platform" in r.output + + def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch): """Agents may use --fingerprint on high-risk lanes (no raw value).""" _proxy_env(monkeypatch, tmp_path) diff --git a/tests/test_routing.py b/tests/test_routing.py index 8c319e1..cd3d6e8 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -143,6 +143,18 @@ def test_route_find_vault_token_ops_warden_prefers_broker_lane(): assert matches[0].id == "ops-warden-warden-sign-token" +def test_route_find_layer_declaration_resolves_to_reference_playbook(): + catalog = load_catalog(_repo_catalog()) + entry = catalog.find("how do I declare my layer", limit=1)[0] + assert entry.id == "netkingdom-layer-declaration" + assert entry.owner_repo == "ops-warden" + assert entry.wiki_ref.endswith( + "netkingdom-layer-declaration.md#worker-checklist" + ) + assert entry.canon_ref.endswith("security-layer-model_v0.7.md") + assert entry.exec_capable is False + + def test_whynot_design_npm_lane_is_concrete_and_resolvable(): """The provisioned Forgejo npm lane is exact, concrete, and resolvable.""" catalog = load_catalog(_repo_catalog()) diff --git a/tests/test_workload_join.py b/tests/test_workload_join.py index d87e5dd..170cd8c 100644 --- a/tests/test_workload_join.py +++ b/tests/test_workload_join.py @@ -14,9 +14,10 @@ def test_repo_catalog_uses_only_explicit_workload_references(): assert report["ok"] is True assert len(report["resolved"]) == 3 assert len(report["unknown"]) == 18 - # 11 since WARDEN-WP-0033: the two NetKingdom SSO lanes (c374d41) are - # provider/control-plane credentials, not workload delivery lanes. - assert len(report["not_applicable"]) == 11 + # 12 since WARDEN-WP-0034: the layer-declaration route is repository + # conformance guidance, not a workload delivery lane. The two NetKingdom SSO + # lanes (c374d41) remain provider/control-plane credentials as well. + assert len(report["not_applicable"]) == 12 assert {row["lane"] for row in report["resolved"]} == { "ops-warden-warden-sign-token", "issue-core-ingestion-api-key", diff --git a/wiki/playbooks/netkingdom-layer-declaration.md b/wiki/playbooks/netkingdom-layer-declaration.md new file mode 100644 index 0000000..dc0cd77 --- /dev/null +++ b/wiki/playbooks/netkingdom-layer-declaration.md @@ -0,0 +1,37 @@ +# NetKingdom layer declaration route + +Date: 2026-09-04 +Catalog: `netkingdom-layer-declaration` +Doctrine owner: `gate-house` +Path steward: `ops-warden` + +This page is a route from the accepted NetKingdom security companion to the +reference declaration artifacts. It does not redefine the layer model. + +## Worker checklist + +1. Read `net-kingdom/SECURITY-COMPANION.md`, then use the accepted + `net-kingdom/canon/standards/security-layer-model_v0.7.md` for the normative + layer, Tooling-contact, and PEP obligations. +2. Use ops-warden's `layer.yaml` as the machine-readable declaration reference. + Adapt its repository, layer, contacts, and ownership facts; do not copy + ops-warden-specific claims as your own. +3. If the repository is PEP-shaped, use `pep-stance.yaml` as the stance-map + reference and publish the resulting path in the repository's layer declaration. +4. Adapt `scripts/check_layer_conformance.py` and + `tests/test_layer_conformance.py`, then run both checks in the declaring repo. +5. Send the declaration and any stance-map inventory pointer to `gate-house` for + doctrine/register review. Route credential or operational-lane questions back + through `warden route`; do not place doctrine in the routing catalog. + +Reference checks in this checkout: + +```bash +python3 scripts/check_layer_conformance.py +pytest tests/test_layer_conformance.py +``` + +## Ownership boundary + +`gate-house` owns what the model requires. Each repository owns the truth of its +own declaration. `ops-warden` owns only this discoverable path to those sources. diff --git a/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md b/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md index 8e52f00..7d779ab 100644 --- a/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md +++ b/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md @@ -4,14 +4,14 @@ type: workplan title: "Layer model v0.7 conformance — state the deadline, bind the agent boundary, steward the estate's newest rule" domain: infotech repo: ops-warden -status: ready +status: active owner: ops-warden topic_slug: netkingdom planning_priority: P1 depends_on_workplans: - WARDEN-WP-0030 created: "2026-08-29" -updated: "2026-08-29" +updated: "2026-09-05" state_hub_workstream_id: "ae3ff76f-883d-5e2f-b6aa-144d61e8fdef" --- @@ -36,7 +36,7 @@ completed directly; none of these qualify. ```task id: WARDEN-WP-0034-T01 -status: todo +status: done priority: high state_hub_task_id: "8b3bdb9f-d2c2-5b3e-89e2-417bf3e37484" ``` @@ -60,9 +60,16 @@ of a revocation channel is stated rather than left to inference. **Not in scope:** shortening it. Whether 48h is acceptable is a joint question with `railiance-infra` (KRL distribution) and is T05's to raise, not this task's to decide. +Completed 2026-09-04. `pep-stance.yaml` now publishes TTL expiry as the only +revocation mechanism, explicitly records that no CRL/KRL channel exists, and +states `adm=48h`, `agt=24h`, and `atm=8h`. The conformance test derives the +expected map from `ActorType`/`MAX_TTL_HOURS`, so declaration and issuance +policy cannot drift independently. The layer and stance declarations now name +the accepted v0.7 standard. + ```task id: WARDEN-WP-0034-T02 -status: todo +status: done priority: high state_hub_task_id: "3318ee1a-b5d9-5d39-baf7-9c42a8bc7b55" ``` @@ -90,9 +97,18 @@ enforced/advisory split is written down. identity claim, adjacent to deciding, and `ADR-0002`/§6 both point away from it. Read the identity; do not adjudicate it. +Completed 2026-09-04. The advisory CLI guard now prefers the exact issued +KeyCape subject `service:codex:railiance-platform` from +`WARDEN_POLICY_SUBJECT`, then falls back to `WARDEN_AGENT_ID`. Tests prove the +issued subject triggers exit 7 without the legacy marker, the fallback remains, +and an operator subject does not manufacture an agent identity. ADR-0004 +revision 2 records the enforced/advisory split: Warden reads the marker and +does not validate a token; OpenBao's `agent-high-risk-boundary` is the real +enforcement. + ```task id: WARDEN-WP-0034-T03 -status: todo +status: done priority: medium state_hub_task_id: "a891b32c-b0a7-59f6-a5cd-977be65c09ca" ``` @@ -111,9 +127,16 @@ that as the finding with the distribution that shows it, and declare the deferra **Done when:** either a declared cadence with its derivation, or a declared deferral carrying the data that justifies it. +Completed 2026-09-04 with an explicit measured deferral. The signatures log +contains three issuances across the 79-day observation window, on only two +active days: one on 2026-06-17 and two within 40 minutes on 2026-08-22. The +unified audit carries the latter two events. `pep-stance.yaml` records the +counts, dates, attributive classification, and reason that this sparse, +operator-driven burst cannot support a meaningful rate threshold. + ```task id: WARDEN-WP-0034-T04 -status: todo +status: done priority: medium state_hub_task_id: "94e73daa-f74d-51fd-8639-68896a4066ee" ``` @@ -138,9 +161,15 @@ to copy — `layer.yaml`, `pep-stance.yaml`, `check_layer_conformance.py`, playbook is reachable from the catalog. **Not** a restatement of the companion — a pointer layer, per `ADR-0001`. +Completed 2026-09-04. Catalog entry `netkingdom-layer-declaration` resolves the +exact query to `wiki/playbooks/netkingdom-layer-declaration.md`. The short +playbook points at the accepted companion/statute, the four reference artifacts +and checks, and the gate-house review path while explicitly leaving doctrine +with gate-house and declaration truth with each repository. + ```task id: WARDEN-WP-0034-T05 -status: todo +status: wait priority: low state_hub_task_id: "7d1b3c82-9b96-5087-a53a-496212909029" ``` @@ -162,8 +191,24 @@ fix them (`ADR-0003`, `ADR-0005`). **Done when:** both are routed with reasoning, and the answers recorded either way — including a refusal, which is an equally good answer. +Raised 2026-09-04 and waiting for answers. Ops-mason received the stance-map +reference and classification question (message `6ddd6cc7`). Railiance-infra +received the KRL-versus-TTL question (message `3bfa306e`). Flex-auth, as the +current access-engine implementation, received the decision-lifetime semantics +question (message `02c99a74`). No implementation or policy decision was assigned +to another owner by these messages. + ## Related +Review 2026-09-05: reviewed the pending implementation and fixed the identity +helper so an explicitly empty environment does not inherit the process identity. +Regression coverage also proves an operator subject cannot override the legacy +agent marker. Refreshed the generated high-risk path artifact after the catalog +changes. T05 remains waiting: no corresponding reply was present in the inbox, +and the checked owner workplans contained no resolution of these questions. +Validation: 415 unit tests and four SSH integration tests pass; Ruff, the +layer-conformance script, and the declaration-route CLI smoke check pass. + - `history/2026-08-29-v07-scope-intent-assessment.md` — the gap analysis behind this plan - `history/2026-08-29-layer-model-v04-review.md`, `-v06-review.md` — the two prior reviews - `security-layer-model_v0.7.md` §3.4, §6.4, §9.6, §9.7, §11, §13.1