docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
592d8d1082
commit
1fc8eed19c
50 changed files with 11442 additions and 0 deletions
|
|
@ -0,0 +1,216 @@
|
|||
---
|
||||
id: NK-WP-0013
|
||||
type: workplan
|
||||
title: "Playbook Capability Contract"
|
||||
domain: infotech
|
||||
repo: net-kingdom
|
||||
status: finished
|
||||
owner: worsch
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
planning_order: 13
|
||||
created: "2026-05-21"
|
||||
updated: "2026-05-22"
|
||||
depends_on:
|
||||
- NK-WP-0006
|
||||
state_hub_workstream_id: 32a54d8e-8633-42a6-8ec1-104842c581c1
|
||||
---
|
||||
|
||||
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
|
||||
|
||||
# NK-WP-0013 — Playbook Capability Contract
|
||||
|
||||
> The **orchestration-layer analog of the IAM Profile**. ADR-0007's
|
||||
> meta-orchestration refinement named this as a prerequisite: for NetKingdom
|
||||
> to select and parametrize playbooks reliably, each Railiance playbook must
|
||||
> publish a **declared interface** — the capability it provisions, its
|
||||
> parameters (with defaults and constraints), and the responsibility it
|
||||
> claims. Without it, meta-orchestration composes against implicit behavior
|
||||
> and breaks silently when a playbook changes. This workplan defines that
|
||||
> contract.
|
||||
|
||||
## Goal
|
||||
|
||||
Define the **Playbook Capability Contract** — the schema and vocabulary by
|
||||
which a playbook declares:
|
||||
|
||||
1. **what capability it provisions** (controlled vocabulary, aligned to the
|
||||
capability ladder and the responsibility-map resource kinds),
|
||||
2. **its parameters** — name, type, default, constraints, required/optional,
|
||||
and which are security-sensitive, and
|
||||
3. **the responsibility it claims** — which element owns which resources, and
|
||||
which trust state(s) / readiness checks it satisfies or requires.
|
||||
|
||||
…plus the **catalog/consumption model** NetKingdom meta-orchestration uses
|
||||
to select, parametrize, and build a responsibility map for a scenario, and a
|
||||
**conformance validator** for declarations.
|
||||
|
||||
## Ownership (the boundary this realizes)
|
||||
|
||||
By symmetry with the IAM Profile, the **consumer that needs a stable
|
||||
interface defines the contract; the provider conforms**:
|
||||
|
||||
| | Contract owner | Publishes conformant declarations | Consumer |
|
||||
|---|---|---|---|
|
||||
| Identity | NetKingdom (IAM Profile) | key-cape / Keycloak | apps, flex-auth |
|
||||
| Orchestration | **NetKingdom (this contract)** | Railiance playbooks | NetKingdom meta-orchestration |
|
||||
|
||||
NetKingdom owns the contract schema; Railiance owns the playbooks and
|
||||
publishes one declaration per playbook; **execution stays in Railiance**
|
||||
(ADR-0007 unchanged). The contract makes a playbook change a versioned event
|
||||
rather than a silent break.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- ownership & boundary decision (ADR) and contract versioning governance
|
||||
- the capability vocabulary a declaration draws from
|
||||
- the parameter declaration format (defaults, constraints, sensitivity)
|
||||
- the responsibility-claim and trust-state/readiness format
|
||||
- the catalog and the meta-orchestration consumption model
|
||||
- a conformance validator for declarations
|
||||
- coordinated adoption proof with one reference Railiance playbook
|
||||
|
||||
Out of scope:
|
||||
|
||||
- authoring the playbooks themselves (Railiance owns execution)
|
||||
- writing declarations for every existing playbook (Railiance work; this
|
||||
workplan proves the contract with one reference declaration)
|
||||
- a dedicated execution-orchestration repo (deferred per ADR-0007)
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: NK-WP-0013-T1
|
||||
state_hub_task_id: d40f8b29-e983-4d52-bc1f-5f1c51709e7d
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Ownership & boundary decision (ADR-0012).** Record that NetKingdom owns
|
||||
the Playbook Capability Contract schema (consumer-defines-contract, IAM
|
||||
Profile precedent), Railiance authors playbooks and publishes conformant
|
||||
declarations, and execution stays in Railiance (ADR-0007). Decide whether
|
||||
the contract is NetKingdom-owned outright or NetKingdom-owned with Railiance
|
||||
co-design. Define contract **versioning and breaking-change governance**.
|
||||
|
||||
```task
|
||||
id: NK-WP-0013-T2
|
||||
state_hub_task_id: ece4b5b1-e1c2-449d-b0f4-83b7010bc838
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Capability vocabulary.** Define the controlled vocabulary of capabilities
|
||||
a playbook can declare it provisions, aligned to the **capability ladder
|
||||
(C0–C6)** and the **responsibility-map resource kinds** (identities,
|
||||
roles/scopes/policies, secrets/credentials, infrastructure resources). This
|
||||
is what "capability" means in a declaration, so two playbooks providing the
|
||||
same capability are comparable.
|
||||
|
||||
```task
|
||||
id: NK-WP-0013-T3
|
||||
state_hub_task_id: c956f4a8-b9fa-44ab-8174-31999b98e3b1
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Parameter declaration format.** Define how a playbook declares its
|
||||
parameters: name, type, **default**, **constraints** (allowed values /
|
||||
ranges), required vs optional, and a **security-sensitivity** marker so
|
||||
NetKingdom knows which parameters it may tune versus which are
|
||||
security-critical and constrained. This is what makes "parametrize where
|
||||
adequate" safe rather than guesswork.
|
||||
|
||||
```task
|
||||
id: NK-WP-0013-T4
|
||||
state_hub_task_id: e7de05a6-528a-4213-b6db-2c2e90353996
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Responsibility-claim & trust-state format.** Define how a declaration
|
||||
states which element **owns which resources** (feeding the responsibility
|
||||
map) and which **trust state(s) / readiness checks** (ADR-0007 model) it
|
||||
satisfies or requires. This lets NetKingdom assemble a coherent
|
||||
responsibility map and sequence for a scenario from the declarations.
|
||||
|
||||
```task
|
||||
id: NK-WP-0013-T5
|
||||
state_hub_task_id: 05a2ff7d-86c4-4de9-9ea8-39a9ad5352a8
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
**Catalog, consumption model, and conformance validator.** Define how
|
||||
declarations are published and discovered (the catalog), how
|
||||
meta-orchestration consumes them to **select**, **parametrize**, and build a
|
||||
**responsibility map** for a scenario, and a **conformance validator** that
|
||||
checks a declaration against the contract (the orchestration-layer analog of
|
||||
the IAM Profile conformance check, NK-WP-0012-T5).
|
||||
|
||||
```task
|
||||
id: NK-WP-0013-T6
|
||||
state_hub_task_id: 769ed490-c091-41c1-b2e2-e8e378470b6b
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
**Reference adoption with Railiance.** Coordinate with Railiance to have
|
||||
**one reference playbook publish a conformant declaration end-to-end**, and
|
||||
demonstrate NetKingdom selecting + parametrizing it from the declaration
|
||||
alone. Proves the contract is sufficient and is the template for adopting
|
||||
the rest. Cross-repo coordination item for the Railiance domain.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- An ADR records contract ownership, the Railiance-publishes/NetKingdom-
|
||||
consumes split, and ADR-0007 is confirmed unchanged.
|
||||
- The contract specifies capability vocabulary, parameter format (defaults,
|
||||
constraints, sensitivity), and responsibility/trust-state claims.
|
||||
- A catalog/consumption model and a declaration conformance validator exist.
|
||||
- At least one Railiance playbook publishes a conformant declaration, and
|
||||
NetKingdom composes and parametrizes it from that declaration alone.
|
||||
- Contract versioning and breaking-change governance is documented.
|
||||
|
||||
## Completion Notes
|
||||
|
||||
- ADR: `docs/adr/ADR-0012-playbook-capability-contract-ownership.md`
|
||||
- Canonical contract:
|
||||
`canon/standards/playbook-capability-contract_v0.1.md`
|
||||
- Machine-readable schema:
|
||||
`canon/schemas/playbook-capability-declaration_v0.1.schema.json`
|
||||
- Validator and composition demo:
|
||||
`tools/playbook-capability-contract/playbook_contract_validator.py`
|
||||
- Reference Railiance declaration:
|
||||
`../railiance-infra/capabilities/playbooks/railiance-infra.bootstrap-host.yaml`
|
||||
- Sample scenario:
|
||||
`examples/playbook-capability-contract/scenario-s1-host-bootstrap.yaml`
|
||||
- Fixture tests cover valid declarations, controlled vocabulary failures,
|
||||
forbidden tenant overrides, missing required parameters, and successful
|
||||
selection/parameter composition.
|
||||
|
||||
## Dependencies & Sequencing
|
||||
|
||||
- **Realizes** the playbook-contract dependency from ADR-0007's
|
||||
meta-orchestration refinement; does not change ADR-0007.
|
||||
- **Depends on NK-WP-0006** for the trust-state model and the
|
||||
responsibility-map resource kinds the declarations reference.
|
||||
- **Coordinates with the Railiance domain** — NetKingdom defines the
|
||||
contract; Railiance publishes declarations (T6) and continues to own
|
||||
execution.
|
||||
- Parallels NK-WP-0012: same consumer-defines-contract pattern, same
|
||||
conformance-check shape, applied to orchestration instead of identity.
|
||||
|
||||
## Resolved Questions
|
||||
|
||||
- Contract format and home: NetKingdom canon standard plus a
|
||||
machine-readable JSON schema and standalone validator.
|
||||
- Catalog mechanism: v0.1 uses file convention
|
||||
`capabilities/playbooks/*.yaml`; a registry can be layered on later.
|
||||
- Parameter sensitivity: tenant scenarios cannot override
|
||||
`platform_only`, `forbidden`, `playbook_default`,
|
||||
`security_sensitive`, or `secret_reference` parameters.
|
||||
- Validator form: standalone NetKingdom tool for v0.1, mirroring
|
||||
NK-WP-0012's executable-contract pattern.
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
id: NK-WP-0014
|
||||
type: workplan
|
||||
title: "User Engine Preparation And Boundary Contracts"
|
||||
domain: infotech
|
||||
repo: net-kingdom
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
planning_order: 14
|
||||
created: "2026-05-22"
|
||||
updated: "2026-05-22"
|
||||
depends_on:
|
||||
- NK-WP-0012
|
||||
- NK-WP-0013
|
||||
state_hub_workstream_id: "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c"
|
||||
---
|
||||
|
||||
> **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`).
|
||||
|
||||
# NK-WP-0014 - User Engine Preparation And Boundary Contracts
|
||||
|
||||
## Goal
|
||||
|
||||
Prepare implementation of `/home/worsch/user-engine` by turning the current
|
||||
architecture review into concrete contracts. This workplan must be completed
|
||||
before production code is started, so the MVP does not duplicate IAM,
|
||||
authorization, application registration, membership, audit, or deployment
|
||||
responsibilities already owned by NetKingdom-aligned components.
|
||||
|
||||
## Context
|
||||
|
||||
The user-engine PRD and architecture blueprint define a headless user-domain
|
||||
service for users, accounts, profiles, preferences, memberships, application
|
||||
catalogs, projections, audit, and events.
|
||||
|
||||
The architecture review in
|
||||
`docs/reviews/2026-05-22T19-19-59+0200-user-engine-architecture-review.md`
|
||||
identified the main risk: duplicate truth across IAM providers, user-engine,
|
||||
flex-auth, application registrations, OIDC clients, protected systems, and
|
||||
profile projections.
|
||||
|
||||
NetKingdom keeps this workplan because it owns cross-repo boundary and
|
||||
orchestration guidance. Implementation work now lives in the `user-engine`
|
||||
repository as `USER-WP-0001` through `USER-WP-0006`.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- source-of-truth matrix;
|
||||
- membership synchronization contract;
|
||||
- application onboarding contract;
|
||||
- projection boundary split;
|
||||
- authorization performance model;
|
||||
- audit correlation contract;
|
||||
- user-engine repo preparation artifacts.
|
||||
- NetKingdom/user-engine interface guidance.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- implementing user-engine production code, which belongs in the user-engine
|
||||
repository workplans;
|
||||
- implementing UI repos;
|
||||
- implementing SCIM or enterprise federation adapters;
|
||||
- changing the NetKingdom IAM Profile.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: NK-WP-0014-T1
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4e941174-ac55-4f6e-8568-40f45b1ed821"
|
||||
```
|
||||
|
||||
**Source-of-truth matrix.** Define which system owns each resource kind:
|
||||
identity claims, human subjects, local user records, account lifecycle,
|
||||
groups, roles, memberships, tenants, applications, OIDC clients,
|
||||
flex-auth protected systems, catalog namespaces, profile values,
|
||||
effective-profile projections, audit records, and events.
|
||||
|
||||
Initial guidance lives in `docs/user-engine-interface-guidance.md` and should
|
||||
be hardened into versioned contracts as implementation feedback arrives.
|
||||
|
||||
```task
|
||||
id: NK-WP-0014-T2
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5ddc46bb-6238-4944-a023-d8b46b410c76"
|
||||
```
|
||||
|
||||
**Membership synchronization contract.** Specify which memberships are mastered
|
||||
in user-engine, which are imported from IAM or provisioning systems, which are
|
||||
exported to flex-auth, and how freshness, conflict handling, deletes, and
|
||||
tenant boundaries are represented.
|
||||
|
||||
```task
|
||||
id: NK-WP-0014-T3
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "696bc65b-0f8d-47b5-bccc-65ed285b42e6"
|
||||
```
|
||||
|
||||
**Application onboarding contract.** Define the binding between a user-engine
|
||||
application, IAM OIDC client, flex-auth protected system, catalog namespace,
|
||||
event/audit identity, and Railiance deployment metadata. Include a checklist
|
||||
for adding one new application safely.
|
||||
|
||||
```task
|
||||
id: NK-WP-0014-T4
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "99ab4535-cbbf-4e13-a2c5-adfc814d5aeb"
|
||||
```
|
||||
|
||||
**Projection and claims-enrichment boundaries.** Split projection types into
|
||||
runtime, self-service UI, admin UI, audit, agent context, and optional claims
|
||||
enrichment. Explicitly state that user-engine does not issue tokens and that
|
||||
claims enrichment is adapter-owned and cache/freshness governed.
|
||||
|
||||
```task
|
||||
id: NK-WP-0014-T5
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d35a1356-a9fe-4cf3-8fb0-6f45cfbbccee"
|
||||
```
|
||||
|
||||
**Authorization and audit contracts.** Define the user-engine resource/action
|
||||
vocabulary for flex-auth, when checks are synchronous, when they may be
|
||||
batched or cached, and how user-engine audit records correlate with flex-auth
|
||||
decision IDs and platform audit sinks.
|
||||
|
||||
```task
|
||||
id: NK-WP-0014-T6
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "04fd2f92-3aa4-468d-9e9e-9b092890507e"
|
||||
```
|
||||
|
||||
**Prepare the user-engine repo.** Add or update user-engine `SCOPE.md`,
|
||||
architecture notes, repo layout decision, local development contract, and
|
||||
initial implementation readiness checklist. Do not add NetKingdom
|
||||
orchestration relationships to user-engine `INTENT.md`; those belong in
|
||||
NetKingdom documents per ADR-0010.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Boundary contracts exist and are reviewed against the PRD and architecture
|
||||
review.
|
||||
- user-engine responsibilities are distinct from key-cape, Keycloak,
|
||||
flex-auth, OpenBao, Railiance, user-account, and user-manager.
|
||||
- The first implementation slice can start without unresolved ownership of
|
||||
memberships, applications, projections, authorization checks, or audit
|
||||
correlation.
|
||||
- user-engine repo-local workplans carry implementation tasks, while
|
||||
NetKingdom retains only boundary, orchestration, and responsibility-map work.
|
||||
- NetKingdom responsibility-map updates are either applied or explicitly
|
||||
deferred until user-engine becomes a shared platform service.
|
||||
|
||||
## Completion Notes
|
||||
|
||||
Completed on 2026-05-22:
|
||||
|
||||
- Added the canonical versioned contract at
|
||||
`canon/standards/user-engine-boundary-contract_v0.1.md`.
|
||||
- Promoted `docs/user-engine-interface-guidance.md` to an implementation guide
|
||||
that points at the canonical contract.
|
||||
- Confirmed `docs/responsibility-map.md` already includes `user-engine` as an
|
||||
orchestrated repository and names NetKingdom's boundary responsibilities.
|
||||
- Confirmed `/home/worsch/user-engine` already carries the required repo-local
|
||||
preparation artifacts: `SCOPE.md`, `wiki/ArchitectureBlueprint.md`,
|
||||
`docs/development.md`, `docs/configuration.md`,
|
||||
`docs/interfaces/netkingdom-integration.md`, and `USER-WP-0001` through
|
||||
`USER-WP-0006`.
|
||||
|
||||
## Dependencies And Sequencing
|
||||
|
||||
- Depends on NK-WP-0012 for IAM Profile consumption rules.
|
||||
- Depends on NK-WP-0013 for NetKingdom meta-orchestration conventions.
|
||||
- Gates implementation work in `/home/worsch/user-engine`, now tracked as
|
||||
`USER-WP-0001` through `USER-WP-0006`.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"exit_code": 0,
|
||||
"commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"base": "001a38ca2aa85155ce4840c4da22aca6e2346171",
|
||||
"stdout": "[main 3236d50] docs: update published Warden original-context checkpoint\n 1 file changed, 23 insertions(+), 10 deletions(-)\n",
|
||||
"stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 1, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n"
|
||||
}
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
{
|
||||
"repo_slug": "net-kingdom",
|
||||
"repo_path": "/home/worsch/net-kingdom",
|
||||
"source_revision": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"source_revision_reason": null,
|
||||
"issues": [
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-24",
|
||||
"message": "Repo classification advisory: capability_tag 'security' is not a recommended family tag (allowed, check for synonym)",
|
||||
"file_path": "",
|
||||
"db_id": "",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-35",
|
||||
"message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['NET-WP', 'NK-WP']",
|
||||
"file_path": "workplans/",
|
||||
"db_id": "",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-37",
|
||||
"message": "Residual workplan 'nk-wp-0011' has no origin/origin_ref provenance (not invented)",
|
||||
"file_path": "workplans/NK-WP-0011-enterprise-federation-saml.md",
|
||||
"db_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md",
|
||||
"db_id": "",
|
||||
"file_value": "USER-WP-0020",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md",
|
||||
"db_id": "",
|
||||
"file_value": "KEY-WP-0004",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md",
|
||||
"db_id": "",
|
||||
"file_value": "USER-WP-0020",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md",
|
||||
"db_id": "",
|
||||
"file_value": "KEY-WP-0004",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'USER-WP-0021' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
|
||||
"db_id": "",
|
||||
"file_value": "USER-WP-0021",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'FLEX-WP-0009' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
|
||||
"db_id": "",
|
||||
"file_value": "FLEX-WP-0009",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'AUDIT-WP-0003' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
|
||||
"db_id": "",
|
||||
"file_value": "AUDIT-WP-0003",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'EMAIL-WP-0004' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
|
||||
"db_id": "",
|
||||
"file_value": "EMAIL-WP-0004",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'USER-WP-0022' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
|
||||
"db_id": "",
|
||||
"file_value": "USER-WP-0022",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'KEY-WP-0008' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
|
||||
"db_id": "",
|
||||
"file_value": "KEY-WP-0008",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'CSOC-WP-0003' is not linked to State Hub",
|
||||
"file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
|
||||
"db_id": "",
|
||||
"file_value": "CSOC-WP-0003",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-37",
|
||||
"message": "Residual workplan 'nk-wp-0027' has no origin/origin_ref provenance (not invented)",
|
||||
"file_path": "workplans/NK-WP-0027-reef-placement-reconciliation.md",
|
||||
"db_id": "965ad365-6b81-50a1-a2a3-2d0c1fcce0b4",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-37",
|
||||
"message": "Residual workplan 'nk-wp-0031' has no origin/origin_ref provenance (not invented)",
|
||||
"file_path": "workplans/NK-WP-0031-deterministic-posture-feedback.md",
|
||||
"db_id": "9d7b04f9-3803-5613-b7a5-8bd606c77f5a",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-37",
|
||||
"message": "Residual workplan 'nk-wp-0040' has no origin/origin_ref provenance (not invented)",
|
||||
"file_path": "workplans/NK-WP-0040-execution-attribution-receipt.md",
|
||||
"db_id": "e2533f3a-aa43-59b3-bff3-8e64b6149487",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-40",
|
||||
"message": "blocker satisfied, task still wait: 'NK-WP-0048-T15' in workplan 'NK-WP-0048' \u2014 every depends_on target is terminal (NK-WP-0048-T31)",
|
||||
"file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T15",
|
||||
"db_id": "",
|
||||
"file_value": "NK-WP-0048-T31",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-22",
|
||||
"message": "Task description drift 'NK-WP-0048-T15': file content differs from DB (file wins)",
|
||||
"file_path": "workplans/NK-WP-0048-security-case-types.md#NK-WP-0048-T15",
|
||||
"db_id": "1b825705-4848-5edc-832f-b1622cd9bdb7",
|
||||
"file_value": "2026-10-17 source part merged (061461e via 65559ef). `build()` takes optional\npaired `holder`/`observe` capabilities and",
|
||||
"db_value": "2026-10-17 source part merged (061461e via 65559ef). `build()` takes optional\npaired `holder`/`observe` capabilities and",
|
||||
"fixable": true
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-20",
|
||||
"message": "Workplan dependency target 'RAIL-PL-WP-0002' is not linked to State Hub",
|
||||
"file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md",
|
||||
"db_id": "",
|
||||
"file_value": "RAIL-PL-WP-0002",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "INFO",
|
||||
"check_id": "C-08",
|
||||
"message": "Retired DB workstream 'nk-wp-0035@retired-20260908' (id=064c5e8b\u2026, status=archived) has no backing workplan file \u2014 projection tombstone, not live work",
|
||||
"file_path": "",
|
||||
"db_id": "064c5e8b-7852-4d7f-a5fa-e26e406ab8e0",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "INFO",
|
||||
"check_id": "C-08",
|
||||
"message": "Retired DB workstream 'adhoc-2026-08-14@retired-20260827' (id=a5097121\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
|
||||
"file_path": "",
|
||||
"db_id": "a5097121-1461-59f8-81fa-e6ecef527337",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "INFO",
|
||||
"check_id": "C-08",
|
||||
"message": "Retired DB workstream 'adhoc-2026-08-23@retired-20260827' (id=e25ba5fa\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
|
||||
"file_path": "",
|
||||
"db_id": "e25ba5fa-eaae-5039-8d93-a69c6d837fc4",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "INFO",
|
||||
"check_id": "C-08",
|
||||
"message": "Retired DB workstream 'net-kingdom-adhoc-2026-06-14@retired-20260827' (id=c6f3d6bf\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
|
||||
"file_path": "",
|
||||
"db_id": "c6f3d6bf-4916-490d-96ce-092d2776d7e7",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "INFO",
|
||||
"check_id": "C-08",
|
||||
"message": "Retired DB workstream 'net-kingdom-adhoc-2026-07-02@retired-20260827' (id=67c1c7ac\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
|
||||
"file_path": "",
|
||||
"db_id": "67c1c7ac-d8b1-41dc-a81a-4e43f1afd068",
|
||||
"file_value": "",
|
||||
"db_value": "",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-26",
|
||||
"message": "Workplan id 'NET-WP-0020' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
|
||||
"file_path": "workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md",
|
||||
"db_id": "",
|
||||
"file_value": "NET-WP-0020",
|
||||
"db_value": "NK-WP",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-26",
|
||||
"message": "Workplan id 'NET-WP-0015' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
|
||||
"file_path": "workplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md",
|
||||
"db_id": "",
|
||||
"file_value": "NET-WP-0015",
|
||||
"db_value": "NK-WP",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-26",
|
||||
"message": "Workplan id 'NET-WP-0016' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
|
||||
"file_path": "workplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md",
|
||||
"db_id": "",
|
||||
"file_value": "NET-WP-0016",
|
||||
"db_value": "NK-WP",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-26",
|
||||
"message": "Workplan id 'NET-WP-0018' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
|
||||
"file_path": "workplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md",
|
||||
"db_id": "",
|
||||
"file_value": "NET-WP-0018",
|
||||
"db_value": "NK-WP",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-26",
|
||||
"message": "Workplan id 'NET-WP-0017' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
|
||||
"file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md",
|
||||
"db_id": "",
|
||||
"file_value": "NET-WP-0017",
|
||||
"db_value": "NK-WP",
|
||||
"fixable": false
|
||||
},
|
||||
{
|
||||
"severity": "WARN",
|
||||
"check_id": "C-26",
|
||||
"message": "Workplan id 'NET-WP-0019' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
|
||||
"file_path": "workplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md",
|
||||
"db_id": "",
|
||||
"file_value": "NET-WP-0019",
|
||||
"db_value": "NK-WP",
|
||||
"fixable": false
|
||||
}
|
||||
],
|
||||
"fixes_applied": [
|
||||
"bindings: synced 64/64",
|
||||
"C-17 cleared: pushed 1 backlogged commit(s)",
|
||||
"C-22 fixed: task 1b825705\u2026 description updated",
|
||||
"push: pushed"
|
||||
],
|
||||
"summary": {
|
||||
"fail": 0,
|
||||
"automation_error": 0,
|
||||
"warn": 26,
|
||||
"info": 5
|
||||
},
|
||||
"result": "warn"
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
from pathlib import Path
|
||||
import json,hashlib,subprocess,re,urllib.request
|
||||
p=Path(__file__).resolve().parent;r=Path('/home/worsch/net-kingdom')
|
||||
commit=json.loads((p/'commit-receipt.json').read_text())['commit']
|
||||
def git(*a):return subprocess.check_output(['git','-C',str(r),*a]).decode().strip()
|
||||
assert git('rev-parse','HEAD')==commit
|
||||
assert git('ls-remote','origin','refs/heads/main').split()[0]==commit
|
||||
projection=json.loads((p/'projection.json').read_text());receipt=projection['receipt']
|
||||
assert projection['ok'] and projection['status'] in ('applied','noop')
|
||||
assert receipt['expected_commit']==receipt['derived_commit']==commit
|
||||
assert (receipt['instance_role'],receipt['instance_label'])==('primary','railiance01')
|
||||
assert receipt['outcome']['counts']['retired']==0
|
||||
before=json.loads((p/'tasks-before.json').read_text())
|
||||
req=urllib.request.Request('http://127.0.0.1:8000/tasks/?workplan_id=e31f3f9d-0748-5137-b0a3-7b3ca46a66eb',headers={'X-StateHub-Component':'net-kingdom'})
|
||||
after=json.load(urllib.request.urlopen(req,timeout=30));(p/'tasks-after.json').write_text(json.dumps(after,indent=2)+'\n')
|
||||
def identity(rows):return sorted((x['id'],x['status']) for x in rows)
|
||||
assert identity(before)==identity(after)
|
||||
name='workplans/NK-WP-0048-security-case-types.md';original=(p/'backup'/name).read_text();current=(r/name).read_text()
|
||||
blocks=lambda text:re.findall(r'```task\n.*?```',text,re.S)
|
||||
assert blocks(original)==blocks(current)
|
||||
assert (r/name).read_bytes()==(p/'candidate.md').read_bytes()
|
||||
t15=[b for b in blocks(current) if 'id: NK-WP-0048-T15\n' in b][0]
|
||||
summary={'status':'published-and-projected','commit':commit,'only_committed_path':git('diff-tree','--no-commit-id','--name-only','-r',commit),'projection_status':projection['status'],'instance_role':receipt['instance_role'],'instance_label':receipt['instance_label'],'expected_commit':receipt['expected_commit'],'derived_commit':receipt['derived_commit'],'original_task_count':len(before),'current_task_count':len(after),'all_task_ids_and_statuses_unchanged':True,'all_source_task_blocks_unchanged':True,'T15_task_block':t15,'preservation':json.loads((p/'preservation-verification.json').read_text()),'native_or_credential_or_adapter_changes':False,'root_git_brief_work_modified':False}
|
||||
(p/'verification.json').write_text(json.dumps(summary,indent=2)+'\n')
|
||||
manifest={'files':[{'path':str(f.relative_to(p)),'sha256':hashlib.sha256(f.read_bytes()).hexdigest()} for f in sorted(p.rglob('*')) if f.is_file() and f.name!='manifest.json']}
|
||||
(p/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
print(json.dumps({'status':summary['status'],'commit':commit,'task_count':len(after),'projection':projection['status'],'manifest_sha256':hashlib.sha256((p/'manifest.json').read_bytes()).hexdigest()}))
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"schema": "repo-manager.identifier-assignment.v1",
|
||||
"ok": true,
|
||||
"executed": false,
|
||||
"repo": "net-kingdom",
|
||||
"namespace": "helixforge",
|
||||
"assignments": [],
|
||||
"historical_invalid_identifiers": [],
|
||||
"historical_identity_collisions": [],
|
||||
"files_changed": []
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "apply_commit.py",
|
||||
"sha256": "ae5b4b42c44ca69e6b97ac0667401064eb81c22eae9ab2db3d579792e587b9a3"
|
||||
},
|
||||
{
|
||||
"path": "approved.patch",
|
||||
"sha256": "4402448cac9b50b745f247aec1c550a5fc3532e702ad4ceeae0812691278118c"
|
||||
},
|
||||
{
|
||||
"path": "backup/.custodian-brief.md",
|
||||
"sha256": "3eb7f91f2443090c9b859a716d83711310364897b6144dcf10d3254342fec3d1"
|
||||
},
|
||||
{
|
||||
"path": "backup/WORK-RECORDS.md",
|
||||
"sha256": "a1fec217a6e380f528778d781dbea1fc5a836fdefa1dd4dd0308100fbe08112a"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-07-02.md",
|
||||
"sha256": "f775fb97ce7c4d49f03f68dec86df8b2f1d0c265a6432d2b995d14852394db3d"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-08-14.md",
|
||||
"sha256": "24e73b2559fecb4da7818fd2dd75af413dfc30d761887029d8dda364cd0205ab"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-08-23.md",
|
||||
"sha256": "f3aabf4e36a27a4692b97d6cb7286f944681917cd3c31420dbb90d9b8a55f646"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-10-03.md",
|
||||
"sha256": "0591aa5c901f6adb73aa9ecbfa89f8e1cf81919ab2f0f2d5bdb6ff4934eef81c"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-10-10.md",
|
||||
"sha256": "01c42780f0299ec96febd083393d45a23da7d17674f8049374db1e4ef7dfb322"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-10-11.md",
|
||||
"sha256": "cdfce94de7390b7abd2a8d2cc10d3e770b718521dd34c69fda81fa0a1f1e83fd"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/ADHOC-2026-10-16.md",
|
||||
"sha256": "4288f317ee4e66b332b5e3907d90878becb829d4492ccd5d86842a914667a9b8"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md",
|
||||
"sha256": "fe8a6c450f734810c78b97eeca70dddcb7555cd16d9e3403b9b4bc8eca371ff8"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md",
|
||||
"sha256": "1d0f5f9674b20c3fbaa76e98010f63a2d83d081b0a1c1410ac6a38d49b3fc631"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0011-enterprise-federation-saml.md",
|
||||
"sha256": "e5692b737a1b2f0ca8c1ed9129d06929be74726e69baccea5520738b7e31a657"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0021-activity-core-ops-sso-operators.md",
|
||||
"sha256": "24fe6fd6a2bd0bf549c24523f4562e5fc8e1a3d317c6858f701f914b33b6059a"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md",
|
||||
"sha256": "a44265859ecab3c65d95f9ea520950e48c223b64f3a5449ab2d4663a8efce336"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0023-user-engine-portal-platform-integration.md",
|
||||
"sha256": "9d1f43313f2b1d0d55ad3d69966397cd310e6c90d76f90c216fb75b5f7b1e053"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
|
||||
"sha256": "7899232d817fde7a0ac1be4d9511c1ba65f44428a08872befe30af0b06a3aac0"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
|
||||
"sha256": "d3570990e1c4d4ee528c72f2db9116b57f9e6bfc76bac4fa881c362b10b90c63"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md",
|
||||
"sha256": "52f0727defd43791c812e045e75eb5ebfb181183290a25f7841bccd1e250e0f4"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0027-reef-placement-reconciliation.md",
|
||||
"sha256": "3360b2bfeedd04ea5c3854dff0c01960f9c1d2e8433a71f393fc9c8c9253aca2"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md",
|
||||
"sha256": "48548faf581e1bacb6952db0050f67b30e634830eb55d20289be85580afb6017"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0029-scope-and-intent-reconciliation.md",
|
||||
"sha256": "f2111f2a8c6661022755824091aa20413e10975ef940d5492ff6e9564d45c372"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0030-deterministic-security-scenario-composition.md",
|
||||
"sha256": "f82e1f33ae1ddf2305ce2efc567d2c55809669a47b91c0cabd3d0b41bd78ab3b"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0031-deterministic-posture-feedback.md",
|
||||
"sha256": "f2b465a883ab8e28ab84f0e94d95e23a15ad6d9b6e038068d96b36dcaa313b5e"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0032-openbao-operator-loopback-callback.md",
|
||||
"sha256": "a1955896b0ac0805c19bd97264e0e755f2d46a99687acdd2fcd6dd36947aa1bb"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0033-keycape-secret-exposure-rotation.md",
|
||||
"sha256": "8268aa86f7fa9f2a3848adfe2dc3022bcf2e9dc9197328993445f4138a40e513"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0034-verification-that-verifies.md",
|
||||
"sha256": "d31227ca44d697d9a3e7bef5931836ab0cc6d2c9641900ab1fa8f63307cb7c83"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0035-emission-cadence-security-profile.md",
|
||||
"sha256": "273a9154a37a7b9efd4fb199e7375d2a3d5cb6919eaa3a2fb8532745a90f83e9"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0036-restore-user-portal-client-registration.md",
|
||||
"sha256": "84019513b7099c39b6dc2daa286017fb48f1286b966fe7c7a142df7c914b575e"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0037-vergabe-company-welcome.md",
|
||||
"sha256": "f4fb6d7972f59afa550d6021a9297303e9329867027229512ec9f85b61dda286"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md",
|
||||
"sha256": "32811a635811c870795aba5d4d2fa6f3b0baecc0b3d270a7cfeb77dbba75206b"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md",
|
||||
"sha256": "31a0811bf36984f1ef3bdf3c7b7e68d46413909bfb4bc0d781bd7bf49d9bbabf"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0040-execution-attribution-receipt.md",
|
||||
"sha256": "8b0b653da2fc6ea62a2b804bdb8feb92046876f4e5e0afa96c7c53bc31945d2e"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0041-onboarding-journey-usability.md",
|
||||
"sha256": "b05d9e14028e83cebbe16b1efcee3477f49a99ae5d6dfc408c41c6cd0b3c3a83"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0042-workload-mfa-step-up.md",
|
||||
"sha256": "12c95fd8a2cbed6c521c1fbf5464d7cf6b7552d63c11992bc93ec88f2013c618"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md",
|
||||
"sha256": "bf75f9fdaf7745596b79843c33d6325236a0c7de27547169464c367f71100de1"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md",
|
||||
"sha256": "071fe448f456b7622e4d6499dab6005af1e41e9c7cc516bc8389b2847e1c01d5"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0045-root-grant-custody-record.md",
|
||||
"sha256": "72957387424f36b376a3b66ea10381d40665497dbcdc7b029ba54fed87c522d1"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md",
|
||||
"sha256": "ef38481d26e599802d94f8b06a4d89c7c99a83c81ca5be9bcf7422b885867479"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0047-platform-password-reset-mail.md",
|
||||
"sha256": "78b159076121c4e53fef8eadd6287b8c9e91a725203be88816840c9f9c1c8516"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0048-security-case-types.md",
|
||||
"sha256": "b682cd607d9ed9b232656cc7934ec09ac4e986c1c81131a44f80240342a0571c"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md",
|
||||
"sha256": "cba3233883e9a5b429aaea1b8eacd250c6efcf5e484c4284ddc97e8e52697085"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0050-playwright-consumer-registration.md",
|
||||
"sha256": "0be0b643409d0731d4924c95566facdef58b1c65c5c1276f7f308ebfb2271fb7"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/NK-WP-0051-sign-in-diagnosis.md",
|
||||
"sha256": "736a06e798d9d15715490f8c3ed2f81a8d065e8446e046e36a2bd19353a083b9"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260522-NK-WP-0015-user-engine-isolated-mvp.md",
|
||||
"sha256": "98e7ae719ae0282e80d541222d12805bebf2ef1abf9dca576f1e32c83d5d31bf"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260522-NK-WP-0016-user-engine-multi-tenancy.md",
|
||||
"sha256": "be9490920a6c8bc1652c334c6b2b1480257b18fffe55406426b04ab240ba7430"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260522-NK-WP-0017-user-engine-multi-application-catalogs.md",
|
||||
"sha256": "ab5c151034d6ceaf8bf6d4154944fa7406e2edb6127e6adc963429920c425270"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260522-NK-WP-0018-user-engine-integrated-test-scenarios.md",
|
||||
"sha256": "2b83295aeaae5a58ad6cef6b0198e9abf8f57a8a9b0216bb166afb71d4d26e28"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260522-NK-WP-0019-user-engine-finalization-polish.md",
|
||||
"sha256": "7db746783b2ad60110451b9b44a04471b188bb8a6b49d76c984a1cd4c72b1ae3"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md",
|
||||
"sha256": "014252768653440c1911512972022122b4f43c3c040c7da34d0f86bba71e1204"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md",
|
||||
"sha256": "936b446483326b3dc7c1be94397f833a1e124311c98654d1877478a09b8b23e8"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260603-NK-WP-0003-keycape-privacyidea-cluster-deployment.md",
|
||||
"sha256": "754955fe18c25c35a1c0036b25f889d281dd8442b86e1793583c97f83f8125c2"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260603-NK-WP-0004-credential-management-foundation.md",
|
||||
"sha256": "62a69453f9dc5db8a208e01df83e198249074d921fa83cba9d919385208709fc"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260603-NK-WP-0005-agent-driven-credential-bootstrap.md",
|
||||
"sha256": "f7cebada5f6344d0baa7625f66938cafd1f156bd20fe9b00f09c994f290fb4e0"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md",
|
||||
"sha256": "784b034ba22125ea2098da40c3ece39f9e910c473d4a3f0f8f3c23250aae7e23"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-ADHOC-2026-06-14.md",
|
||||
"sha256": "455b4cb77ec944551696203c79d3a229cbf9e5f2381931632ea1ce23c587e2fd"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md",
|
||||
"sha256": "74eafd43790bde4d31728518e1b875ecc8262e6af18e1cd5f81e0882178182be"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md",
|
||||
"sha256": "61cd3135222696a7b6afd74b2f3a3fd717f5e7985f8ade2ccc66e119a43a9d14"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0001-sso-mfa-platform.md",
|
||||
"sha256": "c904ad4503b45ea64bc9d360c1cc8394398771cabf1a389dea41496714cde6e6"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0002-local-identity.md",
|
||||
"sha256": "fc2b924da22125a8d0374e795b962612dfd8e01f583b6a0eefbb827e338127d7"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0006-recursive-platform-identity-security-architecture.md",
|
||||
"sha256": "7a70d174baa28f399c63823c264cb1dfba9af69b61934ffc105cba56e4f34b47"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0007-object-storage-sts-credential-vending.md",
|
||||
"sha256": "d921c3d9c015027b99b30e7723acf6c71151a29927fcdbfaa124f776dcfbd64f"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0008-it-security-architecture-patterns-infospace.md",
|
||||
"sha256": "abacfb3651e56b233587492dda5fa6cbbd08e0c1fc49681a22373e156a630b36"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0010-genesis-security-pattern-completion.md",
|
||||
"sha256": "6a0b13e0f82af03e7461f4f433d7876e5f8d9a76acc8888709f4e130e13fb2e4"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0012-iam-profile-specification.md",
|
||||
"sha256": "1ca0aed6549d7cb2aeb490a4110fddfd0f5575832cb7e2f696724b3b4b4e79ba"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0013-playbook-capability-contract.md",
|
||||
"sha256": "5e69691251391706d3419349bc38cb13c6d910d623b03ba788d31dbcd5a7e3bf"
|
||||
},
|
||||
{
|
||||
"path": "backup/workplans/archived/260702-NK-WP-0014-user-engine-preparation-boundary-contracts.md",
|
||||
"sha256": "d4a6e9f60ccfe666645ebf37af0d49b50e692218de46539c50b8b22ec255030a"
|
||||
},
|
||||
{
|
||||
"path": "candidate.md",
|
||||
"sha256": "1502f1f474856172d969489cab8c0642d9456d93d326f6660df3772a4716d30b"
|
||||
},
|
||||
{
|
||||
"path": "commit-receipt.json",
|
||||
"sha256": "1ec30c07304b95ea7474c8e05d0d4770932e726d56d7a7a93356ac559058c797"
|
||||
},
|
||||
{
|
||||
"path": "consistency.json",
|
||||
"sha256": "bf4c3946f5080d8dc878599ab73053eeb8d1ce7f25717067a8cb837be61c5aa6"
|
||||
},
|
||||
{
|
||||
"path": "consistency.stderr.txt",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "final_verify.py",
|
||||
"sha256": "00273ab7067452aafa200e6b5dcbb431a4041a8a6bfc1d4e3a6a69902e97bc10"
|
||||
},
|
||||
{
|
||||
"path": "identifier-preview.json",
|
||||
"sha256": "c7df3d4f74214943d11408c147f1d4331549ac7d9764667cf75bc58b4186b3b9"
|
||||
},
|
||||
{
|
||||
"path": "preimage.json",
|
||||
"sha256": "d223380325bc523c2226a0d4cf91be3c35b94a6db7ad42753c8ad359ecbdeb61"
|
||||
},
|
||||
{
|
||||
"path": "preservation-verification.json",
|
||||
"sha256": "0accde4df20981a3b8732aa5143beb7a9defb333d11c3481543950e676a4674d"
|
||||
},
|
||||
{
|
||||
"path": "projection.json",
|
||||
"sha256": "00a8832cd630b7fb42214883710cfcda6f653b1b22c417f952393d2a43876d39"
|
||||
},
|
||||
{
|
||||
"path": "projection.stderr.txt",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "restore_verify.py",
|
||||
"sha256": "230bed15ab4388aaf7dd3452248fbdab30a14f0ecbbf79965fb609e050816e40"
|
||||
},
|
||||
{
|
||||
"path": "tasks-after.json",
|
||||
"sha256": "26bda42190983a07cb853c69b14635c69f4a4ec21f3bae7b4d1e72fbd3961e79"
|
||||
},
|
||||
{
|
||||
"path": "tasks-before.json",
|
||||
"sha256": "0c79d45200ff95d67558de103ea0a75b8450fcff4cea1e871a70e66c2edfe288"
|
||||
},
|
||||
{
|
||||
"path": "verification.json",
|
||||
"sha256": "6695016b43830abc79fb4da56697e51a59624ef7d0d345fa73afa2cd77fbd8d9"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
{
|
||||
"head": "001a38ca2aa85155ce4840c4da22aca6e2346171",
|
||||
"remote": "001a38ca2aa85155ce4840c4da22aca6e2346171",
|
||||
"status": "M .custodian-brief.md\n M WORK-RECORDS.md\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json\n?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md",
|
||||
"files": [
|
||||
{
|
||||
"path": ".custodian-brief.md",
|
||||
"sha256": "3eb7f91f2443090c9b859a716d83711310364897b6144dcf10d3254342fec3d1",
|
||||
"index": "100644 f60f8bad019e23e6f7e0e074745da0bb2fcbb870 0\t.custodian-brief.md"
|
||||
},
|
||||
{
|
||||
"path": "WORK-RECORDS.md",
|
||||
"sha256": "a1fec217a6e380f528778d781dbea1fc5a836fdefa1dd4dd0308100fbe08112a",
|
||||
"index": "100644 76a6b7fc304c508f1b50010cb72d892826c6a831 0\tWORK-RECORDS.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-07-02.md",
|
||||
"sha256": "f775fb97ce7c4d49f03f68dec86df8b2f1d0c265a6432d2b995d14852394db3d",
|
||||
"index": "100644 66e748e1224a1c212a071a4e3c72d0d3404ed9a9 0\tworkplans/ADHOC-2026-07-02.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-08-14.md",
|
||||
"sha256": "24e73b2559fecb4da7818fd2dd75af413dfc30d761887029d8dda364cd0205ab",
|
||||
"index": "100644 91a3ea1a4256cf7d09d857500be99810c71009a5 0\tworkplans/ADHOC-2026-08-14.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-08-23.md",
|
||||
"sha256": "f3aabf4e36a27a4692b97d6cb7286f944681917cd3c31420dbb90d9b8a55f646",
|
||||
"index": "100644 21f20d066843b000c457851bd44e6ec217999228 0\tworkplans/ADHOC-2026-08-23.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-10-03.md",
|
||||
"sha256": "0591aa5c901f6adb73aa9ecbfa89f8e1cf81919ab2f0f2d5bdb6ff4934eef81c",
|
||||
"index": "100644 360faf210cc73613bf27b4f7f6655aa9cf8c511a 0\tworkplans/ADHOC-2026-10-03.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-10-10.md",
|
||||
"sha256": "01c42780f0299ec96febd083393d45a23da7d17674f8049374db1e4ef7dfb322",
|
||||
"index": "100644 7d350c2746dce164e8fe0c43677c346a68db5cf6 0\tworkplans/ADHOC-2026-10-10.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-10-11.md",
|
||||
"sha256": "cdfce94de7390b7abd2a8d2cc10d3e770b718521dd34c69fda81fa0a1f1e83fd",
|
||||
"index": "100644 94059eb7d549fda7a2a6550ecea896078e3a5f93 0\tworkplans/ADHOC-2026-10-11.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/ADHOC-2026-10-16.md",
|
||||
"sha256": "4288f317ee4e66b332b5e3907d90878becb829d4492ccd5d86842a914667a9b8",
|
||||
"index": "100644 a38773aed4d12e9b214988d8fac1d7061eadc6fb 0\tworkplans/ADHOC-2026-10-16.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md",
|
||||
"sha256": "fe8a6c450f734810c78b97eeca70dddcb7555cd16d9e3403b9b4bc8eca371ff8",
|
||||
"index": "100644 ffe7a7eeee1069f4bdf9511e2dd44e6eff5726f8 0\tworkplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md",
|
||||
"sha256": "1d0f5f9674b20c3fbaa76e98010f63a2d83d081b0a1c1410ac6a38d49b3fc631",
|
||||
"index": "100644 6e55c842670a5f1153c2556495abbf9c78db866e 0\tworkplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0011-enterprise-federation-saml.md",
|
||||
"sha256": "e5692b737a1b2f0ca8c1ed9129d06929be74726e69baccea5520738b7e31a657",
|
||||
"index": "100644 f6dc921d804a45dbb4c225527f7ce5ddb5f53dc6 0\tworkplans/NK-WP-0011-enterprise-federation-saml.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0021-activity-core-ops-sso-operators.md",
|
||||
"sha256": "24fe6fd6a2bd0bf549c24523f4562e5fc8e1a3d317c6858f701f914b33b6059a",
|
||||
"index": "100644 31de057e004086584e85d14595269460896cd5ea 0\tworkplans/NK-WP-0021-activity-core-ops-sso-operators.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md",
|
||||
"sha256": "a44265859ecab3c65d95f9ea520950e48c223b64f3a5449ab2d4663a8efce336",
|
||||
"index": "100644 d0616047e35b52257647ce72914584e32222a5c7 0\tworkplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md",
|
||||
"sha256": "9d1f43313f2b1d0d55ad3d69966397cd310e6c90d76f90c216fb75b5f7b1e053",
|
||||
"index": "100644 c08863a1b0cc9cd9ebf15ac09c07b29e7fbe46e1 0\tworkplans/NK-WP-0023-user-engine-portal-platform-integration.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
|
||||
"sha256": "7899232d817fde7a0ac1be4d9511c1ba65f44428a08872befe30af0b06a3aac0",
|
||||
"index": "100644 0b336e80dc4ad264f6aae2340f380fc041f54df0 0\tworkplans/NK-WP-0024-user-engine-portal-integration-expansion.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
|
||||
"sha256": "d3570990e1c4d4ee528c72f2db9116b57f9e6bfc76bac4fa881c362b10b90c63",
|
||||
"index": "100644 9cd2e946519328b96910be0dbf87c35f7128a0ed 0\tworkplans/NK-WP-0025-public-self-registration-and-application-jit.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md",
|
||||
"sha256": "52f0727defd43791c812e045e75eb5ebfb181183290a25f7841bccd1e250e0f4",
|
||||
"index": "100644 5aadafc881b7f79b0f432bae3eed46800222249e 0\tworkplans/NK-WP-0026-flex-auth-caller-identity-rollout.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0027-reef-placement-reconciliation.md",
|
||||
"sha256": "3360b2bfeedd04ea5c3854dff0c01960f9c1d2e8433a71f393fc9c8c9253aca2",
|
||||
"index": "100644 ea0b21e2685b8304c184103f67a013e8f7456ba9 0\tworkplans/NK-WP-0027-reef-placement-reconciliation.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md",
|
||||
"sha256": "48548faf581e1bacb6952db0050f67b30e634830eb55d20289be85580afb6017",
|
||||
"index": "100644 e92df0c1de0c77be975f2e375221043598f0091d 0\tworkplans/NK-WP-0028-canon-publication-and-grouping-semantics.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0029-scope-and-intent-reconciliation.md",
|
||||
"sha256": "f2111f2a8c6661022755824091aa20413e10975ef940d5492ff6e9564d45c372",
|
||||
"index": "100644 fc1b60f6cafeb38dbaa403bce5e050dd631ad54e 0\tworkplans/NK-WP-0029-scope-and-intent-reconciliation.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0030-deterministic-security-scenario-composition.md",
|
||||
"sha256": "f82e1f33ae1ddf2305ce2efc567d2c55809669a47b91c0cabd3d0b41bd78ab3b",
|
||||
"index": "100644 f60bdbbb04d1c79ed17f40f41cb51c800d162470 0\tworkplans/NK-WP-0030-deterministic-security-scenario-composition.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0031-deterministic-posture-feedback.md",
|
||||
"sha256": "f2b465a883ab8e28ab84f0e94d95e23a15ad6d9b6e038068d96b36dcaa313b5e",
|
||||
"index": "100644 4cda818a55ff1e02f4a37a7ea24e8caa8d6190df 0\tworkplans/NK-WP-0031-deterministic-posture-feedback.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0032-openbao-operator-loopback-callback.md",
|
||||
"sha256": "a1955896b0ac0805c19bd97264e0e755f2d46a99687acdd2fcd6dd36947aa1bb",
|
||||
"index": "100644 ce46547934ea494938f818a41379327ed9f627a6 0\tworkplans/NK-WP-0032-openbao-operator-loopback-callback.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0033-keycape-secret-exposure-rotation.md",
|
||||
"sha256": "8268aa86f7fa9f2a3848adfe2dc3022bcf2e9dc9197328993445f4138a40e513",
|
||||
"index": "100644 1946d3221b8724907911d8b2a0f603583eb12af7 0\tworkplans/NK-WP-0033-keycape-secret-exposure-rotation.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0034-verification-that-verifies.md",
|
||||
"sha256": "d31227ca44d697d9a3e7bef5931836ab0cc6d2c9641900ab1fa8f63307cb7c83",
|
||||
"index": "100644 7302cd6a053bd1e9fa59d1187ee61f7440c3ef88 0\tworkplans/NK-WP-0034-verification-that-verifies.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0035-emission-cadence-security-profile.md",
|
||||
"sha256": "273a9154a37a7b9efd4fb199e7375d2a3d5cb6919eaa3a2fb8532745a90f83e9",
|
||||
"index": "100644 4c22c5376951efbf7f3877fc73cfb96a5cbf2769 0\tworkplans/NK-WP-0035-emission-cadence-security-profile.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0036-restore-user-portal-client-registration.md",
|
||||
"sha256": "84019513b7099c39b6dc2daa286017fb48f1286b966fe7c7a142df7c914b575e",
|
||||
"index": "100644 87b218549c1721ad73962922866b4f119786a475 0\tworkplans/NK-WP-0036-restore-user-portal-client-registration.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0037-vergabe-company-welcome.md",
|
||||
"sha256": "f4fb6d7972f59afa550d6021a9297303e9329867027229512ec9f85b61dda286",
|
||||
"index": "100644 27f964c0da1b2ff04014f954ee02ac7f5d132d4f 0\tworkplans/NK-WP-0037-vergabe-company-welcome.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md",
|
||||
"sha256": "32811a635811c870795aba5d4d2fa6f3b0baecc0b3d270a7cfeb77dbba75206b",
|
||||
"index": "100644 6b274dc6d6743a0d4c0d5c7b2fea9d8c844b7cad 0\tworkplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md",
|
||||
"sha256": "31a0811bf36984f1ef3bdf3c7b7e68d46413909bfb4bc0d781bd7bf49d9bbabf",
|
||||
"index": "100644 af85aaa2b5acc459ceb633f02465a953d0d84212 0\tworkplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0040-execution-attribution-receipt.md",
|
||||
"sha256": "8b0b653da2fc6ea62a2b804bdb8feb92046876f4e5e0afa96c7c53bc31945d2e",
|
||||
"index": "100644 541f7f2ebcc09e4cf92ca564d377fa26133c221f 0\tworkplans/NK-WP-0040-execution-attribution-receipt.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0041-onboarding-journey-usability.md",
|
||||
"sha256": "b05d9e14028e83cebbe16b1efcee3477f49a99ae5d6dfc408c41c6cd0b3c3a83",
|
||||
"index": "100644 549226d24984e4c8a6af8e9791723df4167cfc92 0\tworkplans/NK-WP-0041-onboarding-journey-usability.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0042-workload-mfa-step-up.md",
|
||||
"sha256": "12c95fd8a2cbed6c521c1fbf5464d7cf6b7552d63c11992bc93ec88f2013c618",
|
||||
"index": "100644 71856e4b4b5d7fc7bd943df27d1eff9ff0647bc2 0\tworkplans/NK-WP-0042-workload-mfa-step-up.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md",
|
||||
"sha256": "bf75f9fdaf7745596b79843c33d6325236a0c7de27547169464c367f71100de1",
|
||||
"index": "100644 814c11d1a3da13c2979e4b5d960ba5f72cba2e7b 0\tworkplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md",
|
||||
"sha256": "071fe448f456b7622e4d6499dab6005af1e41e9c7cc516bc8389b2847e1c01d5",
|
||||
"index": "100644 6c9117281daf8de58783018fe5c23d8c58f09d96 0\tworkplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0045-root-grant-custody-record.md",
|
||||
"sha256": "72957387424f36b376a3b66ea10381d40665497dbcdc7b029ba54fed87c522d1",
|
||||
"index": "100644 455c2fb2c64f350836b3b426acdb18bdf3ba3b37 0\tworkplans/NK-WP-0045-root-grant-custody-record.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md",
|
||||
"sha256": "ef38481d26e599802d94f8b06a4d89c7c99a83c81ca5be9bcf7422b885867479",
|
||||
"index": "100644 56cdc6b3580a253b38d17dfe4dad007ca00f03ed 0\tworkplans/NK-WP-0046-secrets-engine-openbao-client-registration.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0047-platform-password-reset-mail.md",
|
||||
"sha256": "78b159076121c4e53fef8eadd6287b8c9e91a725203be88816840c9f9c1c8516",
|
||||
"index": "100644 bdb3c73b42cfe5c9b47c0d9ee77ee44b3a4e187e 0\tworkplans/NK-WP-0047-platform-password-reset-mail.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0048-security-case-types.md",
|
||||
"sha256": "b682cd607d9ed9b232656cc7934ec09ac4e986c1c81131a44f80240342a0571c",
|
||||
"index": "100644 71cba31e0bef994557e972df18a98d1f2002fa1b 0\tworkplans/NK-WP-0048-security-case-types.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md",
|
||||
"sha256": "cba3233883e9a5b429aaea1b8eacd250c6efcf5e484c4284ddc97e8e52697085",
|
||||
"index": "100644 2078d625443a6fcafb22b96f18e9b7bb5c45cc84 0\tworkplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0050-playwright-consumer-registration.md",
|
||||
"sha256": "0be0b643409d0731d4924c95566facdef58b1c65c5c1276f7f308ebfb2271fb7",
|
||||
"index": "100644 00d4833d270ed06f5e54ea992c175565a824d069 0\tworkplans/NK-WP-0050-playwright-consumer-registration.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/NK-WP-0051-sign-in-diagnosis.md",
|
||||
"sha256": "736a06e798d9d15715490f8c3ed2f81a8d065e8446e046e36a2bd19353a083b9",
|
||||
"index": "100644 5844807b7de5f365fc5353b565e070823cc230b2 0\tworkplans/NK-WP-0051-sign-in-diagnosis.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260522-NK-WP-0015-user-engine-isolated-mvp.md",
|
||||
"sha256": "98e7ae719ae0282e80d541222d12805bebf2ef1abf9dca576f1e32c83d5d31bf",
|
||||
"index": "100644 0fa1afc14b570ca23fd6f562ac6ae6d083231580 0\tworkplans/archived/260522-NK-WP-0015-user-engine-isolated-mvp.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260522-NK-WP-0016-user-engine-multi-tenancy.md",
|
||||
"sha256": "be9490920a6c8bc1652c334c6b2b1480257b18fffe55406426b04ab240ba7430",
|
||||
"index": "100644 419f916829f6dc6b453f0988c5b736f79ec7a365 0\tworkplans/archived/260522-NK-WP-0016-user-engine-multi-tenancy.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260522-NK-WP-0017-user-engine-multi-application-catalogs.md",
|
||||
"sha256": "ab5c151034d6ceaf8bf6d4154944fa7406e2edb6127e6adc963429920c425270",
|
||||
"index": "100644 a047efccf34c48a5900e3168f97345ef71776e4f 0\tworkplans/archived/260522-NK-WP-0017-user-engine-multi-application-catalogs.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260522-NK-WP-0018-user-engine-integrated-test-scenarios.md",
|
||||
"sha256": "2b83295aeaae5a58ad6cef6b0198e9abf8f57a8a9b0216bb166afb71d4d26e28",
|
||||
"index": "100644 2d3b99d566519a03e5bf98972ab9b355f08fd560 0\tworkplans/archived/260522-NK-WP-0018-user-engine-integrated-test-scenarios.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260522-NK-WP-0019-user-engine-finalization-polish.md",
|
||||
"sha256": "7db746783b2ad60110451b9b44a04471b188bb8a6b49d76c984a1cd4c72b1ae3",
|
||||
"index": "100644 d7135b4993398d9220e08f5c1f430220a46c114c 0\tworkplans/archived/260522-NK-WP-0019-user-engine-finalization-polish.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md",
|
||||
"sha256": "014252768653440c1911512972022122b4f43c3c040c7da34d0f86bba71e1204",
|
||||
"index": "100644 ef2a569259fd1a08577c09b6b143559ecba375f0 0\tworkplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md",
|
||||
"sha256": "936b446483326b3dc7c1be94397f833a1e124311c98654d1877478a09b8b23e8",
|
||||
"index": "100644 b23e86009bcf6772faf6f939363897f913d45744 0\tworkplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260603-NK-WP-0003-keycape-privacyidea-cluster-deployment.md",
|
||||
"sha256": "754955fe18c25c35a1c0036b25f889d281dd8442b86e1793583c97f83f8125c2",
|
||||
"index": "100644 bd9bf9662654c80fafb192a927aeb35fefe56b2b 0\tworkplans/archived/260603-NK-WP-0003-keycape-privacyidea-cluster-deployment.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260603-NK-WP-0004-credential-management-foundation.md",
|
||||
"sha256": "62a69453f9dc5db8a208e01df83e198249074d921fa83cba9d919385208709fc",
|
||||
"index": "100644 eea4583d2e6059bf9beee17083309eeb06740e78 0\tworkplans/archived/260603-NK-WP-0004-credential-management-foundation.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260603-NK-WP-0005-agent-driven-credential-bootstrap.md",
|
||||
"sha256": "f7cebada5f6344d0baa7625f66938cafd1f156bd20fe9b00f09c994f290fb4e0",
|
||||
"index": "100644 6acf610042aa48292be0a589530ab64d3e6750f2 0\tworkplans/archived/260603-NK-WP-0005-agent-driven-credential-bootstrap.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md",
|
||||
"sha256": "784b034ba22125ea2098da40c3ece39f9e910c473d4a3f0f8f3c23250aae7e23",
|
||||
"index": "100644 a781cb44597b2f87eeac5b88117fc506b20e2fbd 0\tworkplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-ADHOC-2026-06-14.md",
|
||||
"sha256": "455b4cb77ec944551696203c79d3a229cbf9e5f2381931632ea1ce23c587e2fd",
|
||||
"index": "100644 d2efa4d31679469804cf48b6eb56f14c9ccd442a 0\tworkplans/archived/260702-ADHOC-2026-06-14.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md",
|
||||
"sha256": "74eafd43790bde4d31728518e1b875ecc8262e6af18e1cd5f81e0882178182be",
|
||||
"index": "100644 7756f7dd4b7d44a30120b6339fce3e94852e08eb 0\tworkplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md",
|
||||
"sha256": "61cd3135222696a7b6afd74b2f3a3fd717f5e7985f8ade2ccc66e119a43a9d14",
|
||||
"index": "100644 b4635dc00ab6a6d6fac0572495fbf6c4172384e7 0\tworkplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0001-sso-mfa-platform.md",
|
||||
"sha256": "c904ad4503b45ea64bc9d360c1cc8394398771cabf1a389dea41496714cde6e6",
|
||||
"index": "100644 78d58cb5be90924c102ca9a57afe0e9af6a15380 0\tworkplans/archived/260702-NK-WP-0001-sso-mfa-platform.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0002-local-identity.md",
|
||||
"sha256": "fc2b924da22125a8d0374e795b962612dfd8e01f583b6a0eefbb827e338127d7",
|
||||
"index": "100644 a93af8cf2641144212f519bb7fbcb72ad14d2874 0\tworkplans/archived/260702-NK-WP-0002-local-identity.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0006-recursive-platform-identity-security-architecture.md",
|
||||
"sha256": "7a70d174baa28f399c63823c264cb1dfba9af69b61934ffc105cba56e4f34b47",
|
||||
"index": "100644 886d423af68c175cedc3a5daf28814ca673fafdf 0\tworkplans/archived/260702-NK-WP-0006-recursive-platform-identity-security-architecture.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0007-object-storage-sts-credential-vending.md",
|
||||
"sha256": "d921c3d9c015027b99b30e7723acf6c71151a29927fcdbfaa124f776dcfbd64f",
|
||||
"index": "100644 14689c43ac98730ad1ae2bd6aeb4648b87bee3eb 0\tworkplans/archived/260702-NK-WP-0007-object-storage-sts-credential-vending.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0008-it-security-architecture-patterns-infospace.md",
|
||||
"sha256": "abacfb3651e56b233587492dda5fa6cbbd08e0c1fc49681a22373e156a630b36",
|
||||
"index": "100644 88120ba4a87ed785ec63a9edd58b88ec3db6a2bc 0\tworkplans/archived/260702-NK-WP-0008-it-security-architecture-patterns-infospace.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0010-genesis-security-pattern-completion.md",
|
||||
"sha256": "6a0b13e0f82af03e7461f4f433d7876e5f8d9a76acc8888709f4e130e13fb2e4",
|
||||
"index": "100644 06daac6b1e143bc8505439566ae33eaed8d2fa9c 0\tworkplans/archived/260702-NK-WP-0010-genesis-security-pattern-completion.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0012-iam-profile-specification.md",
|
||||
"sha256": "1ca0aed6549d7cb2aeb490a4110fddfd0f5575832cb7e2f696724b3b4b4e79ba",
|
||||
"index": "100644 2f80c1f72c65723633fceb45f88195d6a3a131fe 0\tworkplans/archived/260702-NK-WP-0012-iam-profile-specification.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0013-playbook-capability-contract.md",
|
||||
"sha256": "5e69691251391706d3419349bc38cb13c6d910d623b03ba788d31dbcd5a7e3bf",
|
||||
"index": "100644 aa5edb5fcf8aa7d75220778c595cce455769c95e 0\tworkplans/archived/260702-NK-WP-0013-playbook-capability-contract.md"
|
||||
},
|
||||
{
|
||||
"path": "workplans/archived/260702-NK-WP-0014-user-engine-preparation-boundary-contracts.md",
|
||||
"sha256": "d4a6e9f60ccfe666645ebf37af0d49b50e692218de46539c50b8b22ec255030a",
|
||||
"index": "100644 ab0652790e1456957a70cee59b84fb7810c3e9c5 0\tworkplans/archived/260702-NK-WP-0014-user-engine-preparation-boundary-contracts.md"
|
||||
}
|
||||
],
|
||||
"untracked": [
|
||||
{
|
||||
"path": "docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/capture-provenance.json",
|
||||
"sha256": "b562f51c58c2114c2122286091dceebf3a107f0822b411cd2d8554d45d0555d5"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/events.jsonl",
|
||||
"sha256": "3664dc13a3ce3509ca87afc49c1b4c9095cc56cdfb6c37faa4c227556c7d3858"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/receipts.jsonl",
|
||||
"sha256": "060433115fb784eaf2e7bd7eab7a368d5b4439d925524b68aa4edf920b54b7aa"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/snapshot.json",
|
||||
"sha256": "210c0c5df320ce2c507eee764596b5928f59a99f938432de87811932d1896676"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl",
|
||||
"sha256": "6588a0795b8374e6ee36e822de9a3f17e1a6c0a7852804c5aeea9f8edc18905e"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json",
|
||||
"sha256": "a1ed7d07bae1813315fa7b2a166f3125a6c0195b2ad468568718429381d77153"
|
||||
},
|
||||
{
|
||||
"path": "intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md",
|
||||
"sha256": "988a2df3639f9f17d7c8b01621bf3185850f248778d6ccf34cb23001f7f7690d"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"head": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"restored_generated_paths": [],
|
||||
"unrelated_tracked_files_preserved": 65,
|
||||
"untracked_files_preserved": 7,
|
||||
"status": "M .custodian-brief.md\n M WORK-RECORDS.md\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json\n?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md"
|
||||
}
|
||||
|
|
@ -0,0 +1,996 @@
|
|||
{
|
||||
"ok": true,
|
||||
"status": "applied",
|
||||
"repo_slug": "net-kingdom",
|
||||
"commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"requests": 2,
|
||||
"elapsed_ms": 11496.4,
|
||||
"git": {
|
||||
"upstream": "origin/main",
|
||||
"behind": 0,
|
||||
"ahead": 0
|
||||
},
|
||||
"identifiers": {
|
||||
"schema": "repo-manager.identifier-assignment.v1",
|
||||
"ok": true,
|
||||
"executed": true,
|
||||
"repo": "net-kingdom",
|
||||
"namespace": "helixforge",
|
||||
"assignments": [],
|
||||
"historical_invalid_identifiers": [],
|
||||
"historical_identity_collisions": [],
|
||||
"files_changed": []
|
||||
},
|
||||
"receipt": {
|
||||
"schema": "state-hub.repository-projection-reconcile.v1",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"derived_commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"outcome": {
|
||||
"schema": "state-hub.projection-reset.v1",
|
||||
"repo_slug": "net-kingdom",
|
||||
"commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"status": "applied",
|
||||
"counts": {
|
||||
"created": 0,
|
||||
"updated": 64,
|
||||
"retired": 0,
|
||||
"refused": 0,
|
||||
"released": 0,
|
||||
"created_tasks": 0,
|
||||
"updated_tasks": 8,
|
||||
"cancelled_tasks": 0,
|
||||
"created_edges": 0,
|
||||
"removed_edges": 0,
|
||||
"unresolved_edges": 1,
|
||||
"external_edges": 85
|
||||
},
|
||||
"created": [],
|
||||
"updated": [
|
||||
"NET-WP-0015",
|
||||
"NET-WP-0016",
|
||||
"NET-WP-0017",
|
||||
"NET-WP-0018",
|
||||
"NET-WP-0019",
|
||||
"NET-WP-0020",
|
||||
"NK-WP-0001",
|
||||
"NK-WP-0002",
|
||||
"NK-WP-0003",
|
||||
"NK-WP-0004",
|
||||
"NK-WP-0005",
|
||||
"NK-WP-0006",
|
||||
"NK-WP-0007",
|
||||
"NK-WP-0008",
|
||||
"NK-WP-0009",
|
||||
"NK-WP-0010",
|
||||
"NK-WP-0011",
|
||||
"NK-WP-0012",
|
||||
"NK-WP-0013",
|
||||
"NK-WP-0014",
|
||||
"NK-WP-0015",
|
||||
"NK-WP-0016",
|
||||
"NK-WP-0017",
|
||||
"NK-WP-0018",
|
||||
"NK-WP-0019",
|
||||
"NK-WP-0021",
|
||||
"NK-WP-0022",
|
||||
"NK-WP-0023",
|
||||
"NK-WP-0024",
|
||||
"NK-WP-0025",
|
||||
"NK-WP-0026",
|
||||
"NK-WP-0027",
|
||||
"NK-WP-0028",
|
||||
"NK-WP-0029",
|
||||
"NK-WP-0030",
|
||||
"NK-WP-0031",
|
||||
"NK-WP-0032",
|
||||
"NK-WP-0033",
|
||||
"NK-WP-0034",
|
||||
"NK-WP-0035",
|
||||
"NK-WP-0036",
|
||||
"NK-WP-0037",
|
||||
"NK-WP-0038",
|
||||
"NK-WP-0039",
|
||||
"NK-WP-0040",
|
||||
"NK-WP-0041",
|
||||
"NK-WP-0042",
|
||||
"NK-WP-0043",
|
||||
"NK-WP-0044",
|
||||
"NK-WP-0045",
|
||||
"NK-WP-0046",
|
||||
"NK-WP-0047",
|
||||
"NK-WP-0048",
|
||||
"NK-WP-0049",
|
||||
"NK-WP-0050",
|
||||
"NK-WP-0051",
|
||||
"NK-WP-ADHOC-2026-06-14",
|
||||
"NK-WP-ADHOC-2026-07-02",
|
||||
"NK-WP-ADHOC-2026-08-14",
|
||||
"NK-WP-ADHOC-2026-08-23",
|
||||
"NK-WP-ADHOC-2026-10-03",
|
||||
"NK-WP-ADHOC-2026-10-10",
|
||||
"NK-WP-ADHOC-2026-10-11",
|
||||
"NK-WP-ADHOC-2026-10-16"
|
||||
],
|
||||
"retired": [],
|
||||
"released": [],
|
||||
"created_tasks": [],
|
||||
"updated_tasks": [
|
||||
"NK-WP-0048-T24",
|
||||
"NK-WP-0048-T25",
|
||||
"NK-WP-0048-T26",
|
||||
"NK-WP-0048-T27",
|
||||
"NK-WP-0048-T28",
|
||||
"NK-WP-0048-T29",
|
||||
"NK-WP-0048-T30",
|
||||
"NK-WP-0048-T31"
|
||||
],
|
||||
"cancelled_tasks": [],
|
||||
"created_edges": [],
|
||||
"removed_edges": [],
|
||||
"unresolved_edges": [
|
||||
{
|
||||
"workplan": "NK-WP-0006",
|
||||
"from": "NK-WP-0006",
|
||||
"target": "NK-WP-0001",
|
||||
"reason": "missing"
|
||||
}
|
||||
],
|
||||
"external_edges": [
|
||||
{
|
||||
"workplan": "NK-WP-0008",
|
||||
"from": "NK-WP-0008",
|
||||
"target": "NK-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "053c6d96-9396-40c9-a2e5-c36531e7810d",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0050",
|
||||
"from": "NK-WP-0050-T03",
|
||||
"target": "RPF-WP-0056-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "06ba98d5-609e-5aaa-9bf7-4c4f4460e377",
|
||||
"from_task_id": "5a3aaf57-de5c-5a03-bc99-9bcf2fc5d95e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "a82762eb-bb6d-5600-89ca-a81ada2eba9f"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0050",
|
||||
"from": "NK-WP-0050-T03",
|
||||
"target": "NK-WP-0050-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "06ba98d5-609e-5aaa-9bf7-4c4f4460e377",
|
||||
"from_task_id": "5a3aaf57-de5c-5a03-bc99-9bcf2fc5d95e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "a9dadd07-f136-5dc7-9229-7a9dbb941f0d"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0050",
|
||||
"from": "NK-WP-0050-T02",
|
||||
"target": "NK-WP-0050-T01",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "06ba98d5-609e-5aaa-9bf7-4c4f4460e377",
|
||||
"from_task_id": "a9dadd07-f136-5dc7-9229-7a9dbb941f0d",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "64893d94-9717-5812-b8e3-7cabe095fcdc"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0017",
|
||||
"from": "NK-WP-0017",
|
||||
"target": "NK-WP-0015",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "08398d26-cadf-44bc-97ee-67da790040e6",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "343e035d-41e1-4fc4-b209-6872e4a5fdc8",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011",
|
||||
"target": "NK-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011",
|
||||
"target": "NK-WP-0004",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011",
|
||||
"target": "NK-WP-0003",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "f24cefd4-a09b-4fa1-9b25-94bf783b425e",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T03",
|
||||
"target": "NK-WP-0011-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "2fe6f100-3a5d-563e-b033-7d7b846ae487"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T07",
|
||||
"target": "NK-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T07",
|
||||
"target": "NK-WP-0011-T04",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T07",
|
||||
"target": "NK-WP-0011-T05",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "d845380d-3dbc-5e59-8f2b-5a4d1b32d89d"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T04",
|
||||
"target": "NK-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T06",
|
||||
"target": "NK-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "9cb7fd93-c16e-5102-83ce-195b3aa87446",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T06",
|
||||
"target": "NK-WP-0011-T04",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "9cb7fd93-c16e-5102-83ce-195b3aa87446",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T06",
|
||||
"target": "NK-WP-0011-T05",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "9cb7fd93-c16e-5102-83ce-195b3aa87446",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "d845380d-3dbc-5e59-8f2b-5a4d1b32d89d"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T05",
|
||||
"target": "NK-WP-0011-T04",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "d845380d-3dbc-5e59-8f2b-5a4d1b32d89d",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "6697a994-7343-5ea8-8b37-bc3b921e5a9b"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T09",
|
||||
"target": "NK-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "d9dc4292-6b66-5797-9ff9-a1a4a556278e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T09",
|
||||
"target": "NK-WP-0011-T07",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "d9dc4292-6b66-5797-9ff9-a1a4a556278e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "38998c68-29bf-50d2-8c8d-0c75184d5833"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T09",
|
||||
"target": "NK-WP-0011-T08",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "d9dc4292-6b66-5797-9ff9-a1a4a556278e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "e5f44ddc-9645-5f61-b84c-da5ab9cccb6d"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T08",
|
||||
"target": "NK-WP-0011-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "e5f44ddc-9645-5f61-b84c-da5ab9cccb6d",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "2fe6f100-3a5d-563e-b033-7d7b846ae487"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0011",
|
||||
"from": "NK-WP-0011-T08",
|
||||
"target": "NK-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
|
||||
"from_task_id": "e5f44ddc-9645-5f61-b84c-da5ab9cccb6d",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "32d1411b-10a4-5a8b-8f43-99ac46d83908"
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0016",
|
||||
"from": "NET-WP-0016",
|
||||
"target": "NET-WP-0015",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "16069174-6698-4855-ad9e-5092c8571f38",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0016",
|
||||
"from": "NET-WP-0016",
|
||||
"target": "NK-WP-0012",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "16069174-6698-4855-ad9e-5092c8571f38",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T18",
|
||||
"target": "NK-WP-0049-T33",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "141dd773-c1fa-56c1-a852-bbe2372e5125",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T18",
|
||||
"target": "NK-WP-0049-T32",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "141dd773-c1fa-56c1-a852-bbe2372e5125",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "cd78dbb7-a8f2-5754-8788-6eb1b7d97741"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T35",
|
||||
"target": "NK-WP-0049-T34",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "1bfd1703-589f-5511-bad5-812d860e4a71",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T33",
|
||||
"target": "NK-WP-0049-T35",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "1bfd1703-589f-5511-bad5-812d860e4a71"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T33",
|
||||
"target": "NK-WP-0049-T34",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T33",
|
||||
"target": "NK-WP-0049-T36",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "8b72334b-fc27-5707-a189-bad345eaf3bc"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T33",
|
||||
"target": "NK-WP-0049-T32",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "56c8d469-7a10-5d50-a30e-95cbf3cd4756",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "cd78dbb7-a8f2-5754-8788-6eb1b7d97741"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T36",
|
||||
"target": "NK-WP-0049-T34",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "8b72334b-fc27-5707-a189-bad345eaf3bc",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "821b3a56-6f36-5812-abc9-5a00e37cb2e7"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0049",
|
||||
"from": "NK-WP-0049-T03",
|
||||
"target": "NK-WP-0049-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "17d315f0-829e-57c7-85cc-3a7914152804",
|
||||
"from_task_id": "d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "479d6970-6516-50f5-9436-bb5b56d58aad"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0025",
|
||||
"from": "NK-WP-0025",
|
||||
"target": "NK-WP-0024",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "2412a251-d6b5-5f05-8082-3eb7697c51dc",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "635a73d4-b3cb-5c38-af22-56a6a66beb0e",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0016",
|
||||
"from": "NK-WP-0016",
|
||||
"target": "NK-WP-0015",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "2d592e18-e63d-4856-97a1-f8c3e019e150",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "343e035d-41e1-4fc4-b209-6872e4a5fdc8",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0006",
|
||||
"from": "NK-WP-0006",
|
||||
"target": "NK-WP-0005",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "75bc472b-cc0a-48f2-afb6-62b896f7cc19",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0006",
|
||||
"from": "NK-WP-0006",
|
||||
"target": "NK-WP-0004",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0013",
|
||||
"from": "NK-WP-0013",
|
||||
"target": "NK-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "32a54d8e-8633-42a6-8ec1-104842c581c1",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0015",
|
||||
"from": "NK-WP-0015",
|
||||
"target": "NK-WP-0014",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "343e035d-41e1-4fc4-b209-6872e4a5fdc8",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0017",
|
||||
"from": "NET-WP-0017",
|
||||
"target": "NET-WP-0016",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "16069174-6698-4855-ad9e-5092c8571f38",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0017",
|
||||
"from": "NET-WP-0017",
|
||||
"target": "NET-WP-0015",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0007",
|
||||
"from": "NK-WP-0007",
|
||||
"target": "NK-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "3cbc81ec-7ad5-46cf-a4a0-fc5fe9873695",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0007",
|
||||
"from": "NK-WP-0007",
|
||||
"target": "NK-WP-0005",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "3cbc81ec-7ad5-46cf-a4a0-fc5fe9873695",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "75bc472b-cc0a-48f2-afb6-62b896f7cc19",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0007",
|
||||
"from": "NK-WP-0007",
|
||||
"target": "NK-WP-0004",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "3cbc81ec-7ad5-46cf-a4a0-fc5fe9873695",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0024",
|
||||
"from": "NK-WP-0024",
|
||||
"target": "NK-WP-0023",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "635a73d4-b3cb-5c38-af22-56a6a66beb0e",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "fdb3deee-06c3-5d12-91ce-9c50b2ac0618",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0015",
|
||||
"from": "NET-WP-0015",
|
||||
"target": "NK-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0015",
|
||||
"from": "NET-WP-0015",
|
||||
"target": "NK-WP-0012",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0018",
|
||||
"from": "NK-WP-0018",
|
||||
"target": "NK-WP-0017",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "6f75035a-e056-4eab-8fdb-00a18bacdf87",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "08398d26-cadf-44bc-97ee-67da790040e6",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0018",
|
||||
"from": "NK-WP-0018",
|
||||
"target": "NK-WP-0016",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "6f75035a-e056-4eab-8fdb-00a18bacdf87",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2d592e18-e63d-4856-97a1-f8c3e019e150",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0005",
|
||||
"from": "NK-WP-0005",
|
||||
"target": "NK-WP-0004",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "75bc472b-cc0a-48f2-afb6-62b896f7cc19",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0019",
|
||||
"from": "NET-WP-0019",
|
||||
"target": "NET-WP-0017",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "75d388b6-7ec1-4e1b-8c87-6ff44f953210",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0019",
|
||||
"from": "NET-WP-0019",
|
||||
"target": "NET-WP-0018",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "75d388b6-7ec1-4e1b-8c87-6ff44f953210",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "800f9f16-bc44-4bbf-a771-58a630a3b698",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0018",
|
||||
"from": "NET-WP-0018",
|
||||
"target": "NET-WP-0017",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "800f9f16-bc44-4bbf-a771-58a630a3b698",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "385de708-fd59-4bab-a4f4-28c1c476b3ea",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NET-WP-0018",
|
||||
"from": "NET-WP-0018",
|
||||
"target": "NET-WP-0015",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "800f9f16-bc44-4bbf-a771-58a630a3b698",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "6b9c25e4-1008-429a-8de6-54361872c0dd",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0027",
|
||||
"from": "NK-WP-0027-T03",
|
||||
"target": "NK-WP-0027-T02",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "965ad365-6b81-50a1-a2a3-2d0c1fcce0b4",
|
||||
"from_task_id": "e3f0bb0f-0c85-5cb9-a5ab-75f668cd3447",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "5b35b646-a525-55da-852a-6005613d42ea"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0012",
|
||||
"from": "NK-WP-0012",
|
||||
"target": "NK-WP-0006",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "2eb8a5e0-4e33-4ed3-8996-a2eec3aad862",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0014",
|
||||
"from": "NK-WP-0014",
|
||||
"target": "NK-WP-0013",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "32a54d8e-8633-42a6-8ec1-104842c581c1",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0014",
|
||||
"from": "NK-WP-0014",
|
||||
"target": "NK-WP-0012",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "b9bf56bc-7ef6-43eb-badc-fa0f4896c63c",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "9b8e4afc-eb71-47d9-8750-799a082b320a",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0019",
|
||||
"from": "NK-WP-0019",
|
||||
"target": "NK-WP-0018",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "d2daa8b4-8ecf-4377-b382-492e653735f7",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "6f75035a-e056-4eab-8fdb-00a18bacdf87",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0009",
|
||||
"from": "NK-WP-0009",
|
||||
"target": "NK-WP-0008",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "d4d02dbf-3974-502d-8b87-b776fc63e17e",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "053c6d96-9396-40c9-a2e5-c36531e7810d",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0022",
|
||||
"from": "NK-WP-0022",
|
||||
"target": "NK-WP-0023",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "d76ddccc-00c8-548a-b141-2cd660fa38da",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "fdb3deee-06c3-5d12-91ce-9c50b2ac0618",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T15",
|
||||
"target": "NK-WP-0048-T31",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "1b825705-4848-5edc-832f-b1622cd9bdb7",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "ed1ccd87-89a0-5e69-885c-0eaf5ab0bb5a"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T25",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "0de2a7d9-874d-578b-a076-f653fe42e30b"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T29",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "129db41d-4560-5e20-9f3b-c3c91e0a5601"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T24",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "1a3931ee-3b5f-52fc-9295-4073804f4bd2"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T28",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "6dab281c-ea9a-5a17-a83f-e8e9a4d4484f"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T30",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "6f6073ac-52dd-5b70-8874-6d76d4c072e4"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T26",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "94b3cbf0-c908-5ac6-b0db-67385c3e021c"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T16",
|
||||
"target": "NK-WP-0048-T27",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "e56365f8-29b5-5c89-a329-8115f9f3b2a7"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T07",
|
||||
"target": "NK-WP-0048-T06",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T09",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "0ee12dfc-8260-5038-bd39-cc7fb4113be0"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T15",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "1b825705-4848-5edc-832f-b1622cd9bdb7"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T16",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T05",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "3d987d3d-0797-538b-86e4-51185a327ae2"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "MASON-WP-0010-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "56cea6be-c613-586c-80cd-125340b374da"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T12",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T20",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T17",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "8c3e4baa-a218-5996-af9e-fe1153a46dac"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T08",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "8e519082-dc8b-5e46-b660-03d03e55dcf6"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T18",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "9c45da71-fb4c-5f20-9065-90f448f37c64"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T13",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "b264c10b-565c-5d29-b788-03cf5fbd2883"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "WARDEN-WP-0044-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T06",
|
||||
"target": "NK-WP-0048-T14",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "fce95440-a25b-5f27-8ef3-a3d182099113"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0048",
|
||||
"from": "NK-WP-0048-T14",
|
||||
"target": "NK-WP-0048-T20",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb",
|
||||
"from_task_id": "fce95440-a25b-5f27-8ef3-a3d182099113",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "863fcbdb-e246-51ba-87ca-a2f44d01d6f8"
|
||||
},
|
||||
{
|
||||
"workplan": "NK-WP-0010",
|
||||
"from": "NK-WP-0010",
|
||||
"target": "NK-WP-0008",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "f4faf8b4-ae57-40cf-a881-6fe66ca6ad74",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "053c6d96-9396-40c9-a2e5-c36531e7810d",
|
||||
"to_task_id": null
|
||||
}
|
||||
],
|
||||
"refused": [],
|
||||
"notes": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from pathlib import Path
|
||||
import json,hashlib,subprocess
|
||||
p=Path(__file__).resolve().parent;r=Path('/home/worsch/net-kingdom')
|
||||
pre=json.loads((p/'preimage.json').read_text());head=json.loads((p/'commit-receipt.json').read_text())['commit']
|
||||
def git(*a):return subprocess.check_output(['git','-C',str(r),*a]).decode().strip()
|
||||
assert git('rev-parse','HEAD')==head,'HEAD changed concurrently; stop'
|
||||
restored=[]
|
||||
for row in pre['files']:
|
||||
name=row['path']
|
||||
if name=='workplans/NK-WP-0048-security-case-types.md':
|
||||
assert (r/name).read_bytes()==(p/'candidate.md').read_bytes();continue
|
||||
if name in ('.custodian-brief.md','WORK-RECORDS.md'):
|
||||
if (r/name).read_bytes()!=(p/'backup'/name).read_bytes():
|
||||
(p/(name.lstrip('.')+'.after-consistency')).write_bytes((r/name).read_bytes())
|
||||
(r/name).write_bytes((p/'backup'/name).read_bytes());restored.append(name)
|
||||
if git('ls-files','--stage','--',name)!=row['index']:
|
||||
mode,oid,stage,path=row['index'].split();assert stage=='0';git('update-index','--cacheinfo',mode,oid,path)
|
||||
assert hashlib.sha256((r/name).read_bytes()).hexdigest()==row['sha256'],name
|
||||
assert git('ls-files','--stage','--',name)==row['index'],name
|
||||
for row in pre['untracked']:
|
||||
assert hashlib.sha256((r/row['path']).read_bytes()).hexdigest()==row['sha256'],row['path']
|
||||
(p/'preservation-verification.json').write_text(json.dumps({'head':head,'restored_generated_paths':restored,'unrelated_tracked_files_preserved':len(pre['files'])-1,'untracked_files_preserved':len(pre['untracked']),'status':git('status','--short')},indent=2)+'\n')
|
||||
print(json.dumps({'head':head,'restored':restored,'preserved':True}))
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"status": "published-and-projected",
|
||||
"commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"only_committed_path": "workplans/NK-WP-0048-security-case-types.md",
|
||||
"projection_status": "applied",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"derived_commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"original_task_count": 31,
|
||||
"current_task_count": 31,
|
||||
"all_task_ids_and_statuses_unchanged": true,
|
||||
"all_source_task_blocks_unchanged": true,
|
||||
"T15_task_block": "```task\nid: NK-WP-0048-T15\nstatus: wait\ndepends_on: [NK-WP-0048-T31]\nblocking_reason: \"Source candidate 061461e is merged. Waiting on the owner to select the real authenticated original-attempt holder and native readback, and on independent re-review of the changed T13 adapter source.\"\npriority: high\nstate_hub_task_id: \"1b825705-4848-5edc-832f-b1622cd9bdb7\"\n```",
|
||||
"preservation": {
|
||||
"head": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"restored_generated_paths": [],
|
||||
"unrelated_tracked_files_preserved": 65,
|
||||
"untracked_files_preserved": 7,
|
||||
"status": "M .custodian-brief.md\n M WORK-RECORDS.md\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json\n?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md"
|
||||
},
|
||||
"native_or_credential_or_adapter_changes": false,
|
||||
"root_git_brief_work_modified": false,
|
||||
"canonical_source_record_id_convergence_T24_T31": [
|
||||
{
|
||||
"id": "1a3931ee-3b5f-52fc-9295-4073804f4bd2",
|
||||
"record_id": "NK-WP-0048-T24",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "0de2a7d9-874d-578b-a076-f653fe42e30b",
|
||||
"record_id": "NK-WP-0048-T25",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "94b3cbf0-c908-5ac6-b0db-67385c3e021c",
|
||||
"record_id": "NK-WP-0048-T26",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "e56365f8-29b5-5c89-a329-8115f9f3b2a7",
|
||||
"record_id": "NK-WP-0048-T27",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "6dab281c-ea9a-5a17-a83f-e8e9a4d4484f",
|
||||
"record_id": "NK-WP-0048-T28",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "129db41d-4560-5e20-9f3b-c3c91e0a5601",
|
||||
"record_id": "NK-WP-0048-T29",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "6f6073ac-52dd-5b70-8874-6d76d4c072e4",
|
||||
"record_id": "NK-WP-0048-T30",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
},
|
||||
{
|
||||
"id": "ed1ccd87-89a0-5e69-885c-0eaf5ab0bb5a",
|
||||
"record_id": "NK-WP-0048-T31",
|
||||
"status": "done",
|
||||
"workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb"
|
||||
}
|
||||
],
|
||||
"canonical_counts": {
|
||||
"created": 0,
|
||||
"updated": 64,
|
||||
"retired": 0,
|
||||
"refused": 0,
|
||||
"released": 0,
|
||||
"created_tasks": 0,
|
||||
"updated_tasks": 8,
|
||||
"cancelled_tasks": 0,
|
||||
"created_edges": 0,
|
||||
"removed_edges": 0,
|
||||
"unresolved_edges": 1,
|
||||
"external_edges": 85
|
||||
},
|
||||
"reported_unresolved_edges": [
|
||||
{
|
||||
"workplan": "NK-WP-0006",
|
||||
"from": "NK-WP-0006",
|
||||
"target": "NK-WP-0001",
|
||||
"reason": "missing"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"schema": "custodian.warden-concurrent-original-context-comparison.v1",
|
||||
"observed_at": "2026-10-17T05:56:50.251443+00:00",
|
||||
"verdict": "Concurrent OriginalIntent variant solves local original-dispatch persistence, not independent original-creation authentication. Preserve its additive architecture; apply no older fingerprint-table replacement. Minimal artifact delta addresses concrete local consistency/privacy-contract defects only after root ownership/review.",
|
||||
"base_state": {
|
||||
"observed_at": "2026-10-17T05:53:20.364253+00:00",
|
||||
"owner": "ops-warden",
|
||||
"canonical_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"sources": [
|
||||
{
|
||||
"path": "src/warden/case_run.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671",
|
||||
"worktree_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "src/warden/case_receipt_state.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc",
|
||||
"worktree_sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "src/warden/case_inputs.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7",
|
||||
"worktree_sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "src/warden/__init__.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a",
|
||||
"worktree_sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "tests/test_case_run.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139",
|
||||
"worktree_sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "docs/security-case-execution-kernel-v1.md",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb",
|
||||
"worktree_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "workplans/WARDEN-WP-0044-security-case-follow-through.md",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1",
|
||||
"worktree_sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1",
|
||||
"dirty": false
|
||||
}
|
||||
],
|
||||
"canonical_edits_performed": false
|
||||
},
|
||||
"current_owner_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"current_owner_sources_match_snapshot": true,
|
||||
"canonical_writes_or_user_tree_tests_performed": false,
|
||||
"probes": {
|
||||
"snapshot_diagnostic_probes": 13,
|
||||
"scope": "Exact copied a45cf475 snapshot; probes passing for documented insecure behaviors mean finding reproduced, not secure/native acceptance.",
|
||||
"minimal_delta_cases": 54,
|
||||
"delta_case_scope": "43 existing kernel cases plus11 new metadata/context consistency checks; not additive to prior suites. Source-only generated data and existing synthetic owner trust.",
|
||||
"lint": "Owner-installed Ruff check and format check pass after removing two unused test imports; no dependency installation.",
|
||||
"extra_earlier_delta_cases": 56,
|
||||
"extra_earlier_scope": "Retained diagnostic variant included tenant/realm and missing-original-purpose observations; excluded those from canonical regression proposal to avoid encoding missing native proof as a desired control."
|
||||
},
|
||||
"comparison": [
|
||||
{
|
||||
"topic": "Actual persistence and restart",
|
||||
"concurrent": "OriginalIntent and running receipt share one SQLite transaction. Actual distinct interpreter retrieves original run identity after lost response. This solves the original local memory-loss/context seam.",
|
||||
"fingerprint_candidate": "Atomic receipt-linked invocation fingerprint also survives actual distinct interpreter; avoids raw field storage but introduces a second table and a changed reconcile callback.",
|
||||
"disposition": "Keep existing concurrent intents/reconcile_original surfaces; no duplicate context subsystem."
|
||||
},
|
||||
{
|
||||
"topic": "Privacy and closed field contract",
|
||||
"concurrent": "Normal Executor.admit produces bounded declared metadata strings plus empty tenant/realm, not arbitrary owner response payload. No observed real credential leak. OriginalIntent.payload/decode themselves accept arbitrary JSON-valued target and dynamically serialize all Invocation fields; a direct generated probe reproduces this missing public API invariant.",
|
||||
"fingerprint_candidate": "Stores only a fixed validated projection fingerprint; safer raw-data retention but not necessary to replace the concurrent architecture unilaterally.",
|
||||
"disposition": "Minimal delta validates exact Invocation/Pins and explicit field set/types/lengths/operations before payload/save and bounded duplicate-free decode. Keep raw identifiers only as the existing declared operational source contract; do not claim tenant/realm provenance from serialization."
|
||||
},
|
||||
{
|
||||
"topic": "Actor consistency",
|
||||
"concurrent": "original_intent compares principal/lane/tenant/realm but omits actor. A stored actor=agent is returned against current actor=human.",
|
||||
"fingerprint_candidate": "Fixed exact invocation fingerprint covers actor.",
|
||||
"disposition": "Add actor to existing equality comparison; no new permission or migration."
|
||||
},
|
||||
{
|
||||
"topic": "Original attempt/intent lineage",
|
||||
"concurrent": "Query reads only payload. Altered payload attempt_id can differ from intents.attempt_id and be returned; removing original running receipt leaves accepted orphan intent. Both reproduced in temporary SQLite fixtures.",
|
||||
"fingerprint_candidate": "Intent sequence/fingerprint links its context to an original running receipt; no authority inference.",
|
||||
"disposition": "Minimal delta checks SQL attempt_id equals payload and matches a bounded existing original running receipt with exact step/owner/pins/binding. Save validates intent, invocation and running state agree before atomic write."
|
||||
},
|
||||
{
|
||||
"topic": "Principal, tenant, realm and current admission",
|
||||
"concurrent": "Stored mismatched principal/tenant/realm is refused. However Executor.admit has no tenant/realm input or independent provenance; normal admitted values remain empty. Tests replacing the current Invocation fail at admission and do not prove authentic tenant claims.",
|
||||
"fingerprint_candidate": "Introduces no new tenant/realm fields or migration. Exact current readmission/pins are retained.",
|
||||
"disposition": "Keep tenant/realm explicitly unknown/reserved; do not add caller-settable claims or relax run-sheet matching. If future nonempty coordinates are needed, require existing authoritative claim/session owner contract and review."
|
||||
},
|
||||
{
|
||||
"topic": "Independent original proof versus local correspondence",
|
||||
"concurrent": "ReconcileContext reaches an owner callback, but Adapter.verify and evidence_verifier authenticate the fresh OwnerResult/current attempt and current revocations. No original-context-specific verifier purpose is emitted. Intent is not independent creation evidence and cannot itself establish created-only undo.",
|
||||
"fingerprint_candidate": "Adds explicit original/fresh-context proof purpose; no actual native verifier/holder supplied.",
|
||||
"disposition": "This is an unresolved native owner adapter/readback contract, not proof of a live bypass. Define exact independent original payload authentication in the admitted owner adapter/verification contract before native integration. Do not add an unadmitted second verifier/holder or treat callback context/JSON as assent. Minimal consistency delta intentionally adds no new native proof purpose."
|
||||
},
|
||||
{
|
||||
"topic": "Legacy and API compatibility",
|
||||
"concurrent": "Existing one-argument Adapter.reconcile remains usable; new reconcile_original opts into original-intent lookup and refuses missing records. Existing positional Adapter construction and Invocation defaults remain callable. v1 receipt JSON stays unchanged. Invocation.asdict now includes two extra default fields, so registration attempt payload/source consumers require explicit review before native admission.",
|
||||
"fingerprint_candidate": "Changes reconcile callback signature to two arguments; existing injected one-argument callbacks fail closed.",
|
||||
"disposition": "Retain additive callback compatibility. Missing legacy original intent is never backfilled for new original-aware path. No installed actual native adapters/verifier/CLI are found in inspected current sources."
|
||||
},
|
||||
{
|
||||
"topic": "Parent sessions and unknown outcomes",
|
||||
"concurrent": "Current caller/session checks and independent fresh parent-session/grant revocation remain enforced; withholding fresh parent revocation refuses. Original session proof is not retrieved/authenticated from intent; actual original holder must supply it. Lost reply and interrupted undo stay uncertainty gates.",
|
||||
"fingerprint_candidate": "Same current checks plus explicit contextual proof purpose; no real original/session holder.",
|
||||
"disposition": "Keep native gate closed until independently authenticated original result/revocation and current native UID/version checks exist. Changed session/run-sheet/caller pins cannot inherit source state automatically."
|
||||
}
|
||||
],
|
||||
"minimal_delta": {
|
||||
"path": "minimal-integration.patch",
|
||||
"base_commit": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"changes": [
|
||||
{
|
||||
"path": "src/warden/case_run.py",
|
||||
"before_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671",
|
||||
"after_sha256": "sha256:0cc62a33dbb3b90276b291f77fa5d0a906f19296874d68636957763f576d1ae2"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_case_original_context_consistency.py",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "sha256:8483bb5915701e8bd0749e85097e11076ffd539084ca68e9ec1c565babc157d1"
|
||||
}
|
||||
],
|
||||
"snapshot_patch_check": 0,
|
||||
"scope": "Only original-intent validation and matching + new focused tests; keeps intents schema, Invocation fields and reconcile_original API. No owner adapter, credential, native source activation, original-proof grant, task UUID/status or production mutation.",
|
||||
"apply_rule": "Root must recheck exact current hashes and coordinate implementation owner. This artifact is not an instruction to overwrite concurrent changes."
|
||||
},
|
||||
"actual_consumers_inspected": [
|
||||
{
|
||||
"path": "ops-warden/src/warden/case_run.py",
|
||||
"finding": "No installed default adapters/verifier/CLI; interfaces are injected fixed owner source."
|
||||
},
|
||||
{
|
||||
"path": "ops-warden/tests/test_case_run.py",
|
||||
"finding": "Only inspected reconcile_original consumer is synthetic fixture by_original, which delegates to old.reconcile(ctx.attempt); it does not independently authenticate original custody."
|
||||
},
|
||||
{
|
||||
"path": "net-kingdom/sso-mfa/k8s/keycape/keycape-registration-adapter.py",
|
||||
"finding": "Maintained registration adapter builds existing six positional Adapter fields and has no reconcile callback. Process-local creation and post-restart undo refusal remain."
|
||||
},
|
||||
{
|
||||
"path": "net-kingdom/tools/security-case-types/security_case_evidence.py",
|
||||
"finding": "Reads unchanged v1 receipts only; native_authenticated remains false. It is no original context/owner authenticator."
|
||||
}
|
||||
],
|
||||
"next_native_gate": "Unchanged NK48T15/T16/WARDEN44T03: actual independent original holder/readback, exact source/tenant/principal binding, original/current session proof and native revision/created-only custody remain absent. No gate closure or owner assent inferred.",
|
||||
"older_candidate_disposition": "original-context-r1 remains retained for design/failure history only. Its replacement table/API patch must not be applied over a45cf475; minimal-integration.patch is the non-overwriting proposal on this actual variant.",
|
||||
"failures_retained": "Initial minimal-delta lint found two unused test imports; removed. Snapshot diagnostic and changed-fixture probe results remain recorded separately."
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# Closed case execution kernel v1 — 2026-10-11
|
||||
|
||||
Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal
|
||||
at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
|
||||
was accepted through memo `infd-20261010-security-case-execution`, version 1,
|
||||
approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition
|
||||
disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original
|
||||
approved proposal remains byte-identical. That decision accepts source design;
|
||||
it grants no execution session, credential or deployment authority.
|
||||
|
||||
`src/warden/case_run.py` advances the pure reducer into an executable injected
|
||||
owner harness with private durable receipts. Descriptor version
|
||||
`warden.case.execution.v1` is a closed local implementation seam, not an accepted
|
||||
NetKingdom type schema. It contains fixed owner/adapter/source digest, actor,
|
||||
lane, target parameter, effect, reviewed created-object identifiers declared
|
||||
retry safety and explicit required revocation scopes (parent-session and broker-grant).
|
||||
A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused.
|
||||
An acknowledged type must additionally pin the descriptor bytes under its
|
||||
`execution_descriptors` map and independent owner evidence must authenticate
|
||||
that exact machine-seam revision. Existing descriptive types lack this map and
|
||||
remain plan-only. Their acknowledged labels do not authorize this new seam.
|
||||
|
||||
There are no installed owner adapters, native provenance verifier or dispatch CLI
|
||||
in this change. Callables are injected only through a fixed adapter registry,
|
||||
with immutable owner reference/source pins. A real integration must supply
|
||||
independent owner/native verification or explicit attended manual evidence;
|
||||
a bare JSON receipt identifier is insufficient. Fixture verifiers in tests
|
||||
are deliberately fake and attest no native case. Caller sessions are independently
|
||||
verified for exact principal/actor/lane at admission and again before dispatch;
|
||||
input actor labels alone are never sufficient. Agent mint is refused, including
|
||||
when WARDEN_AGENT_ID appears after initial human admission.
|
||||
|
||||
Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference
|
||||
and run-sheet identity. Actual ops-mason materialization evidence is required
|
||||
against the case source, separately from its status label. Case parameters supply
|
||||
an exact metadata target. Source/session changes cannot inherit journal success.
|
||||
Owner results have a closed metadata shape, result evidence fingerprint and
|
||||
separate independently authenticated grant/parent-session revocation evidence
|
||||
fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and
|
||||
created objects outside the reviewed scope become uncertain, never success.
|
||||
Python and OS stdout/stderr, including inherited child-process output, are
|
||||
discarded without buffering or retention under a serialized fd containment
|
||||
context. Independent provenance verification uses the same containment and
|
||||
sanitizes exceptions. All source inputs are capped at 256 KiB before parsing.
|
||||
Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other
|
||||
owner interruptions are rethrown with a fixed message while retaining running
|
||||
intent for crash recovery.
|
||||
|
||||
The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks
|
||||
and insecure modes, uses SQLite FULL synchronization and commits running intent
|
||||
before calling the adapter. A separate process lock prevents concurrent mutation.
|
||||
A crash between dispatch and final receipt leaves running; the next lock holder
|
||||
persists uncertain before refusing replay. Both intent and outcome remain in
|
||||
history. Each persisted attempt has a unique identity bound to all exact pins,
|
||||
operation (run/reconcile/rollback), sequence and nonce. Adapter results and both
|
||||
revocation proof purposes carry that identity. Prior-attempt evidence cannot
|
||||
verify a safe retry, even if a caller rewrites result labels. Descriptor-created
|
||||
objects are capped at 32 and histories at 128 transitions, with dispatch stopped
|
||||
before archival is required. A throw or lost response is not failed-before-change. A safe retry needs
|
||||
verified failed-before-change, unchanged pins, descriptor retry-safe and explicit
|
||||
retry. Success and no-change cannot replay.
|
||||
|
||||
Reconciliation is an explicit fixed owner callback, never the mutation callback.
|
||||
It may verify an already-present native CAS result as no-change/success, or prove
|
||||
failed-before-change; it never tells Warden to regenerate a credential. Uncertain
|
||||
or failed-after-change requires this owner-native inspection before any retry.
|
||||
Owner proof and revocation remain mandatory. Rollback visits only objects in a
|
||||
verified creation receipt, in reverse order; no-change has no undo and shared
|
||||
objects are excluded. Unknown/lost undo blocks another rollback.
|
||||
|
||||
The kernel operates individual admitted steps. Full ordered run-sheet orchestration,
|
||||
verification inventory, contained login integration, native owner adapters and
|
||||
CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is
|
||||
implemented. Current Forgejo source is not materialized and has no acknowledged
|
||||
machine descriptors. Its first native run remains an acceptance requirement,
|
||||
with real materialization, separately authenticated broker-grant and parent-session
|
||||
revocation receipts, session/owner receipts and consumer enforcement. This
|
||||
implementation neither promotes a type nor changes any case status.
|
||||
|
||||
Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open
|
||||
an independent SQLite connection inside dispatch to prove running was committed,
|
||||
crash after mutation and reopen a fresh journal to prove no duplicate invocation,
|
||||
lose a response then reconcile without mint, require explicit retry, check exact
|
||||
pins/session expiry, withhold malicious output and unconfirmed revocation, and
|
||||
prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and
|
||||
checks pass. No live token or target action occurred.
|
||||
|
||||
## Browser input projection — 2026-10-16 source preparation
|
||||
|
||||
An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser
|
||||
cases before admission. It calls the real NetKingdom type/case validator over
|
||||
verified producer bytes in a disposable Python process (`-I`, empty environment,
|
||||
timeout, no inherited output). The validator, composer, capability contract and lazy evidence
|
||||
modules have exact source pins; missing or changed code refuses. There is no
|
||||
automatic discovery or download. A producer upgrade needs reviewed pin changes.
|
||||
|
||||
Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest
|
||||
of each complete parameter definition list prevents reclassifying secrets as
|
||||
metadata, loosening constraints, or adding fields under a recognized version.
|
||||
The complete case passes producer validation, including required fields,
|
||||
constraints and credential-shape checks. Explicit fixed/default overrides,
|
||||
unknown fields and secret references refuse. Security-sensitive browser metadata
|
||||
can therefore be present without being passed to an owner adapter: the only
|
||||
projection is its descriptor's exact public/operational target identifier.
|
||||
No raw parameter values are added to invocations or persisted receipts.
|
||||
|
||||
`require_implemented=False` validates the acknowledged type's source, not its
|
||||
execution authority. Existing descriptor/type/source/catalog/caller provenance
|
||||
gates stay mandatory. Neither historical browser type contains the required
|
||||
exact machine-descriptor map, and neither is promoted by this change. There are
|
||||
still no installed native owner adapters, authenticated provenance verifier,
|
||||
contained session/ordered run orchestration or live CLI dispatch. A journal digest
|
||||
is a fingerprint and cannot authenticate native evidence. Completion still needs
|
||||
owner-admitted descriptors and actual structural, mint/delivery, consumer and
|
||||
revocation/rollback evidence. Local fixture success is not native acceptance.
|
||||
|
||||
Tests use the actual producer checkout (override sibling location with
|
||||
`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected
|
||||
test-only owner adapter/evidence verifier. Their descriptor extensions and case
|
||||
materialized labels exist only in memory. Canonical case/type bytes stay intact.
|
||||
|
||||
Producer integration refinement: the accepted NetKingdom browser-case patch
|
||||
`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05`
|
||||
is now the tested producer revision. Its evidence module is imported lazily only
|
||||
by `check-evidence`, outside this admission path; Warden nevertheless pins and
|
||||
copies it with the complete reviewed producer set. Missing or modified bytes in
|
||||
any of the four modules refuse admission. The test covers every dependency.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
pythonpath = src
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""OpsWarden — SSH CA and certificate lifecycle manager."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
"""Reviewed browser metadata projection; no native trust or command dispatch.
|
||||
|
||||
The producer validator and all reviewed producer modules are pinned below.
|
||||
Their verified bytes run in a disposable tree, without caller environment or
|
||||
ambient Python imports. Type parameters must match the exact reviewed version.
|
||||
Machine descriptor extensions still require Executor's independent admission.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from .case_receipt_state import Refused
|
||||
|
||||
# Reviewed producer: net-kingdom 015694ee414c802dc2ef1fa0120dfed610086e69.
|
||||
# Review: docs/reviews/2026-10-16-browser-producer-pin-refresh.md
|
||||
SOURCE_PINS = {
|
||||
"tools/security-case-types/security_case_evidence.py": "da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75",
|
||||
"tools/security-case-types/security_case_validator.py": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b",
|
||||
"tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f",
|
||||
"tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d",
|
||||
}
|
||||
PARAMETER_PINS = {
|
||||
"1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c",
|
||||
"1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6",
|
||||
}
|
||||
|
||||
_VALIDATE = """
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types'))
|
||||
import security_case_validator as v
|
||||
case, typ = json.load(sys.stdin)
|
||||
issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False)
|
||||
print('valid' if not v.has_errors(issues) else 'invalid')
|
||||
"""
|
||||
|
||||
|
||||
class BrowserInputs:
|
||||
"""Explicit owner configuration, never inferred from a case-supplied path."""
|
||||
|
||||
def __init__(self, netkingdom_root: Path):
|
||||
self.root = Path(netkingdom_root)
|
||||
|
||||
def target(self, case: dict, typ: dict, target_parameter: str) -> str:
|
||||
try:
|
||||
spec = typ["spec"]
|
||||
parameters = spec["parameters"]
|
||||
fingerprint = hashlib.sha256(
|
||||
json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
if (
|
||||
spec["id"] != "oidc-browser-relying-party"
|
||||
or PARAMETER_PINS.get(spec["version"]) != fingerprint
|
||||
):
|
||||
raise ValueError
|
||||
declared = {item["name"]: item for item in parameters}
|
||||
definition = declared.get(target_parameter, {})
|
||||
if (
|
||||
definition.get("sensitivity") not in ("public", "operational")
|
||||
or definition.get("case_settable") is not True
|
||||
):
|
||||
raise ValueError
|
||||
# No secret references or newly classified metadata are grandfathered.
|
||||
for name in case["spec"]["parameters"]:
|
||||
item = declared.get(name, {})
|
||||
if item.get("case_settable") is not True or item.get(
|
||||
"sensitivity"
|
||||
) not in ("public", "operational", "security_sensitive"):
|
||||
raise ValueError
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="warden-case-validator-"
|
||||
) as directory:
|
||||
for relative, expected in SOURCE_PINS.items():
|
||||
source = (self.root / relative).read_bytes()
|
||||
if hashlib.sha256(source).hexdigest() != expected:
|
||||
raise ValueError
|
||||
dest = Path(directory) / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.write_bytes(source)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-I", "-c", _VALIDATE, directory],
|
||||
input=json.dumps([case, typ]),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
env={},
|
||||
timeout=10,
|
||||
check=True,
|
||||
)
|
||||
if result.stdout != "valid\n":
|
||||
raise ValueError
|
||||
return case["spec"]["parameters"][target_parameter]
|
||||
except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError):
|
||||
raise Refused("browser metadata failed exact-version validation") from None
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
"""Proposed, pure case receipt reducer for WARDEN-WP-0044-T03.
|
||||
|
||||
This prototype executes nothing and implements no accepted owner adapter API.
|
||||
Callers MUST durably persist a returned running receipt before a side effect.
|
||||
Only metadata identifiers/digests enter this model; no command output is accepted.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum
|
||||
import re
|
||||
|
||||
_ID = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}\Z")
|
||||
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
||||
|
||||
|
||||
class Refused(ValueError):
|
||||
"""A transition requires an independent owner reconciliation or new review."""
|
||||
|
||||
|
||||
class Outcome(str, Enum):
|
||||
RUNNING = "running"
|
||||
SUCCESS = "verified-success"
|
||||
NO_CHANGE = "verified-no-change"
|
||||
FAILED_BEFORE = "failed-before-change"
|
||||
FAILED_AFTER = "failed-after-change"
|
||||
UNCERTAIN = "uncertain"
|
||||
ROLLBACK_RUNNING = "rollback-running"
|
||||
ROLLED_BACK = "verified-rolled-back"
|
||||
ROLLBACK_UNCERTAIN = "rollback-uncertain"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pins:
|
||||
"""Exact reviewed case/type/adapter/catalog/acceptance/run-sheet digests."""
|
||||
|
||||
case: str
|
||||
case_type: str
|
||||
adapter: str
|
||||
catalog: str
|
||||
acceptance: str
|
||||
run_sheet: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not all(_DIGEST.fullmatch(value) for value in vars(self).values()):
|
||||
raise Refused("pins require sha256 digests")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Receipt:
|
||||
attempt: int
|
||||
outcome: Outcome
|
||||
created: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StepState:
|
||||
step_id: str
|
||||
owner: str
|
||||
pins: Pins
|
||||
allowed_created: tuple[str, ...] = ()
|
||||
retry_safe: bool = False
|
||||
receipts: tuple[Receipt, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not all(_ID.fullmatch(v) for v in (self.step_id, self.owner, *self.allowed_created)):
|
||||
raise Refused("only metadata identifiers are supported")
|
||||
if len(set(self.allowed_created)) != len(self.allowed_created):
|
||||
raise Refused("duplicate reviewed object identifier")
|
||||
|
||||
@property
|
||||
def last(self) -> Receipt | None:
|
||||
return self.receipts[-1] if self.receipts else None
|
||||
|
||||
|
||||
def _same_pins(state: StepState, pins: Pins) -> None:
|
||||
if state.pins != pins:
|
||||
raise Refused("pins changed; a newly reviewed sheet is required")
|
||||
|
||||
|
||||
def _append(state: StepState, outcome: Outcome, created: tuple[str, ...] = ()) -> StepState:
|
||||
receipt = Receipt(len(state.receipts) + 1, outcome, created)
|
||||
return replace(state, receipts=(*state.receipts, receipt))
|
||||
|
||||
|
||||
def begin(state: StepState, pins: Pins, *, retry: bool = False) -> StepState:
|
||||
"""Return intent to dispatch. Persist before calling an owner adapter."""
|
||||
_same_pins(state, pins)
|
||||
last = state.last
|
||||
if last is None:
|
||||
if retry:
|
||||
raise Refused("no failed attempt to retry")
|
||||
elif not (last.outcome is Outcome.FAILED_BEFORE and state.retry_safe and retry):
|
||||
raise Refused("step cannot dispatch or replay without owner reconciliation")
|
||||
return _append(state, Outcome.RUNNING)
|
||||
|
||||
|
||||
def finish(state: StepState, pins: Pins, outcome: Outcome,
|
||||
*, created: tuple[str, ...] = ()) -> StepState:
|
||||
"""Accept only an adapter's verified, closed metadata result, never output."""
|
||||
_same_pins(state, pins)
|
||||
if not state.last or state.last.outcome is not Outcome.RUNNING:
|
||||
raise Refused("no running side effect")
|
||||
if not isinstance(outcome, Outcome) or outcome not in (Outcome.SUCCESS, Outcome.NO_CHANGE, Outcome.FAILED_BEFORE,
|
||||
Outcome.FAILED_AFTER, Outcome.UNCERTAIN):
|
||||
raise Refused("unsupported adapter result")
|
||||
if len(set(created)) != len(created) or not set(created) <= set(state.allowed_created):
|
||||
raise Refused("created objects are outside the reviewed case")
|
||||
if created and outcome not in (Outcome.SUCCESS, Outcome.FAILED_AFTER):
|
||||
raise Refused("this result cannot claim created objects")
|
||||
return _append(state, outcome, created)
|
||||
|
||||
|
||||
def recover(state: StepState) -> StepState:
|
||||
"""Lost result after persisted dispatch is uncertain, never safe to retry."""
|
||||
if state.last and state.last.outcome is Outcome.RUNNING:
|
||||
return _append(state, Outcome.UNCERTAIN)
|
||||
if state.last and state.last.outcome is Outcome.ROLLBACK_RUNNING:
|
||||
return _append(state, Outcome.ROLLBACK_UNCERTAIN, state.last.created)
|
||||
return state
|
||||
|
||||
|
||||
def rollback_objects(state: StepState, pins: Pins) -> tuple[str, ...]:
|
||||
"""Reviewed identifiers only, reverse created order; no shared-object undo."""
|
||||
_same_pins(state, pins)
|
||||
last = state.last
|
||||
if last is None or last.outcome is Outcome.NO_CHANGE:
|
||||
return ()
|
||||
if last.outcome is not Outcome.SUCCESS:
|
||||
raise Refused("rollback needs a verified successful creation receipt")
|
||||
return tuple(reversed(last.created))
|
||||
|
||||
|
||||
def begin_rollback(state: StepState, pins: Pins) -> StepState:
|
||||
objects = rollback_objects(state, pins)
|
||||
if not objects:
|
||||
raise Refused("no case-created objects to undo")
|
||||
return _append(state, Outcome.ROLLBACK_RUNNING, objects)
|
||||
|
||||
|
||||
def finish_rollback(state: StepState, pins: Pins, *, verified: bool) -> StepState:
|
||||
_same_pins(state, pins)
|
||||
if not state.last or state.last.outcome is not Outcome.ROLLBACK_RUNNING:
|
||||
raise Refused("no running rollback")
|
||||
outcome = Outcome.ROLLED_BACK if verified else Outcome.ROLLBACK_UNCERTAIN
|
||||
return _append(state, outcome, state.last.created)
|
||||
|
|
@ -0,0 +1,981 @@
|
|||
"""Closed local execution kernel v1; no installed adapters or live CLI dispatch.
|
||||
|
||||
An owner must acknowledge each descriptor and provide an independent provenance
|
||||
verifier. Descriptive case types remain plan-only. Nothing executes shell prose.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
||||
from dataclasses import asdict, dataclass, replace
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
import threading
|
||||
import uuid
|
||||
from typing import Callable
|
||||
|
||||
import yaml
|
||||
from .case_inputs import BrowserInputs
|
||||
from .case_receipt_state import (
|
||||
Outcome,
|
||||
Pins,
|
||||
Receipt,
|
||||
Refused,
|
||||
StepState,
|
||||
begin,
|
||||
finish,
|
||||
recover,
|
||||
begin_rollback,
|
||||
finish_rollback,
|
||||
)
|
||||
|
||||
IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z")
|
||||
MAX_SOURCE_BYTES = 262144
|
||||
MAX_HISTORY = 128
|
||||
OUTPUT_LOCK = threading.RLock()
|
||||
|
||||
|
||||
def bounded_source(value: bytes):
|
||||
if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES:
|
||||
raise Refused("source exceeds bounded input contract")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def contained_output():
|
||||
"""Discard Python and inherited OS stdout/stderr; serialize process fd changes."""
|
||||
with OUTPUT_LOCK:
|
||||
saved = []
|
||||
null = os.open(os.devnull, os.O_WRONLY)
|
||||
try:
|
||||
for target in (1, 2):
|
||||
saved.append((target, os.dup(target)))
|
||||
os.dup2(null, target)
|
||||
with redirect_stdout(_Discard()), redirect_stderr(_Discard()):
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
raise Refused("owner callback failed") from None
|
||||
except BaseException:
|
||||
raise OwnerInterrupted() from None
|
||||
finally:
|
||||
for target, original in saved:
|
||||
os.dup2(original, target)
|
||||
os.close(original)
|
||||
os.close(null)
|
||||
|
||||
|
||||
DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
||||
|
||||
|
||||
def digest(value: bytes) -> str:
|
||||
return "sha256:" + hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Descriptor:
|
||||
version: str
|
||||
step: str
|
||||
owner: str
|
||||
adapter: str
|
||||
adapter_source: str
|
||||
actor: str
|
||||
lane: str
|
||||
target_parameter: str
|
||||
effect: str
|
||||
allowed_created: tuple[str, ...]
|
||||
retry_safe: bool
|
||||
revocation_scopes: tuple[str, ...]
|
||||
|
||||
@classmethod
|
||||
def decode(cls, source: bytes) -> Descriptor:
|
||||
bounded_source(source)
|
||||
try:
|
||||
obj = json.loads(source)
|
||||
if set(obj) != set(cls.__dataclass_fields__):
|
||||
raise ValueError
|
||||
if not isinstance(obj["allowed_created"], list):
|
||||
raise ValueError
|
||||
obj["allowed_created"] = tuple(obj["allowed_created"])
|
||||
if not isinstance(obj["revocation_scopes"], list):
|
||||
raise ValueError
|
||||
obj["revocation_scopes"] = tuple(obj["revocation_scopes"])
|
||||
result = cls(**obj)
|
||||
identifiers = (
|
||||
result.step,
|
||||
result.owner,
|
||||
result.adapter,
|
||||
result.lane,
|
||||
result.target_parameter,
|
||||
*result.allowed_created,
|
||||
)
|
||||
if result.version != "warden.case.execution.v1" or not all(
|
||||
isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers
|
||||
):
|
||||
raise ValueError
|
||||
if not DIGEST.fullmatch(result.adapter_source) or result.actor not in (
|
||||
"human",
|
||||
"agent",
|
||||
):
|
||||
raise ValueError
|
||||
if (
|
||||
result.effect not in ("read", "change", "mint")
|
||||
or type(result.retry_safe) is not bool
|
||||
):
|
||||
raise ValueError
|
||||
scopes = set(result.revocation_scopes)
|
||||
if (
|
||||
len(scopes) != len(result.revocation_scopes)
|
||||
or not scopes <= {"parent-session", "broker-grant"}
|
||||
or "parent-session" not in scopes
|
||||
):
|
||||
raise ValueError
|
||||
if result.effect == "mint" and "broker-grant" not in scopes:
|
||||
raise ValueError
|
||||
if len(result.allowed_created) > 32:
|
||||
raise ValueError
|
||||
if len(set(result.allowed_created)) != len(result.allowed_created):
|
||||
raise ValueError
|
||||
return result
|
||||
except (ValueError, TypeError, KeyError):
|
||||
raise Refused("invalid closed execution descriptor") from None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OwnerResult:
|
||||
outcome: Outcome
|
||||
evidence: str
|
||||
created: tuple[str, ...] = ()
|
||||
revoked: bool = False
|
||||
session_revocation_evidence: str | None = None
|
||||
grant_revocation_evidence: str | None = None
|
||||
attempt_id: str = ""
|
||||
operation: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Invocation:
|
||||
step: str
|
||||
owner: str
|
||||
target: str
|
||||
actor: str
|
||||
principal: str
|
||||
lane: str
|
||||
pins: Pins
|
||||
attempt_id: str = ""
|
||||
operation: str = "admission"
|
||||
tenant: str = ""
|
||||
realm: str = ""
|
||||
|
||||
|
||||
INTENT_SCHEMA = "warden.case.original-intent.v1"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OriginalIntent:
|
||||
"""Durable record of the original mutating dispatch, written before dispatch.
|
||||
|
||||
Binds the original Invocation (attempt, step, owner, target, actor, principal,
|
||||
lane), all pins, the admitted adapter source and the tenant/realm provenance
|
||||
the caller was admitted with. It is evidence of what Warden dispatched, not
|
||||
owner proof of creation: owners still authenticate created-only provenance.
|
||||
"""
|
||||
|
||||
binding: str
|
||||
invocation: Invocation
|
||||
source: str
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Fixed bounded metadata only; tenant/realm are not authority claims.
|
||||
|
||||
Current admit leaves tenant/realm empty. Nonempty future coordinates need
|
||||
separate owner provenance; serializability alone must never supply it.
|
||||
"""
|
||||
if (
|
||||
type(self.invocation) is not Invocation
|
||||
or type(self.invocation.pins) is not Pins
|
||||
):
|
||||
raise Refused("exact original invocation required")
|
||||
i = self.invocation
|
||||
fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id)
|
||||
if (
|
||||
not all(
|
||||
type(value) is str and IDENTIFIER.fullmatch(value) for value in fields
|
||||
)
|
||||
or type(i.actor) is not str
|
||||
or i.actor not in ("human", "agent")
|
||||
or type(i.operation) is not str
|
||||
or i.operation not in ("run", "rollback")
|
||||
or not all(
|
||||
type(value) is str and (not value or IDENTIFIER.fullmatch(value))
|
||||
for value in (i.tenant, i.realm)
|
||||
)
|
||||
or type(self.source) is not str
|
||||
or not DIGEST.fullmatch(self.source)
|
||||
or type(self.binding) is not str
|
||||
or self.binding != f"{i.step}:{i.target}"
|
||||
):
|
||||
raise Refused("invalid original intent metadata")
|
||||
if set(Invocation.__dataclass_fields__) != {
|
||||
"step",
|
||||
"owner",
|
||||
"target",
|
||||
"actor",
|
||||
"principal",
|
||||
"lane",
|
||||
"pins",
|
||||
"attempt_id",
|
||||
"operation",
|
||||
"tenant",
|
||||
"realm",
|
||||
}:
|
||||
raise Refused("original invocation field contract changed")
|
||||
if not all(
|
||||
type(v) is str and DIGEST.fullmatch(v)
|
||||
for v in (
|
||||
i.pins.case,
|
||||
i.pins.case_type,
|
||||
i.pins.adapter,
|
||||
i.pins.catalog,
|
||||
i.pins.acceptance,
|
||||
i.pins.run_sheet,
|
||||
)
|
||||
):
|
||||
raise Refused("invalid original intent pins")
|
||||
|
||||
def payload(self) -> str:
|
||||
self.validate()
|
||||
return json.dumps(
|
||||
{"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source}
|
||||
| asdict(self.invocation),
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, raw: str) -> OriginalIntent:
|
||||
def unique(pairs):
|
||||
result = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise Refused("duplicate original intent field")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
try:
|
||||
if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES:
|
||||
raise Refused("original intent input exceeds bound")
|
||||
obj = json.loads(raw, object_pairs_hook=unique)
|
||||
fields = {
|
||||
"step",
|
||||
"owner",
|
||||
"target",
|
||||
"actor",
|
||||
"principal",
|
||||
"lane",
|
||||
"pins",
|
||||
"attempt_id",
|
||||
"operation",
|
||||
"tenant",
|
||||
"realm",
|
||||
}
|
||||
if (
|
||||
type(obj) is not dict
|
||||
or set(obj) != fields | {"schema", "binding", "source"}
|
||||
or obj["schema"] != INTENT_SCHEMA
|
||||
):
|
||||
raise Refused("unknown original intent schema")
|
||||
inv = {k: obj[k] for k in fields}
|
||||
inv["pins"] = Pins(**inv["pins"])
|
||||
result = cls(obj["binding"], Invocation(**inv), obj["source"])
|
||||
result.validate()
|
||||
return result
|
||||
except (TypeError, ValueError, KeyError):
|
||||
raise Refused("invalid original intent record") from None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReconcileContext:
|
||||
"""Original intent and the new reconciliation attempt, kept separate."""
|
||||
|
||||
original: OriginalIntent
|
||||
attempt: Invocation
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Adapter:
|
||||
reference: str
|
||||
source: str
|
||||
invoke: Callable[[Invocation], OwnerResult]
|
||||
verify: Callable[[Invocation, OwnerResult], bool]
|
||||
rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None
|
||||
reconcile: Callable[[Invocation], OwnerResult] | None = None
|
||||
reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None
|
||||
|
||||
|
||||
class Journal:
|
||||
"""Private SQLite journal plus process lock; transaction commits before dispatch."""
|
||||
|
||||
def __init__(self, directory: Path):
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if (
|
||||
directory.is_symlink()
|
||||
or directory.stat().st_uid != os.getuid()
|
||||
or directory.stat().st_mode & 0o077
|
||||
):
|
||||
raise Refused("journal directory is not private")
|
||||
self.directory = directory
|
||||
for name in ("journal.sqlite", "execution.lock"):
|
||||
path = directory / name
|
||||
fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077:
|
||||
raise Refused("journal file is not private")
|
||||
finally:
|
||||
os.close(fd)
|
||||
self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None)
|
||||
self.db.execute("PRAGMA synchronous=FULL")
|
||||
self.db.execute(
|
||||
"CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)"
|
||||
)
|
||||
self.db.execute(
|
||||
"CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)"
|
||||
)
|
||||
|
||||
def original_intent(
|
||||
self, binding: str, current: Invocation, source: str
|
||||
) -> OriginalIntent:
|
||||
"""Latest original run intent for binding, refused unless it matches exactly.
|
||||
|
||||
Missing or mismatched records refuse; the step stays uncertain.
|
||||
"""
|
||||
row = self.db.execute(
|
||||
"SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1",
|
||||
(binding,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise Refused("original intent record missing; step stays uncertain")
|
||||
intent = OriginalIntent.decode(row[1])
|
||||
o = intent.invocation
|
||||
if row[0] != o.attempt_id:
|
||||
raise Refused("original intent attempt identity differs")
|
||||
if intent.binding != binding or o.operation != "run":
|
||||
raise Refused("original intent binding mismatch")
|
||||
if o.pins != current.pins:
|
||||
raise Refused("original intent pins mismatch")
|
||||
if intent.source != source:
|
||||
raise Refused("original intent source mismatch")
|
||||
for name in (
|
||||
"step",
|
||||
"owner",
|
||||
"target",
|
||||
"actor",
|
||||
"principal",
|
||||
"lane",
|
||||
"tenant",
|
||||
"realm",
|
||||
):
|
||||
if getattr(o, name) != getattr(current, name):
|
||||
raise Refused(f"original intent {name} mismatch")
|
||||
# Match the persisted original running receipt. An orphan context or a
|
||||
# payload from another attempt cannot become the current lookup key.
|
||||
receipts = self.db.execute(
|
||||
"SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?",
|
||||
(binding, MAX_HISTORY + 1),
|
||||
).fetchall()
|
||||
if len(receipts) > MAX_HISTORY:
|
||||
raise Refused("original intent receipt history exceeds bound")
|
||||
matched = False
|
||||
try:
|
||||
for (raw,) in receipts:
|
||||
if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES:
|
||||
raise Refused("original receipt input exceeds bound")
|
||||
payload = json.loads(raw)
|
||||
if (
|
||||
payload.get("attempt_id") != o.attempt_id
|
||||
or payload.get("operation") != "run"
|
||||
):
|
||||
continue
|
||||
last = payload.get("receipts", [])[-1]
|
||||
if last.get("outcome") != "running":
|
||||
continue
|
||||
if (
|
||||
payload.get("journal_schema") != "warden.case.step-journal.v1"
|
||||
or payload.get("step_id") != o.step
|
||||
or payload.get("owner") != o.owner
|
||||
or payload.get("pins") != asdict(o.pins)
|
||||
):
|
||||
raise Refused("original running receipt binding differs")
|
||||
matched = True
|
||||
break
|
||||
except (TypeError, ValueError, KeyError, IndexError, AttributeError):
|
||||
raise Refused("invalid original running receipt") from None
|
||||
if not matched:
|
||||
raise Refused("original running receipt missing; no backfill")
|
||||
return intent
|
||||
|
||||
@contextmanager
|
||||
def locked(self):
|
||||
with (self.directory / "execution.lock").open("rb") as fh:
|
||||
try:
|
||||
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
raise Refused("case execution already active") from None
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(fh, fcntl.LOCK_UN)
|
||||
|
||||
def load(self, binding: str, initial: StepState) -> StepState:
|
||||
row = self.db.execute(
|
||||
"SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1",
|
||||
(binding,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return initial
|
||||
obj = json.loads(row[0])
|
||||
expected = {
|
||||
"step_id",
|
||||
"owner",
|
||||
"pins",
|
||||
"allowed_created",
|
||||
"retry_safe",
|
||||
"receipts",
|
||||
"journal_schema",
|
||||
"evidence_digest",
|
||||
"session_revoked",
|
||||
"session_revocation_digest",
|
||||
"grant_revocation_digest",
|
||||
"attempt_id",
|
||||
"operation",
|
||||
}
|
||||
if (
|
||||
set(obj) != expected
|
||||
or obj["journal_schema"] != "warden.case.step-journal.v1"
|
||||
):
|
||||
raise Refused("unknown journal receipt schema")
|
||||
state = StepState(
|
||||
obj["step_id"],
|
||||
obj["owner"],
|
||||
Pins(**obj["pins"]),
|
||||
tuple(obj["allowed_created"]),
|
||||
obj["retry_safe"],
|
||||
tuple(
|
||||
Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"]))
|
||||
for r in obj["receipts"]
|
||||
),
|
||||
)
|
||||
if (
|
||||
state.step_id,
|
||||
state.owner,
|
||||
state.pins,
|
||||
state.allowed_created,
|
||||
state.retry_safe,
|
||||
) != (
|
||||
initial.step_id,
|
||||
initial.owner,
|
||||
initial.pins,
|
||||
initial.allowed_created,
|
||||
initial.retry_safe,
|
||||
):
|
||||
raise Refused("journal pins changed")
|
||||
return state
|
||||
|
||||
def save(
|
||||
self,
|
||||
binding: str,
|
||||
state: StepState,
|
||||
evidence: OwnerResult | None = None,
|
||||
invocation: Invocation | None = None,
|
||||
intent: OriginalIntent | None = None,
|
||||
):
|
||||
if intent is not None:
|
||||
if type(intent) is not OriginalIntent:
|
||||
raise Refused("exact original intent required")
|
||||
intent.validate()
|
||||
expected = (
|
||||
Outcome.RUNNING
|
||||
if intent.invocation.operation == "run"
|
||||
else Outcome.ROLLBACK_RUNNING
|
||||
)
|
||||
if (
|
||||
intent.binding != binding
|
||||
or intent.invocation != invocation
|
||||
or intent.invocation.pins != state.pins
|
||||
or not state.last
|
||||
or state.last.outcome is not expected
|
||||
):
|
||||
raise Refused("original intent and running receipt differ")
|
||||
self.db.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
if intent is not None:
|
||||
self.db.execute(
|
||||
"INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)",
|
||||
(intent.invocation.attempt_id, binding, intent.payload()),
|
||||
)
|
||||
self.db.execute(
|
||||
"INSERT INTO receipts(binding,payload) VALUES (?,?)",
|
||||
(
|
||||
binding,
|
||||
json.dumps(
|
||||
asdict(state)
|
||||
| {
|
||||
"journal_schema": "warden.case.step-journal.v1",
|
||||
"attempt_id": invocation.attempt_id if invocation else None,
|
||||
"operation": invocation.operation if invocation else None,
|
||||
"evidence_digest": digest(evidence.evidence.encode())
|
||||
if evidence
|
||||
else None,
|
||||
"session_revoked": evidence.revoked if evidence else None,
|
||||
"session_revocation_digest": digest(
|
||||
evidence.session_revocation_evidence.encode()
|
||||
)
|
||||
if evidence and evidence.session_revocation_evidence
|
||||
else None,
|
||||
"grant_revocation_digest": digest(
|
||||
evidence.grant_revocation_evidence.encode()
|
||||
)
|
||||
if evidence and evidence.grant_revocation_evidence
|
||||
else None,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
),
|
||||
),
|
||||
)
|
||||
self.db.execute("COMMIT")
|
||||
except BaseException:
|
||||
self.db.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
|
||||
class Executor:
|
||||
"""Injected fixed owner adapters and provenance verifier; none ship by default.
|
||||
|
||||
evidence verifier must authenticate owner custody/provenance or explicitly
|
||||
attest attended manual evidence; a bare JSON reference is never sufficient.
|
||||
It receives purpose, exact source digest, and named evidence reference.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
journal: Journal,
|
||||
adapters: dict[str, Adapter],
|
||||
evidence_verifier: Callable[[str, str, str], bool],
|
||||
*,
|
||||
browser_inputs: BrowserInputs | None = None,
|
||||
):
|
||||
self.journal, self.adapters, self.evidence_verifier = (
|
||||
journal,
|
||||
dict(adapters),
|
||||
evidence_verifier,
|
||||
)
|
||||
self._admitted = {}
|
||||
self.browser_inputs = browser_inputs
|
||||
|
||||
def admit(
|
||||
self,
|
||||
case_source: bytes,
|
||||
type_source: bytes,
|
||||
descriptor_source: bytes,
|
||||
catalog_source: bytes,
|
||||
*,
|
||||
principal: str,
|
||||
actor: str,
|
||||
lane: str,
|
||||
acceptance_evidence: str,
|
||||
materialization_evidence: str,
|
||||
descriptor_evidence: str,
|
||||
session_evidence: str,
|
||||
) -> tuple[Descriptor, Invocation, StepState]:
|
||||
for source in (case_source, type_source, descriptor_source, catalog_source):
|
||||
bounded_source(source)
|
||||
descriptor = Descriptor.decode(descriptor_source)
|
||||
try:
|
||||
case = yaml.safe_load(case_source)["spec"]
|
||||
case_type = yaml.safe_load(type_source)["spec"]
|
||||
if not isinstance(case, dict) or not isinstance(case_type, dict):
|
||||
raise ValueError
|
||||
except (yaml.YAMLError, KeyError, TypeError, ValueError):
|
||||
raise Refused("invalid case or type source shape") from None
|
||||
if case.get("status") != "materialized" or not case.get("acceptance"):
|
||||
raise Refused("case is not accepted and structurally materialized")
|
||||
if case_type.get("status") not in ("acknowledged", "implemented"):
|
||||
raise Refused("type is not acknowledged")
|
||||
if case.get("type") != f"{case_type['id']}@{case_type['version']}":
|
||||
raise Refused("case type pin mismatch")
|
||||
if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest(
|
||||
descriptor_source
|
||||
):
|
||||
raise Refused(
|
||||
"type has no exact acknowledged machine descriptor; plan only"
|
||||
)
|
||||
for purpose, source, evidence in (
|
||||
("case-acceptance", digest(case_source), acceptance_evidence),
|
||||
(
|
||||
"ops-mason-materialization",
|
||||
digest(case_source),
|
||||
materialization_evidence,
|
||||
),
|
||||
(
|
||||
"owner-descriptor-acceptance:" + descriptor.owner,
|
||||
digest(descriptor_source),
|
||||
descriptor_evidence,
|
||||
),
|
||||
("type-machine-seam-acceptance", digest(type_source), descriptor_evidence),
|
||||
("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence),
|
||||
(
|
||||
f"caller-session:{actor}:{principal}:{lane}",
|
||||
digest(case_source),
|
||||
session_evidence,
|
||||
),
|
||||
):
|
||||
if (
|
||||
not isinstance(evidence, str)
|
||||
or not IDENTIFIER.fullmatch(evidence)
|
||||
or self._verify_evidence(purpose, source, evidence) is not True
|
||||
):
|
||||
raise Refused("independent owner evidence not verified")
|
||||
if (
|
||||
not IDENTIFIER.fullmatch(principal)
|
||||
or actor != descriptor.actor
|
||||
or lane != descriptor.lane
|
||||
):
|
||||
raise Refused("principal actor or lane mismatch")
|
||||
if descriptor.effect == "mint" and (
|
||||
actor != "human" or os.getenv("WARDEN_AGENT_ID")
|
||||
):
|
||||
raise Refused("agent mint is forbidden")
|
||||
parameters = case.get("parameters", {})
|
||||
declared = {
|
||||
item["name"]: item
|
||||
for item in case_type.get("parameters", [])
|
||||
if isinstance(item, dict) and isinstance(item.get("name"), str)
|
||||
}
|
||||
if (
|
||||
not isinstance(parameters, dict)
|
||||
or descriptor.target_parameter not in declared
|
||||
):
|
||||
raise Refused("target parameter is not declared by acknowledged type")
|
||||
if case_type.get("id") == "oidc-browser-relying-party":
|
||||
if type(self.browser_inputs) is not BrowserInputs:
|
||||
raise Refused("browser metadata validator is not configured")
|
||||
target = self.browser_inputs.target(
|
||||
yaml.safe_load(case_source),
|
||||
yaml.safe_load(type_source),
|
||||
descriptor.target_parameter,
|
||||
)
|
||||
else:
|
||||
for name in parameters:
|
||||
definition = declared.get(name)
|
||||
if (
|
||||
definition is None
|
||||
or definition.get("sensitivity") not in ("public", "operational")
|
||||
or definition.get("case_settable") is False
|
||||
):
|
||||
raise Refused("unknown, fixed or secret-sensitivity case parameter")
|
||||
target = parameters.get(descriptor.target_parameter)
|
||||
if not isinstance(target, str) or not IDENTIFIER.fullmatch(target):
|
||||
raise Refused("target must be exact case metadata")
|
||||
adapter = self.adapters.get(descriptor.adapter)
|
||||
if adapter is None or (adapter.reference, adapter.source) != (
|
||||
descriptor.adapter,
|
||||
descriptor.adapter_source,
|
||||
):
|
||||
raise Refused("unknown adapter or owner source pin")
|
||||
sheet = json.dumps(
|
||||
[
|
||||
descriptor.step,
|
||||
descriptor.owner,
|
||||
target,
|
||||
actor,
|
||||
principal,
|
||||
lane,
|
||||
acceptance_evidence,
|
||||
materialization_evidence,
|
||||
descriptor_evidence,
|
||||
session_evidence,
|
||||
],
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
pins = Pins(
|
||||
digest(case_source),
|
||||
digest(type_source),
|
||||
digest(descriptor_source),
|
||||
digest(catalog_source),
|
||||
digest(acceptance_evidence.encode()),
|
||||
digest(sheet),
|
||||
)
|
||||
invocation = Invocation(
|
||||
descriptor.step, descriptor.owner, target, actor, principal, lane, pins
|
||||
)
|
||||
initial = StepState(
|
||||
descriptor.step,
|
||||
descriptor.owner,
|
||||
pins,
|
||||
descriptor.allowed_created,
|
||||
descriptor.retry_safe,
|
||||
)
|
||||
self._admitted[invocation] = (descriptor, initial, session_evidence)
|
||||
return descriptor, invocation, initial
|
||||
|
||||
def _result(
|
||||
self, adapter: Adapter, invocation: Invocation, result: OwnerResult
|
||||
) -> OwnerResult:
|
||||
if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome):
|
||||
raise Refused("unknown owner result shape")
|
||||
if type(result.created) is not tuple or not all(
|
||||
isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created
|
||||
):
|
||||
raise Refused("owner created-object result invalid")
|
||||
if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch(
|
||||
result.evidence
|
||||
):
|
||||
raise Refused("owner evidence identifier invalid")
|
||||
if adapter.verify(invocation, result) is not True or result.revoked is not True:
|
||||
raise Refused("owner result or session revocation unverified")
|
||||
if (result.attempt_id, result.operation) != (
|
||||
invocation.attempt_id,
|
||||
invocation.operation,
|
||||
):
|
||||
raise Refused("owner receipt belongs to another attempt")
|
||||
descriptor = self._admitted[
|
||||
replace(invocation, attempt_id="", operation="admission")
|
||||
][0]
|
||||
proof_fields = {
|
||||
"parent-session": (
|
||||
f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}",
|
||||
result.session_revocation_evidence,
|
||||
),
|
||||
"broker-grant": (
|
||||
f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}",
|
||||
result.grant_revocation_evidence,
|
||||
),
|
||||
}
|
||||
proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes]
|
||||
for purpose, evidence in proofs:
|
||||
if (
|
||||
not isinstance(evidence, str)
|
||||
or not IDENTIFIER.fullmatch(evidence)
|
||||
or self._verify_evidence(purpose, invocation.pins.case, evidence)
|
||||
is not True
|
||||
):
|
||||
raise Refused("independent grant or parent-session revocation absent")
|
||||
return result
|
||||
|
||||
def _verify_evidence(self, purpose, source, evidence):
|
||||
try:
|
||||
with contained_output():
|
||||
return self.evidence_verifier(purpose, source, evidence) is True
|
||||
except Exception:
|
||||
raise Refused("independent evidence verification failed") from None
|
||||
|
||||
def _admission(
|
||||
self, descriptor: Descriptor, invocation: Invocation, initial: StepState
|
||||
):
|
||||
if self._admitted.get(invocation, ())[:2] != (descriptor, initial):
|
||||
raise Refused("execution requires verified admission on this executor")
|
||||
adapter = self.adapters.get(descriptor.adapter)
|
||||
if adapter is None or (adapter.reference, adapter.source) != (
|
||||
descriptor.adapter,
|
||||
descriptor.adapter_source,
|
||||
):
|
||||
raise Refused("adapter changed after admission")
|
||||
session = self._admitted[invocation][2]
|
||||
if (
|
||||
self._verify_evidence(
|
||||
f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}",
|
||||
invocation.pins.case,
|
||||
session,
|
||||
)
|
||||
is not True
|
||||
):
|
||||
raise Refused("caller session no longer verified")
|
||||
if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"):
|
||||
raise Refused("agent mint is forbidden")
|
||||
return adapter
|
||||
|
||||
def _attempt(self, invocation: Invocation, state: StepState, operation: str):
|
||||
identity = json.dumps(
|
||||
[asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex],
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
return replace(
|
||||
invocation,
|
||||
attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(),
|
||||
operation=operation,
|
||||
)
|
||||
|
||||
def run(
|
||||
self,
|
||||
descriptor: Descriptor,
|
||||
invocation: Invocation,
|
||||
initial: StepState,
|
||||
*,
|
||||
retry=False,
|
||||
) -> StepState:
|
||||
adapter = self._admission(descriptor, invocation, initial)
|
||||
with self.journal.locked():
|
||||
binding = f"{invocation.step}:{invocation.target}"
|
||||
state = self.journal.load(binding, initial)
|
||||
recovered = recover(state)
|
||||
if recovered != state:
|
||||
self.journal.save(binding, recovered)
|
||||
state = recovered
|
||||
if len(state.receipts) > MAX_HISTORY - 4:
|
||||
raise Refused("bounded journal history requires owner archival")
|
||||
running = begin(state, invocation.pins, retry=retry)
|
||||
dispatched = self._attempt(invocation, running, "run")
|
||||
self.journal.save(
|
||||
binding,
|
||||
running,
|
||||
invocation=dispatched,
|
||||
intent=OriginalIntent(binding, dispatched, adapter.source),
|
||||
)
|
||||
evidence = None
|
||||
try:
|
||||
with contained_output():
|
||||
result = self._result(
|
||||
adapter, dispatched, adapter.invoke(dispatched)
|
||||
)
|
||||
completed = finish(
|
||||
running, invocation.pins, result.outcome, created=result.created
|
||||
)
|
||||
evidence = result
|
||||
except Exception:
|
||||
completed = finish(running, invocation.pins, Outcome.UNCERTAIN)
|
||||
self.journal.save(binding, completed, evidence, dispatched)
|
||||
return completed
|
||||
|
||||
def rollback(
|
||||
self, descriptor: Descriptor, invocation: Invocation, initial: StepState
|
||||
) -> StepState:
|
||||
adapter = self._admission(descriptor, invocation, initial)
|
||||
with self.journal.locked():
|
||||
binding = f"{invocation.step}:{invocation.target}"
|
||||
state = self.journal.load(binding, initial)
|
||||
if recover(state) != state:
|
||||
self.journal.save(binding, recover(state))
|
||||
raise Refused("lost rollback result requires owner reconciliation")
|
||||
if adapter.rollback is None:
|
||||
raise Refused("owner has no reviewed undo adapter")
|
||||
if len(state.receipts) > MAX_HISTORY - 4:
|
||||
raise Refused("bounded journal history requires owner archival")
|
||||
running = begin_rollback(state, invocation.pins)
|
||||
dispatched = self._attempt(invocation, running, "rollback")
|
||||
self.journal.save(
|
||||
binding,
|
||||
running,
|
||||
invocation=dispatched,
|
||||
intent=OriginalIntent(binding, dispatched, adapter.source),
|
||||
)
|
||||
evidence = None
|
||||
try:
|
||||
with contained_output():
|
||||
result = self._result(
|
||||
adapter,
|
||||
dispatched,
|
||||
adapter.rollback(dispatched, running.last.created),
|
||||
)
|
||||
verified = result.outcome is Outcome.ROLLED_BACK and not result.created
|
||||
evidence = result if verified else None
|
||||
except Exception:
|
||||
verified = False
|
||||
completed = finish_rollback(running, invocation.pins, verified=verified)
|
||||
self.journal.save(binding, completed, evidence, dispatched)
|
||||
return completed
|
||||
|
||||
def reconcile(
|
||||
self, descriptor: Descriptor, invocation: Invocation, initial: StepState
|
||||
) -> StepState:
|
||||
"""Explicit owner-native state inspection; never redispatches the mutation."""
|
||||
adapter = self._admission(descriptor, invocation, initial)
|
||||
if adapter.reconcile is None and adapter.reconcile_original is None:
|
||||
raise Refused("owner reconciliation adapter absent")
|
||||
with self.journal.locked():
|
||||
binding = f"{invocation.step}:{invocation.target}"
|
||||
state = recover(self.journal.load(binding, initial))
|
||||
if not state.last or state.last.outcome not in (
|
||||
Outcome.UNCERTAIN,
|
||||
Outcome.FAILED_AFTER,
|
||||
):
|
||||
raise Refused("no uncertain mutation to reconcile")
|
||||
if len(state.receipts) > MAX_HISTORY - 4:
|
||||
raise Refused("bounded journal history requires owner archival")
|
||||
state = replace(
|
||||
state,
|
||||
receipts=(
|
||||
*state.receipts,
|
||||
Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN),
|
||||
),
|
||||
)
|
||||
original = (
|
||||
self.journal.original_intent(binding, invocation, adapter.source)
|
||||
if adapter.reconcile_original is not None
|
||||
else None
|
||||
)
|
||||
dispatched = self._attempt(invocation, state, "reconcile")
|
||||
self.journal.save(binding, state, invocation=dispatched)
|
||||
with contained_output():
|
||||
result = self._result(
|
||||
adapter,
|
||||
dispatched,
|
||||
adapter.reconcile_original(ReconcileContext(original, dispatched))
|
||||
if original is not None
|
||||
else adapter.reconcile(dispatched),
|
||||
)
|
||||
if result.outcome not in (
|
||||
Outcome.NO_CHANGE,
|
||||
Outcome.SUCCESS,
|
||||
Outcome.FAILED_BEFORE,
|
||||
):
|
||||
raise Refused("owner did not reconcile target state")
|
||||
# Validate created-object scope using the same closed result reducer.
|
||||
finish(
|
||||
begin(
|
||||
StepState(
|
||||
state.step_id,
|
||||
state.owner,
|
||||
state.pins,
|
||||
state.allowed_created,
|
||||
state.retry_safe,
|
||||
),
|
||||
state.pins,
|
||||
),
|
||||
state.pins,
|
||||
result.outcome,
|
||||
created=result.created,
|
||||
)
|
||||
reconciled = StepState(
|
||||
state.step_id,
|
||||
state.owner,
|
||||
state.pins,
|
||||
state.allowed_created,
|
||||
state.retry_safe,
|
||||
(
|
||||
*state.receipts,
|
||||
Receipt(len(state.receipts) + 1, result.outcome, result.created),
|
||||
),
|
||||
)
|
||||
self.journal.save(binding, reconciled, result, dispatched)
|
||||
return reconciled
|
||||
|
||||
|
||||
class OwnerInterrupted(KeyboardInterrupt):
|
||||
"""Sanitized interruption preserves persisted running/crash recovery semantics."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("owner execution interrupted")
|
||||
|
||||
|
||||
class _Discard:
|
||||
"""Never retain or forward adapter output, including unbounded malicious text."""
|
||||
|
||||
def write(self, text):
|
||||
return len(text)
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
"""Original-intent consistency checks with generated metadata only.
|
||||
|
||||
Reuse the existing synthetic owner rig. These checks do not provide a native
|
||||
holder or independently authenticate original creation from local SQL/JSON.
|
||||
"""
|
||||
|
||||
from dataclasses import asdict, replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
from test_case_run import rig as rig
|
||||
from warden.case_receipt_state import Outcome, Refused
|
||||
from warden.case_run import OriginalIntent
|
||||
|
||||
|
||||
def interrupted(rig):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
seen = []
|
||||
|
||||
def lost(inv):
|
||||
calls.append(inv)
|
||||
raise TimeoutError("generated lost response")
|
||||
|
||||
def original(ctx):
|
||||
seen.append(ctx)
|
||||
return old.reconcile(ctx.attempt)
|
||||
|
||||
executor.adapters[old.reference] = replace(
|
||||
old, invoke=lost, reconcile=None, reconcile_original=original
|
||||
)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
return executor, args, calls, old, seen
|
||||
|
||||
|
||||
def alter_payload(executor, **changes):
|
||||
row = executor.journal.db.execute(
|
||||
"SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1"
|
||||
).fetchone()
|
||||
obj = json.loads(row[1])
|
||||
obj.update(changes)
|
||||
executor.journal.db.execute(
|
||||
"UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0])
|
||||
)
|
||||
|
||||
|
||||
def test_original_actor_mismatch_is_refused(rig):
|
||||
executor, args, calls, old, _ = interrupted(rig)
|
||||
alter_payload(executor, actor="agent")
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent(
|
||||
"mint-secret:fixture-client", args[1], old.source
|
||||
)
|
||||
|
||||
|
||||
def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig):
|
||||
executor, args, calls, old, _ = interrupted(rig)
|
||||
actual = calls[0].attempt_id
|
||||
alter_payload(executor, attempt_id="attempt-unrelated")
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent(
|
||||
"mint-secret:fixture-client", args[1], old.source
|
||||
)
|
||||
assert (
|
||||
executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0]
|
||||
== actual
|
||||
)
|
||||
|
||||
|
||||
def test_orphan_intent_without_original_running_receipt_is_refused(rig):
|
||||
executor, args, calls, old, _ = interrupted(rig)
|
||||
executor.journal.db.execute(
|
||||
"DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)"
|
||||
)
|
||||
assert (
|
||||
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
is Outcome.UNCERTAIN
|
||||
)
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent(
|
||||
"mint-secret:fixture-client", args[1], old.source
|
||||
)
|
||||
|
||||
|
||||
def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig):
|
||||
_, args, _, _, _ = rig
|
||||
inv = replace(
|
||||
args[1],
|
||||
target={"unexpected": "synthetic-owner-payload"},
|
||||
operation="run",
|
||||
attempt_id="attempt-fixture",
|
||||
)
|
||||
record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64)
|
||||
with pytest.raises(Refused):
|
||||
OriginalIntent.decode(record.payload())
|
||||
# This is a direct serialization API gap, not an observed normal-admit leak.
|
||||
|
||||
# Owner trust is the existing synthetic fixture; this proves call shape only.
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,value",
|
||||
[
|
||||
("principal", "another-person"),
|
||||
("tenant", "another-tenant"),
|
||||
("realm", "another-realm"),
|
||||
],
|
||||
)
|
||||
def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch(
|
||||
rig, field, value
|
||||
):
|
||||
executor, args, _, old, _ = interrupted(rig)
|
||||
alter_payload(executor, **{field: value})
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent(
|
||||
"mint-secret:fixture-client", args[1], old.source
|
||||
)
|
||||
|
||||
|
||||
def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
executor.journal.db.execute(
|
||||
"CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END"
|
||||
)
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
executor.run(*args)
|
||||
assert not calls
|
||||
assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == (
|
||||
0,
|
||||
)
|
||||
assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == (
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def test_existing_missing_original_refuses_before_callback(rig):
|
||||
executor, args, _, _, seen = interrupted(rig)
|
||||
executor.journal.db.execute("DELETE FROM intents")
|
||||
with pytest.raises(Refused):
|
||||
executor.reconcile(*args)
|
||||
assert not seen
|
||||
assert (
|
||||
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
is Outcome.UNCERTAIN
|
||||
)
|
||||
|
||||
|
||||
def test_existing_current_parent_revocation_remains_mandatory(rig):
|
||||
executor, args, _, _, _ = interrupted(rig)
|
||||
old = executor.evidence_verifier
|
||||
|
||||
def refuse_session(purpose, source, evidence):
|
||||
return (
|
||||
False
|
||||
if purpose.startswith("session-revocation:")
|
||||
else old(purpose, source, evidence)
|
||||
)
|
||||
|
||||
executor.evidence_verifier = refuse_session
|
||||
with pytest.raises(Refused):
|
||||
executor.reconcile(*args)
|
||||
assert (
|
||||
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
is Outcome.UNCERTAIN
|
||||
)
|
||||
|
||||
|
||||
def test_existing_intent_survives_actual_distinct_interpreter(rig):
|
||||
executor, args, calls, old, _ = interrupted(rig)
|
||||
code = """import json,sys
|
||||
from pathlib import Path
|
||||
from warden.case_run import Journal,Invocation
|
||||
from warden.case_receipt_state import Pins
|
||||
obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1]))
|
||||
with j.locked():
|
||||
i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2])
|
||||
print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation}))
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source],
|
||||
input=json.dumps(asdict(args[1])),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
env={
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"PYTHONPATH": str(Path(__file__).parents[1] / "src"),
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
},
|
||||
check=True,
|
||||
)
|
||||
assert json.loads(result.stdout) == {
|
||||
"attempt": calls[0].attempt_id,
|
||||
"operation": "run",
|
||||
}
|
||||
|
|
@ -0,0 +1,700 @@
|
|||
"""Injected owner harness; these receipts are not native owner attestations."""
|
||||
|
||||
from dataclasses import asdict, replace
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import sqlite3
|
||||
import pytest
|
||||
import yaml
|
||||
from warden.case_receipt_state import Outcome, Refused
|
||||
from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rig(tmp_path):
|
||||
desc = Descriptor(
|
||||
"warden.case.execution.v1",
|
||||
"mint-secret",
|
||||
"owner",
|
||||
"owner/mint@v1",
|
||||
"sha256:" + "1" * 64,
|
||||
"human",
|
||||
"issuer-lane",
|
||||
"client_id",
|
||||
"mint",
|
||||
("case-policy", "case-role"),
|
||||
True,
|
||||
("parent-session", "broker-grant"),
|
||||
)
|
||||
descriptor = json.dumps(asdict(desc)).encode()
|
||||
typ = yaml.safe_dump(
|
||||
{
|
||||
"spec": {
|
||||
"id": "test",
|
||||
"version": "1",
|
||||
"status": "acknowledged",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "client_id",
|
||||
"sensitivity": "operational",
|
||||
"case_settable": True,
|
||||
}
|
||||
],
|
||||
"execution_descriptors": {"mint-secret": digest(descriptor)},
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
case = yaml.safe_dump(
|
||||
{
|
||||
"spec": {
|
||||
"id": "case-test",
|
||||
"type": "test@1",
|
||||
"status": "materialized",
|
||||
"acceptance": "acceptance",
|
||||
"parameters": {"client_id": "fixture-client"},
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
calls = []
|
||||
|
||||
def invoke(inv):
|
||||
calls.append(inv)
|
||||
return bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"owner-receipt",
|
||||
("case-policy", "case-role"),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
)
|
||||
|
||||
adapter = Adapter(
|
||||
"owner/mint@v1",
|
||||
desc.adapter_source,
|
||||
invoke,
|
||||
lambda inv, result: result.evidence == "owner-receipt",
|
||||
lambda inv, objects: bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.ROLLED_BACK,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
),
|
||||
lambda inv: bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.NO_CHANGE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
),
|
||||
)
|
||||
verified = {
|
||||
("case-acceptance", digest(case), "acceptance"),
|
||||
("ops-mason-materialization", digest(case), "materialization"),
|
||||
("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"),
|
||||
("type-machine-seam-acceptance", digest(typ), "descriptor"),
|
||||
("catalog-routing-acceptance", digest(b"catalog"), "descriptor"),
|
||||
("caller-session:human:operator:issuer-lane", digest(case), "session"),
|
||||
("session-revocation:issuer-lane", digest(case), "session-revoked"),
|
||||
("grant-revocation:issuer-lane", digest(case), "grant-revoked"),
|
||||
}
|
||||
|
||||
def verifier(purpose, source, evidence):
|
||||
if purpose.startswith(("session-revocation:", "grant-revocation:")):
|
||||
scope, lane, operation, attempt = purpose.split(":")
|
||||
return (
|
||||
source == digest(case)
|
||||
and lane == "issuer-lane"
|
||||
and operation in ("run", "rollback", "reconcile")
|
||||
and evidence
|
||||
== ("session-" if scope == "session-revocation" else "grant-") + attempt
|
||||
)
|
||||
return (purpose, source, evidence) in verified
|
||||
|
||||
executor = Executor(
|
||||
Journal(tmp_path / "private"), {"owner/mint@v1": adapter}, verifier
|
||||
)
|
||||
kwargs = dict(
|
||||
principal="operator",
|
||||
actor="human",
|
||||
lane="issuer-lane",
|
||||
acceptance_evidence="acceptance",
|
||||
materialization_evidence="materialization",
|
||||
descriptor_evidence="descriptor",
|
||||
session_evidence="session",
|
||||
)
|
||||
admitted = executor.admit(case, typ, descriptor, b"catalog", **kwargs)
|
||||
return (executor, admitted, calls, (case, typ, descriptor, b"catalog"), kwargs)
|
||||
|
||||
|
||||
def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def inspect(inv):
|
||||
other = sqlite3.connect(executor.journal.directory / "journal.sqlite")
|
||||
payload = json.loads(
|
||||
other.execute(
|
||||
"SELECT payload FROM receipts ORDER BY seq DESC LIMIT 1"
|
||||
).fetchone()[0]
|
||||
)
|
||||
assert payload["receipts"][-1]["outcome"] == "running"
|
||||
return old.invoke(inv)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=inspect)
|
||||
assert executor.run(*args).last.outcome is Outcome.SUCCESS
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_crash_and_fresh_process_resume_never_duplicates_effect(rig):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def crash(inv):
|
||||
calls.append(inv)
|
||||
raise KeyboardInterrupt
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=crash)
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
executor.run(*args)
|
||||
fresh = Executor(
|
||||
Journal(executor.journal.directory),
|
||||
{old.reference: old},
|
||||
executor.evidence_verifier,
|
||||
)
|
||||
admitted = fresh.admit(*sources, **kwargs)
|
||||
with pytest.raises(Refused):
|
||||
fresh.run(*admitted, retry=True)
|
||||
state = fresh.journal.load("mint-secret:fixture-client", admitted[2])
|
||||
assert state.last.outcome is Outcome.UNCERTAIN
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_lost_result_preserved_and_cas_reconciliation_does_not_mint(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def lost(inv):
|
||||
calls.append(inv)
|
||||
raise TimeoutError("SECRET-HIDDEN")
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=lost)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args, retry=True)
|
||||
assert len(calls) == 1
|
||||
assert (
|
||||
b"SECRET-HIDDEN"
|
||||
not in (executor.journal.directory / "journal.sqlite").read_bytes()
|
||||
)
|
||||
|
||||
|
||||
def test_failed_before_change_requires_explicit_retry_and_safe_owner(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def no_change(inv):
|
||||
calls.append(inv)
|
||||
return bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.FAILED_BEFORE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=no_change)
|
||||
assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert executor.run(*args, retry=True).last.outcome is Outcome.FAILED_BEFORE
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"result",
|
||||
[
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"owner-receipt",
|
||||
("shared-policy",),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"owner-receipt",
|
||||
(),
|
||||
False,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"unknown-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
{"outcome": "verified-success", "stdout": "SECRET-HIDDEN"},
|
||||
],
|
||||
)
|
||||
def test_unknown_secret_payload_created_scope_or_missing_revocation_never_succeeds(
|
||||
rig, result, capsys
|
||||
):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def malicious(inv):
|
||||
print("SECRET-HIDDEN" * 1000)
|
||||
return result
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=malicious)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
assert capsys.readouterr().out == ""
|
||||
assert (
|
||||
b"SECRET-HIDDEN"
|
||||
not in (executor.journal.directory / "journal.sqlite").read_bytes()
|
||||
)
|
||||
|
||||
|
||||
def test_rollback_reverses_only_verified_case_created_objects(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
undos = []
|
||||
|
||||
def undo(inv, objects):
|
||||
undos.append(objects)
|
||||
return old.rollback(inv, objects)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, rollback=undo)
|
||||
executor.run(*args)
|
||||
assert executor.rollback(*args).last.outcome is Outcome.ROLLED_BACK
|
||||
assert undos == [("case-role", "case-policy")]
|
||||
assert "shared-policy" not in undos[0]
|
||||
|
||||
|
||||
def test_uncertain_rollback_cannot_replay(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
executor.run(*args)
|
||||
executor.adapters[old.reference] = replace(
|
||||
old,
|
||||
rollback=lambda inv, objs: bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.ROLLED_BACK,
|
||||
"owner-receipt",
|
||||
(),
|
||||
False,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
),
|
||||
)
|
||||
assert executor.rollback(*args).last.outcome is Outcome.ROLLBACK_UNCERTAIN
|
||||
with pytest.raises(Refused):
|
||||
executor.rollback(*args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change", ["materialization", "type-prose", "actor", "lane", "source", "target"]
|
||||
)
|
||||
def test_exact_admission_requires_owner_evidence_and_closed_source_pins(rig, change):
|
||||
executor, _, _, sources, kwargs = rig
|
||||
case, typ, descriptor, catalog = sources
|
||||
kwargs = dict(kwargs)
|
||||
if change == "materialization":
|
||||
kwargs["materialization_evidence"] = "bare-json"
|
||||
if change == "type-prose":
|
||||
obj = yaml.safe_load(typ)
|
||||
obj["spec"].pop("execution_descriptors")
|
||||
typ = yaml.safe_dump(obj).encode()
|
||||
if change == "actor":
|
||||
kwargs["actor"] = "agent"
|
||||
if change == "lane":
|
||||
kwargs["lane"] = "other-lane"
|
||||
if change == "source":
|
||||
obj = json.loads(descriptor)
|
||||
obj["adapter_source"] = "sha256:" + "2" * 64
|
||||
descriptor = json.dumps(obj).encode()
|
||||
if change == "target":
|
||||
obj = yaml.safe_load(case)
|
||||
obj["spec"]["parameters"]["client_id"] = "other-target"
|
||||
case = yaml.safe_dump(obj).encode()
|
||||
with pytest.raises(Refused):
|
||||
executor.admit(case, typ, descriptor, catalog, **kwargs)
|
||||
|
||||
|
||||
def test_agent_env_cannot_dispatch_after_human_admission(rig, monkeypatch):
|
||||
executor, args, calls, _, _ = rig
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "test-agent")
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_forged_invocation_cannot_bypass_admission(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
with pytest.raises(Refused):
|
||||
executor.run(args[0], replace(args[1], target="other-target"), args[2])
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_journal_rejects_unsafe_directory_and_symlink(tmp_path):
|
||||
unsafe = tmp_path / "unsafe"
|
||||
unsafe.mkdir(mode=493)
|
||||
with pytest.raises(Refused):
|
||||
Journal(unsafe)
|
||||
private = tmp_path / "private"
|
||||
private.mkdir(mode=448)
|
||||
(private / "journal.sqlite").symlink_to(tmp_path / "outside")
|
||||
with pytest.raises(OSError):
|
||||
Journal(private)
|
||||
|
||||
|
||||
def test_process_lock_blocks_parallel_dispatch(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
with executor.journal.locked():
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_changed_catalog_pins_cannot_inherit_old_success(rig):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
executor.run(*args)
|
||||
with pytest.raises(Refused):
|
||||
executor.admit(*sources[:3], b"changed-catalog", **kwargs)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_private_journal_does_not_retain_owner_payload_or_accept_unknown_schema(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
executor.run(*args)
|
||||
row = executor.journal.db.execute(
|
||||
"SELECT seq,payload FROM receipts ORDER BY seq DESC LIMIT 1"
|
||||
).fetchone()
|
||||
payload = json.loads(row[1])
|
||||
assert payload["journal_schema"] == "warden.case.step-journal.v1"
|
||||
assert payload["evidence_digest"] == digest(b"owner-receipt")
|
||||
assert payload["session_revoked"] is True
|
||||
assert "owner-receipt" not in row[1]
|
||||
payload["stdout"] = "UNTRUSTED-OUTPUT"
|
||||
executor.journal.db.execute(
|
||||
"UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0])
|
||||
)
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
|
||||
|
||||
def test_expired_caller_session_cannot_use_cached_admission(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.evidence_verifier
|
||||
executor.evidence_verifier = lambda purpose, source, evidence: (
|
||||
False
|
||||
if purpose.startswith("caller-session:")
|
||||
else old(purpose, source, evidence)
|
||||
)
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index", [0, 1, 2, 3])
|
||||
def test_source_sizes_are_bounded_before_parsing(rig, index):
|
||||
executor, _, _, sources, kwargs = rig
|
||||
oversized = list(sources)
|
||||
oversized[index] = b"x" * 262145
|
||||
with pytest.raises(Refused, match="bounded"):
|
||||
executor.admit(*oversized, **kwargs)
|
||||
|
||||
|
||||
def test_owner_fd_and_inherited_subprocess_output_is_contained(rig, capfd):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def leaking(inv):
|
||||
os.write(1, b"SYNTHETIC-SECRET")
|
||||
os.write(2, b"SYNTHETIC-SECRET")
|
||||
subprocess.run([sys.executable, "-c", 'print("SYNTHETIC-SECRET")'], check=True)
|
||||
return old.invoke(inv)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=leaking)
|
||||
assert executor.run(*args).last.outcome is Outcome.SUCCESS
|
||||
observed = capfd.readouterr()
|
||||
assert "SYNTHETIC-SECRET" not in observed.out + observed.err
|
||||
|
||||
|
||||
def test_evidence_provider_output_and_error_never_escape(rig, capfd):
|
||||
executor, _, calls, sources, kwargs = rig
|
||||
|
||||
def broken(*args):
|
||||
print("SYNTHETIC-SECRET")
|
||||
os.write(2, b"SYNTHETIC-SECRET")
|
||||
raise RuntimeError("SYNTHETIC-SECRET")
|
||||
|
||||
executor.evidence_verifier = broken
|
||||
with pytest.raises(Refused) as caught:
|
||||
executor.admit(*sources, **kwargs)
|
||||
assert "SYNTHETIC-SECRET" not in str(caught.value)
|
||||
observed = capfd.readouterr()
|
||||
assert "SYNTHETIC-SECRET" not in observed.out + observed.err
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["session", "grant"])
|
||||
def test_broker_grant_and_parent_session_have_separate_required_evidence(rig, missing):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
result = old.invoke(args[1])
|
||||
if missing == "session":
|
||||
result = replace(result, session_revocation_evidence=None)
|
||||
else:
|
||||
result = replace(result, grant_revocation_evidence=None)
|
||||
executor.adapters[old.reference] = replace(old, invoke=lambda inv: result)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_mint_descriptor_cannot_omit_broker_grant_scope(rig):
|
||||
descriptor = json.loads(rig[3][2])
|
||||
descriptor["revocation_scopes"] = ["parent-session"]
|
||||
with pytest.raises(Refused):
|
||||
Descriptor.decode(json.dumps(descriptor).encode())
|
||||
|
||||
|
||||
def test_unsafe_owner_step_cannot_retry_even_after_verified_failed_before(
|
||||
rig, tmp_path
|
||||
):
|
||||
executor, args, _, sources, kwargs = rig
|
||||
descriptor = replace(args[0], retry_safe=False)
|
||||
descriptor_source = json.dumps(asdict(descriptor)).encode()
|
||||
type_doc = yaml.safe_load(sources[1])
|
||||
type_doc["spec"]["execution_descriptors"][descriptor.step] = digest(
|
||||
descriptor_source
|
||||
)
|
||||
type_source = yaml.safe_dump(type_doc).encode()
|
||||
old_verifier = executor.evidence_verifier
|
||||
|
||||
def verifier(purpose, source, evidence):
|
||||
if purpose == "owner-descriptor-acceptance:owner":
|
||||
return source == digest(descriptor_source) and evidence == "descriptor"
|
||||
if purpose == "type-machine-seam-acceptance":
|
||||
return source == digest(type_source) and evidence == "descriptor"
|
||||
return old_verifier(purpose, source, evidence)
|
||||
|
||||
adapter = executor.adapters[descriptor.adapter]
|
||||
|
||||
def failed(inv):
|
||||
return bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.FAILED_BEFORE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
"session-revoked",
|
||||
"grant-revoked",
|
||||
),
|
||||
)
|
||||
|
||||
fresh = Executor(
|
||||
Journal(tmp_path / "unsafe-retry"),
|
||||
{adapter.reference: replace(adapter, invoke=failed)},
|
||||
verifier,
|
||||
)
|
||||
admitted = fresh.admit(
|
||||
sources[0], type_source, descriptor_source, sources[3], **kwargs
|
||||
)
|
||||
assert fresh.run(*admitted).last.outcome is Outcome.FAILED_BEFORE
|
||||
with pytest.raises(Refused):
|
||||
fresh.run(*admitted, retry=True)
|
||||
|
||||
|
||||
def bound(inv, result):
|
||||
return replace(
|
||||
result,
|
||||
attempt_id=inv.attempt_id,
|
||||
operation=inv.operation,
|
||||
session_revocation_evidence=("session-" + inv.attempt_id)
|
||||
if result.session_revocation_evidence
|
||||
else None,
|
||||
grant_revocation_evidence=("grant-" + inv.attempt_id)
|
||||
if result.grant_revocation_evidence
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
def test_old_grant_and_session_proofs_cannot_verify_a_new_retry(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
adapter = executor.adapters[args[0].adapter]
|
||||
retained = []
|
||||
|
||||
def attempts(inv):
|
||||
if not retained:
|
||||
result = bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.FAILED_BEFORE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
"session-revoked",
|
||||
"grant-revoked",
|
||||
),
|
||||
)
|
||||
retained.append(result)
|
||||
return result
|
||||
# Even a caller rewriting labels cannot authenticate prior revocation
|
||||
# evidence for the newly persisted attempt identity.
|
||||
return replace(retained[0], attempt_id=inv.attempt_id, operation=inv.operation)
|
||||
|
||||
executor.adapters[adapter.reference] = replace(adapter, invoke=attempts)
|
||||
assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE
|
||||
assert executor.run(*args, retry=True).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_owner_interrupt_payload_is_sanitized_with_running_intent_retained(rig, capfd):
|
||||
executor, args, _, _, _ = rig
|
||||
adapter = executor.adapters[args[0].adapter]
|
||||
|
||||
def interrupted(inv):
|
||||
os.write(2, b"SYNTHETIC-SECRET")
|
||||
raise KeyboardInterrupt("SYNTHETIC-SECRET")
|
||||
|
||||
executor.adapters[adapter.reference] = replace(adapter, invoke=interrupted)
|
||||
with pytest.raises(KeyboardInterrupt) as caught:
|
||||
executor.run(*args)
|
||||
assert "SYNTHETIC-SECRET" not in str(caught.value)
|
||||
assert "SYNTHETIC-SECRET" not in "".join(capfd.readouterr())
|
||||
assert (
|
||||
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
is Outcome.RUNNING
|
||||
)
|
||||
|
||||
|
||||
def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal(
|
||||
rig, tmp_path
|
||||
):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
executor.run(*args)
|
||||
original_attempt = calls[-1].attempt_id
|
||||
new_catalog = b"independently-reviewed-catalog-v2"
|
||||
old_verifier = executor.evidence_verifier
|
||||
|
||||
def verifier(purpose, source, evidence):
|
||||
if purpose == "catalog-routing-acceptance":
|
||||
return source == digest(new_catalog) and evidence == "descriptor"
|
||||
return old_verifier(purpose, source, evidence)
|
||||
|
||||
fresh = Executor(Journal(tmp_path / "new-revision"), executor.adapters, verifier)
|
||||
revised = fresh.admit(*sources[:3], new_catalog, **kwargs)
|
||||
assert fresh.run(*revised).last.outcome is Outcome.SUCCESS
|
||||
assert calls[-1].attempt_id != original_attempt
|
||||
assert calls[-1].pins.catalog != calls[0].pins.catalog
|
||||
|
||||
|
||||
def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig):
|
||||
executor, _, _, sources, kwargs = rig
|
||||
type_doc = yaml.safe_load(sources[1])
|
||||
type_doc["spec"]["parameters"][0]["sensitivity"] = "secret"
|
||||
source = yaml.safe_dump(type_doc).encode()
|
||||
old = executor.evidence_verifier
|
||||
executor.evidence_verifier = lambda purpose, digest_value, evidence: (
|
||||
True
|
||||
if purpose == "type-machine-seam-acceptance"
|
||||
else old(purpose, digest_value, evidence)
|
||||
)
|
||||
with pytest.raises(Refused, match="secret-sensitivity"):
|
||||
executor.admit(sources[0], source, sources[2], sources[3], **kwargs)
|
||||
|
||||
|
||||
def _lose_result(executor, args, calls):
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def lost(inv):
|
||||
calls.append(inv)
|
||||
raise TimeoutError("lost")
|
||||
|
||||
seen = []
|
||||
|
||||
def by_original(ctx):
|
||||
seen.append(ctx)
|
||||
return old.reconcile(ctx.attempt)
|
||||
|
||||
executor.adapters[old.reference] = replace(
|
||||
old, invoke=lost, reconcile=None, reconcile_original=by_original
|
||||
)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
return seen
|
||||
|
||||
|
||||
def test_original_intent_survives_restart_separate_from_reconcile_attempt(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
seen = _lose_result(executor, args, calls)
|
||||
executor.journal = Journal(executor.journal.directory) # restart: fresh handle
|
||||
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
|
||||
(ctx,) = seen
|
||||
original = ctx.original.invocation
|
||||
assert original.attempt_id == calls[0].attempt_id
|
||||
assert original.operation == "run" and ctx.attempt.operation == "reconcile"
|
||||
assert ctx.attempt.attempt_id != original.attempt_id
|
||||
assert original.pins == args[1].pins
|
||||
assert (original.principal, original.tenant, original.realm) == (
|
||||
args[1].principal,
|
||||
args[1].tenant,
|
||||
args[1].realm,
|
||||
)
|
||||
assert ctx.original.source == executor.adapters[args[0].adapter].source
|
||||
|
||||
|
||||
def test_missing_original_intent_refuses_and_stays_uncertain(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
seen = _lose_result(executor, args, calls)
|
||||
executor.journal.db.execute("DELETE FROM intents")
|
||||
with pytest.raises(Refused, match="original intent record missing"):
|
||||
executor.reconcile(*args)
|
||||
assert seen == []
|
||||
binding = f"{args[1].step}:{args[1].target}"
|
||||
assert executor.journal.load(binding, args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change", [{"tenant": "other-tenant"}, {"realm": "other-realm"}, {"lane": "x"}]
|
||||
)
|
||||
def test_original_intent_mismatch_refuses(rig, change):
|
||||
executor, args, calls, _, _ = rig
|
||||
seen = _lose_result(executor, args, calls)
|
||||
with pytest.raises(Refused):
|
||||
executor.reconcile(args[0], replace(args[1], **change), args[2])
|
||||
assert seen == []
|
||||
|
||||
|
||||
def test_original_intent_source_mismatch_refuses(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
_lose_result(executor, args, calls)
|
||||
binding = f"{args[1].step}:{args[1].target}"
|
||||
with pytest.raises(Refused, match="source mismatch"):
|
||||
executor.journal.original_intent(binding, calls[0], "other-source")
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
"""Observed source behavior of exact concurrent snapshot, not secure acceptance.
|
||||
|
||||
Counterexamples use generated metadata and the owner's existing synthetic rig.
|
||||
Passing a counterexample means the documented gap reproduced; it is not a native
|
||||
holder, an owner attestation or permission to trust local SQL/JSON.
|
||||
"""
|
||||
from dataclasses import asdict, replace
|
||||
import inspect
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
from test_case_run import rig as rig
|
||||
from warden.case_receipt_state import Outcome, Refused
|
||||
from warden.case_run import Executor, Journal, OriginalIntent
|
||||
|
||||
|
||||
def interrupted(rig):
|
||||
executor,args,calls,sources,kwargs=rig
|
||||
old=executor.adapters[args[0].adapter];seen=[]
|
||||
def lost(inv):calls.append(inv);raise TimeoutError('generated lost response')
|
||||
def original(ctx):seen.append(ctx);return old.reconcile(ctx.attempt)
|
||||
executor.adapters[old.reference]=replace(old,invoke=lost,reconcile=None,reconcile_original=original)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
return executor,args,calls,old,seen
|
||||
|
||||
|
||||
def alter_payload(executor,**changes):
|
||||
row=executor.journal.db.execute('SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1').fetchone()
|
||||
obj=json.loads(row[1]);obj.update(changes)
|
||||
executor.journal.db.execute('UPDATE intents SET payload=? WHERE seq=?',(json.dumps(obj),row[0]))
|
||||
|
||||
|
||||
def test_observed_original_actor_mismatch_is_accepted_by_lookup(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
alter_payload(executor,actor='agent')
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
|
||||
|
||||
def test_observed_payload_attempt_id_is_not_checked_against_sql_column_or_receipt(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
actual=calls[0].attempt_id;alter_payload(executor,attempt_id='attempt-unrelated')
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
assert executor.journal.db.execute('SELECT attempt_id FROM intents').fetchone()[0]==actual
|
||||
|
||||
|
||||
def test_observed_orphan_intent_without_original_running_receipt_is_accepted(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
executor.journal.db.execute('DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)')
|
||||
assert executor.journal.load('mint-secret:fixture-client',args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
with pytest.raises(Refused):
|
||||
executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
|
||||
|
||||
def test_observed_serializer_accepts_arbitrary_values_outside_normal_admit(rig):
|
||||
_,args,_,_,_=rig
|
||||
inv=replace(args[1],target={'unexpected':'synthetic-owner-payload'},operation='run',attempt_id='attempt-fixture')
|
||||
record=OriginalIntent('mint-secret:fixture-client',inv,'sha256:'+'1'*64)
|
||||
with pytest.raises(Refused):
|
||||
OriginalIntent.decode(record.payload())
|
||||
# This is a direct serialization API gap, not an observed normal-admit leak.
|
||||
|
||||
|
||||
def test_observed_tenant_realm_not_admitted_and_remain_empty(rig):
|
||||
_,args,_,_,_=rig
|
||||
params=inspect.signature(Executor.admit).parameters
|
||||
assert 'tenant' not in params and 'realm' not in params
|
||||
assert args[1].tenant==args[1].realm==''
|
||||
|
||||
|
||||
def test_observed_no_original_context_purpose_is_requested(rig):
|
||||
executor,args,_,_,seen=interrupted(rig);old=executor.evidence_verifier;purposes=[]
|
||||
def trace(purpose,source,evidence):purposes.append(purpose);return old(purpose,source,evidence)
|
||||
executor.evidence_verifier=trace
|
||||
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
|
||||
assert seen and any(p.startswith('session-revocation:') for p in purposes)
|
||||
assert not any('original' in p for p in purposes)
|
||||
# Owner trust is the existing synthetic fixture; this proves call shape only.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value',[('principal','another-person'),('tenant','another-tenant'),('realm','another-realm')])
|
||||
def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch(rig,field,value):
|
||||
executor,args,_,old,_=interrupted(rig);alter_payload(executor,**{field:value})
|
||||
with pytest.raises(Refused):executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
|
||||
|
||||
def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig):
|
||||
executor,args,calls,_,_=rig
|
||||
executor.journal.db.execute("CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END")
|
||||
with pytest.raises(sqlite3.IntegrityError):executor.run(*args)
|
||||
assert not calls
|
||||
assert executor.journal.db.execute('SELECT COUNT(*) FROM intents').fetchone()==(0,)
|
||||
assert executor.journal.db.execute('SELECT COUNT(*) FROM receipts').fetchone()==(0,)
|
||||
|
||||
|
||||
def test_existing_missing_original_refuses_before_callback(rig):
|
||||
executor,args,_,_,seen=interrupted(rig)
|
||||
executor.journal.db.execute('DELETE FROM intents')
|
||||
with pytest.raises(Refused):executor.reconcile(*args)
|
||||
assert not seen
|
||||
assert executor.journal.load('mint-secret:fixture-client',args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_existing_current_parent_revocation_remains_mandatory(rig):
|
||||
executor,args,_,_,_=interrupted(rig);old=executor.evidence_verifier
|
||||
def refuse_session(purpose,source,evidence):
|
||||
return False if purpose.startswith('session-revocation:') else old(purpose,source,evidence)
|
||||
executor.evidence_verifier=refuse_session
|
||||
with pytest.raises(Refused):executor.reconcile(*args)
|
||||
assert executor.journal.load('mint-secret:fixture-client',args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_existing_intent_survives_actual_distinct_interpreter(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
code='''import json,sys
|
||||
from pathlib import Path
|
||||
from warden.case_run import Journal,Invocation
|
||||
from warden.case_receipt_state import Pins
|
||||
obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1]))
|
||||
with j.locked():
|
||||
i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2])
|
||||
print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation}))
|
||||
'''
|
||||
result=subprocess.run([sys.executable,'-B','-c',code,str(executor.journal.directory),old.source],
|
||||
input=json.dumps(asdict(args[1])),text=True,capture_output=True,timeout=15,
|
||||
env={'PATH':'/usr/bin:/bin','PYTHONPATH':str(Path(__file__).parents[1]/'src'),'PYTHONDONTWRITEBYTECODE':'1'},check=True)
|
||||
assert json.loads(result.stdout)=={'attempt':calls[0].attempt_id,'operation':'run'}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
---
|
||||
id: WARDEN-WP-0044
|
||||
type: workplan
|
||||
title: "Follow through on KnownSecurityCases along their type's route"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: claude-code
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-08"
|
||||
updated: "2026-10-11"
|
||||
depends_on: []
|
||||
related_repos:
|
||||
- net-kingdom
|
||||
- ops-mason
|
||||
- key-cape
|
||||
- railiance-platform
|
||||
state_hub_workstream_id: "f7e81013-6868-5d99-8528-ba75a442eca9"
|
||||
---
|
||||
|
||||
# WARDEN-WP-0044 — Follow through on KnownSecurityCases along their type's route
|
||||
|
||||
## Why
|
||||
|
||||
NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon
|
||||
`security-case-types_v0.1`, proposed). Once a case of an implemented type is
|
||||
accepted and ops-mason has materialized its structure (`MASON-WP-0010`),
|
||||
ops-warden carries out the remaining steps. These are the owners' declared entry
|
||||
points, run on the authentication path the type defines, followed by the type's
|
||||
verification.
|
||||
|
||||
Today ops-warden routes needs and, for `exec_capable` lanes, proxies fetches as
|
||||
the caller. It does not yet know about cases, and each new integration ends with
|
||||
a founder running a hand-written script (for example the proposed KEY-WP-0038-T02
|
||||
rollout). This plan gives ops-warden the follow-through role, without widening
|
||||
what it may do: each step still runs under its owner's entry point and the
|
||||
caller's identity.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T01
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T03]
|
||||
priority: high
|
||||
state_hub_task_id: "ef0920fc-418e-52cb-acff-9a5c0c728bf5"
|
||||
```
|
||||
|
||||
### T01 — Define the type's follow-through section
|
||||
|
||||
Propose the `follow_through` part of the SecurityCaseType schema together with
|
||||
net-kingdom. For each step it names the owner entry point, `warden_executes`,
|
||||
the auth method and lane, whether an attended human login is unavoidable (with
|
||||
the type's justification), and the verification command. Write it for the first
|
||||
type, `oidc-browser-relying-party`, covering key-cape client registration,
|
||||
custody minting and LLDAP group membership.
|
||||
|
||||
2026-10-08: NK-WP-0048-T03/T04 landed with a drafted section. Review is in
|
||||
`history/2026-10-08-oidc-rp-follow-through-review.md`. It requires three
|
||||
changes before T02: a per-step `lane`, a narrow `warden_executes`, and
|
||||
`value_handling`. It also lists four refinements. This task is done when
|
||||
net-kingdom acknowledges the review (NK-WP-0048-T05).
|
||||
|
||||
2026-10-09: R4 was withdrawn in favour of ops-mason's per-case
|
||||
`{consuming_service}-keycape-client`, with the entry body templated in the type
|
||||
(R4′, see the addendum in the review).
|
||||
|
||||
2026-10-09: the revised 1.0 type and canon now carry R1–R3, R4′, R5–R7.
|
||||
`warden case check-type <type.yaml>` (`src/warden/case_type.py`, tests in
|
||||
`tests/test_case_type.py`) checks a type's follow-through mechanically: the
|
||||
live type is schema-conformant, with one attended session
|
||||
(`key-cape-oidc-login`). It is not yet plannable, because two lanes are not
|
||||
in the catalog: `identity-provisioner-groups` (net-kingdom) and
|
||||
`keycape-client-secret-generate` (railiance-platform). These gate T02, not
|
||||
T01. T01 still closes on net-kingdom's acknowledgement (NK-WP-0048-T05).
|
||||
|
||||
2026-10-09: ops-warden cannot add `identity-provisioner-groups` as the type
|
||||
declares it. The provisioner accepts only user-engine's runtime token and
|
||||
admits only user-engine pods, so no caller-identity path exists. R8 (review
|
||||
addendum) asks net-kingdom either to move `ensure-groups` into the existing
|
||||
attended session (recommended) or to give the provisioner an operator
|
||||
credential. `keycape-client-secret-generate` needs a generate-in-custody entry
|
||||
point from railiance-platform, and the broker does not have one yet. Both were
|
||||
sent to their owners.
|
||||
|
||||
2026-10-09: done. net-kingdom took R8 option 1, so `ensure-groups` runs attended
|
||||
in the `register-client` session and `identity-provisioner-groups` is dropped
|
||||
(net-kingdom `0e86f95`). The founder acknowledged the type with memo r4 (hub
|
||||
decision `e418263e`, NK-WP-0048-T05 done).
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T02
|
||||
status: done
|
||||
depends_on: [WARDEN-WP-0044-T01]
|
||||
priority: high
|
||||
state_hub_task_id: "5f68c991-f490-58f8-a121-92f74073d272"
|
||||
```
|
||||
|
||||
### T02 — Plan a case run
|
||||
|
||||
`warden case plan <case.yaml>` checks that the case is accepted and materialized,
|
||||
resolves each step to its catalog entry and entry point, and prints the run:
|
||||
which steps warden executes, which run as the attended caller, what the
|
||||
verification is. No secret values; no execution.
|
||||
|
||||
Tests: refuse an unaccepted or unmaterialized case; refuse a step whose catalog
|
||||
entry is missing or not reviewed; show attended steps explicitly.
|
||||
|
||||
2026-10-09: done. `warden case plan <case.yaml> [--type] [--json]`
|
||||
(`src/warden/case_plan.py`, 14 tests in `tests/test_case_plan.py`). It refuses:
|
||||
- a case that is not `materialized`, or has no acceptance record
|
||||
- a type that is mismatched or not acknowledged
|
||||
- any `case check-type` finding, including a missing or unreviewed lane
|
||||
- undeclared, type-fixed or secret-sensitivity parameters
|
||||
- a placeholder the case does not set, or an unsupported `when`
|
||||
|
||||
It fills type defaults (`issuer`), evaluates `<param> == <value>` conditions,
|
||||
renders owner, entry point, auth and ready_check, groups attended steps per
|
||||
lane, and lists the applicable verification.
|
||||
|
||||
The live Forgejo case is correctly refused: it is `accepted`, not
|
||||
materialized. A materialized copy, with a stand-in lane for
|
||||
`keycape-client-secret-generate`, renders six steps: two attended steps plus
|
||||
`register-client` in one `key-cape-oidc-login` session, one warden step
|
||||
(`mint-secret`), and two owner steps. Seven checks follow.
|
||||
|
||||
Observation for net-kingdom (not blocking): `bind-env` runs before
|
||||
`register-client`, in type order. Its entry carries a literal
|
||||
`KEYCAPE_<CLIENT_ID>_CLIENT_SECRET`, which is not a `{param}` placeholder, so it
|
||||
reaches the plan unrendered.
|
||||
|
||||
2026-10-09 follow-up:
|
||||
- **Format specs.** `_render` supports net-kingdom's `{param:env}` spec (upper
|
||||
case, `-` as `_`, the same as `keycape-client.py env_name`) and refuses any
|
||||
other spec or `!conversion`.
|
||||
- **Case reference.** The plan prints `case-ref: <case_id>@sha256:<hex>` over the
|
||||
case file's exact bytes. This is the reference proposed to railiance-platform
|
||||
for `credential.py generate --case-ref` (RPF-WP-0054-T01); the broker
|
||||
re-derives it from the file.
|
||||
- 18 tests now.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T03
|
||||
status: progress
|
||||
depends_on: [WARDEN-WP-0044-T02, MASON-WP-0010-T03]
|
||||
blocking_reason: "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run."
|
||||
priority: high
|
||||
state_hub_task_id: "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89"
|
||||
```
|
||||
|
||||
### T03 — Run and verify a case
|
||||
|
||||
`warden case run <case.yaml>` executes the plan. Owner entry points run through
|
||||
the existing proxy as the caller; steps that need an attended login use the
|
||||
existing contained login envelope (silent command, session revoked afterwards),
|
||||
grouped into as few attended sessions as possible. Then it runs the type's
|
||||
verification and writes a receipt without values. `warden case rollback`
|
||||
reverses exactly this case's steps.
|
||||
|
||||
Acceptance: the Forgejo reference case (NK-WP-0048-T06) reaches `live` through
|
||||
this path, with no case-specific script.
|
||||
|
||||
2026-10-17: original-intent lookup/context interface for NK-WP-0048-T15, source
|
||||
and synthetic tests only. `src/warden/case_run.py`: `Executor.run` writes an
|
||||
`OriginalIntent` (schema `warden.case.original-intent.v1`: the dispatched run
|
||||
Invocation — attempt, step, owner, target, actor, principal, lane, all six pins,
|
||||
tenant, realm — plus admitted adapter source) in the same SQLite transaction as
|
||||
the pre-dispatch journal row. `Journal.original_intent(binding, current, source)`
|
||||
returns it after restart and refuses on a missing record or any
|
||||
pins/source/step/owner/target/principal/lane/tenant/realm mismatch, before any
|
||||
journal write, so the step stays uncertain. An adapter's
|
||||
`reconcile_original(ReconcileContext(original, attempt))` receives the original
|
||||
intent separately from the new reconcile attempt. This is Warden's record of what
|
||||
it dispatched, not owner created-only provenance; tenant/realm are as supplied at
|
||||
admission. Owner descriptors/adapters, contained session integration, Forgejo
|
||||
materialization and the native run remain open.
|
||||
|
||||
Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09):
|
||||
- `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate
|
||||
--client-id <id> --case-ref <case-ref> --case-file <case.yaml>` as the caller's
|
||||
child process. The broker re-derives the case-ref and checks status and
|
||||
client_id.
|
||||
- warden refuses that step when `WARDEN_AGENT_ID` is set.
|
||||
|
||||
2026-10-10: railiance-platform implemented the command (RPF-WP-0054-T03). It is
|
||||
catalogued as draft `keycape-client-secret-generate`, with `exec_owner`
|
||||
railiance-platform and `exec_capable: false`. `warden case check-type` now
|
||||
reports the lane as LANE-UNREVIEWED rather than LANE-MISSING. Before it can be
|
||||
activated:
|
||||
- the founder binds the issuer policy to the operator OIDC role
|
||||
- key-cape confirms the secret format
|
||||
- the RPF-WP-0054-T05 probe passes
|
||||
|
||||
2026-10-10 (later):
|
||||
- **Broker lane.** The issuer role is bound (CCR-2026-0043) and the T05 probe
|
||||
passed (railiance-platform `21b8af3`). Only key-cape's format confirmation is
|
||||
left. The playbook now includes the issuer-role login, a 15m token with
|
||||
unlimited uses that `warden case run` must group with the attended session.
|
||||
- **Env spec.** net-kingdom adopted `{client_id:env}` as a 1.0 patch
|
||||
(`9a9b8e3`). The plan renders `KEYCAPE_FORGEJO_CLIENT_SECRET`.
|
||||
- **Forgejo case.** Rendered and built by ops-mason. It is not yet
|
||||
`materialized`: MASON-WP-0010-T03 waits on the railiance-platform CCRs and
|
||||
their founder apply.
|
||||
|
||||
2026-10-10: `keycape-client-secret-generate` activated on railiance-platform's
|
||||
go-ahead (message 20973a9c) after key-cape confirmed the format (4e0213b3).
|
||||
`warden case check-type` on the live type now reports `plannable=True`. The
|
||||
only thing T03 still waits on is the Forgejo case reaching `materialized`
|
||||
(MASON-WP-0010-T03).
|
||||
|
||||
2026-10-10: net-kingdom moved the type to 1.1 (`e2174ce`) and re-pinned the
|
||||
Forgejo case. 1.1 adds the verification `consumer-enforces-groups` and requires
|
||||
the hand-over to enforce `access_groups`. `check-type` on 1.1 reports
|
||||
`plannable=True`, and a materialized copy renders six steps and the new check.
|
||||
The case-ref digest covers the whole case file, including `status`, so it
|
||||
changes when the case becomes `materialized`. `warden case run` therefore takes
|
||||
it from the file at run time, and the broker re-derives it from the same file.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T04
|
||||
status: wait
|
||||
depends_on: [WARDEN-WP-0044-T03]
|
||||
blocking_reason: "Catalog integration follows a proven run."
|
||||
priority: medium
|
||||
state_hub_task_id: "4a0fa4a7-5ca9-518a-ab92-a79a5e2cf93e"
|
||||
```
|
||||
|
||||
### T04 — Catalog entries from types
|
||||
|
||||
Let a type contribute reviewed catalog entries once, so that every case of that
|
||||
type routes without a new catalog edit. Cases add parameter values, not routes.
|
||||
`warden route find` should be able to answer "which SecurityCaseType covers this
|
||||
need?" before anyone builds by hand.
|
||||
|
||||
|
||||
2026-10-10 consolidation source prototype (not an accepted owner adapter API):
|
||||
`src/warden/case_receipt_state.py` provides a pure immutable receipt reducer,
|
||||
with exact case/type/adapter/catalog/acceptance/run-sheet pins, running intent
|
||||
before dispatch, uncertain recovery after a lost result, no success replay,
|
||||
explicit retry-safe failed-before-change retry and created-only reverse undo.
|
||||
It executes nothing and accepts no command output. The caller must durably
|
||||
persist running before any future side effect; storage/authenticated owner
|
||||
receipts/live adapters are not implemented. Focused offline tests cover changed
|
||||
pins, lost receipts, retry/refusal, uncertain rollback and shared-object
|
||||
preservation. Source proposal: `docs/security-case-execution-proposal.md`.
|
||||
|
||||
T03 remains open: typed owner execution/receipt contracts require review, the
|
||||
Forgejo structural materialization needs owner receipts, and CLI dispatch,
|
||||
contained sessions, native verification and provenance remain unimplemented.
|
||||
There is a circularity to resolve with net-kingdom/ops-mason: materialization
|
||||
cannot require mint-secret if mint-secret is itself a follow-through step whose
|
||||
planning requires materialized. No case status is changed by this prototype.
|
||||
|
||||
|
||||
2026-10-11: the exact execution-design memo version1 has confirmed native human
|
||||
acceptance (approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca). The approved proposal
|
||||
bytes remain unchanged. `src/warden/case_run.py` now implements a closed v1 local
|
||||
execution seam, independent admission/evidence callbacks, private transactional
|
||||
SQLite receipt storage, process locking, running-before-dispatch, crash recovery
|
||||
to uncertainty, explicit safe retry, separately required broker-grant and parent-session revocation
|
||||
proofs, native owner reconciliation and created-only
|
||||
reverse rollback. Thirty-seven kernel tests and fourteen reducer tests pass;
|
||||
ruff checks pass. See `docs/security-case-execution-kernel-v1.md` for exact scope
|
||||
and receipt provenance. No installed native adapter or live CLI execution is
|
||||
claimed. Current prose types stay plan-only; acknowledged descriptor/type machine
|
||||
seams and authenticated owner/caller receipts are still required. T03 progresses
|
||||
for implementation but cannot finish before ordered native Forgejo acceptance.
|
||||
|
||||
2026-10-16 source preparation (isolated checkout; no native acceptance):
|
||||
T03's browser parameter mismatch now has a producer-validator-backed projection
|
||||
in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2
|
||||
parameter definitions and all four reviewed producer modules are pinned; full case
|
||||
validation admits reviewed security-sensitive metadata while unknown/fixed/secret
|
||||
inputs and type/producer drift refuse. Only the reviewed descriptor target
|
||||
reaches the owner callback; no parameter journal payload is added. Actual
|
||||
Executor/producer tests cover positive versions and refusal/containment paths.
|
||||
Acknowledged machine descriptors, authenticated owner evidence, real Forgejo
|
||||
materialization and contained native sessions remain mandatory and unimplemented;
|
||||
T03 stays progress, T04 stays wait. No type or case status changed.
|
||||
|
||||
2026-10-16 producer source refresh: independently reviewed NetKingdom
|
||||
`015694ee414c802dc2ef1fa0120dfed610086e69` optional descriptor-map repair.
|
||||
Only the validator/evidence source pins change; exact parameter and transitive
|
||||
source identity remain enforced. Actual canonical producer, Executor and reducer
|
||||
suite passes 87 checks, including 14 new malformed-map integration cases; all
|
||||
four missing/changed modules still refuse. Review:
|
||||
`docs/reviews/2026-10-16-browser-producer-pin-refresh.md`. No adapter attestation,
|
||||
native acceptance, credential use or privileged dispatch. T03 remains progress.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"argv": [
|
||||
"python3",
|
||||
"-B",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-c",
|
||||
"pytest.ini",
|
||||
"tests/test_case_run.py",
|
||||
"tests/test_context_compare_probes.py"
|
||||
],
|
||||
"exit_code": 0,
|
||||
"stdout": "........................................................ [100%]\n56 passed in 2.18s\n",
|
||||
"stderr": "",
|
||||
"scope": "Changed concurrent source snapshot only; no canonical mutation or independent owner proof claimed."
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
........................................................ [100%]
|
||||
56 passed in 2.18s
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "comparison-review.json",
|
||||
"sha256": "sha256:2f1763a685f1ce1da2f42421d3a1cdd2d3a7253be1fb39c6b48194ff29f1ed85"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/docs/security-case-execution-kernel-v1.md",
|
||||
"sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/pytest.ini",
|
||||
"sha256": "sha256:363381771127e3479fe49773659cc2a8dc2b2f727459ebd057ac39e79e98a94e"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/src/warden/__init__.py",
|
||||
"sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/src/warden/case_inputs.py",
|
||||
"sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/src/warden/case_receipt_state.py",
|
||||
"sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/src/warden/case_run.py",
|
||||
"sha256": "sha256:0cc62a33dbb3b90276b291f77fa5d0a906f19296874d68636957763f576d1ae2"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/tests/test_case_original_context_consistency.py",
|
||||
"sha256": "sha256:8483bb5915701e8bd0749e85097e11076ffd539084ca68e9ec1c565babc157d1"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/tests/test_case_run.py",
|
||||
"sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/tests/test_context_compare_probes.py",
|
||||
"sha256": "sha256:ce64b52ba98d559f4e13f382f7c204a38d6779e0958f37e215b43ec4cfe5fc39"
|
||||
},
|
||||
{
|
||||
"path": "delta-fixture/workplans/WARDEN-WP-0044-security-case-follow-through.md",
|
||||
"sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1"
|
||||
},
|
||||
{
|
||||
"path": "delta-probe-results.json",
|
||||
"sha256": "sha256:b5cc1e7fc2f4bba33ce9256d4780149c5c3f8b850b7d666f5de15e582ebad774"
|
||||
},
|
||||
{
|
||||
"path": "delta-probes.stderr.txt",
|
||||
"sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "delta-probes.stdout.txt",
|
||||
"sha256": "sha256:3f7dcab165b24572fff8cfcb6aa0fbe74509c434f163475bf1fbe87c051c6b8d"
|
||||
},
|
||||
{
|
||||
"path": "minimal-delta-tests.json",
|
||||
"sha256": "sha256:0016cc87173f7485a94b2695c48c7df4fd699743b19b530164229bbc2c4ffd18"
|
||||
},
|
||||
{
|
||||
"path": "minimal-integration.patch",
|
||||
"sha256": "sha256:d058f7f5da015d9dc3fe406982e83f52d0c1b2acb33f0e41861a7bd0fdf8f02e"
|
||||
},
|
||||
{
|
||||
"path": "patch-check.json",
|
||||
"sha256": "sha256:7eb832e50261c31f86d01e7e45bfc30197647417086d927e3f72a397e09e11c4"
|
||||
},
|
||||
{
|
||||
"path": "probe-results.json",
|
||||
"sha256": "sha256:d6dc95750038ce5f26fa4bdb1f7954959270eb74b4bb90b0939677896480fec5"
|
||||
},
|
||||
{
|
||||
"path": "probes.stderr.txt",
|
||||
"sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "probes.stdout.txt",
|
||||
"sha256": "sha256:6bf1b3ccd554890c5dc39fed048dbb793b2b82ca9135cd4376303c55798d6139"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/docs/security-case-execution-kernel-v1.md",
|
||||
"sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/pytest.ini",
|
||||
"sha256": "sha256:363381771127e3479fe49773659cc2a8dc2b2f727459ebd057ac39e79e98a94e"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/src/warden/__init__.py",
|
||||
"sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/src/warden/case_inputs.py",
|
||||
"sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/src/warden/case_receipt_state.py",
|
||||
"sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/src/warden/case_run.py",
|
||||
"sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/tests/test_case_run.py",
|
||||
"sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/tests/test_context_compare_probes.py",
|
||||
"sha256": "sha256:85dd7b7ea80c5c2f29fd7125e067c1fa765c99741d15d0a543a238dac2bc524a"
|
||||
},
|
||||
{
|
||||
"path": "snapshot/workplans/WARDEN-WP-0044-security-case-follow-through.md",
|
||||
"sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1"
|
||||
},
|
||||
{
|
||||
"path": "source-state.json",
|
||||
"sha256": "sha256:1d0df4438451b1d0b688d66f09cf924f0b009ce326535c31af30dcdf2d466443"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"argv": [
|
||||
"python3",
|
||||
"-B",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-c",
|
||||
"pytest.ini",
|
||||
"tests/test_case_run.py",
|
||||
"tests/test_case_original_context_consistency.py"
|
||||
],
|
||||
"exit_code": 0,
|
||||
"stdout": "...................................................... [100%]\n54 passed in 1.98s\n",
|
||||
"stderr": "",
|
||||
"scope": "Exact modified temporary snapshot only; no canonical or native invocation."
|
||||
}
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
--- a/src/warden/case_run.py
|
||||
+++ b/src/warden/case_run.py
|
||||
@@ -189,7 +189,66 @@
|
||||
invocation: Invocation
|
||||
source: str
|
||||
|
||||
+ def validate(self) -> None:
|
||||
+ """Fixed bounded metadata only; tenant/realm are not authority claims.
|
||||
+
|
||||
+ Current admit leaves tenant/realm empty. Nonempty future coordinates need
|
||||
+ separate owner provenance; serializability alone must never supply it.
|
||||
+ """
|
||||
+ if (
|
||||
+ type(self.invocation) is not Invocation
|
||||
+ or type(self.invocation.pins) is not Pins
|
||||
+ ):
|
||||
+ raise Refused("exact original invocation required")
|
||||
+ i = self.invocation
|
||||
+ fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id)
|
||||
+ if (
|
||||
+ not all(
|
||||
+ type(value) is str and IDENTIFIER.fullmatch(value) for value in fields
|
||||
+ )
|
||||
+ or type(i.actor) is not str
|
||||
+ or i.actor not in ("human", "agent")
|
||||
+ or type(i.operation) is not str
|
||||
+ or i.operation not in ("run", "rollback")
|
||||
+ or not all(
|
||||
+ type(value) is str and (not value or IDENTIFIER.fullmatch(value))
|
||||
+ for value in (i.tenant, i.realm)
|
||||
+ )
|
||||
+ or type(self.source) is not str
|
||||
+ or not DIGEST.fullmatch(self.source)
|
||||
+ or type(self.binding) is not str
|
||||
+ or self.binding != f"{i.step}:{i.target}"
|
||||
+ ):
|
||||
+ raise Refused("invalid original intent metadata")
|
||||
+ if set(Invocation.__dataclass_fields__) != {
|
||||
+ "step",
|
||||
+ "owner",
|
||||
+ "target",
|
||||
+ "actor",
|
||||
+ "principal",
|
||||
+ "lane",
|
||||
+ "pins",
|
||||
+ "attempt_id",
|
||||
+ "operation",
|
||||
+ "tenant",
|
||||
+ "realm",
|
||||
+ }:
|
||||
+ raise Refused("original invocation field contract changed")
|
||||
+ if not all(
|
||||
+ type(v) is str and DIGEST.fullmatch(v)
|
||||
+ for v in (
|
||||
+ i.pins.case,
|
||||
+ i.pins.case_type,
|
||||
+ i.pins.adapter,
|
||||
+ i.pins.catalog,
|
||||
+ i.pins.acceptance,
|
||||
+ i.pins.run_sheet,
|
||||
+ )
|
||||
+ ):
|
||||
+ raise Refused("invalid original intent pins")
|
||||
+
|
||||
def payload(self) -> str:
|
||||
+ self.validate()
|
||||
return json.dumps(
|
||||
{"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source}
|
||||
| asdict(self.invocation),
|
||||
@@ -199,13 +258,44 @@
|
||||
|
||||
@classmethod
|
||||
def decode(cls, raw: str) -> OriginalIntent:
|
||||
- obj = json.loads(raw)
|
||||
- fields = {f for f in Invocation.__dataclass_fields__}
|
||||
- if set(obj) != fields | {"schema", "binding", "source"} or obj["schema"] != INTENT_SCHEMA:
|
||||
- raise Refused("unknown original intent schema")
|
||||
- inv = {k: obj[k] for k in fields}
|
||||
- inv["pins"] = Pins(**inv["pins"])
|
||||
- return cls(obj["binding"], Invocation(**inv), obj["source"])
|
||||
+ def unique(pairs):
|
||||
+ result = {}
|
||||
+ for key, value in pairs:
|
||||
+ if key in result:
|
||||
+ raise Refused("duplicate original intent field")
|
||||
+ result[key] = value
|
||||
+ return result
|
||||
+
|
||||
+ try:
|
||||
+ if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES:
|
||||
+ raise Refused("original intent input exceeds bound")
|
||||
+ obj = json.loads(raw, object_pairs_hook=unique)
|
||||
+ fields = {
|
||||
+ "step",
|
||||
+ "owner",
|
||||
+ "target",
|
||||
+ "actor",
|
||||
+ "principal",
|
||||
+ "lane",
|
||||
+ "pins",
|
||||
+ "attempt_id",
|
||||
+ "operation",
|
||||
+ "tenant",
|
||||
+ "realm",
|
||||
+ }
|
||||
+ if (
|
||||
+ type(obj) is not dict
|
||||
+ or set(obj) != fields | {"schema", "binding", "source"}
|
||||
+ or obj["schema"] != INTENT_SCHEMA
|
||||
+ ):
|
||||
+ raise Refused("unknown original intent schema")
|
||||
+ inv = {k: obj[k] for k in fields}
|
||||
+ inv["pins"] = Pins(**inv["pins"])
|
||||
+ result = cls(obj["binding"], Invocation(**inv), obj["source"])
|
||||
+ result.validate()
|
||||
+ return result
|
||||
+ except (TypeError, ValueError, KeyError):
|
||||
+ raise Refused("invalid original intent record") from None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -256,28 +346,76 @@
|
||||
"CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)"
|
||||
)
|
||||
|
||||
- def original_intent(self, binding: str, current: Invocation, source: str) -> OriginalIntent:
|
||||
+ def original_intent(
|
||||
+ self, binding: str, current: Invocation, source: str
|
||||
+ ) -> OriginalIntent:
|
||||
"""Latest original run intent for binding, refused unless it matches exactly.
|
||||
|
||||
Missing or mismatched records refuse; the step stays uncertain.
|
||||
"""
|
||||
row = self.db.execute(
|
||||
- "SELECT payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1",
|
||||
+ "SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1",
|
||||
(binding,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise Refused("original intent record missing; step stays uncertain")
|
||||
- intent = OriginalIntent.decode(row[0])
|
||||
+ intent = OriginalIntent.decode(row[1])
|
||||
o = intent.invocation
|
||||
+ if row[0] != o.attempt_id:
|
||||
+ raise Refused("original intent attempt identity differs")
|
||||
if intent.binding != binding or o.operation != "run":
|
||||
raise Refused("original intent binding mismatch")
|
||||
if o.pins != current.pins:
|
||||
raise Refused("original intent pins mismatch")
|
||||
if intent.source != source:
|
||||
raise Refused("original intent source mismatch")
|
||||
- for name in ("step", "owner", "target", "principal", "lane", "tenant", "realm"):
|
||||
+ for name in (
|
||||
+ "step",
|
||||
+ "owner",
|
||||
+ "target",
|
||||
+ "actor",
|
||||
+ "principal",
|
||||
+ "lane",
|
||||
+ "tenant",
|
||||
+ "realm",
|
||||
+ ):
|
||||
if getattr(o, name) != getattr(current, name):
|
||||
raise Refused(f"original intent {name} mismatch")
|
||||
+ # Match the persisted original running receipt. An orphan context or a
|
||||
+ # payload from another attempt cannot become the current lookup key.
|
||||
+ receipts = self.db.execute(
|
||||
+ "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?",
|
||||
+ (binding, MAX_HISTORY + 1),
|
||||
+ ).fetchall()
|
||||
+ if len(receipts) > MAX_HISTORY:
|
||||
+ raise Refused("original intent receipt history exceeds bound")
|
||||
+ matched = False
|
||||
+ try:
|
||||
+ for (raw,) in receipts:
|
||||
+ if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES:
|
||||
+ raise Refused("original receipt input exceeds bound")
|
||||
+ payload = json.loads(raw)
|
||||
+ if (
|
||||
+ payload.get("attempt_id") != o.attempt_id
|
||||
+ or payload.get("operation") != "run"
|
||||
+ ):
|
||||
+ continue
|
||||
+ last = payload.get("receipts", [])[-1]
|
||||
+ if last.get("outcome") != "running":
|
||||
+ continue
|
||||
+ if (
|
||||
+ payload.get("journal_schema") != "warden.case.step-journal.v1"
|
||||
+ or payload.get("step_id") != o.step
|
||||
+ or payload.get("owner") != o.owner
|
||||
+ or payload.get("pins") != asdict(o.pins)
|
||||
+ ):
|
||||
+ raise Refused("original running receipt binding differs")
|
||||
+ matched = True
|
||||
+ break
|
||||
+ except (TypeError, ValueError, KeyError, IndexError, AttributeError):
|
||||
+ raise Refused("invalid original running receipt") from None
|
||||
+ if not matched:
|
||||
+ raise Refused("original running receipt missing; no backfill")
|
||||
return intent
|
||||
|
||||
@contextmanager
|
||||
@@ -355,6 +493,23 @@
|
||||
invocation: Invocation | None = None,
|
||||
intent: OriginalIntent | None = None,
|
||||
):
|
||||
+ if intent is not None:
|
||||
+ if type(intent) is not OriginalIntent:
|
||||
+ raise Refused("exact original intent required")
|
||||
+ intent.validate()
|
||||
+ expected = (
|
||||
+ Outcome.RUNNING
|
||||
+ if intent.invocation.operation == "run"
|
||||
+ else Outcome.ROLLBACK_RUNNING
|
||||
+ )
|
||||
+ if (
|
||||
+ intent.binding != binding
|
||||
+ or intent.invocation != invocation
|
||||
+ or intent.invocation.pins != state.pins
|
||||
+ or not state.last
|
||||
+ or state.last.outcome is not expected
|
||||
+ ):
|
||||
+ raise Refused("original intent and running receipt differ")
|
||||
self.db.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
if intent is not None:
|
||||
--- /dev/null
|
||||
+++ b/tests/test_case_original_context_consistency.py
|
||||
@@ -0,0 +1,198 @@
|
||||
+"""Original-intent consistency checks with generated metadata only.
|
||||
+
|
||||
+Reuse the existing synthetic owner rig. These checks do not provide a native
|
||||
+holder or independently authenticate original creation from local SQL/JSON.
|
||||
+"""
|
||||
+
|
||||
+from dataclasses import asdict, replace
|
||||
+import json
|
||||
+from pathlib import Path
|
||||
+import sqlite3
|
||||
+import subprocess
|
||||
+import sys
|
||||
+import pytest
|
||||
+from test_case_run import rig as rig
|
||||
+from warden.case_receipt_state import Outcome, Refused
|
||||
+from warden.case_run import OriginalIntent
|
||||
+
|
||||
+
|
||||
+def interrupted(rig):
|
||||
+ executor, args, calls, sources, kwargs = rig
|
||||
+ old = executor.adapters[args[0].adapter]
|
||||
+ seen = []
|
||||
+
|
||||
+ def lost(inv):
|
||||
+ calls.append(inv)
|
||||
+ raise TimeoutError("generated lost response")
|
||||
+
|
||||
+ def original(ctx):
|
||||
+ seen.append(ctx)
|
||||
+ return old.reconcile(ctx.attempt)
|
||||
+
|
||||
+ executor.adapters[old.reference] = replace(
|
||||
+ old, invoke=lost, reconcile=None, reconcile_original=original
|
||||
+ )
|
||||
+ assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
+ return executor, args, calls, old, seen
|
||||
+
|
||||
+
|
||||
+def alter_payload(executor, **changes):
|
||||
+ row = executor.journal.db.execute(
|
||||
+ "SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1"
|
||||
+ ).fetchone()
|
||||
+ obj = json.loads(row[1])
|
||||
+ obj.update(changes)
|
||||
+ executor.journal.db.execute(
|
||||
+ "UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0])
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_original_actor_mismatch_is_refused(rig):
|
||||
+ executor, args, calls, old, _ = interrupted(rig)
|
||||
+ alter_payload(executor, actor="agent")
|
||||
+ with pytest.raises(Refused):
|
||||
+ executor.journal.original_intent(
|
||||
+ "mint-secret:fixture-client", args[1], old.source
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig):
|
||||
+ executor, args, calls, old, _ = interrupted(rig)
|
||||
+ actual = calls[0].attempt_id
|
||||
+ alter_payload(executor, attempt_id="attempt-unrelated")
|
||||
+ with pytest.raises(Refused):
|
||||
+ executor.journal.original_intent(
|
||||
+ "mint-secret:fixture-client", args[1], old.source
|
||||
+ )
|
||||
+ assert (
|
||||
+ executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0]
|
||||
+ == actual
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_orphan_intent_without_original_running_receipt_is_refused(rig):
|
||||
+ executor, args, calls, old, _ = interrupted(rig)
|
||||
+ executor.journal.db.execute(
|
||||
+ "DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)"
|
||||
+ )
|
||||
+ assert (
|
||||
+ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
+ is Outcome.UNCERTAIN
|
||||
+ )
|
||||
+ with pytest.raises(Refused):
|
||||
+ executor.journal.original_intent(
|
||||
+ "mint-secret:fixture-client", args[1], old.source
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig):
|
||||
+ _, args, _, _, _ = rig
|
||||
+ inv = replace(
|
||||
+ args[1],
|
||||
+ target={"unexpected": "synthetic-owner-payload"},
|
||||
+ operation="run",
|
||||
+ attempt_id="attempt-fixture",
|
||||
+ )
|
||||
+ record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64)
|
||||
+ with pytest.raises(Refused):
|
||||
+ OriginalIntent.decode(record.payload())
|
||||
+ # This is a direct serialization API gap, not an observed normal-admit leak.
|
||||
+
|
||||
+ # Owner trust is the existing synthetic fixture; this proves call shape only.
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize(
|
||||
+ "field,value",
|
||||
+ [
|
||||
+ ("principal", "another-person"),
|
||||
+ ("tenant", "another-tenant"),
|
||||
+ ("realm", "another-realm"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch(
|
||||
+ rig, field, value
|
||||
+):
|
||||
+ executor, args, _, old, _ = interrupted(rig)
|
||||
+ alter_payload(executor, **{field: value})
|
||||
+ with pytest.raises(Refused):
|
||||
+ executor.journal.original_intent(
|
||||
+ "mint-secret:fixture-client", args[1], old.source
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig):
|
||||
+ executor, args, calls, _, _ = rig
|
||||
+ executor.journal.db.execute(
|
||||
+ "CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END"
|
||||
+ )
|
||||
+ with pytest.raises(sqlite3.IntegrityError):
|
||||
+ executor.run(*args)
|
||||
+ assert not calls
|
||||
+ assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == (
|
||||
+ 0,
|
||||
+ )
|
||||
+ assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == (
|
||||
+ 0,
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_existing_missing_original_refuses_before_callback(rig):
|
||||
+ executor, args, _, _, seen = interrupted(rig)
|
||||
+ executor.journal.db.execute("DELETE FROM intents")
|
||||
+ with pytest.raises(Refused):
|
||||
+ executor.reconcile(*args)
|
||||
+ assert not seen
|
||||
+ assert (
|
||||
+ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
+ is Outcome.UNCERTAIN
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_existing_current_parent_revocation_remains_mandatory(rig):
|
||||
+ executor, args, _, _, _ = interrupted(rig)
|
||||
+ old = executor.evidence_verifier
|
||||
+
|
||||
+ def refuse_session(purpose, source, evidence):
|
||||
+ return (
|
||||
+ False
|
||||
+ if purpose.startswith("session-revocation:")
|
||||
+ else old(purpose, source, evidence)
|
||||
+ )
|
||||
+
|
||||
+ executor.evidence_verifier = refuse_session
|
||||
+ with pytest.raises(Refused):
|
||||
+ executor.reconcile(*args)
|
||||
+ assert (
|
||||
+ executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
+ is Outcome.UNCERTAIN
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def test_existing_intent_survives_actual_distinct_interpreter(rig):
|
||||
+ executor, args, calls, old, _ = interrupted(rig)
|
||||
+ code = """import json,sys
|
||||
+from pathlib import Path
|
||||
+from warden.case_run import Journal,Invocation
|
||||
+from warden.case_receipt_state import Pins
|
||||
+obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1]))
|
||||
+with j.locked():
|
||||
+ i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2])
|
||||
+ print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation}))
|
||||
+"""
|
||||
+ result = subprocess.run(
|
||||
+ [sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source],
|
||||
+ input=json.dumps(asdict(args[1])),
|
||||
+ text=True,
|
||||
+ capture_output=True,
|
||||
+ timeout=15,
|
||||
+ env={
|
||||
+ "PATH": "/usr/bin:/bin",
|
||||
+ "PYTHONPATH": str(Path(__file__).parents[1] / "src"),
|
||||
+ "PYTHONDONTWRITEBYTECODE": "1",
|
||||
+ },
|
||||
+ check=True,
|
||||
+ )
|
||||
+ assert json.loads(result.stdout) == {
|
||||
+ "attempt": calls[0].attempt_id,
|
||||
+ "operation": "run",
|
||||
+ }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"base": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"exit_code": 0,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"current_source_unchanged": true
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"observed_at": "2026-10-17T05:53:57.282196+00:00",
|
||||
"argv": [
|
||||
"python3",
|
||||
"-B",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-c",
|
||||
"pytest.ini",
|
||||
"tests/test_context_compare_probes.py"
|
||||
],
|
||||
"exit_code": 0,
|
||||
"stdout": "............. [100%]\n13 passed in 0.71s\n",
|
||||
"stderr": "",
|
||||
"scope": "Only exact snapshot; no tests against canonical user tree. Passing counterexample probes reproduce documented source gaps, not security/native acceptance."
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
............. [100%]
|
||||
13 passed in 0.71s
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# Closed case execution kernel v1 — 2026-10-11
|
||||
|
||||
Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal
|
||||
at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
|
||||
was accepted through memo `infd-20261010-security-case-execution`, version 1,
|
||||
approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition
|
||||
disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original
|
||||
approved proposal remains byte-identical. That decision accepts source design;
|
||||
it grants no execution session, credential or deployment authority.
|
||||
|
||||
`src/warden/case_run.py` advances the pure reducer into an executable injected
|
||||
owner harness with private durable receipts. Descriptor version
|
||||
`warden.case.execution.v1` is a closed local implementation seam, not an accepted
|
||||
NetKingdom type schema. It contains fixed owner/adapter/source digest, actor,
|
||||
lane, target parameter, effect, reviewed created-object identifiers declared
|
||||
retry safety and explicit required revocation scopes (parent-session and broker-grant).
|
||||
A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused.
|
||||
An acknowledged type must additionally pin the descriptor bytes under its
|
||||
`execution_descriptors` map and independent owner evidence must authenticate
|
||||
that exact machine-seam revision. Existing descriptive types lack this map and
|
||||
remain plan-only. Their acknowledged labels do not authorize this new seam.
|
||||
|
||||
There are no installed owner adapters, native provenance verifier or dispatch CLI
|
||||
in this change. Callables are injected only through a fixed adapter registry,
|
||||
with immutable owner reference/source pins. A real integration must supply
|
||||
independent owner/native verification or explicit attended manual evidence;
|
||||
a bare JSON receipt identifier is insufficient. Fixture verifiers in tests
|
||||
are deliberately fake and attest no native case. Caller sessions are independently
|
||||
verified for exact principal/actor/lane at admission and again before dispatch;
|
||||
input actor labels alone are never sufficient. Agent mint is refused, including
|
||||
when WARDEN_AGENT_ID appears after initial human admission.
|
||||
|
||||
Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference
|
||||
and run-sheet identity. Actual ops-mason materialization evidence is required
|
||||
against the case source, separately from its status label. Case parameters supply
|
||||
an exact metadata target. Source/session changes cannot inherit journal success.
|
||||
Owner results have a closed metadata shape, result evidence fingerprint and
|
||||
separate independently authenticated grant/parent-session revocation evidence
|
||||
fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and
|
||||
created objects outside the reviewed scope become uncertain, never success.
|
||||
Python and OS stdout/stderr, including inherited child-process output, are
|
||||
discarded without buffering or retention under a serialized fd containment
|
||||
context. Independent provenance verification uses the same containment and
|
||||
sanitizes exceptions. All source inputs are capped at 256 KiB before parsing.
|
||||
Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other
|
||||
owner interruptions are rethrown with a fixed message while retaining running
|
||||
intent for crash recovery.
|
||||
|
||||
The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks
|
||||
and insecure modes, uses SQLite FULL synchronization and commits running intent
|
||||
before calling the adapter. A separate process lock prevents concurrent mutation.
|
||||
A crash between dispatch and final receipt leaves running; the next lock holder
|
||||
persists uncertain before refusing replay. Both intent and outcome remain in
|
||||
history. Each persisted attempt has a unique identity bound to all exact pins,
|
||||
operation (run/reconcile/rollback), sequence and nonce. Adapter results and both
|
||||
revocation proof purposes carry that identity. Prior-attempt evidence cannot
|
||||
verify a safe retry, even if a caller rewrites result labels. Descriptor-created
|
||||
objects are capped at 32 and histories at 128 transitions, with dispatch stopped
|
||||
before archival is required. A throw or lost response is not failed-before-change. A safe retry needs
|
||||
verified failed-before-change, unchanged pins, descriptor retry-safe and explicit
|
||||
retry. Success and no-change cannot replay.
|
||||
|
||||
Reconciliation is an explicit fixed owner callback, never the mutation callback.
|
||||
It may verify an already-present native CAS result as no-change/success, or prove
|
||||
failed-before-change; it never tells Warden to regenerate a credential. Uncertain
|
||||
or failed-after-change requires this owner-native inspection before any retry.
|
||||
Owner proof and revocation remain mandatory. Rollback visits only objects in a
|
||||
verified creation receipt, in reverse order; no-change has no undo and shared
|
||||
objects are excluded. Unknown/lost undo blocks another rollback.
|
||||
|
||||
The kernel operates individual admitted steps. Full ordered run-sheet orchestration,
|
||||
verification inventory, contained login integration, native owner adapters and
|
||||
CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is
|
||||
implemented. Current Forgejo source is not materialized and has no acknowledged
|
||||
machine descriptors. Its first native run remains an acceptance requirement,
|
||||
with real materialization, separately authenticated broker-grant and parent-session
|
||||
revocation receipts, session/owner receipts and consumer enforcement. This
|
||||
implementation neither promotes a type nor changes any case status.
|
||||
|
||||
Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open
|
||||
an independent SQLite connection inside dispatch to prove running was committed,
|
||||
crash after mutation and reopen a fresh journal to prove no duplicate invocation,
|
||||
lose a response then reconcile without mint, require explicit retry, check exact
|
||||
pins/session expiry, withhold malicious output and unconfirmed revocation, and
|
||||
prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and
|
||||
checks pass. No live token or target action occurred.
|
||||
|
||||
## Browser input projection — 2026-10-16 source preparation
|
||||
|
||||
An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser
|
||||
cases before admission. It calls the real NetKingdom type/case validator over
|
||||
verified producer bytes in a disposable Python process (`-I`, empty environment,
|
||||
timeout, no inherited output). The validator, composer, capability contract and lazy evidence
|
||||
modules have exact source pins; missing or changed code refuses. There is no
|
||||
automatic discovery or download. A producer upgrade needs reviewed pin changes.
|
||||
|
||||
Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest
|
||||
of each complete parameter definition list prevents reclassifying secrets as
|
||||
metadata, loosening constraints, or adding fields under a recognized version.
|
||||
The complete case passes producer validation, including required fields,
|
||||
constraints and credential-shape checks. Explicit fixed/default overrides,
|
||||
unknown fields and secret references refuse. Security-sensitive browser metadata
|
||||
can therefore be present without being passed to an owner adapter: the only
|
||||
projection is its descriptor's exact public/operational target identifier.
|
||||
No raw parameter values are added to invocations or persisted receipts.
|
||||
|
||||
`require_implemented=False` validates the acknowledged type's source, not its
|
||||
execution authority. Existing descriptor/type/source/catalog/caller provenance
|
||||
gates stay mandatory. Neither historical browser type contains the required
|
||||
exact machine-descriptor map, and neither is promoted by this change. There are
|
||||
still no installed native owner adapters, authenticated provenance verifier,
|
||||
contained session/ordered run orchestration or live CLI dispatch. A journal digest
|
||||
is a fingerprint and cannot authenticate native evidence. Completion still needs
|
||||
owner-admitted descriptors and actual structural, mint/delivery, consumer and
|
||||
revocation/rollback evidence. Local fixture success is not native acceptance.
|
||||
|
||||
Tests use the actual producer checkout (override sibling location with
|
||||
`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected
|
||||
test-only owner adapter/evidence verifier. Their descriptor extensions and case
|
||||
materialized labels exist only in memory. Canonical case/type bytes stay intact.
|
||||
|
||||
Producer integration refinement: the accepted NetKingdom browser-case patch
|
||||
`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05`
|
||||
is now the tested producer revision. Its evidence module is imported lazily only
|
||||
by `check-evidence`, outside this admission path; Warden nevertheless pins and
|
||||
copies it with the complete reviewed producer set. Missing or modified bytes in
|
||||
any of the four modules refuse admission. The test covers every dependency.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
pythonpath = src
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""OpsWarden — SSH CA and certificate lifecycle manager."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
"""Reviewed browser metadata projection; no native trust or command dispatch.
|
||||
|
||||
The producer validator and all reviewed producer modules are pinned below.
|
||||
Their verified bytes run in a disposable tree, without caller environment or
|
||||
ambient Python imports. Type parameters must match the exact reviewed version.
|
||||
Machine descriptor extensions still require Executor's independent admission.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from .case_receipt_state import Refused
|
||||
|
||||
# Reviewed producer: net-kingdom 015694ee414c802dc2ef1fa0120dfed610086e69.
|
||||
# Review: docs/reviews/2026-10-16-browser-producer-pin-refresh.md
|
||||
SOURCE_PINS = {
|
||||
"tools/security-case-types/security_case_evidence.py": "da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75",
|
||||
"tools/security-case-types/security_case_validator.py": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b",
|
||||
"tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f",
|
||||
"tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d",
|
||||
}
|
||||
PARAMETER_PINS = {
|
||||
"1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c",
|
||||
"1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6",
|
||||
}
|
||||
|
||||
_VALIDATE = """
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types'))
|
||||
import security_case_validator as v
|
||||
case, typ = json.load(sys.stdin)
|
||||
issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False)
|
||||
print('valid' if not v.has_errors(issues) else 'invalid')
|
||||
"""
|
||||
|
||||
|
||||
class BrowserInputs:
|
||||
"""Explicit owner configuration, never inferred from a case-supplied path."""
|
||||
|
||||
def __init__(self, netkingdom_root: Path):
|
||||
self.root = Path(netkingdom_root)
|
||||
|
||||
def target(self, case: dict, typ: dict, target_parameter: str) -> str:
|
||||
try:
|
||||
spec = typ["spec"]
|
||||
parameters = spec["parameters"]
|
||||
fingerprint = hashlib.sha256(
|
||||
json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
if (
|
||||
spec["id"] != "oidc-browser-relying-party"
|
||||
or PARAMETER_PINS.get(spec["version"]) != fingerprint
|
||||
):
|
||||
raise ValueError
|
||||
declared = {item["name"]: item for item in parameters}
|
||||
definition = declared.get(target_parameter, {})
|
||||
if (
|
||||
definition.get("sensitivity") not in ("public", "operational")
|
||||
or definition.get("case_settable") is not True
|
||||
):
|
||||
raise ValueError
|
||||
# No secret references or newly classified metadata are grandfathered.
|
||||
for name in case["spec"]["parameters"]:
|
||||
item = declared.get(name, {})
|
||||
if item.get("case_settable") is not True or item.get(
|
||||
"sensitivity"
|
||||
) not in ("public", "operational", "security_sensitive"):
|
||||
raise ValueError
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="warden-case-validator-"
|
||||
) as directory:
|
||||
for relative, expected in SOURCE_PINS.items():
|
||||
source = (self.root / relative).read_bytes()
|
||||
if hashlib.sha256(source).hexdigest() != expected:
|
||||
raise ValueError
|
||||
dest = Path(directory) / relative
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.write_bytes(source)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-I", "-c", _VALIDATE, directory],
|
||||
input=json.dumps([case, typ]),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
env={},
|
||||
timeout=10,
|
||||
check=True,
|
||||
)
|
||||
if result.stdout != "valid\n":
|
||||
raise ValueError
|
||||
return case["spec"]["parameters"][target_parameter]
|
||||
except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError):
|
||||
raise Refused("browser metadata failed exact-version validation") from None
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
"""Proposed, pure case receipt reducer for WARDEN-WP-0044-T03.
|
||||
|
||||
This prototype executes nothing and implements no accepted owner adapter API.
|
||||
Callers MUST durably persist a returned running receipt before a side effect.
|
||||
Only metadata identifiers/digests enter this model; no command output is accepted.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import Enum
|
||||
import re
|
||||
|
||||
_ID = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}\Z")
|
||||
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
||||
|
||||
|
||||
class Refused(ValueError):
|
||||
"""A transition requires an independent owner reconciliation or new review."""
|
||||
|
||||
|
||||
class Outcome(str, Enum):
|
||||
RUNNING = "running"
|
||||
SUCCESS = "verified-success"
|
||||
NO_CHANGE = "verified-no-change"
|
||||
FAILED_BEFORE = "failed-before-change"
|
||||
FAILED_AFTER = "failed-after-change"
|
||||
UNCERTAIN = "uncertain"
|
||||
ROLLBACK_RUNNING = "rollback-running"
|
||||
ROLLED_BACK = "verified-rolled-back"
|
||||
ROLLBACK_UNCERTAIN = "rollback-uncertain"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pins:
|
||||
"""Exact reviewed case/type/adapter/catalog/acceptance/run-sheet digests."""
|
||||
|
||||
case: str
|
||||
case_type: str
|
||||
adapter: str
|
||||
catalog: str
|
||||
acceptance: str
|
||||
run_sheet: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not all(_DIGEST.fullmatch(value) for value in vars(self).values()):
|
||||
raise Refused("pins require sha256 digests")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Receipt:
|
||||
attempt: int
|
||||
outcome: Outcome
|
||||
created: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StepState:
|
||||
step_id: str
|
||||
owner: str
|
||||
pins: Pins
|
||||
allowed_created: tuple[str, ...] = ()
|
||||
retry_safe: bool = False
|
||||
receipts: tuple[Receipt, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not all(_ID.fullmatch(v) for v in (self.step_id, self.owner, *self.allowed_created)):
|
||||
raise Refused("only metadata identifiers are supported")
|
||||
if len(set(self.allowed_created)) != len(self.allowed_created):
|
||||
raise Refused("duplicate reviewed object identifier")
|
||||
|
||||
@property
|
||||
def last(self) -> Receipt | None:
|
||||
return self.receipts[-1] if self.receipts else None
|
||||
|
||||
|
||||
def _same_pins(state: StepState, pins: Pins) -> None:
|
||||
if state.pins != pins:
|
||||
raise Refused("pins changed; a newly reviewed sheet is required")
|
||||
|
||||
|
||||
def _append(state: StepState, outcome: Outcome, created: tuple[str, ...] = ()) -> StepState:
|
||||
receipt = Receipt(len(state.receipts) + 1, outcome, created)
|
||||
return replace(state, receipts=(*state.receipts, receipt))
|
||||
|
||||
|
||||
def begin(state: StepState, pins: Pins, *, retry: bool = False) -> StepState:
|
||||
"""Return intent to dispatch. Persist before calling an owner adapter."""
|
||||
_same_pins(state, pins)
|
||||
last = state.last
|
||||
if last is None:
|
||||
if retry:
|
||||
raise Refused("no failed attempt to retry")
|
||||
elif not (last.outcome is Outcome.FAILED_BEFORE and state.retry_safe and retry):
|
||||
raise Refused("step cannot dispatch or replay without owner reconciliation")
|
||||
return _append(state, Outcome.RUNNING)
|
||||
|
||||
|
||||
def finish(state: StepState, pins: Pins, outcome: Outcome,
|
||||
*, created: tuple[str, ...] = ()) -> StepState:
|
||||
"""Accept only an adapter's verified, closed metadata result, never output."""
|
||||
_same_pins(state, pins)
|
||||
if not state.last or state.last.outcome is not Outcome.RUNNING:
|
||||
raise Refused("no running side effect")
|
||||
if not isinstance(outcome, Outcome) or outcome not in (Outcome.SUCCESS, Outcome.NO_CHANGE, Outcome.FAILED_BEFORE,
|
||||
Outcome.FAILED_AFTER, Outcome.UNCERTAIN):
|
||||
raise Refused("unsupported adapter result")
|
||||
if len(set(created)) != len(created) or not set(created) <= set(state.allowed_created):
|
||||
raise Refused("created objects are outside the reviewed case")
|
||||
if created and outcome not in (Outcome.SUCCESS, Outcome.FAILED_AFTER):
|
||||
raise Refused("this result cannot claim created objects")
|
||||
return _append(state, outcome, created)
|
||||
|
||||
|
||||
def recover(state: StepState) -> StepState:
|
||||
"""Lost result after persisted dispatch is uncertain, never safe to retry."""
|
||||
if state.last and state.last.outcome is Outcome.RUNNING:
|
||||
return _append(state, Outcome.UNCERTAIN)
|
||||
if state.last and state.last.outcome is Outcome.ROLLBACK_RUNNING:
|
||||
return _append(state, Outcome.ROLLBACK_UNCERTAIN, state.last.created)
|
||||
return state
|
||||
|
||||
|
||||
def rollback_objects(state: StepState, pins: Pins) -> tuple[str, ...]:
|
||||
"""Reviewed identifiers only, reverse created order; no shared-object undo."""
|
||||
_same_pins(state, pins)
|
||||
last = state.last
|
||||
if last is None or last.outcome is Outcome.NO_CHANGE:
|
||||
return ()
|
||||
if last.outcome is not Outcome.SUCCESS:
|
||||
raise Refused("rollback needs a verified successful creation receipt")
|
||||
return tuple(reversed(last.created))
|
||||
|
||||
|
||||
def begin_rollback(state: StepState, pins: Pins) -> StepState:
|
||||
objects = rollback_objects(state, pins)
|
||||
if not objects:
|
||||
raise Refused("no case-created objects to undo")
|
||||
return _append(state, Outcome.ROLLBACK_RUNNING, objects)
|
||||
|
||||
|
||||
def finish_rollback(state: StepState, pins: Pins, *, verified: bool) -> StepState:
|
||||
_same_pins(state, pins)
|
||||
if not state.last or state.last.outcome is not Outcome.ROLLBACK_RUNNING:
|
||||
raise Refused("no running rollback")
|
||||
outcome = Outcome.ROLLED_BACK if verified else Outcome.ROLLBACK_UNCERTAIN
|
||||
return _append(state, outcome, state.last.created)
|
||||
|
|
@ -0,0 +1,826 @@
|
|||
"""Closed local execution kernel v1; no installed adapters or live CLI dispatch.
|
||||
|
||||
An owner must acknowledge each descriptor and provide an independent provenance
|
||||
verifier. Descriptive case types remain plan-only. Nothing executes shell prose.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
||||
from dataclasses import asdict, dataclass, replace
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
import threading
|
||||
import uuid
|
||||
from typing import Callable
|
||||
|
||||
import yaml
|
||||
from .case_inputs import BrowserInputs
|
||||
from .case_receipt_state import (
|
||||
Outcome,
|
||||
Pins,
|
||||
Receipt,
|
||||
Refused,
|
||||
StepState,
|
||||
begin,
|
||||
finish,
|
||||
recover,
|
||||
begin_rollback,
|
||||
finish_rollback,
|
||||
)
|
||||
|
||||
IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z")
|
||||
MAX_SOURCE_BYTES = 262144
|
||||
MAX_HISTORY = 128
|
||||
OUTPUT_LOCK = threading.RLock()
|
||||
|
||||
|
||||
def bounded_source(value: bytes):
|
||||
if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES:
|
||||
raise Refused("source exceeds bounded input contract")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def contained_output():
|
||||
"""Discard Python and inherited OS stdout/stderr; serialize process fd changes."""
|
||||
with OUTPUT_LOCK:
|
||||
saved = []
|
||||
null = os.open(os.devnull, os.O_WRONLY)
|
||||
try:
|
||||
for target in (1, 2):
|
||||
saved.append((target, os.dup(target)))
|
||||
os.dup2(null, target)
|
||||
with redirect_stdout(_Discard()), redirect_stderr(_Discard()):
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
raise Refused("owner callback failed") from None
|
||||
except BaseException:
|
||||
raise OwnerInterrupted() from None
|
||||
finally:
|
||||
for target, original in saved:
|
||||
os.dup2(original, target)
|
||||
os.close(original)
|
||||
os.close(null)
|
||||
|
||||
|
||||
DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
||||
|
||||
|
||||
def digest(value: bytes) -> str:
|
||||
return "sha256:" + hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Descriptor:
|
||||
version: str
|
||||
step: str
|
||||
owner: str
|
||||
adapter: str
|
||||
adapter_source: str
|
||||
actor: str
|
||||
lane: str
|
||||
target_parameter: str
|
||||
effect: str
|
||||
allowed_created: tuple[str, ...]
|
||||
retry_safe: bool
|
||||
revocation_scopes: tuple[str, ...]
|
||||
|
||||
@classmethod
|
||||
def decode(cls, source: bytes) -> Descriptor:
|
||||
bounded_source(source)
|
||||
try:
|
||||
obj = json.loads(source)
|
||||
if set(obj) != set(cls.__dataclass_fields__):
|
||||
raise ValueError
|
||||
if not isinstance(obj["allowed_created"], list):
|
||||
raise ValueError
|
||||
obj["allowed_created"] = tuple(obj["allowed_created"])
|
||||
if not isinstance(obj["revocation_scopes"], list):
|
||||
raise ValueError
|
||||
obj["revocation_scopes"] = tuple(obj["revocation_scopes"])
|
||||
result = cls(**obj)
|
||||
identifiers = (
|
||||
result.step,
|
||||
result.owner,
|
||||
result.adapter,
|
||||
result.lane,
|
||||
result.target_parameter,
|
||||
*result.allowed_created,
|
||||
)
|
||||
if result.version != "warden.case.execution.v1" or not all(
|
||||
isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers
|
||||
):
|
||||
raise ValueError
|
||||
if not DIGEST.fullmatch(result.adapter_source) or result.actor not in (
|
||||
"human",
|
||||
"agent",
|
||||
):
|
||||
raise ValueError
|
||||
if (
|
||||
result.effect not in ("read", "change", "mint")
|
||||
or type(result.retry_safe) is not bool
|
||||
):
|
||||
raise ValueError
|
||||
scopes = set(result.revocation_scopes)
|
||||
if (
|
||||
len(scopes) != len(result.revocation_scopes)
|
||||
or not scopes <= {"parent-session", "broker-grant"}
|
||||
or "parent-session" not in scopes
|
||||
):
|
||||
raise ValueError
|
||||
if result.effect == "mint" and "broker-grant" not in scopes:
|
||||
raise ValueError
|
||||
if len(result.allowed_created) > 32:
|
||||
raise ValueError
|
||||
if len(set(result.allowed_created)) != len(result.allowed_created):
|
||||
raise ValueError
|
||||
return result
|
||||
except (ValueError, TypeError, KeyError):
|
||||
raise Refused("invalid closed execution descriptor") from None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OwnerResult:
|
||||
outcome: Outcome
|
||||
evidence: str
|
||||
created: tuple[str, ...] = ()
|
||||
revoked: bool = False
|
||||
session_revocation_evidence: str | None = None
|
||||
grant_revocation_evidence: str | None = None
|
||||
attempt_id: str = ""
|
||||
operation: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Invocation:
|
||||
step: str
|
||||
owner: str
|
||||
target: str
|
||||
actor: str
|
||||
principal: str
|
||||
lane: str
|
||||
pins: Pins
|
||||
attempt_id: str = ""
|
||||
operation: str = "admission"
|
||||
tenant: str = ""
|
||||
realm: str = ""
|
||||
|
||||
|
||||
INTENT_SCHEMA = "warden.case.original-intent.v1"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OriginalIntent:
|
||||
"""Durable record of the original mutating dispatch, written before dispatch.
|
||||
|
||||
Binds the original Invocation (attempt, step, owner, target, actor, principal,
|
||||
lane), all pins, the admitted adapter source and the tenant/realm provenance
|
||||
the caller was admitted with. It is evidence of what Warden dispatched, not
|
||||
owner proof of creation: owners still authenticate created-only provenance.
|
||||
"""
|
||||
|
||||
binding: str
|
||||
invocation: Invocation
|
||||
source: str
|
||||
|
||||
def payload(self) -> str:
|
||||
return json.dumps(
|
||||
{"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source}
|
||||
| asdict(self.invocation),
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, raw: str) -> OriginalIntent:
|
||||
obj = json.loads(raw)
|
||||
fields = {f for f in Invocation.__dataclass_fields__}
|
||||
if set(obj) != fields | {"schema", "binding", "source"} or obj["schema"] != INTENT_SCHEMA:
|
||||
raise Refused("unknown original intent schema")
|
||||
inv = {k: obj[k] for k in fields}
|
||||
inv["pins"] = Pins(**inv["pins"])
|
||||
return cls(obj["binding"], Invocation(**inv), obj["source"])
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReconcileContext:
|
||||
"""Original intent and the new reconciliation attempt, kept separate."""
|
||||
|
||||
original: OriginalIntent
|
||||
attempt: Invocation
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Adapter:
|
||||
reference: str
|
||||
source: str
|
||||
invoke: Callable[[Invocation], OwnerResult]
|
||||
verify: Callable[[Invocation, OwnerResult], bool]
|
||||
rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None
|
||||
reconcile: Callable[[Invocation], OwnerResult] | None = None
|
||||
reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None
|
||||
|
||||
|
||||
class Journal:
|
||||
"""Private SQLite journal plus process lock; transaction commits before dispatch."""
|
||||
|
||||
def __init__(self, directory: Path):
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if (
|
||||
directory.is_symlink()
|
||||
or directory.stat().st_uid != os.getuid()
|
||||
or directory.stat().st_mode & 0o077
|
||||
):
|
||||
raise Refused("journal directory is not private")
|
||||
self.directory = directory
|
||||
for name in ("journal.sqlite", "execution.lock"):
|
||||
path = directory / name
|
||||
fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077:
|
||||
raise Refused("journal file is not private")
|
||||
finally:
|
||||
os.close(fd)
|
||||
self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None)
|
||||
self.db.execute("PRAGMA synchronous=FULL")
|
||||
self.db.execute(
|
||||
"CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)"
|
||||
)
|
||||
self.db.execute(
|
||||
"CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)"
|
||||
)
|
||||
|
||||
def original_intent(self, binding: str, current: Invocation, source: str) -> OriginalIntent:
|
||||
"""Latest original run intent for binding, refused unless it matches exactly.
|
||||
|
||||
Missing or mismatched records refuse; the step stays uncertain.
|
||||
"""
|
||||
row = self.db.execute(
|
||||
"SELECT payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1",
|
||||
(binding,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise Refused("original intent record missing; step stays uncertain")
|
||||
intent = OriginalIntent.decode(row[0])
|
||||
o = intent.invocation
|
||||
if intent.binding != binding or o.operation != "run":
|
||||
raise Refused("original intent binding mismatch")
|
||||
if o.pins != current.pins:
|
||||
raise Refused("original intent pins mismatch")
|
||||
if intent.source != source:
|
||||
raise Refused("original intent source mismatch")
|
||||
for name in ("step", "owner", "target", "principal", "lane", "tenant", "realm"):
|
||||
if getattr(o, name) != getattr(current, name):
|
||||
raise Refused(f"original intent {name} mismatch")
|
||||
return intent
|
||||
|
||||
@contextmanager
|
||||
def locked(self):
|
||||
with (self.directory / "execution.lock").open("rb") as fh:
|
||||
try:
|
||||
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
raise Refused("case execution already active") from None
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(fh, fcntl.LOCK_UN)
|
||||
|
||||
def load(self, binding: str, initial: StepState) -> StepState:
|
||||
row = self.db.execute(
|
||||
"SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1",
|
||||
(binding,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return initial
|
||||
obj = json.loads(row[0])
|
||||
expected = {
|
||||
"step_id",
|
||||
"owner",
|
||||
"pins",
|
||||
"allowed_created",
|
||||
"retry_safe",
|
||||
"receipts",
|
||||
"journal_schema",
|
||||
"evidence_digest",
|
||||
"session_revoked",
|
||||
"session_revocation_digest",
|
||||
"grant_revocation_digest",
|
||||
"attempt_id",
|
||||
"operation",
|
||||
}
|
||||
if (
|
||||
set(obj) != expected
|
||||
or obj["journal_schema"] != "warden.case.step-journal.v1"
|
||||
):
|
||||
raise Refused("unknown journal receipt schema")
|
||||
state = StepState(
|
||||
obj["step_id"],
|
||||
obj["owner"],
|
||||
Pins(**obj["pins"]),
|
||||
tuple(obj["allowed_created"]),
|
||||
obj["retry_safe"],
|
||||
tuple(
|
||||
Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"]))
|
||||
for r in obj["receipts"]
|
||||
),
|
||||
)
|
||||
if (
|
||||
state.step_id,
|
||||
state.owner,
|
||||
state.pins,
|
||||
state.allowed_created,
|
||||
state.retry_safe,
|
||||
) != (
|
||||
initial.step_id,
|
||||
initial.owner,
|
||||
initial.pins,
|
||||
initial.allowed_created,
|
||||
initial.retry_safe,
|
||||
):
|
||||
raise Refused("journal pins changed")
|
||||
return state
|
||||
|
||||
def save(
|
||||
self,
|
||||
binding: str,
|
||||
state: StepState,
|
||||
evidence: OwnerResult | None = None,
|
||||
invocation: Invocation | None = None,
|
||||
intent: OriginalIntent | None = None,
|
||||
):
|
||||
self.db.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
if intent is not None:
|
||||
self.db.execute(
|
||||
"INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)",
|
||||
(intent.invocation.attempt_id, binding, intent.payload()),
|
||||
)
|
||||
self.db.execute(
|
||||
"INSERT INTO receipts(binding,payload) VALUES (?,?)",
|
||||
(
|
||||
binding,
|
||||
json.dumps(
|
||||
asdict(state)
|
||||
| {
|
||||
"journal_schema": "warden.case.step-journal.v1",
|
||||
"attempt_id": invocation.attempt_id if invocation else None,
|
||||
"operation": invocation.operation if invocation else None,
|
||||
"evidence_digest": digest(evidence.evidence.encode())
|
||||
if evidence
|
||||
else None,
|
||||
"session_revoked": evidence.revoked if evidence else None,
|
||||
"session_revocation_digest": digest(
|
||||
evidence.session_revocation_evidence.encode()
|
||||
)
|
||||
if evidence and evidence.session_revocation_evidence
|
||||
else None,
|
||||
"grant_revocation_digest": digest(
|
||||
evidence.grant_revocation_evidence.encode()
|
||||
)
|
||||
if evidence and evidence.grant_revocation_evidence
|
||||
else None,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
),
|
||||
),
|
||||
)
|
||||
self.db.execute("COMMIT")
|
||||
except BaseException:
|
||||
self.db.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
|
||||
class Executor:
|
||||
"""Injected fixed owner adapters and provenance verifier; none ship by default.
|
||||
|
||||
evidence verifier must authenticate owner custody/provenance or explicitly
|
||||
attest attended manual evidence; a bare JSON reference is never sufficient.
|
||||
It receives purpose, exact source digest, and named evidence reference.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
journal: Journal,
|
||||
adapters: dict[str, Adapter],
|
||||
evidence_verifier: Callable[[str, str, str], bool],
|
||||
*,
|
||||
browser_inputs: BrowserInputs | None = None,
|
||||
):
|
||||
self.journal, self.adapters, self.evidence_verifier = (
|
||||
journal,
|
||||
dict(adapters),
|
||||
evidence_verifier,
|
||||
)
|
||||
self._admitted = {}
|
||||
self.browser_inputs = browser_inputs
|
||||
|
||||
def admit(
|
||||
self,
|
||||
case_source: bytes,
|
||||
type_source: bytes,
|
||||
descriptor_source: bytes,
|
||||
catalog_source: bytes,
|
||||
*,
|
||||
principal: str,
|
||||
actor: str,
|
||||
lane: str,
|
||||
acceptance_evidence: str,
|
||||
materialization_evidence: str,
|
||||
descriptor_evidence: str,
|
||||
session_evidence: str,
|
||||
) -> tuple[Descriptor, Invocation, StepState]:
|
||||
for source in (case_source, type_source, descriptor_source, catalog_source):
|
||||
bounded_source(source)
|
||||
descriptor = Descriptor.decode(descriptor_source)
|
||||
try:
|
||||
case = yaml.safe_load(case_source)["spec"]
|
||||
case_type = yaml.safe_load(type_source)["spec"]
|
||||
if not isinstance(case, dict) or not isinstance(case_type, dict):
|
||||
raise ValueError
|
||||
except (yaml.YAMLError, KeyError, TypeError, ValueError):
|
||||
raise Refused("invalid case or type source shape") from None
|
||||
if case.get("status") != "materialized" or not case.get("acceptance"):
|
||||
raise Refused("case is not accepted and structurally materialized")
|
||||
if case_type.get("status") not in ("acknowledged", "implemented"):
|
||||
raise Refused("type is not acknowledged")
|
||||
if case.get("type") != f"{case_type['id']}@{case_type['version']}":
|
||||
raise Refused("case type pin mismatch")
|
||||
if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest(
|
||||
descriptor_source
|
||||
):
|
||||
raise Refused(
|
||||
"type has no exact acknowledged machine descriptor; plan only"
|
||||
)
|
||||
for purpose, source, evidence in (
|
||||
("case-acceptance", digest(case_source), acceptance_evidence),
|
||||
(
|
||||
"ops-mason-materialization",
|
||||
digest(case_source),
|
||||
materialization_evidence,
|
||||
),
|
||||
(
|
||||
"owner-descriptor-acceptance:" + descriptor.owner,
|
||||
digest(descriptor_source),
|
||||
descriptor_evidence,
|
||||
),
|
||||
("type-machine-seam-acceptance", digest(type_source), descriptor_evidence),
|
||||
("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence),
|
||||
(
|
||||
f"caller-session:{actor}:{principal}:{lane}",
|
||||
digest(case_source),
|
||||
session_evidence,
|
||||
),
|
||||
):
|
||||
if (
|
||||
not isinstance(evidence, str)
|
||||
or not IDENTIFIER.fullmatch(evidence)
|
||||
or self._verify_evidence(purpose, source, evidence) is not True
|
||||
):
|
||||
raise Refused("independent owner evidence not verified")
|
||||
if (
|
||||
not IDENTIFIER.fullmatch(principal)
|
||||
or actor != descriptor.actor
|
||||
or lane != descriptor.lane
|
||||
):
|
||||
raise Refused("principal actor or lane mismatch")
|
||||
if descriptor.effect == "mint" and (
|
||||
actor != "human" or os.getenv("WARDEN_AGENT_ID")
|
||||
):
|
||||
raise Refused("agent mint is forbidden")
|
||||
parameters = case.get("parameters", {})
|
||||
declared = {
|
||||
item["name"]: item
|
||||
for item in case_type.get("parameters", [])
|
||||
if isinstance(item, dict) and isinstance(item.get("name"), str)
|
||||
}
|
||||
if (
|
||||
not isinstance(parameters, dict)
|
||||
or descriptor.target_parameter not in declared
|
||||
):
|
||||
raise Refused("target parameter is not declared by acknowledged type")
|
||||
if case_type.get("id") == "oidc-browser-relying-party":
|
||||
if type(self.browser_inputs) is not BrowserInputs:
|
||||
raise Refused("browser metadata validator is not configured")
|
||||
target = self.browser_inputs.target(
|
||||
yaml.safe_load(case_source),
|
||||
yaml.safe_load(type_source),
|
||||
descriptor.target_parameter,
|
||||
)
|
||||
else:
|
||||
for name in parameters:
|
||||
definition = declared.get(name)
|
||||
if (
|
||||
definition is None
|
||||
or definition.get("sensitivity") not in ("public", "operational")
|
||||
or definition.get("case_settable") is False
|
||||
):
|
||||
raise Refused("unknown, fixed or secret-sensitivity case parameter")
|
||||
target = parameters.get(descriptor.target_parameter)
|
||||
if not isinstance(target, str) or not IDENTIFIER.fullmatch(target):
|
||||
raise Refused("target must be exact case metadata")
|
||||
adapter = self.adapters.get(descriptor.adapter)
|
||||
if adapter is None or (adapter.reference, adapter.source) != (
|
||||
descriptor.adapter,
|
||||
descriptor.adapter_source,
|
||||
):
|
||||
raise Refused("unknown adapter or owner source pin")
|
||||
sheet = json.dumps(
|
||||
[
|
||||
descriptor.step,
|
||||
descriptor.owner,
|
||||
target,
|
||||
actor,
|
||||
principal,
|
||||
lane,
|
||||
acceptance_evidence,
|
||||
materialization_evidence,
|
||||
descriptor_evidence,
|
||||
session_evidence,
|
||||
],
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
pins = Pins(
|
||||
digest(case_source),
|
||||
digest(type_source),
|
||||
digest(descriptor_source),
|
||||
digest(catalog_source),
|
||||
digest(acceptance_evidence.encode()),
|
||||
digest(sheet),
|
||||
)
|
||||
invocation = Invocation(
|
||||
descriptor.step, descriptor.owner, target, actor, principal, lane, pins
|
||||
)
|
||||
initial = StepState(
|
||||
descriptor.step,
|
||||
descriptor.owner,
|
||||
pins,
|
||||
descriptor.allowed_created,
|
||||
descriptor.retry_safe,
|
||||
)
|
||||
self._admitted[invocation] = (descriptor, initial, session_evidence)
|
||||
return descriptor, invocation, initial
|
||||
|
||||
def _result(
|
||||
self, adapter: Adapter, invocation: Invocation, result: OwnerResult
|
||||
) -> OwnerResult:
|
||||
if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome):
|
||||
raise Refused("unknown owner result shape")
|
||||
if type(result.created) is not tuple or not all(
|
||||
isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created
|
||||
):
|
||||
raise Refused("owner created-object result invalid")
|
||||
if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch(
|
||||
result.evidence
|
||||
):
|
||||
raise Refused("owner evidence identifier invalid")
|
||||
if adapter.verify(invocation, result) is not True or result.revoked is not True:
|
||||
raise Refused("owner result or session revocation unverified")
|
||||
if (result.attempt_id, result.operation) != (
|
||||
invocation.attempt_id,
|
||||
invocation.operation,
|
||||
):
|
||||
raise Refused("owner receipt belongs to another attempt")
|
||||
descriptor = self._admitted[
|
||||
replace(invocation, attempt_id="", operation="admission")
|
||||
][0]
|
||||
proof_fields = {
|
||||
"parent-session": (
|
||||
f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}",
|
||||
result.session_revocation_evidence,
|
||||
),
|
||||
"broker-grant": (
|
||||
f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}",
|
||||
result.grant_revocation_evidence,
|
||||
),
|
||||
}
|
||||
proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes]
|
||||
for purpose, evidence in proofs:
|
||||
if (
|
||||
not isinstance(evidence, str)
|
||||
or not IDENTIFIER.fullmatch(evidence)
|
||||
or self._verify_evidence(purpose, invocation.pins.case, evidence)
|
||||
is not True
|
||||
):
|
||||
raise Refused("independent grant or parent-session revocation absent")
|
||||
return result
|
||||
|
||||
def _verify_evidence(self, purpose, source, evidence):
|
||||
try:
|
||||
with contained_output():
|
||||
return self.evidence_verifier(purpose, source, evidence) is True
|
||||
except Exception:
|
||||
raise Refused("independent evidence verification failed") from None
|
||||
|
||||
def _admission(
|
||||
self, descriptor: Descriptor, invocation: Invocation, initial: StepState
|
||||
):
|
||||
if self._admitted.get(invocation, ())[:2] != (descriptor, initial):
|
||||
raise Refused("execution requires verified admission on this executor")
|
||||
adapter = self.adapters.get(descriptor.adapter)
|
||||
if adapter is None or (adapter.reference, adapter.source) != (
|
||||
descriptor.adapter,
|
||||
descriptor.adapter_source,
|
||||
):
|
||||
raise Refused("adapter changed after admission")
|
||||
session = self._admitted[invocation][2]
|
||||
if (
|
||||
self._verify_evidence(
|
||||
f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}",
|
||||
invocation.pins.case,
|
||||
session,
|
||||
)
|
||||
is not True
|
||||
):
|
||||
raise Refused("caller session no longer verified")
|
||||
if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"):
|
||||
raise Refused("agent mint is forbidden")
|
||||
return adapter
|
||||
|
||||
def _attempt(self, invocation: Invocation, state: StepState, operation: str):
|
||||
identity = json.dumps(
|
||||
[asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex],
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
return replace(
|
||||
invocation,
|
||||
attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(),
|
||||
operation=operation,
|
||||
)
|
||||
|
||||
def run(
|
||||
self,
|
||||
descriptor: Descriptor,
|
||||
invocation: Invocation,
|
||||
initial: StepState,
|
||||
*,
|
||||
retry=False,
|
||||
) -> StepState:
|
||||
adapter = self._admission(descriptor, invocation, initial)
|
||||
with self.journal.locked():
|
||||
binding = f"{invocation.step}:{invocation.target}"
|
||||
state = self.journal.load(binding, initial)
|
||||
recovered = recover(state)
|
||||
if recovered != state:
|
||||
self.journal.save(binding, recovered)
|
||||
state = recovered
|
||||
if len(state.receipts) > MAX_HISTORY - 4:
|
||||
raise Refused("bounded journal history requires owner archival")
|
||||
running = begin(state, invocation.pins, retry=retry)
|
||||
dispatched = self._attempt(invocation, running, "run")
|
||||
self.journal.save(
|
||||
binding,
|
||||
running,
|
||||
invocation=dispatched,
|
||||
intent=OriginalIntent(binding, dispatched, adapter.source),
|
||||
)
|
||||
evidence = None
|
||||
try:
|
||||
with contained_output():
|
||||
result = self._result(
|
||||
adapter, dispatched, adapter.invoke(dispatched)
|
||||
)
|
||||
completed = finish(
|
||||
running, invocation.pins, result.outcome, created=result.created
|
||||
)
|
||||
evidence = result
|
||||
except Exception:
|
||||
completed = finish(running, invocation.pins, Outcome.UNCERTAIN)
|
||||
self.journal.save(binding, completed, evidence, dispatched)
|
||||
return completed
|
||||
|
||||
def rollback(
|
||||
self, descriptor: Descriptor, invocation: Invocation, initial: StepState
|
||||
) -> StepState:
|
||||
adapter = self._admission(descriptor, invocation, initial)
|
||||
with self.journal.locked():
|
||||
binding = f"{invocation.step}:{invocation.target}"
|
||||
state = self.journal.load(binding, initial)
|
||||
if recover(state) != state:
|
||||
self.journal.save(binding, recover(state))
|
||||
raise Refused("lost rollback result requires owner reconciliation")
|
||||
if adapter.rollback is None:
|
||||
raise Refused("owner has no reviewed undo adapter")
|
||||
if len(state.receipts) > MAX_HISTORY - 4:
|
||||
raise Refused("bounded journal history requires owner archival")
|
||||
running = begin_rollback(state, invocation.pins)
|
||||
dispatched = self._attempt(invocation, running, "rollback")
|
||||
self.journal.save(
|
||||
binding,
|
||||
running,
|
||||
invocation=dispatched,
|
||||
intent=OriginalIntent(binding, dispatched, adapter.source),
|
||||
)
|
||||
evidence = None
|
||||
try:
|
||||
with contained_output():
|
||||
result = self._result(
|
||||
adapter,
|
||||
dispatched,
|
||||
adapter.rollback(dispatched, running.last.created),
|
||||
)
|
||||
verified = result.outcome is Outcome.ROLLED_BACK and not result.created
|
||||
evidence = result if verified else None
|
||||
except Exception:
|
||||
verified = False
|
||||
completed = finish_rollback(running, invocation.pins, verified=verified)
|
||||
self.journal.save(binding, completed, evidence, dispatched)
|
||||
return completed
|
||||
|
||||
def reconcile(
|
||||
self, descriptor: Descriptor, invocation: Invocation, initial: StepState
|
||||
) -> StepState:
|
||||
"""Explicit owner-native state inspection; never redispatches the mutation."""
|
||||
adapter = self._admission(descriptor, invocation, initial)
|
||||
if adapter.reconcile is None and adapter.reconcile_original is None:
|
||||
raise Refused("owner reconciliation adapter absent")
|
||||
with self.journal.locked():
|
||||
binding = f"{invocation.step}:{invocation.target}"
|
||||
state = recover(self.journal.load(binding, initial))
|
||||
if not state.last or state.last.outcome not in (
|
||||
Outcome.UNCERTAIN,
|
||||
Outcome.FAILED_AFTER,
|
||||
):
|
||||
raise Refused("no uncertain mutation to reconcile")
|
||||
if len(state.receipts) > MAX_HISTORY - 4:
|
||||
raise Refused("bounded journal history requires owner archival")
|
||||
state = replace(
|
||||
state,
|
||||
receipts=(
|
||||
*state.receipts,
|
||||
Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN),
|
||||
),
|
||||
)
|
||||
original = (
|
||||
self.journal.original_intent(binding, invocation, adapter.source)
|
||||
if adapter.reconcile_original is not None
|
||||
else None
|
||||
)
|
||||
dispatched = self._attempt(invocation, state, "reconcile")
|
||||
self.journal.save(binding, state, invocation=dispatched)
|
||||
with contained_output():
|
||||
result = self._result(
|
||||
adapter,
|
||||
dispatched,
|
||||
adapter.reconcile_original(ReconcileContext(original, dispatched))
|
||||
if original is not None
|
||||
else adapter.reconcile(dispatched),
|
||||
)
|
||||
if result.outcome not in (
|
||||
Outcome.NO_CHANGE,
|
||||
Outcome.SUCCESS,
|
||||
Outcome.FAILED_BEFORE,
|
||||
):
|
||||
raise Refused("owner did not reconcile target state")
|
||||
# Validate created-object scope using the same closed result reducer.
|
||||
finish(
|
||||
begin(
|
||||
StepState(
|
||||
state.step_id,
|
||||
state.owner,
|
||||
state.pins,
|
||||
state.allowed_created,
|
||||
state.retry_safe,
|
||||
),
|
||||
state.pins,
|
||||
),
|
||||
state.pins,
|
||||
result.outcome,
|
||||
created=result.created,
|
||||
)
|
||||
reconciled = StepState(
|
||||
state.step_id,
|
||||
state.owner,
|
||||
state.pins,
|
||||
state.allowed_created,
|
||||
state.retry_safe,
|
||||
(
|
||||
*state.receipts,
|
||||
Receipt(len(state.receipts) + 1, result.outcome, result.created),
|
||||
),
|
||||
)
|
||||
self.journal.save(binding, reconciled, result, dispatched)
|
||||
return reconciled
|
||||
|
||||
|
||||
class OwnerInterrupted(KeyboardInterrupt):
|
||||
"""Sanitized interruption preserves persisted running/crash recovery semantics."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("owner execution interrupted")
|
||||
|
||||
|
||||
class _Discard:
|
||||
"""Never retain or forward adapter output, including unbounded malicious text."""
|
||||
|
||||
def write(self, text):
|
||||
return len(text)
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,700 @@
|
|||
"""Injected owner harness; these receipts are not native owner attestations."""
|
||||
|
||||
from dataclasses import asdict, replace
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import sqlite3
|
||||
import pytest
|
||||
import yaml
|
||||
from warden.case_receipt_state import Outcome, Refused
|
||||
from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rig(tmp_path):
|
||||
desc = Descriptor(
|
||||
"warden.case.execution.v1",
|
||||
"mint-secret",
|
||||
"owner",
|
||||
"owner/mint@v1",
|
||||
"sha256:" + "1" * 64,
|
||||
"human",
|
||||
"issuer-lane",
|
||||
"client_id",
|
||||
"mint",
|
||||
("case-policy", "case-role"),
|
||||
True,
|
||||
("parent-session", "broker-grant"),
|
||||
)
|
||||
descriptor = json.dumps(asdict(desc)).encode()
|
||||
typ = yaml.safe_dump(
|
||||
{
|
||||
"spec": {
|
||||
"id": "test",
|
||||
"version": "1",
|
||||
"status": "acknowledged",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "client_id",
|
||||
"sensitivity": "operational",
|
||||
"case_settable": True,
|
||||
}
|
||||
],
|
||||
"execution_descriptors": {"mint-secret": digest(descriptor)},
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
case = yaml.safe_dump(
|
||||
{
|
||||
"spec": {
|
||||
"id": "case-test",
|
||||
"type": "test@1",
|
||||
"status": "materialized",
|
||||
"acceptance": "acceptance",
|
||||
"parameters": {"client_id": "fixture-client"},
|
||||
}
|
||||
}
|
||||
).encode()
|
||||
calls = []
|
||||
|
||||
def invoke(inv):
|
||||
calls.append(inv)
|
||||
return bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"owner-receipt",
|
||||
("case-policy", "case-role"),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
)
|
||||
|
||||
adapter = Adapter(
|
||||
"owner/mint@v1",
|
||||
desc.adapter_source,
|
||||
invoke,
|
||||
lambda inv, result: result.evidence == "owner-receipt",
|
||||
lambda inv, objects: bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.ROLLED_BACK,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
),
|
||||
lambda inv: bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.NO_CHANGE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
),
|
||||
)
|
||||
verified = {
|
||||
("case-acceptance", digest(case), "acceptance"),
|
||||
("ops-mason-materialization", digest(case), "materialization"),
|
||||
("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"),
|
||||
("type-machine-seam-acceptance", digest(typ), "descriptor"),
|
||||
("catalog-routing-acceptance", digest(b"catalog"), "descriptor"),
|
||||
("caller-session:human:operator:issuer-lane", digest(case), "session"),
|
||||
("session-revocation:issuer-lane", digest(case), "session-revoked"),
|
||||
("grant-revocation:issuer-lane", digest(case), "grant-revoked"),
|
||||
}
|
||||
|
||||
def verifier(purpose, source, evidence):
|
||||
if purpose.startswith(("session-revocation:", "grant-revocation:")):
|
||||
scope, lane, operation, attempt = purpose.split(":")
|
||||
return (
|
||||
source == digest(case)
|
||||
and lane == "issuer-lane"
|
||||
and operation in ("run", "rollback", "reconcile")
|
||||
and evidence
|
||||
== ("session-" if scope == "session-revocation" else "grant-") + attempt
|
||||
)
|
||||
return (purpose, source, evidence) in verified
|
||||
|
||||
executor = Executor(
|
||||
Journal(tmp_path / "private"), {"owner/mint@v1": adapter}, verifier
|
||||
)
|
||||
kwargs = dict(
|
||||
principal="operator",
|
||||
actor="human",
|
||||
lane="issuer-lane",
|
||||
acceptance_evidence="acceptance",
|
||||
materialization_evidence="materialization",
|
||||
descriptor_evidence="descriptor",
|
||||
session_evidence="session",
|
||||
)
|
||||
admitted = executor.admit(case, typ, descriptor, b"catalog", **kwargs)
|
||||
return (executor, admitted, calls, (case, typ, descriptor, b"catalog"), kwargs)
|
||||
|
||||
|
||||
def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def inspect(inv):
|
||||
other = sqlite3.connect(executor.journal.directory / "journal.sqlite")
|
||||
payload = json.loads(
|
||||
other.execute(
|
||||
"SELECT payload FROM receipts ORDER BY seq DESC LIMIT 1"
|
||||
).fetchone()[0]
|
||||
)
|
||||
assert payload["receipts"][-1]["outcome"] == "running"
|
||||
return old.invoke(inv)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=inspect)
|
||||
assert executor.run(*args).last.outcome is Outcome.SUCCESS
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_crash_and_fresh_process_resume_never_duplicates_effect(rig):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def crash(inv):
|
||||
calls.append(inv)
|
||||
raise KeyboardInterrupt
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=crash)
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
executor.run(*args)
|
||||
fresh = Executor(
|
||||
Journal(executor.journal.directory),
|
||||
{old.reference: old},
|
||||
executor.evidence_verifier,
|
||||
)
|
||||
admitted = fresh.admit(*sources, **kwargs)
|
||||
with pytest.raises(Refused):
|
||||
fresh.run(*admitted, retry=True)
|
||||
state = fresh.journal.load("mint-secret:fixture-client", admitted[2])
|
||||
assert state.last.outcome is Outcome.UNCERTAIN
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_lost_result_preserved_and_cas_reconciliation_does_not_mint(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def lost(inv):
|
||||
calls.append(inv)
|
||||
raise TimeoutError("SECRET-HIDDEN")
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=lost)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args, retry=True)
|
||||
assert len(calls) == 1
|
||||
assert (
|
||||
b"SECRET-HIDDEN"
|
||||
not in (executor.journal.directory / "journal.sqlite").read_bytes()
|
||||
)
|
||||
|
||||
|
||||
def test_failed_before_change_requires_explicit_retry_and_safe_owner(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def no_change(inv):
|
||||
calls.append(inv)
|
||||
return bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.FAILED_BEFORE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=no_change)
|
||||
assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert executor.run(*args, retry=True).last.outcome is Outcome.FAILED_BEFORE
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"result",
|
||||
[
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"owner-receipt",
|
||||
("shared-policy",),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"owner-receipt",
|
||||
(),
|
||||
False,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
OwnerResult(
|
||||
Outcome.SUCCESS,
|
||||
"unknown-receipt",
|
||||
(),
|
||||
True,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
{"outcome": "verified-success", "stdout": "SECRET-HIDDEN"},
|
||||
],
|
||||
)
|
||||
def test_unknown_secret_payload_created_scope_or_missing_revocation_never_succeeds(
|
||||
rig, result, capsys
|
||||
):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def malicious(inv):
|
||||
print("SECRET-HIDDEN" * 1000)
|
||||
return result
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=malicious)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
assert capsys.readouterr().out == ""
|
||||
assert (
|
||||
b"SECRET-HIDDEN"
|
||||
not in (executor.journal.directory / "journal.sqlite").read_bytes()
|
||||
)
|
||||
|
||||
|
||||
def test_rollback_reverses_only_verified_case_created_objects(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
undos = []
|
||||
|
||||
def undo(inv, objects):
|
||||
undos.append(objects)
|
||||
return old.rollback(inv, objects)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, rollback=undo)
|
||||
executor.run(*args)
|
||||
assert executor.rollback(*args).last.outcome is Outcome.ROLLED_BACK
|
||||
assert undos == [("case-role", "case-policy")]
|
||||
assert "shared-policy" not in undos[0]
|
||||
|
||||
|
||||
def test_uncertain_rollback_cannot_replay(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
executor.run(*args)
|
||||
executor.adapters[old.reference] = replace(
|
||||
old,
|
||||
rollback=lambda inv, objs: bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.ROLLED_BACK,
|
||||
"owner-receipt",
|
||||
(),
|
||||
False,
|
||||
session_revocation_evidence="session-revoked",
|
||||
grant_revocation_evidence="grant-revoked",
|
||||
),
|
||||
),
|
||||
)
|
||||
assert executor.rollback(*args).last.outcome is Outcome.ROLLBACK_UNCERTAIN
|
||||
with pytest.raises(Refused):
|
||||
executor.rollback(*args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change", ["materialization", "type-prose", "actor", "lane", "source", "target"]
|
||||
)
|
||||
def test_exact_admission_requires_owner_evidence_and_closed_source_pins(rig, change):
|
||||
executor, _, _, sources, kwargs = rig
|
||||
case, typ, descriptor, catalog = sources
|
||||
kwargs = dict(kwargs)
|
||||
if change == "materialization":
|
||||
kwargs["materialization_evidence"] = "bare-json"
|
||||
if change == "type-prose":
|
||||
obj = yaml.safe_load(typ)
|
||||
obj["spec"].pop("execution_descriptors")
|
||||
typ = yaml.safe_dump(obj).encode()
|
||||
if change == "actor":
|
||||
kwargs["actor"] = "agent"
|
||||
if change == "lane":
|
||||
kwargs["lane"] = "other-lane"
|
||||
if change == "source":
|
||||
obj = json.loads(descriptor)
|
||||
obj["adapter_source"] = "sha256:" + "2" * 64
|
||||
descriptor = json.dumps(obj).encode()
|
||||
if change == "target":
|
||||
obj = yaml.safe_load(case)
|
||||
obj["spec"]["parameters"]["client_id"] = "other-target"
|
||||
case = yaml.safe_dump(obj).encode()
|
||||
with pytest.raises(Refused):
|
||||
executor.admit(case, typ, descriptor, catalog, **kwargs)
|
||||
|
||||
|
||||
def test_agent_env_cannot_dispatch_after_human_admission(rig, monkeypatch):
|
||||
executor, args, calls, _, _ = rig
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "test-agent")
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_forged_invocation_cannot_bypass_admission(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
with pytest.raises(Refused):
|
||||
executor.run(args[0], replace(args[1], target="other-target"), args[2])
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_journal_rejects_unsafe_directory_and_symlink(tmp_path):
|
||||
unsafe = tmp_path / "unsafe"
|
||||
unsafe.mkdir(mode=493)
|
||||
with pytest.raises(Refused):
|
||||
Journal(unsafe)
|
||||
private = tmp_path / "private"
|
||||
private.mkdir(mode=448)
|
||||
(private / "journal.sqlite").symlink_to(tmp_path / "outside")
|
||||
with pytest.raises(OSError):
|
||||
Journal(private)
|
||||
|
||||
|
||||
def test_process_lock_blocks_parallel_dispatch(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
with executor.journal.locked():
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_changed_catalog_pins_cannot_inherit_old_success(rig):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
executor.run(*args)
|
||||
with pytest.raises(Refused):
|
||||
executor.admit(*sources[:3], b"changed-catalog", **kwargs)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_private_journal_does_not_retain_owner_payload_or_accept_unknown_schema(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
executor.run(*args)
|
||||
row = executor.journal.db.execute(
|
||||
"SELECT seq,payload FROM receipts ORDER BY seq DESC LIMIT 1"
|
||||
).fetchone()
|
||||
payload = json.loads(row[1])
|
||||
assert payload["journal_schema"] == "warden.case.step-journal.v1"
|
||||
assert payload["evidence_digest"] == digest(b"owner-receipt")
|
||||
assert payload["session_revoked"] is True
|
||||
assert "owner-receipt" not in row[1]
|
||||
payload["stdout"] = "UNTRUSTED-OUTPUT"
|
||||
executor.journal.db.execute(
|
||||
"UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0])
|
||||
)
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
|
||||
|
||||
def test_expired_caller_session_cannot_use_cached_admission(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
old = executor.evidence_verifier
|
||||
executor.evidence_verifier = lambda purpose, source, evidence: (
|
||||
False
|
||||
if purpose.startswith("caller-session:")
|
||||
else old(purpose, source, evidence)
|
||||
)
|
||||
with pytest.raises(Refused):
|
||||
executor.run(*args)
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index", [0, 1, 2, 3])
|
||||
def test_source_sizes_are_bounded_before_parsing(rig, index):
|
||||
executor, _, _, sources, kwargs = rig
|
||||
oversized = list(sources)
|
||||
oversized[index] = b"x" * 262145
|
||||
with pytest.raises(Refused, match="bounded"):
|
||||
executor.admit(*oversized, **kwargs)
|
||||
|
||||
|
||||
def test_owner_fd_and_inherited_subprocess_output_is_contained(rig, capfd):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def leaking(inv):
|
||||
os.write(1, b"SYNTHETIC-SECRET")
|
||||
os.write(2, b"SYNTHETIC-SECRET")
|
||||
subprocess.run([sys.executable, "-c", 'print("SYNTHETIC-SECRET")'], check=True)
|
||||
return old.invoke(inv)
|
||||
|
||||
executor.adapters[old.reference] = replace(old, invoke=leaking)
|
||||
assert executor.run(*args).last.outcome is Outcome.SUCCESS
|
||||
observed = capfd.readouterr()
|
||||
assert "SYNTHETIC-SECRET" not in observed.out + observed.err
|
||||
|
||||
|
||||
def test_evidence_provider_output_and_error_never_escape(rig, capfd):
|
||||
executor, _, calls, sources, kwargs = rig
|
||||
|
||||
def broken(*args):
|
||||
print("SYNTHETIC-SECRET")
|
||||
os.write(2, b"SYNTHETIC-SECRET")
|
||||
raise RuntimeError("SYNTHETIC-SECRET")
|
||||
|
||||
executor.evidence_verifier = broken
|
||||
with pytest.raises(Refused) as caught:
|
||||
executor.admit(*sources, **kwargs)
|
||||
assert "SYNTHETIC-SECRET" not in str(caught.value)
|
||||
observed = capfd.readouterr()
|
||||
assert "SYNTHETIC-SECRET" not in observed.out + observed.err
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["session", "grant"])
|
||||
def test_broker_grant_and_parent_session_have_separate_required_evidence(rig, missing):
|
||||
executor, args, _, _, _ = rig
|
||||
old = executor.adapters[args[0].adapter]
|
||||
result = old.invoke(args[1])
|
||||
if missing == "session":
|
||||
result = replace(result, session_revocation_evidence=None)
|
||||
else:
|
||||
result = replace(result, grant_revocation_evidence=None)
|
||||
executor.adapters[old.reference] = replace(old, invoke=lambda inv: result)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_mint_descriptor_cannot_omit_broker_grant_scope(rig):
|
||||
descriptor = json.loads(rig[3][2])
|
||||
descriptor["revocation_scopes"] = ["parent-session"]
|
||||
with pytest.raises(Refused):
|
||||
Descriptor.decode(json.dumps(descriptor).encode())
|
||||
|
||||
|
||||
def test_unsafe_owner_step_cannot_retry_even_after_verified_failed_before(
|
||||
rig, tmp_path
|
||||
):
|
||||
executor, args, _, sources, kwargs = rig
|
||||
descriptor = replace(args[0], retry_safe=False)
|
||||
descriptor_source = json.dumps(asdict(descriptor)).encode()
|
||||
type_doc = yaml.safe_load(sources[1])
|
||||
type_doc["spec"]["execution_descriptors"][descriptor.step] = digest(
|
||||
descriptor_source
|
||||
)
|
||||
type_source = yaml.safe_dump(type_doc).encode()
|
||||
old_verifier = executor.evidence_verifier
|
||||
|
||||
def verifier(purpose, source, evidence):
|
||||
if purpose == "owner-descriptor-acceptance:owner":
|
||||
return source == digest(descriptor_source) and evidence == "descriptor"
|
||||
if purpose == "type-machine-seam-acceptance":
|
||||
return source == digest(type_source) and evidence == "descriptor"
|
||||
return old_verifier(purpose, source, evidence)
|
||||
|
||||
adapter = executor.adapters[descriptor.adapter]
|
||||
|
||||
def failed(inv):
|
||||
return bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.FAILED_BEFORE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
"session-revoked",
|
||||
"grant-revoked",
|
||||
),
|
||||
)
|
||||
|
||||
fresh = Executor(
|
||||
Journal(tmp_path / "unsafe-retry"),
|
||||
{adapter.reference: replace(adapter, invoke=failed)},
|
||||
verifier,
|
||||
)
|
||||
admitted = fresh.admit(
|
||||
sources[0], type_source, descriptor_source, sources[3], **kwargs
|
||||
)
|
||||
assert fresh.run(*admitted).last.outcome is Outcome.FAILED_BEFORE
|
||||
with pytest.raises(Refused):
|
||||
fresh.run(*admitted, retry=True)
|
||||
|
||||
|
||||
def bound(inv, result):
|
||||
return replace(
|
||||
result,
|
||||
attempt_id=inv.attempt_id,
|
||||
operation=inv.operation,
|
||||
session_revocation_evidence=("session-" + inv.attempt_id)
|
||||
if result.session_revocation_evidence
|
||||
else None,
|
||||
grant_revocation_evidence=("grant-" + inv.attempt_id)
|
||||
if result.grant_revocation_evidence
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
def test_old_grant_and_session_proofs_cannot_verify_a_new_retry(rig):
|
||||
executor, args, _, _, _ = rig
|
||||
adapter = executor.adapters[args[0].adapter]
|
||||
retained = []
|
||||
|
||||
def attempts(inv):
|
||||
if not retained:
|
||||
result = bound(
|
||||
inv,
|
||||
OwnerResult(
|
||||
Outcome.FAILED_BEFORE,
|
||||
"owner-receipt",
|
||||
(),
|
||||
True,
|
||||
"session-revoked",
|
||||
"grant-revoked",
|
||||
),
|
||||
)
|
||||
retained.append(result)
|
||||
return result
|
||||
# Even a caller rewriting labels cannot authenticate prior revocation
|
||||
# evidence for the newly persisted attempt identity.
|
||||
return replace(retained[0], attempt_id=inv.attempt_id, operation=inv.operation)
|
||||
|
||||
executor.adapters[adapter.reference] = replace(adapter, invoke=attempts)
|
||||
assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE
|
||||
assert executor.run(*args, retry=True).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_owner_interrupt_payload_is_sanitized_with_running_intent_retained(rig, capfd):
|
||||
executor, args, _, _, _ = rig
|
||||
adapter = executor.adapters[args[0].adapter]
|
||||
|
||||
def interrupted(inv):
|
||||
os.write(2, b"SYNTHETIC-SECRET")
|
||||
raise KeyboardInterrupt("SYNTHETIC-SECRET")
|
||||
|
||||
executor.adapters[adapter.reference] = replace(adapter, invoke=interrupted)
|
||||
with pytest.raises(KeyboardInterrupt) as caught:
|
||||
executor.run(*args)
|
||||
assert "SYNTHETIC-SECRET" not in str(caught.value)
|
||||
assert "SYNTHETIC-SECRET" not in "".join(capfd.readouterr())
|
||||
assert (
|
||||
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
|
||||
is Outcome.RUNNING
|
||||
)
|
||||
|
||||
|
||||
def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal(
|
||||
rig, tmp_path
|
||||
):
|
||||
executor, args, calls, sources, kwargs = rig
|
||||
executor.run(*args)
|
||||
original_attempt = calls[-1].attempt_id
|
||||
new_catalog = b"independently-reviewed-catalog-v2"
|
||||
old_verifier = executor.evidence_verifier
|
||||
|
||||
def verifier(purpose, source, evidence):
|
||||
if purpose == "catalog-routing-acceptance":
|
||||
return source == digest(new_catalog) and evidence == "descriptor"
|
||||
return old_verifier(purpose, source, evidence)
|
||||
|
||||
fresh = Executor(Journal(tmp_path / "new-revision"), executor.adapters, verifier)
|
||||
revised = fresh.admit(*sources[:3], new_catalog, **kwargs)
|
||||
assert fresh.run(*revised).last.outcome is Outcome.SUCCESS
|
||||
assert calls[-1].attempt_id != original_attempt
|
||||
assert calls[-1].pins.catalog != calls[0].pins.catalog
|
||||
|
||||
|
||||
def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig):
|
||||
executor, _, _, sources, kwargs = rig
|
||||
type_doc = yaml.safe_load(sources[1])
|
||||
type_doc["spec"]["parameters"][0]["sensitivity"] = "secret"
|
||||
source = yaml.safe_dump(type_doc).encode()
|
||||
old = executor.evidence_verifier
|
||||
executor.evidence_verifier = lambda purpose, digest_value, evidence: (
|
||||
True
|
||||
if purpose == "type-machine-seam-acceptance"
|
||||
else old(purpose, digest_value, evidence)
|
||||
)
|
||||
with pytest.raises(Refused, match="secret-sensitivity"):
|
||||
executor.admit(sources[0], source, sources[2], sources[3], **kwargs)
|
||||
|
||||
|
||||
def _lose_result(executor, args, calls):
|
||||
old = executor.adapters[args[0].adapter]
|
||||
|
||||
def lost(inv):
|
||||
calls.append(inv)
|
||||
raise TimeoutError("lost")
|
||||
|
||||
seen = []
|
||||
|
||||
def by_original(ctx):
|
||||
seen.append(ctx)
|
||||
return old.reconcile(ctx.attempt)
|
||||
|
||||
executor.adapters[old.reference] = replace(
|
||||
old, invoke=lost, reconcile=None, reconcile_original=by_original
|
||||
)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
return seen
|
||||
|
||||
|
||||
def test_original_intent_survives_restart_separate_from_reconcile_attempt(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
seen = _lose_result(executor, args, calls)
|
||||
executor.journal = Journal(executor.journal.directory) # restart: fresh handle
|
||||
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
|
||||
(ctx,) = seen
|
||||
original = ctx.original.invocation
|
||||
assert original.attempt_id == calls[0].attempt_id
|
||||
assert original.operation == "run" and ctx.attempt.operation == "reconcile"
|
||||
assert ctx.attempt.attempt_id != original.attempt_id
|
||||
assert original.pins == args[1].pins
|
||||
assert (original.principal, original.tenant, original.realm) == (
|
||||
args[1].principal,
|
||||
args[1].tenant,
|
||||
args[1].realm,
|
||||
)
|
||||
assert ctx.original.source == executor.adapters[args[0].adapter].source
|
||||
|
||||
|
||||
def test_missing_original_intent_refuses_and_stays_uncertain(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
seen = _lose_result(executor, args, calls)
|
||||
executor.journal.db.execute("DELETE FROM intents")
|
||||
with pytest.raises(Refused, match="original intent record missing"):
|
||||
executor.reconcile(*args)
|
||||
assert seen == []
|
||||
binding = f"{args[1].step}:{args[1].target}"
|
||||
assert executor.journal.load(binding, args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change", [{"tenant": "other-tenant"}, {"realm": "other-realm"}, {"lane": "x"}]
|
||||
)
|
||||
def test_original_intent_mismatch_refuses(rig, change):
|
||||
executor, args, calls, _, _ = rig
|
||||
seen = _lose_result(executor, args, calls)
|
||||
with pytest.raises(Refused):
|
||||
executor.reconcile(args[0], replace(args[1], **change), args[2])
|
||||
assert seen == []
|
||||
|
||||
|
||||
def test_original_intent_source_mismatch_refuses(rig):
|
||||
executor, args, calls, _, _ = rig
|
||||
_lose_result(executor, args, calls)
|
||||
binding = f"{args[1].step}:{args[1].target}"
|
||||
with pytest.raises(Refused, match="source mismatch"):
|
||||
executor.journal.original_intent(binding, calls[0], "other-source")
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
"""Observed source behavior of exact concurrent snapshot, not secure acceptance.
|
||||
|
||||
Counterexamples use generated metadata and the owner's existing synthetic rig.
|
||||
Passing a counterexample means the documented gap reproduced; it is not a native
|
||||
holder, an owner attestation or permission to trust local SQL/JSON.
|
||||
"""
|
||||
from dataclasses import asdict, replace
|
||||
import inspect
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
from test_case_run import rig as rig
|
||||
from warden.case_receipt_state import Outcome, Refused
|
||||
from warden.case_run import Executor, Journal, OriginalIntent
|
||||
|
||||
|
||||
def interrupted(rig):
|
||||
executor,args,calls,sources,kwargs=rig
|
||||
old=executor.adapters[args[0].adapter];seen=[]
|
||||
def lost(inv):calls.append(inv);raise TimeoutError('generated lost response')
|
||||
def original(ctx):seen.append(ctx);return old.reconcile(ctx.attempt)
|
||||
executor.adapters[old.reference]=replace(old,invoke=lost,reconcile=None,reconcile_original=original)
|
||||
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
|
||||
return executor,args,calls,old,seen
|
||||
|
||||
|
||||
def alter_payload(executor,**changes):
|
||||
row=executor.journal.db.execute('SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1').fetchone()
|
||||
obj=json.loads(row[1]);obj.update(changes)
|
||||
executor.journal.db.execute('UPDATE intents SET payload=? WHERE seq=?',(json.dumps(obj),row[0]))
|
||||
|
||||
|
||||
def test_observed_original_actor_mismatch_is_accepted_by_lookup(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
alter_payload(executor,actor='agent')
|
||||
result=executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
assert result.invocation.actor=='agent' and args[1].actor=='human'
|
||||
|
||||
|
||||
def test_observed_payload_attempt_id_is_not_checked_against_sql_column_or_receipt(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
actual=calls[0].attempt_id;alter_payload(executor,attempt_id='attempt-unrelated')
|
||||
result=executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
sql_id=executor.journal.db.execute('SELECT attempt_id FROM intents').fetchone()[0]
|
||||
assert sql_id==actual and result.invocation.attempt_id=='attempt-unrelated'
|
||||
|
||||
|
||||
def test_observed_orphan_intent_without_original_running_receipt_is_accepted(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
executor.journal.db.execute('DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)')
|
||||
assert executor.journal.load('mint-secret:fixture-client',args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
result=executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
assert result.invocation.attempt_id==calls[0].attempt_id
|
||||
|
||||
|
||||
def test_observed_serializer_accepts_arbitrary_values_outside_normal_admit(rig):
|
||||
_,args,_,_,_=rig
|
||||
inv=replace(args[1],target={'unexpected':'synthetic-owner-payload'},operation='run',attempt_id='attempt-fixture')
|
||||
record=OriginalIntent('mint-secret:fixture-client',inv,'sha256:'+'1'*64)
|
||||
decoded=OriginalIntent.decode(record.payload())
|
||||
assert decoded.invocation.target=={'unexpected':'synthetic-owner-payload'}
|
||||
# This is a direct serialization API gap, not an observed normal-admit leak.
|
||||
|
||||
|
||||
def test_observed_tenant_realm_not_admitted_and_remain_empty(rig):
|
||||
_,args,_,_,_=rig
|
||||
params=inspect.signature(Executor.admit).parameters
|
||||
assert 'tenant' not in params and 'realm' not in params
|
||||
assert args[1].tenant==args[1].realm==''
|
||||
|
||||
|
||||
def test_observed_no_original_context_purpose_is_requested(rig):
|
||||
executor,args,_,_,seen=interrupted(rig);old=executor.evidence_verifier;purposes=[]
|
||||
def trace(purpose,source,evidence):purposes.append(purpose);return old(purpose,source,evidence)
|
||||
executor.evidence_verifier=trace
|
||||
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
|
||||
assert seen and any(p.startswith('session-revocation:') for p in purposes)
|
||||
assert not any('original' in p for p in purposes)
|
||||
# Owner trust is the existing synthetic fixture; this proves call shape only.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field,value',[('principal','another-person'),('tenant','another-tenant'),('realm','another-realm')])
|
||||
def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch(rig,field,value):
|
||||
executor,args,_,old,_=interrupted(rig);alter_payload(executor,**{field:value})
|
||||
with pytest.raises(Refused):executor.journal.original_intent('mint-secret:fixture-client',args[1],old.source)
|
||||
|
||||
|
||||
def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig):
|
||||
executor,args,calls,_,_=rig
|
||||
executor.journal.db.execute("CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END")
|
||||
with pytest.raises(sqlite3.IntegrityError):executor.run(*args)
|
||||
assert not calls
|
||||
assert executor.journal.db.execute('SELECT COUNT(*) FROM intents').fetchone()==(0,)
|
||||
assert executor.journal.db.execute('SELECT COUNT(*) FROM receipts').fetchone()==(0,)
|
||||
|
||||
|
||||
def test_existing_missing_original_refuses_before_callback(rig):
|
||||
executor,args,_,_,seen=interrupted(rig)
|
||||
executor.journal.db.execute('DELETE FROM intents')
|
||||
with pytest.raises(Refused):executor.reconcile(*args)
|
||||
assert not seen
|
||||
assert executor.journal.load('mint-secret:fixture-client',args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_existing_current_parent_revocation_remains_mandatory(rig):
|
||||
executor,args,_,_,_=interrupted(rig);old=executor.evidence_verifier
|
||||
def refuse_session(purpose,source,evidence):
|
||||
return False if purpose.startswith('session-revocation:') else old(purpose,source,evidence)
|
||||
executor.evidence_verifier=refuse_session
|
||||
with pytest.raises(Refused):executor.reconcile(*args)
|
||||
assert executor.journal.load('mint-secret:fixture-client',args[2]).last.outcome is Outcome.UNCERTAIN
|
||||
|
||||
|
||||
def test_existing_intent_survives_actual_distinct_interpreter(rig):
|
||||
executor,args,calls,old,_=interrupted(rig)
|
||||
code='''import json,sys
|
||||
from pathlib import Path
|
||||
from warden.case_run import Journal,Invocation
|
||||
from warden.case_receipt_state import Pins
|
||||
obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1]))
|
||||
with j.locked():
|
||||
i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2])
|
||||
print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation}))
|
||||
'''
|
||||
result=subprocess.run([sys.executable,'-B','-c',code,str(executor.journal.directory),old.source],
|
||||
input=json.dumps(asdict(args[1])),text=True,capture_output=True,timeout=15,
|
||||
env={'PATH':'/usr/bin:/bin','PYTHONPATH':str(Path(__file__).parents[1]/'src'),'PYTHONDONTWRITEBYTECODE':'1'},check=True)
|
||||
assert json.loads(result.stdout)=={'attempt':calls[0].attempt_id,'operation':'run'}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
---
|
||||
id: WARDEN-WP-0044
|
||||
type: workplan
|
||||
title: "Follow through on KnownSecurityCases along their type's route"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: claude-code
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-08"
|
||||
updated: "2026-10-11"
|
||||
depends_on: []
|
||||
related_repos:
|
||||
- net-kingdom
|
||||
- ops-mason
|
||||
- key-cape
|
||||
- railiance-platform
|
||||
state_hub_workstream_id: "f7e81013-6868-5d99-8528-ba75a442eca9"
|
||||
---
|
||||
|
||||
# WARDEN-WP-0044 — Follow through on KnownSecurityCases along their type's route
|
||||
|
||||
## Why
|
||||
|
||||
NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon
|
||||
`security-case-types_v0.1`, proposed). Once a case of an implemented type is
|
||||
accepted and ops-mason has materialized its structure (`MASON-WP-0010`),
|
||||
ops-warden carries out the remaining steps. These are the owners' declared entry
|
||||
points, run on the authentication path the type defines, followed by the type's
|
||||
verification.
|
||||
|
||||
Today ops-warden routes needs and, for `exec_capable` lanes, proxies fetches as
|
||||
the caller. It does not yet know about cases, and each new integration ends with
|
||||
a founder running a hand-written script (for example the proposed KEY-WP-0038-T02
|
||||
rollout). This plan gives ops-warden the follow-through role, without widening
|
||||
what it may do: each step still runs under its owner's entry point and the
|
||||
caller's identity.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T01
|
||||
status: done
|
||||
depends_on: [NK-WP-0048-T03]
|
||||
priority: high
|
||||
state_hub_task_id: "ef0920fc-418e-52cb-acff-9a5c0c728bf5"
|
||||
```
|
||||
|
||||
### T01 — Define the type's follow-through section
|
||||
|
||||
Propose the `follow_through` part of the SecurityCaseType schema together with
|
||||
net-kingdom. For each step it names the owner entry point, `warden_executes`,
|
||||
the auth method and lane, whether an attended human login is unavoidable (with
|
||||
the type's justification), and the verification command. Write it for the first
|
||||
type, `oidc-browser-relying-party`, covering key-cape client registration,
|
||||
custody minting and LLDAP group membership.
|
||||
|
||||
2026-10-08: NK-WP-0048-T03/T04 landed with a drafted section. Review is in
|
||||
`history/2026-10-08-oidc-rp-follow-through-review.md`. It requires three
|
||||
changes before T02: a per-step `lane`, a narrow `warden_executes`, and
|
||||
`value_handling`. It also lists four refinements. This task is done when
|
||||
net-kingdom acknowledges the review (NK-WP-0048-T05).
|
||||
|
||||
2026-10-09: R4 was withdrawn in favour of ops-mason's per-case
|
||||
`{consuming_service}-keycape-client`, with the entry body templated in the type
|
||||
(R4′, see the addendum in the review).
|
||||
|
||||
2026-10-09: the revised 1.0 type and canon now carry R1–R3, R4′, R5–R7.
|
||||
`warden case check-type <type.yaml>` (`src/warden/case_type.py`, tests in
|
||||
`tests/test_case_type.py`) checks a type's follow-through mechanically: the
|
||||
live type is schema-conformant, with one attended session
|
||||
(`key-cape-oidc-login`). It is not yet plannable, because two lanes are not
|
||||
in the catalog: `identity-provisioner-groups` (net-kingdom) and
|
||||
`keycape-client-secret-generate` (railiance-platform). These gate T02, not
|
||||
T01. T01 still closes on net-kingdom's acknowledgement (NK-WP-0048-T05).
|
||||
|
||||
2026-10-09: ops-warden cannot add `identity-provisioner-groups` as the type
|
||||
declares it. The provisioner accepts only user-engine's runtime token and
|
||||
admits only user-engine pods, so no caller-identity path exists. R8 (review
|
||||
addendum) asks net-kingdom either to move `ensure-groups` into the existing
|
||||
attended session (recommended) or to give the provisioner an operator
|
||||
credential. `keycape-client-secret-generate` needs a generate-in-custody entry
|
||||
point from railiance-platform, and the broker does not have one yet. Both were
|
||||
sent to their owners.
|
||||
|
||||
2026-10-09: done. net-kingdom took R8 option 1, so `ensure-groups` runs attended
|
||||
in the `register-client` session and `identity-provisioner-groups` is dropped
|
||||
(net-kingdom `0e86f95`). The founder acknowledged the type with memo r4 (hub
|
||||
decision `e418263e`, NK-WP-0048-T05 done).
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T02
|
||||
status: done
|
||||
depends_on: [WARDEN-WP-0044-T01]
|
||||
priority: high
|
||||
state_hub_task_id: "5f68c991-f490-58f8-a121-92f74073d272"
|
||||
```
|
||||
|
||||
### T02 — Plan a case run
|
||||
|
||||
`warden case plan <case.yaml>` checks that the case is accepted and materialized,
|
||||
resolves each step to its catalog entry and entry point, and prints the run:
|
||||
which steps warden executes, which run as the attended caller, what the
|
||||
verification is. No secret values; no execution.
|
||||
|
||||
Tests: refuse an unaccepted or unmaterialized case; refuse a step whose catalog
|
||||
entry is missing or not reviewed; show attended steps explicitly.
|
||||
|
||||
2026-10-09: done. `warden case plan <case.yaml> [--type] [--json]`
|
||||
(`src/warden/case_plan.py`, 14 tests in `tests/test_case_plan.py`). It refuses:
|
||||
- a case that is not `materialized`, or has no acceptance record
|
||||
- a type that is mismatched or not acknowledged
|
||||
- any `case check-type` finding, including a missing or unreviewed lane
|
||||
- undeclared, type-fixed or secret-sensitivity parameters
|
||||
- a placeholder the case does not set, or an unsupported `when`
|
||||
|
||||
It fills type defaults (`issuer`), evaluates `<param> == <value>` conditions,
|
||||
renders owner, entry point, auth and ready_check, groups attended steps per
|
||||
lane, and lists the applicable verification.
|
||||
|
||||
The live Forgejo case is correctly refused: it is `accepted`, not
|
||||
materialized. A materialized copy, with a stand-in lane for
|
||||
`keycape-client-secret-generate`, renders six steps: two attended steps plus
|
||||
`register-client` in one `key-cape-oidc-login` session, one warden step
|
||||
(`mint-secret`), and two owner steps. Seven checks follow.
|
||||
|
||||
Observation for net-kingdom (not blocking): `bind-env` runs before
|
||||
`register-client`, in type order. Its entry carries a literal
|
||||
`KEYCAPE_<CLIENT_ID>_CLIENT_SECRET`, which is not a `{param}` placeholder, so it
|
||||
reaches the plan unrendered.
|
||||
|
||||
2026-10-09 follow-up:
|
||||
- **Format specs.** `_render` supports net-kingdom's `{param:env}` spec (upper
|
||||
case, `-` as `_`, the same as `keycape-client.py env_name`) and refuses any
|
||||
other spec or `!conversion`.
|
||||
- **Case reference.** The plan prints `case-ref: <case_id>@sha256:<hex>` over the
|
||||
case file's exact bytes. This is the reference proposed to railiance-platform
|
||||
for `credential.py generate --case-ref` (RPF-WP-0054-T01); the broker
|
||||
re-derives it from the file.
|
||||
- 18 tests now.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T03
|
||||
status: progress
|
||||
depends_on: [WARDEN-WP-0044-T02, MASON-WP-0010-T03]
|
||||
blocking_reason: "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run."
|
||||
priority: high
|
||||
state_hub_task_id: "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89"
|
||||
```
|
||||
|
||||
### T03 — Run and verify a case
|
||||
|
||||
`warden case run <case.yaml>` executes the plan. Owner entry points run through
|
||||
the existing proxy as the caller; steps that need an attended login use the
|
||||
existing contained login envelope (silent command, session revoked afterwards),
|
||||
grouped into as few attended sessions as possible. Then it runs the type's
|
||||
verification and writes a receipt without values. `warden case rollback`
|
||||
reverses exactly this case's steps.
|
||||
|
||||
Acceptance: the Forgejo reference case (NK-WP-0048-T06) reaches `live` through
|
||||
this path, with no case-specific script.
|
||||
|
||||
2026-10-17: original-intent lookup/context interface for NK-WP-0048-T15, source
|
||||
and synthetic tests only. `src/warden/case_run.py`: `Executor.run` writes an
|
||||
`OriginalIntent` (schema `warden.case.original-intent.v1`: the dispatched run
|
||||
Invocation — attempt, step, owner, target, actor, principal, lane, all six pins,
|
||||
tenant, realm — plus admitted adapter source) in the same SQLite transaction as
|
||||
the pre-dispatch journal row. `Journal.original_intent(binding, current, source)`
|
||||
returns it after restart and refuses on a missing record or any
|
||||
pins/source/step/owner/target/principal/lane/tenant/realm mismatch, before any
|
||||
journal write, so the step stays uncertain. An adapter's
|
||||
`reconcile_original(ReconcileContext(original, attempt))` receives the original
|
||||
intent separately from the new reconcile attempt. This is Warden's record of what
|
||||
it dispatched, not owner created-only provenance; tenant/realm are as supplied at
|
||||
admission. Owner descriptors/adapters, contained session integration, Forgejo
|
||||
materialization and the native run remain open.
|
||||
|
||||
Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09):
|
||||
- `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate
|
||||
--client-id <id> --case-ref <case-ref> --case-file <case.yaml>` as the caller's
|
||||
child process. The broker re-derives the case-ref and checks status and
|
||||
client_id.
|
||||
- warden refuses that step when `WARDEN_AGENT_ID` is set.
|
||||
|
||||
2026-10-10: railiance-platform implemented the command (RPF-WP-0054-T03). It is
|
||||
catalogued as draft `keycape-client-secret-generate`, with `exec_owner`
|
||||
railiance-platform and `exec_capable: false`. `warden case check-type` now
|
||||
reports the lane as LANE-UNREVIEWED rather than LANE-MISSING. Before it can be
|
||||
activated:
|
||||
- the founder binds the issuer policy to the operator OIDC role
|
||||
- key-cape confirms the secret format
|
||||
- the RPF-WP-0054-T05 probe passes
|
||||
|
||||
2026-10-10 (later):
|
||||
- **Broker lane.** The issuer role is bound (CCR-2026-0043) and the T05 probe
|
||||
passed (railiance-platform `21b8af3`). Only key-cape's format confirmation is
|
||||
left. The playbook now includes the issuer-role login, a 15m token with
|
||||
unlimited uses that `warden case run` must group with the attended session.
|
||||
- **Env spec.** net-kingdom adopted `{client_id:env}` as a 1.0 patch
|
||||
(`9a9b8e3`). The plan renders `KEYCAPE_FORGEJO_CLIENT_SECRET`.
|
||||
- **Forgejo case.** Rendered and built by ops-mason. It is not yet
|
||||
`materialized`: MASON-WP-0010-T03 waits on the railiance-platform CCRs and
|
||||
their founder apply.
|
||||
|
||||
2026-10-10: `keycape-client-secret-generate` activated on railiance-platform's
|
||||
go-ahead (message 20973a9c) after key-cape confirmed the format (4e0213b3).
|
||||
`warden case check-type` on the live type now reports `plannable=True`. The
|
||||
only thing T03 still waits on is the Forgejo case reaching `materialized`
|
||||
(MASON-WP-0010-T03).
|
||||
|
||||
2026-10-10: net-kingdom moved the type to 1.1 (`e2174ce`) and re-pinned the
|
||||
Forgejo case. 1.1 adds the verification `consumer-enforces-groups` and requires
|
||||
the hand-over to enforce `access_groups`. `check-type` on 1.1 reports
|
||||
`plannable=True`, and a materialized copy renders six steps and the new check.
|
||||
The case-ref digest covers the whole case file, including `status`, so it
|
||||
changes when the case becomes `materialized`. `warden case run` therefore takes
|
||||
it from the file at run time, and the broker re-derives it from the same file.
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0044-T04
|
||||
status: wait
|
||||
depends_on: [WARDEN-WP-0044-T03]
|
||||
blocking_reason: "Catalog integration follows a proven run."
|
||||
priority: medium
|
||||
state_hub_task_id: "4a0fa4a7-5ca9-518a-ab92-a79a5e2cf93e"
|
||||
```
|
||||
|
||||
### T04 — Catalog entries from types
|
||||
|
||||
Let a type contribute reviewed catalog entries once, so that every case of that
|
||||
type routes without a new catalog edit. Cases add parameter values, not routes.
|
||||
`warden route find` should be able to answer "which SecurityCaseType covers this
|
||||
need?" before anyone builds by hand.
|
||||
|
||||
|
||||
2026-10-10 consolidation source prototype (not an accepted owner adapter API):
|
||||
`src/warden/case_receipt_state.py` provides a pure immutable receipt reducer,
|
||||
with exact case/type/adapter/catalog/acceptance/run-sheet pins, running intent
|
||||
before dispatch, uncertain recovery after a lost result, no success replay,
|
||||
explicit retry-safe failed-before-change retry and created-only reverse undo.
|
||||
It executes nothing and accepts no command output. The caller must durably
|
||||
persist running before any future side effect; storage/authenticated owner
|
||||
receipts/live adapters are not implemented. Focused offline tests cover changed
|
||||
pins, lost receipts, retry/refusal, uncertain rollback and shared-object
|
||||
preservation. Source proposal: `docs/security-case-execution-proposal.md`.
|
||||
|
||||
T03 remains open: typed owner execution/receipt contracts require review, the
|
||||
Forgejo structural materialization needs owner receipts, and CLI dispatch,
|
||||
contained sessions, native verification and provenance remain unimplemented.
|
||||
There is a circularity to resolve with net-kingdom/ops-mason: materialization
|
||||
cannot require mint-secret if mint-secret is itself a follow-through step whose
|
||||
planning requires materialized. No case status is changed by this prototype.
|
||||
|
||||
|
||||
2026-10-11: the exact execution-design memo version1 has confirmed native human
|
||||
acceptance (approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca). The approved proposal
|
||||
bytes remain unchanged. `src/warden/case_run.py` now implements a closed v1 local
|
||||
execution seam, independent admission/evidence callbacks, private transactional
|
||||
SQLite receipt storage, process locking, running-before-dispatch, crash recovery
|
||||
to uncertainty, explicit safe retry, separately required broker-grant and parent-session revocation
|
||||
proofs, native owner reconciliation and created-only
|
||||
reverse rollback. Thirty-seven kernel tests and fourteen reducer tests pass;
|
||||
ruff checks pass. See `docs/security-case-execution-kernel-v1.md` for exact scope
|
||||
and receipt provenance. No installed native adapter or live CLI execution is
|
||||
claimed. Current prose types stay plan-only; acknowledged descriptor/type machine
|
||||
seams and authenticated owner/caller receipts are still required. T03 progresses
|
||||
for implementation but cannot finish before ordered native Forgejo acceptance.
|
||||
|
||||
2026-10-16 source preparation (isolated checkout; no native acceptance):
|
||||
T03's browser parameter mismatch now has a producer-validator-backed projection
|
||||
in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2
|
||||
parameter definitions and all four reviewed producer modules are pinned; full case
|
||||
validation admits reviewed security-sensitive metadata while unknown/fixed/secret
|
||||
inputs and type/producer drift refuse. Only the reviewed descriptor target
|
||||
reaches the owner callback; no parameter journal payload is added. Actual
|
||||
Executor/producer tests cover positive versions and refusal/containment paths.
|
||||
Acknowledged machine descriptors, authenticated owner evidence, real Forgejo
|
||||
materialization and contained native sessions remain mandatory and unimplemented;
|
||||
T03 stays progress, T04 stays wait. No type or case status changed.
|
||||
|
||||
2026-10-16 producer source refresh: independently reviewed NetKingdom
|
||||
`015694ee414c802dc2ef1fa0120dfed610086e69` optional descriptor-map repair.
|
||||
Only the validator/evidence source pins change; exact parameter and transitive
|
||||
source identity remain enforced. Actual canonical producer, Executor and reducer
|
||||
suite passes 87 checks, including 14 new malformed-map integration cases; all
|
||||
four missing/changed modules still refuse. Review:
|
||||
`docs/reviews/2026-10-16-browser-producer-pin-refresh.md`. No adapter attestation,
|
||||
native acceptance, credential use or privileged dispatch. T03 remains progress.
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"observed_at": "2026-10-17T05:53:20.364253+00:00",
|
||||
"owner": "ops-warden",
|
||||
"canonical_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"sources": [
|
||||
{
|
||||
"path": "src/warden/case_run.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671",
|
||||
"worktree_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "src/warden/case_receipt_state.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc",
|
||||
"worktree_sha256": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "src/warden/case_inputs.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7",
|
||||
"worktree_sha256": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "src/warden/__init__.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a",
|
||||
"worktree_sha256": "sha256:047e0feba479474235f0707a207655ecfddfb33cd4f54069134419b8ca4ea24a",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "tests/test_case_run.py",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139",
|
||||
"worktree_sha256": "sha256:bc7264848e76e3576821523fec0c4925c4446eaf470f859fe245c98ea072d139",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "docs/security-case-execution-kernel-v1.md",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb",
|
||||
"worktree_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb",
|
||||
"dirty": false
|
||||
},
|
||||
{
|
||||
"path": "workplans/WARDEN-WP-0044-security-case-follow-through.md",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"base_sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1",
|
||||
"worktree_sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1",
|
||||
"dirty": false
|
||||
}
|
||||
],
|
||||
"canonical_edits_performed": false
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"observed_at": "2026-10-17T06:50:09.029792+00:00",
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"origin_main": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"changed_paths": [
|
||||
{
|
||||
"path": "src/warden/case_run.py",
|
||||
"before_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671",
|
||||
"after_sha256": "sha256:77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_case_original_context_consistency.py",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "sha256:10c5187cbc49222017319249ccb408816497ec6e006859d23003f3b2b375445b"
|
||||
},
|
||||
{
|
||||
"path": "docs/security-case-execution-kernel-v1.md",
|
||||
"before_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb",
|
||||
"after_sha256": "sha256:e5570f542ec3ef4e6c11332cfe48fea16bf90cde5b9a5d926f5a068b7cafdbc7"
|
||||
},
|
||||
{
|
||||
"path": "workplans/WARDEN-WP-0044-security-case-follow-through.md",
|
||||
"before_sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1",
|
||||
"after_sha256": "sha256:721a90b2ed8c896257c34a1451eb642cb8824c8263e37c483fb61b36adc54088"
|
||||
}
|
||||
],
|
||||
"all_task_blocks_byte_identical": true,
|
||||
"task_blocks": 4,
|
||||
"unrelated_WORK_RECORDS_worktree_and_index_unchanged": true,
|
||||
"diff_check": 0,
|
||||
"status": " M WORK-RECORDS.md\n M docs/security-case-execution-kernel-v1.md\n M src/warden/case_run.py\n M workplans/WARDEN-WP-0044-security-case-follow-through.md\n?? tests/test_case_original_context_consistency.py\n",
|
||||
"native_actions": false,
|
||||
"commit_or_sync": false,
|
||||
"git_metadata_edited": false,
|
||||
"retained_packets_unchanged": true,
|
||||
"refinement": "New test fixture import corrected for canonical tests package after retained failed collection; no source control was weakened."
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"base_head": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"origin_main": "a45cf475bc4dbc929e0eecbc1f806a9229d934e2",
|
||||
"paths": [
|
||||
{
|
||||
"path": "src/warden/case_run.py",
|
||||
"existed": true,
|
||||
"before_sha256": "sha256:0574036ca7b74bdb9124f6af4888e63ec231ee60f5c9f34cbe2fb1acee39f671"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_case_original_context_consistency.py",
|
||||
"existed": false,
|
||||
"before_sha256": null
|
||||
},
|
||||
{
|
||||
"path": "docs/security-case-execution-kernel-v1.md",
|
||||
"existed": true,
|
||||
"before_sha256": "sha256:0eb563dbaea739fba928d04708c2c2bdac592783bbe68e1271041b0d1b1994cb"
|
||||
},
|
||||
{
|
||||
"path": "workplans/WARDEN-WP-0044-security-case-follow-through.md",
|
||||
"existed": true,
|
||||
"before_sha256": "sha256:44686df53dac792e46791878c3ccf8c187bb14e2de33eeac411be2005f7f2aa1"
|
||||
}
|
||||
],
|
||||
"status": " M WORK-RECORDS.md\n",
|
||||
"unrelated_worktree_sha256": "sha256:b6af9b1cb21e3a64b2ef8a5e732bc35c39cdeaef6af7a18f24824e8ec9fb5a76",
|
||||
"unrelated_index_sha256": "sha256:c61591efad763a3dcb86751f5bf46f03fec8a162bfe06c4272032fea15d2988f"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue