Execute task-plan catalogue validation from verified source bytes
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13690-09e0-7cd1-964b-ec83cdbcd348
This commit is contained in:
parent
7c79293831
commit
b98d17c09f
5 changed files with 139 additions and 11 deletions
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"schema": "platform-source-correction/v1",
|
||||
"observed_at": "2026-10-13T18:53:24.250702+00:00",
|
||||
"task": "RPF-WP-0035-T12",
|
||||
"source_sha256": {
|
||||
"scripts/task_execution_plan.py": "f81bd36523c42ce880111f774409cb7c554c6ffc06c2686a1864e6e6cd70111a",
|
||||
"tests/test_task_execution_plan.py": "14c587c05a612955f6fc972ad3967a051b36fb127aecc246a28168331a034468",
|
||||
"docs/task-execution-plan-compiler.md": "cf28c8e1ab702a66d5608f56566f184afd8d41b2693a38034e001de46f023b94"
|
||||
},
|
||||
"initial_regression": "cached catalogue validator replacement caused proposal refusal despite unchanged pinned source; regression failed before correction",
|
||||
"test_command": "PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/home/worsch/secrets-engine/src python3 -m pytest -q tests/test_task_execution_plan.py tests/test_task_execution_journal.py",
|
||||
"compiler_tests_passed": 45,
|
||||
"journal_tests_passed": 15,
|
||||
"native_execution_authorized": false,
|
||||
"independent_integration_review": "pending"
|
||||
}
|
||||
|
|
@ -25,7 +25,13 @@ No other lane module, mount enable/config, KV read/write, token/SecretID deliver
|
|||
AppRole mutation, caller HCL/JSON body, paths or command argv can be compiled.
|
||||
|
||||
The catalogue comes from a separately owner-pinned local Secrets catalogue file,
|
||||
loaded by the actual installed/pinned `secrets_engine.catalog.validate_entry`.
|
||||
loaded by `secrets_engine.catalog.validate_entry` executed directly from the
|
||||
verified source snapshot, together with its pinned package/errors/redactor.
|
||||
Each compilation uses temporary private module names and removes them on success
|
||||
or failure. Canonical application imports remain untouched. Cached modules and
|
||||
timestamp-based bytecode cannot substitute different code for the verified bytes;
|
||||
new Secrets Engine imports outside the closed source set refuse. This is source
|
||||
consistency within the admitted Python runtime, not a Python security sandbox.
|
||||
Its catalogue id, existing mount, logical path, fields, production stage and
|
||||
approval CCR reference must match the owner CCR. Engine-managed mounts and auth
|
||||
are refused: existing delivery-auth metadata can be carried, but is never applied
|
||||
|
|
@ -91,8 +97,14 @@ Token custody/revoke, temporary object removal, owner-private recovery after unk
|
|||
and truthful native readback remain gates. General lifecycle/apply/exec/default
|
||||
migration and SecretsT06/RPF35T02/T12 cannot close from this source component.
|
||||
|
||||
Validation:40 offline tests pass, covering real owner K8s/OIDC body generation,
|
||||
Validation:45 compiler tests and 15 journal tests pass, covering real owner K8s/OIDC body generation,
|
||||
exact catalogue unicode digest interoperability, refusal of alternate action/kind/
|
||||
path/body/ordinal, re-pinned root policy, nonapproved/unconfirmed/replayed CCR,
|
||||
engine-managed catalogue, forged immutable plan, source drift/duplicates and
|
||||
symlink/hardlink/writable source. No native action, credential/Secret access or owner checkout writes.
|
||||
|
||||
October 13 correction: a regression first demonstrated that a cached validator
|
||||
could replace the hashed source. Additional cases cover cached dependencies,
|
||||
invalid catalogue acceptance through a substituted validator, and private-module
|
||||
cleanup on both success and refusal. The October 11 v2 review remains evidence
|
||||
for its original bytes only; it does not attest this corrected revision.
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ Six fixed calls on two structure objects. Recompile immediately before each call
|
|||
Canonical PDP request digest is opaque: the authenticated mediator verifies it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import builtins
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import types
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
from uuid import UUID, uuid4
|
||||
import yaml
|
||||
|
||||
class Refused(Exception): pass
|
||||
|
|
@ -123,6 +125,49 @@ class Compiler:
|
|||
finally:
|
||||
if fd is not None: os.close(fd)
|
||||
|
||||
def _catalogue_entry(self, snapshot):
|
||||
# Execute the verified bytes, not cached imports or timestamp-valid .pyc
|
||||
# files. Private names support dataclass annotation lookup without replacing
|
||||
# the application's secrets_engine modules, even during concurrent compiles.
|
||||
prefix = '_platform_catalogue_' + uuid4().hex
|
||||
modules = {}
|
||||
registered = []
|
||||
|
||||
def snapshot_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
if name == 'secrets_engine' or name.startswith('secrets_engine.'):
|
||||
need(level == 0 and name in modules, 'unpinned_catalogue_import')
|
||||
module = modules[name]
|
||||
if fromlist:
|
||||
need(all(hasattr(module, item) for item in fromlist), 'unpinned_catalogue_import')
|
||||
return module
|
||||
return modules['secrets_engine']
|
||||
need(level == 0, 'unpinned_catalogue_import')
|
||||
return builtins.__import__(name, globals, locals, fromlist, level)
|
||||
|
||||
try:
|
||||
sources = [('secrets_engine', self.dependencies[0]),
|
||||
('secrets_engine.errors', self.dependencies[1]),
|
||||
('secrets_engine.redact', self.dependencies[2]),
|
||||
('secrets_engine.catalog', self.catalog_code)]
|
||||
for name, path in sources:
|
||||
private_name = prefix + name[len('secrets_engine'):]
|
||||
module = types.ModuleType(private_name)
|
||||
module.__file__ = str(path)
|
||||
module.__package__ = prefix
|
||||
module.__dict__['__builtins__'] = dict(vars(builtins), __import__=snapshot_import)
|
||||
modules[name] = module
|
||||
sys.modules[private_name] = module
|
||||
registered.append(private_name)
|
||||
if name != 'secrets_engine':
|
||||
setattr(modules['secrets_engine'], name.rsplit('.', 1)[1], module)
|
||||
exec(compile(snapshot[str(path)], str(path), 'exec'), module.__dict__)
|
||||
return modules['secrets_engine.catalog'].validate_entry(
|
||||
yaml.load(snapshot[str(self.catalog)], Loader=UniqueLoader),
|
||||
source='<owner-pinned-catalogue>')
|
||||
finally:
|
||||
for name in registered:
|
||||
sys.modules.pop(name, None)
|
||||
|
||||
def _derive(self):
|
||||
# No native operations, CLI applier, subprocess, network or ambient credentials.
|
||||
snapshot={p:self.read(Path(p)) for p in self.pins}
|
||||
|
|
@ -147,13 +192,7 @@ class Compiler:
|
|||
generated=owner.generated_policy_hcl(ccr)
|
||||
need(owner.normalized_policy_body(snapshot[str(policy)].decode())==owner.normalized_policy_body(generated),'owner_policy_not_generated')
|
||||
# Use the actual consumer catalog validator/defaults, not an alternative broker catalog.
|
||||
import secrets_engine.catalog as catalogue
|
||||
import secrets_engine.errors as errors_module
|
||||
import secrets_engine.redact as redact_module
|
||||
import secrets_engine
|
||||
for module,p in [(catalogue,self.catalog_code),(secrets_engine,self.dependencies[0]),(errors_module,self.dependencies[1]),(redact_module,self.dependencies[2])]:
|
||||
need(Path(module.__file__).resolve()==p,'catalogue_runtime_source_mismatch')
|
||||
entry=catalogue.validate_entry(yaml.load(snapshot[str(self.catalog)],Loader=UniqueLoader),source='<owner-pinned-catalogue>')
|
||||
entry=self._catalogue_entry(snapshot)
|
||||
need(entry.id==ccr['access_frontdoor']['catalog_id'] and entry.stage=='prod'
|
||||
and ccr['target']['environment']=='production' and entry.kind=='kv'
|
||||
and entry.mount_management=='existing'
|
||||
|
|
|
|||
|
|
@ -144,3 +144,52 @@ def test_oidc_role_body_reuses_owner_auth_payload_and_existing_mount(compiler):
|
|||
assert call.path=='auth/netkingdom/role/synthetic-operator-workload-kv-read'
|
||||
assert role['role_type']=='oidc' and role['policies']=='workload-kv-read-issue-core-runtime'
|
||||
assert role['bound_claims']==source['openbao']['auth']['bound_claims']
|
||||
|
||||
|
||||
def test_cached_catalogue_code_cannot_replace_verified_source(compiler, monkeypatch):
|
||||
import secrets_engine.catalog as catalogue
|
||||
expected = compiler.proposal()
|
||||
|
||||
def stale_validator(*args, **kwargs):
|
||||
raise AssertionError('cached code differs from the pinned source')
|
||||
|
||||
monkeypatch.setattr(catalogue, 'validate_entry', stale_validator)
|
||||
assert compiler.proposal() == expected
|
||||
|
||||
|
||||
def test_cached_catalogue_dependency_cannot_replace_verified_source(compiler, monkeypatch):
|
||||
import secrets_engine.catalog as catalogue
|
||||
expected = compiler.proposal()
|
||||
|
||||
def stale_redactor(*args, **kwargs):
|
||||
raise AssertionError('cached dependency differs from the pinned source')
|
||||
|
||||
monkeypatch.setattr(catalogue, 'looks_secret', stale_redactor)
|
||||
assert compiler.proposal() == expected
|
||||
|
||||
|
||||
def test_cached_validator_cannot_admit_invalid_pinned_catalogue(compiler, monkeypatch):
|
||||
import secrets_engine.catalog as catalogue
|
||||
accepted = catalogue.validate_entry(yaml.safe_load(compiler.catalog.read_text()), source='fixture')
|
||||
data = yaml.safe_load(compiler.catalog.read_text())
|
||||
del data['rotation']
|
||||
compiler.catalog.write_text(yaml.safe_dump(data))
|
||||
compiler = repin(compiler, compiler.catalog)
|
||||
monkeypatch.setattr(catalogue, 'validate_entry', lambda *a, **kw: accepted)
|
||||
with pytest.raises(m.Refused):
|
||||
compiler.proposal()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid', [False, True])
|
||||
def test_snapshot_modules_removed_without_replacing_ambient_modules(compiler, invalid):
|
||||
import secrets_engine.catalog
|
||||
before = {k: v for k, v in sys.modules.items() if k.startswith('secrets_engine')}
|
||||
if invalid:
|
||||
compiler.catalog.write_text('id: incomplete\n')
|
||||
compiler = repin(compiler, compiler.catalog)
|
||||
with pytest.raises(m.Refused):
|
||||
compiler.proposal()
|
||||
else:
|
||||
compiler.proposal()
|
||||
assert {k: v for k, v in sys.modules.items() if k.startswith('secrets_engine')} == before
|
||||
assert not any(k.startswith('_platform_catalogue_') for k in sys.modules)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ status: blocked
|
|||
flavor: implementation
|
||||
owner: codex
|
||||
created: "2026-09-05"
|
||||
updated: "2026-10-11"
|
||||
updated: "2026-10-13"
|
||||
related:
|
||||
- RPF-WP-0032
|
||||
- RPF-WP-0033
|
||||
|
|
@ -1213,3 +1213,15 @@ owner-private unknown recovery remain unimplemented integration/native gates.
|
|||
Actual CCR42 has no eligible approved pilot/catalogue; no source metadata is
|
||||
invented. T02 and original negative/capability/native evidence remain unchanged;
|
||||
T11/T13 history retained, no defaultflip/nativego or SecretsT06 closure follows.
|
||||
|
||||
|
||||
2026-10-13 T12 source correction: the compiler hashed the catalogue sources but
|
||||
used cached Python modules with matching paths, which did not establish executing
|
||||
those exact bytes. A failing regression reproduced the substitution. Catalogue,
|
||||
package and dependencies now execute from the verified snapshot in temporary
|
||||
private modules; ambient modules remain untouched and cleanup runs on refusal.
|
||||
45 compiler plus 15 journal tests pass. The v2 source review remains historical;
|
||||
this change needs its own integration review. T12 retains all existing full
|
||||
contract, identity/consume, dispatch, custody and native acceptance requirements.
|
||||
RPF-IN-0012 remains open for the independent contract/wrapped-delivery return;
|
||||
this code correction does not authorize either operation.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue