diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml index 4f64021..be1e9ea 100644 --- a/.forgejo/workflows/image.yaml +++ b/.forgejo/workflows/image.yaml @@ -31,18 +31,19 @@ jobs: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | set -eu - REF="${GITHUB_SHA:-main}" + REF="${GITHUB_SHA:?exact source revision is required}" + echo "${REF}" | grep -Eq '^[0-9a-f]{40}$' SHORT="${REF:0:7}" mkdir -p buildctx "${HOME}/bin" wget -qO /tmp/repo.tar.gz \ - "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" + "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz" tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1 wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \ | tar xz --strip-components=1 -C "${HOME}/bin" docker/docker export PATH="${HOME}/bin:${PATH}" echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin IMAGE="${REGISTRY}/${IMAGE_NAME}" - docker build -f buildctx/Containerfile -t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" buildctx + docker build --label "org.opencontainers.image.revision=${REF}" -f buildctx/Containerfile -t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" buildctx docker push "${IMAGE}:latest" docker push "${IMAGE}:main-${SHORT}" echo "pushed ${IMAGE}:latest and ${IMAGE}:main-${SHORT}" diff --git a/Containerfile b/Containerfile index b55979b..fe9b998 100644 --- a/Containerfile +++ b/Containerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9 +FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN useradd --create-home --uid 10001 user-engine WORKDIR /app diff --git a/docs/conformance/2026-10-11-layer-v08-migration.md b/docs/conformance/2026-10-11-layer-v08-migration.md new file mode 100644 index 0000000..abd8920 --- /dev/null +++ b/docs/conformance/2026-10-11-layer-v08-migration.md @@ -0,0 +1,31 @@ +--- +type: conformance-record +derived: true +derived_from: net-kingdom/canon/standards/security-layer-model_v0.8.md +derived_source_sha256: "3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b" +updated: "2026-10-11" +--- + +# user-engine accepted v0.8 checker migration + +This owner applies GH-WP-0004-T11's post-flip migration to the confirmed +accepted NetKingdom v0.8 cut, immutable accepted-1 artifact SHA256 +3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b. +The checker names that artifact and its local scope in every run. INTENT +frontmatter remains authoritative and the sidecar remains derived; a +contradiction is a finding, never hidden by choosing one form. No declaration +acquires a standard version. + +The pin detector follows OpsWarden's A12 r3 reference semantics: standard and +companion identity values (including nested lists), version-bearing paths and +standard/companion version keys are pins; unrelated intent/schema/spec versions +and bare prose provenance are not. Existing local Tooling/contact and capability +detectors remain in place. Historical version strings in negative fixtures are +retained. This source migration grants no native capability or runtime authority. + +Validation includes the existing local declaration/contact suite and the same +12-case pin/provenance corpus across all four assigned owners, with exact +accepted-artifact and scope assertions. The local test counts and staged source +digests are retained in the coordinating Custodian evidence receipt. Standalone +checker runs continue to inspect only this repository; no estate-wide compliance +claim or novel declaration is produced. diff --git a/docs/evidence/2026-10-11-runtime-base-refresh.md b/docs/evidence/2026-10-11-runtime-base-refresh.md new file mode 100644 index 0000000..4a49cb2 --- /dev/null +++ b/docs/evidence/2026-10-11-runtime-base-refresh.md @@ -0,0 +1,31 @@ +# Runtime base refresh — 2026-10-11 + +The exact source-bound application readback candidate failed its independent +scan with 88 HIGH and three CRITICAL OS findings. Preserve that scan and OCI +index bea54ccf in the managed runtime release record. This remediation changes +only the Containerfile's immutable official Python base index from d764629c +to a6e34c59. It preserves Python 3.12, updates patch 3.12.13 to 3.12.15, and +uses current Docker Official Image source commit +2a3b794c223ab067d122719541cdd54a068732a5 (3.12/slim-trixie). The native new +base contains perl-base 5.40.1-6+deb13u1, libc6 deb13u4 and OpenSSL 3.5.7. + +Detailed registry/source, exact context, installed file hashes, dependency +snapshot and unfiltered scan are in +`rapp-user-engine/docs/evidence/2026-10-11-readback-base-refresh/`. All 47 +copied application inputs and all 45 installed package files match the prior +source. Resolved Python package versions are unchanged. All 20 image readback +tests pass; all 323 source tests pass with 8 optional skips and accepted-v0.8 +layer conformance passes. Native consumer acceptance remains USER-WP-0026-T03. + +The refreshed scan has zero CRITICAL and 44 HIGH findings across 8 distinct +CVEs, all without a published fixed version in the fresh recorded database. +These findings remain visible; no severity reduction, ignore-unfixed option +or suppression was applied. This does not claim a vulnerability-free or +approved release. Clean accepted source commit, publication and independent +release review remain required, alongside exact scoped live rollout permission. + +CI source preparation also replaces short-revision archive lookup with the +validated full 40-hex GITHUB_SHA and labels the built image with that exact +revision. Both workflow shell blocks pass bash syntax checks. This workflow +change is outside the local image runtime context and was not published or +executed; owner CI must produce a new publication receipt from a clean commit. diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index b39daf4..c45128d 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -31,12 +31,11 @@ sys.path.insert(0, str(ROOT / "src")) from user_engine.layer_yaml import load_mapping, load_mapping_text # noqa: E402 # What every run checks against, printed on every run (GH-DEC-2026-020 §4, -# GH-DEC-2026-021 §2). The accepted text is v0.7 at net-kingdom@66dc491; the -# decision records whose rulings this checker enforces beyond v0.7 are named with it. +# GH-DEC-2026-021 §2). The accepted v0.8 cut incorporates A9-A13 and GH024; +# its immutable publication artifact digest is named in each run. VALIDATED_AGAINST = ( - "net-kingdom/canon/standards/security-layer-model_v0.7.md (net-kingdom@66dc491) " - "as amended by GH-DEC-2026-017, GH-DEC-2026-020 and GH-DEC-2026-021 " - "(A9-A13, A12 r3; gate-house@39d9287)" + "net-kingdom/canon/standards/security-layer-model_v0.8.md " + "(accepted-1 sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)" ) # What every run ranges over. Stance, claims and classification maps @@ -148,6 +147,9 @@ def load_intent_layer() -> str: if "layer" not in front: print(f"FAIL: {INTENT.name} frontmatter has no 'layer:' key (§11)", file=sys.stderr) raise SystemExit(2) + if fold(front.get("role", "")) != "pip": + print("FAIL: INTENT.md governing role disagrees with derived Engine/PIP declaration", file=sys.stderr) + raise SystemExit(2) return str(front["layer"]) diff --git a/src/user_engine/application_access.py b/src/user_engine/application_access.py index 8d1efda..44bd77f 100644 --- a/src/user_engine/application_access.py +++ b/src/user_engine/application_access.py @@ -137,12 +137,12 @@ class ApplicationAccessReader: self.bound_applications = bound_applications self.cache_ttl = cache_ttl self.clock = clock - self._cache: dict[tuple[str, str, str], _CacheEntry] = {} + self._cache: dict[tuple[str, str, str, str], _CacheEntry] = {} def rows(self, actor: Actor, session_key: str) -> tuple[ApplicationAccessRow, ...] | None: """Return rendered rows, or None when the catalogue itself is unreadable.""" now = self.clock() - key = (session_key, actor.issuer, actor.subject) + key = (session_key, actor.issuer, actor.subject, actor.tenant) entry = self._cache.get(key) if entry is None or entry.reuse_until <= now: entry = self._read(actor, now) diff --git a/tests/test_application_access.py b/tests/test_application_access.py index 514e669..0890c5b 100644 --- a/tests/test_application_access.py +++ b/tests/test_application_access.py @@ -3,6 +3,7 @@ import json import os import tempfile import unittest +from dataclasses import replace from datetime import UTC, datetime, timedelta from unittest.mock import patch from urllib.error import HTTPError, URLError @@ -159,6 +160,21 @@ class ReaderTests(unittest.TestCase): reader.rows(_actor("alice"), "s1") self.assertEqual(len(readback.reads), 3) + def test_tenant_switch_cannot_reuse_another_tenants_allow(self): + readback = _FakeReadback([ + ReadbackResult("informed-decision", "allow", NOW + timedelta(seconds=30), "decision:1"), + ]) + reader = self._reader(readback) + actor = _actor() + self.assertEqual(reader.rows(actor, "s1")[0].status, "available") + readback.results = [ReadbackResult("informed-decision", "deny")] + other_tenant = replace(actor, tenant="tenant-b") + self.assertEqual(reader.rows(other_tenant, "s1")[0].status, "denied") + self.assertEqual(len(readback.reads), 2) + reader.forget("s1") + reader.rows(actor, "s1") + self.assertEqual(len(readback.reads), 3) + def test_read_failure_is_unchecked_and_not_reused(self): readback = _FakeReadback([], fail_read=True) reader = self._reader(readback) diff --git a/tests/test_layer_v08_convergence.py b/tests/test_layer_v08_convergence.py new file mode 100644 index 0000000..df27a11 --- /dev/null +++ b/tests/test_layer_v08_convergence.py @@ -0,0 +1,46 @@ +"""Accepted A12 r3 semantics, retaining local contact scanners and scope.""" +import importlib.util +from pathlib import Path +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts/check_layer_conformance.py" + +def checker(): + spec = importlib.util.spec_from_file_location("v08_checker", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +@pytest.mark.parametrize("value,pinned", [ + ({"intent_version": "0.3"}, False), + ({"spec_version": "0.7"}, False), + ({"note": "per v0.6 section 13"}, False), + ({"note": "revised 0.7 wording"}, False), + ({"schema_version": "0.1"}, False), + ({"standard": "security-layer-model 0.8"}, True), + ({"STANDARD": ["security-layer-model v0.8"]}, True), + ({"companion": [["SECURITY-COMPANION v0.3"]]}, True), + ({"nested": {"standard_version": "0.8"}}, True), + ({"nested": {"companion_version": "0.3"}}, True), + ({"references": ["security-layer-model@0.8"]}, True), + ({"references": ["security-layer-model_v0.7.md"]}, True), +]) +def test_reference_pin_provenance_semantics(value, pinned): + module = checker() + assert bool(module.find_versions(value, "test")) is pinned + +def test_run_names_exact_accepted_standard_and_scope(): + module = checker() + assert "security-layer-model_v0.8.md" in module.VALIDATED_AGAINST + assert "3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b" in module.VALIDATED_AGAINST + assert "INTENT.md" in module.SCOPE + +def test_governing_role_disagreement_is_a_finding(tmp_path, monkeypatch): + module = checker() + intent = tmp_path / "INTENT.md" + intent.write_text("---\nlayer: Engine\nrole: PDP\n---\n") + monkeypatch.setattr(module, "INTENT", intent) + with pytest.raises(SystemExit) as caught: + module.load_intent_layer() + assert caught.value.code == 2 diff --git a/workplans/ADHOC-2026-10-11.md b/workplans/ADHOC-2026-10-11.md new file mode 100644 index 0000000..365dc17 --- /dev/null +++ b/workplans/ADHOC-2026-10-11.md @@ -0,0 +1,30 @@ +--- +id: USER-WP-ADHOC-2026-10-11 +type: workplan +title: "Refresh vulnerable readback candidate runtime base" +domain: communication +repo: user-engine +status: finished +flavor: implementation +owner: codex +topic_slug: user-engine +created: "2026-10-11" +updated: "2026-10-11" +--- + +## Refresh immutable Python patch base and verify exact application preservation + +```task +id: USER-WP-ADHOC-2026-10-11-T01 +status: done +priority: high +``` + +Replace only the Containerfile base digest with the current official Python 3.12 +index; retain exact application bytes and resolved Python dependency versions. +The bounded local candidate removes all 3 CRITICAL and44 of 88 HIGH OS findings. +Retain remaining 44 unfixed HIGH findings in the unfiltered fresh scan. Exact +source/image/provenance checks and 20 image / 323 source tests pass. Evidence: +`docs/evidence/2026-10-11-runtime-base-refresh.md`. This source remediation +finishes no deployment: USER-WP-0026-T03 and USER-WP-0028-T02 retain native +consumer acceptance, clean source commit/publication and scoped rollout gates. diff --git a/workplans/USER-WP-0026-account-recovery.md b/workplans/USER-WP-0026-account-recovery.md index d002c2b..3468583 100644 --- a/workplans/USER-WP-0026-account-recovery.md +++ b/workplans/USER-WP-0026-account-recovery.md @@ -62,8 +62,8 @@ logout. This does not accept the remaining multi-user Vergabe pilot. ```task id: USER-WP-0026-T03 status: wait -blocked_on: message-from:access-engine -blocking_reason: "Needs access-engine's per-identity decision/grant/revocation/readback contract and a readable protected-system registry (with key-cape client binding) as the workload catalogue; the membership store cannot represent the allow list." +blocked_on: message-from:rapp-user-engine +blocking_reason: "FLEX-WP-0035-T04 delivered the admitted readback pin on 2026-10-10. Needs rapp-user-engine readback URL, separate projected caller token and exact-pin egress, an image containing the readback client, and native consumer allow/deny/unavailable acceptance against the live pin." priority: high state_hub_task_id: "95e9a6d4-7e57-5483-97c5-3f4a45bb6767" ``` @@ -147,3 +147,21 @@ despite its `message-from:net-kingdom` qualifier (added 2026-09-28). Sent one now (message `db1b7d6e`): does NetKingdom own the workload catalogue/admission record and the scoped access-decision contract, and if not, who does. The qualifier stays on net-kingdom until it answers or names another owner. + +2026-10-10 security consolidation review: FLEX-WP-0035-T04 is done. Its founder-run +Helm upgrades placed lifecycle v2 and application-readback on image e8c57c18; +Ready and anonymous 401/non-enforcement 404 checks passed. Those provider +receipts do not establish user-engine caller acceptance. The canonical apply +home, rapp-user-engine/manifests/runtime.yaml, currently has no readback URL +or dedicated projected-token mount and permits flex-auth egress only to the +enforcement pin. Remaining owner work: wire USER_ENGINE_READBACK_URL and +USER_ENGINE_READBACK_TOKEN_FILE to a separate audience flex-auth projection, +permit only the application-readback pod selector, promote the immutable image +containing this client, and verify native consumer readback without disclosing +a token. Reuse that acceptance for USER-WP-0028-T02; it is the same integration +boundary. No grant, browser acceptance or live deployment is inferred here. + +The local readback cache now binds each row set to tenant as well as session, +issuer and subject. A tenant switch within the same session therefore performs +a fresh read instead of displaying another tenant's unexpired allow. A focused +regression exercises an allow in tenant-a followed by a deny in tenant-b.