Record reviewed case source evidence (4/13)
This commit is contained in:
parent
935aed2bcb
commit
42e18ef0c8
20 changed files with 2801 additions and 0 deletions
|
|
@ -0,0 +1,79 @@
|
|||
from pathlib import Path
|
||||
import hashlib,json,subprocess,copy,yaml,re,importlib.util,sys
|
||||
p=Path(__file__).resolve().parent;r=Path('/home/worsch/net-kingdom');base='342a6f25af5cdc602841948f42759ea52ef1c8f5'
|
||||
def git(root,*a):return subprocess.check_output(['git','-C',str(root),*a],text=True).strip()
|
||||
assert git(r,'rev-parse','HEAD')==base
|
||||
assert git(Path('/home/worsch/railiance-platform'),'rev-parse','HEAD')=='104ed90f42a9a86bf9fda343b49b65a85c9ee4b9'
|
||||
template=Path('/home/worsch/railiance-platform/manifests/templates/keycape-issuer-eso.yaml')
|
||||
assert hashlib.sha256(template.read_bytes()).hexdigest()=='e17498bd8a4c820041a4105e4efe9c60c6a50953db8f621ff54887080065c743'
|
||||
name='security-case-type-revisions/1.3/oidc-browser-relying-party/type.yaml';old=(r/name).read_bytes();old_doc=yaml.safe_load(old)
|
||||
assert hashlib.sha256(old).hexdigest()=='e75fe94110ef9fb7fb18072fe2856941b33507c99cb406d36576046481485d0a'
|
||||
(p/'type.preimage.yaml').write_bytes(old)
|
||||
patch=p.parent/'nk48-r5-f2-publication-r1/netkingdom-entry-point.conditional.patch'
|
||||
assert hashlib.sha256(patch.read_bytes()).hexdigest()=='6784a21fbd10bd62712be2e0f4929034601c94629aa74ccaa2e93fc815eecd61'
|
||||
subprocess.run(['git','apply','--check',str(patch.resolve())],cwd=r,check=True)
|
||||
subprocess.run(['git','apply',str(patch.resolve())],cwd=r,check=True)
|
||||
new=(r/name).read_bytes();new_doc=yaml.safe_load(new)
|
||||
a=copy.deepcopy(old_doc);b=copy.deepcopy(new_doc)
|
||||
for doc in [a,b]:
|
||||
step=next(x for x in doc['spec']['follow_through'] if x['id']=='deliver-secret');step['entry_point']='same-entry-removed-for-comparison'
|
||||
assert a==b,'only entry_point may change'
|
||||
assert old_doc['spec']['parameters']==new_doc['spec']['parameters']
|
||||
assert old_doc['spec']['execution_descriptors']==new_doc['spec']['execution_descriptors']
|
||||
assert new_doc.get('metadata',{}).get('status')==old_doc.get('metadata',{}).get('status')
|
||||
# Compose by importing the read-only seven-adapter composer, never dispatch.
|
||||
sys.path.insert(0,'/home/worsch/ops-warden/src');sys.dont_write_bytecode=True
|
||||
module=r/'sso-mfa/k8s/keycape/compose-1.2-descriptors.py'
|
||||
spec=importlib.util.spec_from_file_location('nk48_composer',module);c=importlib.util.module_from_spec(spec);spec.loader.exec_module(c)
|
||||
artifact=r/'sso-mfa/k8s/keycape/descriptor-composition-1.3.candidate.json';before=artifact.read_bytes();doc=json.loads(before)
|
||||
regenerated=(json.dumps(c.artifact(doc['check_config_digest'],doc['params'],'1.3'),indent=2,sort_keys=True)+'\n').encode()
|
||||
assert regenerated==before,'Unexpected descriptor/artifact delta; inspect before writing'
|
||||
(p/'composition-regenerated.json').write_bytes(regenerated)
|
||||
wp=r/'workplans/NK-WP-0048-security-case-types.md';text=wp.read_text();(p/'workplan.preimage.md').write_text(text)
|
||||
for task,status in [('NK-WP-0048-T33','done'),('NK-WP-0048-T34','progress')]:
|
||||
match=re.search(r'```task\nid: '+task+r'\n.*?```',text,re.S);assert match
|
||||
before_block=match.group();assert 'status: todo\n' in before_block
|
||||
text=text[:match.start()]+before_block.replace('status: todo\n','status: '+status+'\n')+text[match.end():]
|
||||
needle='## T34 — Close the consumer configuration inverse finding'
|
||||
completion="""2026-10-17 completed exact proposed-source reference correction: Platform
|
||||
published template, metadata helper and eight focused tests in
|
||||
`104ed90f42a9a86bf9fda343b49b65a85c9ee4b9`; independent source review
|
||||
`nk48-r5-f2-review-r1` found no blocking source-fit defect. The proposed 1.3
|
||||
entry now names ClusterSecretStore `openbao-keycape-{client_id}-issuer`, cluster
|
||||
scope restricted to `sso`, and the exact Platform template digest. Only that
|
||||
entry text changes; parameters and all seven descriptors are unchanged, and
|
||||
regeneration produces the identical not-an-admission composition artifact.
|
||||
This closes the coordinator's vague-reference source finding only. It supplies
|
||||
no formal owner assent, existing live store, installed ESO compatibility,
|
||||
real delivery/removal/session capability, grant or native acceptance; existing
|
||||
RPF-1.3-T4 and T16 keep those obligations. F3 is independent and remains open.
|
||||
Evidence: Custodian `nk48-r5-f2-publication-r1/` and
|
||||
`nk48-issuer-reference-integration-r1/` under the existing 17 October packet.
|
||||
|
||||
"""
|
||||
assert text.count(needle)==1;text=text.replace(needle,completion+needle)
|
||||
needle='## T35 — Align source admission fixtures with the authenticated catalog and attended-session API'
|
||||
note="""2026-10-17 implementation progress: the fresh independent Apps review
|
||||
reproduced a missing fresh inverse-session boundary. Three new regressions fail
|
||||
the original wrapper. Corrected r2 requires a separately authenticated inverse
|
||||
session before the release-writer lock and revokes it on every exit; 19 tests /
|
||||
33 subtests pass, and removing the session guard is detected. This remains a
|
||||
source candidate: direct Apps source inspection found no real owner transaction,
|
||||
durable journal, all-release-writer exclusion or inverse-session backend to bind.
|
||||
The four exact missing owner facilities and smallest existing T05 implementation
|
||||
are retained in Custodian `nk48-r5-f3-implementation-r2/owner-backend-next.md`.
|
||||
T34 progresses for this correction and investigation; it does not claim the real
|
||||
backend, accepted type placement, owner assent or F3 closure. The unsafe original
|
||||
candidate/review remain frozen. Do not integrate its type prose as an implemented
|
||||
capability or reuse the revoked hand-over session.
|
||||
|
||||
"""
|
||||
assert text.count(needle)==1;text=text.replace(needle,note+needle)
|
||||
text=text.replace('and `nk48-catalog-fixture-publication-r1/`. Canonical validation/publication
|
||||
receipts belong in the latter packet when performed.','and `nk48-catalog-fixture-publication-r2/`. Root canonical validation passes
|
||||
31 tests / 53 subtests against Warden `922c26d` (the same reviewed code plus
|
||||
one normal allocator-only source UUID commit). Publication receipts belong
|
||||
in that packet.')
|
||||
wp.write_text(text)
|
||||
(p/'verification.json').write_text(json.dumps({'status':'integrated-proposed-source-only','only_type_delta':'deliver-secret.entry_point','type_before_sha256':hashlib.sha256(old).hexdigest(),'type_after_sha256':hashlib.sha256(new).hexdigest(),'parameters_unchanged':True,'seven_descriptors_unchanged':True,'composition_byte_identical':True,'T33':'done: exact source reference only','T34':'progress: real owner backend absent','owner_assent':False,'native_action':False},indent=2)+'\n')
|
||||
print('Applied exact issuer reference, verified unchanged descriptors/parameters/artifact; recorded bounded T33 completion and T34 progress.')
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
apiVersion: netkingdom.io/security-case-type/v0.1
|
||||
kind: SecurityCaseType
|
||||
spec:
|
||||
id: oidc-browser-relying-party
|
||||
version: '1.3'
|
||||
status: proposed
|
||||
title: Browser OIDC relying party for an internal service
|
||||
purpose: 'Let an internal web service sign its human users in through NetKingdom
|
||||
(KeyCape): an exact-redirect OIDC client, optional MFA pin, the LLDAP groups the
|
||||
service maps to its own roles, and, for confidential clients, a client secret
|
||||
minted straight into OpenBao custody. Replaces the per-client rollout scripts
|
||||
in sso-mfa/k8s/keycape. First case: Forgejo (KEY-WP-0038).'
|
||||
parameters:
|
||||
- name: client_id
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[a-z0-9][a-z0-9-]{1,62}$
|
||||
sensitivity: operational
|
||||
description: OAuth2 client_id, unique in KeyCape.
|
||||
- name: display_name
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[^\n\r]{1,80}$
|
||||
sensitivity: public
|
||||
description: Name shown on the sign-in page.
|
||||
- name: redirect_uris
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^https://[^*?#\s,]+(,https://[^*?#\s,]+)*$
|
||||
sensitivity: security_sensitive
|
||||
description: 'Comma-separated exact https redirect URIs. No wildcards, no plain
|
||||
http, no query and no fragment: KeyCape refuses ''*'' and ''?'' at startup,
|
||||
and OAuth 2.0 forbids fragments (key-cape type review R1).'
|
||||
- name: allowed_scopes
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
enum:
|
||||
- openid,profile,email,groups
|
||||
- openid,email,groups
|
||||
- openid,profile,groups
|
||||
- openid,groups
|
||||
sensitivity: security_sensitive
|
||||
description: Scopes the client may request, openid first. Always includes groups,
|
||||
because access_groups is required and KeyCape's UserInfo returns groups only
|
||||
for that scope (key-cape type review R2).
|
||||
- name: client_type
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
enum:
|
||||
- confidential
|
||||
- public
|
||||
sensitivity: security_sensitive
|
||||
description: confidential for server-side services that can hold a secret; public
|
||||
for SPAs and native apps (PKCE only).
|
||||
- name: mfa_required
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
enum:
|
||||
- 'true'
|
||||
- 'false'
|
||||
sensitivity: security_sensitive
|
||||
description: '"true" pins MFA on this client; required when the service grants
|
||||
administrative or owner rights. "false" is not "no MFA": the client then carries
|
||||
no mfaRequired field and KeyCape applies the per-user privacyIDEA policy. A
|
||||
renderer must never emit an explicit mfaRequired false, which would skip that
|
||||
policy (key-cape type review R3).'
|
||||
- name: access_groups
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[a-z][a-z0-9-]{1,62}(,[a-z][a-z0-9-]{1,62})*$
|
||||
sensitivity: operational
|
||||
description: 'Comma-separated LLDAP groups that the service maps to its roles.
|
||||
Created if absent. KeyCape has no per-client group gate: it issues tokens to
|
||||
any authenticated user and only carries the groups claim, so the consuming service
|
||||
enforces the gate itself (for Forgejo, its required-claim setting; key-cape
|
||||
type review R2).'
|
||||
- name: initial_members
|
||||
type: string
|
||||
required: false
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\+[a-z][a-z0-9._-]*)*(;[a-z][a-z0-9-]*=[a-z][a-z0-9._-]*(\+[a-z][a-z0-9._-]*)*)*$
|
||||
sensitivity: operational
|
||||
description: Initial memberships as group=user+user;group=user. Usernames are
|
||||
directory usernames, never secrets.
|
||||
- name: consuming_service
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[a-z0-9][a-z0-9-]{0,62}$
|
||||
sensitivity: operational
|
||||
description: The service that signs users in with this client.
|
||||
- name: consumer_owner_repo
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[a-z0-9][a-z0-9-]*$
|
||||
sensitivity: operational
|
||||
description: The repository that owns the consuming service's configuration.
|
||||
- name: consumer_namespace
|
||||
type: string
|
||||
case_settable: true
|
||||
constraints:
|
||||
pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$
|
||||
sensitivity: security_sensitive
|
||||
description: Consumer namespace for namespaced ESO TokenRequest reader; actual
|
||||
workload owner must confirm; never infer from service name.
|
||||
- name: issuer
|
||||
type: string
|
||||
case_settable: false
|
||||
default: https://kc.coulomb.social
|
||||
sensitivity: security_sensitive
|
||||
description: The NetKingdom issuer; fixed by the type.
|
||||
composition:
|
||||
authority: netkingdom
|
||||
initial_trust:
|
||||
- runtime_secret_trust
|
||||
- runtime_identity_trust
|
||||
capabilities:
|
||||
- c1.lightweight-sso
|
||||
providers:
|
||||
c1.lightweight-sso: net-kingdom.keycape-client-registration
|
||||
parameter_bindings:
|
||||
net-kingdom.keycape-client-registration.client_id: client_id
|
||||
net-kingdom.keycape-client-registration.display_name: display_name
|
||||
net-kingdom.keycape-client-registration.redirect_uris: redirect_uris
|
||||
net-kingdom.keycape-client-registration.allowed_scopes: allowed_scopes
|
||||
net-kingdom.keycape-client-registration.client_type: client_type
|
||||
net-kingdom.keycape-client-registration.mfa_required: mfa_required
|
||||
materialization:
|
||||
- id: client-secret-path
|
||||
kind: kv_path
|
||||
when: client_type == confidential
|
||||
template: platform/workloads/keycape/{client_id}
|
||||
body: field CLIENT_SECRET; minted by the credential broker, never written by hand
|
||||
- id: keycape-reader-policy
|
||||
kind: openbao_policy
|
||||
when: client_type == confidential
|
||||
template: workload-kv-read-keycape-{client_id}-eso
|
||||
body: "path \"platform/data/workloads/keycape/{client_id}\" {{\n capabilities\
|
||||
\ = [\"read\"]\n}}\n"
|
||||
description: KeyCape reads its client secret only through ESO. No metadata, list,
|
||||
parent or sibling access.
|
||||
- id: consumer-reader-policy
|
||||
kind: openbao_policy
|
||||
when: client_type == confidential
|
||||
template: workload-kv-read-keycape-{client_id}-{consuming_service}
|
||||
body: "path \"platform/data/workloads/keycape/{client_id}\" {{\n capabilities\
|
||||
\ = [\"read\"]\n}}\n"
|
||||
description: The consuming service reads the same secret. No metadata, list, parent
|
||||
or sibling access.
|
||||
- id: eso-role
|
||||
kind: openbao_role
|
||||
when: client_type == confidential
|
||||
template: external-secrets-keycape-{client_id}
|
||||
body: 'auth: kubernetes
|
||||
|
||||
bound_service_account_names: [external-secrets]
|
||||
|
||||
bound_service_account_namespaces: [external-secrets]
|
||||
|
||||
token_policies: [workload-kv-read-keycape-{client_id}-eso]
|
||||
|
||||
token_no_default_policy: true
|
||||
|
||||
token_ttl: 15m
|
||||
|
||||
token_max_ttl: 15m
|
||||
|
||||
'
|
||||
description: The Kubernetes-auth role deliver-secret uses; bound to the ESO service
|
||||
account only.
|
||||
- id: consumer-eso-role
|
||||
kind: openbao_role
|
||||
when: client_type == confidential
|
||||
template: external-secrets-keycape-{client_id}-{consuming_service}
|
||||
body: 'auth: kubernetes
|
||||
|
||||
bound_service_account_names: [{consuming_service}-keycape-{client_id}-eso]
|
||||
|
||||
bound_service_account_namespaces: [{consumer_namespace}]
|
||||
|
||||
audience: openbao
|
||||
|
||||
token_policies: [workload-kv-read-keycape-{client_id}-{consuming_service}]
|
||||
|
||||
token_no_default_policy: true
|
||||
|
||||
token_ttl: 15m
|
||||
|
||||
token_max_ttl: 15m
|
||||
|
||||
'
|
||||
description: Consumer ESO requests a token for this separate reference identity.
|
||||
Common ESO controller remains trusted and can request tokens; no cryptographic
|
||||
controller isolation is claimed.
|
||||
- id: catalog-entry
|
||||
kind: catalog_entry
|
||||
template: '{consuming_service}-keycape-client'
|
||||
body: 'id: {consuming_service}-keycape-client
|
||||
|
||||
owner_repo: key-cape
|
||||
|
||||
warden_executes: false
|
||||
|
||||
risk: high
|
||||
|
||||
status: active once verification passes
|
||||
|
||||
workload_ref: applicable, name {consuming_service}, declaration_ref of the consumer''s
|
||||
published declaration or the explicit unknown form, never inferred
|
||||
|
||||
path_template: platform/workloads/keycape/{client_id}
|
||||
|
||||
field: CLIENT_SECRET
|
||||
|
||||
delegation: native, intended_owner key-cape
|
||||
|
||||
rotation: mint-secret then verification; automatable false until a run has been
|
||||
proven
|
||||
|
||||
'
|
||||
description: One entry per case (ops-mason item 5, ops-warden R4'). It carries
|
||||
the consumer's workload reference, which ADR-0009 needs for zone selection.
|
||||
follow_through:
|
||||
- id: ensure-groups
|
||||
owner: net-kingdom
|
||||
entry_point: reviewed adapter net-kingdom/keycape-ensure-groups, dispatched by Warden's
|
||||
case kernel inside the attended session; it calls identity-provisioner
|
||||
ensure_service_groups in the provisioner pod through the session's contained
|
||||
cluster access (groups {access_groups}; members exactly {initial_members} as
|
||||
recorded in the accepted case, nothing else)
|
||||
auth: the attended human session on lane keycape-attended-case-change (actor human,
|
||||
parent-session revoked per attempt); the in-pod call uses the provisioner's own
|
||||
directory binding, and no provisioner or user-engine token is read or sent
|
||||
lane: keycape-attended-case-change
|
||||
warden_executes: false
|
||||
attended: true
|
||||
value_handling: none
|
||||
attended_justification: The provisioner's only HTTP credential is user-engine's
|
||||
runtime token, and its NetworkPolicy admits only user-engine pods. A proxy lane
|
||||
would act as user-engine (ops-warden review R8; canon section 3; ops-warden
|
||||
ADR-0002). Running it in the session register-client already needs adds no attended
|
||||
session. It can become unattended once the provisioner has an operator credential
|
||||
of its own and an admitted path. Warden's case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher.
|
||||
- id: mint-secret
|
||||
owner: railiance-platform
|
||||
when: client_type == confidential
|
||||
entry_point: 'railiance-platform credential broker: generate a client secret inside
|
||||
custody at bao:platform/workloads/keycape/{client_id}, through reviewed adapter
|
||||
railiance-platform/keycape-mint-secret'
|
||||
auth: a human caller (net-kingdom-admins, aal2) starts it; Warden runs it unattended
|
||||
through the broker lane under the caller's session, with the caller's broker grant
|
||||
for the case's KV path; agents and root are refused, and the value is generated
|
||||
in custody and never returned
|
||||
lane: keycape-client-secret-generate
|
||||
warden_executes: true
|
||||
attended: false
|
||||
value_handling: generated_in_custody
|
||||
- id: deliver-secret
|
||||
owner: railiance-platform
|
||||
when: client_type == confidential
|
||||
entry_point: reviewed adapter railiance-platform/keycape-deliver-secret, dispatched
|
||||
by Warden's case kernel; Platform's deliver capability applies ExternalSecret
|
||||
sso/keycape-{client_id}-client (key client-secret) through the ESO store for
|
||||
role external-secrets-keycape-{client_id}, from bao:platform/workloads/keycape/{client_id}
|
||||
auth: Platform's own delivery path on lane platform-secret-delivery, inside the human
|
||||
aal2 session; ESO reads custody through role external-secrets-keycape-{client_id};
|
||||
Warden holds no Platform credential, and no value leaves custody
|
||||
lane: platform-secret-delivery
|
||||
warden_executes: false
|
||||
attended: false
|
||||
value_handling: none
|
||||
ready_check: the delivery for sso/keycape-{client_id}-client reports Ready=True
|
||||
(status only, no value read)
|
||||
- id: bind-env
|
||||
owner: net-kingdom
|
||||
when: client_type == confidential
|
||||
entry_point: 'reviewed adapter net-kingdom/keycape-bind-env, dispatched by Warden''s
|
||||
case kernel inside the attended session: patch Deployment sso/keycape env
|
||||
KEYCAPE_{client_id:env}_CLIENT_SECRET from sso/keycape-{client_id}-client, by
|
||||
reference only'
|
||||
auth: the attended human session on lane keycape-attended-case-change, with operator
|
||||
kube credentials for namespace sso (actor human, parent-session revoked per attempt)
|
||||
lane: keycape-attended-case-change
|
||||
warden_executes: false
|
||||
attended: true
|
||||
value_handling: none
|
||||
attended_justification: A change to the live identity provider's Deployment. It
|
||||
runs in the same attended operator session as register-client, before it, because
|
||||
KeyCape resolves a confidential client's secret at start and fails closed if
|
||||
the binding is missing. Warden's case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher.
|
||||
- id: register-client
|
||||
owner: net-kingdom
|
||||
entry_point: reviewed adapter net-kingdom/keycape-register-client, dispatched by Warden's
|
||||
case kernel inside the attended session; it runs sso-mfa/k8s/keycape/keycape-client.py
|
||||
apply --client-file <client-from-case> --case-acceptance <record> --check-config
|
||||
<keycape binary> (net-kingdom.keycape-client-registration)
|
||||
auth: the attended human session on lane keycape-attended-case-change, with operator
|
||||
write access to the live KeyCape client registry in namespace sso (actor human,
|
||||
parent-session revoked per attempt)
|
||||
lane: keycape-attended-case-change
|
||||
warden_executes: false
|
||||
attended: true
|
||||
value_handling: none
|
||||
attended_justification: 'The live KeyCape client registry is a Kubernetes Secret,
|
||||
and agents do not read or write Secrets. Until KeyCape loads clients from a
|
||||
non-secret registry with secrets referenced from custody, one attended operator
|
||||
session applies the rendered registration. ops-warden groups all attended steps
|
||||
of a run into that one session. KeyCape loads clients at startup and keeps login,
|
||||
authorization and handoff state in memory, so the restart drops in-flight logins
|
||||
and sessions for every client; runs go in a quiet window (key-cape type review
|
||||
R4). The rendered registry must pass KeyCape''s own startup checks (keycape
|
||||
check-config, the pinned checker artifact sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5
|
||||
built from key-cape 3c71226, which keycape-client.py runs before it patches)
|
||||
before the restart: a refused entry stops the single replica and every client
|
||||
loses sign-in (R1). Warden''s case kernel dispatches the reviewed adapter only inside the one attended human session (descriptor actor human, lane keycape-attended-case-change, parent-session revoked per attempt); the human session is the authority and the kernel is only the dispatcher.'
|
||||
- id: deliver-consumer-secret
|
||||
owner: '{consumer_owner_repo}'
|
||||
when: client_type == confidential
|
||||
entry_point: Reviewed adapter net-kingdom/keycape-deliver-consumer-secret, dispatched
|
||||
by Warden's case kernel inside the attended consumer-owner session; the owner's
|
||||
apply capability creates dedicated ServiceAccount {consumer_namespace}/{consuming_service}-keycape-{client_id}-eso
|
||||
(automount=false, no legacy tokens), namespace SecretStore openbao-keycape-{client_id}-consumer
|
||||
with serviceAccountRef and audiences [openbao], and ExternalSecret {consuming_service}-netkingdom-oidc-client
|
||||
with Owner target, Retain remote deletion and only CLIENT_SECRET from workloads/keycape/{client_id}.
|
||||
Admission validates actual installed ESO TokenRequest support and common controller
|
||||
RBAC before any owner-approved apply.
|
||||
auth: the attended consumer-owner session on lane consumer-owner-deployment (actor
|
||||
human, the consumer owner's operator deployment authority, on the founder's go
|
||||
for the change); ESO authenticates to OpenBao role external-secrets-keycape-{client_id}-{consuming_service}
|
||||
with a TokenRequest for the dedicated namespaced serviceAccountRef; controller
|
||||
remains trusted
|
||||
lane: consumer-owner-deployment
|
||||
warden_executes: false
|
||||
attended: true
|
||||
value_handling: none
|
||||
attended_justification: The consumer owner's only deployment authority in the consumer
|
||||
namespace is its operator credential, and production applies need the founder's
|
||||
go per change. No contained agent deployer identity exists, and the owner does
|
||||
not want a standing credential an agent could trigger. It runs in one attended
|
||||
consumer-owner session together with hand-over. Warden's case kernel dispatches
|
||||
the reviewed adapter only inside that session (actor human, parent-session
|
||||
revoked per attempt); the human session is the authority and the kernel is only
|
||||
the dispatcher.
|
||||
ready_check: Namespace SecretStore and ExternalSecret both report Ready=True;
|
||||
dedicated SA has no legacy token refs; status and fixed metadata only, never
|
||||
Secret values.
|
||||
- id: hand-over
|
||||
owner: '{consumer_owner_repo}'
|
||||
entry_point: The consumer owner first makes its own Helm change configuring {consuming_service}
|
||||
with issuer {issuer}, client {client_id} and custody bao:platform/workloads/keycape/{client_id},
|
||||
requiring one of {access_groups} in the groups claim before sign-in completes.
|
||||
Then reviewed adapter net-kingdom/keycape-hand-over, dispatched by Warden's case
|
||||
kernel inside the attended consumer-owner session, verifies the consumer with
|
||||
four value-free probes (member-login, non-member-refused, membership-add-grants,
|
||||
membership-remove-revokes) and records the hand-over receipt.
|
||||
auth: the attended consumer-owner session on lane apps-consumer-handover (actor human,
|
||||
parent-session revoked per attempt); the consumer configuration itself is the
|
||||
owner's own deployment path
|
||||
lane: apps-consumer-handover
|
||||
warden_executes: false
|
||||
attended: true
|
||||
value_handling: none
|
||||
attended_justification: The verification needs a real member login and a refused
|
||||
non-member login, which only an attended session can perform, and it follows
|
||||
the owner's own Helm change in the same consumer-owner session as
|
||||
deliver-consumer-secret. Warden's case kernel dispatches the reviewed adapter
|
||||
only inside that session (actor human, parent-session revoked per attempt); the
|
||||
human session is the authority and the kernel is only the dispatcher.
|
||||
verification:
|
||||
- id: authorize-accepts-redirects
|
||||
check: GET {issuer}/authorize with client {client_id} and each of {redirect_uris}
|
||||
returns the login page
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: authorize-refuses-other-redirect
|
||||
check: GET {issuer}/authorize with client {client_id} and an unregistered redirect
|
||||
returns an OAuth error
|
||||
expect: deny
|
||||
reads_secret_value: false
|
||||
- id: custody-denied-to-default
|
||||
when: client_type == confidential
|
||||
check: a default-only OpenBao identity has no read capability on bao:platform/workloads/keycape/{client_id}
|
||||
expect: deny
|
||||
reads_secret_value: false
|
||||
- id: custody-allowed-to-consumer
|
||||
when: client_type == confidential
|
||||
check: policy workload-kv-read-keycape-{client_id}-{consuming_service} grants
|
||||
read on bao:platform/workloads/keycape/{client_id} (capabilities check only)
|
||||
expect: allow
|
||||
reads_secret_value: false
|
||||
- id: groups-present
|
||||
check: LLDAP groups {access_groups} exist, with exactly the memberships of the
|
||||
accepted case added
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: consumer-enforces-groups
|
||||
check: '{consuming_service}''s OIDC source for {client_id} (owner {consumer_owner_repo})
|
||||
requires one of {access_groups} in the groups claim (configuration read, no
|
||||
secret value)'
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: secret-delivered
|
||||
when: client_type == confidential
|
||||
check: the delivery for sso/keycape-{client_id}-client reports Ready=True (status
|
||||
only)
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: mfa-pinned
|
||||
when: mfa_required == true
|
||||
check: keycape-client.py inspect for {client_id} shows the MFA pin on the registered
|
||||
client (non-secret client view)
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
- id: consumer-secret-delivered
|
||||
when: client_type == confidential
|
||||
check: '{consumer_namespace} SecretStore openbao-keycape-{client_id}-consumer
|
||||
and ExternalSecret {consuming_service}-netkingdom-oidc-client Ready=True, exact
|
||||
dedicated SA/audience role binding, fixed CLIENT_SECRET mapping; no Secret value
|
||||
read'
|
||||
expect: present
|
||||
reads_secret_value: false
|
||||
rollback:
|
||||
- reverses: hand-over
|
||||
action: the adapter removes only its hand-over receipt. Removing the OIDC source
|
||||
for {client_id} from {consuming_service} is the owner's Helm rollback (owner
|
||||
{consumer_owner_repo}), done inside deliver-consumer-secret's owner_remove
|
||||
before any consumer object is removed
|
||||
- reverses: deliver-consumer-secret
|
||||
when: client_type == confidential
|
||||
action: After reverting consumer secret reference, delete only authenticated created
|
||||
ExternalSecret/owned target by UID and ownerReference, then SecretStore and
|
||||
dedicated SA; preserve preexisting-matched resources and never delete a shared
|
||||
target. Retain audit and ownership receipts; removing SecretStore/SA alone is
|
||||
not immediate OpenBao token revocation.
|
||||
- reverses: register-client
|
||||
action: keycape-client.py apply --operation remove for {client_id}
|
||||
- reverses: bind-env
|
||||
when: client_type == confidential
|
||||
action: remove env KEYCAPE_{client_id:env}_CLIENT_SECRET from Deployment sso/keycape,
|
||||
after register-client is reversed
|
||||
- reverses: deliver-secret
|
||||
when: client_type == confidential
|
||||
action: delete ExternalSecret sso/keycape-{client_id}-client and its target
|
||||
- reverses: mint-secret
|
||||
when: client_type == confidential
|
||||
action: 'covered by client-secret-path: one metadata delete, after deliver-secret
|
||||
is reversed'
|
||||
- reverses: ensure-groups
|
||||
action: identity-provisioner revoke_service_groups inside the provisioner pod
|
||||
(attended, as ensure-groups) with the case's memberships; delete_if_empty only
|
||||
if no other case uses {access_groups}
|
||||
- reverses: client-secret-path
|
||||
when: client_type == confidential
|
||||
action: delete metadata and all versions at platform/workloads/keycape/{client_id},
|
||||
after the ExternalSecret is gone (this also reverses mint-secret)
|
||||
- reverses: eso-role
|
||||
when: client_type == confidential
|
||||
action: delete Kubernetes-auth role external-secrets-keycape-{client_id}
|
||||
- reverses: keycape-reader-policy
|
||||
when: client_type == confidential
|
||||
action: delete policy workload-kv-read-keycape-{client_id}-eso, after eso-role
|
||||
- reverses: consumer-eso-role
|
||||
when: client_type == confidential
|
||||
action: Delete only this case-created role external-secrets-keycape-{client_id}-{consuming_service},
|
||||
before deleting consumer-reader-policy; revoke case-issued OpenBao tokens or
|
||||
prove expiration before declaring deactivation.
|
||||
- reverses: consumer-reader-policy
|
||||
when: client_type == confidential
|
||||
action: delete policy workload-kv-read-keycape-{client_id}-{consuming_service}
|
||||
- reverses: catalog-entry
|
||||
action: remove catalog entry {consuming_service}-keycape-client
|
||||
acceptance:
|
||||
accepted_by:
|
||||
- owner
|
||||
- founder
|
||||
risk_class: medium
|
||||
# Reviewed machine descriptors (NK-WP-0048-T16 successor candidate). Digests are
|
||||
# generated by sso-mfa/k8s/keycape/compose-1.2-descriptors.py --version 1.3.
|
||||
execution_descriptors:
|
||||
bind-env: sha256:d5fcb20cf15bf7454890d2529180c582218e3df12584a99162e4b664b899caa0
|
||||
deliver-consumer-secret: sha256:36281df8139fb1ec64b226d94f80eb592de5addab7063ff54c691c641e275fc7
|
||||
deliver-secret: sha256:75aaa7346aebdea2a0eb0ba993995515b5248ffefc34caf8e713525e9b58da85
|
||||
ensure-groups: sha256:ebc9d5b9167d18e6015f4bbeb9699e8d396d64fbfaf195142781f84f4f47a70a
|
||||
hand-over: sha256:1bce07a3229b7d0c7c730f35ce5b40fe471009a82de8dd53788ed970a9be1224
|
||||
mint-secret: sha256:c92957227235db04953809d92b260d32c8b947472a8e8fd17a258ade3ae8da70
|
||||
register-client: sha256:4ecc769f75433b66c7161f13c0fe7e5599633cf466d46cb6d8031f0456f426e7
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"status": "integrated-proposed-source-only",
|
||||
"only_type_delta": "deliver-secret.entry_point",
|
||||
"type_before_sha256": "e75fe94110ef9fb7fb18072fe2856941b33507c99cb406d36576046481485d0a",
|
||||
"type_after_sha256": "ef02ddae3167bdd1558193a5f6db0379fb8cc84207531706d5ef35caabc3684f",
|
||||
"parameters_unchanged": true,
|
||||
"seven_descriptors_unchanged": true,
|
||||
"composition_byte_identical": true,
|
||||
"T33": "done: exact source reference only",
|
||||
"T34": "progress: real owner backend absent",
|
||||
"owner_assent": false,
|
||||
"native_action": false
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,11 @@
|
|||
# NK48-R5-F1 successor coverage, prepared only
|
||||
|
||||
The patch changes only `sso-mfa/k8s/keycape/test_admit_1_3_type.py`; it has not been applied to the owner checkout, committed or published. Source base: `3236d5000e52e0643f33906a26a7825aa0b67465`. Kernel dependency: frozen `117fec9241e7b985dfb187b992e173d0a5dc3700`.
|
||||
|
||||
The committed-map refusal now explicitly includes an agent-actor mint descriptor. A separate fixture-map test acknowledges precisely that changed descriptor digest, allowing admission to reach the independent `agent mint is forbidden` guard. The human descriptor is also tested with an explicit fixture `WARDEN_AGENT_ID`; the helper sets it for that call instead of clearing it. Existing changed-caller coverage remains.
|
||||
|
||||
Focused results: original tests pass against both frozen and scratch guard-removed kernels (8 tests / 31 subtests each). Successor tests pass against the real frozen kernel (10 tests / 32 subtests). Removing only its admission agent-mint guard in a separate scratch kernel makes exactly both new guard tests fail (2 failed, 8 passed / 32 subtests). The dispatch-time guard remains present in the mutant; no adapter is dispatched.
|
||||
|
||||
`test-admit-1-3-agent-mint-coverage.patch` is the proposed owner patch. `scratch-kernel-mutation.patch` is negative-control evidence only and MUST NOT be applied to any canonical owner source. Preimage, successor, exact commands/results and hashes are retained beside this note. The runner requires the exported scratch trees named in scratch.json; it refuses a mismatched guard and modifies only those scratch trees.
|
||||
|
||||
These are fixture admission checks. Type acknowledgment, the 1.3 parameter pin and the changed descriptor-map entry exist only in memory inside tests. No actual type acknowledgment, catalog/pin change, kernel acceptance, native execution or grant is established. R5 F2/F3 and owner/kernel follow-through are unchanged. Root review/integration is the next step; no canonical task refinement is authored in this preparation-only packet.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"status": "applied_test_only",
|
||||
"path": "/home/worsch/net-kingdom/sso-mfa/k8s/keycape/test_admit_1_3_type.py",
|
||||
"preimage_sha256": "sha256:f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5",
|
||||
"after_sha256": "sha256:c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"native_actions": 0,
|
||||
"kernel_or_type_pin_changes": 0
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
M .custodian-brief.md
|
||||
M WORK-RECORDS.md
|
||||
?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/
|
||||
?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl
|
||||
?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json
|
||||
?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"command": "env PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/home/worsch/ops-warden/src /home/worsch/ops-warden/.venv/bin/python -B -m pytest -q -p no:cacheprovider test_admit_1_3_type.py",
|
||||
"working_directory": "/home/worsch/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"result": "10 passed, 32 subtests passed in 5.05s",
|
||||
"exit_code": 0,
|
||||
"source_sha256": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"publication_status": "not committed; remaining source implementation work retained"
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"schema": "custodian.nk48-r5-f1-source-implementation.v1",
|
||||
"status": "prepared-tested-not-applied",
|
||||
"finding": "NK48-R5-F1",
|
||||
"source_base": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"reviewed_code_base": "001a38ca2aa85155ce4840c4da22aca6e2346171",
|
||||
"kernel_base": "117fec9241e7b985dfb187b992e173d0a5dc3700",
|
||||
"target": "sso-mfa/k8s/keycape/test_admit_1_3_type.py",
|
||||
"preimage_sha256": "f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5",
|
||||
"successor_sha256": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"frozen_r5_review_sha256": "e3016352c58178894ccbfc600b960d3f9e2b5d534a05b8df2c5b79737cb0127a",
|
||||
"canonical_test_still_matches_preimage": true,
|
||||
"canonical_edits": false,
|
||||
"canonical_commit_or_publish": false,
|
||||
"kernel_mutation_scratch_only": true,
|
||||
"mutant_changed_files": [
|
||||
"src/warden/case_run.py"
|
||||
],
|
||||
"native_actions": false,
|
||||
"grants": false,
|
||||
"knowledge_orientation": {
|
||||
"package": "0.2.6",
|
||||
"digest": "8d1d6a9d4d07fec7b619618de7dfe2254c1790f85f4198c7fe19db9751c02",
|
||||
"cards": {
|
||||
"start": "withheld-source-changed",
|
||||
"identity": "withheld-source-changed"
|
||||
},
|
||||
"direct_source_review": [
|
||||
"net-kingdom/AGENTS.md",
|
||||
"net-kingdom/INTENT.md",
|
||||
"net-kingdom/SECURITY-COMPANION.md",
|
||||
"ops-warden/AGENTS.md",
|
||||
"ops-warden/INTENT.md",
|
||||
"ops-warden frozen case_run admission guard"
|
||||
]
|
||||
},
|
||||
"scope": "Only successor test_admit_1_3_type.py coverage. Fixture type status, map and parameter pin are memory-only test setup. No acknowledgment, pin, catalog, canonical kernel, native capability or owner acceptance change. F2/F3 and independent kernel/catalog/owner reviews remain open.",
|
||||
"results": [
|
||||
{
|
||||
"name": "preimage-frozen",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-frozen/src",
|
||||
"test_sha256": "f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5",
|
||||
"kernel_case_run_sha256": "77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0
|
||||
},
|
||||
{
|
||||
"name": "preimage-mutant",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-admission-guard-removed/src",
|
||||
"test_sha256": "f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5",
|
||||
"kernel_case_run_sha256": "6b0d61fb0da4737f305bb959524540752fbcf0f2993a7969bd7e368bdd03ea5a",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0
|
||||
},
|
||||
{
|
||||
"name": "successor-frozen",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-frozen/src",
|
||||
"test_sha256": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"kernel_case_run_sha256": "77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0
|
||||
},
|
||||
{
|
||||
"name": "successor-mutant",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-admission-guard-removed/src",
|
||||
"test_sha256": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"kernel_case_run_sha256": "6b0d61fb0da4737f305bb959524540752fbcf0f2993a7969bd7e368bdd03ea5a",
|
||||
"exit_code": 1,
|
||||
"expected_exit_code": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"status": "prepared-tested-not-applied",
|
||||
"files": {
|
||||
"implementation.json": "33d51784fe3ab7d9307292a265b0e684a8bc2fc77564ba58099833e41c849306",
|
||||
"reproduce-scratch-checks.py": "8f0b01e606c03b3b27b178df60eab664a758df6fa3f9b1bc62ef8b58122c2cd8",
|
||||
"successor-mutant.txt": "9f03083de5863b3855e533b94671cfbab6fd0dd565e0a0b8c385d51d9510d97c",
|
||||
"README.md": "78a4dbef0088f18db2b9c1dbe701b3e8a2c20516ae6d2039420a82b8d6b1fe66",
|
||||
"preimage-frozen.txt": "63dd3d754896c0e8578d82f98ae6f8e460a6ac43f3937a95fa61f65f8aa4accf",
|
||||
"test_admit_1_3_type.after.py": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"scratch.json": "7e329154c5c60528d9f4387f0bbc9cfb4167c79decd49b14497e67d59e21e469",
|
||||
"successor-frozen.txt": "275324dcf33eb852eb467832ff18fe9dd09f09d64d6ac37b43bf01766ea745d3",
|
||||
"canonical-net-kingdom-status-after.txt": "354b3dd65c711df0855c22ba053a4ca295d5491941c5a64c1157a0b7d43cf4cb",
|
||||
"scratch-kernel-mutation.patch": "dd12c63bd331b8f127149c402077e49fc66d62a452f48044486f58e79e0e29f7",
|
||||
"test-results.json": "2aab986ed96fb9fa683992051ffe6ec7b2a9327d816542b908dcc66f9dd8df23",
|
||||
"preimage-mutant.txt": "6c823ed279c355a34169d7753817256890d56fe4a1996ea59aa01fb19e5a5e1f",
|
||||
"test-admit-1-3-agent-mint-coverage.patch": "5bc1da176836eea65195cfe5fae965284c1f38809a502a0be58c231b60111a75",
|
||||
"test_admit_1_3_type.before.py": "f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
........ [100%]
|
||||
8 passed, 31 subtests passed in 4.98s
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
........ [100%]
|
||||
8 passed, 31 subtests passed in 4.90s
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from pathlib import Path
|
||||
import os,json,subprocess,shutil,hashlib,difflib
|
||||
packet=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/nk48-r5-f1-implementation-r1')
|
||||
s=json.loads((packet/'scratch.json').read_text());scratch=Path(s['scratch_root']);nk=scratch/'net-kingdom';frozen=scratch/'warden-frozen';mutant=scratch/'warden-admission-guard-removed';shutil.copytree(frozen,mutant)
|
||||
p=mutant/'src/warden/case_run.py';before=p.read_text()
|
||||
guard=''' if descriptor.effect == "mint" and (
|
||||
actor != "human" or os.getenv("WARDEN_AGENT_ID")
|
||||
):
|
||||
raise Refused("agent mint is forbidden")
|
||||
'''
|
||||
assert before.count(guard)==1
|
||||
p.write_text(before.replace(guard,''))
|
||||
(packet/'scratch-kernel-mutation.patch').write_text(''.join(difflib.unified_diff(before.splitlines(True),p.read_text().splitlines(True),fromfile='frozen/src/warden/case_run.py',tofile='scratch-mutant/src/warden/case_run.py')))
|
||||
test=nk/s['target'];successor=test.read_bytes();original=(packet/'test_admit_1_3_type.before.py').read_bytes()
|
||||
results=[]
|
||||
for name,kernel,source,expected in [('preimage-frozen',frozen,original,0),('preimage-mutant',mutant,original,0),('successor-frozen',frozen,successor,0),('successor-mutant',mutant,successor,1)]:
|
||||
test.write_bytes(source)
|
||||
env=os.environ.copy();env['PYTHONDONTWRITEBYTECODE']='1';env['PYTHONPATH']=str(kernel/'src')
|
||||
cmd=['/home/worsch/ops-warden/.venv/bin/python','-m','pytest','-q','-p','no:cacheprovider',test.name]
|
||||
run=subprocess.run(cmd,cwd=test.parent,env=env,capture_output=True,text=True)
|
||||
(packet/(name+'.txt')).write_text(run.stdout+run.stderr)
|
||||
results.append({'name':name,'command':cmd,'cwd':str(test.parent),'pythonpath':env['PYTHONPATH'],'test_sha256':hashlib.sha256(source).hexdigest(),'kernel_case_run_sha256':hashlib.sha256((kernel/'src/warden/case_run.py').read_bytes()).hexdigest(),'exit_code':run.returncode,'expected_exit_code':expected})
|
||||
print(name,run.returncode,run.stdout.splitlines()[-1],flush=True)
|
||||
assert run.returncode==expected
|
||||
if name=='successor-mutant':
|
||||
assert 'test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor' in run.stdout
|
||||
assert 'test_human_mint_is_refused_with_agent_runtime_identity' in run.stdout
|
||||
assert '2 failed, 8 passed' in run.stdout
|
||||
test.write_bytes(successor)
|
||||
(packet/'test-results.json').write_text(json.dumps(results,indent=2)+'\n')
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
--- frozen/src/warden/case_run.py
|
||||
+++ scratch-mutant/src/warden/case_run.py
|
||||
@@ -646,10 +646,6 @@
|
||||
or lane != descriptor.lane
|
||||
):
|
||||
raise Refused("principal actor or lane mismatch")
|
||||
- if descriptor.effect == "mint" and (
|
||||
- actor != "human" or os.getenv("WARDEN_AGENT_ID")
|
||||
- ):
|
||||
- raise Refused("agent mint is forbidden")
|
||||
parameters = case.get("parameters", {})
|
||||
declared = {
|
||||
item["name"]: item
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"scratch_root": "/tmp/nk48-r5-f1-ahudwokt",
|
||||
"target": "sso-mfa/k8s/keycape/test_admit_1_3_type.py",
|
||||
"net_kingdom_commit": "3236d5000e52e0643f33906a26a7825aa0b67465",
|
||||
"warden_commit": "117fec9241e7b985dfb187b992e173d0a5dc3700"
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
.......... [100%]
|
||||
10 passed, 32 subtests passed in 5.33s
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
F....F.... [100%]
|
||||
=================================== FAILURES ===================================
|
||||
_ Admit13TypeTests.test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor _
|
||||
|
||||
self = <test_admit_1_3_type.Admit13TypeTests testMethod=test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor>
|
||||
|
||||
def test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor(self):
|
||||
# A committed-map mismatch cannot exercise the independent mint guard.
|
||||
mint = replace(self.descriptors['mint-secret'], actor='agent')
|
||||
typ = yaml.safe_load(with_status('acknowledged'))
|
||||
typ['spec']['execution_descriptors']['mint-secret'] = digest(c.encode(mint))
|
||||
> with pinned_13(), self.assertRaisesRegex(Refused, 'agent mint is forbidden'):
|
||||
E AssertionError: Refused not raised
|
||||
|
||||
test_admit_1_3_type.py:150: AssertionError
|
||||
___ Admit13TypeTests.test_human_mint_is_refused_with_agent_runtime_identity ____
|
||||
|
||||
self = <test_admit_1_3_type.Admit13TypeTests testMethod=test_human_mint_is_refused_with_agent_runtime_identity>
|
||||
|
||||
def test_human_mint_is_refused_with_agent_runtime_identity(self):
|
||||
mint = self.descriptors['mint-secret']
|
||||
self.assertEqual(mint.actor, 'human')
|
||||
> with pinned_13(), self.assertRaisesRegex(Refused, 'agent mint is forbidden'):
|
||||
E AssertionError: Refused not raised
|
||||
|
||||
test_admit_1_3_type.py:156: AssertionError
|
||||
=========================== short test summary info ============================
|
||||
FAILED test_admit_1_3_type.py::Admit13TypeTests::test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor
|
||||
FAILED test_admit_1_3_type.py::Admit13TypeTests::test_human_mint_is_refused_with_agent_runtime_identity
|
||||
2 failed, 8 passed, 32 subtests passed in 5.72s
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
--- a/sso-mfa/k8s/keycape/test_admit_1_3_type.py
|
||||
+++ b/sso-mfa/k8s/keycape/test_admit_1_3_type.py
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Kernel admission against the real 1.3 successor type (NK-WP-0048-T16 candidate).
|
||||
|
||||
-Unlike test_admit_1_2_adapter_descriptors.py, nothing is patched into the type's
|
||||
-execution_descriptors: the map tested here is the one committed in
|
||||
-security-case-type-revisions/1.3. The type is `proposed`, so the kernel must refuse
|
||||
-it as it stands. Warden's BrowserInputs also pins each type version's parameter
|
||||
+The committed-map tests use security-case-type-revisions/1.3 unchanged. One
|
||||
+separate fixture-map test acknowledges only an agent-actor mint descriptor digest
|
||||
+to reach the independent kernel mint prohibition after map validation. The type
|
||||
+is `proposed`, so the kernel must refuse it as it stands. Warden's BrowserInputs also pins each type version's parameter
|
||||
fingerprint and knows only 1.1 and 1.2 (ops-warden case_inputs.PARAMETER_PINS), so an
|
||||
acknowledged 1.3 is still refused until ops-warden adds a 1.3 pin. That is an
|
||||
ops-warden owner change, and it is not made here. The tests show that 1.3's
|
||||
@@ -74,7 +74,7 @@
|
||||
ARTIFACT['params']['consumer_owner_repo'],
|
||||
version='1.3'))
|
||||
|
||||
- def admit(self, desc, type_source, *, actor=None, lane=None):
|
||||
+ def admit(self, desc, type_source, *, actor=None, lane=None, agent_id=None):
|
||||
desc_bytes = c.encode(desc)
|
||||
adapter = Adapter(desc.adapter, desc.adapter_source, unused, unused)
|
||||
executor = Executor(Journal(Path(self.temp.name) / desc.step),
|
||||
@@ -82,7 +82,10 @@
|
||||
lambda purpose, source, evidence: evidence in EVIDENCE,
|
||||
browser_inputs=inputs.BrowserInputs(REPO))
|
||||
with mock.patch.dict(os.environ):
|
||||
- os.environ.pop('WARDEN_AGENT_ID', None)
|
||||
+ if agent_id is None:
|
||||
+ os.environ.pop('WARDEN_AGENT_ID', None)
|
||||
+ else:
|
||||
+ os.environ['WARDEN_AGENT_ID'] = agent_id
|
||||
return executor.admit(self.case_source, type_source, desc_bytes,
|
||||
b'fixture-only-catalog', principal='founder', actor=actor or desc.actor,
|
||||
lane=lane or desc.lane,
|
||||
@@ -125,7 +128,8 @@
|
||||
def test_descriptor_outside_the_map_is_refused(self):
|
||||
acknowledged = with_status('acknowledged')
|
||||
mint = self.descriptors['mint-secret']
|
||||
- for changed in (replace(mint, lane='other-lane'), replace(mint, retry_safe=True)):
|
||||
+ for changed in (replace(mint, lane='other-lane'), replace(mint, retry_safe=True),
|
||||
+ replace(mint, actor='agent')):
|
||||
with self.subTest(changed=changed), pinned_13(), self.assertRaisesRegex(
|
||||
Refused, 'no exact acknowledged machine descriptor'):
|
||||
self.admit(changed, acknowledged)
|
||||
@@ -137,6 +141,20 @@
|
||||
self.assertEqual((mint.effect, mint.actor), ('mint', 'human'))
|
||||
with pinned_13(), self.assertRaisesRegex(Refused, 'principal actor or lane mismatch'):
|
||||
self.admit(mint, acknowledged, actor='agent')
|
||||
+
|
||||
+ def test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor(self):
|
||||
+ # A committed-map mismatch cannot exercise the independent mint guard.
|
||||
+ mint = replace(self.descriptors['mint-secret'], actor='agent')
|
||||
+ typ = yaml.safe_load(with_status('acknowledged'))
|
||||
+ typ['spec']['execution_descriptors']['mint-secret'] = digest(c.encode(mint))
|
||||
+ with pinned_13(), self.assertRaisesRegex(Refused, 'agent mint is forbidden'):
|
||||
+ self.admit(mint, yaml.safe_dump(typ).encode())
|
||||
+
|
||||
+ def test_human_mint_is_refused_with_agent_runtime_identity(self):
|
||||
+ mint = self.descriptors['mint-secret']
|
||||
+ self.assertEqual(mint.actor, 'human')
|
||||
+ with pinned_13(), self.assertRaisesRegex(Refused, 'agent mint is forbidden'):
|
||||
+ self.admit(mint, with_status('acknowledged'), agent_id='fixture-agent')
|
||||
|
||||
def test_wrong_lane_is_refused(self):
|
||||
# Moved from test_admit_1_2_adapter_descriptors.py in r5.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
[
|
||||
{
|
||||
"name": "preimage-frozen",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-frozen/src",
|
||||
"test_sha256": "f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5",
|
||||
"kernel_case_run_sha256": "77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0
|
||||
},
|
||||
{
|
||||
"name": "preimage-mutant",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-admission-guard-removed/src",
|
||||
"test_sha256": "f4f6a35cfbada462c939fe357e51fa5d03efc9d20c17a511f224c1359ca0faa5",
|
||||
"kernel_case_run_sha256": "6b0d61fb0da4737f305bb959524540752fbcf0f2993a7969bd7e368bdd03ea5a",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0
|
||||
},
|
||||
{
|
||||
"name": "successor-frozen",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-frozen/src",
|
||||
"test_sha256": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"kernel_case_run_sha256": "77e8117a1fb4e039815b555797778f7180f11e30f64cebe51b350b3eb7e46768",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0
|
||||
},
|
||||
{
|
||||
"name": "successor-mutant",
|
||||
"command": [
|
||||
"/home/worsch/ops-warden/.venv/bin/python",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-p",
|
||||
"no:cacheprovider",
|
||||
"test_admit_1_3_type.py"
|
||||
],
|
||||
"cwd": "/tmp/nk48-r5-f1-ahudwokt/net-kingdom/sso-mfa/k8s/keycape",
|
||||
"pythonpath": "/tmp/nk48-r5-f1-ahudwokt/warden-admission-guard-removed/src",
|
||||
"test_sha256": "c512ba00513d066fd1ca8af08fd6bf106fbd7a132981b1dcc5af80a745ab03f0",
|
||||
"kernel_case_run_sha256": "6b0d61fb0da4737f305bb959524540752fbcf0f2993a7969bd7e368bdd03ea5a",
|
||||
"exit_code": 1,
|
||||
"expected_exit_code": 1
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
"""Kernel admission against the real 1.3 successor type (NK-WP-0048-T16 candidate).
|
||||
|
||||
The committed-map tests use security-case-type-revisions/1.3 unchanged. One
|
||||
separate fixture-map test acknowledges only an agent-actor mint descriptor digest
|
||||
to reach the independent kernel mint prohibition after map validation. The type
|
||||
is `proposed`, so the kernel must refuse it as it stands. Warden's BrowserInputs also pins each type version's parameter
|
||||
fingerprint and knows only 1.1 and 1.2 (ops-warden case_inputs.PARAMETER_PINS), so an
|
||||
acknowledged 1.3 is still refused until ops-warden adds a 1.3 pin. That is an
|
||||
ops-warden owner change, and it is not made here. The tests show that 1.3's
|
||||
parameters carry 1.2's pinned fingerprint, and that with only that one pin added in
|
||||
memory the committed map admits exactly the composed descriptors and nothing else.
|
||||
Trust is fixture-only, nothing is dispatched, and none of this is owner admission
|
||||
or an acknowledgment.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import yaml
|
||||
|
||||
from warden.case_run import Adapter, Executor, Journal, digest
|
||||
from warden.case_receipt_state import Refused
|
||||
import warden.case_inputs as inputs
|
||||
|
||||
ROOT = Path(__file__).parent
|
||||
REPO = ROOT.parents[2]
|
||||
spec = importlib.util.spec_from_file_location('compose_13_admit', ROOT / 'compose-1.2-descriptors.py')
|
||||
c = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(c)
|
||||
|
||||
TYPE_DIR = REPO / 'security-case-type-revisions/1.3/oidc-browser-relying-party'
|
||||
TYPE_BYTES = (TYPE_DIR / 'type.yaml').read_bytes()
|
||||
ARTIFACT = json.loads((ROOT / 'descriptor-composition-1.3.candidate.json').read_text())
|
||||
EVIDENCE = {'fixture-acceptance', 'fixture-materialization', 'fixture-descriptor', 'fixture-session'}
|
||||
|
||||
|
||||
def unused(*_):
|
||||
raise AssertionError('admission must not dispatch')
|
||||
|
||||
|
||||
def parameter_fingerprint(type_bytes):
|
||||
params = yaml.safe_load(type_bytes)['spec']['parameters']
|
||||
return hashlib.sha256(json.dumps(params, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
|
||||
|
||||
def pinned_13():
|
||||
"""Warden's parameter pins plus the one 1.3 entry ops-warden would have to add."""
|
||||
return mock.patch.dict(inputs.PARAMETER_PINS, {'1.3': parameter_fingerprint(TYPE_BYTES)})
|
||||
|
||||
|
||||
def with_status(status):
|
||||
"""The committed 1.3 type with only its status changed."""
|
||||
typ = yaml.safe_load(TYPE_BYTES)
|
||||
typ['spec']['status'] = status
|
||||
return yaml.safe_dump(typ).encode()
|
||||
|
||||
|
||||
class Admit13TypeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
case = yaml.safe_load((TYPE_DIR / 'examples/forgejo.yaml').read_bytes())
|
||||
case['spec'].update(status='materialized', acceptance={
|
||||
'at': '2026-10-17', 'record': 'fixture-only acceptance, not an owner record',
|
||||
'by': [{'name': 'fixture', 'role': 'owner'}, {'name': 'fixture', 'role': 'founder'}]})
|
||||
self.case_source = yaml.safe_dump(case).encode()
|
||||
self.descriptors = dict(c.adapter_descriptors(ARTIFACT['check_config_digest'],
|
||||
ARTIFACT['params']['consumer_owner_repo'],
|
||||
version='1.3'))
|
||||
|
||||
def admit(self, desc, type_source, *, actor=None, lane=None, agent_id=None):
|
||||
desc_bytes = c.encode(desc)
|
||||
adapter = Adapter(desc.adapter, desc.adapter_source, unused, unused)
|
||||
executor = Executor(Journal(Path(self.temp.name) / desc.step),
|
||||
{adapter.reference: adapter},
|
||||
lambda purpose, source, evidence: evidence in EVIDENCE,
|
||||
browser_inputs=inputs.BrowserInputs(REPO))
|
||||
with mock.patch.dict(os.environ):
|
||||
if agent_id is None:
|
||||
os.environ.pop('WARDEN_AGENT_ID', None)
|
||||
else:
|
||||
os.environ['WARDEN_AGENT_ID'] = agent_id
|
||||
return executor.admit(self.case_source, type_source, desc_bytes,
|
||||
b'fixture-only-catalog', principal='founder', actor=actor or desc.actor,
|
||||
lane=lane or desc.lane,
|
||||
acceptance_evidence='fixture-acceptance',
|
||||
materialization_evidence='fixture-materialization',
|
||||
descriptor_evidence='fixture-descriptor', session_evidence='fixture-session')
|
||||
|
||||
def test_committed_map_is_exactly_the_composed_set(self):
|
||||
committed = yaml.safe_load(TYPE_BYTES)['spec']['execution_descriptors']
|
||||
composed = {s: digest(c.encode(d)) for s, d in self.descriptors.items()}
|
||||
self.assertEqual(len(composed), 7)
|
||||
self.assertEqual(committed, composed)
|
||||
self.assertEqual(committed, ARTIFACT['execution_descriptors'])
|
||||
|
||||
def test_proposed_type_is_refused_as_it_stands(self):
|
||||
self.assertEqual(yaml.safe_load(TYPE_BYTES)['spec']['status'], 'proposed')
|
||||
for step, desc in self.descriptors.items():
|
||||
with self.subTest(step=step), self.assertRaisesRegex(Refused, 'type is not acknowledged'):
|
||||
self.admit(desc, TYPE_BYTES)
|
||||
|
||||
def test_warden_has_no_1_3_parameter_pin_yet(self):
|
||||
self.assertNotIn('1.3', inputs.PARAMETER_PINS)
|
||||
# 1.3 keeps 1.2's parameters, so the missing pin is 1.2's fingerprint.
|
||||
self.assertEqual(parameter_fingerprint(TYPE_BYTES), inputs.PARAMETER_PINS['1.2'])
|
||||
acknowledged = with_status('acknowledged')
|
||||
for step, desc in self.descriptors.items():
|
||||
with self.subTest(step=step), self.assertRaisesRegex(
|
||||
Refused, 'failed exact-version validation'):
|
||||
self.admit(desc, acknowledged)
|
||||
|
||||
def test_committed_map_admits_every_descriptor_once_acknowledged_and_pinned(self):
|
||||
acknowledged = with_status('acknowledged')
|
||||
for step, desc in self.descriptors.items():
|
||||
with self.subTest(step=step), pinned_13():
|
||||
admitted, invocation, _ = self.admit(desc, acknowledged)
|
||||
self.assertEqual(admitted, desc)
|
||||
self.assertEqual((invocation.step, invocation.actor, invocation.target),
|
||||
(desc.step, desc.actor, 'forgejo'))
|
||||
|
||||
def test_descriptor_outside_the_map_is_refused(self):
|
||||
acknowledged = with_status('acknowledged')
|
||||
mint = self.descriptors['mint-secret']
|
||||
for changed in (replace(mint, lane='other-lane'), replace(mint, retry_safe=True),
|
||||
replace(mint, actor='agent')):
|
||||
with self.subTest(changed=changed), pinned_13(), self.assertRaisesRegex(
|
||||
Refused, 'no exact acknowledged machine descriptor'):
|
||||
self.admit(changed, acknowledged)
|
||||
|
||||
def test_mint_secret_admits_only_a_human_actor(self):
|
||||
# Moved from test_admit_1_2_adapter_descriptors.py (NK48-R1-F1) in r5.
|
||||
acknowledged = with_status('acknowledged')
|
||||
mint = self.descriptors['mint-secret']
|
||||
self.assertEqual((mint.effect, mint.actor), ('mint', 'human'))
|
||||
with pinned_13(), self.assertRaisesRegex(Refused, 'principal actor or lane mismatch'):
|
||||
self.admit(mint, acknowledged, actor='agent')
|
||||
|
||||
def test_agent_mint_is_refused_after_fixture_map_acknowledges_descriptor(self):
|
||||
# A committed-map mismatch cannot exercise the independent mint guard.
|
||||
mint = replace(self.descriptors['mint-secret'], actor='agent')
|
||||
typ = yaml.safe_load(with_status('acknowledged'))
|
||||
typ['spec']['execution_descriptors']['mint-secret'] = digest(c.encode(mint))
|
||||
with pinned_13(), self.assertRaisesRegex(Refused, 'agent mint is forbidden'):
|
||||
self.admit(mint, yaml.safe_dump(typ).encode())
|
||||
|
||||
def test_human_mint_is_refused_with_agent_runtime_identity(self):
|
||||
mint = self.descriptors['mint-secret']
|
||||
self.assertEqual(mint.actor, 'human')
|
||||
with pinned_13(), self.assertRaisesRegex(Refused, 'agent mint is forbidden'):
|
||||
self.admit(mint, with_status('acknowledged'), agent_id='fixture-agent')
|
||||
|
||||
def test_wrong_lane_is_refused(self):
|
||||
# Moved from test_admit_1_2_adapter_descriptors.py in r5.
|
||||
acknowledged = with_status('acknowledged')
|
||||
for step, desc in self.descriptors.items():
|
||||
with self.subTest(step=step), pinned_13(), self.assertRaisesRegex(
|
||||
Refused, 'principal actor or lane mismatch'):
|
||||
self.admit(desc, acknowledged, lane='wrong-lane')
|
||||
|
||||
def test_earlier_versions_carry_no_execution_map(self):
|
||||
for version in ('1.2',):
|
||||
path = REPO / f'security-case-type-revisions/{version}/oidc-browser-relying-party/type.yaml'
|
||||
with self.subTest(version=version):
|
||||
self.assertNotIn('execution_descriptors', yaml.safe_load(path.read_bytes())['spec'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue