From d53c0d183c6184b8a2d157d4d4a06624c5d35faa Mon Sep 17 00:00:00 2001 From: iliya Date: Wed, 9 Sep 2026 21:18:42 +0000 Subject: [PATCH 1/2] feat(authoring): add closed public journey intake contract Refs #216, #230, #204, #208. --- .../scripts/packed-installer-bridge.js | 46 ++- .../scripts/packed-installer-bridge.md | 73 +++++ .../scripts/packed-installer-bridge.test.js | 45 +++ .../scripts/public-authoring-acceptance.js | 292 ++++++++++++++++++ .../test/public-authoring-acceptance.test.js | 202 ++++++++++++ scripts/check-packed-ci.py | 114 ++++++- scripts/run-packed-ci.py | 58 +++- scripts/test_packed_ci.py | 123 ++++++++ 8 files changed, 950 insertions(+), 3 deletions(-) create mode 100644 npm/agentplugins/scripts/public-authoring-acceptance.js create mode 100644 npm/agentplugins/test/public-authoring-acceptance.test.js diff --git a/npm/agentplugins/scripts/packed-installer-bridge.js b/npm/agentplugins/scripts/packed-installer-bridge.js index 947d4c9d..a81a05dc 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.js @@ -308,7 +308,24 @@ function publicEvidence(cfg, native, configPath, intake = "public-fixture/v1") { pair_marker_sha256: native.pair_marker_sha256, tools: native.tools, binaries: native.binaries, ...(v2 ? { installer_boundary: native.installer_boundary } : {}) } }; } +function authenticatedIntake(request) { + const reader = require("./public-authoring-acceptance"); + reader.request(request); + // The production reader remains closed until genuine result validation exists. + // No caller callback, fixture conversion or completed/authenticated flag. + const inputs = reader.readJourney(request); + assert.equal(inputs.record.cell, "linux-amd64/pair-node22", "designated bridge cell"); + assert.equal(inputs.identity.commit, request.expectedCommit); + assert.equal(inputs.projects.length, 10); + return { identity: inputs.identity, repo: inputs.repo, candidate_sha256: inputs.candidate_sha256, + packs: inputs.packs, projects: inputs.projects, snapshots: inputs.snapshots, + public_inputs: { schema: "authoring-public-local-inputs/v1", journey_sha256: request.journeySha256, + admission_sha256: request.admissionSha256, stage: inputs.record.stage, native_inputs: inputs.record.native_inputs, + producer: inputs.record.producer, cell: inputs.record.cell, tools: inputs.record.tools, protected_paths: inputs.protected_paths, + signed_promotion: false, public_eligible: false, qualification: null } }; +} function intake(request) { + if (request.intake === "public-authenticated/v1") return authenticatedIntake(request); if (!Object.hasOwn(request, "intake")) return privateIntake(request); c.keys(request, [...REQUEST_KEYS, "intake"], "public bridge request"); assert.ok(["public-fixture/v1", "public-fixture/v2"].includes(request.intake)); assert.equal(request.disposableEvidence, true); @@ -321,12 +338,24 @@ function intake(request) { } function seal(request) { const inputs = intake(request); + if (request.intake === "public-authenticated/v1") return { + schema: "packed-installer-bridge/public-authenticated/v1", request, + verifier_sha256: hash(__filename), helper_sha256: hash(require.resolve("./dual-authoring-candidate")), + reader_sha256: hash(require.resolve("./public-authoring-acceptance")), inputs, + release_eligible: false, platform_acceptance: false, attested: false }; return { schema: "packed-installer-bridge/v1", request, verifier_sha256: hash(__filename), helper_sha256: hash(require.resolve("./dual-authoring-candidate")), inputs, release_eligible: false, platform_acceptance: false, attested: false }; } function verify(configFile, configDigest, expectedCommit) { pin(configFile, configDigest); const cfg = json(configFile); + if (cfg.schema === "packed-installer-bridge/public-authenticated/v1") { + c.keys(cfg, ["schema", "request", "verifier_sha256", "helper_sha256", "reader_sha256", "inputs", "release_eligible", "platform_acceptance", "attested"], "authentic seal"); + falseClaims(cfg); assert.equal(cfg.request.intake, "public-authenticated/v1"); + assert.equal(cfg.request.expectedCommit, expectedCommit); + assert.deepEqual(cfg, seal(cfg.request), "authenticated local seal changed"); + return cfg.inputs; + } c.keys(cfg, ["schema", "request", "verifier_sha256", "helper_sha256", "inputs", "release_eligible", "platform_acceptance", "attested"], "bridge config"); assert.equal(cfg.schema, "packed-installer-bridge/v1"); falseClaims(cfg); assert.equal(cfg.request.expectedCommit, expectedCommit); @@ -335,6 +364,11 @@ function verify(configFile, configDigest, expectedCommit) { } function publishSeal(request, output) { const result = seal(request); absolute(output); + if (request.intake === "public-authenticated/v1") { + for (const protectedPath of result.inputs.public_inputs.protected_paths) { + require("./public-authoring-acceptance").disjoint([output, protectedPath]); + } + } for (const root of [result.inputs.repo, request.fixtureRoot, ...result.inputs.snapshots.map(s => s.root)]) { const a = output.toLowerCase(), b = root.toLowerCase(); assert.ok(a !== b && !a.startsWith(b + "/") && !b.startsWith(a + "/"), "output overlaps evidence/source"); @@ -346,7 +380,17 @@ function publishSeal(request, output) { if (require.main === module) { try { const [command, file, digest, commit] = process.argv.slice(2); - if (command === "seal" && process.argv.length === 5) { + if (command === "authenticated-options" && process.argv.length === 4) { + const options = require("./public-authoring-acceptance").fileJSON(file); + c.keys(options, ["request", "go", "node", "modCache"], "authenticated runner options"); + process.stdout.write(c.encode(authenticatedIntake(options.request))); + } else if (command === "authenticated-intake" && process.argv.length === 4) { + process.stdout.write(c.encode(authenticatedIntake(require("./public-authoring-acceptance").fileJSON(file)))); + } else if (command === "authenticated-seal" && process.argv.length === 5) { + const request = require("./public-authoring-acceptance").fileJSON(file); + assert.equal(request.intake, "public-authenticated/v1"); + process.stdout.write(publishSeal(request, digest) + "\n"); + } else if (command === "seal" && process.argv.length === 5) { process.stdout.write(publishSeal(json(file), digest) + "\n"); } else if (command === "verify" && process.argv.length === 6) { process.stdout.write(c.encode(verify(file, digest, commit))); diff --git a/npm/agentplugins/scripts/packed-installer-bridge.md b/npm/agentplugins/scripts/packed-installer-bridge.md index 7a9c9903..c58e4a24 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.md +++ b/npm/agentplugins/scripts/packed-installer-bridge.md @@ -285,3 +285,76 @@ bridge intake. Genuine C3 execution needs a separately reviewed authenticated intake and actual installed launchers/postinstall and installer lifecycle. N2, C3 E, P/Q, B, anonymous public readbacks, pair channels, PyPI/Homebrew, full-platform E2E, release and phases 0–11 remain open. + +## C3a local authenticated input contract (execution remains closed) + +C3a adds a distinct `public-authenticated/v1` request, with exactly +`intake,expectedCommit,journey,journeySha256,admission,admissionSha256,fixtureRoot`. +It does not translate private, public-fixture/v1 or public-fixture/v2 receipts. +Their existing false/null/not_evaluated claims and the offline v2 71-call +boundary remain unchanged. No workflow, promotion, native qualification export, +public launcher, dependency or package closure changes are part of C3a. + +`public-authoring-acceptance.js` fixes `public-wrapper-matrix/v1`'s eighteen +host/runtime cells and thirty product/runtime executions. Its J codec binds the +exact canonical I and S, both unchanged packs including SHA256/size/SRI/SHA1, +all twelve native outer/inner subject pins, source F, producer run/attempt/ref, +actual host and distinct controller/npm/shim Node/tool pins. It rejects unknown +fields, alternate canonical spelling, duplicate keys, invalid UTF-8, excessive +nesting and oversized records. Local paths must be canonical and quiescent; +this does not introduce a hostile concurrent filesystem security guarantee. + +The fixed local receipt schema is `authoring-public-local-inputs/v1`. Its fields +are `schema,selected,workflow_sha,input_file,stage,repo,work_parent,stage_root, +input_root,journey_root,fixture_root,cell,tools,producer`. It contains comparison +pins, never an authenticated/completed boolean. `readJourneyInputs` genuinely +calls the existing completed `readStage` and `readInputs`, retaining their +three- and nineteen-subject contracts. The existing stage reader supplies source, +pack closure and pack inspection; C3a adds no extractor or verifier engine. +Fresh authenticated subjects are compared with the original retained S/I/packs. +Re-admission scratch is separate from original custody, source, projects and +journey evidence, so later seal checks refer to the same original roots. + +J is `authoring-public-journey/v1`, with the plan's exact top-level fields. +Evidence is an ordered table of fixed `commands.json`, `projects.json`, +`npm-lifecycle.json`, `cache-process.json`, `installer.json` path/size/SHA256 rows. +The limit is 1 MiB per record and non-command evidence file, 16 MiB for commands, +and 1 MiB for each command stdout/stderr. The five-file closure therefore fits +within the plan's 128 MiB aggregate cap. Original project snapshots include +empty directories, bytes, sizes and modes. No regeneration or project copy is +implemented. A fixed core inventory contains 55 kit-only or 127 pair command +rows, including the eighteen genuine installer operations required per pair. +Core argv/cwd/status/signal and bounded author result checks are recomputed; +structural true assertions never imply their truth. + +**Live J admission deliberately fails.** Full npm shim/postinstall and peer +lifecycle, cache/process/cancellation, complete conformance/parity and genuine +installer result/observation validators belong to C3b. In particular there is +no reviewed public-shim installer validator or whole-descendant observer +interface available at this boundary. `verifyJourney` emits an explicit C3b +missing-capability error even for structurally consistent transcripts. The +three corresponding evidence payloads are bound bytes, not semantically +validated results. No production producer returns synthetic success. +`readJourneyInputs` is input-custody-only; `readJourney` cannot publish a seal. +The local reader CLI exposes only `--read-local-inputs REQUEST` and explicitly +labels that scope. Completed remote `readAcceptance` always fails in C3a. + +The bridge has separate `authenticated-options`, `authenticated-intake` and +`authenticated-seal` commands. Its new seal pins the reader source in addition +to the existing verifier/helper and repeats admission on `verify`. The runner's +`--public-authenticated ROOT F OPTIONS` accepts exactly `request,go,node,modCache`. +It calls authentic intake before creating output, then preserves the existing +Linux planner commands and five environment variables. The terminal checker +re-enters the real Node reader, checks ten original project paths, exact thirty +plan tuples and post-plan snapshots. Summary claims remain false, with null +qualification and scope `local-authenticated-inputs-and-injected-planner`. +A local J/bridge, successful fixture TAP or injected planner cannot stand for E. + +Positive tests explicitly mock custody interfaces and, for bridge-only seal +controls, the unavailable J result boundary. These are source controls only; +they execute no npm, native, installer, scanner, verifier or provider. C3b must +supply the genuine producer, full result validators, fixed workflow, completed +E aggregate/authenticated reader and P adapter together. Its npm/process, +workflow and P test names remain outstanding. Native/N2, genuine E, P/Q/B, +anonymous acquisition/readbacks/channels, stable release and phases 0–11 +remain open; independent review and required remote CI are not waived. diff --git a/npm/agentplugins/scripts/packed-installer-bridge.test.js b/npm/agentplugins/scripts/packed-installer-bridge.test.js index e2cdfba5..eaed11da 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.test.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.test.js @@ -215,3 +215,48 @@ test('SYNTHETIC v2 help/preflight boundary cannot stand for production add', pos } finally { c.frozenCandidate = original; } assert.throws(()=>seal(), 'synthetic fixtures cannot publish native evidence'); }); + +// C3-only synthetic seam: bypass the unavailable journey result adapter ONLY +// inside these tests. The real reader and CLI cannot accept this local J. +test('C3 bridge authenticated intake preserves legacy dispatch', t => { + const a = require('./public-authoring-acceptance'); + const {fixture, withReaders} = require('../test/public-authoring-acceptance.test'); + const f = fixture(t); + withReaders(t, f, () => { + assert.throws(() => bridge.seal(f.request), /C3b required/); + t.mock.method(a, 'readJourney', request => a.readJourneyInputs(request)); + const result = bridge.seal(f.request); + assert.equal(result.schema, 'packed-installer-bridge/public-authenticated/v1'); + assert.equal(result.inputs.projects.length, 10); + assert.equal(result.inputs.public_inputs.qualification, null); + assert.equal(result.attested, false); + for (const intake of ['public-fixture/v1', 'public-fixture/v2', undefined]) { + const request = {...f.request}; if (intake) request.intake = intake; else delete request.intake; + assert.throws(() => bridge.seal(request)); + } + for (const extra of [{authenticated:true}, {nativeTap:'fixture.tap'}, {disposableEvidence:true}]) assert.throws(() => bridge.seal({...f.request,...extra})); + }); + assert.throws(() => a.readAcceptance(f.request), /completed remote E/); +}); +test('C3 bridge seal binds original ten projects', t => { + const a = require('./public-authoring-acceptance'); + const {fixture, withReaders} = require('../test/public-authoring-acceptance.test'); + const f = fixture(t); + withReaders(t, f, () => { + t.mock.method(a, 'readJourney', request => a.readJourneyInputs(request)); + const sealed = path.join(f.root, 'sealed.json'), pin = bridge.publishSeal(f.request, sealed); + const verify = () => bridge.verify(sealed, pin, f.request.expectedCommit); + assert.equal(verify().projects.length, 10); + for (const output of [path.join(f.admission.work_parent, 'overlap.json'), f.request.admission, f.j.tools.go.path]) { + assert.throws(() => bridge.publishSeal(f.request, output), /overlapping roots/); + } + const source = path.join(f.j.projects.agentplugins, 'skill'), manifest = path.join(source, 'plugin.json'); + const original = fs.readFileSync(manifest), mode = fs.statSync(manifest).mode & 0o777; + fs.appendFileSync(manifest, 'changed'); assert.throws(verify); fs.writeFileSync(manifest, original); + fs.chmodSync(manifest, mode ^ 0o020); assert.throws(verify); fs.chmodSync(manifest, mode); + const extra = path.join(source, 'unexpected-empty'); fs.mkdirSync(extra); assert.throws(verify); + // Retain the changed fixture; no cleanup and no claim that it still verifies. + assert.throws(() => bridge.publishSeal(f.request, path.join(f.root, 'late-seal.json'))); + assert.equal(fs.existsSync(path.join(f.root, 'late-seal.json')), false); + }); +}); diff --git a/npm/agentplugins/scripts/public-authoring-acceptance.js b/npm/agentplugins/scripts/public-authoring-acceptance.js new file mode 100644 index 00000000..84da463b --- /dev/null +++ b/npm/agentplugins/scripts/public-authoring-acceptance.js @@ -0,0 +1,292 @@ +"use strict"; + +// C3a: closed local input custody and structural J contract. No producer, E +// authentication, installer policy, archive engine or execution override lives here. +const fs = require("node:fs"); +const path = require("node:path"); +const assert = require("node:assert/strict"); +const c = require("./dual-authoring-candidate"); +const contract = require("../lib/public-authoring-contract"); +const { fields, hash, positive, fixed } = contract.checks; +const LIMIT = 1024 * 1024, TRANSCRIPT_LIMIT = 16 * LIMIT; +const SCHEMA = "authoring-public-journey/v1"; +const MATRIX_SCHEMA = "public-wrapper-matrix/v1"; +const INTAKE = "public-authenticated/v1"; +const WORKFLOW = ".github/workflows/authoring-public-packed.yml"; +const REQUEST = ["intake", "expectedCommit", "journey", "journeySha256", "admission", "admissionSha256", "fixtureRoot"]; +const J_FIELDS = ["schema", "status", "identity", "authoring_mode", "asset_scope", "candidate_sha256", "pair_marker_sha256", + "native_inputs", "stage", "packs", "producer", "cell", "tools", "command_contract_sha256", "subjects", "projects", "evidence", "assertions"]; +const LANES = Object.freeze(["skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"]); +const EVIDENCE = Object.freeze(["commands.json", "projects.json", "npm-lifecycle.json", "cache-process.json", "installer.json"]); +const ASSERTIONS = Object.freeze(["fixed_commands", "pair_parity", "projects_preserved", "npm_lifecycle", "cache_process", "production_installer", "children_reaped"]); +const MISSING = "C3b required: reviewed public installer result validator and whole-descendant observer; full npm/cache/process/parity finalization is not implemented; J execution admission is closed"; +const matrix = Object.freeze(["linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64", "windows-amd64", "windows-arm64"].flatMap(target => + [18, 22, 24].map(node => Object.freeze({ key: `${target}/${node === 18 ? "kit" : "pair"}-node${node}`, target, node, + products: Object.freeze(node === 18 ? ["plugin-kit-ai"] : [...c.PRODUCTS]) })))); +const agree = (a, b, label) => assert.deepEqual(a, b, `C3 ${label}`); +function cell(key) { const found = matrix.find(row => row.key === key); assert.ok(found, "fixed C3 cell required"); return found; } +function absolute(value) { + assert.ok(typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) && + path.isAbsolute(value) && path.normalize(value) === value && value !== path.parse(value).root, "canonical absolute C3 path"); + return value; +} +function disjoint(roots) { + roots.forEach(absolute); + roots.forEach((a, i) => roots.slice(i + 1).forEach(b => { + a = a.toLowerCase(); b = b.toLowerCase(); + assert.ok(a !== b && !a.startsWith(b + path.sep) && !b.startsWith(a + path.sep), "C3 overlapping roots"); + })); +} +function pin(file, sha256, maximum = LIMIT) { + hash(sha256, "file"); const body = c.readFile(absolute(file), maximum); + agree(c.digest(body), sha256, "retained file pin"); return body; +} +function bounded(body, maximum) { + assert.ok(Buffer.isBuffer(body) && body.length > 0 && body.length <= maximum, "bounded canonical C3 bytes"); + // Fixed schemas are shallow. Bound nesting before parse, including arrays. + const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body); + let depth = 0, quoted = false, escaped = false; + for (const ch of text) { + if (quoted) { if (escaped) escaped = false; else if (ch === "\\") escaped = true; else if (ch === '"') quoted = false; } + else if (ch === '"') quoted = true; + else if (ch === "{" || ch === "[") assert.ok(++depth <= 16, "C3 depth limit"); + else if (ch === "}" || ch === "]") depth--; + } + const value = JSON.parse(text); + agree(body, c.encode(value), "canonical JSON (no duplicate keys or alternate spelling)"); return value; +} +function fileJSON(file, maximum = LIMIT) { return bounded(c.readFile(absolute(file), maximum), maximum); } +function locator(value) { + fields(value, ["sha256", "artifact"], "C3 locator"); hash(value.sha256, "locator"); + fields(value.artifact, ["run_id", "run_attempt", "artifact_id", "artifact_sha256"], "C3 artifact"); + for (const k of ["run_id", "artifact_id"]) positive(value.artifact[k], Number.MAX_SAFE_INTEGER, k); + positive(value.artifact.run_attempt, 1000, "attempt"); hash(value.artifact.artifact_sha256, "artifact"); +} +function producer(value, input) { + fields(value, ["workflow", "source", "ref", "run_id", "run_attempt"], "C3 producer"); + fixed(value.workflow, WORKFLOW, "public workflow"); fixed(value.source, input.identity.commit, "source F"); + fixed(value.ref, `refs/tags/${input.products.agentplugins.tag}`, "public ref"); + positive(value.run_id, Number.MAX_SAFE_INTEGER, "public run"); positive(value.run_attempt, 1000, "public attempt"); +} +function tools(value, selected) { + fields(value, ["orchestrator_node", "npm_node", "shim_node", "npm", "go", "host"], "C3 tools"); + const [os, cpu] = selected.target.split("-"); + agree(value.host, { platform: os === "windows" ? "win32" : os, arch: cpu === "amd64" ? "x64" : "arm64" }, "host matrix"); + for (const key of ["orchestrator_node", "npm_node", "shim_node", "npm", "go"]) { + const t = value[key]; + if (key === "go" && selected.key !== "linux-amd64/pair-node22") { agree(t, null, "Go only in bridge cell"); continue; } + fields(t, ["path", "sha256", "version"], "C3 tool pin"); absolute(t.path); hash(t.sha256, key); + assert.ok(typeof t.version === "string" && t.version.length < 128 && /^[\x21-\x7e]+$/.test(t.version), "bounded tool version"); + if (key.endsWith("_node")) assert.match(t.version, /^v[1-9][0-9]*\.[0-9]+\.[0-9]+$/); + if (["npm_node", "shim_node"].includes(key)) assert.ok(t.version.startsWith(`v${selected.node}.`), "actual selected Node major"); + } +} + +/** Fixed core command/result inventory, never executable caller argv. C3b must + * also implement the separately required npm/cache/process evidence validators. */ +function commandContract(key) { + const selected = cell(key), rows = []; + for (const product of selected.products) { + const add = (id, args, status = 0, author = true, lane = null, scenario = "projects") => rows.push({ product, id, + argv: [...(author && product === "agentplugins" ? ["author"] : []), ...args], status, author, lane, scenario }); + add("product-version", ["version", "--format=json"], 0, false); + add("product-help", ["--help"], 0, false); + add("engine-version", ["version", "--format=json"]); add("author-help", ["--help", "--format=json"]); + add("capabilities", ["capabilities", "--format=json"]); + for (const lane of LANES) { + // Existing bridge constructor, loaded only on invocation (no require cycle). + const init = [...require("./packed-installer-bridge").publicInit(lane), "--format=json"]; + add(`${lane}/init`, init, 0, true, lane); + add(`${lane}/extra-skill`, ["skills", "init", "extra-skill", lane, "--description", "Use for extra documentation requests", "--format=json"], 0, true, lane); + for (const verb of ["skills validate", "validate", "inspect", "test", "compat", "doctor"]) { + add(`${lane}/${verb.replace(" ", "-")}`, [...verb.split(" "), lane, + ...(verb === "compat" ? ["--target", "claude,codex"] : []), "--format=json"], verb === "doctor" && lane !== "skill" ? 1 : 0, true, lane); + } + add(`${lane}/existing`, init, 1, true, lane); + } + add("malformed-skill", ["validate", "skill", "--format=json"], 1, true, "skill", "malformed-skill"); + add("invalid-flag", ["init", "invalid-destination", "--force", "--format=json"], 2); + add("missing-template-input", ["init", "missing-destination", "--template", "mcp-stdio", "--format=json"], 2); + add("installer-flag", ["validate", "skill", "--scope=user", "--format=json"], 2, true, "skill"); + if (product === "plugin-kit-ai") add("retired-v1", ["update", "--all", "--format=json"], 2, false); + if (product === "agentplugins") for (const lane of ["skill", "mcp-remote", "hybrid-stdio"]) { + for (const verb of ["dry-run", "add", "info", "update", "remove", "list"]) { + const argv = verb === "list" ? ["list"] : [verb === "dry-run" ? "add" : verb, lane, "--target=codex"]; + add(`installer/${lane}/${verb}`, [...argv, ...(verb === "dry-run" ? ["--dry-run"] : []), "--format=json"], 0, false, lane); + } + } + } + return rows; +} +function journey(value, inputBytes, stageBytes) { + const input = contract.decodeInputs(inputBytes); + const stage = require("./stage-authoring-npm").decodeStage(stageBytes, inputBytes); + fields(value, J_FIELDS, "C3 J"); fixed(value.schema, SCHEMA, "J schema"); fixed(value.status, "completed", "J status syntax"); + for (const name of ["identity", "authoring_mode", "asset_scope", "candidate_sha256", "pair_marker_sha256", "native_inputs", "packs"]) agree(value[name], stage[name], name); + locator(value.stage); agree(value.stage.sha256, c.digest(stageBytes), "exact S bytes"); + agree([value.stage.artifact.run_id, value.stage.artifact.run_attempt], [stage.producer.run_id, stage.producer.run_attempt], "S attempt"); + assert.ok(![stage.native_inputs.artifact.artifact_id, input.preparation.artifact.artifact_id].includes(value.stage.artifact.artifact_id), "separate stage artifact"); + producer(value.producer, input); + assert.ok(![stage.producer.run_id, input.producer.run_id, input.preparation.artifact.run_id].includes(value.producer.run_id), "separate public invocation"); + const selected = cell(value.cell); tools(value.tools, selected); + agree(value.command_contract_sha256, c.digest(c.encode(commandContract(value.cell))), "fixed core command contract"); + agree(value.subjects, Object.fromEntries(c.PRODUCTS.map(p => [p, input.products[p].assets])), "all twelve outer/inner native subject pins"); + fields(value.projects, selected.products, "C3 projects"); + for (const p of selected.products) absolute(value.projects[p]); + assert.ok(Array.isArray(value.evidence) && value.evidence.length === EVIDENCE.length, "fixed evidence table"); + agree(Reflect.ownKeys(value.evidence), [...EVIDENCE.map((_, i) => String(i)), "length"], "plain evidence array fields"); + value.evidence.forEach((row, i) => { + fields(row, ["path", "size", "sha256"], "C3 evidence row"); fixed(row.path, EVIDENCE[i], "fixed evidence filename"); + positive(row.size, i === 0 ? TRANSCRIPT_LIMIT : LIMIT, "evidence size"); hash(row.sha256, "evidence"); + }); + fields(value.assertions, ASSERTIONS, "C3 assertion syntax"); + for (const key of ASSERTIONS) fixed(value.assertions[key], true, "assertion syntax only"); + // Reconstruct every object in fixed order, rather than preserving caller key + // insertion order. Structural syntax never recomputes successful assertions. + const orderedLocator = v => ({ sha256: v.sha256, artifact: Object.fromEntries( + ["run_id", "run_attempt", "artifact_id", "artifact_sha256"].map(k => [k, v.artifact[k]])) }); + const normalized = { ...value, + ...Object.fromEntries(["identity", "authoring_mode", "asset_scope", "candidate_sha256", "pair_marker_sha256", "native_inputs", "packs"].map(k => [k, stage[k]])), + stage: orderedLocator(value.stage), + producer: Object.fromEntries(["workflow", "source", "ref", "run_id", "run_attempt"].map(k => [k, value.producer[k]])), + tools: Object.fromEntries(["orchestrator_node", "npm_node", "shim_node", "npm", "go", "host"].map(k => [k, + value.tools[k] === null ? null : Object.fromEntries((k === "host" ? ["platform", "arch"] : ["path", "sha256", "version"]).map(n => [n, value.tools[k][n]]))])), + subjects: Object.fromEntries(c.PRODUCTS.map(p => [p, input.products[p].assets])), + projects: Object.fromEntries(selected.products.map(p => [p, value.projects[p]])), + evidence: value.evidence.map(row => ({ path: row.path, size: row.size, sha256: row.sha256 })), + assertions: Object.fromEntries(ASSERTIONS.map(k => [k, value.assertions[k]])) }; + return Object.fromEntries(J_FIELDS.map(key => [key, normalized[key]])); +} +function encodeJourney(value, input, stage) { + const body = c.encode(journey(value, input, stage)); assert.ok(body.length <= LIMIT, "J size limit"); return body; +} +function decodeJourney(body, input, stage) { + const value = journey(bounded(body, LIMIT), input, stage); + agree(body, c.encode(value), "fixed J field order"); return value; +} +function request(value) { + fields(value, REQUEST, "C3 bridge request"); fixed(value.intake, INTAKE, "authentic intake"); + assert.ok(typeof value.expectedCommit === "string" && value.expectedCommit.length === 40 && /^[0-9a-f]{40}$/.test(value.expectedCommit) && !/^0+$/.test(value.expectedCommit), "exact source F"); + for (const key of ["journey", "admission", "fixtureRoot"]) absolute(value[key]); + for (const key of ["journeySha256", "admissionSha256"]) hash(value[key], key); + assert.ok(c.encode(value).length <= LIMIT, "request size limit"); return value; +} + +/** Input custody only: genuinely call completed S and I readers. The local + * receipt is comparison data, never its own authentication or completed E. */ +function readJourneyInputs(value) { + const r = request(value), receiptBytes = pin(r.admission, r.admissionSha256); + const a = bounded(receiptBytes, LIMIT); + fields(a, ["schema", "selected", "workflow_sha", "input_file", "stage", "repo", "work_parent", "stage_root", "input_root", + "journey_root", "fixture_root", "cell", "tools", "producer"], "C3 local admission"); + fixed(a.schema, "authoring-public-local-inputs/v1", "local input schema"); locator(a.stage); + fields(a.selected, ["tag", "ref", "source", "versions"], "selection"); + for (const key of ["repo", "work_parent", "stage_root", "input_root", "journey_root", "fixture_root"]) c.safeDirectory(absolute(a[key])); + const roots = [a.repo, a.work_parent, a.stage_root, a.input_root, a.journey_root, a.fixture_root]; disjoint(roots); + for (const root of roots) disjoint([root, r.admission]); + agree(a.repo, path.resolve(__dirname, "../../.."), "executing source checkout"); + agree(r.journey, path.join(a.journey_root, "public-journey.json"), "fixed local J filename"); + agree(r.fixtureRoot, a.fixture_root, "original fixture root"); + agree(a.input_file, path.join(a.input_root, contract.INPUT_FILE), "retained original I filename"); + const inputBytes = c.readFile(a.input_file, LIMIT), input = contract.decodeInputs(inputBytes); + agree(a.selected, { tag: input.products.agentplugins.tag, ref: `refs/tags/${input.products.agentplugins.tag}`, + source: input.identity.commit, versions: input.identity.versions }, "selection from I"); + agree(a.workflow_sha, r.expectedCommit, "requested F"); agree(a.workflow_sha, input.identity.commit, "I source F"); + const stageBytes = pin(path.join(a.stage_root, "completion.json"), a.stage.sha256); + const body = pin(r.journey, r.journeySha256), j = decodeJourney(body, inputBytes, stageBytes); + agree(j.stage, a.stage, "stage locator"); agree(j.cell, a.cell, "local cell"); agree(j.tools, a.tools, "local tools"); + agree(j.producer, a.producer, "same public invocation pins"); + agree(j.tools.host, { platform: process.platform, arch: process.arch }, "actual reader host"); + agree(j.tools.orchestrator_node, { path: process.execPath, sha256: c.digest(c.readFile(process.execPath)), version: process.version }, "executing Node"); + const toolPins = Object.entries(j.tools).filter(([k, v]) => k !== "host" && v !== null).map(([, v]) => v); + for (const t of toolPins) { pin(t.path, t.sha256, contract.MAX_NATIVE_BYTES); for (const root of roots) disjoint([root, t.path]); } + const stages = require("./stage-authoring-npm"), inputs = require("./authoring-native-inputs"); + assert.equal(typeof stages.readStage, "function", "missing completed stage verifier"); + assert.equal(typeof inputs.readInputs, "function", "missing completed input verifier"); + const admittedStage = stages.readStage({ input: inputBytes, selected: a.selected, workflow_sha: a.workflow_sha, + artifact: a.stage.artifact, stage_sha256: a.stage.sha256, repo: a.repo, workParent: a.work_parent, + node: a.tools.orchestrator_node.path, npm: a.tools.npm.path }); + agree(admittedStage.record, stages.decodeStage(stageBytes, inputBytes), "authenticated S record"); + const admittedInputs = inputs.readInputs({ input: inputBytes, selected: a.selected, workflow_sha: a.workflow_sha, + artifact: j.native_inputs.artifact, scratch: a.work_parent }); + agree(admittedInputs.input, input, "authenticated I record"); + // Compare retained original custody to independently acquired authenticated bytes. + const retained = []; + for (const [admitted, root, count] of [[admittedStage, a.stage_root, 3], [admittedInputs, a.input_root, 19]]) { + assert.equal(admitted.subjects.length, count, "original subject multiset count"); + for (const row of admitted.subjects) { + const relative = path.relative(admitted.root, row.file); + assert.ok(relative && !relative.startsWith("..") && !path.isAbsolute(relative), "checked subject containment"); + const file = path.join(root, relative); + pin(row.file, row.sha256, contract.MAX_NATIVE_BYTES); pin(file, row.sha256, contract.MAX_NATIVE_BYTES); + retained.push({ file, sha256: row.sha256 }); + } + } + // These local snapshots are stable across re-admission; freshly extracted + // authentication scratch is not part of the local project's seal identity. + const bridge = require("./packed-installer-bridge"), projects = []; + for (const product of cell(j.cell).products) { + const parent = path.join(a.fixture_root, `${product} projects ü`); + agree(j.projects[product], parent, "original public project parent"); + agree(fs.readdirSync(parent).sort(), [...LANES].sort(), "all and only five original projects"); + for (const lane of LANES) { + const source = path.join(parent, lane); c.safeDirectory(source); + c.readFile(path.join(source, "plugin.json"), LIMIT); c.readFile(path.join(source, "skills/extra-skill/SKILL.md"), LIMIT); + projects.push({ product, lane, source }); + } + } + const evidence = {}; + for (const row of j.evidence) { + const bytes = pin(path.join(a.journey_root, row.path), row.sha256, row.path === "commands.json" ? TRANSCRIPT_LIMIT : LIMIT); + agree(bytes.length, row.size, "evidence size"); evidence[row.path] = bounded(bytes, row.path === "commands.json" ? TRANSCRIPT_LIMIT : LIMIT); + } + const snapshots = [a.stage_root, a.input_root, a.journey_root, a.fixture_root].map(root => bridge.snapshot(root)); + agree(evidence["projects.json"], Object.fromEntries(cell(j.cell).products.map(p => [p, bridge.snapshot(j.projects[p])])), "original project trees and modes"); + for (const row of retained) pin(row.file, row.sha256, contract.MAX_NATIVE_BYTES); + for (const t of toolPins) pin(t.path, t.sha256, contract.MAX_NATIVE_BYTES); + agree(pin(r.admission, r.admissionSha256), receiptBytes, "late admission bytes"); agree(pin(r.journey, r.journeySha256), body, "late J bytes"); + return { record: j, evidence, identity: j.identity, repo: a.repo, candidate_sha256: j.candidate_sha256, + packs: j.packs, projects, snapshots, protected_paths: [a.work_parent, r.admission, ...toolPins.map(t => t.path)] }; +} +function verifyJourney(local) { + const { record: j, evidence } = local, expected = commandContract(j.cell), rows = evidence["commands.json"]; + assert.ok(Array.isArray(rows) && rows.length === expected.length, "exact ordered C3 core command rows"); + rows.forEach((row, i) => { + const want = expected[i]; fields(row, ["product", "id", "argv", "cwd", "status", "signal", "stdout", "stderr"], "C3 command result"); + for (const key of ["product", "id", "argv", "status"]) agree(row[key], want[key], `command ${key}`); + const parent = j.projects[want.product]; + const cwd = want.scenario === "projects" ? parent : path.join(path.dirname(parent), `${want.product} malformed-skill ü`); + agree(row.cwd, cwd, "fixed cwd"); agree(row.signal, null, "complete command (no signal)"); agree(row.stderr, "", "clean stderr"); + assert.ok(typeof row.stdout === "string" && Buffer.byteLength(row.stdout) <= LIMIT, "bounded stdout"); + if (want.id === "product-help") { assert.ok(row.stdout.length > 50, "product help"); return; } + const v = JSON.parse(row.stdout); agree(v.schema_version, 1, "result schema"); + agree(v.result, want.status === 0 ? "success" : "failure", "result status"); + assert.ok(v.data && typeof v.data === "object" && !Array.isArray(v.data), "result data"); + if (want.author) { + const args = want.argv.slice(want.product === "agentplugins" ? 1 : 0); + agree(v.command, args[0] === "--help" ? "author" : `author.${args[0]}${args[0] === "skills" ? `.${args[1]}` : ""}`, "author operation"); + agree(v.data.revision, j.identity.commit, "engine F"); agree(v.data.engine, "standard-first-slice/1", "engine"); + agree(v.data.authoring_schema_version, 1, "author schema"); agree(v.data.runtime_evidence?.status, "not_evaluated", "offline runtime boundary"); + if (want.lane) agree(v.data.committed, /\/(init|extra-skill)$/.test(want.id), "mutation boundary"); + if (want.id.endsWith("/doctor")) agree(v.data.toolchain?.status, want.lane === "skill" ? "pass" : "not_evaluated", "doctor boundary"); + } else if (want.id === "product-version") agree(v.data[want.product === "agentplugins" ? "version" : "product_version"], j.identity.versions[want.product], "product version"); + }); + // A complete structural transcript still cannot attest process observation, + // installer assessment, npm postinstall, parity or final child quiescence. + throw new Error(MISSING); +} +function readJourney(value) { const local = readJourneyInputs(value); verifyJourney(local); } +function readAcceptance() { throw new Error("C3b required: completed remote E reader is closed; local J and fixture success are not E"); } +function main(args) { + assert.ok(args.length === 2 && args[0] === "--read-local-inputs", "C3a supports only --read-local-inputs REQUEST; public execution and E are closed"); + const requestValue = fileJSON(args[1]), result = readJourneyInputs(requestValue); + return { scope: "authenticated-input-custody-only", cell: result.record.cell, source: result.identity.commit, + journey_sha256: requestValue.journeySha256, release_eligible: false, platform_acceptance: false, attested: false }; +} +// No producer or completed-E CLI can return a success-shaped placeholder. +module.exports = { matrix, commandContract, encodeJourney, decodeJourney, readJourneyInputs, verifyJourney, readJourney, + readAcceptance, request, fileJSON, disjoint, main, LIMIT, SCHEMA, MATRIX_SCHEMA, INTAKE, WORKFLOW, MISSING }; +if (require.main === module) { + try { process.stdout.write(c.encode(main(process.argv.slice(2)))); } + catch (error) { process.stderr.write(`C3 local inputs: ${error.message}\n`); process.exitCode = 1; } +} diff --git a/npm/agentplugins/test/public-authoring-acceptance.test.js b/npm/agentplugins/test/public-authoring-acceptance.test.js new file mode 100644 index 00000000..fa5cf84d --- /dev/null +++ b/npm/agentplugins/test/public-authoring-acceptance.test.js @@ -0,0 +1,202 @@ +"use strict"; +// SYNTHETIC SOURCE CONTROLS ONLY. No npm, native, provider or verifier executes. +const test = require("node:test"), assert = require("node:assert/strict"); +const fs = require("node:fs"), path = require("node:path"), os = require("node:os"), crypto = require("node:crypto"); +const c = require("../scripts/dual-authoring-candidate"), a = require("../scripts/public-authoring-acceptance"); +const s = require("../scripts/stage-authoring-npm"), ic = require("../lib/public-authoring-contract"); +const bridge = require("../scripts/packed-installer-bridge"); +const repo = path.resolve(__dirname, "../../.."), H = n => c.digest(Buffer.from(String(n))); +const write = (file, value) => fs.writeFileSync(file, Buffer.isBuffer(value) ? value : c.encode(value), { mode: 0o600 }); +const hash = file => c.digest(fs.readFileSync(file)); +function fixture(t) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "C3-SYNTHETIC-")); t.diagnostic(`SYNTHETIC ONLY retained: ${root}`); + const dir = name => { const file = path.join(root, name); fs.mkdirSync(file, { mode: 0o700 }); return file; }; + const inputRoot = dir("inputs"), stageRoot = dir("stage"), journeyRoot = dir("journey"), fixtureRoot = dir("fixtures"), work = dir("admission-scratch"), toolRoot = dir("tools"); + const id = { repository: c.REPOSITORY, commit: "a".repeat(40), engine_revision: "a".repeat(40), versions: { agentplugins: "0.1.99", "plugin-kit-ai": "2.0.0" } }; + const artifact = n => ({ run_id: n, run_attempt: 2, artifact_id: n + 100, artifact_sha256: H(n) }); + const input = { schema: ic.INPUT_SCHEMA, identity: id, authoring_mode: ic.MODE, asset_scope: ic.SCOPE, + candidate_sha256: H("candidate"), pair_marker_sha256: H("pair"), products: {}, + preparation: { sha256: H("prep"), artifact: artifact(1) }, + producer: { workflow: ic.WORKFLOW, source: id.commit, run_id: 2, run_attempt: 2 } }; + for (const p of c.PRODUCTS) { + const assets = {}; + for (const target of c.TARGETS) { + const binary = { file: c.executableName(p, target), sha256: H(p + target), size: 32 }; + assets[target] = { file: c.assetName(p, id.versions[p], target), sha256: p === "agentplugins" ? binary.sha256 : H("outer" + p + target), size: 32, binary }; + } + input.products[p] = { tag: (p === "agentplugins" ? "agentplugins-v" : "v") + id.versions[p], manifest_sha256: H("manifest"), checksums_sha256: H("checksums"), assets }; + const projection = ic.projectionBytes(input, p); + input.products[p].manifest_sha256 = c.digest(projection.manifest); input.products[p].checksums_sha256 = c.digest(projection.checksums); + } + const inputBytes = ic.encodeInputs(input); write(path.join(inputRoot, ic.INPUT_FILE), inputBytes); + const source = Object.fromEntries(s.STAGE_ALLOWLIST.map(name => { + const bytes = fs.readFileSync(path.join(repo, name)), mode = fs.statSync(path.join(repo, name)).mode & 0o111 ? "100755" : "100644"; + return [name, { bytes, mode, sha256: c.digest(bytes), git_blob: crypto.createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex") }]; + })); + const pair = s.pairedPackageFiles(source, Object.fromEntries(c.PRODUCTS.map(p => [p, ic.projectionBytes(input, p).manifest])), inputBytes); + const packs = {}; + for (const p of c.PRODUCTS) { + const body = Buffer.from("SYNTHETIC NOT A TARBALL " + p), file = `${ic.PACKAGES[p]}-${id.versions[p]}.tgz`; + write(path.join(stageRoot, file), body); + packs[p] = { file, sha256: c.digest(body), size: body.length, integrity: "sha512-" + crypto.createHash("sha512").update(body).digest("base64"), shasum: crypto.createHash("sha1").update(body).digest("hex") }; + } + const stage = { schema: "dual-authoring-public-stage/v1", identity: id, authoring_mode: ic.MODE, asset_scope: ic.SCOPE, + candidate_sha256: input.candidate_sha256, pair_marker_sha256: input.pair_marker_sha256, + projection_pins: Object.fromEntries(c.PRODUCTS.map(p => [p, { manifest_sha256: input.products[p].manifest_sha256, checksums_sha256: input.products[p].checksums_sha256 }])), + native_inputs: { sha256: c.digest(inputBytes), artifact: artifact(2) }, + wrapper_blobs: Object.fromEntries(Object.entries(source).map(([k, { bytes, ...pin }]) => [k, pin])), + generated: Object.fromEntries(c.PRODUCTS.map(p => [p, Object.fromEntries(Object.entries(pair[p]).map(([k, v]) => [k, c.digest(v)]))])), + packs, tools: Object.fromEntries(["node", "npm", "git", "tar", "gh"].map(k => [k, { version: "synthetic", sha256: H(k) }])), + producer: { workflow: ".github/workflows/agentplugins-npm-publish.yml", source: id.commit, ref: `refs/tags/${input.products.agentplugins.tag}`, run_id: 3, run_attempt: 2 }, + assertions: Object.fromEntries(["authenticated_native_inputs", "exact_preparation_binding", "exact_source_blobs", "exact_generated_closures", "exact_pack_entries_modes_bytes", "both_products_complete", "shared_runtime_bytes_equal", "pack_once", "inputs_unchanged", "no_native_execution", "no_publication"].map(k => [k, true])) }; + const stageBytes = s.encodeStage(stage, inputBytes); write(path.join(stageRoot, "completion.json"), stageBytes); + const tools = {}; + for (const name of ["orchestrator_node", "npm_node", "shim_node", "npm", "go"]) { + const file = name === "orchestrator_node" ? process.execPath : path.join(toolRoot, name); + if (file !== process.execPath) write(file, Buffer.from("SYNTHETIC TOOL " + name)); + tools[name] = { path: file, sha256: hash(file), version: name.endsWith("_node") ? process.version : "1.0.0" }; + } + tools.host = { platform: process.platform, arch: process.arch }; + const projects = {}; + for (const p of c.PRODUCTS) { + const parent = projects[p] = path.join(fixtureRoot, `${p} projects ü`); fs.mkdirSync(parent); + for (const lane of bridge.LANES) { + const dir = path.join(parent, lane); fs.mkdirSync(dir); fs.mkdirSync(path.join(dir, "empty")); + fs.mkdirSync(path.join(dir, "skills/extra-skill"), { recursive: true }); + write(path.join(dir, "plugin.json"), { name: lane }); write(path.join(dir, "skills/extra-skill/SKILL.md"), Buffer.from("SYNTHETIC")); + } + } + const j = { schema: a.SCHEMA, status: "completed", identity: id, authoring_mode: ic.MODE, asset_scope: ic.SCOPE, + candidate_sha256: stage.candidate_sha256, pair_marker_sha256: stage.pair_marker_sha256, native_inputs: stage.native_inputs, + stage: { sha256: c.digest(stageBytes), artifact: artifact(3) }, packs, producer: { ...stage.producer, workflow: a.WORKFLOW, run_id: 4 }, + cell: "linux-amd64/pair-node22", tools, command_contract_sha256: H("pending"), + subjects: Object.fromEntries(c.PRODUCTS.map(p => [p, input.products[p].assets])), projects, evidence: [], + assertions: Object.fromEntries(["fixed_commands", "pair_parity", "projects_preserved", "npm_lifecycle", "cache_process", "production_installer", "children_reaped"].map(k => [k, true])) }; + const commands = a.commandContract(j.cell); + j.command_contract_sha256 = c.digest(c.encode(commands)); + const rows = commands.map(row => { + const args = row.argv.slice(row.product === "agentplugins" && row.author ? 1 : 0); + const data = { revision: id.commit, engine: "standard-first-slice/1", authoring_schema_version: 1, + runtime_evidence: { status: "not_evaluated" }, committed: /\/(init|extra-skill)$/.test(row.id), + toolchain: { status: row.lane === "skill" ? "pass" : "not_evaluated" }, version: id.versions[row.product], product_version: id.versions[row.product] }; + const result = { schema_version: 1, command: args[0] === "--help" ? "author" : `author.${args[0]}${args[0] === "skills" ? `.${args[1]}` : ""}`, result: row.status ? "failure" : "success", data }; + return { product: row.product, id: row.id, argv: row.argv, + cwd: row.scenario === "projects" ? projects[row.product] : path.join(fixtureRoot, `${row.product} malformed-skill ü`), + status: row.status, signal: null, stdout: row.id === "product-help" ? "SYNTHETIC help ".repeat(10) : JSON.stringify(result), stderr: "" }; + }); + const evidence = { "commands.json": rows, "projects.json": Object.fromEntries(c.PRODUCTS.map(p => [p, bridge.snapshot(projects[p])])), + "npm-lifecycle.json": { synthetic: true }, "cache-process.json": { synthetic: true }, "installer.json": { synthetic: true } }; + const save = () => { + j.evidence = Object.entries(evidence).map(([name, value]) => { const file = path.join(journeyRoot, name); write(file, value); return { path: name, size: fs.statSync(file).size, sha256: hash(file) }; }); + write(path.join(journeyRoot, "public-journey.json"), a.encodeJourney(j, inputBytes, stageBytes)); + }; + save(); + const admission = { schema: "authoring-public-local-inputs/v1", selected: { tag: input.products.agentplugins.tag, ref: j.producer.ref, source: id.commit, versions: id.versions }, + workflow_sha: id.commit, input_file: path.join(inputRoot, ic.INPUT_FILE), stage: j.stage, repo, work_parent: work, + stage_root: stageRoot, input_root: inputRoot, journey_root: journeyRoot, fixture_root: fixtureRoot, cell: j.cell, tools, producer: j.producer }; + const admissionPath = path.join(root, "admission.json"); write(admissionPath, admission); + const request = { intake: a.INTAKE, expectedCommit: id.commit, journey: path.join(journeyRoot, "public-journey.json"), + journeySha256: hash(path.join(journeyRoot, "public-journey.json")), admission: admissionPath, admissionSha256: hash(admissionPath), fixtureRoot }; + const stageResult = { root: stageRoot, record: s.decodeStage(stageBytes, inputBytes), subjects: ["completion.json", ...c.PRODUCTS.map(p => packs[p].file)].map(n => ({ file: path.join(stageRoot, n), sha256: hash(path.join(stageRoot, n)) })) }; + // Explicit opaque interface fixture, not a substitute provenance implementation. + const inputSubjects = [{ file: path.join(inputRoot, ic.INPUT_FILE), sha256: c.digest(inputBytes) }]; + for (let i = 0; i < 18; i++) { const file = path.join(inputRoot, `synthetic-subject-${i}`); write(file, Buffer.from(`SYNTHETIC ${i}`)); inputSubjects.push({ file, sha256: hash(file) }); } + const inputResult = { root: inputRoot, input, subjects: inputSubjects }; + return { root, j, inputBytes, stageBytes, evidence, request, admission, stageResult, inputResult, save, + repin() { save(); request.journeySha256 = hash(request.journey); write(admissionPath, admission); request.admissionSha256 = hash(admissionPath); } }; +} +function withReaders(t, f, run) { + const name = require.resolve("../scripts/authoring-native-inputs"), original = require.cache[name].exports; + t.mock.method(s, "readStage", options => { assert.equal(options.stage_sha256, f.j.stage.sha256); return f.stageResult; }); + require.cache[name].exports = { ...original, readInputs(options) { assert.deepEqual(options.artifact, f.j.native_inputs.artifact); return f.inputResult; } }; + try { return run(); } finally { require.cache[name].exports = original; t.mock.restoreAll(); } +} +module.exports = { fixture, withReaders }; +if (require.main === module) { + test("C3 unit closed schemas and immutable pair bindings", t => { + const f = fixture(t), encoded = a.encodeJourney(f.j, f.inputBytes, f.stageBytes); + assert.deepEqual(a.decodeJourney(encoded, f.inputBytes, f.stageBytes), f.j); + const reordered = structuredClone(f.j); + reordered.producer = Object.fromEntries(Object.entries(reordered.producer).reverse()); + assert.deepEqual(a.encodeJourney(reordered, f.inputBytes, f.stageBytes), encoded); + assert.throws(() => a.decodeJourney(c.encode(reordered), f.inputBytes, f.stageBytes), /fixed J field order/); + const cases = [j => { j.extra = true; }, j => { delete j.stage; }, j => { j.identity.commit = "b".repeat(40); }, + j => { j.packs["plugin-kit-ai"].sha256 = H("other"); }, j => { j.stage.artifact.run_attempt++; }, + j => { j.native_inputs.artifact.run_attempt++; }, j => { j.producer.workflow = "other"; }, + j => { j.tools.shim_node.version = "v18.1.0"; }, j => { j.subjects.agentplugins["linux-amd64"].binary.size++; }, + j => { j.evidence.extra = true; }, j => { j.evidence[1].path = "arbitrary.json"; }, j => { j.assertions.extra = true; }]; + cases.forEach((mutate, i) => { const j = structuredClone(f.j); mutate(j); assert.throws(() => a.encodeJourney(j, f.inputBytes, f.stageBytes)); t.diagnostic(`closed mutation ${i}`); }); + for (const body of [Buffer.concat([encoded, Buffer.from(" ")]), Buffer.from('{"schema":1,"schema":2}\n'), Buffer.alloc(a.LIMIT + 1), Buffer.from("[".repeat(17) + "]".repeat(17)), encoded.subarray(0, -5)]) assert.throws(() => a.decodeJourney(body, f.inputBytes, f.stageBytes)); + }); + test("C3 unit stage admission precedes npm and native effects", t => { + const f = fixture(t); + withReaders(t, f, () => { + const local = a.readJourneyInputs(f.request); assert.equal(local.projects.length, 10); + assert.equal(s.readStage.mock.callCount(), 1); + assert.throws(() => a.readJourney(f.request), /C3b required/); + const cache = require.cache[require.resolve("../scripts/authoring-native-inputs")], previous = cache.exports; + cache.exports = { ...previous, readInputs() { throw new Error("I verifier unavailable"); } }; + try { assert.throws(() => a.readJourneyInputs(f.request), /I verifier unavailable/); } + finally { cache.exports = previous; } + t.mock.method(s, "readStage", () => { throw new Error("S verifier unavailable"); }); + assert.throws(() => a.readJourneyInputs(f.request), /S verifier unavailable/); + assert.equal(fs.readdirSync(f.admission.work_parent).length, 0); + }); + }); + test("C3 unit fixed host runtime and command matrix", t => { + assert.equal(a.MATRIX_SCHEMA, "public-wrapper-matrix/v1"); + assert.equal(a.matrix.length, 18); assert.equal(a.matrix.reduce((n, r) => n + r.products.length, 0), 30); + assert.ok(Object.isFrozen(a.matrix) && a.matrix.every(Object.isFrozen)); + for (const row of a.matrix) { + const cmds = a.commandContract(row.key); assert.equal(cmds.length, row.node === 18 ? 55 : 127); + assert.equal(cmds.filter(x => x.id.startsWith("installer/")).length, row.node === 18 ? 0 : 18); + assert.ok(!cmds.some(x => x.argv.includes("--ignore-scripts"))); + } + for (const key of [null, "linux-amd64/pair-node18", "linux-amd64/pair-node23"]) assert.throws(() => a.commandContract(key)); + t.diagnostic("18 fixed host cells; 30 product runtimes; no execution"); + }); + test("C3 unit journey results parity and preservation", t => { + const f = fixture(t); withReaders(t, f, () => { + const local = a.readJourneyInputs(f.request); + assert.throws(() => a.verifyJourney(local), /C3b required/); + for (const [name, mutate] of [ ["drop row", x => x.evidence["commands.json"].pop()], + ["arbitrary argv", x => x.evidence["commands.json"][0].argv.push("--extra")], + ["cwd", x => { x.evidence["commands.json"][0].cwd = f.root; }], + ["signal", x => { x.evidence["commands.json"][0].signal = "SIGTERM"; }], + ["false result", x => { x.evidence["commands.json"][0].stdout = "{}"; }]]) { + const bad = structuredClone(local); mutate(bad); assert.throws(() => a.verifyJourney(bad), e => !e.message.includes("C3b required")); t.diagnostic(name); + } + const file = path.join(f.j.projects.agentplugins, "skill/plugin.json"); fs.chmodSync(file, 0o400); + assert.throws(() => a.readJourneyInputs(f.request), /original project trees/); + }); + }); + test("C3 unit production installer evidence is independently required", t => { + const f = fixture(t); withReaders(t, f, () => { + const local = a.readJourneyInputs(f.request); + assert.throws(() => a.verifyJourney(local), /reviewed public installer result validator and whole-descendant observer/); + assert.throws(() => bridge.publishSeal(f.request, path.join(f.root, "must-not-exist")), /C3b required/); + assert.equal(fs.existsSync(path.join(f.root, "must-not-exist")), false); + }); + }); + test("C3 unit terminal closure rejects missing mismatched or stale evidence", t => { + const f = fixture(t); withReaders(t, f, () => { + for (const file of [f.request.journey, f.request.admission, f.inputResult.subjects[1].file, + f.stageResult.subjects[2].file, f.j.tools.go.path, path.join(f.admission.journey_root, "commands.json")]) { + const bytes = fs.readFileSync(file); fs.appendFileSync(file, "changed"); assert.throws(() => a.readJourneyInputs(f.request)); write(file, bytes); t.diagnostic(path.basename(file)); + } + t.mock.method(s, "readStage", () => { const bad = structuredClone(f.stageResult); bad.record.producer.run_attempt++; return bad; }); + assert.throws(() => a.readJourneyInputs(f.request), /authenticated S/); + }); + }); + test("C3 unit same invocation bridge cannot authenticate remote E", t => { + const f = fixture(t); assert.throws(() => a.readAcceptance(f.request), /completed remote E reader is closed/); + assert.throws(() => a.request({ ...f.request, expectedCommit: f.request.expectedCommit + "\n" })); + for (const extra of [{ authenticated: true }, { completed: true }, { allowPublic: true }]) assert.throws(() => a.request({ ...f.request, ...extra })); + assert.throws(() => a.main(["--produce-journey", f.request.journey]), /only --read-local-inputs/); + }); + test("C3 unit legacy fixtures cannot qualify authentic acceptance", t => { + const f = fixture(t); + for (const intake of ["public-fixture/v1", "public-fixture/v2", "private"]) assert.throws(() => a.request({ ...f.request, intake })); + for (const schema of ["dual-authoring-public-native/v1", "dual-authoring-public-native/v2", "authoring-public-packed/v1"]) assert.throws(() => a.encodeJourney({ ...f.j, schema }, f.inputBytes, f.stageBytes)); + }); +} diff --git a/scripts/check-packed-ci.py b/scripts/check-packed-ci.py index eba2248d..fb113b79 100644 --- a/scripts/check-packed-ci.py +++ b/scripts/check-packed-ci.py @@ -350,8 +350,120 @@ def check_public(root, sha, require_valid_add=False, require_summary=True): release_eligible=False, platform_acceptance=False, attested=False, signed_promotion=False, public_eligible=False), 'public summary scope/gap') +AUTHENTIC = 'public-authenticated/v1' +AUTHENTIC_SEAL = 'packed-installer-bridge/public-authenticated/v1' + + +def authenticated_options(options, sha): + require(type(options) is dict and set(options) == {'request', 'go', 'node', 'modCache'}, 'closed authenticated options') + request = options['request'] + require(type(request) is dict and set(request) == {'intake', 'expectedCommit', 'journey', 'journeySha256', + 'admission', 'admissionSha256', 'fixtureRoot'}, 'closed authenticated request') + require(request['intake'] == AUTHENTIC and request['expectedCommit'] == sha and + re.fullmatch('[0-9a-f]{40}', sha) and sha != '0' * 40, 'authentic source identity') + for key in ('journeySha256', 'admissionSha256'): + require(type(request[key]) is str and re.fullmatch('[0-9a-f]{64}', request[key]) and request[key] != '0' * 64, 'authentic pin') + for name in ('go', 'node', 'modCache'): + file = Path(options[name]) + require(file.is_absolute() and file.resolve(strict=True) == file, 'canonical authenticated tool/cache') + for key in ('journey', 'admission'): + require(Path(request[key]).lstat().st_size <= 1024 * 1024, 'bounded authentic input') + require(digest(request[key]) == request[key + 'Sha256'], 'changed authentic input') + return request + + +def authentic_read(path): + require(Path(path).lstat().st_size <= 1024 * 1024, 'bounded authentic JSON') + value = read(path) + require(data(path) == (json.dumps(value, indent=2, ensure_ascii=False) + '\n').encode(), 'canonical authentic JSON') + return value + + +def authenticated_verify(node, argv): + # Fixed Node reader re-admits S/I; saved stdout, booleans and fixture TAP do + # not authenticate a local J. This process cannot execute public products. + import subprocess + repo = Path(__file__).resolve().parent.parent + bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' + result = subprocess.run([str(node), str(bridge), *map(str, argv)], cwd=repo, + env={'PATH': '/usr/local/bin:/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8'}, + capture_output=True, timeout=1200) + require(result.returncode == 0 and result.stderr == b'', 'authenticated reader failed: ' + result.stderr.decode(errors='replace')[:4096]) + require(len(result.stdout) <= 32 * 1024 * 1024, 'bounded authenticated reader output') + return json.loads(result.stdout) + + +def authenticated_plans(root, sha, inputs, sealed_pin, fixture_root): + """Complementary injected plans only; does not authenticate J or remote E.""" + result = read(root / 'results/completion.json'); false_claims(result) + require(result['kind'] == 'packed-generated-existing-injected-installer-planner' and result['commit'] == sha and + result['config_sha256'] == sealed_pin, 'authentic planner identity') + require(result['inputs'] == inputs == json.loads(data(root / 'logs/post-verify.stdout')), 'authentic post-plan seal') + projects = inputs['projects'] + require(Counter((x['product'], x['lane']) for x in projects) == Counter({(p, l): 1 for p in PRODUCTS for l in LANES}) and + len({x['source'] for x in projects}) == 10, 'same ten authentic projects') + for row in projects: + require(row['source'] == str(Path(fixture_root) / (row['product'] + ' projects ü') / row['lane']), 'original project path') + unchanged_snapshots(inputs); plans(result) + + +def check_authenticated(root, sha, require_summary=True, require_completed_e=False): + require(not require_completed_e, 'completed E cannot use local J or fixture success; C3b E reader required') + run = authentic_read(root / 'authenticated-run.json'); false_claims(run) + require(set(run) == {'schema', 'head', 'options', 'tools', *CLAIMS} and + run['schema'] == 'public-authenticated-packed-run/v1' and run['head'] == sha, 'authentic run schema') + options = run['options']; request = authenticated_options(options, sha) + sealed_path = root / 'bridge-config/sealed.json'; sealed = read(sealed_path); false_claims(sealed) + require(set(sealed) == {'schema', 'request', 'verifier_sha256', 'helper_sha256', 'reader_sha256', 'inputs', *CLAIMS} and + sealed['schema'] == AUTHENTIC_SEAL and sealed['request'] == request == read(root / 'bridge-config/request.json'), 'authentic seal schema') + repo = Path(__file__).resolve().parent.parent; bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' + for key, file in [('verifier_sha256', bridge), ('helper_sha256', bridge.with_name('dual-authoring-candidate.js')), + ('reader_sha256', bridge.with_name('public-authoring-acceptance.js'))]: + require(sealed[key] == digest(file), 'authentic verifier pin') + for key in ('go', 'node'): + require(run['tools'][key] == dict(path=options[key], sha256=digest(options[key])), 'authentic tool pin') + node, go = options['node'], options['go']; sealed_pin = digest(sealed_path) + fresh = authenticated_verify(node, ['verify', sealed_path, sealed_pin, sha]) + require(fresh == sealed['inputs'], 'fresh authenticated seal') + public = fresh['public_inputs'] + require(public['schema'] == 'authoring-public-local-inputs/v1' and public['cell'] == 'linux-amd64/pair-node22' and + public['journey_sha256'] == request['journeySha256'] and public['admission_sha256'] == request['admissionSha256'], 'local custody boundary') + false_claims(public, ('signed_promotion', 'public_eligible')); require(public['qualification'] is None, 'no local qualification') + for key, tool in [('node', 'orchestrator_node'), ('go', 'go')]: + require(public['tools'][tool]['path'] == options[key] and public['tools'][tool]['sha256'] == digest(options[key]), 'journey planner tools') + commands = {'head': ['/usr/bin/git', 'rev-parse', 'HEAD'], + 'clean': ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'], + 'terminal-clean': ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'], + 'seal': [node, str(bridge), 'authenticated-seal', str(root / 'bridge-config/request.json'), str(sealed_path)], + 'post-verify': [node, str(bridge), 'verify', str(sealed_path), sealed_pin, sha]} + for name, flag in [('discovery', '-list'), ('planner', '-run')]: + commands[name] = [go, 'test', '-p=2', '-tags=packedci', '-json', + *([] if name == 'discovery' else ['-count=1', '-timeout=20m']), flag, REGEX, PACKAGE_PATH] + for name, command in commands.items(): + phase = read(root / 'logs' / (name + '.json')) + require(type(phase['exit']) is int and phase['exit'] == 0 and phase['argv'] == command and phase['cwd'] == str(repo), 'authentic phase ' + name) + require(all(phase['env'].get(k) == v for k, v in dict(GOPROXY='off', GOSUMDB='off', GOVCS='*:off', GOENV='off', GOTOOLCHAIN='local').items()), 'authentic offline planner environment') + require(not any(k in phase['env'] for k in ('GOFLAGS', 'NODE_OPTIONS', 'AGENTPLUGINS_STAGED_TEST_CHILD')), 'authentic inherited override') + if name in ('discovery', 'planner'): + expected = dict(UAP_PACKED_INSTALLER_NODE=node, UAP_PACKED_INSTALLER_CONFIG=str(sealed_path), + UAP_PACKED_INSTALLER_CONFIG_SHA256=sealed_pin, UAP_PACKED_INSTALLER_COMMIT=sha, + UAP_PACKED_INSTALLER_OUTPUT=str(root / 'results/completion.json')) + require({k: v for k, v in phase['env'].items() if k.startswith('UAP_PACKED_INSTALLER_')} == expected, 'five exact planner variables') + require(data(root / 'logs' / (name + '.stderr')) == b'', 'authentic phase stderr') + log = lambda name: data(root / 'logs' / (name + '.stdout')).decode() + go_discovery(log('discovery')); go_results(log('planner')) + require(log('seal').strip() == sealed_pin and log('head').strip() == sha and log('clean') == log('terminal-clean') == '', 'authentic source/seal logs') + authenticated_plans(root, sha, fresh, sealed_pin, request['fixtureRoot']) + if require_summary: + require(authentic_read(root / 'summary.json') == dict(status='passed', scope='local-authenticated-inputs-and-injected-planner', + intake=AUTHENTIC, head=sha, projects=10, plans=30, release_eligible=False, platform_acceptance=False, + attested=False, signed_promotion=False, public_eligible=False, qualification=None), 'authentic summary scope') + + if __name__ == '__main__': - if len(sys.argv) == 4 and sys.argv[1] == '--public': + if len(sys.argv) == 4 and sys.argv[1] == '--public-authenticated': + check_authenticated(Path(sys.argv[2]), sys.argv[3]) + elif len(sys.argv) == 4 and sys.argv[1] == '--public': check_public(Path(sys.argv[2]), sys.argv[3]) elif len(sys.argv) == 3: check(Path(sys.argv[1]), sys.argv[2]) diff --git a/scripts/run-packed-ci.py b/scripts/run-packed-ci.py index b8e3badc..ee193b7b 100644 --- a/scripts/run-packed-ci.py +++ b/scripts/run-packed-ci.py @@ -203,8 +203,64 @@ def run(name, argv, extra=None): proof.check_public(root, sha) +def authenticated_main(root, sha, options_path): + """Same invocation local J intake. Never generate, copy or replay projects.""" + repo = Path(__file__).resolve().parent.parent + options = proof.authentic_read(options_path) + request = proof.authenticated_options(options, sha) + proof.require(root.is_absolute() and root.resolve() == root and not root.exists(), 'new canonical authentic output') + proof.require(platform.system() == 'Linux' and platform.machine() == 'x86_64', 'native Linux amd64 required') + admission = proof.authentic_read(request['admission']) + protected = [repo, Path(options_path), Path(request['admission']), Path(request['journey']), Path(request['fixtureRoot']), + *[Path(options[k]) for k in ('go', 'node', 'modCache')], + *[Path(admission[k]) for k in ('repo', 'work_parent', 'stage_root', 'input_root', 'journey_root', 'fixture_root')]] + for other in protected: + proof.require(other.is_absolute() and other.resolve() == other and not root.is_relative_to(other) and not other.is_relative_to(root), 'authentic output overlaps input') + # Real reader admission happens before even the output directory exists. + # C3a's unavailable result validator is an explicit error here, before Go. + inputs = proof.authenticated_verify(options['node'], ['authenticated-options', options_path]) + proof.require(inputs['repo'] == str(repo), 'authenticated source checkout') + for key, name in [('node', 'orchestrator_node'), ('go', 'go')]: + tool = inputs['public_inputs']['tools'][name] + proof.require(tool['path'] == options[key] and tool['sha256'] == proof.digest(options[key]), 'authenticated planner tool') + proof.unchanged_snapshots(inputs) + go, node, modules = [Path(options[k]) for k in ('go', 'node', 'modCache')] + root.mkdir(mode=0o700); (root / 'logs').mkdir(); (root / 'bridge-config').mkdir() + env = private_env(root / 'planner', go, node, repo, modules) + env.update(GOPROXY='off', GOSUMDB='off', GOVCS='*:off') + def run(name, argv, extra=None): + argv = list(map(str, argv)); record = dict(argv=argv, cwd=str(repo), env=dict(env, **(extra or {})), exit=None) + started = time.monotonic() + try: + with (root / 'logs' / (name + '.stdout')).open('x') as out, (root / 'logs' / (name + '.stderr')).open('x') as err: + record['exit'] = subprocess.run(argv, cwd=repo, env=record['env'], stdout=out, stderr=err, timeout=1200).returncode + finally: + record['seconds'] = round(time.monotonic() - started, 3); write(root / 'logs' / (name + '.json'), record) + proof.require(record['exit'] == 0, 'authenticated phase failed: ' + name) + return (root / 'logs' / (name + '.stdout')).read_text() + proof.require(run('head', ['/usr/bin/git', 'rev-parse', 'HEAD']).strip() == sha, 'wrong checkout') + proof.require(run('clean', ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all']) == '', 'dirty checkout') + write(root / 'authenticated-run.json', dict(schema='public-authenticated-packed-run/v1', head=sha, options=options, + tools={k: dict(path=str(p), sha256=proof.digest(p)) for k, p in dict(go=go, node=node).items()}, + release_eligible=False, platform_acceptance=False, attested=False)) + request_path = root / 'bridge-config/request.json'; write(request_path, request) + sealed = root / 'bridge-config/sealed.json'; bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' + printed = run('seal', [node, bridge, 'authenticated-seal', request_path, sealed]).strip() + proof.require(printed == proof.digest(sealed), 'authenticated seal pin') + proof.require(proof.read(sealed)['inputs'] == inputs, 'original intake changed before planner') + planner(root, sha, go, node, sealed, printed, run) + proof.require(run('terminal-clean', ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all']) == '', 'checkout changed') + proof.check_authenticated(root, sha, require_summary=False) + write(root / 'summary.json', dict(status='passed', scope='local-authenticated-inputs-and-injected-planner', + intake=proof.AUTHENTIC, head=sha, projects=10, plans=30, release_eligible=False, platform_acceptance=False, + attested=False, signed_promotion=False, public_eligible=False, qualification=None)) + proof.check_authenticated(root, sha) + + if __name__ == '__main__': - if len(sys.argv) == 5 and sys.argv[1] == '--public': + if len(sys.argv) == 5 and sys.argv[1] == '--public-authenticated': + authenticated_main(Path(sys.argv[2]), sys.argv[3], Path(sys.argv[4])) + elif len(sys.argv) == 5 and sys.argv[1] == '--public': public_main(Path(sys.argv[2]), sys.argv[3], Path(sys.argv[4])) elif len(sys.argv) == 3: main(Path(sys.argv[1]), sys.argv[2]) diff --git a/scripts/test_packed_ci.py b/scripts/test_packed_ci.py index 321bf451..8941100c 100644 --- a/scripts/test_packed_ci.py +++ b/scripts/test_packed_ci.py @@ -387,4 +387,127 @@ def test_dependencies_must_all_succeed(self): with self.assertRaises(ValueError): w.results(bad) + + +class C3AuthenticatedControls(unittest.TestCase): + def test_closed_intake_before_output(self): + from unittest.mock import patch + root = Path(tempfile.mkdtemp(prefix='C3-runner-SYNTHETIC-')) + options = root / 'options.json'; output = root / 'must-not-exist' + for value in ({}, dict(request={'intake': 'public-fixture/v2'}, go='/unused', node='/unused', modCache='/unused')): + options.write_text(json.dumps(value, indent=2) + '\n') + with self.assertRaises(ValueError): r.authenticated_main(output, 'a' * 40, options) + self.assertFalse(output.exists()) + # The external authenticated reader fails deterministically; no process, + # real provider, planner or output creation may follow its failure. + tool = root / 'tool'; tool.write_text('SYNTHETIC') + modules = root / 'modules'; modules.mkdir() + admission = root / 'admission.json'; journey = root / 'journey.json'; journey.write_text('{}\n') + dirs = {} + for name in ('repo', 'work_parent', 'stage_root', 'input_root', 'journey_root', 'fixture_root'): + directory = root / name; directory.mkdir(); dirs[name] = str(directory) + admission.write_text(json.dumps(dirs, indent=2) + '\n') + request = dict(intake=p.AUTHENTIC, expectedCommit='a' * 40, journey=str(journey), journeySha256=p.digest(journey), + admission=str(admission), admissionSha256=p.digest(admission), fixtureRoot=dirs['fixture_root']) + value = dict(request=request, go=str(tool), node=str(tool), modCache=str(modules)) + options.write_text(json.dumps(value, indent=2) + '\n') + with patch.object(r.proof, 'authenticated_verify', side_effect=ValueError('missing reviewed installer/observer')) as reader, \ + patch.object(r, 'planner', side_effect=AssertionError('planner effect')) as planner: + with self.assertRaisesRegex(ValueError, 'missing reviewed'): r.authenticated_main(output, 'a' * 40, options) + reader.assert_called_once(); planner.assert_not_called(); self.assertFalse(output.exists()) + + def test_exact_thirty_plans_and_post_seal(self): + root = Path(tempfile.mkdtemp(prefix='C3-plans-SYNTHETIC-')); (root / 'results').mkdir(); (root / 'logs').mkdir() + fixture = root / 'original-projects'; fixture.mkdir() + entries = [dict(path='.', mode=fixture.stat().st_mode & 0o777, kind='directory')] + inputs = dict(projects=[dict(product=product, lane=lane, source=str(fixture / (product + ' projects ü') / lane)) + for product in p.PRODUCTS for lane in p.LANES], snapshots=[dict(root=str(fixture), entries=entries, + sha256=p.hashlib.sha256((json.dumps(entries, indent=2) + '\n').encode()).hexdigest())]) + record = dict(plan_fixture(), kind='packed-generated-existing-injected-installer-planner', commit='a' * 40, + config_sha256='b' * 64, inputs=inputs, release_eligible=False, platform_acceptance=False, attested=False) + terminal = root / 'results/completion.json'; post = root / 'logs/post-verify.stdout' + put = lambda file, value: file.write_text(json.dumps(value, indent=2) + '\n') + put(terminal, record); put(post, inputs) + # Exercise the entire distinct terminal branch with a SYNTHETIC opaque + # readback, then corrupt real retained logs. No subprocess is launched. + from unittest.mock import patch + tool = root / 'tool'; tool.write_text('SYNTHETIC TOOL') + modules = root / 'modules'; modules.mkdir() + journey = root / 'J.json'; admission = root / 'admission.json' + put(journey, {}); put(admission, {}) + request = dict(intake=p.AUTHENTIC, expectedCommit='a' * 40, journey=str(journey), journeySha256=p.digest(journey), + admission=str(admission), admissionSha256=p.digest(admission), fixtureRoot=str(fixture)) + options = dict(request=request, go=str(tool), node=str(tool), modCache=str(modules)) + claims = dict(release_eligible=False, platform_acceptance=False, attested=False) + pin_tool = dict(path=str(tool), sha256=p.digest(tool)) + inputs['public_inputs'] = dict(schema='authoring-public-local-inputs/v1', cell='linux-amd64/pair-node22', + journey_sha256=request['journeySha256'], admission_sha256=request['admissionSha256'], + tools=dict(go=pin_tool, orchestrator_node=pin_tool), signed_promotion=False, public_eligible=False, qualification=None) + put(root / 'authenticated-run.json', dict(schema='public-authenticated-packed-run/v1', head='a' * 40, + options=options, tools=dict(go=pin_tool, node=pin_tool), **claims)) + bridge = ROOT / 'npm/agentplugins/scripts/packed-installer-bridge.js' + config = root / 'bridge-config'; config.mkdir(); sealed = config / 'sealed.json' + put(config / 'request.json', request) + put(sealed, dict(schema=p.AUTHENTIC_SEAL, request=request, inputs=inputs, + verifier_sha256=p.digest(bridge), helper_sha256=p.digest(bridge.with_name('dual-authoring-candidate.js')), + reader_sha256=p.digest(bridge.with_name('public-authoring-acceptance.js')), **claims)) + pin = p.digest(sealed); record['config_sha256'] = pin + put(terminal, record); put(post, inputs) + commands = {'head': ['/usr/bin/git', 'rev-parse', 'HEAD'], + 'clean': ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'], + 'terminal-clean': ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'], + 'seal': [str(tool), str(bridge), 'authenticated-seal', str(config / 'request.json'), str(sealed)], + 'post-verify': [str(tool), str(bridge), 'verify', str(sealed), pin, 'a' * 40]} + for name, flag in [('discovery', '-list'), ('planner', '-run')]: + commands[name] = [str(tool), 'test', '-p=2', '-tags=packedci', '-json', + *([] if name == 'discovery' else ['-count=1', '-timeout=20m']), flag, p.REGEX, p.PACKAGE_PATH] + for name, argv in commands.items(): + env = dict(GOPROXY='off', GOSUMDB='off', GOVCS='*:off', GOENV='off', GOTOOLCHAIN='local') + if name in ('discovery', 'planner'): + env.update(UAP_PACKED_INSTALLER_NODE=str(tool), UAP_PACKED_INSTALLER_CONFIG=str(sealed), + UAP_PACKED_INSTALLER_CONFIG_SHA256=pin, UAP_PACKED_INSTALLER_COMMIT='a' * 40, + UAP_PACKED_INSTALLER_OUTPUT=str(terminal)) + put(root / 'logs' / (name + '.json'), dict(argv=argv, cwd=str(ROOT), env=env, exit=0)) + (root / 'logs' / (name + '.stderr')).write_text('') + logs = {'head': 'a' * 40 + '\n', 'clean': '', 'terminal-clean': '', 'seal': pin + '\n', + 'discovery': go([dict(Action='output', Output=p.NAME + '\n'), dict(Action='pass')]), + 'planner': go([dict(Action=action, Test=name) for name in [p.NAME] + + [p.NAME + '/' + product + '/' + lane for product in p.PRODUCTS for lane in p.LANES] + for action in ('run', 'pass')] + [dict(Action='pass')])} + for name, text in logs.items(): (root / 'logs' / (name + '.stdout')).write_text(text) + put(root / 'summary.json', dict(status='passed', scope='local-authenticated-inputs-and-injected-planner', + intake=p.AUTHENTIC, head='a' * 40, projects=10, plans=30, **claims, + signed_promotion=False, public_eligible=False, qualification=None)) + with patch.object(p, 'authenticated_verify', return_value=copy.deepcopy(inputs)) as reader: + p.check_authenticated(root, 'a' * 40); reader.assert_called_once() + for file, key, value in [('logs/planner.json', 'argv', []), ('logs/post-verify.json', 'exit', 1), + ('summary.json', 'scope', 'completed-authenticated-E'), ('bridge-config/sealed.json', 'reader_sha256', '0' * 64)]: + target = root / file; old = p.read(target); put(target, dict(old, **{key: value})) + with self.subTest(file=file), self.assertRaises(ValueError): p.check_authenticated(root, 'a' * 40) + put(target, old) + with patch.object(p, 'authenticated_verify', side_effect=ValueError('late custody cancellation')): + with self.assertRaisesRegex(ValueError, 'late custody cancellation'): p.check_authenticated(root, 'a' * 40) + record['config_sha256'] = 'b' * 64; put(terminal, record) + check = lambda: p.authenticated_plans(root, 'a' * 40, inputs, 'b' * 64, str(fixture)) + check() + for name, mutate in [('29 plans', lambda v: v['plans'].pop()), + ('duplicate plans', lambda v: v['plans'].__setitem__(1, v['plans'][0])), + ('wrong seal', lambda v: v.update(config_sha256='c' * 64)), + ('changed original project', lambda v: v['inputs']['projects'][0].update(source='/different/project'))]: + bad = copy.deepcopy(record); mutate(bad); put(terminal, bad) + with self.subTest(case=name), self.assertRaises(ValueError): check() + put(terminal, record); put(post, {}) + with self.assertRaisesRegex(ValueError, 'post-plan seal'): check() + put(post, inputs); (fixture / 'extra-empty').mkdir() + with self.assertRaisesRegex(ValueError, 'sealed tree changed'): check() + + def test_completed_e_cannot_use_fixture_success(self): + root = Path(tempfile.mkdtemp(prefix='C3-E-closed-SYNTHETIC-')) + for schema in ('public-fixture/v1', 'public-fixture/v2', p.AUTHENTIC): + (root / 'summary.json').write_text(json.dumps(dict(status='passed', intake=schema, plans=30, projects=10))) + with self.subTest(intake=schema), self.assertRaisesRegex(ValueError, 'completed E cannot use'): + p.check_authenticated(root, 'a' * 40, require_completed_e=True) + with self.assertRaises(FileNotFoundError): p.check_authenticated(root, 'a' * 40) + + if __name__ == '__main__': unittest.main() From 210c77b5fbb7d8792774cbceb1dbb8676b5feb45 Mon Sep 17 00:00:00 2001 From: iliya Date: Wed, 9 Sep 2026 21:33:51 +0000 Subject: [PATCH 2/2] fix(authoring): reject untrusted authenticated reader runtime Refs #232, #216, #208. --- .../scripts/packed-installer-bridge.md | 14 ++++--- scripts/check-packed-ci.py | 10 ++++- scripts/run-packed-ci.py | 4 +- scripts/test_packed_ci.py | 42 +++++++++++++++++-- 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/npm/agentplugins/scripts/packed-installer-bridge.md b/npm/agentplugins/scripts/packed-installer-bridge.md index c58e4a24..8a1b7bd5 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.md +++ b/npm/agentplugins/scripts/packed-installer-bridge.md @@ -343,11 +343,15 @@ The bridge has separate `authenticated-options`, `authenticated-intake` and `authenticated-seal` commands. Its new seal pins the reader source in addition to the existing verifier/helper and repeats admission on `verify`. The runner's `--public-authenticated ROOT F OPTIONS` accepts exactly `request,go,node,modCache`. -It calls authentic intake before creating output, then preserves the existing -Linux planner commands and five environment variables. The terminal checker -re-enters the real Node reader, checks ten original project paths, exact thirty -plan tuples and post-plan snapshots. Summary claims remain false, with null -qualification and scope `local-authenticated-inputs-and-injected-planner`. +Python authentic runner, checker and direct reader entrypoints unconditionally +reject with `missing independently provisioned trusted controller; C3b capability required` +before interpreter subprocesses, planner effects, output creation or authenticated +success. Receipt-selected `node` and self-supplied tool/reader hashes are not +independent interpreter authority; no receipt or environment override opens this +boundary. Prepared intake, Linux planner commands, five environment variables and +terminal validations remain for C3b, covered only by explicitly synthetic gate +mocks. C3b must independently provision the controller and make full public +execution work; this closed subcheckpoint is not its completion. A local J/bridge, successful fixture TAP or injected planner cannot stand for E. Positive tests explicitly mock custody interfaces and, for bridge-only seal diff --git a/scripts/check-packed-ci.py b/scripts/check-packed-ci.py index fb113b79..a8aca890 100644 --- a/scripts/check-packed-ci.py +++ b/scripts/check-packed-ci.py @@ -379,9 +379,14 @@ def authentic_read(path): return value +def require_authenticated_controller(): + # Receipt-selected executables and their self-supplied hashes are not authority. + raise ValueError('missing independently provisioned trusted controller; C3b capability required') + + def authenticated_verify(node, argv): - # Fixed Node reader re-admits S/I; saved stdout, booleans and fixture TAP do - # not authenticate a local J. This process cannot execute public products. + require_authenticated_controller() + # Prepared reader path; C3b must independently bind its controller before use. import subprocess repo = Path(__file__).resolve().parent.parent bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' @@ -409,6 +414,7 @@ def authenticated_plans(root, sha, inputs, sealed_pin, fixture_root): def check_authenticated(root, sha, require_summary=True, require_completed_e=False): require(not require_completed_e, 'completed E cannot use local J or fixture success; C3b E reader required') + require_authenticated_controller() run = authentic_read(root / 'authenticated-run.json'); false_claims(run) require(set(run) == {'schema', 'head', 'options', 'tools', *CLAIMS} and run['schema'] == 'public-authenticated-packed-run/v1' and run['head'] == sha, 'authentic run schema') diff --git a/scripts/run-packed-ci.py b/scripts/run-packed-ci.py index ee193b7b..751ddefc 100644 --- a/scripts/run-packed-ci.py +++ b/scripts/run-packed-ci.py @@ -205,6 +205,7 @@ def run(name, argv, extra=None): def authenticated_main(root, sha, options_path): """Same invocation local J intake. Never generate, copy or replay projects.""" + proof.require_authenticated_controller() repo = Path(__file__).resolve().parent.parent options = proof.authentic_read(options_path) request = proof.authenticated_options(options, sha) @@ -216,8 +217,7 @@ def authenticated_main(root, sha, options_path): *[Path(admission[k]) for k in ('repo', 'work_parent', 'stage_root', 'input_root', 'journey_root', 'fixture_root')]] for other in protected: proof.require(other.is_absolute() and other.resolve() == other and not root.is_relative_to(other) and not other.is_relative_to(root), 'authentic output overlaps input') - # Real reader admission happens before even the output directory exists. - # C3a's unavailable result validator is an explicit error here, before Go. + # Prepared admission remains behind the unconditional Python controller gate. inputs = proof.authenticated_verify(options['node'], ['authenticated-options', options_path]) proof.require(inputs['repo'] == str(repo), 'authenticated source checkout') for key, name in [('node', 'orchestrator_node'), ('go', 'go')]: diff --git a/scripts/test_packed_ci.py b/scripts/test_packed_ci.py index 8941100c..1f66d55a 100644 --- a/scripts/test_packed_ci.py +++ b/scripts/test_packed_ci.py @@ -6,6 +6,7 @@ from pathlib import Path import tempfile import unittest +from unittest.mock import patch ROOT = Path(__file__).resolve().parent.parent @@ -390,7 +391,9 @@ def test_dependencies_must_all_succeed(self): class C3AuthenticatedControls(unittest.TestCase): - def test_closed_intake_before_output(self): + # SYNTHETIC gate mock exercises retained prepared intake validations only. + @patch.object(r.proof, 'require_authenticated_controller', return_value=None) + def test_closed_intake_before_output(self, synthetic_gate): from unittest.mock import patch root = Path(tempfile.mkdtemp(prefix='C3-runner-SYNTHETIC-')) options = root / 'options.json'; output = root / 'must-not-exist' @@ -416,7 +419,9 @@ def test_closed_intake_before_output(self): with self.assertRaisesRegex(ValueError, 'missing reviewed'): r.authenticated_main(output, 'a' * 40, options) reader.assert_called_once(); planner.assert_not_called(); self.assertFalse(output.exists()) - def test_exact_thirty_plans_and_post_seal(self): + # SYNTHETIC gate mock exercises retained prepared terminal validations only. + @patch.object(p, 'require_authenticated_controller', return_value=None) + def test_exact_thirty_plans_and_post_seal(self, synthetic_gate): root = Path(tempfile.mkdtemp(prefix='C3-plans-SYNTHETIC-')); (root / 'results').mkdir(); (root / 'logs').mkdir() fixture = root / 'original-projects'; fixture.mkdir() entries = [dict(path='.', mode=fixture.stat().st_mode & 0o777, kind='directory')] @@ -501,13 +506,44 @@ def test_exact_thirty_plans_and_post_seal(self): put(post, inputs); (fixture / 'extra-empty').mkdir() with self.assertRaisesRegex(ValueError, 'sealed tree changed'): check() + def test_substituted_node_rejected_before_authenticated_effects(self): + import subprocess + root = Path(tempfile.mkdtemp(prefix='C3-controller-SYNTHETIC-')) + tool = root / 'substitute-node'; tool.write_text('SYNTHETIC TOOL') + options = dict(request=dict(intake=p.AUTHENTIC, expectedCommit='a' * 40), + go=str(tool), node=str(tool), modCache=str(root)) + options_path = root / 'options.json' + options_path.write_text(json.dumps(options, indent=2) + '\n') + receipt = dict(schema='public-authenticated-packed-run/v1', head='a' * 40, + options=options, tools=dict(node=dict(path=str(tool), sha256=p.digest(tool)))) + (root / 'authenticated-run.json').write_text(json.dumps(receipt, indent=2) + '\n') + output = root / 'must-not-exist' + before = {str(f): f.read_bytes() for f in root.iterdir()} + # Actual entrypoints and rejecting gate: no test-only gate mock here. + with patch.object(subprocess, 'run', side_effect=AssertionError('subprocess effect')) as child, \ + patch.object(r, 'planner', side_effect=AssertionError('planner effect')) as planner, \ + patch.object(r, 'write', side_effect=AssertionError('output effect')) as write: + for name, call in ( + ('runner', lambda: r.authenticated_main(output, 'a' * 40, options_path)), + ('checker', lambda: p.check_authenticated(root, 'a' * 40)), + ('checker-before-summary', lambda: p.check_authenticated(root, 'a' * 40, require_summary=False)), + ('direct-reader', lambda: p.authenticated_verify(options['node'], ['authenticated-options', options_path])), + ): + with self.subTest(entrypoint=name), self.assertRaisesRegex(ValueError, + 'missing independently provisioned trusted controller; C3b capability required'): + call() + child.assert_not_called(); planner.assert_not_called(); write.assert_not_called() + self.assertFalse(output.exists()) + self.assertEqual(before, {str(f): f.read_bytes() for f in root.iterdir()}) + def test_completed_e_cannot_use_fixture_success(self): root = Path(tempfile.mkdtemp(prefix='C3-E-closed-SYNTHETIC-')) for schema in ('public-fixture/v1', 'public-fixture/v2', p.AUTHENTIC): (root / 'summary.json').write_text(json.dumps(dict(status='passed', intake=schema, plans=30, projects=10))) with self.subTest(intake=schema), self.assertRaisesRegex(ValueError, 'completed E cannot use'): p.check_authenticated(root, 'a' * 40, require_completed_e=True) - with self.assertRaises(FileNotFoundError): p.check_authenticated(root, 'a' * 40) + with self.assertRaisesRegex(ValueError, 'missing independently provisioned trusted controller'): + p.check_authenticated(root, 'a' * 40) if __name__ == '__main__': unittest.main()