feat(orchestration): compose security scenarios
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
This commit is contained in:
parent
ad46cc89fc
commit
d96aab2321
20 changed files with 1464 additions and 30 deletions
10
Makefile
10
Makefile
|
|
@ -185,12 +185,20 @@ openbao-init-unseal: ## SOPS-held OpenBao init/unseal (NET-WP-0020 T2; requires
|
||||||
openbao-init-unseal-dry-run: ## Dry-run the SOPS-held OpenBao init/unseal path
|
openbao-init-unseal-dry-run: ## Dry-run the SOPS-held OpenBao init/unseal path
|
||||||
@bash sso-mfa/bootstrap/openbao-init-unseal.sh --dry-run
|
@bash sso-mfa/bootstrap/openbao-init-unseal.sh --dry-run
|
||||||
|
|
||||||
iam-profile-conformance-test: ## Run IAM Profile v0.2 conformance fixture tests
|
iam-profile-conformance-test: ## Run current IAM Profile conformance fixture tests
|
||||||
python3 -m pytest tools/iam-profile-conformance/tests
|
python3 -m pytest tools/iam-profile-conformance/tests
|
||||||
|
|
||||||
playbook-contract-test: ## Run Playbook Capability Contract fixture tests
|
playbook-contract-test: ## Run Playbook Capability Contract fixture tests
|
||||||
python3 -m pytest tools/playbook-capability-contract/tests
|
python3 -m pytest tools/playbook-capability-contract/tests
|
||||||
|
|
||||||
|
security-scenario-composer-test: ## Run deterministic security scenario composer tests
|
||||||
|
python3 -m pytest tools/security-scenario-composer/tests
|
||||||
|
|
||||||
|
security-scenario-compose-c0: ## Compose the plan-only C0 local-identity reference
|
||||||
|
python3 tools/security-scenario-composer/security_scenario_composer.py \
|
||||||
|
--scenario examples/security-scenarios/c0-local-identity.yaml \
|
||||||
|
capabilities/playbooks/net-kingdom.local-identity.yaml
|
||||||
|
|
||||||
security-bootstrap-console-test: ## Run automated tests for bootstrap console UI/sections/runbooks (NET-WP-0018-T07)
|
security-bootstrap-console-test: ## Run automated tests for bootstrap console UI/sections/runbooks (NET-WP-0018-T07)
|
||||||
python3 -m pytest tools/security-bootstrap-console/tests
|
python3 -m pytest tools/security-bootstrap-console/tests
|
||||||
|
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -1,10 +1,19 @@
|
||||||
# NetKingdom
|
# NetKingdom
|
||||||
|
|
||||||
NetKingdom provides a dynamic self optimizing full circle security-platform for kubernetes deployed IT-infrastructures.
|
NetKingdom is the canonical security architecture, integration boundary, and
|
||||||
|
bootstrap/reference implementation for NetKingdom environments. It defines
|
||||||
|
identity, tenancy, credential, workload-zone, and security-composition
|
||||||
|
contracts while leaving provider and Railiance execution in their owning
|
||||||
|
repositories.
|
||||||
|
|
||||||
|
The dynamic, self-optimizing security platform is the long-term direction in
|
||||||
|
[INTENT.md](INTENT.md), not a claim about current delivery.
|
||||||
|
|
||||||
## Orientation
|
## Orientation
|
||||||
|
|
||||||
- [SCOPE.md](SCOPE.md) — what this repo owns, current state, and when it is relevant
|
- [SCOPE.md](SCOPE.md) — what this repo owns, current state, and when it is relevant
|
||||||
|
- [Security scenario composition](canon/standards/security-scenario-composition_v0.1.md)
|
||||||
|
— deterministic, plan-only capability and trust composition
|
||||||
|
|
||||||
## Security Infrastructure Documents
|
## Security Infrastructure Documents
|
||||||
|
|
||||||
|
|
|
||||||
16
SCOPE.md
16
SCOPE.md
|
|
@ -42,6 +42,8 @@ about the current implementation.
|
||||||
model.
|
model.
|
||||||
- Playbook Capability Contract v0.1 for the boundary between NetKingdom
|
- Playbook Capability Contract v0.1 for the boundary between NetKingdom
|
||||||
selection/parameterization and Railiance execution.
|
selection/parameterization and Railiance execution.
|
||||||
|
- Security Scenario Composition v0.1 for deterministic provider selection,
|
||||||
|
trust ordering, responsibility mapping, and non-executing owner handoff.
|
||||||
- Tenancy Posture v0.1 and Security Zones v0.1 proposed standards, their schemas,
|
- Tenancy Posture v0.1 and Security Zones v0.1 proposed standards, their schemas,
|
||||||
validators, evidence rules, and publication stewardship. Zone semantics are
|
validators, evidence rules, and publication stewardship. Zone semantics are
|
||||||
owned by `zone-engine`; NetKingdom owns their canon publication.
|
owned by `zone-engine`; NetKingdom owns their canon publication.
|
||||||
|
|
@ -54,6 +56,9 @@ about the current implementation.
|
||||||
development, test, and sandbox use.
|
development, test, and sandbox use.
|
||||||
- IAM Profile, playbook-capability, tenancy-posture, custody, evidence, and
|
- IAM Profile, playbook-capability, tenancy-posture, custody, evidence, and
|
||||||
bootstrap-policy validators.
|
bootstrap-policy validators.
|
||||||
|
- `tools/security-scenario-composer/`: fail-closed, plan-only composition of
|
||||||
|
declared capabilities, parameters, trust transitions, responsibilities, and
|
||||||
|
readiness obligations.
|
||||||
- `tools/security-bootstrap-console/`: guarded platform-root and OpenBao
|
- `tools/security-bootstrap-console/`: guarded platform-root and OpenBao
|
||||||
bootstrap workflow, including refusal of unsafe live initialization.
|
bootstrap workflow, including refusal of unsafe live initialization.
|
||||||
- SOPS/age bootstrap integration, credential-generation and rotation helpers,
|
- SOPS/age bootstrap integration, credential-generation and rotation helpers,
|
||||||
|
|
@ -99,7 +104,7 @@ repositories while NetKingdom retains the contracts and reference evidence.
|
||||||
|
|
||||||
| Tier | Current repository/estate capability | Delivery state |
|
| Tier | Current repository/estate capability | Delivery state |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| C0 — Bootstrap identity | Local OIDC identity, SOPS/age bootstrap, guarded credential workflow, and greenfield OpenBao init/unseal proof | Implemented as reference/bootstrap tooling |
|
| C0 — Bootstrap identity | Local OIDC identity, SOPS/age bootstrap, guarded credential workflow, greenfield OpenBao init/unseal proof, and deterministic plan-only C0 composition | Implemented as reference/bootstrap tooling |
|
||||||
| C1 — Lightweight SSO | IAM-profile-based KeyCape composition using Authelia and LLDAP | Live-proven integration; implementation externally owned |
|
| C1 — Lightweight SSO | IAM-profile-based KeyCape composition using Authelia and LLDAP | Live-proven integration; implementation externally owned |
|
||||||
| C2 — MFA/token authority | Authelia factors and privacyIDEA integration | Live-proven integration; implementation externally owned |
|
| C2 — MFA/token authority | Authelia factors and privacyIDEA integration | Live-proven integration; implementation externally owned |
|
||||||
| C3 — Runtime secrets | OpenBao custody, bootstrap, policy, delivery, and recovery contracts | Integrated with an externally deployed runtime; production evidence remains gated |
|
| C3 — Runtime secrets | OpenBao custody, bootstrap, policy, delivery, and recovery contracts | Integrated with an externally deployed runtime; production evidence remains gated |
|
||||||
|
|
@ -110,7 +115,9 @@ repositories while NetKingdom retains the contracts and reference evidence.
|
||||||
Current open work as of 2026-08-23 is either externally blocked, date-gated, or
|
Current open work as of 2026-08-23 is either externally blocked, date-gated, or
|
||||||
explicit backlog: reef carrier/public-classification decisions in NK-WP-0027,
|
explicit backlog: reef carrier/public-classification decisions in NK-WP-0027,
|
||||||
the NK-WP-0022 retirement gate, security tutorials in NK-WP-0009, and
|
the NK-WP-0022 retirement gate, security tutorials in NK-WP-0009, and
|
||||||
enterprise federation in NK-WP-0011.
|
enterprise federation in NK-WP-0011. NK-WP-0030 has delivered the local C0
|
||||||
|
scenario-composition slice and waits for externally owned C1/C2 provider
|
||||||
|
declarations before the lightweight SSO scenario can compose.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -144,7 +151,7 @@ enterprise federation in NK-WP-0011.
|
||||||
```capability
|
```capability
|
||||||
type: governance
|
type: governance
|
||||||
title: NetKingdom security canon
|
title: NetKingdom security canon
|
||||||
description: Provider-neutral IAM v0.3, user/tenant boundaries, credential, playbook-composition, tenancy-posture, and workload-zone standards with explicit ownership and conformance rules.
|
description: Provider-neutral IAM v0.3, user/tenant boundaries, credential, playbook and scenario composition, tenancy-posture, and workload-zone standards with explicit ownership and conformance rules.
|
||||||
keywords: [iam, oidc, tenancy, workload-identity, security-zones, credentials, canon]
|
keywords: [iam, oidc, tenancy, workload-identity, security-zones, credentials, canon]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -184,5 +191,6 @@ keywords: [meta-orchestration, railiance, responsibility, capability, trust-stat
|
||||||
`docs/openbao-unseal-custody-models.md`, and
|
`docs/openbao-unseal-custody-models.md`, and
|
||||||
`tools/security-bootstrap-console/`
|
`tools/security-bootstrap-console/`
|
||||||
- Executable surfaces: `local-identity/`, `tools/iam-profile-conformance/`,
|
- Executable surfaces: `local-identity/`, `tools/iam-profile-conformance/`,
|
||||||
`tools/playbook-capability-contract/`, and `tools/tenancy-posture/`
|
`tools/playbook-capability-contract/`, `tools/security-scenario-composer/`,
|
||||||
|
and `tools/tenancy-posture/`
|
||||||
- Work state: `.custodian-brief.md` and `workplans/`
|
- Work state: `.custodian-brief.md` and `workplans/`
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
| workplan | NK-WP-0027 | blocked | — | workplans/NK-WP-0027-reef-placement-reconciliation.md |
|
| workplan | NK-WP-0027 | blocked | — | workplans/NK-WP-0027-reef-placement-reconciliation.md |
|
||||||
| workplan | NK-WP-0028 | finished | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md |
|
| workplan | NK-WP-0028 | finished | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md |
|
||||||
| workplan | NK-WP-0029 | finished | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
| workplan | NK-WP-0029 | finished | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
||||||
|
| workplan | NK-WP-0030 | blocked | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md |
|
||||||
| task | ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md |
|
| task | ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md |
|
||||||
| task | ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md |
|
| task | ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md |
|
||||||
| task | ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md |
|
| task | ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md |
|
||||||
|
|
@ -94,5 +95,10 @@
|
||||||
| task | NK-WP-0029-T02 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
| task | NK-WP-0029-T02 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
||||||
| task | NK-WP-0029-T03 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
| task | NK-WP-0029-T03 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
||||||
| task | NK-WP-0029-T04 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
| task | NK-WP-0029-T04 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md |
|
||||||
|
| task | NK-WP-0030-T01 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md |
|
||||||
|
| task | NK-WP-0030-T02 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md |
|
||||||
|
| task | NK-WP-0030-T03 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md |
|
||||||
|
| task | NK-WP-0030-T04 | wait | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md |
|
||||||
|
| task | NK-WP-0030-T05 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md |
|
||||||
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||||
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||||
|
|
|
||||||
78
canon/schemas/security-scenario_v0.1.schema.json
Normal file
78
canon/schemas/security-scenario_v0.1.schema.json
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://netkingdom.local/schemas/security-scenario_v0.1.schema.json",
|
||||||
|
"title": "NetKingdom Security Scenario v0.1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "authority", "requires"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^scenario:[a-z0-9][a-z0-9._:-]*$"
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"enum": ["platform", "netkingdom", "tenant"]
|
||||||
|
},
|
||||||
|
"initial_trust": {
|
||||||
|
"$ref": "#/$defs/trustStates"
|
||||||
|
},
|
||||||
|
"requires": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["capabilities"],
|
||||||
|
"properties": {
|
||||||
|
"capabilities": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {"$ref": "#/$defs/capability"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {"$ref": "#/$defs/capability"},
|
||||||
|
"additionalProperties": {"type": "string", "minLength": 1}
|
||||||
|
},
|
||||||
|
"parameter_overrides": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"capability": {
|
||||||
|
"enum": [
|
||||||
|
"s1.os-baseline",
|
||||||
|
"s1.secret-bootstrap",
|
||||||
|
"s2.cluster-runtime",
|
||||||
|
"s3.platform-services",
|
||||||
|
"c0.bootstrap-identity",
|
||||||
|
"c1.lightweight-sso",
|
||||||
|
"c2a.light-2fa",
|
||||||
|
"c2b.token-authority",
|
||||||
|
"c3.runtime-secrets",
|
||||||
|
"c4.fine-grained-authorization",
|
||||||
|
"c5.enterprise-federation",
|
||||||
|
"c6.self-optimizing-audit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"trustStates": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"enum": [
|
||||||
|
"bare_host_trust",
|
||||||
|
"cluster_trust",
|
||||||
|
"bootstrap_secret_trust",
|
||||||
|
"bootstrap_identity_trust",
|
||||||
|
"runtime_secret_trust",
|
||||||
|
"runtime_identity_trust",
|
||||||
|
"runtime_authorization_trust",
|
||||||
|
"tenant_onboarding_trust"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ domain: netkingdom
|
||||||
status: accepted
|
status: accepted
|
||||||
version: "0.1"
|
version: "0.1"
|
||||||
created: "2026-05-22"
|
created: "2026-05-22"
|
||||||
updated: "2026-05-22"
|
updated: "2026-08-23"
|
||||||
scope: meta-orchestration
|
scope: meta-orchestration
|
||||||
adr:
|
adr:
|
||||||
- docs/adr/ADR-0012-playbook-capability-contract-ownership.md
|
- docs/adr/ADR-0012-playbook-capability-contract-ownership.md
|
||||||
|
|
@ -161,6 +161,12 @@ Security-sensitive and secret-reference parameters MUST NOT be
|
||||||
`tenant_tunable`. Secret-reference defaults must be references or paths,
|
`tenant_tunable`. Secret-reference defaults must be references or paths,
|
||||||
not plaintext secret values.
|
not plaintext secret values.
|
||||||
|
|
||||||
|
For executable validation, a `secret_reference` value MUST be a non-whitespace
|
||||||
|
URI such as `openbao://kv/platform/example` or
|
||||||
|
`kubernetes://namespace/name#key`, or an explicit absolute/relative path
|
||||||
|
beginning with `/`, `./`, or `../`. Bare strings are rejected because the
|
||||||
|
composer cannot distinguish them from secret values.
|
||||||
|
|
||||||
Supported constraints:
|
Supported constraints:
|
||||||
|
|
||||||
| Constraint | Applies to | Meaning |
|
| Constraint | Applies to | Meaning |
|
||||||
|
|
@ -244,7 +250,8 @@ separate playbook runner execute safely.
|
||||||
|
|
||||||
## Scenario Shape
|
## Scenario Shape
|
||||||
|
|
||||||
The validator supports a small scenario file for conformance demos:
|
The validator supports a small scenario file for single-provider conformance
|
||||||
|
demos:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
id: scenario:s1-host-bootstrap-reference
|
id: scenario:s1-host-bootstrap-reference
|
||||||
|
|
@ -263,6 +270,13 @@ Allowed scenario authorities are `platform`, `netkingdom`, and `tenant`.
|
||||||
Tenant authority cannot override `platform_only`,
|
Tenant authority cannot override `platform_only`,
|
||||||
`security_sensitive`, or `secret_reference` parameters.
|
`security_sensitive`, or `secret_reference` parameters.
|
||||||
|
|
||||||
|
The demo composer refuses ambiguous providers and overrides aimed at unselected
|
||||||
|
declarations. Deterministic multi-provider selection, explicit provider pins,
|
||||||
|
trust sequencing, responsibility maps, and owner-routed readiness handoffs are
|
||||||
|
defined by `security-scenario-composition_v0.1.md` and implemented by
|
||||||
|
`tools/security-scenario-composer/`. Use that contract for operational planning;
|
||||||
|
the demo path does not authorize or execute playbooks.
|
||||||
|
|
||||||
## Conformance
|
## Conformance
|
||||||
|
|
||||||
A declaration conforms when it passes:
|
A declaration conforms when it passes:
|
||||||
|
|
|
||||||
142
canon/standards/security-scenario-composition_v0.1.md
Normal file
142
canon/standards/security-scenario-composition_v0.1.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
id: netkingdom-security-scenario-composition-v0.1
|
||||||
|
type: standard
|
||||||
|
title: "NetKingdom Security Scenario Composition v0.1"
|
||||||
|
domain: netkingdom
|
||||||
|
status: proposed
|
||||||
|
version: "0.1"
|
||||||
|
owner: net-kingdom
|
||||||
|
created: "2026-08-23"
|
||||||
|
updated: "2026-08-23"
|
||||||
|
last_reviewed: "2026-08-23"
|
||||||
|
review_interval: 3m
|
||||||
|
scope: meta-orchestration
|
||||||
|
schema:
|
||||||
|
- canon/schemas/security-scenario_v0.1.schema.json
|
||||||
|
validator:
|
||||||
|
- tools/security-scenario-composer/security_scenario_composer.py
|
||||||
|
adr:
|
||||||
|
- docs/adr/ADR-0007-security-orchestration-boundary.md
|
||||||
|
- docs/adr/ADR-0012-playbook-capability-contract-ownership.md
|
||||||
|
related:
|
||||||
|
- canon/standards/playbook-capability-contract_v0.1.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# NetKingdom Security Scenario Composition v0.1
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
This contract defines the deterministic, plan-only boundary between a requested
|
||||||
|
NetKingdom capability set and the independently owned playbook entry points that
|
||||||
|
can realize it. It consumes conformant Playbook Capability Contract v0.1
|
||||||
|
declarations and produces an owner-routed responsibility, trust, parameter, and
|
||||||
|
readiness handoff.
|
||||||
|
|
||||||
|
Composition answers **what is selected, in which trust order, with which safe
|
||||||
|
parameters, and who must execute and evidence it**. It does not run a playbook,
|
||||||
|
mint a credential, infer authority, or declare a runtime ready.
|
||||||
|
|
||||||
|
## 2. Authority boundary
|
||||||
|
|
||||||
|
- NetKingdom owns scenario intent, provider selection, parameter-policy checks,
|
||||||
|
trust sequencing, and the composed responsibility map.
|
||||||
|
- The declaration owner owns the playbook or stable entry point, execution,
|
||||||
|
rollback, and readiness evidence.
|
||||||
|
- Railiance retains deployment execution for Railiance-owned declarations.
|
||||||
|
- A composed plan is not authorization to execute. Any approval, custody,
|
||||||
|
credential, or change-window gate named by an owner remains in force.
|
||||||
|
|
||||||
|
## 3. Scenario input
|
||||||
|
|
||||||
|
Scenario files conform to
|
||||||
|
`canon/schemas/security-scenario_v0.1.schema.json`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: scenario:c0-local-identity-reference
|
||||||
|
authority: netkingdom
|
||||||
|
initial_trust:
|
||||||
|
- bare_host_trust
|
||||||
|
requires:
|
||||||
|
capabilities:
|
||||||
|
- c0.bootstrap-identity
|
||||||
|
providers:
|
||||||
|
c0.bootstrap-identity: net-kingdom.local-identity
|
||||||
|
parameter_overrides:
|
||||||
|
net-kingdom.local-identity:
|
||||||
|
bootstrap_username: bootstrap-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
`authority` uses the Playbook Capability Contract vocabulary: `platform`,
|
||||||
|
`netkingdom`, or `tenant`. Parameter sensitivity and tuning-authority rules are
|
||||||
|
applied before a plan is emitted.
|
||||||
|
|
||||||
|
`initial_trust` lists trust states established outside this composition. The
|
||||||
|
composer never assumes an initial trust state. A required state must be present
|
||||||
|
there or be satisfied by an earlier selected declaration.
|
||||||
|
|
||||||
|
`providers` pins a required capability to an exact declaration id. A pin is
|
||||||
|
mandatory when more than one valid declaration provides the capability. A pin
|
||||||
|
may not name an unrequested capability or a declaration that does not provide
|
||||||
|
the keyed capability.
|
||||||
|
|
||||||
|
## 4. Fail-closed selection rules
|
||||||
|
|
||||||
|
Composition fails when any of the following is true:
|
||||||
|
|
||||||
|
- a declaration is invalid or declaration ids are duplicated;
|
||||||
|
- a capability is unknown, duplicated, or has no provider;
|
||||||
|
- multiple providers match and the scenario does not pin one;
|
||||||
|
- a provider pin does not match the requested capability;
|
||||||
|
- an override targets an unselected declaration or unknown parameter;
|
||||||
|
- an override violates type, constraint, sensitivity, or tuning authority;
|
||||||
|
- a required parameter has neither a default nor an override;
|
||||||
|
- a required trust state cannot be established without a cycle or inference.
|
||||||
|
|
||||||
|
Selection order never resolves ambiguity. Filesystem order, catalog order,
|
||||||
|
lexical order, and prior deployment state are not provider authority.
|
||||||
|
|
||||||
|
## 5. Trust sequencing
|
||||||
|
|
||||||
|
The composer starts only with the scenario's explicit `initial_trust` set. It
|
||||||
|
then selects the lexically first **eligible** declaration, where eligible means
|
||||||
|
all of that declaration's required trust states have already been established.
|
||||||
|
After the step, and only for composition purposes, the declaration's satisfied
|
||||||
|
states become available to later steps.
|
||||||
|
|
||||||
|
Lexical ordering makes independent eligible steps reproducible; it does not
|
||||||
|
grant one provider precedence during selection. If no remaining declaration is
|
||||||
|
eligible, composition fails and reports the unresolved trust states.
|
||||||
|
|
||||||
|
Readiness checks attached to a satisfied state are obligations for the owning
|
||||||
|
executor. They are not marked satisfied merely because the plan contains them.
|
||||||
|
|
||||||
|
## 6. Composition output
|
||||||
|
|
||||||
|
A successful output has
|
||||||
|
`apiVersion: netkingdom.io/security-scenario-composition/v0.1` and
|
||||||
|
`kind: SecurityScenarioComposition`. It contains:
|
||||||
|
|
||||||
|
- the requested capabilities and exact selected declaration ids;
|
||||||
|
- effective parameter values with their source, sensitivity, and tuning
|
||||||
|
authority;
|
||||||
|
- ordered execution handoffs containing owner, repository, entry point,
|
||||||
|
required trust, produced trust, and readiness obligations;
|
||||||
|
- a flattened responsibility map attributable to declaration ids;
|
||||||
|
- the final *planned* trust-state set;
|
||||||
|
- an explicit `execution.permitted: false` boundary.
|
||||||
|
|
||||||
|
The output is non-secret planning material. Declarations and scenarios must use
|
||||||
|
secret references rather than secret values as required by the Playbook
|
||||||
|
Capability Contract.
|
||||||
|
|
||||||
|
## 7. Conformance
|
||||||
|
|
||||||
|
Use the canonical tool:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 tools/security-scenario-composer/security_scenario_composer.py \
|
||||||
|
--scenario <scenario.yaml> <declaration.yaml> [<declaration.yaml> ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit zero means the declarations and scenario compose deterministically. It
|
||||||
|
does not mean the plan was executed or its readiness evidence was observed.
|
||||||
114
capabilities/playbooks/net-kingdom.local-identity.yaml
Normal file
114
capabilities/playbooks/net-kingdom.local-identity.yaml
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
apiVersion: netkingdom.io/playbook-capability/v0.1
|
||||||
|
kind: PlaybookCapabilityDeclaration
|
||||||
|
metadata:
|
||||||
|
id: net-kingdom.local-identity
|
||||||
|
name: NetKingdom local bootstrap identity
|
||||||
|
owner: net-kingdom
|
||||||
|
repo: net-kingdom
|
||||||
|
domain: infotech
|
||||||
|
contract_version: "0.1"
|
||||||
|
source_links:
|
||||||
|
- label: implementation
|
||||||
|
path: local-identity/
|
||||||
|
- label: workplan
|
||||||
|
path: workplans/archived/260702-NK-WP-0002-local-identity.md
|
||||||
|
spec:
|
||||||
|
playbook:
|
||||||
|
path: local-identity/
|
||||||
|
type: cli-sequence
|
||||||
|
invocation: uv run --project local-identity local-identity init
|
||||||
|
description: Initialize and serve the loopback-only bootstrap OIDC identity store.
|
||||||
|
capabilities:
|
||||||
|
- id: c0.bootstrap-identity
|
||||||
|
tier: C0
|
||||||
|
resource_kinds:
|
||||||
|
- identities
|
||||||
|
- secrets_credentials
|
||||||
|
description: Provide a local OIDC issuer before the managed runtime identity plane exists.
|
||||||
|
parameters:
|
||||||
|
- name: local_identity_home
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: .local/local-identity
|
||||||
|
constraints:
|
||||||
|
pattern: ^[^\n\r]+$
|
||||||
|
sensitivity: operational
|
||||||
|
tuning_authority: netkingdom_tunable
|
||||||
|
description: LOCAL_IDENTITY_HOME used by the owning execution environment.
|
||||||
|
- name: bootstrap_username
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
constraints:
|
||||||
|
pattern: ^[a-z_][a-z0-9_-]{0,31}$
|
||||||
|
sensitivity: operational
|
||||||
|
tuning_authority: netkingdom_tunable
|
||||||
|
description: Bootstrap username passed to local-identity init.
|
||||||
|
- name: bootstrap_fullname
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
constraints:
|
||||||
|
pattern: ^[^\n\r]{1,120}$
|
||||||
|
sensitivity: operational
|
||||||
|
tuning_authority: netkingdom_tunable
|
||||||
|
description: Display name passed to local-identity init.
|
||||||
|
- name: bootstrap_email
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
constraints:
|
||||||
|
pattern: ^[^@\s]+@[^@\s]+$
|
||||||
|
sensitivity: operational
|
||||||
|
tuning_authority: netkingdom_tunable
|
||||||
|
description: Bootstrap email passed to local-identity init.
|
||||||
|
- name: listen_port
|
||||||
|
type: integer
|
||||||
|
required: false
|
||||||
|
default: 8443
|
||||||
|
constraints:
|
||||||
|
minimum: 1024
|
||||||
|
maximum: 65535
|
||||||
|
sensitivity: operational
|
||||||
|
tuning_authority: netkingdom_tunable
|
||||||
|
description: Loopback port passed to local-identity serve.
|
||||||
|
- name: token_ttl_seconds
|
||||||
|
type: integer
|
||||||
|
required: false
|
||||||
|
default: 3600
|
||||||
|
constraints:
|
||||||
|
minimum: 60
|
||||||
|
maximum: 3600
|
||||||
|
sensitivity: security_sensitive
|
||||||
|
tuning_authority: platform_only
|
||||||
|
description: Maximum bootstrap access-token lifetime.
|
||||||
|
responsibilities:
|
||||||
|
- resource_kind: identities
|
||||||
|
owner: net-kingdom
|
||||||
|
resources:
|
||||||
|
- local-identity:bootstrap-store
|
||||||
|
- local-identity:loopback-oidc-issuer
|
||||||
|
repo_owns: Local store initialization, loopback issuer behavior, token lifecycle, and security checks.
|
||||||
|
netkingdom_orchestrates: Select C0, provide non-secret bootstrap identity inputs, and require readiness evidence.
|
||||||
|
- resource_kind: secrets_credentials
|
||||||
|
owner: net-kingdom
|
||||||
|
resources:
|
||||||
|
- local-identity:signing-key
|
||||||
|
- local-identity:filesystem-permissions
|
||||||
|
repo_owns: Generate local signing material and enforce private filesystem permissions.
|
||||||
|
netkingdom_orchestrates: Constrain the store location and retain secret values outside the scenario plan.
|
||||||
|
trust:
|
||||||
|
requires:
|
||||||
|
- state: bare_host_trust
|
||||||
|
readiness_checks: []
|
||||||
|
satisfies:
|
||||||
|
- state: bootstrap_identity_trust
|
||||||
|
readiness_checks:
|
||||||
|
- id: local-identity-permissions
|
||||||
|
description: The local identity store and signing material have private permissions.
|
||||||
|
evidence: local-identity security-check exits zero
|
||||||
|
- id: local-identity-discovery
|
||||||
|
description: The loopback issuer serves OIDC discovery at the planned issuer URL.
|
||||||
|
evidence: discovery document is fetched from loopback and its issuer matches the planned port
|
||||||
|
catalog:
|
||||||
|
publish: capabilities/playbooks/net-kingdom.local-identity.yaml
|
||||||
|
maturity: implemented-reference
|
||||||
|
consumers:
|
||||||
|
- netkingdom-security-scenario-composer
|
||||||
17
examples/security-scenarios/README.md
Normal file
17
examples/security-scenarios/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Security Scenario Examples
|
||||||
|
|
||||||
|
`c0-local-identity.yaml` is the first real, locally owned scenario composition.
|
||||||
|
It selects the checked-in NetKingdom local-identity declaration, starts from an
|
||||||
|
explicit `bare_host_trust` state, and produces a plan for
|
||||||
|
`bootstrap_identity_trust`.
|
||||||
|
|
||||||
|
The example uses non-production identity values and does not execute anything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/security-scenario-composer/security_scenario_composer.py \
|
||||||
|
--scenario examples/security-scenarios/c0-local-identity.yaml \
|
||||||
|
capabilities/playbooks/net-kingdom.local-identity.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
A C1/C2 lightweight SSO example belongs here only after the KeyCape/Railiance
|
||||||
|
owners publish conformant declarations beside their execution entry points.
|
||||||
15
examples/security-scenarios/c0-local-identity.yaml
Normal file
15
examples/security-scenarios/c0-local-identity.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
id: scenario:c0-local-identity-reference
|
||||||
|
authority: netkingdom
|
||||||
|
initial_trust:
|
||||||
|
- bare_host_trust
|
||||||
|
requires:
|
||||||
|
capabilities:
|
||||||
|
- c0.bootstrap-identity
|
||||||
|
providers:
|
||||||
|
c0.bootstrap-identity: net-kingdom.local-identity
|
||||||
|
parameter_overrides:
|
||||||
|
net-kingdom.local-identity:
|
||||||
|
local_identity_home: .local/local-identity-reference
|
||||||
|
bootstrap_username: bootstrap-admin
|
||||||
|
bootstrap_fullname: Bootstrap Reference Operator
|
||||||
|
bootstrap_email: bootstrap@example.invalid
|
||||||
|
|
@ -135,3 +135,13 @@ or provider inputs (NK-WP-0027), date-gated (NK-WP-0022), or explicitly backlog
|
||||||
(NK-WP-0009 and NK-WP-0011). This is not an assertion that the intended system
|
(NK-WP-0009 and NK-WP-0011). This is not an assertion that the intended system
|
||||||
is complete; it is a statement about the executable queue in this repository on
|
is complete; it is a statement about the executable queue in this repository on
|
||||||
2026-08-23.
|
2026-08-23.
|
||||||
|
|
||||||
|
## Same-day implementation follow-up
|
||||||
|
|
||||||
|
NK-WP-0030 subsequently implemented the first bounded G1 increment: proposed
|
||||||
|
Security Scenario Composition v0.1 canon, a fail-closed plan-only composer, and
|
||||||
|
a real C0 local-identity declaration and reference scenario. This establishes
|
||||||
|
deterministic provider pins, trust ordering, responsibility mapping, and
|
||||||
|
readiness handoff without executing provider actions. G1 remains open for the
|
||||||
|
lightweight SSO path until the KeyCape/Railiance owners publish conformant C1
|
||||||
|
and C2 declarations.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
id: capability.security.iam-tooling-suite
|
id: capability.security.iam-tooling-suite
|
||||||
name: NetKingdom Security/IAM Tooling Suite
|
name: NetKingdom Security/IAM Tooling Suite
|
||||||
summary: Dynamic, self-optimizing security platform for Kubernetes-deployed IT infrastructure; owns canonical
|
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture validation,
|
||||||
IAM/security standards and executable conformance tooling (IAM profile conformance, playbook capability
|
and deterministic plan-only security scenario composition.
|
||||||
contract validation, security bootstrap console).
|
|
||||||
owner: net-kingdom
|
owner: net-kingdom
|
||||||
status: draft
|
status: draft
|
||||||
domain: infotech
|
domain: infotech
|
||||||
|
|
@ -17,17 +16,17 @@ maturity:
|
||||||
current: D3
|
current: D3
|
||||||
target: D5
|
target: D5
|
||||||
confidence: medium
|
confidence: medium
|
||||||
rationale: README plus docs/secrets-engine-security-infrastructure-boundary.md describe an explicit
|
rationale: SCOPE plus docs/secrets-engine-security-infrastructure-boundary.md describe an explicit
|
||||||
integration boundary with OpenBao, flex-auth, user-engine, ops-warden, ops-bridge, info-tech-canon,
|
integration boundary with OpenBao, flex-auth, user-engine, ops-warden, ops-bridge, info-tech-canon,
|
||||||
and State Hub; canon/standards/ holds versioned standards (iam-profile_v0.2.md, playbook-capability-contract_v0.1.md)
|
and State Hub; canon/standards/ holds versioned IAM v0.3, playbook capability, scenario composition,
|
||||||
that key-cape and other repos implement against.
|
tenancy, zone, and boundary standards that provider repos implement against.
|
||||||
availability:
|
availability:
|
||||||
current: A2
|
current: A2
|
||||||
target: A3
|
target: A3
|
||||||
confidence: medium
|
confidence: medium
|
||||||
rationale: 'No top-level package manifest, but tools/ holds three real, independently documented and
|
rationale: 'No top-level package manifest, but tools/ holds independently documented and runnable
|
||||||
runnable tools: iam-profile-conformance (executable checks, pytest fixtures), playbook-capability-contract
|
conformance, posture, scenario-composition, and guarded bootstrap tools; local-identity is separately
|
||||||
(executable validator), and security-bootstrap-console (local console + localhost web UI).'
|
packaged and tested.'
|
||||||
external_evidence:
|
external_evidence:
|
||||||
completeness:
|
completeness:
|
||||||
level: C1
|
level: C1
|
||||||
|
|
@ -35,7 +34,7 @@ external_evidence:
|
||||||
basis: scope_vs_intent_and_consumer_expectations
|
basis: scope_vs_intent_and_consumer_expectations
|
||||||
satisfied_expectations:
|
satisfied_expectations:
|
||||||
- versioned canon standards already implemented by a sibling repo (key-cape)
|
- versioned canon standards already implemented by a sibling repo (key-cape)
|
||||||
- three documented, runnable conformance/bootstrap tools under tools/
|
- documented, runnable conformance, posture, composition, and bootstrap tools under tools/
|
||||||
broken_expectations: []
|
broken_expectations: []
|
||||||
out_of_scope_expectations: []
|
out_of_scope_expectations: []
|
||||||
reliability:
|
reliability:
|
||||||
|
|
@ -46,13 +45,16 @@ external_evidence:
|
||||||
- no top-level packaging; each tool under tools/ has its own runtime dependencies, no unified install
|
- no top-level packaging; each tool under tools/ has its own runtime dependencies, no unified install
|
||||||
path yet
|
path yet
|
||||||
discovery:
|
discovery:
|
||||||
intent: Own the canonical IAM/security standards for the Coulomb ecosystem and provide executable conformance
|
intent: Own canonical NetKingdom security semantics and provide executable reference and conformance tooling
|
||||||
tooling so implementers (like key-cape) can verify against the standard rather than guessing.
|
so provider implementations can integrate without inferred authority or hidden deployment coupling.
|
||||||
includes:
|
includes:
|
||||||
- canon/standards/ versioned IAM and playbook-capability-contract standards
|
- canon/standards/ versioned IAM, tenancy, zone, credential, boundary, and composition standards
|
||||||
- IAM profile conformance checker
|
- IAM profile conformance checker
|
||||||
- playbook capability contract validator
|
- playbook capability contract validator
|
||||||
|
- deterministic plan-only security scenario composer
|
||||||
|
- tenancy posture schema and validator
|
||||||
- security bootstrap console (local, non-secret-collecting)
|
- security bootstrap console (local, non-secret-collecting)
|
||||||
|
- packaged local bootstrap identity CLI
|
||||||
excludes:
|
excludes:
|
||||||
- concrete IAM implementations themselves (see key-cape for lightweight mode)
|
- concrete IAM implementations themselves (see key-cape for lightweight mode)
|
||||||
- live secret value handling (bootstrap console explicitly refuses live OpenBao initialization)
|
- live secret value handling (bootstrap console explicitly refuses live OpenBao initialization)
|
||||||
|
|
@ -65,7 +67,10 @@ availability:
|
||||||
current_artifacts:
|
current_artifacts:
|
||||||
- tools/iam-profile-conformance
|
- tools/iam-profile-conformance
|
||||||
- tools/playbook-capability-contract
|
- tools/playbook-capability-contract
|
||||||
|
- tools/security-scenario-composer
|
||||||
|
- tools/tenancy-posture
|
||||||
- tools/security-bootstrap-console
|
- tools/security-bootstrap-console
|
||||||
|
- local-identity
|
||||||
target_artifacts: []
|
target_artifacts: []
|
||||||
consumption_modes:
|
consumption_modes:
|
||||||
- cli
|
- cli
|
||||||
|
|
@ -98,17 +103,28 @@ promotion_history: []
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`net-kingdom` provides a dynamic, self-optimizing security platform for Kubernetes-deployed infrastructure. It owns the canonical IAM and security standards (implemented by sibling repos like `key-cape`) and ships three executable conformance/bootstrap tools under `tools/`: IAM profile conformance checks, a playbook capability contract validator, and a non-secret-collecting security bootstrap console.
|
`net-kingdom` provides canonical IAM and security architecture, executable
|
||||||
|
conformance and posture checks, deterministic plan-only scenario composition,
|
||||||
|
a local bootstrap identity package, and a non-secret-collecting security
|
||||||
|
bootstrap console. Provider implementations remain in sibling repositories such
|
||||||
|
as `key-cape`; the self-optimizing platform remains an intent rather than a
|
||||||
|
current delivery claim.
|
||||||
|
|
||||||
## Assessment notes
|
## Assessment notes
|
||||||
|
|
||||||
### Discovery
|
### Discovery
|
||||||
|
|
||||||
README plus docs/secrets-engine-security-infrastructure-boundary.md describe an explicit integration boundary with OpenBao, flex-auth, user-engine, ops-warden, ops-bridge, info-tech-canon, and State Hub; canon/standards/ holds versioned standards (iam-profile_v0.2.md, playbook-capability-contract_v0.1.md) that key-cape and other repos implement against.
|
SCOPE plus docs/secrets-engine-security-infrastructure-boundary.md describe an
|
||||||
|
explicit integration boundary with OpenBao, flex-auth, user-engine, ops-warden,
|
||||||
|
ops-bridge, info-tech-canon, and State Hub. `canon/standards/` holds the current
|
||||||
|
IAM v0.3 and related boundary, posture, zone, credential, playbook, and scenario
|
||||||
|
composition standards.
|
||||||
|
|
||||||
### Availability
|
### Availability
|
||||||
|
|
||||||
No top-level package manifest, but tools/ holds three real, independently documented and runnable tools: iam-profile-conformance (executable checks, pytest fixtures), playbook-capability-contract (executable validator), and security-bootstrap-console (local console + localhost web UI).
|
No top-level package manifest exists. The independently runnable surfaces are
|
||||||
|
documented under `tools/`, and `local-identity` has its own package manifest and
|
||||||
|
test environment.
|
||||||
|
|
||||||
### Completeness
|
### Completeness
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
version: 1
|
version: 1
|
||||||
updated: '2026-07-06'
|
updated: '2026-08-23'
|
||||||
domain: helix_forge
|
domain: helix_forge
|
||||||
capabilities:
|
capabilities:
|
||||||
- id: capability.security.iam-tooling-suite
|
- id: capability.security.iam-tooling-suite
|
||||||
name: NetKingdom Security/IAM Tooling Suite
|
name: NetKingdom Security/IAM Tooling Suite
|
||||||
summary: Dynamic, self-optimizing security platform for Kubernetes-deployed IT infrastructure; owns
|
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture
|
||||||
canonical IAM/security standards and executable conformance tooling (IAM profile conformance, playbook
|
validation, and deterministic plan-only security scenario composition.
|
||||||
capability contract validation, security bootstrap console).
|
|
||||||
vector: D3 / A2 / C1 / R1
|
vector: D3 / A2 / C1 / R1
|
||||||
domain: infotech
|
domain: infotech
|
||||||
status: draft
|
status: draft
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ python3 tools/playbook-capability-contract/playbook_contract_validator.py \
|
||||||
../railiance-infra/capabilities/playbooks/railiance-infra.bootstrap-host.yaml
|
../railiance-infra/capabilities/playbooks/railiance-infra.bootstrap-host.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Validate and compose a sample scenario:
|
Validate and compose the legacy single-provider conformance sample:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/playbook-capability-contract/playbook_contract_validator.py \
|
python3 tools/playbook-capability-contract/playbook_contract_validator.py \
|
||||||
|
|
@ -21,6 +21,17 @@ python3 tools/playbook-capability-contract/playbook_contract_validator.py \
|
||||||
--scenario examples/playbook-capability-contract/scenario-s1-host-bootstrap.yaml
|
--scenario examples/playbook-capability-contract/scenario-s1-host-bootstrap.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `--scenario` path exists for v0.1 conformance compatibility. It refuses
|
||||||
|
ambiguous providers and unselected overrides, but it does not perform trust
|
||||||
|
sequencing or build a complete owner handoff. Use the canonical Security
|
||||||
|
Scenario Composer for deterministic multi-provider plans:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/security-scenario-composer/security_scenario_composer.py \
|
||||||
|
--scenario examples/security-scenarios/c0-local-identity.yaml \
|
||||||
|
capabilities/playbooks/net-kingdom.local-identity.yaml
|
||||||
|
```
|
||||||
|
|
||||||
Run tests:
|
Run tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,9 @@ TRUST_STATES = {
|
||||||
"tenant_onboarding_trust",
|
"tenant_onboarding_trust",
|
||||||
}
|
}
|
||||||
SCENARIO_AUTHORITIES = {"platform", "netkingdom", "tenant"}
|
SCENARIO_AUTHORITIES = {"platform", "netkingdom", "tenant"}
|
||||||
|
SECRET_REFERENCE_PATTERN = re.compile(
|
||||||
|
r"(?:[a-z][a-z0-9+.-]*://\S+|(?:/|\./|\.\./)\S+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -127,6 +130,10 @@ def is_type(value: Any, declared_type: str) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_secret_reference(value: Any) -> bool:
|
||||||
|
return isinstance(value, str) and SECRET_REFERENCE_PATTERN.fullmatch(value) is not None
|
||||||
|
|
||||||
|
|
||||||
def validate_constraints(value: Any, param: dict[str, Any], path: str) -> list[Issue]:
|
def validate_constraints(value: Any, param: dict[str, Any], path: str) -> list[Issue]:
|
||||||
issues: list[Issue] = []
|
issues: list[Issue] = []
|
||||||
constraints = param.get("constraints", {})
|
constraints = param.get("constraints", {})
|
||||||
|
|
@ -277,6 +284,18 @@ def validate_parameters(spec: dict[str, Any]) -> list[Issue]:
|
||||||
if item.get("sensitivity") in {"security_sensitive", "secret_reference"} and item.get("tuning_authority") == "tenant_tunable":
|
if item.get("sensitivity") in {"security_sensitive", "secret_reference"} and item.get("tuning_authority") == "tenant_tunable":
|
||||||
issues.append(issue("ERROR", path, "security-sensitive parameters cannot be tenant_tunable"))
|
issues.append(issue("ERROR", path, "security-sensitive parameters cannot be tenant_tunable"))
|
||||||
|
|
||||||
|
if item.get("sensitivity") == "secret_reference":
|
||||||
|
if declared_type != "string":
|
||||||
|
issues.append(issue("ERROR", f"{path}.type", "secret_reference parameters must use string type"))
|
||||||
|
if "default" in item and not is_secret_reference(item["default"]):
|
||||||
|
issues.append(
|
||||||
|
issue(
|
||||||
|
"ERROR",
|
||||||
|
f"{path}.default",
|
||||||
|
"secret_reference default must be a URI or explicit absolute/relative path",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if "default" in item and declared_type in PARAM_TYPES and not is_type(item["default"], declared_type):
|
if "default" in item and declared_type in PARAM_TYPES and not is_type(item["default"], declared_type):
|
||||||
issues.append(issue("ERROR", f"{path}.default", f"default does not match type {declared_type!r}"))
|
issues.append(issue("ERROR", f"{path}.default", f"default does not match type {declared_type!r}"))
|
||||||
if declared_type in PARAM_TYPES:
|
if declared_type in PARAM_TYPES:
|
||||||
|
|
@ -405,6 +424,15 @@ def validate_override_allowed(param: dict[str, Any], value: Any, scenario_author
|
||||||
if scenario_authority == "tenant" and sensitivity in {"security_sensitive", "secret_reference"}:
|
if scenario_authority == "tenant" and sensitivity in {"security_sensitive", "secret_reference"}:
|
||||||
issues.append(issue("ERROR", path, f"tenant authority cannot override {sensitivity} parameter {name!r}"))
|
issues.append(issue("ERROR", path, f"tenant authority cannot override {sensitivity} parameter {name!r}"))
|
||||||
|
|
||||||
|
if sensitivity == "secret_reference" and not is_secret_reference(value):
|
||||||
|
issues.append(
|
||||||
|
issue(
|
||||||
|
"ERROR",
|
||||||
|
path,
|
||||||
|
f"override for {name!r} must be a secret-reference URI or explicit absolute/relative path",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not is_type(value, declared_type):
|
if not is_type(value, declared_type):
|
||||||
issues.append(issue("ERROR", path, f"override for {name!r} does not match type {declared_type!r}"))
|
issues.append(issue("ERROR", path, f"override for {name!r} does not match type {declared_type!r}"))
|
||||||
issues.extend(validate_constraints(value, param, path))
|
issues.extend(validate_constraints(value, param, path))
|
||||||
|
|
@ -433,10 +461,23 @@ def compose_scenario(declarations: list[Declaration], scenario: dict[str, Any])
|
||||||
|
|
||||||
selected: list[Declaration] = []
|
selected: list[Declaration] = []
|
||||||
for cap_id in required_caps:
|
for cap_id in required_caps:
|
||||||
matches = [declaration for declaration in declarations if cap_id in declaration.capabilities]
|
matches = sorted(
|
||||||
|
[declaration for declaration in declarations if cap_id in declaration.capabilities],
|
||||||
|
key=lambda declaration: declaration.id,
|
||||||
|
)
|
||||||
if not matches:
|
if not matches:
|
||||||
issues.append(issue("ERROR", "scenario.requires.capabilities", f"no declaration provides {cap_id!r}"))
|
issues.append(issue("ERROR", "scenario.requires.capabilities", f"no declaration provides {cap_id!r}"))
|
||||||
continue
|
continue
|
||||||
|
if len(matches) > 1:
|
||||||
|
issues.append(
|
||||||
|
issue(
|
||||||
|
"ERROR",
|
||||||
|
"scenario.requires.capabilities",
|
||||||
|
f"ambiguous providers {[declaration.id for declaration in matches]}; "
|
||||||
|
"use the Security Scenario Composer to pin one explicitly",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
selected.append(matches[0])
|
selected.append(matches[0])
|
||||||
|
|
||||||
# Preserve order while deduplicating declarations selected for several capabilities.
|
# Preserve order while deduplicating declarations selected for several capabilities.
|
||||||
|
|
@ -444,6 +485,16 @@ def compose_scenario(declarations: list[Declaration], scenario: dict[str, Any])
|
||||||
for declaration in selected:
|
for declaration in selected:
|
||||||
selected_by_id.setdefault(declaration.id, declaration)
|
selected_by_id.setdefault(declaration.id, declaration)
|
||||||
|
|
||||||
|
for declaration_id in overrides:
|
||||||
|
if declaration_id not in selected_by_id:
|
||||||
|
issues.append(
|
||||||
|
issue(
|
||||||
|
"ERROR",
|
||||||
|
f"scenario.parameter_overrides.{declaration_id}",
|
||||||
|
"override targets an unselected declaration",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
composed = {
|
composed = {
|
||||||
"scenario": scenario.get("id", "scenario:unnamed"),
|
"scenario": scenario.get("id", "scenario:unnamed"),
|
||||||
"authority": authority,
|
"authority": authority,
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,81 @@ def test_tenant_tunable_secret_reference_fails(tmp_path):
|
||||||
assert any("security-sensitive parameters cannot be tenant_tunable" in msg for msg in error_messages(issues))
|
assert any("security-sensitive parameters cannot be tenant_tunable" in msg for msg in error_messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_plaintext_secret_reference_default_fails(tmp_path):
|
||||||
|
data = valid_declaration()
|
||||||
|
data["spec"]["parameters"][2].update(
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "plaintext-looking-value",
|
||||||
|
"sensitivity": "secret_reference",
|
||||||
|
"tuning_authority": "platform_only",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
declaration = declaration_from(data, tmp_path)
|
||||||
|
|
||||||
|
issues = validator.validate_declaration(declaration)
|
||||||
|
|
||||||
|
assert any("secret_reference default must be" in msg for msg in error_messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_secret_reference_uri_override_passes(tmp_path):
|
||||||
|
data = valid_declaration()
|
||||||
|
data["spec"]["parameters"][2].update(
|
||||||
|
{
|
||||||
|
"name": "credential_ref",
|
||||||
|
"type": "string",
|
||||||
|
"default": "openbao://kv/platform/reference",
|
||||||
|
"sensitivity": "secret_reference",
|
||||||
|
"tuning_authority": "platform_only",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
declaration = declaration_from(data, tmp_path)
|
||||||
|
scenario = {
|
||||||
|
"id": "scenario:secret-reference",
|
||||||
|
"authority": "platform",
|
||||||
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
||||||
|
"parameter_overrides": {
|
||||||
|
"railiance-infra.bootstrap-host": {
|
||||||
|
"target_hosts": ["railiance01"],
|
||||||
|
"credential_ref": "kubernetes://platform/reference#token",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, _ = validator.compose_scenario([declaration], scenario)
|
||||||
|
|
||||||
|
assert error_messages(issues) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_plaintext_secret_reference_override_fails(tmp_path):
|
||||||
|
data = valid_declaration()
|
||||||
|
data["spec"]["parameters"][2].update(
|
||||||
|
{
|
||||||
|
"name": "credential_ref",
|
||||||
|
"type": "string",
|
||||||
|
"default": "openbao://kv/platform/reference",
|
||||||
|
"sensitivity": "secret_reference",
|
||||||
|
"tuning_authority": "platform_only",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
declaration = declaration_from(data, tmp_path)
|
||||||
|
scenario = {
|
||||||
|
"id": "scenario:bad-secret-reference",
|
||||||
|
"authority": "platform",
|
||||||
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
||||||
|
"parameter_overrides": {
|
||||||
|
"railiance-infra.bootstrap-host": {
|
||||||
|
"target_hosts": ["railiance01"],
|
||||||
|
"credential_ref": "plaintext-looking-value",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, _ = validator.compose_scenario([declaration], scenario)
|
||||||
|
|
||||||
|
assert any("must be a secret-reference URI" in msg for msg in error_messages(issues))
|
||||||
|
|
||||||
|
|
||||||
def test_scenario_composition_selects_and_overrides(tmp_path):
|
def test_scenario_composition_selects_and_overrides(tmp_path):
|
||||||
declaration = declaration_from(valid_declaration(), tmp_path)
|
declaration = declaration_from(valid_declaration(), tmp_path)
|
||||||
scenario = {
|
scenario = {
|
||||||
|
|
@ -196,3 +271,36 @@ def test_required_parameter_without_override_fails(tmp_path):
|
||||||
issues, _ = validator.compose_scenario([declaration], scenario)
|
issues, _ = validator.compose_scenario([declaration], scenario)
|
||||||
|
|
||||||
assert any("required parameter has no default or override" in msg for msg in error_messages(issues))
|
assert any("required parameter has no default or override" in msg for msg in error_messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_composition_refuses_ambiguous_provider(tmp_path):
|
||||||
|
first = declaration_from(valid_declaration(), tmp_path)
|
||||||
|
second_data = valid_declaration()
|
||||||
|
second_data["metadata"]["id"] = "railiance-infra.bootstrap-host-alternative"
|
||||||
|
second_path = tmp_path / "alternative.yaml"
|
||||||
|
second_path.write_text(yaml.safe_dump(second_data, sort_keys=False), encoding="utf-8")
|
||||||
|
second = validator.Declaration(path=second_path, data=second_data)
|
||||||
|
scenario = {
|
||||||
|
"id": "scenario:ambiguous-provider",
|
||||||
|
"authority": "platform",
|
||||||
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
||||||
|
"parameter_overrides": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, _ = validator.compose_scenario([second, first], scenario)
|
||||||
|
|
||||||
|
assert any("ambiguous providers" in msg for msg in error_messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_composition_refuses_unselected_override(tmp_path):
|
||||||
|
declaration = declaration_from(valid_declaration(), tmp_path)
|
||||||
|
scenario = {
|
||||||
|
"id": "scenario:unselected-override",
|
||||||
|
"authority": "platform",
|
||||||
|
"requires": {"capabilities": ["s1.os-baseline"]},
|
||||||
|
"parameter_overrides": {"owner.unselected": {"target_hosts": ["railiance01"]}},
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, _ = validator.compose_scenario([declaration], scenario)
|
||||||
|
|
||||||
|
assert any("override targets an unselected declaration" in msg for msg in error_messages(issues))
|
||||||
|
|
|
||||||
24
tools/security-scenario-composer/README.md
Normal file
24
tools/security-scenario-composer/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Security Scenario Composer
|
||||||
|
|
||||||
|
The composer is the executable, plan-only implementation of
|
||||||
|
`canon/standards/security-scenario-composition_v0.1.md`. It validates
|
||||||
|
Playbook Capability Contract v0.1 declarations, selects exact providers,
|
||||||
|
applies authority-bound overrides, orders trust transitions, and emits an
|
||||||
|
owner-routed JSON handoff.
|
||||||
|
|
||||||
|
It never invokes an entry point and always emits
|
||||||
|
`execution.permitted: false`.
|
||||||
|
|
||||||
|
Compose the checked-in C0 reference:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/security-scenario-composer/security_scenario_composer.py \
|
||||||
|
--scenario examples/security-scenarios/c0-local-identity.yaml \
|
||||||
|
capabilities/playbooks/net-kingdom.local-identity.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pytest tools/security-scenario-composer/tests
|
||||||
|
```
|
||||||
445
tools/security-scenario-composer/security_scenario_composer.py
Normal file
445
tools/security-scenario-composer/security_scenario_composer.py
Normal file
|
|
@ -0,0 +1,445 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compose a deterministic, plan-only NetKingdom security scenario."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
TOOL_DIR = Path(__file__).resolve().parent
|
||||||
|
CONTRACT_TOOL_DIR = TOOL_DIR.parent / "playbook-capability-contract"
|
||||||
|
sys.path.insert(0, str(CONTRACT_TOOL_DIR))
|
||||||
|
|
||||||
|
import playbook_contract_validator as contract # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
PLAN_API_VERSION = "netkingdom.io/security-scenario-composition/v0.1"
|
||||||
|
PLAN_KIND = "SecurityScenarioComposition"
|
||||||
|
SCENARIO_ID = re.compile(r"^scenario:[a-z0-9][a-z0-9._:-]*$")
|
||||||
|
SCENARIO_KEYS = {
|
||||||
|
"id",
|
||||||
|
"authority",
|
||||||
|
"initial_trust",
|
||||||
|
"requires",
|
||||||
|
"providers",
|
||||||
|
"parameter_overrides",
|
||||||
|
}
|
||||||
|
|
||||||
|
Issue = contract.Issue
|
||||||
|
Declaration = contract.Declaration
|
||||||
|
|
||||||
|
|
||||||
|
def error(path: str, message: str) -> Issue:
|
||||||
|
return Issue("ERROR", path, message)
|
||||||
|
|
||||||
|
|
||||||
|
def has_errors(issues: list[Issue]) -> bool:
|
||||||
|
return any(item.level == "ERROR" for item in issues)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_scenario(scenario: dict[str, Any]) -> list[Issue]:
|
||||||
|
issues: list[Issue] = []
|
||||||
|
unknown_keys = sorted(set(scenario) - SCENARIO_KEYS)
|
||||||
|
if unknown_keys:
|
||||||
|
issues.append(error("scenario", f"unknown fields: {unknown_keys}"))
|
||||||
|
|
||||||
|
scenario_id = scenario.get("id")
|
||||||
|
if not isinstance(scenario_id, str) or not SCENARIO_ID.fullmatch(scenario_id):
|
||||||
|
issues.append(error("scenario.id", "must match 'scenario:<lowercase-id>'"))
|
||||||
|
|
||||||
|
authority = scenario.get("authority")
|
||||||
|
if authority not in contract.SCENARIO_AUTHORITIES:
|
||||||
|
issues.append(error("scenario.authority", f"unknown authority {authority!r}"))
|
||||||
|
|
||||||
|
requires = scenario.get("requires")
|
||||||
|
if not isinstance(requires, dict):
|
||||||
|
issues.append(error("scenario.requires", "must be an object"))
|
||||||
|
required_caps: list[Any] = []
|
||||||
|
else:
|
||||||
|
unknown_requires = sorted(set(requires) - {"capabilities"})
|
||||||
|
if unknown_requires:
|
||||||
|
issues.append(error("scenario.requires", f"unknown fields: {unknown_requires}"))
|
||||||
|
required_caps = requires.get("capabilities", [])
|
||||||
|
if not isinstance(required_caps, list) or not required_caps:
|
||||||
|
issues.append(error("scenario.requires.capabilities", "must be a non-empty list"))
|
||||||
|
required_caps = []
|
||||||
|
|
||||||
|
seen_caps: set[str] = set()
|
||||||
|
for index, cap_id in enumerate(required_caps):
|
||||||
|
path = f"scenario.requires.capabilities[{index}]"
|
||||||
|
if cap_id not in contract.CAPABILITIES:
|
||||||
|
issues.append(error(path, f"unknown capability id {cap_id!r}"))
|
||||||
|
if cap_id in seen_caps:
|
||||||
|
issues.append(error(path, f"duplicate capability id {cap_id!r}"))
|
||||||
|
if isinstance(cap_id, str):
|
||||||
|
seen_caps.add(cap_id)
|
||||||
|
|
||||||
|
initial_trust = scenario.get("initial_trust", [])
|
||||||
|
if not isinstance(initial_trust, list):
|
||||||
|
issues.append(error("scenario.initial_trust", "must be a list"))
|
||||||
|
initial_trust = []
|
||||||
|
seen_trust: set[str] = set()
|
||||||
|
for index, state in enumerate(initial_trust):
|
||||||
|
path = f"scenario.initial_trust[{index}]"
|
||||||
|
if state not in contract.TRUST_STATES:
|
||||||
|
issues.append(error(path, f"unknown trust state {state!r}"))
|
||||||
|
if state in seen_trust:
|
||||||
|
issues.append(error(path, f"duplicate trust state {state!r}"))
|
||||||
|
if isinstance(state, str):
|
||||||
|
seen_trust.add(state)
|
||||||
|
|
||||||
|
providers = scenario.get("providers", {})
|
||||||
|
if not isinstance(providers, dict):
|
||||||
|
issues.append(error("scenario.providers", "must be an object"))
|
||||||
|
providers = {}
|
||||||
|
for cap_id, declaration_id in providers.items():
|
||||||
|
path = f"scenario.providers.{cap_id}"
|
||||||
|
if cap_id not in seen_caps:
|
||||||
|
issues.append(error(path, "provider pin targets an unrequested capability"))
|
||||||
|
if not isinstance(declaration_id, str) or not declaration_id:
|
||||||
|
issues.append(error(path, "provider declaration id must be a non-empty string"))
|
||||||
|
|
||||||
|
overrides = scenario.get("parameter_overrides", {})
|
||||||
|
if not isinstance(overrides, dict):
|
||||||
|
issues.append(error("scenario.parameter_overrides", "must be an object"))
|
||||||
|
else:
|
||||||
|
for declaration_id, values in overrides.items():
|
||||||
|
if not isinstance(declaration_id, str) or not declaration_id:
|
||||||
|
issues.append(error("scenario.parameter_overrides", "declaration ids must be non-empty strings"))
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
issues.append(error(f"scenario.parameter_overrides.{declaration_id}", "must be an object"))
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def validate_declarations(declarations: list[Declaration]) -> list[Issue]:
|
||||||
|
issues: list[Issue] = []
|
||||||
|
ids: dict[str, list[str]] = {}
|
||||||
|
for declaration in declarations:
|
||||||
|
for item in contract.validate_declaration(declaration):
|
||||||
|
issues.append(
|
||||||
|
Issue(
|
||||||
|
item.level,
|
||||||
|
f"declarations[{declaration.path}].{item.path}",
|
||||||
|
item.message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ids.setdefault(declaration.id, []).append(str(declaration.path))
|
||||||
|
|
||||||
|
for declaration_id, paths in sorted(ids.items()):
|
||||||
|
if not declaration_id:
|
||||||
|
continue
|
||||||
|
if len(paths) > 1:
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
"declarations",
|
||||||
|
f"duplicate declaration id {declaration_id!r}: {sorted(paths)}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def declaration_trust(declaration: Declaration, section: str) -> list[dict[str, Any]]:
|
||||||
|
trust = declaration.data.get("spec", {}).get("trust", {})
|
||||||
|
values = trust.get(section, []) if isinstance(trust, dict) else []
|
||||||
|
return values if isinstance(values, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def trust_states(declaration: Declaration, section: str) -> set[str]:
|
||||||
|
return {
|
||||||
|
str(item.get("state"))
|
||||||
|
for item in declaration_trust(declaration, section)
|
||||||
|
if isinstance(item, dict) and item.get("state")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def stable_digest(scenario: dict[str, Any], selected: dict[str, Declaration]) -> str:
|
||||||
|
payload = {
|
||||||
|
"scenario": scenario,
|
||||||
|
"declarations": {
|
||||||
|
declaration_id: selected[declaration_id].data
|
||||||
|
for declaration_id in sorted(selected)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
|
def select_declarations(
|
||||||
|
declarations: list[Declaration],
|
||||||
|
scenario: dict[str, Any],
|
||||||
|
) -> tuple[list[Issue], dict[str, Declaration], dict[str, str]]:
|
||||||
|
issues: list[Issue] = []
|
||||||
|
required_caps = scenario["requires"]["capabilities"]
|
||||||
|
pins = scenario.get("providers", {})
|
||||||
|
selected: dict[str, Declaration] = {}
|
||||||
|
capability_providers: dict[str, str] = {}
|
||||||
|
|
||||||
|
for cap_id in required_caps:
|
||||||
|
matches = sorted(
|
||||||
|
(declaration for declaration in declarations if cap_id in declaration.capabilities),
|
||||||
|
key=lambda declaration: declaration.id,
|
||||||
|
)
|
||||||
|
pin = pins.get(cap_id)
|
||||||
|
if pin is not None:
|
||||||
|
pinned = [declaration for declaration in matches if declaration.id == pin]
|
||||||
|
if not pinned:
|
||||||
|
available = [declaration.id for declaration in matches]
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
f"scenario.providers.{cap_id}",
|
||||||
|
f"pinned declaration {pin!r} does not provide {cap_id!r}; available={available}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
chosen = pinned[0]
|
||||||
|
elif not matches:
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
"scenario.requires.capabilities",
|
||||||
|
f"no declaration provides {cap_id!r}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
elif len(matches) > 1:
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
f"scenario.providers.{cap_id}",
|
||||||
|
f"ambiguous providers {[item.id for item in matches]}; pin one explicitly",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
chosen = matches[0]
|
||||||
|
|
||||||
|
selected[chosen.id] = chosen
|
||||||
|
capability_providers[str(cap_id)] = chosen.id
|
||||||
|
|
||||||
|
return issues, selected, capability_providers
|
||||||
|
|
||||||
|
|
||||||
|
def effective_parameters(
|
||||||
|
declaration: Declaration,
|
||||||
|
overrides: dict[str, Any],
|
||||||
|
authority: str,
|
||||||
|
) -> tuple[list[Issue], dict[str, Any]]:
|
||||||
|
issues: list[Issue] = []
|
||||||
|
params_out: dict[str, Any] = {}
|
||||||
|
for name in sorted(overrides):
|
||||||
|
if name not in declaration.parameters:
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
f"scenario.parameter_overrides.{declaration.id}.{name}",
|
||||||
|
"unknown parameter override",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, param in sorted(declaration.parameters.items()):
|
||||||
|
overridden, value = contract.effective_parameter_value(param, overrides, declaration.id)
|
||||||
|
path = f"scenario.parameter_overrides.{declaration.id}.{name}"
|
||||||
|
if param.get("required") is True and value is None:
|
||||||
|
issues.append(error(path, "required parameter has no default or override"))
|
||||||
|
if overridden:
|
||||||
|
issues.extend(contract.validate_override_allowed(param, value, authority, path))
|
||||||
|
params_out[name] = {
|
||||||
|
"value": value,
|
||||||
|
"source": "override" if overridden else "default",
|
||||||
|
"sensitivity": param.get("sensitivity"),
|
||||||
|
"tuning_authority": param.get("tuning_authority"),
|
||||||
|
}
|
||||||
|
return issues, params_out
|
||||||
|
|
||||||
|
|
||||||
|
def sequence_declarations(
|
||||||
|
selected: dict[str, Declaration],
|
||||||
|
initial_trust: set[str],
|
||||||
|
) -> tuple[list[Issue], list[Declaration], set[str]]:
|
||||||
|
issues: list[Issue] = []
|
||||||
|
established = set(initial_trust)
|
||||||
|
remaining = dict(selected)
|
||||||
|
ordered: list[Declaration] = []
|
||||||
|
|
||||||
|
while remaining:
|
||||||
|
eligible = sorted(
|
||||||
|
(
|
||||||
|
declaration
|
||||||
|
for declaration in remaining.values()
|
||||||
|
if trust_states(declaration, "requires") <= established
|
||||||
|
),
|
||||||
|
key=lambda declaration: declaration.id,
|
||||||
|
)
|
||||||
|
if not eligible:
|
||||||
|
for declaration_id, declaration in sorted(remaining.items()):
|
||||||
|
missing = sorted(trust_states(declaration, "requires") - established)
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
f"composition.execution_steps.{declaration_id}",
|
||||||
|
f"cannot sequence declaration; unresolved trust states: {missing}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
chosen = eligible[0]
|
||||||
|
ordered.append(chosen)
|
||||||
|
established.update(trust_states(chosen, "satisfies"))
|
||||||
|
del remaining[chosen.id]
|
||||||
|
|
||||||
|
return issues, ordered, established
|
||||||
|
|
||||||
|
|
||||||
|
def compose_scenario(
|
||||||
|
declarations: list[Declaration],
|
||||||
|
scenario: dict[str, Any],
|
||||||
|
) -> tuple[list[Issue], dict[str, Any] | None]:
|
||||||
|
issues = validate_scenario(scenario)
|
||||||
|
issues.extend(validate_declarations(declarations))
|
||||||
|
if has_errors(issues):
|
||||||
|
return issues, None
|
||||||
|
|
||||||
|
selection_issues, selected, capability_providers = select_declarations(declarations, scenario)
|
||||||
|
issues.extend(selection_issues)
|
||||||
|
if has_errors(issues):
|
||||||
|
return issues, None
|
||||||
|
|
||||||
|
overrides = scenario.get("parameter_overrides", {})
|
||||||
|
for declaration_id in sorted(overrides):
|
||||||
|
if declaration_id not in selected:
|
||||||
|
issues.append(
|
||||||
|
error(
|
||||||
|
f"scenario.parameter_overrides.{declaration_id}",
|
||||||
|
"override targets an unselected declaration",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
parameters: dict[str, dict[str, Any]] = {}
|
||||||
|
authority = str(scenario["authority"])
|
||||||
|
for declaration_id, declaration in sorted(selected.items()):
|
||||||
|
param_issues, values = effective_parameters(
|
||||||
|
declaration,
|
||||||
|
overrides.get(declaration_id, {}),
|
||||||
|
authority,
|
||||||
|
)
|
||||||
|
issues.extend(param_issues)
|
||||||
|
parameters[declaration_id] = values
|
||||||
|
|
||||||
|
sequence_issues, ordered, planned_trust = sequence_declarations(
|
||||||
|
selected,
|
||||||
|
set(scenario.get("initial_trust", [])),
|
||||||
|
)
|
||||||
|
issues.extend(sequence_issues)
|
||||||
|
if has_errors(issues):
|
||||||
|
return issues, None
|
||||||
|
|
||||||
|
execution_steps: list[dict[str, Any]] = []
|
||||||
|
responsibilities: list[dict[str, Any]] = []
|
||||||
|
for index, declaration in enumerate(ordered, start=1):
|
||||||
|
metadata = declaration.data["metadata"]
|
||||||
|
spec = declaration.data["spec"]
|
||||||
|
required = declaration_trust(declaration, "requires")
|
||||||
|
satisfied = declaration_trust(declaration, "satisfies")
|
||||||
|
readiness = [
|
||||||
|
{
|
||||||
|
"state": item["state"],
|
||||||
|
"checks": item.get("readiness_checks", []),
|
||||||
|
}
|
||||||
|
for item in satisfied
|
||||||
|
]
|
||||||
|
execution_steps.append(
|
||||||
|
{
|
||||||
|
"order": index,
|
||||||
|
"declaration_id": declaration.id,
|
||||||
|
"executor_owner": metadata["owner"],
|
||||||
|
"repo": metadata["repo"],
|
||||||
|
"capabilities": sorted(
|
||||||
|
cap_id
|
||||||
|
for cap_id, provider in capability_providers.items()
|
||||||
|
if provider == declaration.id
|
||||||
|
),
|
||||||
|
"entry_point": spec["playbook"],
|
||||||
|
"parameters": parameters[declaration.id],
|
||||||
|
"requires_trust": required,
|
||||||
|
"satisfies_trust": satisfied,
|
||||||
|
"readiness_obligations": readiness,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for responsibility in spec["responsibilities"]:
|
||||||
|
responsibilities.append(
|
||||||
|
{
|
||||||
|
"declaration_id": declaration.id,
|
||||||
|
**responsibility,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
composition = {
|
||||||
|
"apiVersion": PLAN_API_VERSION,
|
||||||
|
"kind": PLAN_KIND,
|
||||||
|
"scenario": scenario["id"],
|
||||||
|
"authority": authority,
|
||||||
|
"composition_digest": stable_digest(scenario, selected),
|
||||||
|
"requested_capabilities": list(scenario["requires"]["capabilities"]),
|
||||||
|
"capability_providers": capability_providers,
|
||||||
|
"initial_trust": sorted(scenario.get("initial_trust", [])),
|
||||||
|
"planned_final_trust": sorted(planned_trust),
|
||||||
|
"execution": {
|
||||||
|
"mode": "plan-only",
|
||||||
|
"permitted": False,
|
||||||
|
"reason": "Composition does not authorize or perform provider execution.",
|
||||||
|
},
|
||||||
|
"execution_steps": execution_steps,
|
||||||
|
"responsibility_map": responsibilities,
|
||||||
|
}
|
||||||
|
return issues, composition
|
||||||
|
|
||||||
|
|
||||||
|
def load_declarations(paths: list[str]) -> tuple[list[Declaration], list[Issue]]:
|
||||||
|
declarations: list[Declaration] = []
|
||||||
|
issues: list[Issue] = []
|
||||||
|
for raw_path in paths:
|
||||||
|
path = Path(raw_path)
|
||||||
|
try:
|
||||||
|
data = contract.load_yaml(path)
|
||||||
|
except Exception as exc:
|
||||||
|
issues.append(error(f"declarations[{path}]", f"failed to load: {exc}"))
|
||||||
|
continue
|
||||||
|
declarations.append(Declaration(path=path, data=data))
|
||||||
|
return declarations, issues
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Compose a deterministic, non-executing NetKingdom security scenario."
|
||||||
|
)
|
||||||
|
parser.add_argument("declarations", nargs="+", help="Playbook capability declaration YAML files")
|
||||||
|
parser.add_argument("--scenario", required=True, help="Security scenario YAML file")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
declarations, issues = load_declarations(args.declarations)
|
||||||
|
try:
|
||||||
|
scenario = contract.load_yaml(Path(args.scenario))
|
||||||
|
except Exception as exc:
|
||||||
|
issues.append(error("scenario", f"failed to load {args.scenario}: {exc}"))
|
||||||
|
scenario = {}
|
||||||
|
|
||||||
|
composition: dict[str, Any] | None = None
|
||||||
|
if not has_errors(issues):
|
||||||
|
composition_issues, composition = compose_scenario(declarations, scenario)
|
||||||
|
issues.extend(composition_issues)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"ok": not has_errors(issues),
|
||||||
|
"issues": [item.__dict__ for item in issues],
|
||||||
|
"composition": composition,
|
||||||
|
}
|
||||||
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||||
|
return 1 if has_errors(issues) else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
import copy
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
TOOL_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
TOOL_PATH = TOOL_DIR / "security_scenario_composer.py"
|
||||||
|
sys.path.insert(0, str(TOOL_DIR))
|
||||||
|
SPEC = importlib.util.spec_from_file_location("security_scenario_composer", TOOL_PATH)
|
||||||
|
composer = importlib.util.module_from_spec(SPEC)
|
||||||
|
assert SPEC.loader is not None
|
||||||
|
sys.modules[SPEC.name] = composer
|
||||||
|
SPEC.loader.exec_module(composer)
|
||||||
|
|
||||||
|
|
||||||
|
def declaration_data(
|
||||||
|
declaration_id="owner.c0",
|
||||||
|
capability="c0.bootstrap-identity",
|
||||||
|
tier="C0",
|
||||||
|
requires=None,
|
||||||
|
satisfies=None,
|
||||||
|
):
|
||||||
|
requires = ["bare_host_trust"] if requires is None else requires
|
||||||
|
satisfies = ["bootstrap_identity_trust"] if satisfies is None else satisfies
|
||||||
|
return {
|
||||||
|
"apiVersion": composer.contract.API_VERSION,
|
||||||
|
"kind": composer.contract.KIND,
|
||||||
|
"metadata": {
|
||||||
|
"id": declaration_id,
|
||||||
|
"name": declaration_id,
|
||||||
|
"owner": "owner",
|
||||||
|
"repo": "owner-repo",
|
||||||
|
"domain": "infotech",
|
||||||
|
"contract_version": "0.1",
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"playbook": {
|
||||||
|
"path": "playbooks/reference.yaml",
|
||||||
|
"type": "reference",
|
||||||
|
"invocation": "make reference",
|
||||||
|
"description": "Reference entry point.",
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
{
|
||||||
|
"id": capability,
|
||||||
|
"tier": tier,
|
||||||
|
"resource_kinds": ["identities"],
|
||||||
|
"description": "Reference capability.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "target",
|
||||||
|
"type": "string",
|
||||||
|
"required": True,
|
||||||
|
"default": "reference",
|
||||||
|
"sensitivity": "operational",
|
||||||
|
"tuning_authority": "netkingdom_tunable",
|
||||||
|
"description": "Reference target.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "secure_mode",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": False,
|
||||||
|
"default": True,
|
||||||
|
"sensitivity": "security_sensitive",
|
||||||
|
"tuning_authority": "platform_only",
|
||||||
|
"description": "Security-sensitive reference switch.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"responsibilities": [
|
||||||
|
{
|
||||||
|
"resource_kind": "identities",
|
||||||
|
"owner": "owner",
|
||||||
|
"resources": ["identity:reference"],
|
||||||
|
"repo_owns": "Reference execution.",
|
||||||
|
"netkingdom_orchestrates": "Reference selection.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trust": {
|
||||||
|
"requires": [
|
||||||
|
{"state": state, "readiness_checks": []}
|
||||||
|
for state in requires
|
||||||
|
],
|
||||||
|
"satisfies": [
|
||||||
|
{
|
||||||
|
"state": state,
|
||||||
|
"readiness_checks": [
|
||||||
|
{
|
||||||
|
"id": f"{state}-ready",
|
||||||
|
"description": f"{state} is ready.",
|
||||||
|
"evidence": "reference evidence",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for state in satisfies
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"publish": f"capabilities/playbooks/{declaration_id}.yaml",
|
||||||
|
"maturity": "reference",
|
||||||
|
"consumers": ["netkingdom-security-scenario-composer"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def declaration(tmp_path, data, filename="declaration.yaml"):
|
||||||
|
path = tmp_path / filename
|
||||||
|
path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
||||||
|
return composer.Declaration(path=path, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def scenario(capabilities=None):
|
||||||
|
capabilities = ["c0.bootstrap-identity"] if capabilities is None else capabilities
|
||||||
|
return {
|
||||||
|
"id": "scenario:test",
|
||||||
|
"authority": "netkingdom",
|
||||||
|
"initial_trust": ["bare_host_trust"],
|
||||||
|
"requires": {"capabilities": capabilities},
|
||||||
|
"providers": {},
|
||||||
|
"parameter_overrides": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def messages(issues):
|
||||||
|
return [item.message for item in issues if item.level == "ERROR"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_provider_emits_plan_only_handoff(tmp_path):
|
||||||
|
item = declaration(tmp_path, declaration_data())
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([item], scenario())
|
||||||
|
|
||||||
|
assert messages(issues) == []
|
||||||
|
assert plan["execution"] == {
|
||||||
|
"mode": "plan-only",
|
||||||
|
"permitted": False,
|
||||||
|
"reason": "Composition does not authorize or perform provider execution.",
|
||||||
|
}
|
||||||
|
assert plan["execution_steps"][0]["executor_owner"] == "owner"
|
||||||
|
assert plan["execution_steps"][0]["readiness_obligations"][0]["checks"]
|
||||||
|
assert plan["responsibility_map"][0]["declaration_id"] == "owner.c0"
|
||||||
|
assert plan["composition_digest"].startswith("sha256:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ambiguous_provider_requires_pin(tmp_path):
|
||||||
|
first = declaration(tmp_path, declaration_data("owner.c0-a"), "a.yaml")
|
||||||
|
second = declaration(tmp_path, declaration_data("owner.c0-b"), "b.yaml")
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([second, first], scenario())
|
||||||
|
|
||||||
|
assert plan is None
|
||||||
|
assert any("ambiguous providers" in message for message in messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_provider_pin_resolves_ambiguity(tmp_path):
|
||||||
|
first = declaration(tmp_path, declaration_data("owner.c0-a"), "a.yaml")
|
||||||
|
second = declaration(tmp_path, declaration_data("owner.c0-b"), "b.yaml")
|
||||||
|
request = scenario()
|
||||||
|
request["providers"] = {"c0.bootstrap-identity": "owner.c0-b"}
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([second, first], request)
|
||||||
|
|
||||||
|
assert messages(issues) == []
|
||||||
|
assert plan["capability_providers"] == {"c0.bootstrap-identity": "owner.c0-b"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_provider_pin_fails_closed(tmp_path):
|
||||||
|
item = declaration(tmp_path, declaration_data())
|
||||||
|
request = scenario()
|
||||||
|
request["providers"] = {"c0.bootstrap-identity": "owner.missing"}
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([item], request)
|
||||||
|
|
||||||
|
assert plan is None
|
||||||
|
assert any("does not provide" in message for message in messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_for_unselected_declaration_fails(tmp_path):
|
||||||
|
item = declaration(tmp_path, declaration_data())
|
||||||
|
request = scenario()
|
||||||
|
request["parameter_overrides"] = {"owner.other": {"target": "wrong"}}
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([item], request)
|
||||||
|
|
||||||
|
assert plan is None
|
||||||
|
assert "override targets an unselected declaration" in messages(issues)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tenant_cannot_override_platform_only_parameter(tmp_path):
|
||||||
|
item = declaration(tmp_path, declaration_data())
|
||||||
|
request = scenario()
|
||||||
|
request["authority"] = "tenant"
|
||||||
|
request["parameter_overrides"] = {"owner.c0": {"secure_mode": False}}
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([item], request)
|
||||||
|
|
||||||
|
assert plan is None
|
||||||
|
assert any("tenant authority cannot override" in message for message in messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_trust_dependencies_override_request_order(tmp_path):
|
||||||
|
c0 = declaration(tmp_path, declaration_data(), "c0.yaml")
|
||||||
|
c1_data = declaration_data(
|
||||||
|
"owner.c1",
|
||||||
|
"c1.lightweight-sso",
|
||||||
|
"C1",
|
||||||
|
requires=["bootstrap_identity_trust"],
|
||||||
|
satisfies=["runtime_identity_trust"],
|
||||||
|
)
|
||||||
|
c1 = declaration(tmp_path, c1_data, "c1.yaml")
|
||||||
|
request = scenario(["c1.lightweight-sso", "c0.bootstrap-identity"])
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([c1, c0], request)
|
||||||
|
|
||||||
|
assert messages(issues) == []
|
||||||
|
assert [step["declaration_id"] for step in plan["execution_steps"]] == [
|
||||||
|
"owner.c0",
|
||||||
|
"owner.c1",
|
||||||
|
]
|
||||||
|
assert "runtime_identity_trust" in plan["planned_final_trust"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_unresolved_trust_fails_closed(tmp_path):
|
||||||
|
item = declaration(tmp_path, declaration_data())
|
||||||
|
request = scenario()
|
||||||
|
request["initial_trust"] = []
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([item], request)
|
||||||
|
|
||||||
|
assert plan is None
|
||||||
|
assert any("unresolved trust states" in message for message in messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_declaration_ids_fail_closed(tmp_path):
|
||||||
|
data = declaration_data()
|
||||||
|
first = declaration(tmp_path, data, "a.yaml")
|
||||||
|
second = declaration(tmp_path, copy.deepcopy(data), "b.yaml")
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([first, second], scenario())
|
||||||
|
|
||||||
|
assert plan is None
|
||||||
|
assert any("duplicate declaration id" in message for message in messages(issues))
|
||||||
|
|
||||||
|
|
||||||
|
def test_checked_in_c0_scenario_composes():
|
||||||
|
repo_root = Path(__file__).resolve().parents[3]
|
||||||
|
declaration_path = repo_root / "capabilities/playbooks/net-kingdom.local-identity.yaml"
|
||||||
|
scenario_path = repo_root / "examples/security-scenarios/c0-local-identity.yaml"
|
||||||
|
item = composer.Declaration(
|
||||||
|
path=declaration_path,
|
||||||
|
data=composer.contract.load_yaml(declaration_path),
|
||||||
|
)
|
||||||
|
request = composer.contract.load_yaml(scenario_path)
|
||||||
|
|
||||||
|
issues, plan = composer.compose_scenario([item], request)
|
||||||
|
|
||||||
|
assert messages(issues) == []
|
||||||
|
assert plan["capability_providers"] == {
|
||||||
|
"c0.bootstrap-identity": "net-kingdom.local-identity"
|
||||||
|
}
|
||||||
|
assert plan["execution_steps"][0]["repo"] == "net-kingdom"
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
---
|
||||||
|
id: NK-WP-0030
|
||||||
|
type: workplan
|
||||||
|
title: "Implement deterministic security scenario composition"
|
||||||
|
domain: infotech
|
||||||
|
repo: net-kingdom
|
||||||
|
status: blocked
|
||||||
|
owner: codex
|
||||||
|
topic_slug: netkingdom
|
||||||
|
planning_priority: P1
|
||||||
|
created: "2026-08-23"
|
||||||
|
updated: "2026-08-23"
|
||||||
|
---
|
||||||
|
|
||||||
|
# NK-WP-0030 — Deterministic security scenario composition
|
||||||
|
|
||||||
|
Authorized by the operator as the first implementation increment following the
|
||||||
|
2026-08-23 scope-to-intent gap assessment. This work closes the locally owned
|
||||||
|
part of G1 without moving playbook execution into NetKingdom.
|
||||||
|
|
||||||
|
## Define the scenario composition contract
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0030-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Published proposed `security-scenario-composition_v0.1` canon and its scenario
|
||||||
|
schema. It requires exact provider pins under ambiguity, explicit initial trust,
|
||||||
|
fail-closed sequencing, owner attribution, readiness obligations, and an
|
||||||
|
unconditional non-execution boundary.
|
||||||
|
|
||||||
|
## Implement the canonical composer
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0030-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemented `tools/security-scenario-composer/`. It consumes Playbook Capability
|
||||||
|
Contract v0.1 declarations, rejects invalid or ambiguous inputs, validates
|
||||||
|
authority-bound parameter overrides, orders selected declarations by explicit
|
||||||
|
trust transitions, and emits a deterministic owner-routed JSON plan with a
|
||||||
|
content digest and `execution.permitted: false`. The older conformance-demo
|
||||||
|
selector now also refuses ambiguity and overrides targeting unselected
|
||||||
|
declarations.
|
||||||
|
|
||||||
|
## Publish a C0 reference composition
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0030-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Published `capabilities/playbooks/net-kingdom.local-identity.yaml` and
|
||||||
|
`examples/security-scenarios/c0-local-identity.yaml`. The non-secret reference
|
||||||
|
selects the exact local provider, starts from explicit `bare_host_trust`, and
|
||||||
|
plans the readiness obligations for `bootstrap_identity_trust` without
|
||||||
|
executing the CLI entry point.
|
||||||
|
|
||||||
|
## Obtain lightweight-provider declarations
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0030-T04
|
||||||
|
status: wait
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Ask the KeyCape/Railiance owners to publish conformant C1 and selected C2
|
||||||
|
declarations beside their execution entry points. Do not fabricate provider
|
||||||
|
authority in NetKingdom. The complete lightweight SSO scenario remains blocked
|
||||||
|
until those declarations exist and pass the canonical composer. Routed to
|
||||||
|
`key-cape` in State Hub message `64d247e4-9170-4d18-b7ed-d8b071282104`.
|
||||||
|
|
||||||
|
## Verify the fail-closed and reference paths
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0030-T05
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Added ten composer tests and five regression tests for the legacy declaration
|
||||||
|
validator/demo selector, including secret-reference syntax enforcement.
|
||||||
|
Exact selection, ambiguity refusal, bad pins, override boundaries, duplicate
|
||||||
|
ids, dependency ordering, unresolved trust, responsibility/readiness mapping,
|
||||||
|
and the checked-in C0 composition pass. The complete relevant repository suite
|
||||||
|
passes 70 tests; the isolated local-identity suite passes 142 tests. JSON/YAML
|
||||||
|
parsing, Python compilation, the reference CLI, and `git diff --check` also
|
||||||
|
pass.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue