feat(orchestration): compose security scenarios
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
This commit is contained in:
tegwick 2026-08-23 12:40:52 +02:00
parent ad46cc89fc
commit d96aab2321
20 changed files with 1464 additions and 30 deletions

View file

@ -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
@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
playbook-contract-test: ## Run Playbook Capability Contract fixture 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)
python3 -m pytest tools/security-bootstrap-console/tests

View file

@ -1,10 +1,19 @@
# 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
- [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

View file

@ -42,6 +42,8 @@ about the current implementation.
model.
- Playbook Capability Contract v0.1 for the boundary between NetKingdom
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,
validators, evidence rules, and publication stewardship. Zone semantics are
owned by `zone-engine`; NetKingdom owns their canon publication.
@ -54,6 +56,9 @@ about the current implementation.
development, test, and sandbox use.
- IAM Profile, playbook-capability, tenancy-posture, custody, evidence, and
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
bootstrap workflow, including refusal of unsafe live initialization.
- 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 |
| --- | --- | --- |
| 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 |
| 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 |
@ -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
explicit backlog: reef carrier/public-classification decisions in NK-WP-0027,
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
type: governance
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]
```
@ -184,5 +191,6 @@ keywords: [meta-orchestration, railiance, responsibility, capability, trust-stat
`docs/openbao-unseal-custody-models.md`, and
`tools/security-bootstrap-console/`
- 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/`

View file

@ -23,6 +23,7 @@
| 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-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-T02 | done | — | workplans/ADHOC-2026-07-02.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-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-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-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |

View 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"
]
}
}
}
}

View file

@ -6,7 +6,7 @@ domain: netkingdom
status: accepted
version: "0.1"
created: "2026-05-22"
updated: "2026-05-22"
updated: "2026-08-23"
scope: meta-orchestration
adr:
- 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,
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:
| Constraint | Applies to | Meaning |
@ -244,7 +250,8 @@ separate playbook runner execute safely.
## 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
id: scenario:s1-host-bootstrap-reference
@ -263,6 +270,13 @@ Allowed scenario authorities are `platform`, `netkingdom`, and `tenant`.
Tenant authority cannot override `platform_only`,
`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
A declaration conforms when it passes:

View 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.

View 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

View 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.

View 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

View file

@ -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
is complete; it is a statement about the executable queue in this repository on
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.

View file

@ -1,9 +1,8 @@
---
id: capability.security.iam-tooling-suite
name: NetKingdom Security/IAM Tooling Suite
summary: Dynamic, self-optimizing security platform for Kubernetes-deployed IT infrastructure; owns canonical
IAM/security standards and executable conformance tooling (IAM profile conformance, playbook capability
contract validation, security bootstrap console).
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture validation,
and deterministic plan-only security scenario composition.
owner: net-kingdom
status: draft
domain: infotech
@ -17,17 +16,17 @@ maturity:
current: D3
target: D5
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,
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.
and State Hub; canon/standards/ holds versioned IAM v0.3, playbook capability, scenario composition,
tenancy, zone, and boundary standards that provider repos implement against.
availability:
current: A2
target: A3
confidence: medium
rationale: '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).'
rationale: 'No top-level package manifest, but tools/ holds independently documented and runnable
conformance, posture, scenario-composition, and guarded bootstrap tools; local-identity is separately
packaged and tested.'
external_evidence:
completeness:
level: C1
@ -35,7 +34,7 @@ external_evidence:
basis: scope_vs_intent_and_consumer_expectations
satisfied_expectations:
- 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: []
out_of_scope_expectations: []
reliability:
@ -46,13 +45,16 @@ external_evidence:
- no top-level packaging; each tool under tools/ has its own runtime dependencies, no unified install
path yet
discovery:
intent: Own the canonical IAM/security standards for the Coulomb ecosystem and provide executable conformance
tooling so implementers (like key-cape) can verify against the standard rather than guessing.
intent: Own canonical NetKingdom security semantics and provide executable reference and conformance tooling
so provider implementations can integrate without inferred authority or hidden deployment coupling.
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
- playbook capability contract validator
- deterministic plan-only security scenario composer
- tenancy posture schema and validator
- security bootstrap console (local, non-secret-collecting)
- packaged local bootstrap identity CLI
excludes:
- concrete IAM implementations themselves (see key-cape for lightweight mode)
- live secret value handling (bootstrap console explicitly refuses live OpenBao initialization)
@ -65,7 +67,10 @@ availability:
current_artifacts:
- tools/iam-profile-conformance
- tools/playbook-capability-contract
- tools/security-scenario-composer
- tools/tenancy-posture
- tools/security-bootstrap-console
- local-identity
target_artifacts: []
consumption_modes:
- cli
@ -98,17 +103,28 @@ promotion_history: []
## 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
### 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
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

View file

@ -1,12 +1,11 @@
version: 1
updated: '2026-07-06'
updated: '2026-08-23'
domain: helix_forge
capabilities:
- id: capability.security.iam-tooling-suite
name: NetKingdom Security/IAM Tooling Suite
summary: Dynamic, self-optimizing security platform for Kubernetes-deployed IT infrastructure; owns
canonical IAM/security standards and executable conformance tooling (IAM profile conformance, playbook
capability contract validation, security bootstrap console).
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture
validation, and deterministic plan-only security scenario composition.
vector: D3 / A2 / C1 / R1
domain: infotech
status: draft

View file

@ -13,7 +13,7 @@ python3 tools/playbook-capability-contract/playbook_contract_validator.py \
../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
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
```
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:
```bash

View file

@ -60,6 +60,9 @@ TRUST_STATES = {
"tenant_onboarding_trust",
}
SCENARIO_AUTHORITIES = {"platform", "netkingdom", "tenant"}
SECRET_REFERENCE_PATTERN = re.compile(
r"(?:[a-z][a-z0-9+.-]*://\S+|(?:/|\./|\.\./)\S+)"
)
@dataclass
@ -127,6 +130,10 @@ def is_type(value: Any, declared_type: str) -> bool:
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]:
issues: list[Issue] = []
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":
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):
issues.append(issue("ERROR", f"{path}.default", f"default does not match type {declared_type!r}"))
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"}:
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):
issues.append(issue("ERROR", path, f"override for {name!r} does not match type {declared_type!r}"))
issues.extend(validate_constraints(value, param, path))
@ -433,10 +461,23 @@ def compose_scenario(declarations: list[Declaration], scenario: dict[str, Any])
selected: list[Declaration] = []
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:
issues.append(issue("ERROR", "scenario.requires.capabilities", f"no declaration provides {cap_id!r}"))
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])
# 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:
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 = {
"scenario": scenario.get("id", "scenario:unnamed"),
"authority": authority,

View file

@ -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))
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):
declaration = declaration_from(valid_declaration(), tmp_path)
scenario = {
@ -196,3 +271,36 @@ def test_required_parameter_without_override_fails(tmp_path):
issues, _ = validator.compose_scenario([declaration], scenario)
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))

View 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
```

View 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())

View file

@ -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"

View file

@ -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.