feat(posture): add deterministic feedback proposals
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 13:16:34 +02:00
parent dc8da422f8
commit cfc9e7d0cb
19 changed files with 1428 additions and 16 deletions

View file

@ -199,6 +199,16 @@ security-scenario-compose-c0: ## Compose the plan-only C0 local-identity referen
--scenario examples/security-scenarios/c0-local-identity.yaml \
capabilities/playbooks/net-kingdom.local-identity.yaml
posture-feedback-test: ## Run deterministic posture-feedback tests
python3 -m pytest tools/posture-feedback/tests tools/tenancy-posture/test_validate.py
posture-feedback-reference: ## Emit the proposal-only reference report at its fixed example time
python3 tools/posture-feedback/posture_feedback.py \
--as-of 2026-08-23T12:00:00Z \
--horizon-days 14 \
--fail-on none \
examples/posture-feedback/expired-e2.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
@ -358,7 +368,8 @@ security-bootstrap-ui: security-bootstrap-metadata-init ## Serve local custody a
creds-status creds-rotate \
creds-agent-init creds-agent-status creds-emergency-reprint \
openbao-init-unseal openbao-init-unseal-dry-run \
iam-profile-conformance-test playbook-contract-test \
iam-profile-conformance-test playbook-contract-test posture-feedback-test \
posture-feedback-reference \
security-bootstrap-console-test security-bootstrap-scripts-syntax \
security-bootstrap-console security-bootstrap-king-kit \
security-bootstrap-validate-kit security-bootstrap-validate-t02 \

View file

@ -14,6 +14,8 @@ The dynamic, self-optimizing security platform is the long-term direction in
- [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
- [Posture feedback](canon/standards/posture-feedback_v0.1.md) — deterministic,
proposal-only posture and evidence remediation findings
## Security Infrastructure Documents

View file

@ -44,6 +44,8 @@ about the current implementation.
selection/parameterization and Railiance execution.
- Security Scenario Composition v0.1 for deterministic provider selection,
trust ordering, responsibility mapping, and non-executing owner handoff.
- Posture Feedback v0.1 for deterministic, proposal-only routing of posture
review, evidence-freshness, implementation, and declared-gap findings.
- 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,11 +56,13 @@ about the current implementation.
- `local-identity/`: minimal file-backed OIDC identity for bootstrap,
development, test, and sandbox use.
- IAM Profile, playbook-capability, tenancy-posture, custody, evidence, and
bootstrap-policy validators.
- IAM Profile, playbook-capability, tenancy-posture, posture-feedback, 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/posture-feedback/`: deterministic, time-explicit remediation proposals
with authoritative owner resolution and no external or policy writes.
- `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,
@ -110,14 +114,16 @@ repositories while NetKingdom retains the contracts and reference evidence.
| C3 — Runtime secrets | OpenBao custody, bootstrap, policy, delivery, and recovery contracts | Integrated with an externally deployed runtime; production evidence remains gated |
| C4 — Fine-grained authorization | flex-auth caller identity and boundary integration | Partially delivered; full estate/PDP readiness is not established here |
| C5 — Enterprise federation | Keycloak/SAML/enterprise-IdP design | Backlog; not a current provided runtime capability |
| C6 — Self-optimizing security | Declarations, validators, evidence freshness, workplans, and drift surfacing | Early governance mechanisms only; no autonomous closed loop |
| C6 — Self-optimizing security | Declarations, validators, evidence freshness, and deterministic owner-routed remediation proposals | First proposal-only feedback loop delivered; no autonomous policy mutation or closed loop |
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. 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.
declarations before the lightweight SSO scenario can compose. NK-WP-0031 has
delivered the local proposal-only feedback evaluator and waits for authoritative
freshness adoption by `audit-core`.
---
@ -126,8 +132,8 @@ declarations before the lightweight SSO scenario can compose.
- Defining or reviewing identity, tenancy, workload-zone, credential, and
security-composition canon.
- Bootstrapping identity and trust before the normal platform is available.
- Validating an IAM issuer, posture declaration, or Railiance capability
declaration against NetKingdom contracts.
- Validating an IAM issuer, posture declaration, posture feedback report, or
Railiance capability declaration against NetKingdom contracts.
- Integrating KeyCape, flex-auth, OpenBao, user-engine, tenant-engine, or a
Railiance package across an explicit security boundary.
- Deciding which repository owns a security semantic, runtime, deployment, or
@ -151,15 +157,15 @@ declarations before the lightweight SSO scenario can compose.
```capability
type: governance
title: NetKingdom security canon
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.
description: Provider-neutral IAM v0.3, user/tenant boundaries, credential, playbook and scenario composition, tenancy-posture, posture-feedback, and workload-zone standards with explicit ownership and conformance rules.
keywords: [iam, oidc, tenancy, workload-identity, security-zones, credentials, canon]
```
```capability
type: validation
title: Security contract conformance
description: Executable validation for IAM Profile issuers, playbook capability declarations, tenancy posture, bootstrap custody, and non-secret evidence records.
keywords: [validation, conformance, iam, posture, evidence, playbooks]
description: Executable validation for IAM Profile issuers, playbook capability declarations, tenancy posture, deterministic proposal-only feedback, bootstrap custody, and non-secret evidence records.
keywords: [validation, conformance, iam, posture, feedback, evidence, playbooks]
```
```capability
@ -192,5 +198,5 @@ keywords: [meta-orchestration, railiance, responsibility, capability, trust-stat
`tools/security-bootstrap-console/`
- Executable surfaces: `local-identity/`, `tools/iam-profile-conformance/`,
`tools/playbook-capability-contract/`, `tools/security-scenario-composer/`,
and `tools/tenancy-posture/`
`tools/tenancy-posture/`, and `tools/posture-feedback/`
- Work state: `.custodian-brief.md` and `workplans/`

View file

@ -0,0 +1,145 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://netkingdom.local/schemas/posture-feedback-report_v0.1.schema.json",
"title": "NetKingdom Posture Feedback Report v0.1",
"type": "object",
"additionalProperties": false,
"required": [
"apiVersion",
"kind",
"ok",
"as_of",
"horizon_days",
"fail_on",
"automation",
"summary",
"validation_errors",
"findings",
"report_digest"
],
"properties": {
"apiVersion": {
"const": "netkingdom.io/posture-feedback/v0.1"
},
"kind": {
"const": "PostureFeedbackReport"
},
"ok": {
"type": "boolean"
},
"as_of": {
"type": "string",
"format": "date-time"
},
"horizon_days": {
"type": "integer",
"minimum": 0
},
"fail_on": {
"enum": ["none", "low", "medium", "high"]
},
"automation": {
"type": "object",
"additionalProperties": false,
"required": [
"mode",
"external_write_permitted",
"policy_mutation_permitted",
"declaration_mutation_permitted"
],
"properties": {
"mode": {"const": "proposal-only"},
"external_write_permitted": {"const": false},
"policy_mutation_permitted": {"const": false},
"declaration_mutation_permitted": {"const": false}
}
},
"summary": {
"type": "object",
"additionalProperties": false,
"required": ["total", "high", "medium", "low", "unknown_owner"],
"properties": {
"total": {"type": "integer", "minimum": 0},
"high": {"type": "integer", "minimum": 0},
"medium": {"type": "integer", "minimum": 0},
"low": {"type": "integer", "minimum": 0},
"unknown_owner": {"type": "integer", "minimum": 0}
}
},
"validation_errors": {
"type": "array",
"items": {"type": "string", "minLength": 1}
},
"findings": {
"type": "array",
"items": {
"$ref": "#/$defs/finding"
}
},
"report_digest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
}
},
"$defs": {
"finding": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"class",
"severity",
"source",
"service",
"control",
"owner",
"evidence_state",
"reason",
"recommended_action"
],
"properties": {
"id": {
"type": "string",
"pattern": "^NKFB-[0-9a-f]{16}$"
},
"class": {
"enum": [
"posture-review-overdue",
"posture-review-due-soon",
"zone-review-overdue",
"zone-review-due-soon",
"evidence-freshness-unknown",
"evidence-expired",
"evidence-due-soon",
"implemented-not-evidenced",
"declared-gap"
]
},
"severity": {
"enum": ["low", "medium", "high"]
},
"source": {"type": "string", "minLength": 1},
"service": {"type": "string", "minLength": 1},
"control": {"type": "string", "minLength": 1},
"owner": {"type": "string", "minLength": 1},
"evidence_state": {
"enum": [
"not-applicable",
"due-soon",
"overdue",
"unknown",
"expired",
"implemented",
"gap"
]
},
"due": {
"type": "string",
"minLength": 1
},
"reason": {"type": "string", "minLength": 1},
"recommended_action": {"type": "string", "minLength": 1}
}
}
}
}

View file

@ -21,6 +21,9 @@
"type": "string",
"minLength": 1
},
"responsible_repo": {
"$ref": "#/$defs/repoName"
},
"workload_identity": {
"$ref": "#/$defs/workloadIdentity"
},
@ -36,6 +39,9 @@
"evidence": {
"$ref": "#/$defs/evidence"
},
"evidence_freshness": {
"$ref": "#/$defs/evidenceFreshness"
},
"notes": {
"$ref": "#/$defs/stringList"
},
@ -100,6 +106,16 @@
"required": [
"evidence"
]
},
{
"required": [
"responsible_repo"
]
},
{
"required": [
"evidence_freshness"
]
}
]
}
@ -601,6 +617,76 @@
]
}
},
"evidenceFreshnessEntry": {
"type": "object",
"required": [
"kind",
"observed_at",
"responsible_repo",
"scope",
"remediation"
],
"properties": {
"kind": {
"enum": [
"mechanical",
"adversarial"
]
},
"observed_at": {
"type": "string",
"format": "date-time"
},
"valid_until": {
"type": "string",
"format": "date-time"
},
"responsible_repo": {
"$ref": "#/$defs/repoName"
},
"scope": {
"type": "string",
"minLength": 1
},
"remediation": {
"type": "string",
"minLength": 1
},
"source_revision": {
"type": "string",
"minLength": 1
}
},
"allOf": [
{
"if": {
"properties": {
"kind": {
"const": "adversarial"
}
},
"required": [
"kind"
]
},
"then": {
"required": [
"valid_until"
]
}
}
],
"additionalProperties": false
},
"evidenceFreshness": {
"type": "object",
"patternProperties": {
"^[IAEPRV][0-4]$": {
"$ref": "#/$defs/evidenceFreshnessEntry"
}
},
"additionalProperties": false
},
"service": {
"type": "object",
"required": [
@ -616,6 +702,9 @@
"type": "string",
"minLength": 1
},
"responsible_repo": {
"$ref": "#/$defs/repoName"
},
"workload_identity": {
"$ref": "#/$defs/workloadIdentity"
},
@ -628,6 +717,9 @@
"evidence": {
"$ref": "#/$defs/evidence"
},
"evidence_freshness": {
"$ref": "#/$defs/evidenceFreshness"
},
"notes": {
"$ref": "#/$defs/stringList"
},

View file

@ -0,0 +1,109 @@
---
id: netkingdom-posture-feedback-v0.1
type: standard
title: "NetKingdom Posture Feedback 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: self-optimizing-security
schema:
- canon/schemas/posture-feedback-report_v0.1.schema.json
validator:
- tools/posture-feedback/posture_feedback.py
related:
- canon/standards/tenancy-posture_v0.1.md
- canon/standards/security-zones_v0.1.md
---
# NetKingdom Posture Feedback v0.1
## 1. Purpose
This contract is the first bounded C6 feedback mechanism. It turns explicit
posture review dates, evidence freshness, implemented-but-unevidenced controls,
and declared gaps into deterministic remediation **proposals**.
It does not modify a posture level, policy, declaration, workplan, State Hub,
or runtime. Human or separately governed automation decides whether a proposal
becomes work.
## 2. Deterministic time
Evaluation requires an explicit RFC 3339 `as_of` timestamp. Wall-clock time is
never read implicitly. A date-only `review_due` remains current through that
calendar date in UTC and becomes overdue on the following UTC date.
A non-negative horizon in days identifies items due soon. Changing the horizon
changes the report digest and is therefore visible.
## 3. Owner resolution
The evaluator routes only from authoritative declaration fields:
- posture review, gaps, and implemented controls: `responsible_repo`;
- evidence replacement: `evidence_freshness.<level>.responsible_repo`;
- security-zone review: `zones.responsible_party`.
If the relevant field is absent, owner resolution is `unknown`. The evaluator
must not infer ownership from the service name, repository path, Git remote,
previous work, or another policy subject.
## 4. Finding classes
| Finding | Trigger | Severity |
| --- | --- | --- |
| `posture-review-overdue` | `as_of` is after `tenancy.review_due` | high |
| `posture-review-due-soon` | review is within the horizon | medium |
| `zone-review-overdue` | `as_of` is after `zones.review_due` | high |
| `zone-review-due-soon` | zone review is within the horizon | medium |
| `evidence-freshness-unknown` | a current adversarial level has no complete freshness entry | high |
| `evidence-expired` | `as_of` is after `valid_until` | high |
| `evidence-due-soon` | evidence expires within the horizon | medium |
| `implemented-not-evidenced` | an `implemented` level is above `current` | medium |
| `declared-gap` | a non-empty `tenancy.gap` entry exists | low |
The review horizon does not generate a due-soon finding for an item already
overdue or expired. Exact equality with a timestamp is still valid; expiry is
strictly `as_of > valid_until`.
Current adversarial levels are `E2`, `R4`, and `V2``V4`. This vocabulary comes
from Tenancy Posture §13. Mechanical evidence is evaluated for expiry only when
its declaration explicitly supplies `valid_until`.
## 5. Proposal and safety boundary
Every finding receives a stable id derived from its source declaration,
service, finding class, control, and due value. It contains the authoritative
owner or `unknown`, current evidence state, reason, and recommended action.
For declarations under the workspace containing this repository, the source is
normalized to `<repo>/<path>` so absolute checkout locations do not perturb the
identity. This source normalization identifies an input only; it is never an
ownership inference.
Every report declares:
```yaml
automation:
mode: proposal-only
external_write_permitted: false
policy_mutation_permitted: false
declaration_mutation_permitted: false
```
Expired or unknown evidence does not silently inherit freshness and does not
silently downgrade a level. It makes the uncertainty visible for governed
review. Consumers that use the report as an admission gate may fail closed on
high findings, but that is a separate owner decision.
## 6. Exit behavior
The CLI emits a report conforming to
`posture-feedback-report_v0.1.schema.json`. `--fail-on high` exits non-zero when
at least one high-severity finding exists; `medium` includes medium and high;
`low` includes every finding; `none` reports without a finding-based failure.
Invalid declarations always exit non-zero.

View file

@ -8,7 +8,7 @@ version: "0.1"
created: "2026-08-17"
updated: "2026-08-23"
scope: multi-tenancy-security-framework
revision: "draft-13"
revision: "draft-14"
owner: net-kingdom
last_reviewed: "2026-08-23"
review_interval: 6m
@ -87,6 +87,11 @@ and the tenant-engine boundary contract, not in the work-factory canon.
required by §13.2. The claim remains explicitly bounded and freshness-dated:
the attempted cross-tenant attacks did not work; this is not a universal
isolation proof.
- **draft-14** makes evidence freshness and remediation ownership declarable.
Adversarial evidence may now carry its observation and expiry timestamps,
bounded scope, responsible repository, and replacement action. A separate
proposal-only evaluator treats absent authoritative owner or freshness as
`unknown`; it does not infer either or mutate the declared posture.
**Reviewed by all six. The score:** six repos found three live defects in their
own code by reading the ladders — `tenant-engine`'s unfiltered
@ -826,6 +831,11 @@ declaration but do not replace it. The schema carries `current`, `implemented`,
`target`, `reviewed`, `review_due`, `gap`, `placement_exceptions`,
`service_class` (§8.3), per-path detail (§5.2), and provider reachability
(§5.5), plus the workload identity prerequisite for zone membership (§5.6.2).
It also permits `responsible_repo` for authoritative posture routing and
`evidence_freshness` for machine-readable evidence observation, expiry, scope,
owner, and remediation metadata. Their absence remains valid declaration
syntax; feedback resolution must report `unknown`, never infer them from a
directory, service name, or previous owner.
From the `net-kingdom` repo, owners validate one or more declarations
with `uv run tools/tenancy-posture/validate.py <path>...`; the validator applies
the JSON Schema and the evidence, date, implemented/current and provider-range
@ -1129,6 +1139,22 @@ rather than a green build. Cross-tenant findings are the category external
testing practice identifies as needing human review. **A passing CI run is not
E2 evidence.**
**Decision 13.5 — freshness and ownership are explicit inputs.** A declaration
may attach `evidence_freshness.<level>` to an evidence key. An adversarial entry
requires `observed_at`, `valid_until`, `responsible_repo`, `scope`, and
`remediation`; a mechanical entry may omit `valid_until` when the artifact is
continuously re-established by the referenced revision or CI control. The
timestamps use RFC 3339 and the responsible repository is the authority for
replacement evidence.
The feedback evaluator does not parse prose for dates, infer ownership from a
file path, or silently extend a validity window. A current adversarial claim
without freshness metadata resolves to **freshness `unknown`**. An expired
artifact resolves to **freshness `expired`**. Neither automatically rewrites the
declared level: the evaluator emits a deterministic owner-routed remediation
proposal so review remains observable and controlled. The proposal contract is
`posture-feedback_v0.1`; it performs no State Hub write or policy mutation.
| Level | Evidence | Kind |
|---|---|---|
| **I2** | Identifiers validated against the vocabulary; rejection test for a malformed id; binding shown to come from a verified token | Mechanical |

View file

@ -0,0 +1,16 @@
# Posture Feedback Example
`expired-e2.yaml` is deliberately valid as a posture declaration while carrying
expired E2 evidence at the example evaluation time. It also has posture and
zone reviews due within 14 days, an E3 control awaiting evidence, and one
declared retention gap.
The evaluator reports these as proposals and performs no mutation:
```bash
uv run tools/posture-feedback/posture_feedback.py \
--as-of 2026-08-23T12:00:00Z \
--horizon-days 14 \
--fail-on none \
examples/posture-feedback/expired-e2.yaml
```

View file

@ -0,0 +1,60 @@
schema_version: "0.1"
framework: netkingdom-tenancy-posture
service: example-security-api
role: tenant-api
responsible_repo: net-kingdom
workload_identity:
name: example-security-api
kind: application
responsible_repo: net-kingdom
identity_bindings:
- scheme: iam-profile
authority: key-cape
subject: example-security-api
principal_type: service
environment: reference
tenancy:
current: {I: 1, A: 2, E: 2, P: 0, R: 1, V: 0}
implemented: {E: 3}
target: {I: 2, A: 3, E: 3, P: 1, R: 2, V: 1}
reviewed: "2026-08-01"
review_due: "2026-08-31"
service_class: interactive
reason:
I: Request identity is not verified in this reference fixture.
P: Reference fixture has no provisioned datastore.
R: Platform default retention applies.
V: No availability exercise exists.
gap:
R: Publish and evidence an explicit erasure horizon.
zones:
standard: security-zones_v0.1
membership: z1-operational
responsible_party: team:platform-security
justification: Reference workload with bounded internal data.
context:
maturity: M1
criticality: medium
data_classification: internal
evidence:
- ref: docs/evidence/reference-zone.md
supports: [M1, reference]
reviewed: "2026-08-01"
review_due: "2026-08-25"
evidence:
A2: docs/evidence/reference-authorization.md
E2: docs/evidence/reference-adversarial-e2.md
evidence_freshness:
E2:
kind: adversarial
observed_at: "2026-08-20T12:00:00Z"
valid_until: "2026-08-22T12:00:00Z"
responsible_repo: net-kingdom
scope: Two bounded cross-tenant read probes against the reference fixture.
remediation: Repeat the bounded E2 probes and replace the evidence metadata.
source_revision: reference@0000000

View file

@ -0,0 +1,47 @@
# Posture feedback estate baseline
Date: 2026-08-23
Workplan: NK-WP-0031
Mode: read-only, proposal-only
## Evaluation
The proposed Posture Feedback v0.1 evaluator was run with:
- `as_of`: `2026-08-23T11:05:14Z`
- `horizon_days`: `30`
- `fail_on`: `none`
- inputs: the tenancy declarations in `adaptive-pricing`, `audit-core`,
`flex-auth`, `ops-warden`, `railiance-platform`, `rapp-postgres`, and
`tenant-engine`
The report digest was
`sha256:66174f08cf5feb1e5618477a374975100a4712bcaa354fcb1b1702103482b6c1`.
It contained 34 proposals: one high, six medium, and 27 low. All 34 owners were
`unknown`, which is the required result because these declarations do not yet
carry the new authoritative `responsible_repo` fields.
## High finding
`NKFB-c08b78e82248bf27` reports `audit-core` E2 evidence freshness as
`unknown`. The declaration describes the Whitehat run and its 24-hour validity
in prose but does not yet supply the machine-readable `evidence_freshness.E2`
authority. NetKingdom did not infer timestamps, ownership, scope, or a
replacement action from that prose.
Adoption was routed to `audit-core` in State Hub message
`874e5fa5-e05c-4b1a-a915-26c4dba07b87`.
## Declaration validation errors
The `railiance-platform` declaration did not enter evaluation because its
`apps-pg` service declares current R2 and V1 without the evidence entries
required for current non-zero levels. Remediation was routed to
`railiance-platform` in State Hub message
`d65f8383-bdab-488e-a36f-f8cabd8a5f65`.
## Safety result
The evaluator changed no declaration, policy, workplan, State Hub record, or
runtime. The two State Hub messages above were explicit operator-process
handoffs after inspection of the report; they were not emitted by the tool.

View file

@ -145,3 +145,13 @@ 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.
NK-WP-0031 subsequently implemented the first bounded G2 increment: proposed
Posture Feedback v0.1 canon and a deterministic evaluator that turns explicit
review dates, evidence freshness, implemented-but-unevidenced controls, and
declared gaps into owner-routed remediation proposals. Time is an explicit
input; missing ownership or freshness resolves to `unknown`; and the report
forbids external, declaration, and policy writes. G2 remains open because the
tool does not create work, change policy, close remediation, or consume runtime
telemetry autonomously. Those powers require separate authority, rollback, and
review decisions.

View file

@ -2,7 +2,7 @@
id: capability.security.iam-tooling-suite
name: NetKingdom Security/IAM Tooling Suite
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture validation,
and deterministic plan-only security scenario composition.
deterministic plan-only security scenario composition, and proposal-only posture feedback.
owner: net-kingdom
status: draft
domain: infotech
@ -25,7 +25,7 @@ maturity:
target: A3
confidence: medium
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
conformance, posture, posture-feedback, scenario-composition, and guarded bootstrap tools; local-identity is separately
packaged and tested.'
external_evidence:
completeness:
@ -53,6 +53,7 @@ discovery:
- playbook capability contract validator
- deterministic plan-only security scenario composer
- tenancy posture schema and validator
- deterministic proposal-only posture feedback evaluator
- security bootstrap console (local, non-secret-collecting)
- packaged local bootstrap identity CLI
excludes:
@ -69,6 +70,7 @@ availability:
- tools/playbook-capability-contract
- tools/security-scenario-composer
- tools/tenancy-posture
- tools/posture-feedback
- tools/security-bootstrap-console
- local-identity
target_artifacts: []
@ -105,7 +107,7 @@ promotion_history: []
`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
proposal-only posture feedback, 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.

View file

@ -5,7 +5,7 @@ capabilities:
- id: capability.security.iam-tooling-suite
name: NetKingdom Security/IAM Tooling Suite
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture
validation, and deterministic plan-only security scenario composition.
validation, deterministic plan-only security scenario composition, and proposal-only posture feedback.
vector: D3 / A2 / C1 / R1
domain: infotech
status: draft

View file

@ -0,0 +1,22 @@
# Posture Feedback Evaluator
This tool implements the proposal-only feedback boundary in
`canon/standards/posture-feedback_v0.1.md`. It reads tenancy declarations and
emits deterministic JSON. It never writes State Hub, edits a declaration,
changes policy, or executes remediation.
Evaluate the checked-in reference at an explicit time:
```bash
uv run tools/posture-feedback/posture_feedback.py \
--as-of 2026-08-23T12:00:00Z \
--horizon-days 14 \
--fail-on none \
examples/posture-feedback/expired-e2.yaml
```
Run tests:
```bash
uv run pytest tools/posture-feedback/tests
```

View file

@ -0,0 +1,440 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["jsonschema>=4.23,<5", "PyYAML>=6,<7"]
# ///
"""Emit deterministic, proposal-only feedback for tenancy posture declarations."""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import importlib.util
import json
import pathlib
import sys
from collections import Counter
from typing import Any
import jsonschema
import yaml
ROOT = pathlib.Path(__file__).resolve().parents[2]
TENANCY_SCHEMA = ROOT / "canon/schemas/tenancy-posture_v0.1.schema.json"
REPORT_SCHEMA = ROOT / "canon/schemas/posture-feedback-report_v0.1.schema.json"
TENANCY_VALIDATOR_PATH = ROOT / "tools/tenancy-posture/validate.py"
API_VERSION = "netkingdom.io/posture-feedback/v0.1"
KIND = "PostureFeedbackReport"
ADVERSARIAL_LEVELS = {"E2", "R4", "V2", "V3", "V4"}
SEVERITY_RANK = {"low": 1, "medium": 2, "high": 3}
SPEC = importlib.util.spec_from_file_location(
"tenancy_posture_validator_for_feedback", TENANCY_VALIDATOR_PATH
)
assert SPEC and SPEC.loader
TENANCY_VALIDATOR = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(TENANCY_VALIDATOR)
def parse_timestamp(value: str) -> dt.datetime:
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ValueError("timestamp must include an explicit UTC offset")
return parsed.astimezone(dt.timezone.utc)
def format_timestamp(value: dt.datetime) -> str:
return value.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
def load_document(path: pathlib.Path) -> dict[str, Any]:
document = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(document, dict):
raise ValueError("document must be a YAML object")
return document
def service_entries(document: dict[str, Any]) -> list[dict[str, Any]]:
if "services" in document:
return document["services"]
return [document]
def source_identifier(path: pathlib.Path) -> str:
"""Return a portable declaration id without treating the path as ownership."""
resolved = path.resolve()
try:
return resolved.relative_to(ROOT.parent).as_posix()
except ValueError:
return resolved.as_posix()
def finding_id(
source: str,
service: str,
finding_class: str,
control: str,
due: str | None,
) -> str:
identity = "\x1f".join((source, service, finding_class, control, due or ""))
digest = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:16]
return f"NKFB-{digest}"
def finding(
*,
source: str,
service: str,
finding_class: str,
severity: str,
control: str,
owner: str,
evidence_state: str,
reason: str,
recommended_action: str,
due: str | None = None,
) -> dict[str, Any]:
result = {
"id": finding_id(source, service, finding_class, control, due),
"class": finding_class,
"severity": severity,
"source": source,
"service": service,
"control": control,
"owner": owner,
"evidence_state": evidence_state,
"reason": reason,
"recommended_action": recommended_action,
}
if due is not None:
result["due"] = due
return result
def review_finding(
*,
source: str,
service: str,
owner: str,
control: str,
due: dt.date,
as_of: dt.datetime,
horizon_days: int,
prefix: str,
) -> dict[str, Any] | None:
due_text = due.isoformat()
if as_of.date() > due:
return finding(
source=source,
service=service,
finding_class=f"{prefix}-review-overdue",
severity="high",
control=control,
owner=owner,
evidence_state="overdue",
due=due_text,
reason=f"{control} review was due on {due_text} and is overdue at {format_timestamp(as_of)}.",
recommended_action=f"Review {control}, update its declaration and evidence, and retain the review record.",
)
days = (due - as_of.date()).days
if days <= horizon_days:
return finding(
source=source,
service=service,
finding_class=f"{prefix}-review-due-soon",
severity="medium",
control=control,
owner=owner,
evidence_state="due-soon",
due=due_text,
reason=f"{control} review is due in {days} day(s), within the {horizon_days}-day horizon.",
recommended_action=f"Schedule and evidence the {control} review before {due_text}.",
)
return None
def current_evidence_keys(entry: dict[str, Any]) -> set[str]:
current = entry["tenancy"]["current"]
return {
f"{axis}{level}"
for axis, level in current.items()
if isinstance(level, int)
}
def evidence_findings(
*,
source: str,
entry: dict[str, Any],
as_of: dt.datetime,
horizon_days: int,
) -> list[dict[str, Any]]:
service = entry["service"]
freshness = entry.get("evidence_freshness", {})
current_keys = current_evidence_keys(entry)
results: list[dict[str, Any]] = []
for key in sorted(ADVERSARIAL_LEVELS & current_keys):
if key not in freshness:
results.append(
finding(
source=source,
service=service,
finding_class="evidence-freshness-unknown",
severity="high",
control=key,
owner="unknown",
evidence_state="unknown",
reason=f"Current adversarial claim {key} has no authoritative evidence_freshness entry.",
recommended_action=(
f"Declare the {key} observation, expiry, bounded scope, responsible repository, "
"and replacement action; do not infer freshness from prose."
),
)
)
for key in sorted(current_keys & set(freshness)):
metadata = freshness[key]
valid_until_raw = metadata.get("valid_until")
if not valid_until_raw:
continue
valid_until = parse_timestamp(valid_until_raw)
owner = metadata["responsible_repo"]
if as_of > valid_until:
results.append(
finding(
source=source,
service=service,
finding_class="evidence-expired",
severity="high",
control=key,
owner=owner,
evidence_state="expired",
due=format_timestamp(valid_until),
reason=f"Evidence for {key} expired before {format_timestamp(as_of)}.",
recommended_action=metadata["remediation"],
)
)
continue
remaining = valid_until - as_of
if remaining <= dt.timedelta(days=horizon_days):
results.append(
finding(
source=source,
service=service,
finding_class="evidence-due-soon",
severity="medium",
control=key,
owner=owner,
evidence_state="due-soon",
due=format_timestamp(valid_until),
reason=(
f"Evidence for {key} expires within the {horizon_days}-day horizon "
f"at {format_timestamp(valid_until)}."
),
recommended_action=metadata["remediation"],
)
)
return results
def evaluate_entry(
source: str,
entry: dict[str, Any],
as_of: dt.datetime,
horizon_days: int,
) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
service = entry["service"]
posture = entry["tenancy"]
posture_owner = entry.get("responsible_repo", "unknown")
posture_review = review_finding(
source=source,
service=service,
owner=posture_owner,
control="tenancy-posture",
due=dt.date.fromisoformat(posture["review_due"]),
as_of=as_of,
horizon_days=horizon_days,
prefix="posture",
)
if posture_review:
results.append(posture_review)
zones = entry.get("zones")
if zones:
zone_review = review_finding(
source=source,
service=service,
owner=zones["responsible_party"],
control="security-zones",
due=dt.date.fromisoformat(zones["review_due"]),
as_of=as_of,
horizon_days=horizon_days,
prefix="zone",
)
if zone_review:
results.append(zone_review)
results.extend(
evidence_findings(
source=source,
entry=entry,
as_of=as_of,
horizon_days=horizon_days,
)
)
current = posture["current"]
for axis, level in sorted(posture.get("implemented", {}).items()):
current_level = current[axis]
results.append(
finding(
source=source,
service=service,
finding_class="implemented-not-evidenced",
severity="medium",
control=f"{axis}{level}",
owner=posture_owner,
evidence_state="implemented",
reason=f"{axis}{level} is implemented while the current evidenced level is {axis}{current_level}.",
recommended_action=f"Produce the canon-required evidence for {axis}{level} or remove the implemented claim.",
)
)
for axis, description in sorted(posture.get("gap", {}).items()):
results.append(
finding(
source=source,
service=service,
finding_class="declared-gap",
severity="low",
control=axis,
owner=posture_owner,
evidence_state="gap",
reason=description,
recommended_action=f"Review the declared {axis}-axis gap and retain, schedule, or resolve it explicitly.",
)
)
return results
def report_digest(report_without_digest: dict[str, Any]) -> str:
encoded = json.dumps(
report_without_digest,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
def build_report(
paths: list[pathlib.Path],
*,
as_of: dt.datetime,
horizon_days: int,
fail_on: str,
) -> dict[str, Any]:
tenancy_schema = json.loads(TENANCY_SCHEMA.read_text(encoding="utf-8"))
validation_errors: list[str] = []
findings: list[dict[str, Any]] = []
for path in paths:
source = source_identifier(path)
try:
document = load_document(path)
except Exception as exc:
validation_errors.append(f"{path}: {exc}")
continue
errors = TENANCY_VALIDATOR.validate(path, tenancy_schema)
if errors:
validation_errors.extend(errors)
continue
for entry in service_entries(document):
findings.extend(evaluate_entry(source, entry, as_of, horizon_days))
findings.sort(
key=lambda item: (
item["source"],
item["service"],
item["class"],
item["control"],
item.get("due", ""),
)
)
counts = Counter(item["severity"] for item in findings)
threshold_failed = False
if fail_on != "none":
threshold = SEVERITY_RANK[fail_on]
threshold_failed = any(
SEVERITY_RANK[item["severity"]] >= threshold for item in findings
)
report: dict[str, Any] = {
"apiVersion": API_VERSION,
"kind": KIND,
"ok": not validation_errors and not threshold_failed,
"as_of": format_timestamp(as_of),
"horizon_days": horizon_days,
"fail_on": fail_on,
"automation": {
"mode": "proposal-only",
"external_write_permitted": False,
"policy_mutation_permitted": False,
"declaration_mutation_permitted": False,
},
"summary": {
"total": len(findings),
"high": counts["high"],
"medium": counts["medium"],
"low": counts["low"],
"unknown_owner": sum(item["owner"] == "unknown" for item in findings),
},
"validation_errors": sorted(validation_errors),
"findings": findings,
}
report["report_digest"] = report_digest(report)
report_schema = json.loads(REPORT_SCHEMA.read_text(encoding="utf-8"))
jsonschema.Draft202012Validator(
report_schema,
format_checker=jsonschema.FormatChecker(),
).validate(report)
return report
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Emit deterministic, proposal-only NetKingdom posture feedback."
)
parser.add_argument("declarations", nargs="+", type=pathlib.Path)
parser.add_argument("--as-of", required=True, help="RFC 3339 timestamp with explicit offset")
parser.add_argument("--horizon-days", type=int, default=30)
parser.add_argument("--fail-on", choices=("none", "low", "medium", "high"), default="high")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.horizon_days < 0:
raise SystemExit("--horizon-days must be non-negative")
try:
as_of = parse_timestamp(args.as_of)
except ValueError as exc:
raise SystemExit(f"invalid --as-of: {exc}") from exc
report = build_report(
args.declarations,
as_of=as_of,
horizon_days=args.horizon_days,
fail_on=args.fail_on,
)
print(json.dumps(report, indent=2, sort_keys=True))
if report["validation_errors"]:
return 2
return 0 if report["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,261 @@
from __future__ import annotations
import copy
import importlib.util
import json
import pathlib
import sys
import yaml
TOOL_PATH = pathlib.Path(__file__).resolve().parents[1] / "posture_feedback.py"
SPEC = importlib.util.spec_from_file_location("posture_feedback", TOOL_PATH)
feedback = importlib.util.module_from_spec(SPEC)
assert SPEC and SPEC.loader
sys.modules[SPEC.name] = feedback
SPEC.loader.exec_module(feedback)
def declaration(service="example") -> dict:
return {
"schema_version": "0.1",
"framework": "netkingdom-tenancy-posture",
"service": service,
"role": "test-service",
"responsible_repo": "example-owner",
"tenancy": {
"current": {"I": 1, "A": 1, "E": 0, "P": 0, "R": 1, "V": 0},
"target": {"I": 1, "A": 1, "E": 0, "P": 0, "R": 1, "V": 0},
"reviewed": "2026-08-01",
"review_due": "2026-12-31",
"service_class": "interactive",
"reason": {
"I": "floor explained",
"A": "floor explained",
"E": "floor explained",
"P": "floor explained",
"R": "floor explained",
"V": "floor explained",
},
},
}
def write_declaration(tmp_path: pathlib.Path, document: dict, name="tenancy.yaml") -> pathlib.Path:
path = tmp_path / name
path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8")
return path
def build(path: pathlib.Path, *, as_of="2026-08-23T12:00:00Z", horizon=14, fail_on="none"):
return feedback.build_report(
[path],
as_of=feedback.parse_timestamp(as_of),
horizon_days=horizon,
fail_on=fail_on,
)
def classes(report: dict) -> list[str]:
return [item["class"] for item in report["findings"]]
def test_checked_in_reference_emits_five_proposals_and_no_authority():
repo_root = pathlib.Path(__file__).resolve().parents[3]
path = repo_root / "examples/posture-feedback/expired-e2.yaml"
report = build(path)
assert report["summary"] == {
"total": 5,
"high": 1,
"medium": 3,
"low": 1,
"unknown_owner": 0,
}
assert report["automation"] == {
"mode": "proposal-only",
"external_write_permitted": False,
"policy_mutation_permitted": False,
"declaration_mutation_permitted": False,
}
def test_missing_adversarial_freshness_is_unknown_without_owner_inference(tmp_path):
document = declaration()
document["tenancy"]["current"]["E"] = 2
document["tenancy"]["target"]["E"] = 2
document["tenancy"]["reason"].pop("E")
document["evidence"] = {"E2": "docs/evidence/e2.md"}
path = write_declaration(tmp_path, document)
report = build(path)
finding = next(item for item in report["findings"] if item["class"] == "evidence-freshness-unknown")
assert finding["owner"] == "unknown"
assert finding["evidence_state"] == "unknown"
def test_exact_evidence_expiry_is_still_valid_but_due(tmp_path):
document = declaration()
document["tenancy"]["current"]["E"] = 2
document["tenancy"]["target"]["E"] = 2
document["tenancy"]["reason"].pop("E")
document["evidence"] = {"E2": "docs/evidence/e2.md"}
document["evidence_freshness"] = {
"E2": {
"kind": "adversarial",
"observed_at": "2026-08-22T12:00:00Z",
"valid_until": "2026-08-23T12:00:00Z",
"responsible_repo": "evidence-owner",
"scope": "bounded",
"remediation": "repeat",
}
}
path = write_declaration(tmp_path, document)
report = build(path, horizon=0)
assert "evidence-expired" not in classes(report)
assert "evidence-due-soon" in classes(report)
def test_review_due_date_expires_after_utc_calendar_day(tmp_path):
document = declaration()
document["tenancy"]["review_due"] = "2026-08-23"
path = write_declaration(tmp_path, document)
due_today = build(path, as_of="2026-08-23T23:59:59Z", horizon=0)
overdue = build(path, as_of="2026-08-24T00:00:00Z", horizon=0)
assert "posture-review-due-soon" in classes(due_today)
assert "posture-review-overdue" not in classes(due_today)
assert "posture-review-overdue" in classes(overdue)
def test_posture_owner_is_unknown_when_not_declared(tmp_path):
document = declaration()
del document["responsible_repo"]
document["tenancy"]["review_due"] = "2026-08-01"
path = write_declaration(tmp_path, document)
report = build(path)
finding = next(item for item in report["findings"] if item["class"] == "posture-review-overdue")
assert finding["owner"] == "unknown"
assert report["summary"]["unknown_owner"] == 1
def test_multi_service_declaration_is_evaluated_per_service(tmp_path):
first = declaration("first")
second = declaration("second")
for entry in (first, second):
entry.pop("schema_version")
entry.pop("framework")
first["tenancy"]["gap"] = {"A": "first gap"}
second["tenancy"]["gap"] = {"V": "second gap"}
document = {
"schema_version": "0.1",
"framework": "netkingdom-tenancy-posture",
"services": [first, second],
}
path = write_declaration(tmp_path, document)
report = build(path)
assert {(item["service"], item["control"]) for item in report["findings"]} == {
("first", "A"),
("second", "V"),
}
def test_mechanical_evidence_without_expiry_creates_no_freshness_finding(tmp_path):
document = declaration()
document["tenancy"]["current"]["A"] = 2
document["tenancy"]["target"]["A"] = 2
document["tenancy"]["reason"].pop("A")
document["evidence"] = {"A2": "tests/authorization.py"}
document["evidence_freshness"] = {
"A2": {
"kind": "mechanical",
"observed_at": "2026-08-22T12:00:00Z",
"responsible_repo": "example-owner",
"scope": "continuous test",
"remediation": "repair the test",
}
}
path = write_declaration(tmp_path, document)
report = build(path)
assert not any(item["class"].startswith("evidence-") for item in report["findings"])
def test_report_digest_is_stable_for_identical_inputs(tmp_path):
path = write_declaration(tmp_path, declaration())
first = build(path)
second = build(path)
assert first == second
assert first["report_digest"].startswith("sha256:")
def test_workspace_source_identifier_is_portable_and_not_an_owner_inference():
repo_root = pathlib.Path(__file__).resolve().parents[3]
path = repo_root / "examples/posture-feedback/expired-e2.yaml"
report = build(path)
assert {item["source"] for item in report["findings"]} == {
"net-kingdom/examples/posture-feedback/expired-e2.yaml"
}
gap = next(item for item in report["findings"] if item["class"] == "declared-gap")
zone = next(item for item in report["findings"] if item["class"].startswith("zone-"))
assert gap["owner"] == "net-kingdom"
assert zone["owner"] == "team:platform-security"
assert all(item["owner"] != item["source"] for item in report["findings"])
def test_fail_on_threshold_changes_ok_not_findings(tmp_path):
document = declaration()
document["tenancy"]["gap"] = {"R": "declared gap"}
path = write_declaration(tmp_path, document)
report_only = build(path, fail_on="none")
failing = build(path, fail_on="low")
assert report_only["ok"] is True
assert failing["ok"] is False
assert report_only["findings"] == failing["findings"]
def test_invalid_declaration_is_reported_and_cli_exits_two(tmp_path, capsys):
document = declaration()
del document["role"]
path = write_declaration(tmp_path, document)
exit_code = feedback.main(
[
"--as-of",
"2026-08-23T12:00:00Z",
"--fail-on",
"none",
str(path),
]
)
payload = json.loads(capsys.readouterr().out)
assert exit_code == 2
assert payload["ok"] is False
assert payload["validation_errors"]
def test_timestamp_without_offset_is_rejected():
try:
feedback.parse_timestamp("2026-08-23T12:00:00")
except ValueError as exc:
assert "explicit UTC offset" in str(exc)
else:
raise AssertionError("timezone-naive timestamp was accepted")

View file

@ -89,6 +89,41 @@ class SemanticValidationTests(unittest.TestCase):
document["tenancy"]["review_due"] = "2026-08-16"
self.assertIn("review_due precedes reviewed", self.validate(document)[0])
def test_evidence_freshness_must_reference_evidence_key(self) -> None:
document = declaration()
document["evidence_freshness"] = {
"E2": {
"kind": "adversarial",
"observed_at": "2026-08-22T22:10:25Z",
"valid_until": "2026-08-23T22:10:25Z",
"responsible_repo": "example",
"scope": "bounded tenant probes",
"remediation": "repeat the bounded run",
}
}
self.assertIn(
"evidence_freshness E2 has no evidence entry",
self.validate(document)[0],
)
def test_evidence_freshness_expiry_must_follow_observation(self) -> None:
document = declaration()
document["evidence"] = {"E2": "docs/evidence/e2.md"}
document["evidence_freshness"] = {
"E2": {
"kind": "adversarial",
"observed_at": "2026-08-22T22:10:25Z",
"valid_until": "2026-08-22T22:10:25Z",
"responsible_repo": "example",
"scope": "bounded tenant probes",
"remediation": "repeat the bounded run",
}
}
self.assertIn(
"valid_until must be after observed_at",
self.validate(document)[0],
)
def test_service_names_are_unique(self) -> None:
entry = declaration()
document = {

View file

@ -67,6 +67,7 @@ def validate_semantics(document: dict[str, Any], path: pathlib.Path) -> list[str
reason = posture.get("reason", {})
gap = posture.get("gap", {})
evidence = entry.get("evidence", {})
evidence_freshness = entry.get("evidence_freshness", {})
reviewed = dt.date.fromisoformat(posture["reviewed"])
review_due = dt.date.fromisoformat(posture["review_due"])
@ -92,6 +93,22 @@ def validate_semantics(document: dict[str, Any], path: pathlib.Path) -> list[str
f"{name}: implemented {axis}{level} must be above current {axis}{current_level}"
)
for key, freshness in evidence_freshness.items():
if key not in evidence:
errors.append(f"{name}: evidence_freshness {key} has no evidence entry")
observed_at = dt.datetime.fromisoformat(
freshness["observed_at"].replace("Z", "+00:00")
)
valid_until_raw = freshness.get("valid_until")
if valid_until_raw:
valid_until = dt.datetime.fromisoformat(
valid_until_raw.replace("Z", "+00:00")
)
if valid_until <= observed_at:
errors.append(
f"{name}: evidence_freshness {key} valid_until must be after observed_at"
)
provider = entry.get("provider", {})
for axis, reach in provider.get("axes", {}).items():
available = reach["available"]

View file

@ -0,0 +1,111 @@
---
id: NK-WP-0031
type: workplan
title: "Implement deterministic posture and evidence feedback"
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-0031 — Deterministic posture and evidence feedback
Authorized by the operator as the next locally owned increment after
NK-WP-0030. This implements the safe first C6 loop proposed by the 2026-08-23
scope-to-intent assessment: surface explicit drift as proposals without
autonomous policy or work-record mutation.
## Define freshness, owner, and proposal semantics
```task
id: NK-WP-0031-T01
status: done
priority: high
```
Extend Tenancy Posture declarations with optional authoritative repository and
evidence-freshness metadata. Define deterministic time, owner resolution,
finding severity, stable identity, and the proposal-only safety boundary.
Implemented in Tenancy Posture draft-14, Posture Feedback v0.1, and their
schemas. Missing authoritative ownership or freshness resolves to `unknown`;
source-path normalization never becomes owner inference.
## Implement the posture feedback evaluator
```task
id: NK-WP-0031-T02
status: done
priority: high
```
Validate declarations and emit deterministic JSON findings for overdue/due-soon
reviews, unknown/expired/due-soon evidence, implemented-but-unevidenced levels,
and declared gaps. Require explicit `--as-of`; never perform external writes.
Implemented under `tools/posture-feedback/`. The emitted report declares that
external, policy, and declaration writes are all forbidden.
## Publish reference workflow and tests
```task
id: NK-WP-0031-T03
status: done
priority: high
```
Provide an example declaration and cover freshness boundaries, missing owners,
multi-service inputs, stable digests, severity exit thresholds, and invalid
declarations.
The checked-in expired-E2 example yields five deterministic proposals at its
fixed evaluation time. Focused coverage includes every listed boundary plus
portable source identity.
## Obtain audit-core freshness adoption
```task
id: NK-WP-0031-T04
status: wait
priority: high
```
Ask `audit-core` to add authoritative owner and E2 freshness metadata for
`WH-ENG-20260822-AUDIT-E2-03`. The evaluator must report its freshness as
unknown until the source declaration supplies the exact timestamps and
replacement action; NetKingdom must not parse them from prose.
Requested in State Hub message `874e5fa5-e05c-4b1a-a915-26c4dba07b87`.
The same estate baseline found pre-existing missing evidence entries for
`railiance-platform` `apps-pg` current R2 and V1; that separate correction was
routed in `d65f8383-bdab-488e-a36f-f8cabd8a5f65`.
## Verify and reconcile
```task
id: NK-WP-0031-T05
status: done
priority: medium
```
Run focused and repository regression tests, validate schemas and examples,
reconcile State Hub, and publish the implementation evidence.
Verification on 2026-08-23:
- 28 focused posture-feedback and tenancy-validator tests passed;
- 84 root tests passed with the independently packaged `local-identity`
project excluded, then all 142 `local-identity` tests passed in its own
environment;
- both JSON schemas parsed, the fixed-time reference target succeeded, and
`git diff --check` passed;
- reuse-surface validated the capability registry entry; and
- the read-only seven-declaration baseline is recorded in
`history/2026-08-23-posture-feedback-estate-baseline.md`.
Local implementation is complete. The workplan remains blocked only on T04's
externally owned audit-core declaration adoption.