426 lines
18 KiB
Diff
426 lines
18 KiB
Diff
|
|
diff --git a/docs/security-case-execution-kernel-v1.md b/docs/security-case-execution-kernel-v1.md
|
||
|
|
index bb9d775..35289d7 100644
|
||
|
|
--- a/docs/security-case-execution-kernel-v1.md
|
||
|
|
+++ b/docs/security-case-execution-kernel-v1.md
|
||
|
|
@@ -84,3 +84,44 @@ lose a response then reconcile without mint, require explicit retry, check exact
|
||
|
|
pins/session expiry, withhold malicious output and unconfirmed revocation, and
|
||
|
|
prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and
|
||
|
|
checks pass. No live token or target action occurred.
|
||
|
|
+
|
||
|
|
+## Browser input projection — 2026-10-16 source preparation
|
||
|
|
+
|
||
|
|
+An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser
|
||
|
|
+cases before admission. It calls the real NetKingdom type/case validator over
|
||
|
|
+verified producer bytes in a disposable Python process (`-I`, empty environment,
|
||
|
|
+timeout, no inherited output). The validator, composer, capability contract and lazy evidence
|
||
|
|
+modules have exact source pins; missing or changed code refuses. There is no
|
||
|
|
+automatic discovery or download. A producer upgrade needs reviewed pin changes.
|
||
|
|
+
|
||
|
|
+Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest
|
||
|
|
+of each complete parameter definition list prevents reclassifying secrets as
|
||
|
|
+metadata, loosening constraints, or adding fields under a recognized version.
|
||
|
|
+The complete case passes producer validation, including required fields,
|
||
|
|
+constraints and credential-shape checks. Explicit fixed/default overrides,
|
||
|
|
+unknown fields and secret references refuse. Security-sensitive browser metadata
|
||
|
|
+can therefore be present without being passed to an owner adapter: the only
|
||
|
|
+projection is its descriptor's exact public/operational target identifier.
|
||
|
|
+No raw parameter values are added to invocations or persisted receipts.
|
||
|
|
+
|
||
|
|
+`require_implemented=False` validates the acknowledged type's source, not its
|
||
|
|
+execution authority. Existing descriptor/type/source/catalog/caller provenance
|
||
|
|
+gates stay mandatory. Neither historical browser type contains the required
|
||
|
|
+exact machine-descriptor map, and neither is promoted by this change. There are
|
||
|
|
+still no installed native owner adapters, authenticated provenance verifier,
|
||
|
|
+contained session/ordered run orchestration or live CLI dispatch. A journal digest
|
||
|
|
+is a fingerprint and cannot authenticate native evidence. Completion still needs
|
||
|
|
+owner-admitted descriptors and actual structural, mint/delivery, consumer and
|
||
|
|
+revocation/rollback evidence. Local fixture success is not native acceptance.
|
||
|
|
+
|
||
|
|
+Tests use the actual producer checkout (override sibling location with
|
||
|
|
+`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected
|
||
|
|
+test-only owner adapter/evidence verifier. Their descriptor extensions and case
|
||
|
|
+materialized labels exist only in memory. Canonical case/type bytes stay intact.
|
||
|
|
+
|
||
|
|
+Producer integration refinement: the accepted NetKingdom browser-case patch
|
||
|
|
+`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05`
|
||
|
|
+is now the tested producer revision. Its evidence module is imported lazily only
|
||
|
|
+by `check-evidence`, outside this admission path; Warden nevertheless pins and
|
||
|
|
+copies it with the complete reviewed producer set. Missing or modified bytes in
|
||
|
|
+any of the four modules refuse admission. The test covers every dependency.
|
||
|
|
diff --git a/src/warden/case_inputs.py b/src/warden/case_inputs.py
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000..baa00b2
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/src/warden/case_inputs.py
|
||
|
|
@@ -0,0 +1,96 @@
|
||
|
|
+"""Reviewed browser metadata projection; no native trust or command dispatch.
|
||
|
|
+
|
||
|
|
+The producer validator and all reviewed producer modules are pinned below.
|
||
|
|
+Their verified bytes run in a disposable tree, without caller environment or
|
||
|
|
+ambient Python imports. Type parameters must match the exact reviewed version.
|
||
|
|
+Machine descriptor extensions still require Executor's independent admission.
|
||
|
|
+"""
|
||
|
|
+
|
||
|
|
+import hashlib
|
||
|
|
+import json
|
||
|
|
+from pathlib import Path
|
||
|
|
+import subprocess
|
||
|
|
+import sys
|
||
|
|
+import tempfile
|
||
|
|
+
|
||
|
|
+from .case_receipt_state import Refused
|
||
|
|
+
|
||
|
|
+SOURCE_PINS = {
|
||
|
|
+ "tools/security-case-types/security_case_evidence.py": "3c5d021ca7f1fc41fba4b2ff42fcb036b916a2a09ffc5e147cc3db1874dd2401",
|
||
|
|
+ "tools/security-case-types/security_case_validator.py": "cbb9ab532d328f78cf37aca57891400e2e65d63a8ab3c5fe7bd1a4ff21dee56f",
|
||
|
|
+ "tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f",
|
||
|
|
+ "tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d",
|
||
|
|
+}
|
||
|
|
+PARAMETER_PINS = {
|
||
|
|
+ "1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c",
|
||
|
|
+ "1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6",
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+_VALIDATE = """
|
||
|
|
+import json, sys
|
||
|
|
+from pathlib import Path
|
||
|
|
+sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types'))
|
||
|
|
+import security_case_validator as v
|
||
|
|
+case, typ = json.load(sys.stdin)
|
||
|
|
+issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False)
|
||
|
|
+print('valid' if not v.has_errors(issues) else 'invalid')
|
||
|
|
+"""
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+class BrowserInputs:
|
||
|
|
+ """Explicit owner configuration, never inferred from a case-supplied path."""
|
||
|
|
+
|
||
|
|
+ def __init__(self, netkingdom_root: Path):
|
||
|
|
+ self.root = Path(netkingdom_root)
|
||
|
|
+
|
||
|
|
+ def target(self, case: dict, typ: dict, target_parameter: str) -> str:
|
||
|
|
+ try:
|
||
|
|
+ spec = typ["spec"]
|
||
|
|
+ parameters = spec["parameters"]
|
||
|
|
+ fingerprint = hashlib.sha256(
|
||
|
|
+ json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode()
|
||
|
|
+ ).hexdigest()
|
||
|
|
+ if (
|
||
|
|
+ spec["id"] != "oidc-browser-relying-party"
|
||
|
|
+ or PARAMETER_PINS.get(spec["version"]) != fingerprint
|
||
|
|
+ ):
|
||
|
|
+ raise ValueError
|
||
|
|
+ declared = {item["name"]: item for item in parameters}
|
||
|
|
+ definition = declared.get(target_parameter, {})
|
||
|
|
+ if (
|
||
|
|
+ definition.get("sensitivity") not in ("public", "operational")
|
||
|
|
+ or definition.get("case_settable") is not True
|
||
|
|
+ ):
|
||
|
|
+ raise ValueError
|
||
|
|
+ # No secret references or newly classified metadata are grandfathered.
|
||
|
|
+ for name in case["spec"]["parameters"]:
|
||
|
|
+ item = declared.get(name, {})
|
||
|
|
+ if item.get("case_settable") is not True or item.get(
|
||
|
|
+ "sensitivity"
|
||
|
|
+ ) not in ("public", "operational", "security_sensitive"):
|
||
|
|
+ raise ValueError
|
||
|
|
+ with tempfile.TemporaryDirectory(
|
||
|
|
+ prefix="warden-case-validator-"
|
||
|
|
+ ) as directory:
|
||
|
|
+ for relative, expected in SOURCE_PINS.items():
|
||
|
|
+ source = (self.root / relative).read_bytes()
|
||
|
|
+ if hashlib.sha256(source).hexdigest() != expected:
|
||
|
|
+ raise ValueError
|
||
|
|
+ dest = Path(directory) / relative
|
||
|
|
+ dest.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
+ dest.write_bytes(source)
|
||
|
|
+ result = subprocess.run(
|
||
|
|
+ [sys.executable, "-I", "-c", _VALIDATE, directory],
|
||
|
|
+ input=json.dumps([case, typ]),
|
||
|
|
+ text=True,
|
||
|
|
+ stdout=subprocess.PIPE,
|
||
|
|
+ stderr=subprocess.DEVNULL,
|
||
|
|
+ env={},
|
||
|
|
+ timeout=10,
|
||
|
|
+ check=True,
|
||
|
|
+ )
|
||
|
|
+ if result.stdout != "valid\n":
|
||
|
|
+ raise ValueError
|
||
|
|
+ return case["spec"]["parameters"][target_parameter]
|
||
|
|
+ except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError):
|
||
|
|
+ raise Refused("browser metadata failed exact-version validation") from None
|
||
|
|
diff --git a/src/warden/case_run.py b/src/warden/case_run.py
|
||
|
|
index 7f1cce1..f16ab2a 100644
|
||
|
|
--- a/src/warden/case_run.py
|
||
|
|
+++ b/src/warden/case_run.py
|
||
|
|
@@ -20,6 +20,7 @@ import uuid
|
||
|
|
from typing import Callable
|
||
|
|
|
||
|
|
import yaml
|
||
|
|
+from .case_inputs import BrowserInputs
|
||
|
|
from .case_receipt_state import (
|
||
|
|
Outcome,
|
||
|
|
Pins,
|
||
|
|
@@ -329,6 +330,8 @@ class Executor:
|
||
|
|
journal: Journal,
|
||
|
|
adapters: dict[str, Adapter],
|
||
|
|
evidence_verifier: Callable[[str, str, str], bool],
|
||
|
|
+ *,
|
||
|
|
+ browser_inputs: BrowserInputs | None = None,
|
||
|
|
):
|
||
|
|
self.journal, self.adapters, self.evidence_verifier = (
|
||
|
|
journal,
|
||
|
|
@@ -336,6 +339,7 @@ class Executor:
|
||
|
|
evidence_verifier,
|
||
|
|
)
|
||
|
|
self._admitted = {}
|
||
|
|
+ self.browser_inputs = browser_inputs
|
||
|
|
|
||
|
|
def admit(
|
||
|
|
self,
|
||
|
|
@@ -421,15 +425,24 @@ class Executor:
|
||
|
|
or descriptor.target_parameter not in declared
|
||
|
|
):
|
||
|
|
raise Refused("target parameter is not declared by acknowledged type")
|
||
|
|
- for name in parameters:
|
||
|
|
- definition = declared.get(name)
|
||
|
|
- if (
|
||
|
|
- definition is None
|
||
|
|
- or definition.get("sensitivity") not in ("public", "operational")
|
||
|
|
- or definition.get("case_settable") is False
|
||
|
|
- ):
|
||
|
|
- raise Refused("unknown, fixed or secret-sensitivity case parameter")
|
||
|
|
- target = parameters.get(descriptor.target_parameter)
|
||
|
|
+ if case_type.get("id") == "oidc-browser-relying-party":
|
||
|
|
+ if type(self.browser_inputs) is not BrowserInputs:
|
||
|
|
+ raise Refused("browser metadata validator is not configured")
|
||
|
|
+ target = self.browser_inputs.target(
|
||
|
|
+ yaml.safe_load(case_source),
|
||
|
|
+ yaml.safe_load(type_source),
|
||
|
|
+ descriptor.target_parameter,
|
||
|
|
+ )
|
||
|
|
+ else:
|
||
|
|
+ for name in parameters:
|
||
|
|
+ definition = declared.get(name)
|
||
|
|
+ if (
|
||
|
|
+ definition is None
|
||
|
|
+ or definition.get("sensitivity") not in ("public", "operational")
|
||
|
|
+ or definition.get("case_settable") is False
|
||
|
|
+ ):
|
||
|
|
+ raise Refused("unknown, fixed or secret-sensitivity case parameter")
|
||
|
|
+ target = parameters.get(descriptor.target_parameter)
|
||
|
|
if not isinstance(target, str) or not IDENTIFIER.fullmatch(target):
|
||
|
|
raise Refused("target must be exact case metadata")
|
||
|
|
adapter = self.adapters.get(descriptor.adapter)
|
||
|
|
diff --git a/tests/test_case_inputs.py b/tests/test_case_inputs.py
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000..01e9407
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/tests/test_case_inputs.py
|
||
|
|
@@ -0,0 +1,185 @@
|
||
|
|
+"""Actual producer validation and Executor; fake owner evidence, no native claims."""
|
||
|
|
+
|
||
|
|
+import os
|
||
|
|
+from pathlib import Path
|
||
|
|
+
|
||
|
|
+import pytest
|
||
|
|
+import yaml
|
||
|
|
+
|
||
|
|
+from warden.case_inputs import BrowserInputs, SOURCE_PINS
|
||
|
|
+from warden.case_run import digest
|
||
|
|
+from warden.case_receipt_state import Outcome, Refused
|
||
|
|
+from tests.test_case_run import rig as base_rig
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@pytest.fixture
|
||
|
|
+def rig(tmp_path):
|
||
|
|
+ return base_rig.__wrapped__(tmp_path)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+NK = Path(
|
||
|
|
+ os.environ.get(
|
||
|
|
+ "WARDEN_TEST_NETKINGDOM_ROOT",
|
||
|
|
+ Path(__file__).resolve().parents[2] / "net-kingdom",
|
||
|
|
+ )
|
||
|
|
+)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def browser(rig, version="1.1", mutate=None):
|
||
|
|
+ executor, _, calls, sources, kwargs = rig
|
||
|
|
+ case = yaml.safe_load(
|
||
|
|
+ (NK / "security-cases/forgejo-netkingdom-oidc.yaml").read_bytes()
|
||
|
|
+ )
|
||
|
|
+ relative = (
|
||
|
|
+ "security-case-types"
|
||
|
|
+ if version == "1.1"
|
||
|
|
+ else "security-case-type-revisions/1.2"
|
||
|
|
+ )
|
||
|
|
+ typ = yaml.safe_load(
|
||
|
|
+ (NK / relative / "oidc-browser-relying-party/type.yaml").read_bytes()
|
||
|
|
+ )
|
||
|
|
+ case["spec"]["type"] = "oidc-browser-relying-party@" + version
|
||
|
|
+ case["spec"]["status"] = "materialized"
|
||
|
|
+ if version == "1.2":
|
||
|
|
+ case["spec"]["parameters"]["consumer_namespace"] = "forgejo"
|
||
|
|
+ # This extension and all native provenance are TEST ONLY, never source assent.
|
||
|
|
+ typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])}
|
||
|
|
+ if mutate:
|
||
|
|
+ mutate(case, typ)
|
||
|
|
+ changed = (
|
||
|
|
+ yaml.safe_dump(case).encode(),
|
||
|
|
+ yaml.safe_dump(typ).encode(),
|
||
|
|
+ *sources[2:],
|
||
|
|
+ )
|
||
|
|
+ old = executor.evidence_verifier
|
||
|
|
+
|
||
|
|
+ def verified(purpose, source, evidence):
|
||
|
|
+ return old(
|
||
|
|
+ purpose,
|
||
|
|
+ source.replace(digest(changed[0]), digest(sources[0])).replace(
|
||
|
|
+ digest(changed[1]), digest(sources[1])
|
||
|
|
+ ),
|
||
|
|
+ evidence,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ executor.evidence_verifier = verified
|
||
|
|
+ executor.browser_inputs = BrowserInputs(NK)
|
||
|
|
+ return executor, calls, changed, kwargs
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@pytest.mark.parametrize("version", ["1.1", "1.2"])
|
||
|
|
+def test_actual_browser_executor_and_owner_projection(rig, version):
|
||
|
|
+ executor, calls, sources, kwargs = browser(rig, version)
|
||
|
|
+ args = executor.admit(*sources, **kwargs)
|
||
|
|
+ assert args[1].target == "forgejo"
|
||
|
|
+ assert not calls
|
||
|
|
+ assert executor.run(*args).last.outcome is Outcome.SUCCESS
|
||
|
|
+ assert calls[0].target == "forgejo"
|
||
|
|
+ assert not hasattr(calls[0], "parameters")
|
||
|
|
+ # Owner callbacks/journal get target and fingerprints, no full metadata.
|
||
|
|
+ persisted = (executor.journal.directory / "journal.sqlite").read_bytes()
|
||
|
|
+ assert b"https://forgejo.coulomb.social" not in persisted
|
||
|
|
+ assert b"forge-admins" not in persisted
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@pytest.mark.parametrize(
|
||
|
|
+ "field,value",
|
||
|
|
+ [
|
||
|
|
+ ("redirect_uris", "https://*.example.org/callback"),
|
||
|
|
+ ("redirect_uris", "http://example.org/callback"),
|
||
|
|
+ ("allowed_scopes", "openid,admin"),
|
||
|
|
+ ("client_type", "arbitrary"),
|
||
|
|
+ ("mfa_required", False),
|
||
|
|
+ ("unknown", "value"),
|
||
|
|
+ ("issuer", "https://other.example"),
|
||
|
|
+ ("display_name", "hvs." + "A" * 30),
|
||
|
|
+ ],
|
||
|
|
+)
|
||
|
|
+def test_case_refusals_even_with_test_evidence(rig, field, value):
|
||
|
|
+ executor, calls, sources, kwargs = browser(
|
||
|
|
+ rig, mutate=lambda c, t: c["spec"]["parameters"].update({field: value})
|
||
|
|
+ )
|
||
|
|
+ with pytest.raises(Refused, match="exact-version"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@pytest.mark.parametrize(
|
||
|
|
+ "mutation",
|
||
|
|
+ [
|
||
|
|
+ lambda c, t: t["spec"].update(version="1.3"),
|
||
|
|
+ lambda c, t: t["spec"]["parameters"][2].update(sensitivity="public"),
|
||
|
|
+ lambda c, t: t["spec"]["parameters"][2].update(constraints={}),
|
||
|
|
+ lambda c, t: t["spec"]["parameters"].append(
|
||
|
|
+ {
|
||
|
|
+ "name": "secret",
|
||
|
|
+ "type": "string",
|
||
|
|
+ "case_settable": True,
|
||
|
|
+ "sensitivity": "secret_reference",
|
||
|
|
+ }
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+)
|
||
|
|
+def test_type_revision_cannot_widen_reviewed_parameters(rig, mutation):
|
||
|
|
+ def change(case, typ):
|
||
|
|
+ mutation(case, typ)
|
||
|
|
+ case["spec"]["type"] = typ["spec"]["id"] + "@" + typ["spec"]["version"]
|
||
|
|
+
|
||
|
|
+ executor, calls, sources, kwargs = browser(rig, mutate=change)
|
||
|
|
+ with pytest.raises(Refused, match="exact-version"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def test_namespace_and_missing_required_are_validated(rig):
|
||
|
|
+ executor, calls, sources, kwargs = browser(
|
||
|
|
+ rig, "1.2", lambda c, t: c["spec"]["parameters"].update(consumer_namespace="*")
|
||
|
|
+ )
|
||
|
|
+ with pytest.raises(Refused, match="exact-version"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@pytest.mark.parametrize("changed_dependency", list(SOURCE_PINS))
|
||
|
|
+def test_missing_producer_and_changed_transitive_source_refuse(
|
||
|
|
+ rig, tmp_path, changed_dependency
|
||
|
|
+):
|
||
|
|
+ executor, calls, sources, kwargs = browser(rig)
|
||
|
|
+ executor.browser_inputs = BrowserInputs(tmp_path)
|
||
|
|
+ for relative in SOURCE_PINS:
|
||
|
|
+ path = tmp_path / relative
|
||
|
|
+ path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
+ if relative != changed_dependency:
|
||
|
|
+ path.write_bytes((NK / relative).read_bytes())
|
||
|
|
+ with pytest.raises(Refused, match="exact-version"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ (tmp_path / changed_dependency).write_text('raise RuntimeError("must not execute")')
|
||
|
|
+ with pytest.raises(Refused, match="exact-version"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def test_browser_without_validator_still_refuses(rig):
|
||
|
|
+ executor, calls, sources, kwargs = browser(rig)
|
||
|
|
+ executor.browser_inputs = None
|
||
|
|
+ with pytest.raises(Refused, match="not configured"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def test_real_unextended_type_stays_plan_only(rig):
|
||
|
|
+ executor, calls, sources, kwargs = browser(rig)
|
||
|
|
+ typ = yaml.safe_load(sources[1])
|
||
|
|
+ del typ["spec"]["execution_descriptors"]
|
||
|
|
+ with pytest.raises(Refused, match="plan only"):
|
||
|
|
+ executor.admit(sources[0], yaml.safe_dump(typ).encode(), *sources[2:], **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def test_required_browser_parameter_is_not_defaulted_away(rig):
|
||
|
|
+ executor, calls, sources, kwargs = browser(
|
||
|
|
+ rig, mutate=lambda c, t: c["spec"]["parameters"].pop("redirect_uris")
|
||
|
|
+ )
|
||
|
|
+ with pytest.raises(Refused, match="exact-version"):
|
||
|
|
+ executor.admit(*sources, **kwargs)
|
||
|
|
+ assert not calls
|
||
|
|
diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md
|
||
|
|
index b9ef795..7d6799d 100644
|
||
|
|
--- a/workplans/WARDEN-WP-0044-security-case-follow-through.md
|
||
|
|
+++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md
|
||
|
|
@@ -251,3 +251,15 @@ and receipt provenance. No installed native adapter or live CLI execution is
|
||
|
|
claimed. Current prose types stay plan-only; acknowledged descriptor/type machine
|
||
|
|
seams and authenticated owner/caller receipts are still required. T03 progresses
|
||
|
|
for implementation but cannot finish before ordered native Forgejo acceptance.
|
||
|
|
+
|
||
|
|
+2026-10-16 source preparation (isolated checkout; no native acceptance):
|
||
|
|
+T03's browser parameter mismatch now has a producer-validator-backed projection
|
||
|
|
+in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2
|
||
|
|
+parameter definitions and all four reviewed producer modules are pinned; full case
|
||
|
|
+validation admits reviewed security-sensitive metadata while unknown/fixed/secret
|
||
|
|
+inputs and type/producer drift refuse. Only the reviewed descriptor target
|
||
|
|
+reaches the owner callback; no parameter journal payload is added. Actual
|
||
|
|
+Executor/producer tests cover positive versions and refusal/containment paths.
|
||
|
|
+Acknowledged machine descriptors, authenticated owner evidence, real Forgejo
|
||
|
|
+materialization and contained native sessions remain mandatory and unimplemented;
|
||
|
|
+T03 stays progress, T04 stays wait. No type or case status changed.
|