From 2818a9674307e251cfd221f985a1e9ea68461720 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 8 Sep 2026 17:17:28 +0000 Subject: [PATCH 1/8] feat(authoring): add frozen native qualification producer Refs #203, #199, #208. Keep production observation and native admission fail-closed pending actual execution and independent review. --- .github/workflows/agentplugins-release.yml | 34 + .github/workflows/authoring-frozen-native.yml | 166 +++++ .../cmd/agentplugins/release_workflow_test.go | 100 +++ .../scripts/authoring-native-qualification.js | 588 ++++++++++++++++++ .../scripts/authoring-promotion.js | 49 +- npm/agentplugins/scripts/authoring-release.js | 58 +- .../authoring-native-qualification.test.js | 461 ++++++++++++++ 7 files changed, 1413 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/authoring-frozen-native.yml create mode 100644 npm/agentplugins/scripts/authoring-native-qualification.js create mode 100644 npm/agentplugins/test/authoring-native-qualification.test.js diff --git a/.github/workflows/agentplugins-release.yml b/.github/workflows/agentplugins-release.yml index 85fab3a4..2b6a8799 100644 --- a/.github/workflows/agentplugins-release.yml +++ b/.github/workflows/agentplugins-release.yml @@ -490,11 +490,45 @@ jobs: verifyAuthoringRelease(options); fs.appendFileSync(process.env.GITHUB_ENV, `PAIRED_OUTPUT=${root}\n`); NODE + - name: Bind preparation invocation outside unchanged frozen input bytes + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const n = require('./npm/agentplugins/scripts/authoring-native-qualification'); + const root = process.env.PAIRED_OUTPUT; + const pairBody = c.readFile(path.join(root, 'pair-prepared.json')); + const pair = JSON.parse(pairBody); + const pins = { identity: pair.identity, candidate_sha256: pair.candidate_sha256, + pair_marker_sha256: c.digest(pairBody), products: pair.products }; + // Input provenance only: no future qualification or npm tarball hash. + // The external provider artifact ID/digest is acquired after upload. + // Keep candidate-identity.json and all eighteen input subjects intact. + const before = require('./npm/agentplugins/scripts/authoring-release') + .verifyProjectedPair(root, pins).subjects.map(x => [x.file, x.sha256]); + n.writePreparation(root, pins, { + repository: process.env.GITHUB_REPOSITORY, + workflow: '.github/workflows/agentplugins-release.yml', + source: process.env.SOURCE_SHA, + workflow_sha: process.env.WORKFLOW_SHA, + run_id: Number(process.env.GITHUB_RUN_ID), + run_attempt: Number(process.env.GITHUB_RUN_ATTEMPT) + }); + const after = require('./npm/agentplugins/scripts/authoring-release') + .verifyProjectedPair(root, pins).subjects.map(x => [x.file, x.sha256]); + require('node:assert/strict').deepEqual(after, before); + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, + 'Preparation receipt binds input provenance only. Native and public acceptance remain required.\n'); + NODE - name: Upload candidate identity and both prepared projections together uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 with: name: authoring-preparation-${{ inputs.source_sha }} path: | + ${{ env.PAIRED_OUTPUT }}/preparation-run.json ${{ env.PAIRED_OUTPUT }}/candidate-identity.json ${{ env.PAIRED_OUTPUT }}/candidate/candidate.json ${{ env.PAIRED_OUTPUT }}/pair-prepared.json diff --git a/.github/workflows/authoring-frozen-native.yml b/.github/workflows/authoring-frozen-native.yml new file mode 100644 index 00000000..5c350217 --- /dev/null +++ b/.github/workflows/authoring-frozen-native.yml @@ -0,0 +1,166 @@ +name: Frozen Authoring Native Observations + +# N1 only. No automatic event, source build, promotion, signing or publication. +# Local closed receipts are not authenticated provider admission. Whole-OS +# observation and real installer services remain required execution prerequisites. +on: + workflow_dispatch: + inputs: + source_sha: + description: Exact prepared source and producer workflow revision + required: true + type: string + preparation_run: + description: Exact completed preparation run ID + required: true + type: string + preparation_attempt: + description: Exact completed preparation attempt + required: true + type: string + artifact_id: + description: Exact preparation artifact ID, never a name or latest selector + required: true + type: string + artifact_sha256: + description: Independently retained provider ZIP digest + required: true + type: string + receipt_sha256: + description: Independently retained preparation receipt digest + required: true + type: string + pair_sha256: + description: Independently retained pair marker digest + required: true + type: string + candidate_sha256: + description: Independently retained candidate digest + required: true + type: string + projection_pins: + description: Exact two-product manifest and checksum pins as bounded JSON + required: true + type: string + host_go_sha256: + description: Independent Linux amd64 Go 1.25.13 executable digest + required: true + type: string +permissions: + contents: read +concurrency: + group: frozen-native-${{ inputs.source_sha }}-${{ inputs.artifact_id }} + cancel-in-progress: false +jobs: + linux-amd64: + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-24.04 + timeout-minutes: 25 + permissions: + contents: read + actions: read + env: + PATH: /usr/local/bin:/usr/bin:/bin + SOURCE_SHA: ${{ inputs.source_sha }} + WORKFLOW_SHA: ${{ github.sha }} + PREPARATION_RUN: ${{ inputs.preparation_run }} + PREPARATION_ATTEMPT: ${{ inputs.preparation_attempt }} + ARTIFACT_ID: ${{ inputs.artifact_id }} + ARTIFACT_SHA256: ${{ inputs.artifact_sha256 }} + RECEIPT_SHA256: ${{ inputs.receipt_sha256 }} + PAIR_SHA256: ${{ inputs.pair_sha256 }} + CANDIDATE_SHA256: ${{ inputs.candidate_sha256 }} + PROJECTION_PINS: ${{ inputs.projection_pins }} + HOST_GO_SHA256: ${{ inputs.host_go_sha256 }} + steps: + - name: Validate exact read-only selection before acquisition + shell: bash + run: | + set -euo pipefail + test "${GITHUB_REPOSITORY}" = "777genius/universal-agent-plugins" + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0{40}$ ]] + test "${SOURCE_SHA}" = "${WORKFLOW_SHA}" + for value in "${PREPARATION_RUN}" "${PREPARATION_ATTEMPT}" "${ARTIFACT_ID}"; do + [[ "${value}" =~ ^[1-9][0-9]{0,14}$ ]] + done + test "${PREPARATION_ATTEMPT}" -le 1000 + for value in "${ARTIFACT_SHA256}" "${RECEIPT_SHA256}" "${PAIR_SHA256}" "${CANDIDATE_SHA256}" "${HOST_GO_SHA256}"; do + [[ "${value}" =~ ^[0-9a-f]{64}$ && ! "${value}" =~ ^0{64}$ ]] + done + test "${#PROJECTION_PINS}" -le 1024 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.source_sha }} + persist-credentials: false + - name: Check source without changing history + run: test "$(git rev-parse HEAD)" = "${SOURCE_SHA}" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.21.1 + package-manager-cache: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: 1.25.13 + cache: false + - name: Inspect exact preparation attempt and artifact metadata + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + node <<'NODE' + const p = require('./npm/agentplugins/scripts/authoring-promotion'); + // Read-only consistency check, not accepted cryptographic custody. + p.inspectArtifact({ run_id: Number(process.env.PREPARATION_RUN), + run_attempt: Number(process.env.PREPARATION_ATTEMPT), + artifact_id: Number(process.env.ARTIFACT_ID), artifact_sha256: process.env.ARTIFACT_SHA256 }, + '.github/workflows/agentplugins-release.yml', process.env.SOURCE_SHA, process.env.RUNNER_TEMP); + NODE + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + artifact-ids: ${{ inputs.artifact_id }} + run-id: ${{ inputs.preparation_run }} + github-token: ${{ github.token }} + repository: 777genius/universal-agent-plugins + merge-multiple: true + path: ${{ runner.temp }}/frozen-inputs + - name: Inspect host Go and execute the fixed pair journey + shell: bash + run: | + set -euo pipefail + export HOST_GO="$(command -v go)" + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const root = path.join(process.env.RUNNER_TEMP, 'frozen-inputs'); + const pair = c.readFile(path.join(root, 'pair-prepared.json')); + if (c.digest(pair) !== process.env.PAIR_SHA256) throw Error('pair pin'); + const identity = JSON.parse(pair).identity; + if (identity.commit !== process.env.SOURCE_SHA) throw Error('source pin'); + const producer = { repository: process.env.GITHUB_REPOSITORY, + workflow: '.github/workflows/authoring-frozen-native.yml', source: process.env.SOURCE_SHA, + workflow_sha: process.env.WORKFLOW_SHA, run_id: Number(process.env.GITHUB_RUN_ID), + run_attempt: Number(process.env.GITHUB_RUN_ATTEMPT) }; + const options = { root, pins: { identity, candidate_sha256: process.env.CANDIDATE_SHA256, + pair_marker_sha256: process.env.PAIR_SHA256, products: JSON.parse(process.env.PROJECTION_PINS) }, + preparation: { sha256: process.env.RECEIPT_SHA256, producer: { ...producer, + workflow: '.github/workflows/agentplugins-release.yml', run_id: Number(process.env.PREPARATION_RUN), + run_attempt: Number(process.env.PREPARATION_ATTEMPT) } }, producer, + go: process.env.HOST_GO, go_sha256: process.env.HOST_GO_SHA256, + output: path.join(process.env.RUNNER_TEMP, 'frozen-native-evidence'), + work: path.join(process.env.RUNNER_TEMP, 'frozen-native-work') }; + fs.writeFileSync(path.join(process.env.RUNNER_TEMP, 'native-options.json'), c.encode(options), { flag: 'wx', mode: 0o600 }); + NODE + node npm/agentplugins/scripts/authoring-native-qualification.js "${RUNNER_TEMP}/native-options.json" + - name: Retain bounded observations or failure diagnostics + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: frozen-native-linux-amd64-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/frozen-native-evidence/* + if-no-files-found: error + retention-days: 7 +# N2 must replace metadata-plus-download with same-checked-ZIP intake before +# provider admission. This job does not claim that a supplied boolean verifies +# the attempt. N1 terminals bind embedded attempts and reject stale preparation. +# No OS observer is provisioned here: production retains failure diagnostics. diff --git a/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go b/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go index fda5cdcd..b2e41e12 100644 --- a/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go +++ b/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go @@ -493,3 +493,103 @@ func TestReleasePairedPromotionShellSyntax(t *testing.T) { } } } + +func TestFrozenNativeReadOnlyWorkflowContract(t *testing.T) { + w := readProducerWorkflow(t, "authoring-frozen-native.yml") + if len(w.Jobs) != 1 || len(w.On.Dispatch.Inputs) != 10 || len(w.On.Run.Workflows) != 0 { + t.Fatal("N1 requires one explicit Linux lane and ten bounded identity inputs") + } + if len(w.Permissions) != 1 || w.Permissions["contents"] != "read" || w.Concurrency.Cancel { + t.Fatal("native producer must preserve read-only permissions and owned cancellation") + } + job, ok := w.Jobs["linux-amd64"] + if !ok || job.If != "${{ github.event_name == 'workflow_dispatch' }}" || job.Needs != nil || job.Environment != nil { + t.Fatal("native route must remain independently dispatched without protected effects") + } + if len(job.Permissions) != 2 || job.Permissions["actions"] != "read" || job.Permissions["contents"] != "read" { + t.Fatal("only contents/actions read is permitted") + } + if job.Env["PATH"] != "/usr/local/bin:/usr/bin:/bin" || len(job.Steps) == 0 || job.Steps[0].Uses != "" { + t.Fatal("guarded PATH and pre-acquisition validation required") + } + var scripts strings.Builder + downloads, uploads := 0, 0 + for _, step := range job.Steps { + scripts.WriteString(step.Run) + if step.Run != "" { + command := exec.Command("/bin/bash", "-n") + command.Env = []string{"PATH=/usr/local/bin:/usr/bin:/bin"} + command.Stdin = strings.NewReader(step.Run) + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("%s: %v %s", step.Name, err, output) + } + } + if token, ok := step.Env["GH_TOKEN"]; ok && (step.Name != "Inspect exact preparation attempt and artifact metadata" || token != "${{ github.token }}") { + t.Fatal("read token escaped acquisition") + } + if strings.HasPrefix(step.Uses, "actions/download-artifact@") { + downloads++ + for k, v := range map[string]string{"artifact-ids": "${{ inputs.artifact_id }}", "run-id": "${{ inputs.preparation_run }}", "repository": "777genius/universal-agent-plugins"} { + if step.With[k] != v { + t.Fatalf("download lost exact %s", k) + } + } + if _, ok := step.With["name"]; ok { + t.Fatal("artifact name must not select frozen bytes") + } + } + if strings.HasPrefix(step.Uses, "actions/upload-artifact@") { + uploads++ + if step.With["path"] != "${{ runner.temp }}/frozen-native-evidence/*" { + t.Fatal("upload must exclude binaries, client state and compilation caches") + } + } + if step.Uses != "" && !regexp.MustCompile(`^actions/(checkout|setup-node|setup-go|download-artifact|upload-artifact)@[0-9a-f]{40}$`).MatchString(step.Uses) { + t.Fatalf("unreviewed action %s", step.Uses) + } + } + if downloads != 1 || uploads != 1 { + t.Fatal("one exact input acquisition and one diagnostic/evidence upload required") + } + body := scripts.String() + for _, forbidden := range []string{"go build", "go test", "go mod", "stageCandidate", "frozenCandidate", "npm install", "npm publish", "attestation verify", "git push", "--auth-complete", "--accept-security-risk", "strace", "ptrace"} { + if strings.Contains(body, forbidden) { + t.Fatalf("native route reaches forbidden operation %s", forbidden) + } + } + for _, required := range []string{"p.inspectArtifact", "run_attempt: Number(process.env.PREPARATION_ATTEMPT)", "authoring-native-qualification.js", "go_sha256: process.env.HOST_GO_SHA256", "pair_marker_sha256: process.env.PAIR_SHA256"} { + if !strings.Contains(body, required) { + t.Fatalf("native route lacks %s", required) + } + } +} + +func TestFrozenPreparationReceiptOutsideProjectionBytes(t *testing.T) { + job := readProducerWorkflow(t, "agentplugins-release.yml").Jobs["paired-preparation"] + receipts, uploads := 0, 0 + for _, step := range job.Steps { + if step.Name == "Bind preparation invocation outside unchanged frozen input bytes" { + receipts++ + for _, required := range []string{"n.writePreparation(root, pins", "workflow_sha: process.env.WORKFLOW_SHA", "run_attempt: Number(process.env.GITHUB_RUN_ATTEMPT)", "deepEqual(after, before)"} { + if !strings.Contains(step.Run, required) { + t.Fatalf("receipt does not bind %s", required) + } + } + for _, forbidden := range []string{"qualification_sha256", "tarball_sha256", "attest", "stageCandidate({"} { + if strings.Contains(step.Run, forbidden) { + t.Fatalf("provenance-only receipt includes %s", forbidden) + } + } + } + if strings.HasPrefix(step.Uses, "actions/upload-artifact@") { + uploads++ + paths, _ := step.With["path"].(string) + if !strings.Contains(paths, "${{ env.PAIRED_OUTPUT }}/preparation-run.json\n") || strings.Contains(paths, "/agentplugins/preparation-run") { + t.Fatal("receipt must be a sibling of unchanged eight-file projections") + } + } + } + if receipts != 1 || uploads != 1 { + t.Fatal("one byte-bound receipt before the existing upload required") + } +} diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js new file mode 100644 index 00000000..0646d047 --- /dev/null +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -0,0 +1,588 @@ +#!/usr/bin/env node +"use strict"; + +// N1 observes one frozen pair. This is not provider admission or qualification +// signing. No promotion caller accepts these local records in this checkpoint. +const fs = require("node:fs"); +const path = require("node:path"); +const os = require("node:os"); +const cp = require("node:child_process"); +const assert = require("node:assert/strict"); +const c = require("./dual-authoring-candidate"); +const { verifyProjectedPair } = require("./authoring-release"); +const { buildInfo } = require("./stage-dual-authoring-candidate"); +const { lifecycleResult } = require("./platform-proof"); +const SCHEMA = "authoring-frozen-native/v1"; +const WORKFLOW = ".github/workflows/authoring-frozen-native.yml"; +const PREPARATION = ".github/workflows/agentplugins-release.yml"; +const TARGET = "linux-amd64"; +const MODE = "release-cli-contract-v1"; +const LIMIT = 1024 * 1024; +const CASES = Object.freeze(["skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"]); +const LEAVES = Object.freeze(["author.capabilities", "author.compat", "author.doctor", "author.init", "author.inspect", + "author.skills.init", "author.skills.validate", "author.test", "author.validate"]); +const FILES = Object.freeze(["transcripts.json", "trees.json", "build-info.json", "preservation.json", "preparation.json", "host.json"]); +const TOP = ["schema", "lane", "identity", "candidate_sha256", "pair_marker_sha256", "projection_pins", "subject", + "peer_subject", "preparation", "producer", "host", "tools", "assertions", "evidence"]; +const fail = message => { throw new Error(message); }; +const exact = (a, b, label) => assert.deepEqual(a, b, label); +function sha(s, length = 64) { + assert.equal(typeof s, "string"); + assert.match(s, new RegExp(`^[0-9a-f]{${length}}$`)); + assert.ok(!/^0+$/.test(s)); return s; +} +function positive(n) { assert.ok(Number.isSafeInteger(n) && n > 0 && n <= Number.MAX_SAFE_INTEGER); return n; } +// Tokenize only to reject duplicate object keys and excessive nesting; JSON.parse +// still owns grammar validation. This accepts Go's whitespace/field order without +// accepting duplicate keys or ambiguous UTF-8 from a successful child process. +function jsonDocument(text) { + assert.equal(typeof text, "string"); assert.ok(Buffer.byteLength(text) <= LIMIT); + const tokens = text.match(/"(?:[^"\\\x00-\x1f]|\\.)*"|[{}\[\]:,]|true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g) || []; + const stack = []; let expectKey = false; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if (token === "{" || token === "[") { + stack.push(token === "{" ? new Set() : null); assert.ok(stack.length <= 64); expectKey = token === "{"; + } else if (token === "}" || token === "]") { stack.pop(); expectKey = false; } + else if (token === ",") expectKey = stack[stack.length - 1] !== null; + else if (expectKey && token.startsWith('"')) { + const key = JSON.parse(token), keys = stack[stack.length - 1]; + assert.ok(keys && !keys.has(key), "duplicate JSON key"); keys.add(key); expectKey = false; + } + } + return JSON.parse(text); +} +function canonical(bytes, limit = LIMIT) { + assert.ok(Buffer.isBuffer(bytes) && bytes.length > 0 && bytes.length <= limit, "bounded JSON required"); + const v = JSON.parse(bytes.toString("utf8")); + exact(bytes, c.encode(v), "canonical UTF-8 JSON, no duplicate keys"); return v; +} +function invocation(v, workflow, source) { + c.keys(v, ["repository", "workflow", "source", "workflow_sha", "run_id", "run_attempt"], "invocation"); + exact([v.repository, v.workflow, v.source, v.workflow_sha], [c.REPOSITORY, workflow, source, source]); + sha(source, 40); positive(v.run_id); positive(v.run_attempt); assert.ok(v.run_attempt <= 1000); + return v; +} +function subject(manifest, product) { + return { product, target: TARGET, ...manifest.products[product].assets[TARGET] }; +} +function preparationRecord(root, pins, producer) { + const verified = verifyProjectedPair(root, pins); + invocation(producer, PREPARATION, pins.identity.commit); + return { schema: "authoring-preparation-run/v1", identity: pins.identity, producer, + subjects: verified.subjects.map(s => ({ file: path.relative(root, s.file).split(path.sep).join("/"), + ...c.metadata(c.readFile(s.file)) })) }; +} +function writePreparation(root, pins, producer) { + const v = preparationRecord(root, pins, producer); + const file = path.join(root, "preparation-run.json"); + fs.writeFileSync(file, c.encode(v), { flag: "wx", mode: 0o444 }); + return { file, ...c.metadata(c.readFile(file)) }; +} +function readPreparation(root, pins, expected) { + c.keys(expected, ["sha256", "producer"], "preparation pin"); sha(expected.sha256); + const bytes = c.readFile(path.join(root, "preparation-run.json"), LIMIT); + exact(c.digest(bytes), expected.sha256); + const value = canonical(bytes); + exact(value, preparationRecord(root, pins, expected.producer), "all eighteen preparation input subjects"); + return value; +} +// Only retained owned trees are read; bounds include empty files/directories and +// modes. No recursive cleanup, special files, aliases or silent exclusions. +function tree(root) { + c.safeDirectory(root); + const result = []; let total = 0; + function walk(rel) { + assert.ok(result.length < 4096, "tree entry bound"); + const file = rel === "." ? root : path.join(root, rel), st = fs.lstatSync(file); + assert.equal(st.mode & 0o7000, 0); + assert.ok(!st.isSymbolicLink() && (st.isDirectory() || st.isFile() && st.nlink === 1)); + const item = { path: rel, mode: st.mode & 0o777, kind: st.isDirectory() ? "directory" : "file" }; + if (st.isFile()) { + total += st.size; assert.ok(total <= 16 * LIMIT, "tree byte bound"); + // c.readFile intentionally rejects empty candidate assets. An empty + // generated file is still part of the tree identity. + const bytes = st.size ? c.readFile(file, 16 * LIMIT) : fs.readFileSync(file, { flag: fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW }); + Object.assign(item, c.metadata(bytes)); + } + result.push(item); + if (st.isDirectory()) for (const name of fs.readdirSync(file).sort()) { + assert.ok(!/[\\\x00-\x1f]/.test(name)); walk(rel === "." ? name : `${rel}/${name}`); + } + } + walk("."); return result; +} +function inputCustody(root, subjects) { + return [...subjects.map(x => x.file), path.join(root, "preparation-run.json")].map(file => { + const st = fs.lstatSync(file); + return { file: path.relative(root, file), mode: st.mode, dev: st.dev, ino: st.ino, + links: st.nlink, size: st.size, mtime: st.mtimeMs, ctime: st.ctimeMs }; + }); +} +function context(root) { + fs.mkdirSync(root, { mode: 0o700 }); + for (const name of ["home", "tmp", "state", "config", "cache", "data", "empty-bin", "projects"]) + fs.mkdirSync(path.join(root, name), { mode: 0o700 }); + // Credential-free allowlist. Even workflow tokens and caller proxy settings + // are discarded. PATH contains neither product nor ambient toolchains. + return { root, env: { PATH: path.join(root, "empty-bin"), HOME: path.join(root, "home"), + TMPDIR: path.join(root, "tmp"), XDG_CONFIG_HOME: path.join(root, "config"), + XDG_CACHE_HOME: path.join(root, "cache"), XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), AGENTPLUGINS_HOME: path.join(root, "state"), + GOENV: "off", GOTOOLCHAIN: "local", GOPROXY: "off", GOSUMDB: "off", GOVCS: "*:off", + GOMAXPROCS: "2", LC_ALL: "C", TZ: "UTC", NO_COLOR: "1" } }; +} +// Linux owned process group supervision. Escaped descendants/whole-OS network +// observation are NOT proved by kill(0) or PATH traps; see observationGate. +function subprocess(file, argv, ctx, signal, installer = false) { + return new Promise((resolve, reject) => { + if (signal?.aborted) return reject(new Error("cancelled before process")); + const child = cp.spawn(file, argv, { cwd: path.join(ctx.root, "projects"), env: ctx.env, + shell: false, detached: true, stdio: ["ignore", "pipe", "pipe"] }); + let stdout = [], stderr = [], bytes = 0, problem, closed = false; + const stop = reason => { + problem ||= reason; + if (child.pid && !closed) { + try { process.kill(-child.pid, "SIGKILL"); } + catch (e) { if (e.code !== "ESRCH") problem = `owned-child cleanup denied: ${e.code}`; } + } + }; + const timer = setTimeout(() => stop("timeout"), installer ? 120000 : 15000); + const cancel = () => stop("cancelled"); signal?.addEventListener("abort", cancel, { once: true }); + for (const [stream, chunks] of [[child.stdout, stdout], [child.stderr, stderr]]) stream.on("data", b => { + bytes += b.length; + if (bytes > LIMIT) stop("output flood"); else chunks.push(b); + }); + child.on("error", e => { problem = `subprocess failed: ${e.code}`; }); + child.on("close", (status, sig) => { + closed = true; clearTimeout(timer); signal?.removeEventListener("abort", cancel); + if (child.pid) { + try { process.kill(-child.pid, 0); problem ||= "owned descendants survived"; process.kill(-child.pid, "SIGKILL"); } + catch (e) { if (e.code !== "ESRCH") problem ||= `cleanup uncertain: ${e.code}`; } + } + if (problem || sig) return reject(new Error(problem || `signal ${sig}`)); + try { + const decoder = new TextDecoder("utf-8", { fatal: true }); + resolve({ status, stdout: decoder.decode(Buffer.concat(stdout)), stderr: decoder.decode(Buffer.concat(stderr)) }); + } catch { reject(new Error("invalid UTF-8 subprocess output")); } + }); + }); +} +function initArgs(lane) { + const args = ["init", lane, "--template", lane.startsWith("hybrid-") ? "hybrid" : lane]; + if (lane.startsWith("hybrid-")) args.push("--mcp-template", lane.endsWith("remote") ? "mcp-remote" : "mcp-stdio"); + if (lane.endsWith("remote")) args.push("--url", "https://docs.example.com/mcp"); + if (lane.endsWith("stdio")) args.push("--runtime", "node"); + return args; +} +// Closed inventory is owned by producer and reader, never supplied by callers. +function commands(product) { + const rows = []; + const add = (id, args, status = 0, author = true, lane = null) => rows.push({ id, args: + [...(author && product === "agentplugins" ? ["author"] : []), ...args, "--format=json"], status, author, lane }); + add("product-version", ["version"], 0, false); + rows.push({ id: "product-help", args: ["--help"], status: 0, author: false, lane: null }); + add("engine-version", ["version"]); add("author-help", ["--help"]); add("capabilities", ["capabilities"]); + for (const lane of CASES) { + add(`${lane}/init`, initArgs(lane), 0, true, lane); + add(`${lane}/extra-skill`, ["skills", "init", "extra-skill", lane, "--description", "Use for extra documentation requests"], 0, true, lane); + for (const verb of ["skills validate", "validate", "inspect", "test", "compat", "doctor"]) { + const args = [...verb.split(" "), lane]; + if (verb === "compat") args.push("--target", "claude,codex"); + add(`${lane}/${verb.replace(" ", "-")}`, args, verb === "doctor" && lane !== "skill" ? 1 : 0, true, lane); + } + add(`${lane}/existing`, initArgs(lane), 1, true, lane); + } + add("malformed-skill", ["validate", "skill"], 1, true, "skill"); + add("invalid-flag", ["init", "invalid-destination", "--force"], 2); + add("missing-template-input", ["init", "missing-destination", "--template", "mcp-stdio"], 2); + add("installer-flag", ["validate", "skill", "--scope=user"], 2, true, "skill"); + if (product === "plugin-kit-ai") add("retired-v1", ["update", "--all"], 2, false); + return rows; +} +function envelope(row, expected) { + exact(row.status, expected.status, `${expected.id} exit`); exact(row.stderr, "", `${expected.id} stderr`); + if (expected.id === "product-help") { + assert.ok(row.stdout.length > 50 && row.stdout.length < LIMIT); return null; + } + const v = jsonDocument(row.stdout); // one complete document; trailing prompts fail + exact(v.schema_version, 1); exact(v.result, expected.status === 0 ? "success" : "failure"); + assert.ok(v.data && !Array.isArray(v.data)); return v; +} +function authorResult(row, spec, product, identity) { + const r = envelope(row, spec); + if (!r) return; + if (!spec.author) { + if (spec.id === "product-version") { + exact(r.command, product === "agentplugins" ? "version" : "author.version", "product version command identifier"); + exact(r.data[product === "agentplugins" ? "version" : "product_version"], identity.versions[product]); + } + if (spec.id === "retired-v1") exact(r.data.error?.code, "v1_operation_unavailable"); + return; + } + const args = spec.args.slice(product === "agentplugins" ? 1 : 0); + const operation = args[0] === "--help" ? "author" : `author.${args[0]}${args[0] === "skills" ? `.${args[1]}` : ""}`; + exact(r.command, operation, "author command identifier"); + const d = r.data; + exact(d.revision, identity.commit); exact(d.engine, "standard-first-slice/1"); + exact(d.authoring_schema_version, 1); exact(d.engine_version, d.engine); + exact(d.runtime_evidence.status, "not_evaluated"); + if (spec.id === "author-help" || spec.id === "capabilities") exact(d.commands, LEAVES); + if (spec.id === "engine-version") { exact(d.product, product); exact(d.product_version, identity.versions[product]); return; } + if (spec.id === "capabilities") { assert.ok(d.capabilities); return; } + if (spec.id === "author-help") { assert.ok(d.help.use.startsWith(product === "agentplugins" ? "agentplugins author" : "plugin-kit-ai")); return; } + const mutation = /\/(init|extra-skill)$/.test(spec.id); + if (mutation) { exact(d.committed, true); exact(d.effects.committed, true); } + else exact(d.committed, false); + if (!spec.lane || /\/(existing)$/.test(spec.id) || spec.id === "installer-flag") return; + assert.match(d.identity.tree_digest, /^sha256:[0-9a-f]{64}$/); + exact(d.identity.read_profile, "packageview-local-linux-v1"); + assert.ok(Array.isArray(d.profiles) && d.profiles.length > 0, "embedded Skills profile"); + for (const profile of d.profiles) { assert.ok(profile.id && profile.revision); assert.match(profile.digest, /^sha256:[0-9a-f]{64}$/); } + assert.ok(d.profiles.some(p => p.id === "agent-skills/2026-09-06" && + p.revision === "69ef37e9424c0a7ea9dd2293b559e43ec8176379" && + p.digest === "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220"), "pinned Skills rules"); + exact(d.loadability.status, "pass"); + exact(d.normative_conformance.status, spec.id === "malformed-skill" ? "fail" : "pass"); + exact(d.authoring_readiness.status, spec.id === "malformed-skill" ? "fail" : "pass"); + exact(d.release_policy.status, "not_evaluated"); + if (spec.id === "malformed-skill") { + assert.ok(d.components.some(x => x.type === "skill" && x.status === "pass"), "valid Skills survive malformed peer"); + assert.ok(d.findings.some(x => x.severity === "error")); + } + if (spec.id.endsWith("/doctor")) exact(d.toolchain.status, spec.lane === "skill" ? "pass" : "not_evaluated"); + if (spec.id.endsWith("/compat")) { + exact(d.clients.map(x => x.client_id).sort(), ["claude", "codex"]); + exact(d.compatibility.status, "pass"); + } + if (spec.id.endsWith("/inspect")) { + exact(d.inspection.name, spec.lane); + exact(d.inspection.schema, "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"); + const types = d.inspection.components.map(x => x.type).sort(); + const skills = spec.lane === "skill" || spec.lane.startsWith("hybrid-") ? 2 : 1; + exact(types, [...Array(skills).fill("skill"), ...(spec.lane === "skill" ? [] : [spec.lane.endsWith("remote") ? "mcp_streamable-http" : "mcp_stdio"])].sort()); + } +} +function normalized(row) { + const r = jsonDocument(row.stdout); + delete r.data.product; delete r.data.product_version; + if (r.data.help) r.data.help.use = r.data.help.use.replace(/^agentplugins author|^plugin-kit-ai/, ""); + return { status: row.status, stderr: row.stderr, result: r }; +} +function treeShape(entries) { + assert.ok(Array.isArray(entries) && entries.length > 0 && entries.length <= 4096); + const names = new Set(); let bytes = 0; + for (const v of entries) { + c.keys(v, v.kind === "directory" ? ["path", "mode", "kind"] : ["path", "mode", "kind", "sha256", "size"], "tree entry"); + assert.ok(typeof v.path === "string" && v.path.length <= 512 && !names.has(v.path)); + assert.ok(v.path === "." || /^(?!\.\.?($|\/))[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(v.path)); + assert.ok(!v.path.split("/").includes("..")); names.add(v.path); + assert.ok(Number.isInteger(v.mode) && v.mode >= 0 && v.mode <= 511); + assert.ok(v.kind === "file" || v.kind === "directory"); + if (v.kind === "file") { sha(v.sha256); assert.ok(Number.isSafeInteger(v.size) && v.size >= 0); bytes += v.size; } + } + assert.ok(names.has(".") && bytes <= 16 * LIMIT); + for (const v of entries) if (v.path !== ".") { + const parent = path.posix.dirname(v.path); + assert.ok(entries.some(x => x.path === parent && x.kind === "directory"), "tree parent closure"); + } +} +function projectEvidence(root, lane) { + const files = tree(root); + const manifest = JSON.parse(c.readFile(path.join(root, "plugin.json"), LIMIT)); + const mcp = lane === "skill" ? null : JSON.parse(c.readFile(path.join(root, "mcp.json"), LIMIT)); + const lock = lane.endsWith("stdio") ? JSON.parse(c.readFile(path.join(root, "package-lock.json"), LIMIT)) : null; + const pkg = lock ? JSON.parse(c.readFile(path.join(root, "package.json"), LIMIT)) : null; + const documents = Object.fromEntries(["plugin.json", ...(mcp ? ["mcp.json"] : []), ...(lock ? ["package.json", "package-lock.json"] : [])] + .map(n => [n, c.readFile(path.join(root, n), LIMIT).toString("utf8")])); + return { files, manifest, mcp, lock, package: pkg, documents }; +} +function checkProject(value, lane) { + c.keys(value, ["files", "manifest", "mcp", "lock", "package", "documents"], "project evidence"); + treeShape(value.files); assert.ok(value.files.length > 3); + const documents = { "plugin.json": value.manifest, ...(value.mcp ? { "mcp.json": value.mcp } : {}), + ...(value.lock ? { "package.json": value.package, "package-lock.json": value.lock } : {}) }; + c.keys(value.documents, Object.keys(documents), "exact project documents"); + for (const [name, parsed] of Object.entries(documents)) { + exact(jsonDocument(value.documents[name]), parsed); + const pin = value.files.find(x => x.path === name); assert.ok(pin); + exact([pin.sha256, pin.size], [c.digest(Buffer.from(value.documents[name])), Buffer.byteLength(value.documents[name])]); + } + const names = value.files.map(x => x.path); + exact(new Set(names).size, names.length); + for (const name of names) assert.ok(!/(^|\/)(plugin\.yaml|hooks|\.codex-plugin|\.mcp\.json|\.app\.json|node_modules)(\/|$)/.test(name)); + for (const name of ["plugin.json", "README.md", ".gitignore", "skills/extra-skill/SKILL.md"]) assert.ok(names.includes(name), name); + exact(value.manifest.$schema, "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"); + exact(value.manifest.name, lane); exact(value.manifest.version, "0.1.0"); + assert.equal(value.manifest.author, undefined); assert.equal(value.manifest.license, undefined); + if (lane === "skill") exact(value.mcp, null); + else { + exact(value.mcp.$schema, "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"); + const servers = Object.values(value.mcp.mcpServers); exact(servers.length, 1); + exact(servers[0].type, lane.endsWith("remote") ? "streamable-http" : "stdio"); + } + if (lane.endsWith("stdio")) { + const version = value.package.dependencies["@modelcontextprotocol/sdk"]; + exact(version, "1.30.0"); + exact(value.lock.packages["node_modules/@modelcontextprotocol/sdk"].version, version); + exact(value.lock.packages["node_modules/@modelcontextprotocol/sdk"].integrity, + "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="); + } else { exact(value.lock, null); exact(value.package, null); } +} +function installationCommands() { + return [ + ...["skill", "mcp-remote", "hybrid-stdio"].map(lane => ({ id: `dry-run/${lane}`, args: ["add", `/${lane}`, "--target=codex", "--dry-run", "--format=json"] })), + { id: "add", args: ["add", "/skill", "--target=codex", "--format=json"] }, + { id: "info", args: ["info", "skill", "--target=codex", "--format=json"] }, + { id: "update", args: ["update", "skill", "--target=codex", "--format=json"] }, + { id: "remove", args: ["remove", "skill", "--target=codex", "--format=json"] }, + { id: "list", args: ["list", "--format=json"] } + ]; +} +function scanEvidence(r, source) { + const data = r.data.security ? r.data : r.data.targets?.[0]?.output; + const security = data?.security; + assert.ok(security && security.scanned_files > 0, "production scan evidence"); + exact(security.scanner, { id: "lintai", version: "0.1.3" }); + exact(security.evidence_source, source); exact(security.outcome, "no_blocking_findings"); + exact(security.subject, { tree_digest: data.tree_digest, manifest_digest: data.manifest_digest }); + assert.match(security.report_digest, /^sha256:[0-9a-f]{64}$/); +} +function installed(row, spec, projects) { + const r = envelope(row, { ...spec, status: 0 }), verb = spec.args[0]; exact(r.command, verb); + if (spec.id.startsWith("dry-run/")) { + exact(r.data.dry_run, true); + scanEvidence(r, "local_scan"); + const lane = spec.id.slice(8), result = lifecycleResult(r, "add", "codex"), plan = result.plan; + exact([plan.client_id, plan.scope, plan.status], ["codex", "user", "manual_activation_required"]); + const skills = projects[lane].files.filter(x => /^skills\/[^/]+\/SKILL.md$/.test(x.path)).map(x => `skill:${x.path.split("/")[1]}`); + const servers = Object.keys(projects[lane].mcp?.mcpServers || {}).map(x => `mcp_server:${x}`); + exact(plan.components.map(x => `${x.kind}:${x.name}`).sort(), [...skills, ...servers].sort()); + assert.ok(plan.components.every(x => x.support && x.support !== "unsupported")); + exact(row.before.client, row.after.client); exact(row.before.state, row.after.state); + return; + } + if (["add", "update", "remove"].includes(verb)) { + const result = lifecycleResult(r, verb, "codex"); + exact(result.mutated, verb !== "update"); + if (verb === "update") { exact(result.no_change, true); exact(row.before.state, row.after.state); exact(row.before.client, row.after.client); } + else assert.notDeepEqual(row.before.state, row.after.state, "real lifecycle state mutation"); + if (verb === "add") { + exact(result.activation.authentication, "not_checked"); + assert.notEqual(result.activation.authentication_attested, true); + assert.notDeepEqual(row.before.client, row.after.client, "real client projection"); + assert.ok(row.after.state.some(x => x.path === "state-v2.json"), "production lifecycle state"); + scanEvidence(r, "cache"); // The preceding fresh dry-run scanned these identical bytes. + } + } else if (verb === "info") { + exact(r.data.name, "skill"); exact(r.data.version, "0.1.0"); + exact(r.data.clients.length, 1); exact(r.data.clients[0].client_id, "codex"); + exact(r.data.clients[0].package_revision.version, "0.1.0"); + } else exact(r.data.installations, []); +} +// No supported, reviewed whole-descendant observer has been provisioned for +// this contract. Never retry the denied ptrace flow, or treat a caller boolean, +// control trap, local file, or an empty syscall list as host observation. +function observationGate() { + fail("WHOLE_OS_OBSERVATION_UNAVAILABLE: supported authoring process/network and owned-descendant observation remains a separate execution prerequisite; no passing terminal"); +} +function verifyJourney(e, pins) { + c.keys(e, FILES, "evidence bundle"); + const transcript = e["transcripts.json"], projects = e["trees.json"]; + c.keys(transcript, [...c.PRODUCTS, "installer"], "transcripts"); c.keys(projects, c.PRODUCTS, "trees"); + for (const p of c.PRODUCTS) { + const specs = commands(p), rows = transcript[p]; exact(rows.length, specs.length, "complete command inventory"); + for (let i = 0; i < specs.length; i++) { + const row = rows[i], spec = specs[i]; + c.keys(row, ["id", "args", "status", "stdout", "stderr", "before", "after"], "command transcript"); + treeShape(row.before); treeShape(row.after); + exact([row.id, row.args], [spec.id, spec.args]); authorResult(row, spec, p, pins.identity); + if (!/\/(init|extra-skill)$/.test(spec.id)) exact(row.before, row.after, "read/rejection preservation"); + } + c.keys(projects[p], CASES, "five templates"); + for (const lane of CASES) checkProject(projects[p][lane], lane); + } + for (let i = 0; i < commands("agentplugins").length; i++) { + const spec = commands("agentplugins")[i]; + if (spec.author) exact(normalized(transcript.agentplugins[i]), normalized(transcript["plugin-kit-ai"][i]), `parity ${spec.id}`); + } + exact(projects.agentplugins, projects["plugin-kit-ai"], "pair tree/mode parity"); + const specs = installationCommands(); exact(transcript.installer.length, specs.length); + for (let i = 0; i < specs.length; i++) { + const row = transcript.installer[i]; + c.keys(row, ["id", "args", "status", "stdout", "stderr", "before", "after"], "installer transcript"); + for (const state of [row.before, row.after]) { + c.keys(state, ["client", "state", "acquisition"], "separate installer state and acquisition"); + treeShape(state.client); treeShape(state.state); + assert.ok(Array.isArray(state.acquisition) && state.acquisition.length <= 4096); + } + exact([row.id, row.args], [specs[i].id, specs[i].args]); installed(row, specs[i], projects.agentplugins); + } + const preserve = e["preservation.json"]; + c.keys(preserve, ["inputs_before", "inputs_after", "projects_before", "projects_after", "author_homes_before", "author_homes_after", "custody_before", "custody_after"], "preservation"); + for (const name of ["inputs", "projects", "author_homes", "custody"]) exact(preserve[`${name}_before`], preserve[`${name}_after`]); + exact(preserve.projects_before, projects); + exact(preserve.inputs_before, e["preparation.json"].subjects, "preserved eighteen independently pinned subjects"); + c.keys(preserve.author_homes_before, c.PRODUCTS, "isolated author homes"); + for (const p of c.PRODUCTS) { + c.keys(preserve.author_homes_before[p], ["home", "state", "config", "cache", "data", "tmp"], "author isolation"); + for (const entries of Object.values(preserve.author_homes_before[p])) exact(entries, [{ path: ".", mode: 448, kind: "directory" }]); + } + return { inventory: "paired-linux-journey/1", commands: Object.fromEntries(c.PRODUCTS.map(p => [p, commands(p).map(x => x.id)])), + installer: specs.map(x => x.id), parity: "both-products", preservation: "inputs-projects-client-state", runtime: "not_evaluated" }; +} +function terminal(p, pins, manifest, options, evidence, tools) { + return { schema: SCHEMA, lane: `${p}/${TARGET}`, identity: pins.identity, candidate_sha256: pins.candidate_sha256, + pair_marker_sha256: pins.pair_marker_sha256, projection_pins: pins.products, subject: subject(manifest, p), + peer_subject: subject(manifest, c.PRODUCTS.find(x => x !== p)), preparation: options.preparation, + producer: options.producer, host: evidence["host.json"], tools, assertions: verifyJourney(evidence, pins), + evidence: FILES.map(file => ({ file, ...c.metadata(c.encode(evidence[file])) })) }; +} +function readTerminals(root, inputRoot, pins, expected) { + c.safeDirectory(root); c.keys(expected, ["producer", "preparation", "tools"], "terminal expectations"); + invocation(expected.producer, WORKFLOW, pins.identity.commit); + const prepared = readPreparation(inputRoot, pins, expected.preparation); + const { manifest } = verifyProjectedPair(inputRoot, pins); + const names = [...FILES, ...c.PRODUCTS.map(p => `${p}-terminal.json`)]; + exact(fs.readdirSync(root).sort(), names.sort(), "exact paired artifact closure"); + const evidence = Object.fromEntries(FILES.map(file => [file, canonical(c.readFile(path.join(root, file), 32 * LIMIT), 32 * LIMIT)])); + exact(evidence["preparation.json"], prepared); + const build = evidence["build-info.json"]; c.keys(build, c.PRODUCTS, "selected build info"); + for (const p of c.PRODUCTS) buildInfo(build[p], p, TARGET, pins.identity, MODE); + const host = evidence["host.json"]; + exact(host, { platform: "linux", architecture: "x64", machine: "x86_64", target: TARGET, + observation: "whole-descendant-authoring-no-process-network/1" }); + c.keys(expected.tools, ["go", "node"], "host tools"); + for (const [name, version] of [["go", "go1.25.13"], ["node", "v22.21.1"]]) { + c.keys(expected.tools[name], ["sha256", "version"], "tool pin"); sha(expected.tools[name].sha256); exact(expected.tools[name].version, version); + } + const result = []; + for (const p of c.PRODUCTS) { + const value = canonical(c.readFile(path.join(root, `${p}-terminal.json`), LIMIT)); + c.keys(value, TOP, "closed native terminal"); + exact(value, terminal(p, pins, manifest, expected, evidence, expected.tools)); result.push(value); + } + // This only verifies local observation encoding. Provider completed attempt, + // signer revision, authenticated artifact custody and all 13 P lanes are N2+. + return result; +} +async function produce(options, signal) { + c.keys(options, ["root", "pins", "preparation", "producer", "go", "go_sha256", "output", "work"], "native options"); + const { root, pins } = options; + invocation(options.producer, WORKFLOW, pins.identity.commit); + exact([process.platform, process.arch, os.machine()], ["linux", "x64", "x86_64"], "actual Linux amd64 host"); + exact(process.version, "v22.21.1"); sha(options.go_sha256); + assert.ok(path.isAbsolute(options.go)); exact(c.digest(c.readFile(options.go)), options.go_sha256); + // Host Go has its own pin; a different-platform builder executable need not hash identically. + const frozen = verifyProjectedPair(root, pins), prep = readPreparation(root, pins, options.preparation); + c.outputPlacement(options.output, [root, path.dirname(options.go), path.resolve(__dirname, "../../..")]); + c.outputPlacement(options.work, [root, path.dirname(options.go), path.resolve(__dirname, "../../..")]); + assert.ok(!options.output.startsWith(options.work + "/") && !options.work.startsWith(options.output + "/") && options.work !== options.output); + fs.mkdirSync(options.output, { mode: 0o700 }); fs.mkdirSync(options.work, { mode: 0o700 }); + const e = {}, rows = {}, projects = {}, binaries = {}, build = {}, contexts = {}; + const frozenBefore = frozen.subjects.map(s => ({ file: path.relative(root, s.file), ...c.metadata(c.readFile(s.file)) })); + const custodyBefore = inputCustody(root, frozen.subjects); + const ownedTerminals = []; + try { + const tool = context(path.join(options.work, "host-tool")); + const go = await subprocess(options.go, ["env", "-json", "GOVERSION", "GOHOSTOS", "GOHOSTARCH"], tool, signal); + exact(go.status, 0); exact(go.stderr, ""); + exact(JSON.parse(go.stdout), { GOVERSION: "go1.25.13", GOHOSTOS: "linux", GOHOSTARCH: "amd64" }); + for (const p of c.PRODUCTS) { + contexts[p] = context(path.join(options.work, p)); + const a = frozen.manifest.products[p].assets[TARGET], bytes = c.readFile(path.join(root, p, a.file)); + const binary = p === "plugin-kit-ai" ? c.unpack(bytes, a.binary.file) : bytes; + binaries[p] = path.join(contexts[p].root, p); fs.writeFileSync(binaries[p], binary, { flag: "wx", mode: 0o500 }); + exact(c.metadata(c.readFile(binaries[p])), { sha256: a.binary.sha256, size: a.binary.size }); + const r = await subprocess(options.go, ["version", "-m", "-json", binaries[p]], tool, signal); + exact(r.status, 0); exact(r.stderr, ""); build[p] = JSON.parse(r.stdout); buildInfo(build[p], p, TARGET, pins.identity, MODE); + } + const protectedHomes = () => Object.fromEntries(c.PRODUCTS.map(p => [p, Object.fromEntries(["home", "state", "config", "cache", "data", "tmp"].map(n => [n, tree(path.join(contexts[p].root, n))]))])); + const homesBefore = protectedHomes(); + for (const p of c.PRODUCTS) { + rows[p] = []; projects[p] = {}; const ctx = contexts[p], projectRoot = path.join(ctx.root, "projects"); + for (const spec of commands(p)) { + const malformed = path.join(projectRoot, "skill/skills/broken"); + if (spec.id === "malformed-skill") { + fs.mkdirSync(malformed, { mode: 0o700 }); fs.writeFileSync(path.join(malformed, "SKILL.md"), "---\nname: [\n---\nBroken\n", { flag: "wx" }); + } + const before = tree(projectRoot), r = await subprocess(binaries[p], spec.args, ctx, signal); + const row = { id: spec.id, args: spec.args, ...r, before, after: tree(projectRoot) }; + rows[p].push(row); authorResult(row, spec, p, pins.identity); + if (!/\/(init|extra-skill)$/.test(spec.id)) exact(row.before, row.after); + if (spec.id === "malformed-skill") { fs.unlinkSync(path.join(malformed, "SKILL.md")); fs.rmdirSync(malformed); } + } + for (const lane of CASES) projects[p][lane] = projectEvidence(path.join(projectRoot, lane), lane); + } + const homesAfter = protectedHomes(); exact(homesBefore, homesAfter, "authoring homes/cache/client/state preservation"); + const install = context(path.join(options.work, "installer")); + const client = path.join(install.env.HOME, ".codex"); fs.mkdirSync(client, { mode: 0o700 }); + fs.writeFileSync(path.join(client, "preservation-marker"), "owned client content\n", { flag: "wx", mode: 0o600 }); + const markerBefore = c.metadata(c.readFile(path.join(client, "preservation-marker"))); + fs.writeFileSync(path.join(client, "config.toml"), "# owned native journey client root\n", { flag: "wx", mode: 0o600 }); + // Real supported detector must recognize this isolated config root. No fake + // executable, injected detector, scanner seed, auth or activation flag. + rows.installer = []; + const state = () => { + const all = tree(install.env.AGENTPLUGINS_HOME); + // Genuine feed/scanner acquisition is retained separately. These are the + // production cache names, not wildcard state exclusions or seeded inputs. + const isAcquisition = x => /^(security(?:\/|$)|directory-v1-cache\.json$|discovery-v1-cache\.json$)/.test(x.path); + return { client: tree(client), state: all.filter(x => !isAcquisition(x)), acquisition: all.filter(isAcquisition) }; + }; + for (const spec of installationCommands()) { + const args = spec.args.map(a => a.replace("", path.join(contexts.agentplugins.root, "projects"))); + const before = state(), r = await subprocess(binaries.agentplugins, args, install, signal, true); + const row = { id: spec.id, args: spec.args, ...r, before, after: state() }; + rows.installer.push(row); installed(row, spec, projects.agentplugins); + } + exact(c.metadata(c.readFile(path.join(client, "preservation-marker"))), markerBefore, "client sentinel preserved through installer"); + const afterProjects = Object.fromEntries(c.PRODUCTS.map(p => [p, Object.fromEntries(CASES.map(lane => + [lane, projectEvidence(path.join(contexts[p].root, "projects", lane), lane)]))])); + const after = verifyProjectedPair(root, pins); readPreparation(root, pins, options.preparation); + exact(c.digest(c.readFile(options.go)), options.go_sha256); + for (const p of c.PRODUCTS) { + exact(c.digest(c.readFile(binaries[p])), frozen.manifest.products[p].assets[TARGET].binary.sha256); + exact(fs.lstatSync(binaries[p]).mode & 0o777, 0o500, "selected executable mode preserved"); + } + e["transcripts.json"] = rows; e["trees.json"] = projects; e["build-info.json"] = build; e["preparation.json"] = prep; + e["preservation.json"] = { inputs_before: frozenBefore, + inputs_after: after.subjects.map(s => ({ file: path.relative(root, s.file), ...c.metadata(c.readFile(s.file)) })), + projects_before: projects, projects_after: afterProjects, author_homes_before: homesBefore, author_homes_after: homesAfter, + custody_before: custodyBefore, custody_after: inputCustody(root, after.subjects) }; + verifyJourney({ ...e, "host.json": {} }, pins); + observationGate(); // Unavailable capability is a failure diagnostic, never a pass stub. + e["host.json"] = { platform: process.platform, architecture: process.arch, machine: os.machine(), target: TARGET, + observation: "whole-descendant-authoring-no-process-network/1" }; + const tools = { go: { sha256: options.go_sha256, version: "go1.25.13" }, node: { sha256: c.digest(c.readFile(process.execPath)), version: process.version } }; + for (const file of FILES) fs.writeFileSync(path.join(options.output, file), c.encode(e[file]), { flag: "wx", mode: 0o400 }); + if (signal?.aborted) fail("cancelled before terminal"); + for (const p of c.PRODUCTS) { + const file = path.join(options.output, `${p}-terminal.json`); + const fd = fs.openSync(file, "wx", 0o400); ownedTerminals.push(file); + try { fs.writeFileSync(fd, c.encode(terminal(p, pins, frozen.manifest, options, e, tools))); } + finally { fs.closeSync(fd); } + } + return readTerminals(options.output, root, pins, { producer: options.producer, preparation: options.preparation, tools }); + } catch (error) { + for (const file of ownedTerminals) fs.unlinkSync(file); + fs.writeFileSync(path.join(options.output, "failure-transcripts.json"), c.encode({ + schema: "authoring-frozen-native-failure-transcripts/v1", + invocations: Object.fromEntries(Object.entries(rows).map(([p, r]) => [p, r.slice(-10).map(x => ({ + id: x.id, args: x.args, status: x.status, stdout: x.stdout.slice(0, 65536), stderr: x.stderr.slice(0, 65536) }))])) + }), { flag: "wx", mode: 0o600 }); + fs.writeFileSync(path.join(options.output, "diagnostic.json"), c.encode({ schema: "authoring-frozen-native-failure/v1", + error: String(error.message).slice(0, 2000), commands: Object.fromEntries(Object.entries(rows).map(([p, r]) => [p, r.map(x => ({ id: x.id, status: x.status }))])), + native_acceptance: false }), { flag: "wx", mode: 0o600 }); + throw error; + } +} +if (require.main === module) { + const controller = new AbortController(); + for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => controller.abort()); + Promise.resolve().then(() => { + assert.equal(process.argv.length, 3, "one exact configuration file required"); + return produce(canonical(c.readFile(process.argv[2], LIMIT)), controller.signal); + }).catch(error => { process.stderr.write(String(error.message).slice(0, 2000) + "\n"); process.exitCode = 1; }); +} +module.exports = { writePreparation, readPreparation, produce, readTerminals }; diff --git a/npm/agentplugins/scripts/authoring-promotion.js b/npm/agentplugins/scripts/authoring-promotion.js index d14607ef..aa6384e0 100644 --- a/npm/agentplugins/scripts/authoring-promotion.js +++ b/npm/agentplugins/scripts/authoring-promotion.js @@ -245,48 +245,13 @@ function verifySubject(file, expected, cwd) { // beside them. Reconstruct the expected manifests/marker from pinned candidate // bytes; this never builds or launches native subjects, even for Go build info. function frozenSubjects(root, record) { - c.safeDirectory(root); - const candidateFile = path.join(root, "candidate", "candidate.json"); - const body = c.readFile(candidateFile, LIMIT); - exact(c.digest(body), record.candidate_sha256, "candidate digest"); - const manifest = JSON.parse(body); - if (!body.equals(c.encode(manifest))) fail("noncanonical candidate"); - c.manifestShape(manifest, record.identity, SCOPE, MODE); - const result = [{ file: candidateFile, sha256: record.candidate_sha256 }]; - const products = {}; - for (const p of c.PRODUCTS) { - const projection = path.join(root, p); c.safeDirectory(projection); - exact(manifest.products[p].assets, record.products[p].assets, "candidate product pins"); - const m = { schema_version: 3, status: "CANDIDATE", product: p, repository: REPOSITORY, - tag: tag(record.identity, p), version: record.identity.versions[p], commit: record.identity.commit, - engine_revision: record.identity.commit, versions: record.identity.versions, candidate_sha256: record.candidate_sha256, - authoring_mode: MODE, asset_scope: SCOPE, assets: manifest.products[p].assets, - release_eligible: false, platform_acceptance: false, attested: false }; - const mBody = c.encode(m); - const checks = Buffer.from([...Object.values(m.assets).map(a => `${a.sha256} ${a.file}`), `${c.digest(mBody)} release-manifest.json`].join("\n") + "\n"); - for (const [name, bytes, hash] of [["release-manifest.json", mBody, record.products[p].manifest_sha256], ["checksums.txt", checks, record.products[p].checksums_sha256]]) { - const file = path.join(projection, name); - exact(c.readFile(file, LIMIT), bytes, "projection bytes"); exact(c.digest(bytes), hash, "independent projection pin"); - result.push({ file, sha256: hash }); - } - for (const a of Object.values(m.assets)) { - const file = path.join(projection, a.file); const bytes = c.readFile(file); - exact(c.metadata(bytes), { sha256: a.sha256, size: a.size }, "asset bytes"); - const binary = p === "plugin-kit-ai" ? c.unpack(bytes, a.binary.file) : bytes; - exact(c.metadata(binary), { sha256: a.binary.sha256, size: a.binary.size }, "inner binary bytes"); - result.push({ file, sha256: a.sha256 }); - } - exact(fs.readdirSync(projection).sort(), [...Object.values(m.assets).map(a => a.file), "release-manifest.json", "checksums.txt"].sort(), "projection closure"); - products[p] = { manifest_sha256: c.digest(mBody), checksums_sha256: c.digest(checks) }; - } - const marker = { schema: "authoring-release-pair/v1", status: "CANDIDATE", identity: record.identity, - candidate_sha256: record.candidate_sha256, authoring_mode: MODE, asset_scope: SCOPE, products, - release_eligible: false, platform_acceptance: false, attested: false }; - const markerFile = path.join(root, "pair-prepared.json"); - exact(c.readFile(markerFile, LIMIT), c.encode(marker), "pair marker"); - exact(c.digest(c.encode(marker)), record.pair_marker_sha256, "pair marker pin"); - result.push({ file: markerFile, sha256: record.pair_marker_sha256 }); - return result; // 18 unchanged input subjects, promotion record is the 19th. + const pins = { identity: record.identity, candidate_sha256: record.candidate_sha256, + pair_marker_sha256: record.pair_marker_sha256, + products: Object.fromEntries(c.PRODUCTS.map(p => [p, { + manifest_sha256: record.products[p].manifest_sha256, checksums_sha256: record.products[p].checksums_sha256 }])) }; + const verified = require("./authoring-release").verifyProjectedPair(root, pins); + for (const p of c.PRODUCTS) exact(verified.manifest.products[p].assets, record.products[p].assets, "candidate product pins"); + return verified.subjects; } function releasePins(record, p) { return [...Object.values(record.products[p].assets).map(a => ({ name: a.file, sha256: a.sha256, size: a.size })), diff --git a/npm/agentplugins/scripts/authoring-release.js b/npm/agentplugins/scripts/authoring-release.js index 52b2ab80..27acb4fa 100644 --- a/npm/agentplugins/scripts/authoring-release.js +++ b/npm/agentplugins/scripts/authoring-release.js @@ -2,6 +2,9 @@ const fs = require("node:fs"); const path = require("node:path"); +const { isDeepStrictEqual: equal } = require("node:util"); +const fail = message => { throw new Error(message); }; +const exact = (a, b, label) => { if (!equal(a, b)) fail(`${label}: binding mismatch`); }; // Schema v3 is an explicit projection of one sealed native pair. Historical // consumers continue to require v2; consistency is never publication approval. @@ -148,4 +151,57 @@ function verifyAuthoringRelease(input) { return { ...pair, consistency_verified: true }; } -module.exports = { prepareAuthoringRelease, verifyAuthoringRelease }; +function verifyProjectedPair(root, pins) { + c.keys(pins, ["identity", "candidate_sha256", "pair_marker_sha256", "products"], "projected pair pins"); + c.identity(pins.identity); + if (/^0{40}$/.test(pins.identity.commit) || pins.identity.versions["plugin-kit-ai"] !== "2.0.0") fail("first-cut identity required"); + c.keys(pins.products, c.PRODUCTS, "projection pins"); + const record = pins; + const seen = new Set(); + c.safeDirectory(root); + const candidateFile = path.join(root, "candidate", "candidate.json"); + const body = c.readFile(candidateFile, 1024 * 1024); + exact(c.digest(body), record.candidate_sha256, "candidate digest"); + const manifest = JSON.parse(body); + if (!body.equals(c.encode(manifest))) fail("noncanonical candidate"); + c.manifestShape(manifest, record.identity, AUTHORING_SCOPE, AUTHORING_MODE); + const result = [{ file: candidateFile, sha256: record.candidate_sha256 }]; + const products = {}; + for (const p of c.PRODUCTS) { + const projection = path.join(root, p); c.safeDirectory(projection); + c.keys(pins.products[p], ["manifest_sha256", "checksums_sha256"], "product projection pins"); + const m = { schema_version: 3, status: "CANDIDATE", product: p, repository: c.REPOSITORY, + tag: productManifest({ manifest, manifest_sha256: pins.candidate_sha256 }, p).tag, version: record.identity.versions[p], commit: record.identity.commit, + engine_revision: record.identity.commit, versions: record.identity.versions, candidate_sha256: record.candidate_sha256, + authoring_mode: AUTHORING_MODE, asset_scope: AUTHORING_SCOPE, assets: manifest.products[p].assets, + release_eligible: false, platform_acceptance: false, attested: false }; + const mBody = c.encode(m); + const checks = Buffer.from([...Object.values(m.assets).map(a => `${a.sha256} ${a.file}`), `${c.digest(mBody)} release-manifest.json`].join("\n") + "\n"); + for (const [name, bytes, hash] of [["release-manifest.json", mBody, record.products[p].manifest_sha256], ["checksums.txt", checks, record.products[p].checksums_sha256]]) { + const file = path.join(projection, name); + exact(c.readFile(file, 1024 * 1024), bytes, "projection bytes"); exact(c.digest(bytes), hash, "independent projection pin"); + result.push({ file, sha256: hash }); + } + for (const a of Object.values(m.assets)) { + const file = path.join(projection, a.file); const bytes = c.readFile(file); + exact(c.metadata(bytes), { sha256: a.sha256, size: a.size }, "asset bytes"); + const binary = p === "plugin-kit-ai" ? c.unpack(bytes, a.binary.file) : bytes; + exact(c.metadata(binary), { sha256: a.binary.sha256, size: a.binary.size }, "inner binary bytes"); + if (seen.has(a.binary.sha256)) fail("duplicate product/target binary"); + seen.add(a.binary.sha256); + result.push({ file, sha256: a.sha256 }); + } + exact(fs.readdirSync(projection).sort(), [...Object.values(m.assets).map(a => a.file), "release-manifest.json", "checksums.txt"].sort(), "projection closure"); + products[p] = { manifest_sha256: c.digest(mBody), checksums_sha256: c.digest(checks) }; + } + const marker = { schema: "authoring-release-pair/v1", status: "CANDIDATE", identity: record.identity, + candidate_sha256: record.candidate_sha256, authoring_mode: AUTHORING_MODE, asset_scope: AUTHORING_SCOPE, products, + release_eligible: false, platform_acceptance: false, attested: false }; + const markerFile = path.join(root, "pair-prepared.json"); + exact(c.readFile(markerFile, 1024 * 1024), c.encode(marker), "pair marker"); + exact(c.digest(c.encode(marker)), record.pair_marker_sha256, "pair marker pin"); + result.push({ file: markerFile, sha256: record.pair_marker_sha256 }); + return { manifest, subjects: result }; // Eighteen unchanged frozen inputs. +} + +module.exports = { prepareAuthoringRelease, verifyAuthoringRelease, verifyProjectedPair }; diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js new file mode 100644 index 00000000..4f0a10d4 --- /dev/null +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -0,0 +1,461 @@ +"use strict"; + +// All executable responses below are test-local subprocess fixtures. No real +// frozen binary, installer, scanner, network service or OS observer is executed. +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const os = require("node:os"); +const vm = require("node:vm"); +const cp = require("node:child_process"); +const { createRequire } = require("node:module"); +const c = require("../scripts/dual-authoring-candidate"); +const release = require("../scripts/authoring-release"); +const native = require("../scripts/authoring-native-qualification"); +const promotion = require("../scripts/authoring-promotion"); +const moduleFile = path.resolve(__dirname, "../scripts/authoring-native-qualification.js"); +const hash = s => c.digest(Buffer.from(s)); +const ID = { repository: c.REPOSITORY, commit: "a".repeat(40), engine_revision: "a".repeat(40), + versions: { agentplugins: "0.1.54", "plugin-kit-ai": "2.0.0" } }; +const invocation = workflow => ({ repository: c.REPOSITORY, workflow, source: ID.commit, + workflow_sha: ID.commit, run_id: 42, run_attempt: 2 }); + +function fixture() { + // Reuse only the established text/ustar fixture construction, never its tests + // or provider seam. These bytes are intentionally not native executables. + const source = fs.readFileSync(path.join(__dirname, "authoring-promotion.test.js"), "utf8"); + const prefix = source.slice(0, source.indexOf("function expected(")); + const Module = require("node:module"), original = path.join(__dirname, "authoring-promotion.test.js"); + const instance = new Module(original, module); + instance.filename = original; instance.paths = Module._nodeModulePaths(__dirname); + instance._compile(prefix + "\nmodule.exports = fixture;", original); + const f = instance.exports(); + f.pins = { identity: structuredClone(ID), candidate_sha256: f.record.candidate_sha256, + pair_marker_sha256: f.record.pair_marker_sha256, products: Object.fromEntries(c.PRODUCTS.map(p => [p, { + manifest_sha256: f.record.products[p].manifest_sha256, checksums_sha256: f.record.products[p].checksums_sha256 }])) }; + const preparationProducer = invocation(".github/workflows/agentplugins-release.yml"); + const receipt = native.writePreparation(f.root, f.pins, preparationProducer); + f.go = path.join(f.sandbox, "host-go"); fs.writeFileSync(f.go, "host Go fixture; not executed", { mode: 0o500 }); + f.options = { root: f.root, pins: f.pins, preparation: { sha256: receipt.sha256, producer: preparationProducer }, + producer: invocation(".github/workflows/authoring-frozen-native.yml"), go: f.go, + go_sha256: c.digest(c.readFile(f.go)), output: path.join(f.sandbox, "evidence"), work: path.join(f.sandbox, "journey") }; + // Sibling outputs must not be inside the protected tool parent. + const tools = path.join(f.sandbox, "tools"); fs.mkdirSync(tools); + const moved = path.join(tools, "go"); fs.renameSync(f.go, moved); f.go = moved; f.options.go = moved; + return f; +} +function internal(observation = false, fastTimeout = false) { + let source = fs.readFileSync(moduleFile, "utf8"); + if (observation) source = source.replace(' observationGate(); //', ' /* Test-only synthetic observation; not a native pass. */ //'); + if (fastTimeout) source = source.replace('installer ? 120000 : 15000', '100'); + // Expose lexical contracts only in this test VM. Production exports no policy, + // verifier, command inventory, child executable or success switch. + source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape };'; + const Module = require("node:module"); + const instance = new Module(moduleFile, module); + instance.filename = moduleFile; instance.paths = Module._nodeModulePaths(path.dirname(moduleFile)); + instance._compile(source, moduleFile); + return instance.exports; +} +function noTerminal(f) { + for (const p of c.PRODUCTS) assert.equal(fs.existsSync(path.join(f.options.output, `${p}-terminal.json`)), false); +} +function fixtureProgram() { + // Deliberately independent response implementation with actual mkdir/write/ + // state mutations in each child. A zero status without those effects is tested. + return String.raw` +const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'); +const cfg = JSON.parse(fs.readFileSync(process.argv[2])); +const selected = process.argv[3], argv = process.argv.slice(4); +const sha = s => crypto.createHash('sha256').update(s).digest('hex'); +const product = selected.includes('plugin-kit-ai') ? 'plugin-kit-ai' : 'agentplugins'; +fs.appendFileSync(cfg.log, JSON.stringify({selected,argv,env:process.env})+'\n'); +const scenario = cfg.scenario; +if (scenario === 'timeout') setInterval(()=>{},1000); +else if (scenario === 'flood') process.stdout.write('x'.repeat(2*1024*1024)); +else if (scenario === 'cancel') setInterval(()=>{},1000); +else main(); +function output(value, status=0) { + if (scenario === 'invalid-utf8') process.stdout.write(Buffer.from([0xff])); + else if (scenario === 'bad-json') process.stdout.write('{bad'); + else process.stdout.write(JSON.stringify(value)+'\n'); + process.exitCode=status; +} +function main() { + if (selected === cfg.go) { + if (argv[0]==='env') return output({GOVERSION:'go1.25.13',GOHOSTOS:'linux',GOHOSTARCH:'amd64'}); + const p = path.basename(argv[3]); + const settings = {GOOS:'linux',GOARCH:'amd64',CGO_ENABLED:'0','-buildmode':'exe','-compiler':'gc', + '-ldflags':cfg.linker[p]}; + if(scenario==='wrong-build-target') settings.GOARCH='arm64'; + if(scenario==='wrong-build-mode') settings['-buildmode']='pie'; + if(scenario==='wrong-build-source') settings['-ldflags']=settings['-ldflags'].replace(cfg.identity.commit,'b'.repeat(40)); + return output({GoVersion:'go1.25.13',Path:'github.com/777genius/plugin-kit-ai/cli/cmd/'+(scenario==='wrong-binary'?'peer':p), + Settings:Object.entries(settings).map(([Key,Value])=>({Key,Value}))}); + } + let args=argv.filter(x=>x!=='--format=json'); + if(args.length===1 && args[0]==='--help' && !argv.includes('--format=json')) { process.stdout.write('Usage: '+product+'\nAvailable commands: author init validate inspect test capabilities skills version doctor compat\n'); return; } + const author=product==='plugin-kit-ai'||args[0]==='author'; + if(args[0]==='author') args.shift(); + if(!author) return installer(args); + const verb=args[0], lane=args[1], op=verb==='skills'?'author.skills.'+args[1]:verb==='--help'?'author':'author.'+verb; + const data={engine:'standard-first-slice/1',revision:cfg.identity.commit,authoring_schema_version:1, + engine_version:'standard-first-slice/1',runtime_evidence:{status:'not_evaluated'},committed:false,effects:{committed:false}, + product,product_version:cfg.identity.versions[product]}; + const done=(status=0)=>output({schema_version:1,command:scenario==='wrong-command'&&verb==='validate'?'author.inspect':op,result:status?'failure':'success',data},status); + if(scenario==='wrong-source') data.revision='b'.repeat(40); + if(scenario==='runtime-claim') data.runtime_evidence.status='pass'; + if(verb==='version') {if(scenario==='wrong-version') data.product_version='9.9.9';return done();} + if(verb==='update') {data.error={code:'v1_operation_unavailable'};return done(2);} + if(verb==='--help'||verb==='capabilities') { + data.commands=['author.capabilities','author.compat','author.doctor','author.init','author.inspect','author.skills.init','author.skills.validate','author.test','author.validate']; + if(scenario==='missing-command') data.commands.pop(); + if(scenario==='deferred-command') data.commands.push('author.publish'); + if(verb==='--help')data.help={use:product==='agentplugins'?'agentplugins author':'plugin-kit-ai'}; + else data.capabilities={schemas:[{id:'fixture-only'}]}; + return done(); + } + if(args.includes('--force')||args.includes('--scope=user')||lane==='missing-destination')return done(2); + if(verb==='init'&&fs.existsSync(lane))return done(1); + const root=verb==='skills'?args[1]==='init'?args[3]:args[2]:lane; + if(verb==='init') { + if(scenario==='missing-template'&&lane==='hybrid-stdio')return done(); + fs.mkdirSync(root,{mode:0o700}); + write(root,'plugin.json',{$schema:'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',name:lane,version:'0.1.0',description:'Owned fixture'}); + write(root,'README.md','Owned fixture\n');write(root,'.gitignore','cache\n'); + if(lane==='skill'||lane.startsWith('hybrid-'))skill(root,lane); + if(lane!=='skill')write(root,'mcp.json',{$schema:'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json',mcpServers:{[lane]:{type:lane.endsWith('remote')?'streamable-http':'stdio'}}}); + if(lane.endsWith('stdio')) { + write(root,'package.json',{dependencies:{'@modelcontextprotocol/sdk':'1.30.0'}}); + write(root,'package-lock.json',{packages:{'node_modules/@modelcontextprotocol/sdk':{version:'1.30.0',integrity:'sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=='}}}); + } + if(scenario==='yaml')write(root,'plugin.yaml','legacy'); + data.committed=true;data.effects.committed=true; + } + if(verb==='skills'&&args[1]==='init'){skill(root,'extra-skill');data.committed=true;data.effects.committed=true;} + const malformed=fs.existsSync(path.join(root,'skills/broken')); + data.identity={read_profile:'packageview-local-linux-v1',tree_digest:'sha256:'+sha('tree '+root)}; + data.profiles=[{id:'agent-skills/2026-09-06',revision:'69ef37e9424c0a7ea9dd2293b559e43ec8176379',digest:'sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220'}]; + data.loadability={status:'pass'};data.normative_conformance={status:malformed?'fail':'pass'}; + data.authoring_readiness={status:malformed?'fail':'pass'};data.release_policy={status:'not_evaluated'}; + data.components=[{type:'skill',status:'pass'}];data.findings=malformed?[{severity:'error'}]:[]; + if(verb==='compat'){data.compatibility={status:'pass'};data.clients=[{client_id:'claude'},{client_id:'codex'}];} + if(verb==='doctor'){data.toolchain={status:root==='skill'?'pass':'not_evaluated'};return done(root==='skill'?0:1);} + if(verb==='inspect') { + const count=root==='skill'||root.startsWith('hybrid-')?2:1; + data.inspection={name:root,schema:'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + components:[...Array(count).fill({type:'skill'}),...(root==='skill'?[]:[{type:root.endsWith('remote')?'mcp_streamable-http':'mcp_stdio'}])]}; + } + if(scenario==='changed-input'&&verb==='test')fs.appendFileSync(cfg.input,'changed'); + if(scenario==='changed-project'&&verb==='test')write(root,'unexpected','mutation'); + if(scenario==='wrong-result'&&verb==='validate')data.normative_conformance.status='fail'; + if(scenario==='parity'&&product==='plugin-kit-ai')data.findings.push({severity:'warning'}); + return done(malformed?1:0); +} +function write(root,name,value){fs.writeFileSync(path.join(root,name),typeof value==='string'?value:JSON.stringify(value)+'\n');} +function skill(root,name){fs.mkdirSync(path.join(root,'skills',name),{recursive:true,mode:0o700});write(root,'skills/'+name+'/SKILL.md','---\nname: '+name+'\ndescription: Owned fixture\n---\nInstructions\n');} +function security(source){return {scanner:{id:'lintai',version:'0.1.3'},scanned_files:4,evidence_source:source,outcome:'no_blocking_findings',subject:{tree_digest:'sha256:'+sha('tree'),manifest_digest:'sha256:'+sha('manifest')},report_digest:'sha256:'+sha('report')};} +function installer(args) { + const verb=args[0], state=process.env.AGENTPLUGINS_HOME,client=path.join(process.env.HOME,'.codex'); + const data={}, result={schema_version:1,command:verb,result:'success',data}; + if(verb==='version'){data.version=scenario==='wrong-version'?'9.9.9':cfg.identity.versions.agentplugins;return output(result);} + if(scenario==='scan-failure'){result.result='failure';return output(result,1);} + if(verb==='add'&&args.includes('--dry-run')) { + const lane=path.basename(args[1]), names=fs.readdirSync(path.join(args[1],'skills')); + const components=names.map(name=>({kind:'skill',name,support:'native'})); + if(lane!=='skill')components.push({kind:'mcp_server',name:lane,support:'native'}); + data.security=security('local_scan');data.tree_digest='sha256:'+sha('tree');data.manifest_digest='sha256:'+sha('manifest'); + data.dry_run=true;data.result={plan:{client_id:'codex',scope:'user',status:'manual_activation_required',components}}; + if(scenario==='wrong-plan')data.result.plan.components=[]; + if(scenario==='dry-run-effect')write(client,'unexpected','effect'); + } else if(verb==='add') { + if(scenario==='add-failure'){result.result='failure';return output(result,1);} + data.result={mutated:true,activation:{authentication:'not_checked'}}; + data.security=security('cache');data.tree_digest='sha256:'+sha('tree');data.manifest_digest='sha256:'+sha('manifest'); + if(scenario!=='no-state')write(state,'state-v2.json',{installed:true}); + if(scenario!=='no-effect')write(client,'installed','fixture projection'); + if(scenario==='auth-claim')data.result.activation.authentication_attested=true; + } else if(verb==='info'){data.name='skill';data.version='0.1.0';data.clients=[{client_id:'codex',package_revision:{version:'0.1.0'}}];} + else if(verb==='update')data.result={mutated:false,no_change:scenario!=='wrong-update'}; + else if(verb==='remove'){data.result={mutated:true};write(state,'state-v2.json',{installed:false});fs.unlinkSync(path.join(client,'installed'));} + else if(verb==='list')data.installations=scenario==='remaining-installation'?[{name:'skill'}]:[]; + return output(result); +} +`; +} +function subprocessFixtures(t, f, scenario = "ok") { + const script = path.join(f.sandbox, "child-fixture.js"), config = path.join(f.sandbox, "child-config.json"); + f.log = path.join(f.sandbox, "subprocesses.jsonl"); + fs.writeFileSync(script, fixtureProgram()); + fs.writeFileSync(config, JSON.stringify({ scenario, identity: ID, go: f.go, log: f.log, + input: path.join(f.root, "candidate/candidate.json"), linker: Object.fromEntries(c.PRODUCTS.map(p => [p, c.linkerFlags(p, ID, "release-cli-contract-v1")])) })); + const spawn = cp.spawn; + t.mock.method(cp, "spawn", (file, args, options) => { + assert.ok(file === f.go || c.PRODUCTS.some(p => file === path.join(f.options.work, p, p)), "only pinned tool or selected binary"); + assert.equal(options.shell, false); assert.equal(options.detached, true); + for (const forbidden of ["GH_TOKEN", "GITHUB_TOKEN", "NODE_OPTIONS", "HTTPS_PROXY", "UAP_PROOF_MODE", "GOFLAGS"]) + assert.equal(options.env[forbidden], undefined, forbidden); + return spawn(process.execPath, [script, config, file, ...args], options); + }); +} +function expectations(f) { + return { producer: f.options.producer, preparation: f.options.preparation, + tools: { go: { sha256: f.options.go_sha256, version: "go1.25.13" }, node: { sha256: c.digest(c.readFile(process.execPath)), version: process.version } } }; +} + +test("shared verifier consumes uploaded projections, not missing flat candidate assets", () => { + const f = fixture(), before = release.verifyProjectedPair(f.root, f.pins); + assert.equal(before.subjects.length, 18); + assert.deepEqual(fs.readdirSync(path.join(f.root, "candidate")), ["candidate.json"]); + for (const p of c.PRODUCTS) assert.equal(fs.readdirSync(path.join(f.root, p)).length, 8); + assert.throws(() => c.frozenCandidate(path.join(f.root, "candidate"), ID, f.pins.candidate_sha256, "six-platform-pair", "release-cli-contract-v1")); + assert.equal(native.readPreparation(f.root, f.pins, f.options.preparation).subjects.length, 18); + assert.deepEqual(promotion.frozenSubjects(f.root, f.record), before.subjects); +}); +for (const kind of ["candidate", "pair", "manifest", "checksum", "outer", "extra", "link", "hardlink", "identity", "mode"]) { + test(`projected closure rejects ${kind}`, () => { + const f = fixture(), p = "agentplugins"; + const file = kind === "candidate" ? path.join(f.root, "candidate/candidate.json") : kind === "pair" ? path.join(f.root, "pair-prepared.json") : + path.join(f.root, p, kind === "manifest" ? "release-manifest.json" : kind === "checksum" ? "checksums.txt" : f.record.products[p].assets["linux-amd64"].file); + if (kind === "identity") f.pins.identity.commit = "b".repeat(40); + else if (kind === "mode") {const v=JSON.parse(c.readFile(path.join(f.root,"candidate/candidate.json")));v.build.authoring_mode="vertical-slice-v1";fs.writeFileSync(path.join(f.root,"candidate/candidate.json"),c.encode(v));f.pins.candidate_sha256=c.digest(c.encode(v));} + else if (kind === "extra") fs.writeFileSync(path.join(f.root,p,"extra"),"extra"); + else if (kind === "link") {fs.renameSync(file,file+"-original");fs.symlinkSync(file+"-original",file);} + else if (kind === "hardlink") fs.linkSync(file,path.join(f.sandbox,"alias")); + else fs.appendFileSync(file,"changed"); + assert.throws(()=>release.verifyProjectedPair(f.root,f.pins)); noTerminal(f); + }); +} +for (const kind of ["missing", "attempt", "workflow", "source", "duplicate-key", "future-qualification", "subject"]) { + test(`preparation receipt rejects ${kind}`, () => { + const f=fixture(),file=path.join(f.root,"preparation-run.json"); + if(kind==="missing")fs.unlinkSync(file); + else if(kind==="attempt")f.options.preparation.producer.run_attempt++; + else if(kind==="workflow")f.options.preparation.producer.workflow=".github/workflows/other.yml"; + else if(kind==="source")f.options.preparation.producer.source="b".repeat(40); + else { + let v=JSON.parse(c.readFile(file)); + if(kind==="future-qualification")v.qualification_sha256=hash("future"); + if(kind==="subject")v.subjects.pop(); + let bytes=c.encode(v);if(kind==="duplicate-key")bytes=Buffer.from(bytes.toString().replace('{','{"schema":"duplicate",')); + fs.chmodSync(file,0o600);fs.writeFileSync(file,bytes);f.options.preparation.sha256=c.digest(bytes); + } + assert.throws(()=>native.readPreparation(f.root,f.pins,f.options.preparation));noTerminal(f); + }); +} + +test("fixed production orchestration and closed reader with subprocess fixtures only", async t => { + const f=fixture();subprocessFixtures(t,f); + const n=internal(true), result=await n.produce(f.options); + assert.equal(result.length,2); + const reread=native.readTerminals(f.options.output,f.root,f.pins,expectations(f)); + assert.equal(reread[0].lane,"agentplugins/linux-amd64"); assert.equal(reread[1].lane,"plugin-kit-ai/linux-amd64"); + assert.notDeepEqual(reread[0].subject,reread[1].subject); + assert.deepEqual(reread[0].peer_subject,reread[1].subject); + assert.equal(reread[0].assertions.installer.length,8); + assert.equal(reread[0].assertions.commands.agentplugins.length,54); + const calls=fs.readFileSync(f.log,"utf8").trim().split("\n").map(JSON.parse); + assert.ok(calls.some(x=>x.argv.includes('--dry-run'))); + assert.ok(calls.some(x=>x.argv[0]==='remove')); + assert.ok(calls.every(x=>!x.argv.includes('--auth-complete')&&!x.argv.includes('--accept-security-risk'))); + assert.throws(()=>promotion.requireNativeContracts(result),/unknown or duplicate terminal lane|NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); +}); +for (const scenario of ["wrong-binary","wrong-build-target","wrong-build-mode","wrong-build-source","wrong-version","wrong-source", + "runtime-claim","bad-json","invalid-utf8","missing-command","deferred-command","missing-template","yaml","wrong-result","wrong-command","parity","changed-project", + "changed-input","scan-failure","wrong-plan","dry-run-effect","add-failure","no-state","no-effect","auth-claim","wrong-update","remaining-installation"]) { + test(`full subprocess journey fails without completion: ${scenario}`,async t=>{ + const f=fixture();subprocessFixtures(t,f,scenario); + await assert.rejects(internal(true).produce(f.options), scenario === "wrong-command" ? /author command identifier/ : undefined);noTerminal(f); + assert.ok(fs.existsSync(path.join(f.options.output,"diagnostic.json"))); + }); +} +test("unavailable whole-OS observation cannot emit native acceptance after fixture installer",async t=>{ + const f=fixture();subprocessFixtures(t,f); + await assert.rejects(native.produce(f.options),/WHOLE_OS_OBSERVATION_UNAVAILABLE/);noTerminal(f); + assert.match(fs.readFileSync(f.log,"utf8"),/"remove"/); +}); +for(const scenario of ["timeout","flood","cancel"]){ + test(`bounded child ${scenario} retains no terminal`,async t=>{ + const f=fixture();subprocessFixtures(t,f,scenario);const control=new AbortController(); + const promise=internal(true,true).produce(f.options,control.signal); + if(scenario==='cancel')setTimeout(()=>control.abort(),40); + await assert.rejects(promise,/timeout|flood|cancel/);noTerminal(f); + }); +} +test("already-cancelled invocation never starts child or emits terminal",async t=>{ + const f=fixture();subprocessFixtures(t,f);const control=new AbortController();control.abort(); + await assert.rejects(native.produce(f.options,control.signal),/cancel/);noTerminal(f); + assert.equal(fs.existsSync(f.log),false); +}); + +test("closed reader rejects terminal and evidence mutations independently", async t => { + const f=fixture();subprocessFixtures(t,f);await internal(true).produce(f.options); + const expected=expectations(f),root=f.options.output; + const original=Object.fromEntries(fs.readdirSync(root).map(name=>[name,fs.readFileSync(path.join(root,name))])); + const put=(name,body)=>{const file=path.join(root,name);fs.chmodSync(file,0o600);fs.writeFileSync(file,body);}; + for(const scenario of ["lane","subject","peer","source","attempt","producer-workflow","tool","unknown-key","duplicate-key","missing-command", + "missing-template","missing-installer","extra-evidence","missing-evidence","evidence-link","evidence-hardlink","evidence-digest","traversal","noncanonical"]){ + await t.test(scenario,()=>{ + const file='agentplugins-terminal.json',v=JSON.parse(original[file]);let special; + if(scenario==='lane')v.lane='agentplugins/linux-arm64'; + if(scenario==='subject')v.subject.sha256=hash('wrong outer'); + if(scenario==='peer')v.peer_subject=v.subject; + if(scenario==='source')v.identity.commit='b'.repeat(40); + if(scenario==='attempt')v.producer.run_attempt++; + if(scenario==='producer-workflow')v.producer.workflow='.github/workflows/other.yml'; + if(scenario==='tool')v.tools.go.sha256=hash('wrong host Go'); + if(scenario==='unknown-key')v.provider_attempt_verified=true; + if(scenario==='missing-command')v.assertions.commands.agentplugins.pop(); + if(scenario==='missing-template')v.assertions.commands.agentplugins=v.assertions.commands.agentplugins.filter(x=>!x.startsWith('hybrid-stdio/')); + if(scenario==='missing-installer')v.assertions.installer=['add']; + if(scenario==='evidence-digest')v.evidence[0].sha256=hash('wrong transcript'); + if(scenario==='traversal')v.evidence[0].file='../transcripts.json'; + let bytes=c.encode(v); + if(scenario==='duplicate-key')bytes=Buffer.from(bytes.toString().replace('{','{"schema":"duplicate",')); + if(scenario==='noncanonical')bytes=Buffer.from(JSON.stringify(v)); + put(file,bytes); + if(scenario==='extra-evidence'){special=path.join(root,'extra.json');fs.writeFileSync(special,'{}');} + if(scenario==='missing-evidence'){fs.renameSync(path.join(root,'host.json'),path.join(f.sandbox,'host-held.json'));} + if(scenario==='evidence-link'){ + fs.renameSync(path.join(root,'host.json'),path.join(f.sandbox,'host-held.json')); + fs.symlinkSync(path.join(f.sandbox,'host-held.json'),path.join(root,'host.json')); + } + if(scenario==='evidence-hardlink'){special=path.join(f.sandbox,'host-alias.json');fs.linkSync(path.join(root,'host.json'),special);} + assert.throws(()=>native.readTerminals(root,f.root,f.pins,expected)); + if(special)fs.unlinkSync(special); + if(scenario==='evidence-link')fs.unlinkSync(path.join(root,'host.json')); + if(scenario==='evidence-link'||scenario==='missing-evidence')fs.renameSync(path.join(f.sandbox,'host-held.json'),path.join(root,'host.json')); + for(const [name,body]of Object.entries(original))put(name,body); + }); + } + assert.equal(native.readTerminals(root,f.root,f.pins,expected).length,2); +}); + +test("rehashed evidence cannot hide omitted commands, bad plans or false preservation",async t=>{ + const f=fixture();subprocessFixtures(t,f);const n=internal(true);await n.produce(f.options); + const root=f.options.output,expected=expectations(f); + const originals=Object.fromEntries(fs.readdirSync(root).map(file=>[file,fs.readFileSync(path.join(root,file))])); + function put(file,value){const out=path.join(root,file);fs.chmodSync(out,0o600);fs.writeFileSync(out,c.encode(value));} + for(const scenario of ['omitted','duplicate','wrong-json','changed-state','empty-preservation','changed-tree','wrong-build-info']){ + await t.test(scenario,()=>{ + const e=Object.fromEntries(Object.entries(originals).filter(([file])=>!file.endsWith('-terminal.json')).map(([file,b])=>[file,JSON.parse(b)])); + const rows=e['transcripts.json'].agentplugins; + if(scenario==='omitted')rows.splice(8,1); + if(scenario==='duplicate')rows[8]=rows[7]; + if(scenario==='wrong-json')rows[8].stdout='{"schema_version":1,"schema_version":1}'; + if(scenario==='changed-state')e['transcripts.json'].installer[0].after.state.push({path:'state-v2.json'}); + if(scenario==='empty-preservation'){e['preservation.json'].inputs_before=[];e['preservation.json'].inputs_after=[];} + if(scenario==='changed-tree')e['trees.json'].agentplugins.skill.manifest.name='other'; + if(scenario==='wrong-build-info')e['build-info.json'].agentplugins.Path='other'; + for(const [file,v]of Object.entries(e))put(file,v); + // Attacker updates outer evidence hashes too. Semantic replay must reject. + for(const p of c.PRODUCTS){const v=JSON.parse(originals[p+'-terminal.json']);v.evidence=v.evidence.map(pin=>({file:pin.file,...c.metadata(c.encode(e[pin.file]))}));put(p+'-terminal.json',v);} + assert.throws(()=>native.readTerminals(root,f.root,f.pins,expected)); + for(const [file,b]of Object.entries(originals)){fs.chmodSync(path.join(root,file),0o600);fs.writeFileSync(path.join(root,file),b);} + }); + } +}); + +test("closed configuration has no success, observer, executable or provider boolean seam",async()=>{ + for(const key of ['provider_attempt_verified','observer','run','accept','binary','skip_installer']){ + const f=fixture();f.options[key]=true;await assert.rejects(native.produce(f.options),/unexpected or missing fields/);noTerminal(f); + } + assert.deepEqual(Object.keys(native).sort(),['produce','readPreparation','readTerminals','writePreparation']); +}); + +test("host Go pin is independent of builder executable digest and checked before effects",async t=>{ + const f=fixture();subprocessFixtures(t,f); + const manifest=release.verifyProjectedPair(f.root,f.pins).manifest; + assert.notEqual(manifest.build.go_sha256,f.options.go_sha256); + f.options.go_sha256=hash('incorrect host pin'); + await assert.rejects(native.produce(f.options));noTerminal(f);assert.equal(fs.existsSync(f.log),false); +}); + +test("bounded JSON decoding rejects duplicate keys including escaped aliases",()=>{ + const parse=internal().test.jsonDocument; + for(const text of ['{"a":1,"a":2}','{"nested":{"a":1,"\\u0061":2}}','{"x":[{"a":1,"a":2}]}', + '[] trailing','{"a":','[1,]','["'+ 'x'.repeat(1024*1024)+'"]','['.repeat(65)+'0'+']'.repeat(65)]) + assert.throws(()=>parse(text)); + assert.deepEqual(parse(' { "x": [1, {"a":true}], "nested":{"a":false} } '),{x:[1,{a:true}],nested:{a:false}}); +}); + +test("tree decoder rejects aliases, special modes, traversal and incomplete parents",()=>{ + const check=internal().test.treeShape; + const root={path:'.',mode:448,kind:'directory'}; + const file={path:'plugin.json',mode:420,kind:'file',size:1,sha256:hash('x')}; + check([root,file]); + for(const entries of [[],[file],[root,file,file],[root,{...file,path:'../outside'}], + [root,{...file,path:'/absolute'}],[root,{...file,path:'missing/child'}],[root,{...file,kind:'symlink'}], + [root,{...file,mode:0o4777}],[root,{...file,size:-1}],[root,{...file,sha256:null}],[root,{...file,extra:true}]]) + assert.throws(()=>check(entries)); +}); + +test("owned child cleanup denial rejects without any terminal",async t=>{ + const f=fixture();subprocessFixtures(t,f); + const kill=process.kill; + // This only models the post-exit check of our own fixture group. It never + // touches another worker or probes a real observation service. + t.mock.method(process,'kill',(pid,signal)=>{ + if(signal===0){const error=new Error('fixture cleanup denial');error.code='EPERM';throw error;} + return kill(pid,signal); + }); + await assert.rejects(internal(true).produce(f.options),/cleanup uncertain/);noTerminal(f); +}); + +test("failed second exclusive terminal write removes both owned terminal names",async t=>{ + const f=fixture();subprocessFixtures(t,f); + const open=fs.openSync,write=fs.writeFileSync;let fd; + t.mock.method(fs,'openSync',(file,...args)=>{ + const opened=open(file,...args); + if(typeof file==='string'&&file===path.join(f.options.output,'plugin-kit-ai-terminal.json'))fd=opened; + return opened; + }); + t.mock.method(fs,'writeFileSync',(file,...args)=>{ + if(fd!==undefined&&file===fd){fd=undefined;const error=new Error('fixture terminal capacity failure');error.code='ENOSPC';throw error;} + return write(file,...args); + }); + await assert.rejects(internal(true).produce(f.options),/terminal capacity/);noTerminal(f); +}); + +test("input modes and candidate/projection bytes remain unchanged by receipt creation",()=>{ + const f=fixture(),n=internal(),before=n.test.tree(f.root); + assert.throws(()=>native.writePreparation(f.root,f.pins,f.options.preparation.producer),/EEXIST/); + assert.deepEqual(n.test.tree(f.root),before); + assert.equal(JSON.parse(c.readFile(path.join(f.root,'pair-prepared.json'))).platform_acceptance,false); + assert.equal(JSON.parse(c.readFile(path.join(f.root,'agentplugins/release-manifest.json'))).attested,false); +}); + +test("changed host/source/attempt expectations cannot read a matching local terminal",async t=>{ + const f=fixture();subprocessFixtures(t,f);await internal(true).produce(f.options); + for(const kind of ['source','workflow-sha','run','attempt','preparation-attempt','tool']){ + const expect=structuredClone(expectations(f)); + if(kind==='source')expect.producer.source='b'.repeat(40); + if(kind==='workflow-sha')expect.producer.workflow_sha='b'.repeat(40); + if(kind==='run')expect.producer.run_id++; + if(kind==='attempt')expect.producer.run_attempt++; + if(kind==='preparation-attempt')expect.preparation.producer.run_attempt++; + if(kind==='tool')expect.tools.go.sha256=hash('different host Go'); + assert.throws(()=>native.readTerminals(f.options.output,f.root,f.pins,expect)); + } +}); + +test("all thirteen promotion lanes still reject independently of N1 registration",()=>{ + const f=fixture(); + for(const lane of f.record.qualification.lanes)lane.schema='authoring-frozen-native/v1'; + assert.equal(promotion.LANES.length,13); + assert.throws(()=>promotion.requireNativeContracts(f.record.qualification.lanes),/NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); + assert.equal(f.record.qualification.lanes.at(-1).lane,'public-packed-pair'); +}); + +test("input and output placement reject overlap before subprocess effects",async t=>{ + for(const kind of ['input','peer-output','tool-directory','existing']){ + const f=fixture(); + if(kind==='input')f.options.output=path.join(f.root,'nested-output'); + if(kind==='peer-output')f.options.output=f.options.work; + if(kind==='tool-directory')f.options.output=path.join(path.dirname(f.go),'nested-output'); + if(kind==='existing')fs.mkdirSync(f.options.output); + await assert.rejects(native.produce(f.options));noTerminal(f); + assert.equal(fs.existsSync(f.options.work),false); + } +}); From 8015bb63cfe284eb33957f6476d0bb29dbce7d84 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 8 Sep 2026 18:25:14 +0000 Subject: [PATCH 2/8] fix(authoring): bind native qualification evidence and lifecycle Refs #210, #203, #208. Keep production observation and native admission fail-closed pending actual execution and independent review. --- .../scripts/authoring-native-qualification.js | 365 ++++++++++++++++-- .../authoring-native-qualification.test.js | 319 +++++++++++++-- 2 files changed, 621 insertions(+), 63 deletions(-) diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js index 0646d047..4fa51f21 100644 --- a/npm/agentplugins/scripts/authoring-native-qualification.js +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -7,6 +7,7 @@ const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const cp = require("node:child_process"); +const crypto = require("node:crypto"); const assert = require("node:assert/strict"); const c = require("./dual-authoring-candidate"); const { verifyProjectedPair } = require("./authoring-release"); @@ -21,7 +22,41 @@ const LIMIT = 1024 * 1024; const CASES = Object.freeze(["skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"]); const LEAVES = Object.freeze(["author.capabilities", "author.compat", "author.doctor", "author.init", "author.inspect", "author.skills.init", "author.skills.validate", "author.test", "author.validate"]); -const FILES = Object.freeze(["transcripts.json", "trees.json", "build-info.json", "preservation.json", "preparation.json", "host.json"]); +const FILES = Object.freeze(["transcripts.json", "trees.json", "build-info.json", "preservation.json", "preparation.json", "host.json", "scans.json", "acquisition.json"]); +// Fixed identities from conformance/profile.go, specregistry/registry.go, +// readiness.Engine and domain.ClientDefinitions at this reviewed source. +const SCHEMAS = [ + { id: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", digest: "sha256:0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883" }, + { id: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", digest: "sha256:6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb" } +]; +const PROFILES = [ + { id: "agent-plugins/1.0.0", revision: "ff8ab5e392cc87bd88d87c060815a87490e51003", digest: "sha256:97a658b7dca3ce1b4c2266b95da300fa51d9dc4ade59d73168e5f9104272da18" }, + { id: "agent-skills/2026-09-06", revision: "69ef37e9424c0a7ea9dd2293b559e43ec8176379", digest: "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220" }, + ...SCHEMAS.map(x => ({ ...x, revision: "1.0.0" })), + { id: "author-document-bounds/v1", revision: "1", digest: "sha256:4b8ab8fd50481ccd1a0b777dcbbfa06cf89516a5ea61ce09d56d6dd6a2c43004" } +]; +function capabilities() { + const rows = [ + ["chatgpt", "compatibility_projection", "projected", "unsupported", "unsupported"], + ["claude", "compatibility_projection", "projected", "projected", "unsupported", "automatic"], + ["cline", "native", "native", "native", "unsupported", "automatic"], + ["codex", "compatibility_projection", "projected", "projected", "unsupported"], + ["copilot", "native", "native", "native", "native"], ["cursor", "native", "native", "native", "native"], + ["gemini", "native", "native", "native", "unsupported"], ["kiro", "native", "native", "native", "unsupported"], + ["opencode", "prepared_package", "prepared", "prepared", "unsupported", "automatic"], + ["vscode", "prepared_package", "prepared", "prepared", "prepared"], + ["windsurf", "prepared_package", "prepared", "prepared", "prepared"] + ]; + return { schemas: SCHEMAS, profiles: PROFILES, commands: LEAVES, + evidence_limits: ["static_only", "no_path_lookup", "no_executable_version_probe", "no_runtime_or_oauth_evidence", "native_files_metadata_only"], + clients: rows.map(([client_id, package_mode, skill_support, mcp, extension_support, activation_mode = "manual"]) => ({ + client_id, package_mode, activation_mode, scopes: ["user"], skill_support, + mcp_transports: { stdio: mcp, "streamable-http": mcp, sse: mcp }, + app_support: client_id === "chatgpt" ? "projected" : "unsupported", extension_support })) }; +} +const POLICY = { id: "agent-plugin-install", version: 2, + digest: "sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037" }; +const BROKEN = "---\nname: [\n---\nBroken\n"; const TOP = ["schema", "lane", "identity", "candidate_sha256", "pair_marker_sha256", "projection_pins", "subject", "peer_subject", "preparation", "producer", "host", "tools", "assertions", "evidence"]; const fail = message => { throw new Error(message); }; @@ -139,35 +174,60 @@ function subprocess(file, argv, ctx, signal, installer = false) { if (signal?.aborted) return reject(new Error("cancelled before process")); const child = cp.spawn(file, argv, { cwd: path.join(ctx.root, "projects"), env: ctx.env, shell: false, detached: true, stdio: ["ignore", "pipe", "pipe"] }); - let stdout = [], stderr = [], bytes = 0, problem, closed = false; + let stdout = [], stderr = [], bytes = 0, problem, closed = false, settled = false, stopped = false, grace; + const finish = error => { + if (settled) return; settled = true; + clearTimeout(timer); clearTimeout(grace); signal?.removeEventListener("abort", cancel); + if (error) reject(new Error(error)); + }; + const uncertain = () => { + let detail = ""; + try { child.stdout.destroy?.(); child.stderr.destroy?.(); child.unref?.(); } + catch (e) { detail = `; stream detach failed: ${e.code || "unknown"}`; } + finish(`${problem}; cleanup uncertain: child close not observed${detail}`); + }; const stop = reason => { - problem ||= reason; + if (stopped || settled) return; + stopped = true; problem ||= reason; if (child.pid && !closed) { try { process.kill(-child.pid, "SIGKILL"); } - catch (e) { if (e.code !== "ESRCH") problem = `owned-child cleanup denied: ${e.code}`; } + catch (e) { if (e.code !== "ESRCH") { problem += `; owned-child cleanup denied: ${e.code}`; uncertain(); return; } } } + // One cleanup attempt only. A missing close (including inherited pipes) + // must not hold failure diagnostics hostage. Unref is not cleanup proof. + grace = setTimeout(uncertain, 1000); }; const timer = setTimeout(() => stop("timeout"), installer ? 120000 : 15000); const cancel = () => stop("cancelled"); signal?.addEventListener("abort", cancel, { once: true }); for (const [stream, chunks] of [[child.stdout, stdout], [child.stderr, stderr]]) stream.on("data", b => { + if (settled) return; bytes += b.length; if (bytes > LIMIT) stop("output flood"); else chunks.push(b); }); - child.on("error", e => { problem = `subprocess failed: ${e.code}`; }); + child.on("error", e => stop(`subprocess failed: ${e.code}`)); child.on("close", (status, sig) => { - closed = true; clearTimeout(timer); signal?.removeEventListener("abort", cancel); + closed = true; + if (settled) return; // Never retry denied cleanup after a late close. if (child.pid) { - try { process.kill(-child.pid, 0); problem ||= "owned descendants survived"; process.kill(-child.pid, "SIGKILL"); } - catch (e) { if (e.code !== "ESRCH") problem ||= `cleanup uncertain: ${e.code}`; } + try { + process.kill(-child.pid, 0); problem ||= "owned descendants survived"; + if (!stopped) { + stopped = true; + try { process.kill(-child.pid, "SIGKILL"); } + catch (e) { if (e.code !== "ESRCH") problem += `; owned-child cleanup denied: ${e.code}`; } + } + } catch (e) { if (e.code !== "ESRCH") problem ||= `cleanup uncertain: ${e.code}`; } } - if (problem || sig) return reject(new Error(problem || `signal ${sig}`)); + if (problem || sig) return finish(problem || `signal ${sig}`); try { const decoder = new TextDecoder("utf-8", { fatal: true }); - resolve({ status, stdout: decoder.decode(Buffer.concat(stdout)), stderr: decoder.decode(Buffer.concat(stderr)) }); - } catch { reject(new Error("invalid UTF-8 subprocess output")); } + const result = { status, stdout: decoder.decode(Buffer.concat(stdout)), stderr: decoder.decode(Buffer.concat(stderr)) }; + finish(); resolve(result); + } catch { finish("invalid UTF-8 subprocess output"); } }); }); } + function initArgs(lane) { const args = ["init", lane, "--template", lane.startsWith("hybrid-") ? "hybrid" : lane]; if (lane.startsWith("hybrid-")) args.push("--mcp-template", lane.endsWith("remote") ? "mcp-remote" : "mcp-stdio"); @@ -229,7 +289,7 @@ function authorResult(row, spec, product, identity) { exact(d.runtime_evidence.status, "not_evaluated"); if (spec.id === "author-help" || spec.id === "capabilities") exact(d.commands, LEAVES); if (spec.id === "engine-version") { exact(d.product, product); exact(d.product_version, identity.versions[product]); return; } - if (spec.id === "capabilities") { assert.ok(d.capabilities); return; } + if (spec.id === "capabilities") { exact(d.capabilities, capabilities(), "fixed capabilities inventory"); return; } if (spec.id === "author-help") { assert.ok(d.help.use.startsWith(product === "agentplugins" ? "agentplugins author" : "plugin-kit-ai")); return; } const mutation = /\/(init|extra-skill)$/.test(spec.id); if (mutation) { exact(d.committed, true); exact(d.effects.committed, true); } @@ -237,11 +297,8 @@ function authorResult(row, spec, product, identity) { if (!spec.lane || /\/(existing)$/.test(spec.id) || spec.id === "installer-flag") return; assert.match(d.identity.tree_digest, /^sha256:[0-9a-f]{64}$/); exact(d.identity.read_profile, "packageview-local-linux-v1"); - assert.ok(Array.isArray(d.profiles) && d.profiles.length > 0, "embedded Skills profile"); - for (const profile of d.profiles) { assert.ok(profile.id && profile.revision); assert.match(profile.digest, /^sha256:[0-9a-f]{64}$/); } - assert.ok(d.profiles.some(p => p.id === "agent-skills/2026-09-06" && - p.revision === "69ef37e9424c0a7ea9dd2293b559e43ec8176379" && - p.digest === "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220"), "pinned Skills rules"); + exact(d.profiles, PROFILES, "exact conformance profiles"); + exact(d.schema_ids, (spec.lane === "skill" ? [SCHEMAS[0].id] : SCHEMAS.map(x => x.id).sort()), "accepted package schema inventory"); exact(d.loadability.status, "pass"); exact(d.normative_conformance.status, spec.id === "malformed-skill" ? "fail" : "pass"); exact(d.authoring_readiness.status, spec.id === "malformed-skill" ? "fail" : "pass"); @@ -276,25 +333,55 @@ function treeShape(entries) { c.keys(v, v.kind === "directory" ? ["path", "mode", "kind"] : ["path", "mode", "kind", "sha256", "size"], "tree entry"); assert.ok(typeof v.path === "string" && v.path.length <= 512 && !names.has(v.path)); assert.ok(v.path === "." || /^(?!\.\.?($|\/))[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(v.path)); - assert.ok(!v.path.split("/").includes("..")); names.add(v.path); + assert.ok(v.path === "." || v.path.split("/").every(x => x !== "." && x !== "..")); names.add(v.path); assert.ok(Number.isInteger(v.mode) && v.mode >= 0 && v.mode <= 511); assert.ok(v.kind === "file" || v.kind === "directory"); if (v.kind === "file") { sha(v.sha256); assert.ok(Number.isSafeInteger(v.size) && v.size >= 0); bytes += v.size; } } - assert.ok(names.has(".") && bytes <= 16 * LIMIT); + assert.ok(entries[0]?.path === "." && entries[0].kind === "directory" && bytes <= 16 * LIMIT); for (const v of entries) if (v.path !== ".") { const parent = path.posix.dirname(v.path); assert.ok(entries.some(x => x.path === parent && x.kind === "directory"), "tree parent closure"); } } +// Base64 preserves exact bounded bytes, including empty/non-UTF8 generated +// files. Entries remain no-link inventories; no evidence path is opened here. +function capturedBytes(entries, documents) { + c.keys(documents, entries.filter(x => x.kind === "file").map(x => x.path), "referenced byte closure"); + const result = Object.create(null); let total = 0; + for (const item of entries.filter(x => x.kind === "file")) { + const value = documents[item.path]; assert.equal(typeof value, "string"); + assert.ok(value.length <= 24 * LIMIT); + const bytes = Buffer.from(value, "base64"); exact(bytes.toString("base64"), value); + exact(c.metadata(bytes), { size: item.size, sha256: item.sha256 }, "referenced bytes"); + total += bytes.length; assert.ok(total <= 16 * LIMIT); result[item.path] = bytes; + } + return result; +} +// Exact packagedigest/snapshot.go v1 framing for this fixed no-link journey. +// Hash content, not per-file digest text, and exclude the synthetic root entry. +function packageIdentity(project) { + treeShape(project.files); const bytes = capturedBytes(project.files, project.documents); + const h = crypto.createHash("sha256"); + const length = n => { const b = Buffer.alloc(8); b.writeBigUInt64BE(BigInt(n)); h.update(b); }; + const frame = text => { const b = Buffer.from(text); length(b.length); h.update(b); }; + frame("agentplugins.package-tree\0sha256\0v1"); + for (const item of project.files.filter(x => x.path !== ".").sort((a, b) => a.path < b.path ? -1 : 1)) { + const body = item.kind === "file" ? bytes[item.path] : Buffer.alloc(0); + for (const text of ["entry", item.path, item.kind, item.kind === "directory" ? "040000" : item.mode & 0o111 ? "100755" : "100644", ""]) frame(text); + length(body.length); h.update(body); + } + return { tree_digest: `sha256:${h.digest("hex")}`, manifest_digest: `sha256:${c.digest(bytes["plugin.json"])}` }; +} function projectEvidence(root, lane) { const files = tree(root); const manifest = JSON.parse(c.readFile(path.join(root, "plugin.json"), LIMIT)); const mcp = lane === "skill" ? null : JSON.parse(c.readFile(path.join(root, "mcp.json"), LIMIT)); const lock = lane.endsWith("stdio") ? JSON.parse(c.readFile(path.join(root, "package-lock.json"), LIMIT)) : null; const pkg = lock ? JSON.parse(c.readFile(path.join(root, "package.json"), LIMIT)) : null; - const documents = Object.fromEntries(["plugin.json", ...(mcp ? ["mcp.json"] : []), ...(lock ? ["package.json", "package-lock.json"] : [])] - .map(n => [n, c.readFile(path.join(root, n), LIMIT).toString("utf8")])); + const documents = Object.fromEntries(files.filter(x => x.kind === "file").map(x => + [x.path, (x.size ? c.readFile(path.join(root, x.path), 16 * LIMIT) : Buffer.alloc(0)).toString("base64")])); + exact(tree(root), files, "project bytes and modes stable during capture"); return { files, manifest, mcp, lock, package: pkg, documents }; } function checkProject(value, lane) { @@ -302,11 +389,11 @@ function checkProject(value, lane) { treeShape(value.files); assert.ok(value.files.length > 3); const documents = { "plugin.json": value.manifest, ...(value.mcp ? { "mcp.json": value.mcp } : {}), ...(value.lock ? { "package.json": value.package, "package-lock.json": value.lock } : {}) }; - c.keys(value.documents, Object.keys(documents), "exact project documents"); + const captured = capturedBytes(value.files, value.documents); for (const [name, parsed] of Object.entries(documents)) { - exact(jsonDocument(value.documents[name]), parsed); + exact(jsonDocument(captured[name].toString("utf8")), parsed); const pin = value.files.find(x => x.path === name); assert.ok(pin); - exact([pin.sha256, pin.size], [c.digest(Buffer.from(value.documents[name])), Buffer.byteLength(value.documents[name])]); + exact([pin.sha256, pin.size], [c.digest(captured[name]), captured[name].length]); } const names = value.files.map(x => x.path); exact(new Set(names).size, names.length); @@ -339,46 +426,166 @@ function installationCommands() { { id: "list", args: ["list", "--format=json"] } ]; } -function scanEvidence(r, source) { +function scanEvidence(r, source, project) { const data = r.data.security ? r.data : r.data.targets?.[0]?.output; const security = data?.security; - assert.ok(security && security.scanned_files > 0, "production scan evidence"); - exact(security.scanner, { id: "lintai", version: "0.1.3" }); + assert.ok(security, "production scan evidence"); + c.keys(security, ["schema_version", "subject", "scanner", "policy", "outcome", "counts", "scanned_files", "report_digest", "evidence_source", + ...(Object.hasOwn(security, "findings") ? ["findings"] : [])], "security assessment"); + exact(security.schema_version, 1, "fixed production security schema"); + exact(security.scanner, { id: "lintai", version: "0.1.3" }); exact(security.policy, POLICY, "fixed production security policy"); exact(security.evidence_source, source); exact(security.outcome, "no_blocking_findings"); - exact(security.subject, { tree_digest: data.tree_digest, manifest_digest: data.manifest_digest }); + // This journey already requires no warnings or blocking findings. In this + // outcome the production counts and published finding projection are empty. + exact(security.counts, { blocking: 0, warnings: 0, total: 0 }, "security counts match outcome"); + exact(security.findings ?? [], [], "security findings match counts"); positive(security.scanned_files); + const subject = packageIdentity(project); + exact(security.subject, subject, "independently captured package security subject"); + exact({ tree_digest: data.tree_digest, manifest_digest: data.manifest_digest }, subject); assert.match(security.report_digest, /^sha256:[0-9a-f]{64}$/); + return security; +} +function acquisition(state, bodies) { + const root = { path: ".", mode: 448, kind: "directory" }; + assert.ok(Array.isArray(state.acquisition)); treeShape([root, ...state.acquisition]); + for (const item of state.acquisition) { + const directory = /^(?:security(?:\/(?:lintai|assessments))?|security\/lintai\/0\.1\.3(?:\/linux-amd64(?:-musl)?)?)$/.test(item.path); + const file = /^(?:security\/lintai\/0\.1\.3\/linux-amd64(?:-musl)?\/lintai|security\/assessments\/[0-9a-f]{64}\.json|(?:directory|discovery)-v1-cache\.json)$/.test(item.path); + assert.ok(directory || file, "fixed acquisition paths"); exact(item.kind, directory ? "directory" : "file"); + } + return capturedBytes(state.acquisition, Object.fromEntries(state.acquisition.filter(x => x.kind === "file") + .map(x => [x.path, bodies[x.sha256]]))); +} +function acquisitionClosure(rows, bodies) { + const files = rows.flatMap(row => [row.before, row.after].flatMap(state => state.acquisition.filter(x => x.kind === "file"))); + const pins = [...new Map(files.map(x => [x.sha256, x])).values()]; + c.keys(bodies, pins.map(x => x.sha256), "closed acquisition byte subjects"); + let total = 0; + for (const item of pins) { total += item.size; assert.ok(total <= 16 * LIMIT, "total acquisition byte bound"); } + for (const row of rows) for (const state of [row.before, row.after]) acquisition(state, bodies); +} +function scanReplay(rows, projects, scans, bodies) { + assert.ok(Array.isArray(scans)); exact(scans.length, 3, "three observed fresh scans and report bytes"); + const fresh = new Map(); let executable; + for (let i = 0; i < 4; i++) { + const row = rows[i], lane = i < 3 ? row.id.slice(8) : "skill"; + const assessment = scanEvidence(jsonDocument(row.stdout), i < 3 ? "local_scan" : "cache", projects[lane]); + const key = c.digest(Buffer.from([assessment.subject.tree_digest, assessment.subject.manifest_digest, + "lintai", "0.1.3", POLICY.id, String(POLICY.version), POLICY.digest].join("\0"))); + const file = `security/assessments/${key}.json`, before = acquisition(row.before, bodies), after = acquisition(row.after, bodies); + const cached = { ...assessment }; delete cached.evidence_source; + assert.ok(after[file], "required assessment cache bytes"); exact(jsonDocument(after[file].toString("utf8")), cached); + if (i < 3) { + assert.equal(before[file], undefined, "fresh scan must precede cache creation"); + const scan = scans[i]; c.keys(scan, ["id", "args", "subject", "executable", "report"], "observed scanner call"); + exact([scan.id, scan.args, scan.subject], [row.id, ["scan-agent-plugin", `/${lane}`], assessment.subject]); + c.keys(scan.executable, ["path", "sha256"], "observed scanner executable"); + assert.match(scan.executable.path, /^security\/lintai\/0\.1\.3\/linux-amd64(?:-musl)?\/lintai$/); + assert.ok(after[scan.executable.path], "required scanner acquisition bytes"); + exact(c.digest(after[scan.executable.path]), sha(scan.executable.sha256)); + const item = row.after.acquisition.find(x => x.path === scan.executable.path); exact(item.mode, 0o700); + if (executable) exact(scan.executable, executable, "same acquired scanner"); else executable = scan.executable; + assert.equal(typeof scan.report, "string"); + const report = jsonDocument(scan.report); + exact(`sha256:${c.digest(Buffer.from(scan.report))}`, assessment.report_digest, "exact scanner report bytes"); + exact(report.schema_version, 1); exact(report.tool, { name: "lintai", version: "0.1.3" }); + exact([report.policy.id, report.policy.version], [POLICY.id, POLICY.version], "scanner report policy"); + assert.ok(Array.isArray(report.policy.presets) && report.policy.presets.every(x => typeof x === "string")); + exact(report.stats.scanned_files, assessment.scanned_files, "scanner report file counts"); + assert.ok(Number.isSafeInteger(report.stats.skipped_files) && report.stats.skipped_files >= 0); + exact(report.findings, [], "scanner report findings"); exact(report.runtime_errors ?? [], []); + assert.ok(Array.isArray(report.diagnostics ?? [])); + fresh.set(lane, { assessment: cached, bytes: after[file] }); + } else { + const preceding = fresh.get(lane); assert.ok(preceding, "preceding genuine scan required"); + exact(cached, preceding.assessment, "cache must reuse exact preceding scan"); + exact(before[file], preceding.bytes); exact(after[file], preceding.bytes); + exact(c.digest(before[executable.path]), executable.sha256); + exact(before[executable.path], after[executable.path]); + } + } +} +function stateDocument(state) { + const item = state.state.find(x => x.path === "state-v2.json"); + if (!item) { exact(state.state_document, null); return { installations: [] }; } + assert.equal(typeof state.state_document, "string"); + exact(c.metadata(Buffer.from(state.state_document)), { sha256: item.sha256, size: item.size }); + const document = jsonDocument(state.state_document); exact(document.schema_version, 4); + assert.ok(Array.isArray(document.installations)); return document; +} +function installedIdentity(state, project) { + const document = stateDocument(state); exact(document.installations.length, 1); + const registration = document.installations[0], subject = packageIdentity(project); + exact(registration.declared_name, "skill"); exact(registration.package.declared_name, "skill"); + exact(registration.package.version, "0.1.0"); exact(registration.source.tree_digest, subject.tree_digest); + exact(registration.package.manifest_digest, subject.manifest_digest); + assert.match(registration.installation_id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + const clients = Object.values(registration.clients); exact(clients.length, 1); + const client = clients[0]; exact([client.client_id, client.scope, client.materialization], ["codex", "user", "materialized"]); + const physical = `skill-${c.digest(Buffer.from(registration.installation_id)).slice(0, 12)}`; + exact(client.physical_artifact_id, physical); + const projection = `managed/clients/codex/${physical}`; + assert.ok(typeof client.target_locator === "string" && path.posix.isAbsolute(client.target_locator) && + path.posix.normalize(client.target_locator) === client.target_locator && client.target_locator.endsWith(`/${projection}`)); + const binding = "client_" + c.digest(Buffer.from([registration.installation_id, "codex", "user", client.target_locator].join("\0"))).slice(0, 24); + exact(Object.keys(registration.clients), [binding]); exact(client.client_binding_id, binding); + exact([client.package_revision.version, client.package_revision.tree_digest, client.package_revision.manifest_digest], + ["0.1.0", subject.tree_digest, subject.manifest_digest]); + assert.ok(state.state.some(x => x.path === projection && x.kind === "directory"), "owned client projection exists"); + // Skills are copied without changes into the Codex compatibility projection. + for (const skill of project.files.filter(x => /^skills\/[^/]+\/SKILL.md$/.test(x.path))) { + const copied = state.state.find(x => x.path === `${projection}/${skill.path}`); + assert.ok(copied && copied.kind === "file"); exact([copied.sha256, copied.size], [skill.sha256, skill.size]); + } + return { registration, client, projection }; } function installed(row, spec, projects) { const r = envelope(row, { ...spec, status: 0 }), verb = spec.args[0]; exact(r.command, verb); if (spec.id.startsWith("dry-run/")) { exact(r.data.dry_run, true); - scanEvidence(r, "local_scan"); + scanEvidence(r, "local_scan", projects[spec.id.slice(8)]); const lane = spec.id.slice(8), result = lifecycleResult(r, "add", "codex"), plan = result.plan; exact([plan.client_id, plan.scope, plan.status], ["codex", "user", "manual_activation_required"]); const skills = projects[lane].files.filter(x => /^skills\/[^/]+\/SKILL.md$/.test(x.path)).map(x => `skill:${x.path.split("/")[1]}`); const servers = Object.keys(projects[lane].mcp?.mcpServers || {}).map(x => `mcp_server:${x}`); exact(plan.components.map(x => `${x.kind}:${x.name}`).sort(), [...skills, ...servers].sort()); - assert.ok(plan.components.every(x => x.support && x.support !== "unsupported")); + assert.ok(plan.components.every(x => x.support === "projected"), "Codex compatibility projection support"); exact(row.before.client, row.after.client); exact(row.before.state, row.after.state); return; } if (["add", "update", "remove"].includes(verb)) { const result = lifecycleResult(r, verb, "codex"); exact(result.mutated, verb !== "update"); + const identity = installedIdentity(verb === "add" ? row.after : row.before, projects.skill); + exact(result.installation_id, identity.registration.installation_id, "lifecycle installed identity"); if (verb === "update") { exact(result.no_change, true); exact(row.before.state, row.after.state); exact(row.before.client, row.after.client); } else assert.notDeepEqual(row.before.state, row.after.state, "real lifecycle state mutation"); if (verb === "add") { exact(result.activation.authentication, "not_checked"); assert.notEqual(result.activation.authentication_attested, true); - assert.notDeepEqual(row.before.client, row.after.client, "real client projection"); + exact(row.before.client, row.after.client, "preexisting client configuration preservation"); + exact(stateDocument(row.before).installations, [], "new installation starts unregistered"); + assert.ok(!row.before.state.some(x => x.path === identity.projection || x.path.startsWith(identity.projection + "/")), "new owned projection"); assert.ok(row.after.state.some(x => x.path === "state-v2.json"), "production lifecycle state"); - scanEvidence(r, "cache"); // The preceding fresh dry-run scanned these identical bytes. + scanEvidence(r, "cache", projects.skill); // The preceding fresh dry-run scanned these identical bytes. + } + if (verb === "remove") { + assert.ok(!row.after.state.some(x => x.path === identity.projection || x.path.startsWith(identity.projection + "/")), "remove owned projection"); + exact(stateDocument(row.after).installations, [], "remove registration"); + assert.ok(!row.after.state.some(x => x.path.startsWith("managed/clients/codex/") && x.kind === "file"), "no remaining Codex projection files"); + exact(row.before.client, row.after.client, "remove preserves client configuration"); } } else if (verb === "info") { + exact(row.before, row.after, "info is read only"); + const identity = installedIdentity(row.before, projects.skill); + exact(r.data.installation_id, identity.registration.installation_id, "info installed identity"); exact(r.data.name, "skill"); exact(r.data.version, "0.1.0"); exact(r.data.clients.length, 1); exact(r.data.clients[0].client_id, "codex"); - exact(r.data.clients[0].package_revision.version, "0.1.0"); - } else exact(r.data.installations, []); + exact(r.data.clients[0].package_revision, identity.client.package_revision); + } else { + exact(row.before, row.after, "list is read only"); + exact(r.data.installations, []); exact(stateDocument(row.after).installations, []); + } } // No supported, reviewed whole-descendant observer has been provisioned for // this contract. Never retry the denied ptrace flow, or treat a caller boolean, @@ -386,6 +593,60 @@ function installed(row, spec, projects) { function observationGate() { fail("WHOLE_OS_OBSERVATION_UNAVAILABLE: supported authoring process/network and owned-descendant observation remains a separate execution prerequisite; no passing terminal"); } +function commandContinuity(rows, projects) { + const sort = xs => xs.slice().sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); + let expected = [{ path: ".", mode: 448, kind: "directory" }]; + const broken = [ + { path: "skill/skills/broken", mode: 448, kind: "directory" }, + { path: "skill/skills/broken/SKILL.md", mode: 384, kind: "file", ...c.metadata(Buffer.from(BROKEN)) } + ]; + for (const row of rows) { + if (row.id === "malformed-skill") expected = sort([...expected, ...broken]); + exact(row.before, expected, `command continuity before ${row.id}`); + if (/\/(init|extra-skill)$/.test(row.id)) { + const [lane, operation] = row.id.split("/"); + let files = projects[lane].files; + if (operation === "init") files = files.filter(x => !/^skills\/extra-skill(?:\/|$)/.test(x.path) && + !(x.path === "skills" && lane.startsWith("mcp-"))); + const added = files.map(x => ({ ...x, path: x.path === "." ? lane : `${lane}/${x.path}` })); + const keep = expected.filter(x => x.path !== lane && !x.path.startsWith(lane + "/")); + if (operation === "init") assert.ok(!expected.some(x => x.path === lane)); + else for (const old of expected.filter(x => x.path === lane || x.path.startsWith(lane + "/"))) + exact(added.find(x => x.path === old.path), old, "extra Skill preserves all existing bytes and modes"); + expected = sort([...keep, ...added]); + assert.notDeepEqual(row.before, expected, "declared authoring mutation has effects"); + } + exact(row.after, expected, `command effects and final project tree ${row.id}`); + if (row.id !== "product-help") { + const data = jsonDocument(row.stdout).data; + if (data.identity) { + const lane = row.id === "malformed-skill" ? "skill" : row.id.split("/")[0]; + const files = row.after.filter(x => x.path === lane || x.path.startsWith(lane + "/")) + .map(x => ({ ...x, path: x.path === lane ? "." : x.path.slice(lane.length + 1) })); + const documents = Object.fromEntries(files.filter(x => x.kind === "file").map(x => [x.path, + x.path === "skills/broken/SKILL.md" ? Buffer.from(BROKEN).toString("base64") : projects[lane].documents[x.path]])); + exact(data.identity.tree_digest, packageIdentity({ files, documents }).tree_digest, "author result describes observed command tree"); + } + } + if (row.id === "malformed-skill") expected = expected.filter(x => !broken.some(b => b.path === x.path)); + } + exact(expected, sort([{ path: ".", mode: 448, kind: "directory" }, ...CASES.flatMap(lane => + projects[lane].files.map(x => ({ ...x, path: x.path === "." ? lane : `${lane}/${x.path}` })))]), "final command tree closure"); +} +function custodyCoverage(entries, preparation) { + const subjects = [...preparation.subjects, { file: "preparation-run.json", ...c.metadata(c.encode(preparation)) }]; + assert.ok(Array.isArray(entries)); exact(entries.length, 19, "eighteen inputs plus preparation custody"); + exact(entries.map(x => x.file), subjects.map(x => x.file)); + const inodes = new Set(); + entries.forEach((v, i) => { + c.keys(v, ["file", "mode", "dev", "ino", "links", "size", "mtime", "ctime"], "input custody fields"); + exact(v.size, subjects[i].size); exact(v.links, 1); + for (const key of ["mode", "dev", "ino"]) assert.ok(Number.isSafeInteger(v[key]) && v[key] >= 0); + assert.ok(v.mode <= 0o177777); exact(v.mode & 0o170000, 0o100000); exact(v.mode & 0o7000, 0); + for (const key of ["mtime", "ctime"]) assert.ok(Number.isFinite(v[key]) && v[key] > 0); + const identity = `${v.dev}:${v.ino}`; assert.ok(!inodes.has(identity)); inodes.add(identity); + }); +} function verifyJourney(e, pins) { c.keys(e, FILES, "evidence bundle"); const transcript = e["transcripts.json"], projects = e["trees.json"]; @@ -401,6 +662,7 @@ function verifyJourney(e, pins) { } c.keys(projects[p], CASES, "five templates"); for (const lane of CASES) checkProject(projects[p][lane], lane); + commandContinuity(rows, projects[p]); } for (let i = 0; i < commands("agentplugins").length; i++) { const spec = commands("agentplugins")[i]; @@ -412,15 +674,24 @@ function verifyJourney(e, pins) { const row = transcript.installer[i]; c.keys(row, ["id", "args", "status", "stdout", "stderr", "before", "after"], "installer transcript"); for (const state of [row.before, row.after]) { - c.keys(state, ["client", "state", "acquisition"], "separate installer state and acquisition"); + c.keys(state, ["client", "state", "acquisition", "state_document"], "separate installer state and acquisition"); treeShape(state.client); treeShape(state.state); - assert.ok(Array.isArray(state.acquisition) && state.acquisition.length <= 4096); + acquisition(state, e["acquisition.json"]); stateDocument(state); } + if (i) exact(row.before, transcript.installer[i - 1].after, "installer state continuity"); + else { exact(row.before.acquisition, []); exact(stateDocument(row.before).installations, []); } + // Every original client file/directory survives every command, including modes. + for (const original of transcript.installer[0].before.client) + exact(row.after.client.find(x => x.path === original.path), original, "preexisting client preservation"); exact([row.id, row.args], [specs[i].id, specs[i].args]); installed(row, specs[i], projects.agentplugins); } + acquisitionClosure(transcript.installer, e["acquisition.json"]); + scanReplay(transcript.installer, projects.agentplugins, e["scans.json"], e["acquisition.json"]); const preserve = e["preservation.json"]; c.keys(preserve, ["inputs_before", "inputs_after", "projects_before", "projects_after", "author_homes_before", "author_homes_after", "custody_before", "custody_after"], "preservation"); for (const name of ["inputs", "projects", "author_homes", "custody"]) exact(preserve[`${name}_before`], preserve[`${name}_after`]); + custodyCoverage(preserve.custody_before, e["preparation.json"]); + custodyCoverage(preserve.custody_after, e["preparation.json"]); exact(preserve.projects_before, projects); exact(preserve.inputs_before, e["preparation.json"].subjects, "preserved eighteen independently pinned subjects"); c.keys(preserve.author_homes_before, c.PRODUCTS, "isolated author homes"); @@ -480,6 +751,7 @@ async function produce(options, signal) { assert.ok(!options.output.startsWith(options.work + "/") && !options.work.startsWith(options.output + "/") && options.work !== options.output); fs.mkdirSync(options.output, { mode: 0o700 }); fs.mkdirSync(options.work, { mode: 0o700 }); const e = {}, rows = {}, projects = {}, binaries = {}, build = {}, contexts = {}; + const acquisitionBodies = Object.create(null); let acquisitionTotal = 0; const frozenBefore = frozen.subjects.map(s => ({ file: path.relative(root, s.file), ...c.metadata(c.readFile(s.file)) })); const custodyBefore = inputCustody(root, frozen.subjects); const ownedTerminals = []; @@ -504,7 +776,7 @@ async function produce(options, signal) { for (const spec of commands(p)) { const malformed = path.join(projectRoot, "skill/skills/broken"); if (spec.id === "malformed-skill") { - fs.mkdirSync(malformed, { mode: 0o700 }); fs.writeFileSync(path.join(malformed, "SKILL.md"), "---\nname: [\n---\nBroken\n", { flag: "wx" }); + fs.mkdirSync(malformed, { mode: 0o700 }); fs.writeFileSync(path.join(malformed, "SKILL.md"), BROKEN, { flag: "wx", mode: 0o600 }); } const before = tree(projectRoot), r = await subprocess(binaries[p], spec.args, ctx, signal); const row = { id: spec.id, args: spec.args, ...r, before, after: tree(projectRoot) }; @@ -528,7 +800,16 @@ async function produce(options, signal) { // Genuine feed/scanner acquisition is retained separately. These are the // production cache names, not wildcard state exclusions or seeded inputs. const isAcquisition = x => /^(security(?:\/|$)|directory-v1-cache\.json$|discovery-v1-cache\.json$)/.test(x.path); - return { client: tree(client), state: all.filter(x => !isAcquisition(x)), acquisition: all.filter(isAcquisition) }; + const acquisition = all.filter(isAcquisition); + const stateFile = path.join(install.env.AGENTPLUGINS_HOME, "state-v2.json"); + for (const item of acquisition.filter(x => x.kind === "file")) { + const bytes = c.readFile(path.join(install.env.AGENTPLUGINS_HOME, item.path), 16 * LIMIT); + exact(c.metadata(bytes), { sha256: item.sha256, size: item.size }, "acquisition snapshot bytes"); + if (!Object.hasOwn(acquisitionBodies, item.sha256)) { acquisitionTotal += bytes.length; assert.ok(acquisitionTotal <= 16 * LIMIT, "total acquisition byte bound"); } + acquisitionBodies[item.sha256] = bytes.toString("base64"); + } + return { client: tree(client), state: all.filter(x => !isAcquisition(x)), acquisition, + state_document: all.some(x => x.path === "state-v2.json") ? c.readFile(stateFile, LIMIT).toString("utf8") : null }; }; for (const spec of installationCommands()) { const args = spec.args.map(a => a.replace("", path.join(contexts.agentplugins.root, "projects"))); @@ -550,10 +831,14 @@ async function produce(options, signal) { inputs_after: after.subjects.map(s => ({ file: path.relative(root, s.file), ...c.metadata(c.readFile(s.file)) })), projects_before: projects, projects_after: afterProjects, author_homes_before: homesBefore, author_homes_after: homesAfter, custody_before: custodyBefore, custody_after: inputCustody(root, after.subjects) }; - verifyJourney({ ...e, "host.json": {} }, pins); - observationGate(); // Unavailable capability is a failure diagnostic, never a pass stub. + e["acquisition.json"] = acquisitionBodies; + e["scans.json"] = observationGate(); // Unavailable capability is a failure diagnostic, never a pass stub. + // ReleaseScanner keeps neither its raw report stdout nor acquisition HTTP + // bytes. No scan record can be inferred from a cached assessment. A future + // reviewed observer must close that custody; production has no positive seam. e["host.json"] = { platform: process.platform, architecture: process.arch, machine: os.machine(), target: TARGET, observation: "whole-descendant-authoring-no-process-network/1" }; + verifyJourney(e, pins); const tools = { go: { sha256: options.go_sha256, version: "go1.25.13" }, node: { sha256: c.digest(c.readFile(process.execPath)), version: process.version } }; for (const file of FILES) fs.writeFileSync(path.join(options.output, file), c.encode(e[file]), { flag: "wx", mode: 0o400 }); if (signal?.aborted) fail("cancelled before terminal"); diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js index 4f0a10d4..d991e88d 100644 --- a/npm/agentplugins/test/authoring-native-qualification.test.js +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -31,6 +31,7 @@ function fixture() { instance.filename = original; instance.paths = Module._nodeModulePaths(__dirname); instance._compile(prefix + "\nmodule.exports = fixture;", original); const f = instance.exports(); + if (process.env.N1_FIXTURE_LOG) fs.appendFileSync(process.env.N1_FIXTURE_LOG, f.sandbox + "\n"); f.pins = { identity: structuredClone(ID), candidate_sha256: f.record.candidate_sha256, pair_marker_sha256: f.record.pair_marker_sha256, products: Object.fromEntries(c.PRODUCTS.map(p => [p, { manifest_sha256: f.record.products[p].manifest_sha256, checksums_sha256: f.record.products[p].checksums_sha256 }])) }; @@ -47,11 +48,11 @@ function fixture() { } function internal(observation = false, fastTimeout = false) { let source = fs.readFileSync(moduleFile, "utf8"); - if (observation) source = source.replace(' observationGate(); //', ' /* Test-only synthetic observation; not a native pass. */ //'); + if (observation) source = source.replace(' e["scans.json"] = observationGate(); //', ' e["scans.json"] = JSON.parse(fs.readFileSync(path.join(install.env.TMPDIR, "fixture-scans.json"))); // Test-only captured child fixtures.'); if (fastTimeout) source = source.replace('installer ? 120000 : 15000', '100'); // Expose lexical contracts only in this test VM. Production exports no policy, // verifier, command inventory, child executable or success switch. - source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape };'; + source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY };'; const Module = require("node:module"); const instance = new Module(moduleFile, module); instance.filename = moduleFile; instance.paths = Module._nodeModulePaths(path.dirname(moduleFile)); @@ -72,10 +73,7 @@ const sha = s => crypto.createHash('sha256').update(s).digest('hex'); const product = selected.includes('plugin-kit-ai') ? 'plugin-kit-ai' : 'agentplugins'; fs.appendFileSync(cfg.log, JSON.stringify({selected,argv,env:process.env})+'\n'); const scenario = cfg.scenario; -if (scenario === 'timeout') setInterval(()=>{},1000); -else if (scenario === 'flood') process.stdout.write('x'.repeat(2*1024*1024)); -else if (scenario === 'cancel') setInterval(()=>{},1000); -else main(); +const profiles = [{"id": "agent-plugins/1.0.0", "revision": "ff8ab5e392cc87bd88d87c060815a87490e51003", "digest": "sha256:97a658b7dca3ce1b4c2266b95da300fa51d9dc4ade59d73168e5f9104272da18"}, {"id": "agent-skills/2026-09-06", "revision": "69ef37e9424c0a7ea9dd2293b559e43ec8176379", "digest": "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220"}, {"id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "revision": "1.0.0", "digest": "sha256:0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883"}, {"id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "revision": "1.0.0", "digest": "sha256:6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb"}, {"id": "author-document-bounds/v1", "revision": "1", "digest": "sha256:4b8ab8fd50481ccd1a0b777dcbbfa06cf89516a5ea61ce09d56d6dd6a2c43004"}]; function output(value, status=0) { if (scenario === 'invalid-utf8') process.stdout.write(Buffer.from([0xff])); else if (scenario === 'bad-json') process.stdout.write('{bad'); @@ -113,7 +111,19 @@ function main() { if(scenario==='missing-command') data.commands.pop(); if(scenario==='deferred-command') data.commands.push('author.publish'); if(verb==='--help')data.help={use:product==='agentplugins'?'agentplugins author':'plugin-kit-ai'}; - else data.capabilities={schemas:[{id:'fixture-only'}]}; + else { + const kinds={chatgpt:['compatibility_projection','projected','unsupported','unsupported'],claude:['compatibility_projection','projected','projected','unsupported'], + cline:['native','native','native','unsupported'],codex:['compatibility_projection','projected','projected','unsupported'], + copilot:['native','native','native','native'],cursor:['native','native','native','native'],gemini:['native','native','native','unsupported'],kiro:['native','native','native','unsupported'], + opencode:['prepared_package','prepared','prepared','unsupported'],vscode:['prepared_package','prepared','prepared','prepared'],windsurf:['prepared_package','prepared','prepared','prepared']}; + data.capabilities={schemas:profiles.slice(2,4).map(p=>({id:p.id,digest:p.digest})),profiles,commands:data.commands, + evidence_limits:['static_only','no_path_lookup','no_executable_version_probe','no_runtime_or_oauth_evidence','native_files_metadata_only'], + clients:Object.entries(kinds).map(([id,k])=>({client_id:id,package_mode:k[0],activation_mode:['claude','cline','opencode'].includes(id)?'automatic':'manual', + scopes:['user'],skill_support:k[1],mcp_transports:{stdio:k[2],'streamable-http':k[2],sse:k[2]},app_support:id==='chatgpt'?'projected':'unsupported',extension_support:k[3]}))}; + if(scenario==='empty-capabilities')data.capabilities={}; + if(scenario==='missing-profile')data.capabilities.profiles=profiles.slice(1); + if(scenario==='wrong-schema')data.capabilities.schemas[0].digest='sha256:'+sha('wrong schema'); + } return done(); } if(args.includes('--force')||args.includes('--scope=user')||lane==='missing-destination')return done(2); @@ -135,8 +145,8 @@ function main() { } if(verb==='skills'&&args[1]==='init'){skill(root,'extra-skill');data.committed=true;data.effects.committed=true;} const malformed=fs.existsSync(path.join(root,'skills/broken')); - data.identity={read_profile:'packageview-local-linux-v1',tree_digest:'sha256:'+sha('tree '+root)}; - data.profiles=[{id:'agent-skills/2026-09-06',revision:'69ef37e9424c0a7ea9dd2293b559e43ec8176379',digest:'sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220'}]; + data.identity={read_profile:'packageview-local-linux-v1',tree_digest:identity(root).tree_digest}; + data.profiles=profiles;data.schema_ids=(root==='skill'?[profiles[2].id]:profiles.slice(2,4).map(x=>x.id).sort()); data.loadability={status:'pass'};data.normative_conformance={status:malformed?'fail':'pass'}; data.authoring_readiness={status:malformed?'fail':'pass'};data.release_policy={status:'not_evaluated'}; data.components=[{type:'skill',status:'pass'}];data.findings=malformed?[{severity:'error'}]:[]; @@ -155,7 +165,62 @@ function main() { } function write(root,name,value){fs.writeFileSync(path.join(root,name),typeof value==='string'?value:JSON.stringify(value)+'\n');} function skill(root,name){fs.mkdirSync(path.join(root,'skills',name),{recursive:true,mode:0o700});write(root,'skills/'+name+'/SKILL.md','---\nname: '+name+'\ndescription: Owned fixture\n---\nInstructions\n');} -function security(source){return {scanner:{id:'lintai',version:'0.1.3'},scanned_files:4,evidence_source:source,outcome:'no_blocking_findings',subject:{tree_digest:'sha256:'+sha('tree'),manifest_digest:'sha256:'+sha('manifest')},report_digest:'sha256:'+sha('report')};} +// Independent implementation of the existing uint64-BE package framing on +// actual child-created bytes (not the producer's package identity helper). +function identity(root) { + const parts=[]; + const number=n=>{const b=Buffer.alloc(8);b.writeBigUInt64BE(BigInt(n));return b;}; + const frame=x=>{const b=Buffer.from(x);parts.push(number(b.length),b);}; + frame('agentplugins.package-tree\0sha256\0v1'); + const entries=[]; + const walk=rel=>{for(const name of fs.readdirSync(path.join(root,rel)).sort()){ + const p=path.posix.join(rel,name),st=fs.statSync(path.join(root,p));entries.push([p,st]);if(st.isDirectory())walk(p); + }};walk('');entries.sort((a,b)=>a[0]/'+path.basename(root)],subject:identity(root), + executable:{path:scanner,sha256:sha(executable)},report:scenario==='missing-report'?'':report}); + fs.writeFileSync(file,JSON.stringify(scans)); + } + return value; +} +const installationID='12345678-1234-4234-8234-123456789abc'; +const physical='skill-'+sha(installationID).slice(0,12), projection='managed/clients/codex/'+physical; +function registration(state,root){ + const subject=identity(root),revision={version:'0.1.0',...subject}; + const target=path.join(state,projection),binding='client_'+sha([installationID,'codex','user',target].join('\0')).slice(0,24); + return {installation_id:installationID,declared_name:'skill',source:{tree_digest:subject.tree_digest}, + package:{declared_name:'skill',version:'0.1.0',manifest_digest:subject.manifest_digest}, + clients:{[binding]:{client_binding_id:binding,client_id:'codex',scope:'user',materialization:'materialized',physical_artifact_id:physical, + target_locator:path.join(state,projection),package_revision:revision}}}; +} + function installer(args) { const verb=args[0], state=process.env.AGENTPLUGINS_HOME,client=path.join(process.env.HOME,'.codex'); const data={}, result={schema_version:1,command:verb,result:'success',data}; @@ -163,25 +228,48 @@ function installer(args) { if(scenario==='scan-failure'){result.result='failure';return output(result,1);} if(verb==='add'&&args.includes('--dry-run')) { const lane=path.basename(args[1]), names=fs.readdirSync(path.join(args[1],'skills')); - const components=names.map(name=>({kind:'skill',name,support:'native'})); - if(lane!=='skill')components.push({kind:'mcp_server',name:lane,support:'native'}); - data.security=security('local_scan');data.tree_digest='sha256:'+sha('tree');data.manifest_digest='sha256:'+sha('manifest'); + const components=names.map(name=>({kind:'skill',name,support:'projected'})); + if(lane!=='skill')components.push({kind:'mcp_server',name:lane,support:'projected'}); + data.security=security('local_scan',args[1]);Object.assign(data,data.security.subject); data.dry_run=true;data.result={plan:{client_id:'codex',scope:'user',status:'manual_activation_required',components}}; if(scenario==='wrong-plan')data.result.plan.components=[]; if(scenario==='dry-run-effect')write(client,'unexpected','effect'); } else if(verb==='add') { if(scenario==='add-failure'){result.result='failure';return output(result,1);} - data.result={mutated:true,activation:{authentication:'not_checked'}}; - data.security=security('cache');data.tree_digest='sha256:'+sha('tree');data.manifest_digest='sha256:'+sha('manifest'); - if(scenario!=='no-state')write(state,'state-v2.json',{installed:true}); - if(scenario!=='no-effect')write(client,'installed','fixture projection'); + data.result={installation_id:installationID,mutated:true,activation:{authentication:'not_checked'}}; + data.security=security('cache',args[1]);Object.assign(data,data.security.subject); + if(scenario!=='no-state')write(state,'state-v2.json',{schema_version:4,installations:[registration(state,args[1])]}); + if(scenario!=='no-effect'){ + for(const name of ['skill','extra-skill']){ + fs.mkdirSync(path.join(state,projection,'skills',name),{recursive:true,mode:0o700}); + fs.copyFileSync(path.join(args[1],'skills',name,'SKILL.md'),path.join(state,projection,'skills',name,'SKILL.md')); + } + } + if(scenario==='alter-client')write(client,'config.toml','unexpected replacement'); + if(scenario==='alter-client-mode')fs.chmodSync(path.join(client,'config.toml'),0o644); if(scenario==='auth-claim')data.result.activation.authentication_attested=true; - } else if(verb==='info'){data.name='skill';data.version='0.1.0';data.clients=[{client_id:'codex',package_revision:{version:'0.1.0'}}];} - else if(verb==='update')data.result={mutated:false,no_change:scenario!=='wrong-update'}; - else if(verb==='remove'){data.result={mutated:true};write(state,'state-v2.json',{installed:false});fs.unlinkSync(path.join(client,'installed'));} - else if(verb==='list')data.installations=scenario==='remaining-installation'?[{name:'skill'}]:[]; + if(scenario==='wrong-installation-identity')data.result.installation_id='87654321-1234-4234-8234-123456789abc'; + } else if(verb==='info'){ + if(scenario==='info-mutates-state')write(state,'unexpected-info-write','x'); + data.installation_id=installationID;data.name='skill';data.version='0.1.0';data.clients=Object.values(JSON.parse(fs.readFileSync(path.join(state,'state-v2.json'))).installations[0].clients); + } + else if(verb==='update')data.result={installation_id:installationID,mutated:false,no_change:scenario!=='wrong-update'}; + else if(verb==='remove'){ + data.result={installation_id:installationID,mutated:true}; + if(scenario!=='remaining-registration')write(state,'state-v2.json',{schema_version:4,installations:[]}); + if(scenario!=='remove-leaves-client-projection'){ + for(const name of ['skill','extra-skill']){fs.unlinkSync(path.join(state,projection,'skills',name,'SKILL.md'));fs.rmdirSync(path.join(state,projection,'skills',name));} + fs.rmdirSync(path.join(state,projection,'skills'));fs.rmdirSync(path.join(state,projection)); + } + } + else if(verb==='list'){data.installations=scenario==='remaining-installation'?[{name:'skill'}]:[];if(scenario==='list-mutates-state')write(state,'list-write','x');} return output(result); } +if (scenario === 'timeout') setInterval(()=>{},1000); +else if (scenario === 'flood') process.stdout.write('x'.repeat(2*1024*1024)); +else if (scenario === 'cancel') setInterval(()=>{},1000); +else main(); + `; } function subprocessFixtures(t, f, scenario = "ok") { @@ -263,10 +351,18 @@ test("fixed production orchestration and closed reader with subprocess fixtures }); for (const scenario of ["wrong-binary","wrong-build-target","wrong-build-mode","wrong-build-source","wrong-version","wrong-source", "runtime-claim","bad-json","invalid-utf8","missing-command","deferred-command","missing-template","yaml","wrong-result","wrong-command","parity","changed-project", - "changed-input","scan-failure","wrong-plan","dry-run-effect","add-failure","no-state","no-effect","auth-claim","wrong-update","remaining-installation"]) { + "changed-input","scan-failure","wrong-plan","dry-run-effect","add-failure","no-state","no-effect","auth-claim","wrong-update","remaining-installation","wrong-package","invalid-security-contract","wrong-policy","wrong-counts","wrong-findings","cache-mismatch", + "missing-scanner","missing-cache","missing-scan","missing-report","alter-client","alter-client-mode","info-mutates-state", + "remove-leaves-client-projection","remaining-registration","list-mutates-state","empty-capabilities","missing-profile","wrong-schema","raw-report-policy","raw-report-counts","raw-report-findings","wrong-installation-identity"]) { test(`full subprocess journey fails without completion: ${scenario}`,async t=>{ const f=fixture();subprocessFixtures(t,f,scenario); - await assert.rejects(internal(true).produce(f.options), scenario === "wrong-command" ? /author command identifier/ : undefined);noTerminal(f); + const reasons = { "wrong-command": /author command identifier/, "wrong-package": /independently captured package security subject/, + "invalid-security-contract": /fixed production security schema/, "wrong-policy": /fixed production security policy/, + "wrong-counts": /security counts match outcome/, "wrong-findings": /security findings match counts/, + "remove-leaves-client-projection": /remove owned projection/, "info-mutates-state": /info is read only/, + "list-mutates-state": /list is read only/, "raw-report-policy": /scanner report policy/, + "raw-report-counts": /scanner report file counts/, "raw-report-findings": /scanner report findings/, "wrong-installation-identity": /lifecycle installed identity/ }; + await assert.rejects(internal(true).produce(f.options), reasons[scenario]);noTerminal(f); assert.ok(fs.existsSync(path.join(f.options.output,"diagnostic.json"))); }); } @@ -459,3 +555,180 @@ test("input and output placement reject overlap before subprocess effects",async assert.equal(fs.existsSync(f.options.work),false); } }); + +// Rehash every outer pin as the independent reviewer did: each rejection must +// come from semantic replay, not a stale digest or a broken fixture baseline. +test("N1 reader rejects rehashed semantic omissions and contradictions", async t => { + const f = fixture(); subprocessFixtures(t, f); await internal(true).produce(f.options); + const root = f.options.output, expected = expectations(f); + const originals = Object.fromEntries(fs.readdirSync(root).map(file => [file, fs.readFileSync(path.join(root, file))])); + const rootOnly = [{ path: ".", mode: 448, kind: "directory" }]; + const editSecurity = (e, change) => { + for (const row of e["transcripts.json"].installer.slice(0, 4)) { + const v = JSON.parse(row.stdout); change(v.data, row); row.stdout = JSON.stringify(v) + "\n"; + } + }; + function changeRawReport(e, change) { + const scan = e["scans.json"][0], report = JSON.parse(scan.report); change(report); + scan.report = JSON.stringify(report) + "\n"; + const digest = "sha256:" + hash(scan.report); + editSecurity(e, (d, row) => { if (row.id === "dry-run/skill" || row.id === "add") d.security.report_digest = digest; }); + // Update the referenced cache bytes and every snapshot pin too. Only the + // report's production semantics are invalid, not its integrity chain. + const states = e["transcripts.json"].installer.flatMap(r => [r.before, r.after]); + const files = states.flatMap(s => s.acquisition).filter(x => x.path.startsWith("security/assessments/") && x.kind === "file"); + for (const sha256 of new Set(files.map(x => x.sha256))) { + const value = JSON.parse(Buffer.from(e["acquisition.json"][sha256], "base64")); + if (value.subject.tree_digest !== scan.subject.tree_digest) continue; + value.report_digest = digest; const body = Buffer.from(JSON.stringify(value) + "\n"), pin = c.metadata(body); + delete e["acquisition.json"][sha256]; e["acquisition.json"][pin.sha256] = body.toString("base64"); + for (const item of files.filter(x => x.sha256 === sha256)) Object.assign(item, pin); + } + } + const cases = { + "missing custody": e => { e["preservation.json"].custody_before = []; e["preservation.json"].custody_after = []; }, + "preparation custody omitted": e => { e["preservation.json"].custody_before.pop(); e["preservation.json"].custody_after.pop(); }, + "custody substituted subject": e => { for (const k of ["custody_before", "custody_after"]) e["preservation.json"][k][0].file = "invented"; }, + "custody invalid inode": e => { for (const k of ["custody_before", "custody_after"]) e["preservation.json"][k][0].ino = "not-an-inode"; }, + "remove leaves projection": e => { + const rows = e["transcripts.json"].installer, r = rows[6]; + r.after.state.push(...r.before.state.filter(x => x.path.startsWith("managed/clients/codex/") && !r.after.state.some(a => a.path === x.path))); + rows[7].before = structuredClone(r.after); rows[7].after = structuredClone(r.after); + }, + "info mutates state": e => { e["transcripts.json"].installer[4].after.state.push({ path: "unauthorized", mode: 384, kind: "file", size: 1, sha256: hash("x") }); }, + "list mutates client": e => { e["transcripts.json"].installer[7].after.client[1].mode ^= 0o100; }, + "preexisting client changed continuously": e => { + const rows = e["transcripts.json"].installer; + for (let i = 3; i < rows.length; i++) for (const when of i === 3 ? ["after"] : ["before", "after"]) + rows[i][when].client.find(x => x.path === "config.toml").sha256 = hash("changed config"); + }, + "wrong add installation ID": e => { const row = e["transcripts.json"].installer[3], v = JSON.parse(row.stdout); v.data.result.installation_id = "other"; row.stdout = JSON.stringify(v); }, + "wrong info revision": e => { const row = e["transcripts.json"].installer[4], v = JSON.parse(row.stdout); v.data.clients[0].package_revision.tree_digest = "sha256:" + hash("other tree"); row.stdout = JSON.stringify(v); }, + "wrong state client binding": e => { + for (const row of e["transcripts.json"].installer) for (const state of [row.before, row.after]) { + if (!state.state_document) continue; const document = JSON.parse(state.state_document); + if (!document.installations.length) continue; + document.installations[0].clients = { wrong: Object.values(document.installations[0].clients)[0] }; + state.state_document = JSON.stringify(document); Object.assign(state.state.find(x => x.path === "state-v2.json"), c.metadata(Buffer.from(state.state_document))); + } + }, + "state discontinuity": e => { e["transcripts.json"].installer[4].before = structuredClone(e["transcripts.json"].installer[2].after); }, + "missing command effects": e => { for (const p of c.PRODUCTS) for (const r of e["transcripts.json"][p]) { r.before = rootOnly; r.after = rootOnly; } }, + "init never created": e => { for (const p of c.PRODUCTS) e["transcripts.json"][p].find(r => r.id === "skill/init").after = rootOnly; }, + "extra Skill overwrites existing file": e => { for (const p of c.PRODUCTS) e["transcripts.json"][p].find(r => r.id === "skill/extra-skill").after.find(x => x.path === "skill/README.md").mode ^= 0o100; }, + "malformed harness omitted": e => { for (const p of c.PRODUCTS) { const r = e["transcripts.json"][p].find(r => r.id === "malformed-skill"); r.before = r.before.filter(x => !x.path.includes("/broken")); r.after = r.before; } }, + "malformed harness wrong bytes": e => { for (const p of c.PRODUCTS) { const r = e["transcripts.json"][p].find(r => r.id === "malformed-skill"); for (const when of ["before", "after"]) r[when].find(x => x.path.endsWith("broken/SKILL.md")).sha256 = hash("other invalid Skill"); } }, + "invented acquisition path": e => { for (const r of e["transcripts.json"].installer) for (const state of [r.before, r.after]) state.acquisition = [{ path: "../../outside", kind: "symlink", sha256: "not-a-digest" }]; }, + "acquisition missing bytes": e => { e["acquisition.json"] = {}; }, + "acquisition altered bytes": e => { const a = e["acquisition.json"]; a[Object.keys(a)[0]] = Buffer.from("unrelated").toString("base64"); }, + "acquisition oversize": e => { e["transcripts.json"].installer[0].after.acquisition.find(x => x.kind === "file").size = 17 * 1024 * 1024; }, + "wrong scanned package and policy": e => editSecurity(e, d => { + d.tree_digest = "sha256:" + "b".repeat(64); d.manifest_digest = "sha256:" + "c".repeat(64); + Object.assign(d.security, { schema_version: 999, policy: { id: "wrong", version: -1, digest: "invalid" }, counts: { blocking: 42 }, + subject: { tree_digest: d.tree_digest, manifest_digest: d.manifest_digest } }); + }), + "wrong package only": e => editSecurity(e, d => { d.tree_digest = "sha256:" + hash("other package"); d.security.subject.tree_digest = d.tree_digest; }), + "wrong policy only": e => editSecurity(e, d => { d.security.policy.digest = "sha256:" + hash("stale policy"); }), + "invalid counts only": e => editSecurity(e, d => { d.security.counts.blocking = 42; }), + "invalid findings only": e => editSecurity(e, d => { d.security.findings = [{ code: "SEC330", disposition: "blocking", message: "blocking" }]; }), + "missing scanner call": e => { e["scans.json"].pop(); }, + "missing raw report": e => { e["scans.json"][0].report = ""; }, + "changed raw report": e => { e["scans.json"][0].report += " "; }, + "raw report invalid counts with coherent pins": e => changeRawReport(e, r => { r.stats.scanned_files = -1; }), + "raw report invalid policy with coherent pins": e => changeRawReport(e, r => { r.policy.version = 999; }), + "raw report findings with coherent pins": e => changeRawReport(e, r => { r.findings = [{ rule_code: "SEC330" }]; }), + "raw report runtime errors with coherent pins": e => changeRawReport(e, r => { r.runtime_errors = [{}]; }), + "scanner unrelated executable": e => { e["scans.json"][0].executable.sha256 = hash("other scanner"); }, + "scan unrelated subject": e => { e["scans.json"][0].subject.tree_digest = "sha256:" + hash("other subject"); }, + "scan wrong command": e => { e["scans.json"][0].args = ["--version"]; }, + "cache report mismatch": e => editSecurity(e, (d, row) => { if (row.id === "add") d.security.report_digest = "sha256:" + hash("another report"); }), + "empty capability contract": e => { for (const p of c.PRODUCTS) { const r = e["transcripts.json"][p].find(r => r.id === "capabilities"), v = JSON.parse(r.stdout); v.data.capabilities = {}; r.stdout = JSON.stringify(v); } }, + "missing capabilities client": e => { for (const p of c.PRODUCTS) { const r = e["transcripts.json"][p].find(r => r.id === "capabilities"), v = JSON.parse(r.stdout); v.data.capabilities.clients.pop(); r.stdout = JSON.stringify(v); } }, + "missing conformance profile": e => { for (const p of c.PRODUCTS) { const r = e["transcripts.json"][p].find(r => r.id === "skill/validate"), v = JSON.parse(r.stdout); v.data.profiles.pop(); r.stdout = JSON.stringify(v); } }, + "wrong schema inventory": e => { for (const p of c.PRODUCTS) { const r = e["transcripts.json"][p].find(r => r.id === "skill/validate"), v = JSON.parse(r.stdout); v.data.schema_ids = []; r.stdout = JSON.stringify(v); } } + }; + for (const [name, change] of Object.entries(cases)) await t.test(name, () => { + const e = Object.fromEntries(Object.entries(originals).filter(([n]) => !n.endsWith("-terminal.json")).map(([n, b]) => [n, JSON.parse(b)])); + change(e); + const put = (n, b) => { fs.chmodSync(path.join(root, n), 0o600); fs.writeFileSync(path.join(root, n), b); }; + for (const [n, v] of Object.entries(e)) put(n, c.encode(v)); + for (const p of c.PRODUCTS) { + const v = JSON.parse(originals[p + "-terminal.json"]); + v.evidence = v.evidence.map(pin => ({ file: pin.file, ...c.metadata(c.encode(e[pin.file])) })); + put(p + "-terminal.json", c.encode(v)); + } + assert.throws(() => native.readTerminals(root, f.root, f.pins, expected), name); + for (const [n, b] of Object.entries(originals)) put(n, b); + }); + assert.equal(native.readTerminals(root, f.root, f.pins, expected).length, 2); +}); + +for (const scenario of ["pre-close kill denied", "close never arrives", "successful termination"]) { + test(`N1 bounded cleanup: ${scenario}`, async t => { + const { EventEmitter } = require("node:events"), child = new EventEmitter(); + child.pid = 123456789; child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); + let kills = 0, probes = 0; + t.mock.method(cp, "spawn", () => child); + t.mock.method(process, "kill", (_pid, signal) => { + if (signal === 0) { probes++; const e = new Error("gone"); e.code = "ESRCH"; throw e; } + kills++; + if (scenario === "pre-close kill denied") { const e = new Error("denied"); e.code = "EPERM"; throw e; } + if (scenario === "successful termination") setImmediate(() => child.emit("close", null, "SIGKILL")); + return true; + }); + const begin = performance.now(); + const result = internal(false, true).test.subprocess("/never-executed", [], { root: os.tmpdir(), env: {} }); + const outcome = await Promise.race([ + result.then(() => "unexpected success", e => e.message), + new Promise(resolve => { const timer = setTimeout(() => resolve("unbounded"), 1800); timer.unref(); }) + ]); + assert.notEqual(outcome, "unbounded"); assert.match(outcome, /timeout/); assert.equal(kills, 1); + if (scenario === "successful termination") { assert.ok(performance.now() - begin < 900); assert.doesNotMatch(outcome, /uncertain/); } + else { assert.match(outcome, /cleanup uncertain: child close not observed/); assert.equal(probes, 0); } + if (scenario === "pre-close kill denied") { assert.match(outcome, /cleanup denied: EPERM/); assert.ok(performance.now() - begin < 500); } + child.emit("close", null, "SIGKILL"); assert.equal(kills, 1); // Late close cannot retry cleanup. + }); +} + +test("denied pre-close cleanup reaches producer failure diagnostics without retries", async t => { + const f = fixture(), { EventEmitter } = require("node:events"), child = new EventEmitter(); + child.pid = 123456789; child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); + let calls = 0; + t.mock.method(cp, "spawn", () => child); + t.mock.method(process, "kill", () => { calls++; const e = new Error("synthetic denial"); e.code = "EPERM"; throw e; }); + const pending = internal(true, true).produce(f.options); + await assert.rejects(pending, /cleanup denied: EPERM; cleanup uncertain: child close not observed/); + noTerminal(f); assert.equal(calls, 1); + const diagnostic = JSON.parse(fs.readFileSync(path.join(f.options.output, "diagnostic.json"))); + assert.equal(diagnostic.native_acceptance, false); assert.match(diagnostic.error, /cleanup uncertain/); + child.emit("close", null, "SIGKILL"); assert.equal(calls, 1); +}); + +test("independent package identity uses content framing and portable executable modes", () => { + const documents = { "plugin.json": Buffer.from('{"name":"skill"}\n'), "skills/demo/SKILL.md": Buffer.from([0, ...Buffer.from("fixture"), 255]) }; + const project = { files: [{ path: ".", kind: "directory", mode: 448 }, + { path: "plugin.json", kind: "file", mode: 420, ...c.metadata(documents["plugin.json"]) }, + { path: "skills", kind: "directory", mode: 448 }, { path: "skills/demo", kind: "directory", mode: 448 }, + { path: "skills/demo/SKILL.md", kind: "file", mode: 493, ...c.metadata(documents["skills/demo/SKILL.md"]) }], + documents: Object.fromEntries(Object.entries(documents).map(([name, body]) => [name, body.toString("base64")])) }; + const digest = internal().test.packageIdentity; + // Independently framed uint64-BE vector; includes non-UTF8 content. + const expected = { tree_digest: "sha256:68fa0b7927e71c2ff3c74c3235085d8aed2b85968db22ee27fe02ca1abff9bae", + manifest_digest: "sha256:2950ebbb0911376d0566dead6130d2b13b00608eac7119a6fe50ae65e6a0967d" }; + assert.deepEqual(digest(project), expected); + project.files[1].mode = 384; assert.deepEqual(digest(project), expected); // Non-executable permission changes are not package identity. + project.files[4].mode = 420; assert.notEqual(digest(project).tree_digest, expected.tree_digest); + project.documents["plugin.json"] = Buffer.from("other bytes").toString("base64"); assert.throws(() => digest(project)); +}); + +test("fixed profile and schema pins agree with preserved domain contracts", () => { + const n = internal().test, root = path.resolve(__dirname, "../../.."); + for (const schema of n.capabilities().schemas) { + const name = schema.id.endsWith("/plugin.schema.json") ? "plugin" : "mcp"; + const file = path.join(root, `install/integrationctl/agentplugins/adapters/specregistry/schemas/1.0.0/${name}.schema.json`); + assert.equal("sha256:" + c.digest(fs.readFileSync(file)), schema.digest); + } + assert.equal(n.PROFILES.at(-1).digest, "sha256:" + c.digest(fs.readFileSync(path.join(root, "install/integrationctl/agentplugins/conformance/profiles/README.md")))); + const policy = fs.readFileSync(path.join(root, "install/integrationctl/agentplugins/adapters/securityscan/policy_test.go"), "utf8"); + assert.ok(policy.includes(n.POLICY.digest)); +}); From 5d7093ed84071df0ae93bed23f234a3edbb734a8 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 8 Sep 2026 19:14:30 +0000 Subject: [PATCH 3/8] fix(authoring): bind scanner release and installed Codex projection Refs #210, #203, #208. Keep production observation and native admission fail-closed pending actual execution and independent review. --- .../scripts/authoring-native-qualification.js | 127 ++++++++++++- .../authoring-native-qualification.test.js | 168 ++++++++++++++++-- 2 files changed, 274 insertions(+), 21 deletions(-) diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js index 4fa51f21..e44d79bf 100644 --- a/npm/agentplugins/scripts/authoring-native-qualification.js +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -8,6 +8,7 @@ const path = require("node:path"); const os = require("node:os"); const cp = require("node:child_process"); const crypto = require("node:crypto"); +const zlib = require("node:zlib"); const assert = require("node:assert/strict"); const c = require("./dual-authoring-candidate"); const { verifyProjectedPair } = require("./authoring-release"); @@ -464,6 +465,53 @@ function acquisitionClosure(rows, bodies) { for (const item of pins) { total += item.size; assert.ok(total <= 16 * LIMIT, "total acquisition byte bound"); } for (const row of rows) for (const state of [row.before, row.after]) acquisition(state, bodies); } +// Fixed ReleaseScanner release.go pins, never supplied by an evidence caller. +// HTTP bodies are observer evidence, not files invented in the scanner cache. +const SCANNER_RELEASES = Object.freeze({ + "linux-amd64": { name: "lintai-v0.1.3-x86_64-unknown-linux-gnu.tar.gz", sha256: "2b3d176db752433b904a4b42375543ff398f4841d22e48f7d4f23ded925b72da" }, + "linux-amd64-musl": { name: "lintai-v0.1.3-x86_64-unknown-linux-musl.tar.gz", sha256: "3da60f749c61e2caca029a44a9ce422d570aef8c57f82ce51c411c8cec12f61b" } +}); +function scannerArchive(scan, executable) { + const asset = SCANNER_RELEASES[scan.executable.path.split("/")[3]]; + assert.ok(asset, "fixed scanner platform"); + c.keys(scan.archive, ["url", "bytes"], "observed release archive"); + exact(scan.archive.url, `https://github.com/777genius/lintai/releases/download/v0.1.3/${asset.name}`, "fixed scanner release URL"); + assert.equal(typeof scan.archive.bytes, "string"); + assert.ok(scan.archive.bytes.length <= 44 * LIMIT, "scanner archive byte bound"); + const body = Buffer.from(scan.archive.bytes, "base64"); + exact(body.toString("base64"), scan.archive.bytes); + assert.ok(body.length > 0 && body.length <= 32 * LIMIT, "production scanner archive bound"); + exact(c.digest(body), asset.sha256, "independently pinned scanner release archive"); + // Bounded in-memory tar inspection. No extraction to disk or execution. This + // N1 profile accepts ordinary tar files/directories; unknown extensions fail + // closed pending inspection of the genuine pinned archive. + const tar = zlib.gunzipSync(body, { maxOutputLength: 64 * LIMIT }); + let offset = 0, binary, count = 0; + const octal = b => { const v = b.toString("ascii").replace(/\0.*$/, "").trim(); assert.match(v, /^[0-7]+$/); return parseInt(v, 8); }; + const names = new Set(); + while (offset + 512 <= tar.length) { + const header = tar.subarray(offset, offset + 512); + if (header.every(x => x === 0)) break; + assert.ok(++count <= 4096, "scanner archive entry bound"); + const sum = header.reduce((n, x, i) => n + (i >= 148 && i < 156 ? 32 : x), 0); + exact(octal(header.subarray(148, 156)), sum, "scanner tar checksum"); + const text = (a, b) => header.subarray(a, b).toString("utf8").replace(/\0.*$/, ""); + const prefix = text(345, 500), name = (prefix ? prefix + "/" : "") + text(0, 100); + assert.ok(name && !name.startsWith("/") && !/[\\\x00-\x1f]/.test(name) && !name.split("/").includes(".."), "safe scanner archive name"); + assert.ok(!names.has(name), "unique scanner archive entry"); names.add(name); + const type = header[156], size = octal(header.subarray(124, 136)); + assert.ok([0, 48, 53].includes(type), "ordinary scanner archive entries only"); + assert.ok(size <= 32 * LIMIT && (type !== 53 || size === 0)); + const end = offset + 512 + size; assert.ok(end <= tar.length, "complete scanner archive entry"); + if (path.posix.basename(name) === "lintai" && type !== 53) { + assert.equal(binary, undefined, "one release scanner executable"); binary = tar.subarray(offset + 512, end); + } + offset = offset + 512 + Math.ceil(size / 512) * 512; + } + assert.ok(tar.length - offset >= 1024 && tar.subarray(offset).every(x => x === 0), "complete scanner tar terminator"); + assert.ok(binary?.length, "release archive contains lintai"); + exact(binary, executable, "scanner executable is the pinned archive member"); +} function scanReplay(rows, projects, scans, bodies) { assert.ok(Array.isArray(scans)); exact(scans.length, 3, "three observed fresh scans and report bytes"); const fresh = new Map(); let executable; @@ -477,12 +525,13 @@ function scanReplay(rows, projects, scans, bodies) { assert.ok(after[file], "required assessment cache bytes"); exact(jsonDocument(after[file].toString("utf8")), cached); if (i < 3) { assert.equal(before[file], undefined, "fresh scan must precede cache creation"); - const scan = scans[i]; c.keys(scan, ["id", "args", "subject", "executable", "report"], "observed scanner call"); + const scan = scans[i]; c.keys(scan, ["id", "args", "subject", "executable", "archive", "report"], "observed scanner call"); exact([scan.id, scan.args, scan.subject], [row.id, ["scan-agent-plugin", `/${lane}`], assessment.subject]); c.keys(scan.executable, ["path", "sha256"], "observed scanner executable"); assert.match(scan.executable.path, /^security\/lintai\/0\.1\.3\/linux-amd64(?:-musl)?\/lintai$/); assert.ok(after[scan.executable.path], "required scanner acquisition bytes"); exact(c.digest(after[scan.executable.path]), sha(scan.executable.sha256)); + scannerArchive(scan, after[scan.executable.path]); const item = row.after.acquisition.find(x => x.path === scan.executable.path); exact(item.mode, 0o700); if (executable) exact(scan.executable, executable, "same acquired scanner"); else executable = scan.executable; assert.equal(typeof scan.report, "string"); @@ -513,6 +562,41 @@ function stateDocument(state) { const document = jsonDocument(state.state_document); exact(document.schema_version, 4); assert.ok(Array.isArray(document.installations)); return document; } +// Only capture knows the actual operation root. Check exact equality before +// replacing it with this fixed evidence token; replay never accepts a target +// expectation supplied by a caller or a different root inside the evidence. +const INSTALLER_ROOT = ""; +function normalizeInstaller(rows, root) { + const normalizeClient = (client, id) => { + const physical = `skill-${c.digest(Buffer.from(id)).slice(0, 12)}`; + exact(client.physical_artifact_id, physical); + exact(client.target_locator, `${root}/managed/clients/codex/${physical}`, "registration targets operation-owned installer root"); + const binding = locator => "client_" + c.digest(Buffer.from([id, "codex", "user", locator].join("\0"))).slice(0, 24); + exact(client.client_binding_id, binding(client.target_locator)); + client.target_locator = `${INSTALLER_ROOT}/managed/clients/codex/${physical}`; + client.client_binding_id = binding(client.target_locator); + }; + for (const row of rows) { + for (const state of [row.before, row.after]) { + exact(state.root, root); state.root = INSTALLER_ROOT; + if (!state.state_document) continue; + const document = stateDocument(state); + for (const registration of document.installations) { + const clients = Object.values(registration.clients); exact(clients.length, 1); + exact(Object.keys(registration.clients), [clients[0].client_binding_id]); + normalizeClient(clients[0], registration.installation_id); + registration.clients = { [clients[0].client_binding_id]: clients[0] }; + } + state.state_document = c.encode(document).toString("utf8"); + Object.assign(state.state.find(x => x.path === "state-v2.json"), c.metadata(Buffer.from(state.state_document))); + } + if (row.id === "info") { + const document = jsonDocument(row.stdout); + for (const client of document.data.clients) normalizeClient(client, document.data.installation_id); + row.stdout = c.encode(document).toString("utf8"); + } + } +} function installedIdentity(state, project) { const document = stateDocument(state); exact(document.installations.length, 1); const registration = document.installations[0], subject = packageIdentity(project); @@ -525,8 +609,7 @@ function installedIdentity(state, project) { const physical = `skill-${c.digest(Buffer.from(registration.installation_id)).slice(0, 12)}`; exact(client.physical_artifact_id, physical); const projection = `managed/clients/codex/${physical}`; - assert.ok(typeof client.target_locator === "string" && path.posix.isAbsolute(client.target_locator) && - path.posix.normalize(client.target_locator) === client.target_locator && client.target_locator.endsWith(`/${projection}`)); + exact(client.target_locator, `${state.root}/${projection}`, "registration targets operation-owned installer root"); const binding = "client_" + c.digest(Buffer.from([registration.installation_id, "codex", "user", client.target_locator].join("\0"))).slice(0, 24); exact(Object.keys(registration.clients), [binding]); exact(client.client_binding_id, binding); exact([client.package_revision.version, client.package_revision.tree_digest, client.package_revision.manifest_digest], @@ -537,6 +620,20 @@ function installedIdentity(state, project) { const copied = state.state.find(x => x.path === `${projection}/${skill.path}`); assert.ok(copied && copied.kind === "file"); exact([copied.sha256, copied.size], [skill.sha256, skill.size]); } + const documents = capturedBytes(state.state.filter(x => /\/(?:\.codex-plugin\/plugin|\.agents\/plugins\/marketplace)\.json$/.test(x.path)), state.projection_documents); + const read = leaf => { + const bytes = documents[`${projection}/${leaf}`]; assert.ok(bytes, `mandatory Codex projection ${leaf}`); + return jsonDocument(bytes.toString("utf8")); + }; + const manifest = { name: "skill", version: "0.1.0", skills: "./skills/" }; + for (const key of ["description", "homepage", "repository", "license", "author", "keywords"]) + if (project.manifest[key] !== undefined) manifest[key] = project.manifest[key]; + exact(read(".codex-plugin/plugin.json"), manifest, "Codex plugin identity and component references"); + exact(read(".agents/plugins/marketplace.json"), { + name: "agentplugins-" + c.digest(Buffer.from(physical)).slice(0, 12), + plugins: [{ name: "skill", source: { source: "local", path: "./" }, + policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: "Productivity" }] + }, "Codex managed marketplace identity and reference"); return { registration, client, projection }; } function installed(row, spec, projects) { @@ -551,6 +648,7 @@ function installed(row, spec, projects) { exact(plan.components.map(x => `${x.kind}:${x.name}`).sort(), [...skills, ...servers].sort()); assert.ok(plan.components.every(x => x.support === "projected"), "Codex compatibility projection support"); exact(row.before.client, row.after.client); exact(row.before.state, row.after.state); + exact(row.before.state_document_source, row.after.state_document_source, "dry-run raw state bytes preserved"); return; } if (["add", "update", "remove"].includes(verb)) { @@ -558,7 +656,7 @@ function installed(row, spec, projects) { exact(result.mutated, verb !== "update"); const identity = installedIdentity(verb === "add" ? row.after : row.before, projects.skill); exact(result.installation_id, identity.registration.installation_id, "lifecycle installed identity"); - if (verb === "update") { exact(result.no_change, true); exact(row.before.state, row.after.state); exact(row.before.client, row.after.client); } + if (verb === "update") { exact(row.before.state_document_source, row.after.state_document_source, "update raw state bytes preserved"); exact(result.no_change, true); exact(row.before.state, row.after.state); exact(row.before.client, row.after.client); } else assert.notDeepEqual(row.before.state, row.after.state, "real lifecycle state mutation"); if (verb === "add") { exact(result.activation.authentication, "not_checked"); @@ -674,8 +772,16 @@ function verifyJourney(e, pins) { const row = transcript.installer[i]; c.keys(row, ["id", "args", "status", "stdout", "stderr", "before", "after"], "installer transcript"); for (const state of [row.before, row.after]) { - c.keys(state, ["client", "state", "acquisition", "state_document"], "separate installer state and acquisition"); + c.keys(state, ["root", "client", "state", "acquisition", "state_document", "state_document_source", "projection_documents"], "separate installer state and acquisition"); + exact(state.root, INSTALLER_ROOT, "fixed operation-owned installer root token"); + if (state.state_document === null) exact(state.state_document_source, null); + else { + c.keys(state.state_document_source, ["size", "sha256"], "raw state document byte pin"); + sha(state.state_document_source.sha256); positive(state.state_document_source.size); + assert.ok(state.state_document_source.size <= LIMIT); + } treeShape(state.client); treeShape(state.state); + capturedBytes(state.state.filter(x => /\/(?:\.codex-plugin\/plugin|\.agents\/plugins\/marketplace)\.json$/.test(x.path)), state.projection_documents); acquisition(state, e["acquisition.json"]); stateDocument(state); } if (i) exact(row.before, transcript.installer[i - 1].after, "installer state continuity"); @@ -808,7 +914,15 @@ async function produce(options, signal) { if (!Object.hasOwn(acquisitionBodies, item.sha256)) { acquisitionTotal += bytes.length; assert.ok(acquisitionTotal <= 16 * LIMIT, "total acquisition byte bound"); } acquisitionBodies[item.sha256] = bytes.toString("base64"); } - return { client: tree(client), state: all.filter(x => !isAcquisition(x)), acquisition, + const projection_documents = Object.fromEntries(all.filter(x => /\/(?:\.codex-plugin\/plugin|\.agents\/plugins\/marketplace)\.json$/.test(x.path)) + .map(x => [x.path, c.readFile(path.join(install.env.AGENTPLUGINS_HOME, x.path), LIMIT).toString("base64")])); + // The root comes from the fixed context used for this very subprocess, + // never registration text or a readTerminals expected-target parameter. + // Retain the actual byte pin as well as the normalized document pin so + // path normalization cannot conceal info/update/list byte mutation. + const rawState = all.find(x => x.path === "state-v2.json"); + return { state_document_source: rawState ? { size: rawState.size, sha256: rawState.sha256 } : null, + root: install.env.AGENTPLUGINS_HOME, client: tree(client), state: all.filter(x => !isAcquisition(x)), acquisition, projection_documents, state_document: all.some(x => x.path === "state-v2.json") ? c.readFile(stateFile, LIMIT).toString("utf8") : null }; }; for (const spec of installationCommands()) { @@ -826,6 +940,7 @@ async function produce(options, signal) { exact(c.digest(c.readFile(binaries[p])), frozen.manifest.products[p].assets[TARGET].binary.sha256); exact(fs.lstatSync(binaries[p]).mode & 0o777, 0o500, "selected executable mode preserved"); } + normalizeInstaller(rows.installer, install.env.AGENTPLUGINS_HOME); e["transcripts.json"] = rows; e["trees.json"] = projects; e["build-info.json"] = build; e["preparation.json"] = prep; e["preservation.json"] = { inputs_before: frozenBefore, inputs_after: after.subjects.map(s => ({ file: path.relative(root, s.file), ...c.metadata(c.readFile(s.file)) })), diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js index d991e88d..750c7741 100644 --- a/npm/agentplugins/test/authoring-native-qualification.test.js +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -46,13 +46,26 @@ function fixture() { const moved = path.join(tools, "go"); fs.renameSync(f.go, moved); f.go = moved; f.options.go = moved; return f; } +const scannerFixture = Buffer.from("not executable: scanner acquisition fixture"); +function scannerTar(binary) { + const zlib = require("node:zlib"), tar = zlib.gunzipSync(c.archive(binary, "agentplugins")); + tar.fill(0, 0, 100); tar.write("lintai", 0); tar.fill(32, 148, 156); + const sum = tar.subarray(0, 512).reduce((n, b) => n + b, 0); + tar.write(sum.toString(8).padStart(6, "0") + "\0 ", 148); + return zlib.gzipSync(tar); +} +const scannerFixtureArchive = scannerTar(scannerFixture); +function fixtureReader(...args) { return internal(true).readTerminals(...args); } function internal(observation = false, fastTimeout = false) { let source = fs.readFileSync(moduleFile, "utf8"); if (observation) source = source.replace(' e["scans.json"] = observationGate(); //', ' e["scans.json"] = JSON.parse(fs.readFileSync(path.join(install.env.TMPDIR, "fixture-scans.json"))); // Test-only captured child fixtures.'); + // Only this isolated test module pins the tiny synthetic tar. Unmodified + // production readers must reject it; no runtime pin parameter is introduced. + if (observation) source = source.replace("2b3d176db752433b904a4b42375543ff398f4841d22e48f7d4f23ded925b72da", c.digest(scannerFixtureArchive)); if (fastTimeout) source = source.replace('installer ? 120000 : 15000', '100'); // Expose lexical contracts only in this test VM. Production exports no policy, // verifier, command inventory, child executable or success switch. - source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY };'; + source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES };'; const Module = require("node:module"); const instance = new Module(moduleFile, module); instance.filename = moduleFile; instance.paths = Module._nodeModulePaths(path.dirname(moduleFile)); @@ -197,7 +210,7 @@ function security(source,root){ const key=sha([value.subject.tree_digest,value.subject.manifest_digest,'lintai','0.1.3',policy.id,'2',policy.digest].join('\0')); const state=process.env.AGENTPLUGINS_HOME; if(source==='local_scan'){ - const scanner='security/lintai/0.1.3/linux-amd64/lintai',executable='not executable: scanner acquisition fixture'; + const scanner='security/lintai/0.1.3/linux-amd64/lintai',executable=scenario==='unrelated-scanner'?'arbitrary unrelated scanner bytes; never executed':'not executable: scanner acquisition fixture'; fs.mkdirSync(path.dirname(path.join(state,scanner)),{recursive:true,mode:0o700}); if(scenario!=='missing-scanner')fs.writeFileSync(path.join(state,scanner),executable,{mode:0o700}); fs.mkdirSync(path.join(state,'security/assessments'),{recursive:true,mode:0o700}); @@ -205,7 +218,7 @@ function security(source,root){ if(scenario!=='missing-cache')write(state,'security/assessments/'+key+'.json',cached); const file=path.join(process.env.TMPDIR,'fixture-scans.json'),scans=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):[]; if(scenario!=='missing-scan')scans.push({id:'dry-run/'+path.basename(root),args:['scan-agent-plugin','/'+path.basename(root)],subject:identity(root), - executable:{path:scanner,sha256:sha(executable)},report:scenario==='missing-report'?'':report}); + executable:{path:scanner,sha256:sha(executable)},archive:{url:'https://github.com/777genius/lintai/releases/download/v0.1.3/lintai-v0.1.3-x86_64-unknown-linux-gnu.tar.gz',bytes:cfg.scannerArchive},report:scenario==='missing-report'?'':report}); fs.writeFileSync(file,JSON.stringify(scans)); } return value; @@ -214,11 +227,11 @@ const installationID='12345678-1234-4234-8234-123456789abc'; const physical='skill-'+sha(installationID).slice(0,12), projection='managed/clients/codex/'+physical; function registration(state,root){ const subject=identity(root),revision={version:'0.1.0',...subject}; - const target=path.join(state,projection),binding='client_'+sha([installationID,'codex','user',target].join('\0')).slice(0,24); + const target=path.join(scenario==='wrong-target-locator'?'/not-the-owned-installer-root':state,projection),binding='client_'+sha([installationID,'codex','user',target].join('\0')).slice(0,24); return {installation_id:installationID,declared_name:'skill',source:{tree_digest:subject.tree_digest}, package:{declared_name:'skill',version:'0.1.0',manifest_digest:subject.manifest_digest}, clients:{[binding]:{client_binding_id:binding,client_id:'codex',scope:'user',materialization:'materialized',physical_artifact_id:physical, - target_locator:path.join(state,projection),package_revision:revision}}}; + target_locator:target,package_revision:revision}}}; } function installer(args) { @@ -245,6 +258,19 @@ function installer(args) { fs.copyFileSync(path.join(args[1],'skills',name,'SKILL.md'),path.join(state,projection,'skills',name,'SKILL.md')); } } + if(scenario!=='no-effect') { + fs.mkdirSync(path.join(state,projection,'.codex-plugin'),{mode:0o700}); + fs.mkdirSync(path.join(state,projection,'.agents/plugins'),{recursive:true,mode:0o700}); + const manifest={name:'skill',version:'0.1.0',description:'Owned fixture',skills:'./skills/'}; + const marketplace={name:'agentplugins-'+sha(physical).slice(0,12),plugins:[{name:'skill',source:{source:'local',path:'./'}, + policy:{installation:'AVAILABLE',authentication:'ON_INSTALL'},category:'Productivity'}]}; + if(scenario==='wrong-codex-identity')manifest.name='unrelated'; + if(scenario==='wrong-codex-reference')manifest.skills='./elsewhere/'; + if(scenario==='wrong-marketplace-identity')marketplace.name='unrelated'; + if(scenario==='wrong-marketplace-reference')marketplace.plugins[0].source.path='../unrelated'; + if(scenario!=='missing-codex-projection')write(path.join(state,projection),'.codex-plugin/plugin.json',scenario==='malformed-codex-projection'?'{malformed':manifest); + if(scenario!=='missing-marketplace')write(path.join(state,projection),'.agents/plugins/marketplace.json',scenario==='malformed-marketplace'?'{malformed':marketplace); + } if(scenario==='alter-client')write(client,'config.toml','unexpected replacement'); if(scenario==='alter-client-mode')fs.chmodSync(path.join(client,'config.toml'),0o644); if(scenario==='auth-claim')data.result.activation.authentication_attested=true; @@ -259,6 +285,8 @@ function installer(args) { if(scenario!=='remaining-registration')write(state,'state-v2.json',{schema_version:4,installations:[]}); if(scenario!=='remove-leaves-client-projection'){ for(const name of ['skill','extra-skill']){fs.unlinkSync(path.join(state,projection,'skills',name,'SKILL.md'));fs.rmdirSync(path.join(state,projection,'skills',name));} + fs.unlinkSync(path.join(state,projection,'.codex-plugin/plugin.json'));fs.rmdirSync(path.join(state,projection,'.codex-plugin')); + fs.unlinkSync(path.join(state,projection,'.agents/plugins/marketplace.json'));fs.rmdirSync(path.join(state,projection,'.agents/plugins'));fs.rmdirSync(path.join(state,projection,'.agents')); fs.rmdirSync(path.join(state,projection,'skills'));fs.rmdirSync(path.join(state,projection)); } } @@ -276,7 +304,7 @@ function subprocessFixtures(t, f, scenario = "ok") { const script = path.join(f.sandbox, "child-fixture.js"), config = path.join(f.sandbox, "child-config.json"); f.log = path.join(f.sandbox, "subprocesses.jsonl"); fs.writeFileSync(script, fixtureProgram()); - fs.writeFileSync(config, JSON.stringify({ scenario, identity: ID, go: f.go, log: f.log, + fs.writeFileSync(config, JSON.stringify({ scenario, scannerArchive: scannerFixtureArchive.toString("base64"), identity: ID, go: f.go, log: f.log, input: path.join(f.root, "candidate/candidate.json"), linker: Object.fromEntries(c.PRODUCTS.map(p => [p, c.linkerFlags(p, ID, "release-cli-contract-v1")])) })); const spawn = cp.spawn; t.mock.method(cp, "spawn", (file, args, options) => { @@ -337,7 +365,9 @@ test("fixed production orchestration and closed reader with subprocess fixtures const f=fixture();subprocessFixtures(t,f); const n=internal(true), result=await n.produce(f.options); assert.equal(result.length,2); - const reread=native.readTerminals(f.options.output,f.root,f.pins,expectations(f)); + // This synthetic archive is never genuine acquisition for the production reader. + assert.throws(() => native.readTerminals(f.options.output,f.root,f.pins,expectations(f)), /independently pinned scanner release archive/); + const reread=fixtureReader(f.options.output,f.root,f.pins,expectations(f)); assert.equal(reread[0].lane,"agentplugins/linux-amd64"); assert.equal(reread[1].lane,"plugin-kit-ai/linux-amd64"); assert.notDeepEqual(reread[0].subject,reread[1].subject); assert.deepEqual(reread[0].peer_subject,reread[1].subject); @@ -353,10 +383,10 @@ for (const scenario of ["wrong-binary","wrong-build-target","wrong-build-mode"," "runtime-claim","bad-json","invalid-utf8","missing-command","deferred-command","missing-template","yaml","wrong-result","wrong-command","parity","changed-project", "changed-input","scan-failure","wrong-plan","dry-run-effect","add-failure","no-state","no-effect","auth-claim","wrong-update","remaining-installation","wrong-package","invalid-security-contract","wrong-policy","wrong-counts","wrong-findings","cache-mismatch", "missing-scanner","missing-cache","missing-scan","missing-report","alter-client","alter-client-mode","info-mutates-state", - "remove-leaves-client-projection","remaining-registration","list-mutates-state","empty-capabilities","missing-profile","wrong-schema","raw-report-policy","raw-report-counts","raw-report-findings","wrong-installation-identity"]) { + "remove-leaves-client-projection","remaining-registration","list-mutates-state","empty-capabilities","missing-profile","wrong-schema","raw-report-policy","raw-report-counts","raw-report-findings","wrong-installation-identity", "unrelated-scanner", "wrong-target-locator", "missing-codex-projection", "malformed-codex-projection", "wrong-codex-identity", "wrong-codex-reference", "missing-marketplace", "malformed-marketplace", "wrong-marketplace-identity", "wrong-marketplace-reference"]) { test(`full subprocess journey fails without completion: ${scenario}`,async t=>{ const f=fixture();subprocessFixtures(t,f,scenario); - const reasons = { "wrong-command": /author command identifier/, "wrong-package": /independently captured package security subject/, + const reasons = { "unrelated-scanner": /scanner executable is the pinned archive member/, "wrong-target-locator": /registration targets operation-owned installer root/, "wrong-command": /author command identifier/, "wrong-package": /independently captured package security subject/, "invalid-security-contract": /fixed production security schema/, "wrong-policy": /fixed production security policy/, "wrong-counts": /security counts match outcome/, "wrong-findings": /security findings match counts/, "remove-leaves-client-projection": /remove owned projection/, "info-mutates-state": /info is read only/, @@ -418,14 +448,14 @@ test("closed reader rejects terminal and evidence mutations independently", asyn fs.symlinkSync(path.join(f.sandbox,'host-held.json'),path.join(root,'host.json')); } if(scenario==='evidence-hardlink'){special=path.join(f.sandbox,'host-alias.json');fs.linkSync(path.join(root,'host.json'),special);} - assert.throws(()=>native.readTerminals(root,f.root,f.pins,expected)); + assert.throws(()=>fixtureReader(root,f.root,f.pins,expected)); if(special)fs.unlinkSync(special); if(scenario==='evidence-link')fs.unlinkSync(path.join(root,'host.json')); if(scenario==='evidence-link'||scenario==='missing-evidence')fs.renameSync(path.join(f.sandbox,'host-held.json'),path.join(root,'host.json')); for(const [name,body]of Object.entries(original))put(name,body); }); } - assert.equal(native.readTerminals(root,f.root,f.pins,expected).length,2); + assert.equal(fixtureReader(root,f.root,f.pins,expected).length,2); }); test("rehashed evidence cannot hide omitted commands, bad plans or false preservation",async t=>{ @@ -447,7 +477,7 @@ test("rehashed evidence cannot hide omitted commands, bad plans or false preserv for(const [file,v]of Object.entries(e))put(file,v); // Attacker updates outer evidence hashes too. Semantic replay must reject. for(const p of c.PRODUCTS){const v=JSON.parse(originals[p+'-terminal.json']);v.evidence=v.evidence.map(pin=>({file:pin.file,...c.metadata(c.encode(e[pin.file]))}));put(p+'-terminal.json',v);} - assert.throws(()=>native.readTerminals(root,f.root,f.pins,expected)); + assert.throws(()=>fixtureReader(root,f.root,f.pins,expected)); for(const [file,b]of Object.entries(originals)){fs.chmodSync(path.join(root,file),0o600);fs.writeFileSync(path.join(root,file),b);} }); } @@ -532,7 +562,7 @@ test("changed host/source/attempt expectations cannot read a matching local term if(kind==='attempt')expect.producer.run_attempt++; if(kind==='preparation-attempt')expect.preparation.producer.run_attempt++; if(kind==='tool')expect.tools.go.sha256=hash('different host Go'); - assert.throws(()=>native.readTerminals(f.options.output,f.root,f.pins,expect)); + assert.throws(()=>fixtureReader(f.options.output,f.root,f.pins,expect)); } }); @@ -585,7 +615,53 @@ test("N1 reader rejects rehashed semantic omissions and contradictions", async t for (const item of files.filter(x => x.sha256 === sha256)) Object.assign(item, pin); } } + const states = e => e["transcripts.json"].installer.flatMap(r => [r.before, r.after]); + function projectionChange(e, leaf, change) { + for (const state of states(e)) { + const item = state.state.find(x => x.path.endsWith("/" + leaf)); if (!item) continue; + if (!change) { state.state = state.state.filter(x => x !== item); delete state.projection_documents[item.path]; continue; } + const old = Buffer.from(state.projection_documents[item.path], "base64"); + const body = Buffer.from(change(old.toString("utf8"))); + Object.assign(item, c.metadata(body)); state.projection_documents[item.path] = body.toString("base64"); + } + } const cases = { + "unrelated scanner with every acquisition digest rehashed": e => { + const old = e["scans.json"][0].executable.sha256, body = Buffer.from("arbitrary unrelated scanner bytes; never executed"), pin = c.metadata(body); + delete e["acquisition.json"][old]; e["acquisition.json"][pin.sha256] = body.toString("base64"); + for (const scan of e["scans.json"]) scan.executable.sha256 = pin.sha256; + for (const state of states(e)) for (const item of state.acquisition) if (item.sha256 === old) Object.assign(item, pin); + }, + "arbitrary root expectation in every snapshot": e => { + for (const state of states(e)) state.root = "/not-the-owned-installer-root"; + }, + "raw state byte mutation hidden by path normalization": e => { + const row = e["transcripts.json"].installer[4]; row.after.state_document_source.sha256 = hash("mutated raw bytes"); + }, + "missing release archive": e => { delete e["scans.json"][0].archive; }, + "substituted release archive": e => { for (const scan of e["scans.json"]) scan.archive.bytes = scannerTar(Buffer.from("unrelated")).toString("base64"); }, + "wrong release URL": e => { e["scans.json"][0].archive.url = "https://unrelated.invalid/scanner"; }, + "wrong root with coherent binding and info": e => { + for (const state of states(e)) { + if (!state.state_document) continue; const doc = JSON.parse(state.state_document); + for (const r of doc.installations) { + const cl = Object.values(r.clients)[0]; cl.target_locator = '/not-the-owned-installer-root/managed/clients/codex/' + cl.physical_artifact_id; + cl.client_binding_id = 'client_' + hash([r.installation_id, 'codex', 'user', cl.target_locator].join('\0')).slice(0, 24); + r.clients = { [cl.client_binding_id]: cl }; + } + state.state_document = JSON.stringify(doc) + '\n'; Object.assign(state.state.find(x => x.path === 'state-v2.json'), c.metadata(Buffer.from(state.state_document))); + } + const row = e['transcripts.json'].installer[4], doc = JSON.parse(row.stdout); + doc.data.clients = Object.values(JSON.parse(row.before.state_document).installations[0].clients); row.stdout = JSON.stringify(doc) + '\n'; + }, + "missing Codex manifest": e => projectionChange(e, '.codex-plugin/plugin.json'), + "malformed Codex manifest": e => projectionChange(e, '.codex-plugin/plugin.json', () => '{malformed'), + "wrong Codex identity": e => projectionChange(e, '.codex-plugin/plugin.json', s => { const v = JSON.parse(s); v.name = 'unrelated'; return JSON.stringify(v); }), + "wrong Codex reference": e => projectionChange(e, '.codex-plugin/plugin.json', s => { const v = JSON.parse(s); v.skills = './elsewhere/'; return JSON.stringify(v); }), + "missing marketplace": e => projectionChange(e, '.agents/plugins/marketplace.json'), + "malformed marketplace": e => projectionChange(e, '.agents/plugins/marketplace.json', () => '{malformed'), + "wrong marketplace identity": e => projectionChange(e, '.agents/plugins/marketplace.json', s => { const v = JSON.parse(s); v.name = 'unrelated'; return JSON.stringify(v); }), + "wrong marketplace reference": e => projectionChange(e, '.agents/plugins/marketplace.json', s => { const v = JSON.parse(s); v.plugins[0].source.path = '../unrelated'; return JSON.stringify(v); }), "missing custody": e => { e["preservation.json"].custody_before = []; e["preservation.json"].custody_after = []; }, "preparation custody omitted": e => { e["preservation.json"].custody_before.pop(); e["preservation.json"].custody_after.pop(); }, "custody substituted subject": e => { for (const k of ["custody_before", "custody_after"]) e["preservation.json"][k][0].file = "invented"; }, @@ -657,10 +733,22 @@ test("N1 reader rejects rehashed semantic omissions and contradictions", async t v.evidence = v.evidence.map(pin => ({ file: pin.file, ...c.metadata(c.encode(e[pin.file])) })); put(p + "-terminal.json", c.encode(v)); } - assert.throws(() => native.readTerminals(root, f.root, f.pins, expected), name); + const precise = { + "unrelated scanner with every acquisition digest rehashed": /scanner executable is the pinned archive member/, + "substituted release archive": /independently pinned scanner release archive/, + "wrong root with coherent binding and info": /registration targets operation-owned installer root/, + "arbitrary root expectation in every snapshot": /fixed operation-owned installer root token/, + "missing Codex manifest": /mandatory Codex projection/, + "missing marketplace": /mandatory Codex projection/, + "wrong Codex identity": /Codex plugin identity and component references/, + "wrong Codex reference": /Codex plugin identity and component references/, + "wrong marketplace identity": /Codex managed marketplace identity and reference/, + "wrong marketplace reference": /Codex managed marketplace identity and reference/ + }; + assert.throws(() => fixtureReader(root, f.root, f.pins, expected), precise[name], name); for (const [n, b] of Object.entries(originals)) put(n, b); }); - assert.equal(native.readTerminals(root, f.root, f.pins, expected).length, 2); + assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); }); for (const scenario of ["pre-close kill denied", "close never arrives", "successful termination"]) { @@ -732,3 +820,53 @@ test("fixed profile and schema pins agree with preserved domain contracts", () = const policy = fs.readFileSync(path.join(root, "install/integrationctl/agentplugins/adapters/securityscan/policy_test.go"), "utf8"); assert.ok(policy.includes(n.POLICY.digest)); }); + +// No genuine release archive is provisioned. These validators exercise only +// tiny test tar bytes; source-pinned production acceptance is checked separately. +test("scanner release pins match production source and synthetic acquisition is rejected", () => { + const contract = internal().test, source = fs.readFileSync(path.resolve(__dirname, + "../../../install/integrationctl/agentplugins/adapters/securityscan/release.go"), "utf8"); + for (const asset of Object.values(contract.SCANNER_RELEASES)) { + assert.ok(source.includes(`{"${asset.name}", "${asset.sha256}", false}`)); + assert.notEqual(asset.sha256, c.digest(scannerFixtureArchive)); + } + const scan = { executable: { path: "security/lintai/0.1.3/linux-amd64/lintai" }, archive: { + url: "https://github.com/777genius/lintai/releases/download/v0.1.3/" + contract.SCANNER_RELEASES["linux-amd64"].name, + bytes: scannerFixtureArchive.toString("base64") } }; + assert.throws(() => contract.scannerArchive(scan, scannerFixture), /independently pinned scanner release archive/); + assert.doesNotThrow(() => internal(true).test.scannerArchive(scan, scannerFixture)); + assert.throws(() => internal(true).test.scannerArchive(scan, Buffer.from("unrelated")), /pinned archive member/); +}); +test("scanner archive parsing fails closed on bounded malformed test archives", async t => { + const zlib = require("node:zlib"); + const base = zlib.gunzipSync(scannerFixtureArchive); + const checksum = tar => { + tar.fill(32, 148, 156); + tar.write(tar.subarray(0, 512).reduce((n, b) => n + b, 0).toString(8).padStart(6, "0") + "\0 ", 148); + }; + const cases = { + "checksum": tar => { tar[0] ^= 1; return tar; }, + "traversal": tar => { tar.fill(0, 0, 100); tar.write("../lintai"); checksum(tar); return tar; }, + "link": tar => { tar[156] = 50; checksum(tar); return tar; }, + "unsupported extension": tar => { tar[156] = 120; checksum(tar); return tar; }, + "missing binary": tar => { tar.fill(0, 0, 100); tar.write("other"); checksum(tar); return tar; }, + "oversized member": tar => { tar.write((33 * 1024 * 1024).toString(8).padStart(11, "0") + "\0", 124); checksum(tar); return tar; }, + "truncated entry": tar => tar.subarray(0, 520), + "missing terminator": tar => tar.subarray(0, 1024), + "duplicate member": tar => Buffer.concat([tar.subarray(0, 1024), tar]), + "trailing nonzero bytes": tar => { tar[tar.length - 1] = 1; return tar; } + }; + for (const [name, change] of Object.entries(cases)) await t.test(name, () => { + const body = zlib.gzipSync(change(Buffer.from(base))); + // Test-local pin substitution reaches the tar validator, never production + // acquisition or a caller-selectable expected digest in shipped code. + const Module = require("node:module"), m = new Module(moduleFile, module); + m.filename = moduleFile; m.paths = Module._nodeModulePaths(path.dirname(moduleFile)); + m._compile(fs.readFileSync(moduleFile, "utf8").replace( + "2b3d176db752433b904a4b42375543ff398f4841d22e48f7d4f23ded925b72da", c.digest(body)) + + "\nmodule.exports.probe = scannerArchive;", moduleFile); + assert.throws(() => m.exports.probe({ executable: { path: "security/lintai/0.1.3/linux-amd64/lintai" }, archive: { + url: "https://github.com/777genius/lintai/releases/download/v0.1.3/lintai-v0.1.3-x86_64-unknown-linux-gnu.tar.gz", + bytes: body.toString("base64") } }, scannerFixture), name); + }); +}); From 0bb8243a37e17c826259c13f7198c1bb8b6af06a Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 8 Sep 2026 21:13:38 +0000 Subject: [PATCH 4/8] fix(authoring): validate production public info projection Refs #210, #203, #208. Keep production observation and native admission fail-closed pending actual execution and independent review. --- .../scripts/authoring-native-qualification.js | 34 ++++- .../authoring-native-qualification.test.js | 128 +++++++++++++++++- 2 files changed, 153 insertions(+), 9 deletions(-) diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js index e44d79bf..d48d7374 100644 --- a/npm/agentplugins/scripts/authoring-native-qualification.js +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -590,13 +590,33 @@ function normalizeInstaller(rows, root) { state.state_document = c.encode(document).toString("utf8"); Object.assign(state.state.find(x => x.path === "state-v2.json"), c.metadata(Buffer.from(state.state_document))); } - if (row.id === "info") { - const document = jsonDocument(row.stdout); - for (const client of document.data.clients) normalizeClient(client, document.data.installation_id); - row.stdout = c.encode(document).toString("utf8"); - } } } +function publicInfoClient(value, client) { + // read.go: publicInstallationView/publicPackageRevision expose a projection, + // never the private binding, locator, physical child or catalog evidence. + // This assertion is shared by per-command capture and terminal replay. + const expected = {}; + for (const key of ["client_id", "scope", "materialization", "activation", "authentication", "policy", "verification"]) { + assert.equal(typeof client[key], "string", `registered public info ${key}`); + expected[key] = client[key]; + } + const revision = client.package_revision, exposed = {}; + for (const key of ["version", "distribution_id", "release_sequence"]) + if (revision[key]) exposed[key] = revision[key]; + // Go strings.TrimSpace uses Unicode White_Space (unlike JS trim's BOM rule). + const resolved = (revision.resolved_revision || "").replace(/^\p{White_Space}+|\p{White_Space}+$/gu, ""); + if (/^[0-9a-f]{40}$/.test(resolved)) exposed.resolved_revision = resolved; + for (const key of ["tree_digest", "manifest_digest"]) exposed[key] = revision[key]; + expected.package_revision = exposed; + if (client.affected_surfaces?.length) expected.affected_surfaces = client.affected_surfaces.slice().sort(); + // read_reconciliation.go returns indeterminate with false reconciliation for + // this isolated Codex config, with no installed/versioned native client. + // Optional observations cannot invent successful native discovery evidence. + if (["receipt_reconciled", "native_discovery_reconciled", "native_identity_state"].some(key => Object.hasOwn(value, key))) + Object.assign(expected, { receipt_reconciled: false, native_discovery_reconciled: false, native_identity_state: "indeterminate" }); + exact(value, expected, "public info client matches checked registration and isolated lifecycle observations"); +} function installedIdentity(state, project) { const document = stateDocument(state); exact(document.installations.length, 1); const registration = document.installations[0], subject = packageIdentity(project); @@ -678,8 +698,8 @@ function installed(row, spec, projects) { const identity = installedIdentity(row.before, projects.skill); exact(r.data.installation_id, identity.registration.installation_id, "info installed identity"); exact(r.data.name, "skill"); exact(r.data.version, "0.1.0"); - exact(r.data.clients.length, 1); exact(r.data.clients[0].client_id, "codex"); - exact(r.data.clients[0].package_revision, identity.client.package_revision); + exact(r.data.clients.length, 1); + publicInfoClient(r.data.clients[0], identity.client); } else { exact(row.before, row.after, "list is read only"); exact(r.data.installations, []); exact(stateDocument(row.after).installations, []); diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js index 750c7741..b1c96414 100644 --- a/npm/agentplugins/test/authoring-native-qualification.test.js +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -65,7 +65,7 @@ function internal(observation = false, fastTimeout = false) { if (fastTimeout) source = source.replace('installer ? 120000 : 15000', '100'); // Expose lexical contracts only in this test VM. Production exports no policy, // verifier, command inventory, child executable or success switch. - source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES };'; + source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES, publicInfoClient };'; const Module = require("node:module"); const instance = new Module(moduleFile, module); instance.filename = moduleFile; instance.paths = Module._nodeModulePaths(path.dirname(moduleFile)); @@ -75,6 +75,50 @@ function internal(observation = false, fastTimeout = false) { function noTerminal(f) { for (const p of c.PRODUCTS) assert.equal(fs.existsSync(path.join(f.options.output, `${p}-terminal.json`)), false); } +// Independent fixture of read.go's publicClient/publicPackageRevision JSON, +// including Go omitempty and immutable-revision redaction. Never return state. +function publicClientFixture(binding) { + const { client_id, scope, materialization, activation, authentication, policy, verification } = binding; + const value = { client_id, scope, materialization, activation, authentication, policy, verification }; + if (binding.package_revision) { + const r = binding.package_revision; + value.package_revision = { tree_digest: r.tree_digest, manifest_digest: r.manifest_digest }; + if (r.version) value.package_revision.version = r.version; + if (r.distribution_id) value.package_revision.distribution_id = r.distribution_id; + if (r.release_sequence) value.package_revision.release_sequence = r.release_sequence; + const immutable = (r.resolved_revision || "").replace(/^\p{White_Space}+|\p{White_Space}+$/gu, ""); + if (/^[0-9a-f]{40}$/.test(immutable)) value.package_revision.resolved_revision = immutable; + } + if (binding.affected_surfaces?.length) value.affected_surfaces = [...binding.affected_surfaces].sort(); + return value; +} +const publicInfoChanges = { + "scope": v => { v.scope = "project"; }, + "materialization": v => { v.materialization = "degraded"; }, + "revision": v => { v.package_revision.tree_digest = "sha256:" + "b".repeat(64); }, + "target_locator": v => { v.target_locator = "/foreign/managed/clients/codex/skill-232a373eb43a"; }, + "physical_artifact_id": v => { v.physical_artifact_id = "skill-000000000000"; }, + "client_binding_id": v => { v.client_binding_id = "client_000000000000000000000000"; }, + "client ID": v => { v.client_id = "cursor"; }, + "missing scope": v => { delete v.scope; }, + "missing materialization": v => { delete v.materialization; }, + "activation": v => { v.activation = "active"; }, + "authentication": v => { v.authentication = "authenticated"; }, + "policy": v => { v.policy = "blocked"; }, + "verification": v => { v.verification = "runtime_verified"; }, + "surfaces": v => { v.affected_surfaces = ["unregistered"]; }, + "revision version": v => { v.package_revision.version = "9.0.0"; }, + "revision manifest": v => { v.package_revision.manifest_digest = "sha256:" + "b".repeat(64); }, + "revision resolved": v => { v.package_revision.resolved_revision = "b".repeat(40); }, + "revision distribution": v => { v.package_revision.distribution_id = "invented"; }, + "revision sequence": v => { v.package_revision.release_sequence = 1; }, + "revision private evidence": v => { v.package_revision.catalog_evidence = {}; }, + "receipt claim": v => { v.receipt_reconciled = true; }, + "discovery claim": v => { v.native_discovery_reconciled = true; }, + "identity claim": v => { v.native_identity_state = "managed"; }, + "version claim": v => { v.client_version = "1.0.0"; }, + "discovery evidence": v => { v.native_discovery_evidence = { basis: "invented" }; } +}; function fixtureProgram() { // Deliberately independent response implementation with actual mkdir/write/ // state mutations in each child. A zero status without those effects is tested. @@ -86,6 +130,8 @@ const sha = s => crypto.createHash('sha256').update(s).digest('hex'); const product = selected.includes('plugin-kit-ai') ? 'plugin-kit-ai' : 'agentplugins'; fs.appendFileSync(cfg.log, JSON.stringify({selected,argv,env:process.env})+'\n'); const scenario = cfg.scenario; +${publicClientFixture.toString()} +const publicInfoChanges = {${Object.entries(publicInfoChanges).map(([key, fn]) => JSON.stringify(key) + ":" + fn.toString()).join(",")}}; const profiles = [{"id": "agent-plugins/1.0.0", "revision": "ff8ab5e392cc87bd88d87c060815a87490e51003", "digest": "sha256:97a658b7dca3ce1b4c2266b95da300fa51d9dc4ade59d73168e5f9104272da18"}, {"id": "agent-skills/2026-09-06", "revision": "69ef37e9424c0a7ea9dd2293b559e43ec8176379", "digest": "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220"}, {"id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "revision": "1.0.0", "digest": "sha256:0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883"}, {"id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "revision": "1.0.0", "digest": "sha256:6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb"}, {"id": "author-document-bounds/v1", "revision": "1", "digest": "sha256:4b8ab8fd50481ccd1a0b777dcbbfa06cf89516a5ea61ce09d56d6dd6a2c43004"}]; function output(value, status=0) { if (scenario === 'invalid-utf8') process.stdout.write(Buffer.from([0xff])); @@ -231,6 +277,7 @@ function registration(state,root){ return {installation_id:installationID,declared_name:'skill',source:{tree_digest:subject.tree_digest}, package:{declared_name:'skill',version:'0.1.0',manifest_digest:subject.manifest_digest}, clients:{[binding]:{client_binding_id:binding,client_id:'codex',scope:'user',materialization:'materialized',physical_artifact_id:physical, + activation:'manual_activation_required',authentication:'not_checked',policy:'allowed',verification:'installation_verified', target_locator:target,package_revision:revision}}}; } @@ -277,7 +324,10 @@ function installer(args) { if(scenario==='wrong-installation-identity')data.result.installation_id='87654321-1234-4234-8234-123456789abc'; } else if(verb==='info'){ if(scenario==='info-mutates-state')write(state,'unexpected-info-write','x'); - data.installation_id=installationID;data.name='skill';data.version='0.1.0';data.clients=Object.values(JSON.parse(fs.readFileSync(path.join(state,'state-v2.json'))).installations[0].clients); + data.installation_id=installationID;data.name='skill';data.version='0.1.0';data.source='local';data.mixed_version=false; + data.clients=Object.values(JSON.parse(fs.readFileSync(path.join(state,'state-v2.json'))).installations[0].clients).map(publicClientFixture); + Object.assign(data.clients[0],{receipt_reconciled:false,native_discovery_reconciled:false,native_identity_state:'indeterminate'}); + if(scenario.startsWith('public-info/'))publicInfoChanges[scenario.slice('public-info/'.length)](data.clients[0]); } else if(verb==='update')data.result={installation_id:installationID,mutated:false,no_change:scenario!=='wrong-update'}; else if(verb==='remove'){ @@ -373,12 +423,86 @@ test("fixed production orchestration and closed reader with subprocess fixtures assert.deepEqual(reread[0].peer_subject,reread[1].subject); assert.equal(reread[0].assertions.installer.length,8); assert.equal(reread[0].assertions.commands.agentplugins.length,54); + const rows = JSON.parse(fs.readFileSync(path.join(f.options.output, "transcripts.json"))).installer; + const info = rows.find(row => row.id === "info"), value = JSON.parse(info.stdout).data; + const registered = Object.values(JSON.parse(info.before.state_document).installations[0].clients)[0]; + const go = fs.readFileSync(path.resolve(__dirname, "../../../cli/plugin-kit-ai/internal/agentpluginscli/read.go"), "utf8"); + const shape = go.split("type publicClient struct {")[1].split("\n}")[0]; + const fields = [...shape.matchAll(/`json:"([^",]+)[^"]*"`/g)].map(m => m[1]).filter(k => k !== "-"); + assert.match(shape, /BindingID[^\n]+`json:"-"`/); + assert.ok(Object.keys(value.clients[0]).every(key => fields.includes(key))); + assert.deepEqual(value.clients[0], { ...publicClientFixture(registered), receipt_reconciled: false, + native_discovery_reconciled: false, native_identity_state: "indeterminate" }); + for (const key of ["target_locator", "physical_artifact_id", "client_binding_id"]) { + assert.ok(!fields.includes(key)); assert.equal(Object.hasOwn(value.clients[0], key), false); + } + assert.ok(!info.stdout.includes(f.sandbox)); + assert.deepEqual(info.before.state_document_source, info.after.state_document_source); + // The public stdout is preserved byte-for-byte rather than normalized into + // a fabricated private response; state normalization keeps separate raw pins. + assert.equal(info.stdout, JSON.stringify({ schema_version: 1, command: "info", result: "success", data: value }) + "\n"); const calls=fs.readFileSync(f.log,"utf8").trim().split("\n").map(JSON.parse); assert.ok(calls.some(x=>x.argv.includes('--dry-run'))); assert.ok(calls.some(x=>x.argv[0]==='remove')); assert.ok(calls.every(x=>!x.argv.includes('--auth-complete')&&!x.argv.includes('--accept-security-risk'))); assert.throws(()=>promotion.requireNativeContracts(result),/unknown or duplicate terminal lane|NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); }); +for (const name of ["scope", "materialization", "revision", "target_locator", "physical_artifact_id", "client_binding_id"]) { + test(`public info subprocess rejects ${name}`, async t => { + const f = fixture(); subprocessFixtures(t, f, "public-info/" + name); + await assert.rejects(internal(true).produce(f.options), /public info client matches checked registration/); + noTerminal(f); + assert.ok(fs.existsSync(path.join(f.options.output, "diagnostic.json"))); + }); +} +test("public info replay rejects coherently rehashed client contradictions", async t => { + const f = fixture(); subprocessFixtures(t, f); await internal(true).produce(f.options); + const root = f.options.output, expected = expectations(f); + const originals = Object.fromEntries(fs.readdirSync(root).map(file => [file, fs.readFileSync(path.join(root, file))])); + const put = (file, bytes) => { fs.chmodSync(path.join(root, file), 0o600); fs.writeFileSync(path.join(root, file), bytes); }; + for (const [name, change] of Object.entries(publicInfoChanges)) await t.test(name, () => { + const transcripts = JSON.parse(originals["transcripts.json"]), row = transcripts.installer.find(r => r.id === "info"); + const value = JSON.parse(row.stdout); change(value.data.clients[0]); row.stdout = JSON.stringify(value) + "\n"; + // All state snapshots/raw pins remain identical. Rehash the changed + // transcript and BOTH terminal pins, so rejection must be semantic. + const bytes = c.encode(transcripts); put("transcripts.json", bytes); + for (const p of c.PRODUCTS) { + const file = p + "-terminal.json", terminal = JSON.parse(originals[file]); + Object.assign(terminal.evidence.find(pin => pin.file === "transcripts.json"), c.metadata(bytes)); + put(file, c.encode(terminal)); + } + assert.throws(() => fixtureReader(root, f.root, f.pins, expected), /public info client matches checked registration/); + for (const [file, bytes] of Object.entries(originals)) put(file, bytes); + }); + assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); +}); +test("public info revision uses Go projection and omitempty without leaking private evidence", () => { + const binding = { client_id: "codex", scope: "user", materialization: "materialized", activation: "manual_activation_required", + authentication: "not_checked", policy: "allowed", verification: "installation_verified", affected_surfaces: ["z", "a"], + target_locator: "/private", physical_artifact_id: "private", client_binding_id: "private", + package_revision: { version: "0.1.0", resolved_revision: " " + "a".repeat(40) + " ", distribution_id: "recorded", release_sequence: 3, + tree_digest: "sha256:" + hash("tree"), manifest_digest: "sha256:" + hash("manifest"), catalog_evidence: { private: true } } }; + const check = internal().test.publicInfoClient, projected = publicClientFixture(binding); + check(projected, binding); + assert.deepEqual(projected.affected_surfaces, ["a", "z"]); + assert.equal(projected.package_revision.resolved_revision, "a".repeat(40)); + assert.equal(Object.hasOwn(projected.package_revision, "catalog_evidence"), false); + for (const key of Object.keys(projected.package_revision)) { + const missing = structuredClone(projected); delete missing.package_revision[key]; + assert.throws(() => check(missing, binding), /public info client matches checked registration/); + } + binding.package_revision.resolved_revision = "\u0085" + "a".repeat(40) + "\u0085"; + check(projected, binding); // Go trims NEL; JS String.trim does not. + for (const resolved of ["", "main", "/private/revision", "A".repeat(40), "a".repeat(39), "\uFEFF" + "a".repeat(40)]) { + binding.package_revision.resolved_revision = resolved; + binding.package_revision.version = ""; binding.package_revision.distribution_id = ""; binding.package_revision.release_sequence = 0; + binding.affected_surfaces = []; + const value = publicClientFixture(binding); check(value, binding); + assert.deepEqual(Object.keys(value.package_revision).sort(), ["manifest_digest", "tree_digest"]); + const invented = structuredClone(value); invented.package_revision.resolved_revision = resolved; + assert.throws(() => check(invented, binding), /public info client matches checked registration/); + } +}); for (const scenario of ["wrong-binary","wrong-build-target","wrong-build-mode","wrong-build-source","wrong-version","wrong-source", "runtime-claim","bad-json","invalid-utf8","missing-command","deferred-command","missing-template","yaml","wrong-result","wrong-command","parity","changed-project", "changed-input","scan-failure","wrong-plan","dry-run-effect","add-failure","no-state","no-effect","auth-claim","wrong-update","remaining-installation","wrong-package","invalid-security-contract","wrong-policy","wrong-counts","wrong-findings","cache-mismatch", From 1166c0e098ab7330deb01347444a10a6cee780b9 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 8 Sep 2026 22:11:02 +0000 Subject: [PATCH 5/8] fix(authoring): validate enclosing public installation readback Refs #210, #203, #208. Keep production observation and native admission fail-closed pending actual execution and independent review. --- .../scripts/authoring-native-qualification.js | 26 +++- .../authoring-native-qualification.test.js | 125 +++++++++++++++++- 2 files changed, 146 insertions(+), 5 deletions(-) diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js index d48d7374..ae8d2652 100644 --- a/npm/agentplugins/scripts/authoring-native-qualification.js +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -617,6 +617,27 @@ function publicInfoClient(value, client) { Object.assign(expected, { receipt_reconciled: false, native_discovery_reconciled: false, native_identity_state: "indeterminate" }); exact(value, expected, "public info client matches checked registration and isolated lifecycle observations"); } +function publicInfoInstallation(value, registration, client) { + // This journey installs one local generated package. read.go's publicSource + // returns local for its empty/absolute canonical source, with no repository + // or Directory origin. Do not invent remote/Directory readback support here. + const source = registration.source; + const canonical = (source.canonical_source || "").replace(/^\p{White_Space}+|\p{White_Space}+$/gu, ""); + assert.ok(!source.repository && (!canonical || path.posix.isAbsolute(canonical)), "public info local source registration"); + assert.ok(registration.directory == null, "public info local registration has no Directory origin"); + if (registration.needs_rebind !== undefined) exact(typeof registration.needs_rebind, "boolean"); + // installedIdentity has already checked the single materialized client's + // tree/manifest against the package. read_directory.go convergenceState + // therefore has no pending client (and no Directory release sequence). + const expected = { installation_id: registration.installation_id, name: registration.declared_name, + source: "local", clients: value.clients, mixed_version: false }; + if (registration.package.version) expected.version = registration.package.version; + if (registration.needs_rebind === true) expected.needs_rebind = true; + exact(value, expected, "public info installation matches checked registration and Go omission rules"); + assert.ok(Array.isArray(value.clients), "public info installation clients array"); + exact(value.clients.length, 1, "public info installation client count"); + publicInfoClient(value.clients[0], client); +} function installedIdentity(state, project) { const document = stateDocument(state); exact(document.installations.length, 1); const registration = document.installations[0], subject = packageIdentity(project); @@ -696,10 +717,7 @@ function installed(row, spec, projects) { } else if (verb === "info") { exact(row.before, row.after, "info is read only"); const identity = installedIdentity(row.before, projects.skill); - exact(r.data.installation_id, identity.registration.installation_id, "info installed identity"); - exact(r.data.name, "skill"); exact(r.data.version, "0.1.0"); - exact(r.data.clients.length, 1); - publicInfoClient(r.data.clients[0], identity.client); + publicInfoInstallation(r.data, identity.registration, identity.client); } else { exact(row.before, row.after, "list is read only"); exact(r.data.installations, []); exact(stateDocument(row.after).installations, []); diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js index b1c96414..b683d2d9 100644 --- a/npm/agentplugins/test/authoring-native-qualification.test.js +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -119,6 +119,44 @@ const publicInfoChanges = { "version claim": v => { v.client_version = "1.0.0"; }, "discovery evidence": v => { v.native_discovery_evidence = { basis: "invented" }; } }; +// Enclosing read.go publicInstallation mutations; private additions use the +// actual checked binding so privacy failures cannot be mistaken for bad IDs. +const publicInstallationChanges = { + "wrong installation_id": v => { v.installation_id = "00000000-0000-4000-8000-000000000000"; }, + "wrong name": v => { v.name = "foreign"; }, + "wrong version": v => { v.version = "9.0.0"; }, + "wrong source": v => { v.source = "https://example.invalid/foreign"; }, + "mixed version": v => { v.mixed_version = true; }, + "wrong mixed version type": v => { v.mixed_version = "false"; }, + "false rebind claim": v => { v.needs_rebind = true; }, + "explicit false rebind": v => { v.needs_rebind = false; }, + "null rebind": v => { v.needs_rebind = null; }, + "empty convergence": v => { v.convergence_action = ""; }, + "null convergence": v => { v.convergence_action = null; }, + "false convergence": v => { v.convergence_action = "update"; }, + "production-format false convergence": v => { + v.mixed_version = true; + v.convergence_action = "run `agentplugins update " + v.installation_id + " --target codex` to converge the remaining clients"; + }, + "invented directory": v => { v.directory = {}; }, + "null directory": v => { v.directory = null; }, + "invented warnings": v => { v.warnings = [{ code: "invented" }]; }, + "empty warnings": v => { v.warnings = []; }, + "null warnings": v => { v.warnings = null; }, + "unknown field": v => { v.private_claim = true; }, + "empty clients": v => { v.clients = []; }, + "null clients": v => { v.clients = null; }, + "extra client": v => { v.clients.push(structuredClone(v.clients[0])); }, + "missing installation_id": v => { delete v.installation_id; }, + "missing name": v => { delete v.name; }, + "missing version": v => { delete v.version; }, + "missing source": v => { delete v.source; }, + "missing clients": v => { delete v.clients; }, + "missing mixed_version": v => { delete v.mixed_version; }, + "private target_locator": (v, binding) => { v.target_locator = binding.target_locator; }, + "private physical_artifact_id": (v, binding) => { v.physical_artifact_id = binding.physical_artifact_id; }, + "private client_binding_id": (v, binding) => { v.client_binding_id = binding.client_binding_id; }, +}; function fixtureProgram() { // Deliberately independent response implementation with actual mkdir/write/ // state mutations in each child. A zero status without those effects is tested. @@ -131,6 +169,7 @@ const product = selected.includes('plugin-kit-ai') ? 'plugin-kit-ai' : 'agentplu fs.appendFileSync(cfg.log, JSON.stringify({selected,argv,env:process.env})+'\n'); const scenario = cfg.scenario; ${publicClientFixture.toString()} +const publicInstallationChanges = {${Object.entries(publicInstallationChanges).map(([key, fn]) => JSON.stringify(key) + ":" + fn.toString()).join(",")}}; const publicInfoChanges = {${Object.entries(publicInfoChanges).map(([key, fn]) => JSON.stringify(key) + ":" + fn.toString()).join(",")}}; const profiles = [{"id": "agent-plugins/1.0.0", "revision": "ff8ab5e392cc87bd88d87c060815a87490e51003", "digest": "sha256:97a658b7dca3ce1b4c2266b95da300fa51d9dc4ade59d73168e5f9104272da18"}, {"id": "agent-skills/2026-09-06", "revision": "69ef37e9424c0a7ea9dd2293b559e43ec8176379", "digest": "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220"}, {"id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "revision": "1.0.0", "digest": "sha256:0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883"}, {"id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "revision": "1.0.0", "digest": "sha256:6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb"}, {"id": "author-document-bounds/v1", "revision": "1", "digest": "sha256:4b8ab8fd50481ccd1a0b777dcbbfa06cf89516a5ea61ce09d56d6dd6a2c43004"}]; function output(value, status=0) { @@ -274,7 +313,8 @@ const physical='skill-'+sha(installationID).slice(0,12), projection='managed/cli function registration(state,root){ const subject=identity(root),revision={version:'0.1.0',...subject}; const target=path.join(scenario==='wrong-target-locator'?'/not-the-owned-installer-root':state,projection),binding='client_'+sha([installationID,'codex','user',target].join('\0')).slice(0,24); - return {installation_id:installationID,declared_name:'skill',source:{tree_digest:subject.tree_digest}, + return {installation_id:installationID,declared_name:'skill',source:{tree_digest:subject.tree_digest,...(scenario.startsWith('installation-rebind')?{canonical_source:root}:{})}, + ...(scenario.startsWith('installation-rebind')?{needs_rebind:true}:{}), package:{declared_name:'skill',version:'0.1.0',manifest_digest:subject.manifest_digest}, clients:{[binding]:{client_binding_id:binding,client_id:'codex',scope:'user',materialization:'materialized',physical_artifact_id:physical, activation:'manual_activation_required',authentication:'not_checked',policy:'allowed',verification:'installation_verified', @@ -327,6 +367,12 @@ function installer(args) { data.installation_id=installationID;data.name='skill';data.version='0.1.0';data.source='local';data.mixed_version=false; data.clients=Object.values(JSON.parse(fs.readFileSync(path.join(state,'state-v2.json'))).installations[0].clients).map(publicClientFixture); Object.assign(data.clients[0],{receipt_reconciled:false,native_discovery_reconciled:false,native_identity_state:'indeterminate'}); + const reg=JSON.parse(fs.readFileSync(path.join(state,'state-v2.json'))).installations[0]; + if(reg.needs_rebind)data.needs_rebind=true; + if(scenario==='installation-rebind/missing')delete data.needs_rebind; + if(scenario==='installation-rebind/false')data.needs_rebind=false; + if(scenario==='installation-rebind/string')data.needs_rebind='true'; + if(scenario.startsWith('public-installation/'))publicInstallationChanges[scenario.slice('public-installation/'.length)](data,Object.values(reg.clients)[0]); if(scenario.startsWith('public-info/'))publicInfoChanges[scenario.slice('public-info/'.length)](data.clients[0]); } else if(verb==='update')data.result={installation_id:installationID,mutated:false,no_change:scenario!=='wrong-update'}; @@ -436,6 +482,14 @@ test("fixed production orchestration and closed reader with subprocess fixtures for (const key of ["target_locator", "physical_artifact_id", "client_binding_id"]) { assert.ok(!fields.includes(key)); assert.equal(Object.hasOwn(value.clients[0], key), false); } + const installationShape = go.split("type publicInstallation struct {")[1].split("\n}")[0]; + const installationFields = [...installationShape.matchAll(/`json:"([^",]+)(,omitempty)?"`/g)]; + assert.deepEqual(installationFields.filter(m => !m[2]).map(m => m[1]).sort(), + ["installation_id", "name", "source", "clients", "mixed_version"].sort()); + assert.deepEqual(value, { installation_id: "12345678-1234-4234-8234-123456789abc", name: "skill", + version: "0.1.0", source: "local", clients: value.clients, mixed_version: false }); + for (const key of ["target_locator", "physical_artifact_id", "client_binding_id"]) + assert.ok(!installationFields.some(m => m[1] === key)); assert.ok(!info.stdout.includes(f.sandbox)); assert.deepEqual(info.before.state_document_source, info.after.state_document_source); // The public stdout is preserved byte-for-byte rather than normalized into @@ -447,6 +501,75 @@ test("fixed production orchestration and closed reader with subprocess fixtures assert.ok(calls.every(x=>!x.argv.includes('--auth-complete')&&!x.argv.includes('--accept-security-risk'))); assert.throws(()=>promotion.requireNativeContracts(result),/unknown or duplicate terminal lane|NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); }); +for (const name of Object.keys(publicInstallationChanges)) { + test(`public installation subprocess rejects ${name}`, async t => { + const f = fixture(); subprocessFixtures(t, f, "public-installation/" + name); + await assert.rejects(internal(true).produce(f.options), /public info installation/); + noTerminal(f); + assert.ok(fs.existsSync(path.join(f.options.output, "diagnostic.json"))); + }); +} +for (const name of ["missing", "false", "string"]) { + test(`public installation subprocess rejects registered rebind ${name}`, async t => { + const f = fixture(); subprocessFixtures(t, f, "installation-rebind/" + name); + await assert.rejects(internal(true).produce(f.options), /public info installation/); + noTerminal(f); + }); +} +test("public installation replay rejects all coherently rehashed enclosing contradictions", async t => { + const f = fixture(); subprocessFixtures(t, f); await internal(true).produce(f.options); + const root = f.options.output, expected = expectations(f); + assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); + const originals = Object.fromEntries(fs.readdirSync(root).map(file => [file, fs.readFileSync(path.join(root, file))])); + const put = (file, bytes) => { fs.chmodSync(path.join(root, file), 0o600); fs.writeFileSync(path.join(root, file), bytes); }; + for (const [name, change] of Object.entries(publicInstallationChanges)) await t.test(name, () => { + const transcripts = JSON.parse(originals["transcripts.json"]), row = transcripts.installer.find(r => r.id === "info"); + const reg = JSON.parse(row.before.state_document).installations[0], value = JSON.parse(row.stdout); + change(value.data, Object.values(reg.clients)[0]); row.stdout = JSON.stringify(value) + "\n"; + put("transcripts.json", c.encode(transcripts)); + for (const p of c.PRODUCTS) { + const file = p + "-terminal.json", terminal = JSON.parse(originals[file]); + for (const pin of terminal.evidence) Object.assign(pin, c.metadata(fs.readFileSync(path.join(root, pin.file)))); + put(file, c.encode(terminal)); + for (const pin of terminal.evidence) + assert.deepEqual(c.metadata(fs.readFileSync(path.join(root, pin.file))), { size: pin.size, sha256: pin.sha256 }); + } + assert.throws(() => fixtureReader(root, f.root, f.pins, expected), /public info installation/); + for (const [file, bytes] of Object.entries(originals)) put(file, bytes); + }); + assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); +}); +test("public installation reports registered needs_rebind with Go omission and exact stdout", async t => { + const f = fixture(); subprocessFixtures(t, f, "installation-rebind"); + // Deliberately noncanonical public bytes must survive both assertion paths. + const script = path.join(f.sandbox, "child-fixture.js"), source = fs.readFileSync(script, "utf8"); + const marker = "else process.stdout.write(JSON.stringify(value)+'\\n');"; + assert.ok(source.includes(marker)); + fs.writeFileSync(script, source.replace(marker, + "else if(value.command==='info')process.stdout.write(' \\n'+JSON.stringify(value,null,2)+'\\n\\t'); " + marker)); + assert.equal((await internal(true).produce(f.options)).length, 2); + assert.equal(fixtureReader(f.options.output, f.root, f.pins, expectations(f)).length, 2); + const file = path.join(f.options.output, "transcripts.json"), transcripts = JSON.parse(fs.readFileSync(file)); + const row = transcripts.installer.find(r => r.id === "info"), value = JSON.parse(row.stdout); + assert.equal(JSON.parse(row.before.state_document).installations[0].needs_rebind, true); + assert.deepEqual(Object.keys(value.data).sort(), ["installation_id", "name", "version", "source", "clients", "mixed_version", "needs_rebind"].sort()); + assert.equal(value.data.needs_rebind, true); assert.equal(value.data.mixed_version, false); + assert.equal(row.stdout, " \n" + JSON.stringify(value, null, 2) + "\n\t"); + assert.deepEqual(row.before, row.after); + const originals = Object.fromEntries(fs.readdirSync(f.options.output).map(n => [n, fs.readFileSync(path.join(f.options.output, n))])); + for (const replacement of [undefined, false, "true"]) { + const changed = structuredClone(value); + if (replacement === undefined) delete changed.data.needs_rebind; else changed.data.needs_rebind = replacement; + row.stdout = JSON.stringify(changed) + "\n"; + fs.chmodSync(file, 0o600); fs.writeFileSync(file, c.encode(transcripts)); + for (const p of c.PRODUCTS) { + const name = p + "-terminal.json", terminal = JSON.parse(originals[name]); + for (const pin of terminal.evidence) Object.assign(pin, c.metadata(fs.readFileSync(path.join(f.options.output, pin.file)))); + fs.chmodSync(path.join(f.options.output, name), 0o600); fs.writeFileSync(path.join(f.options.output, name), c.encode(terminal)); + } + assert.throws(() => fixtureReader(f.options.output, f.root, f.pins, expectations(f)), /public info installation/); + } +}); for (const name of ["scope", "materialization", "revision", "target_locator", "physical_artifact_id", "client_binding_id"]) { test(`public info subprocess rejects ${name}`, async t => { const f = fixture(); subprocessFixtures(t, f, "public-info/" + name); From 7f545a7cd78eb2c683635ade67d5453aaa86fde2 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 8 Sep 2026 23:53:36 +0000 Subject: [PATCH 6/8] fix(authoring): enforce reconciliation and update eligibility Refs #210, #203, #208. Keep production observation and native admission fail-closed pending actual execution and independent review. --- .../scripts/authoring-native-qualification.js | 15 ++- .../authoring-native-qualification.test.js | 113 +++++++++++++++--- 2 files changed, 107 insertions(+), 21 deletions(-) diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js index ae8d2652..8bf6feb8 100644 --- a/npm/agentplugins/scripts/authoring-native-qualification.js +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -612,9 +612,9 @@ function publicInfoClient(value, client) { if (client.affected_surfaces?.length) expected.affected_surfaces = client.affected_surfaces.slice().sort(); // read_reconciliation.go returns indeterminate with false reconciliation for // this isolated Codex config, with no installed/versioned native client. - // Optional observations cannot invent successful native discovery evidence. - if (["receipt_reconciled", "native_discovery_reconciled", "native_identity_state"].some(key => Object.hasOwn(value, key))) - Object.assign(expected, { receipt_reconciled: false, native_discovery_reconciled: false, native_identity_state: "indeterminate" }); + // The fixed info --target=codex route always reconciles its selected binding. + // Go serializes nonnil false pointers; stdout cannot opt out by omitting them. + Object.assign(expected, { receipt_reconciled: false, native_discovery_reconciled: false, native_identity_state: "indeterminate" }); exact(value, expected, "public info client matches checked registration and isolated lifecycle observations"); } function publicInfoInstallation(value, registration, client) { @@ -697,7 +697,14 @@ function installed(row, spec, projects) { exact(result.mutated, verb !== "update"); const identity = installedIdentity(verb === "add" ? row.after : row.before, projects.skill); exact(result.installation_id, identity.registration.installation_id, "lifecycle installed identity"); - if (verb === "update") { exact(row.before.state_document_source, row.after.state_document_source, "update raw state bytes preserved"); exact(result.no_change, true); exact(row.before.state, row.after.state); exact(row.before.client, row.after.client); } + if (verb === "update") { + // prepareUpdateMany (and lifecycle.go) reject unbound/legacy registrations + // before an unchanged update can succeed. Use captured state, not stdout. + assert.ok(!identity.registration.needs_rebind && identity.registration.package.loader_kind === "agent_plugins", + "update requires a bound Agent Plugins installation"); + exact(row.before.state_document_source, row.after.state_document_source, "update raw state bytes preserved"); + exact(result.no_change, true); exact(row.before.state, row.after.state); exact(row.before.client, row.after.client); + } else assert.notDeepEqual(row.before.state, row.after.state, "real lifecycle state mutation"); if (verb === "add") { exact(result.activation.authentication, "not_checked"); diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js index b683d2d9..e09507e2 100644 --- a/npm/agentplugins/test/authoring-native-qualification.test.js +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -65,7 +65,7 @@ function internal(observation = false, fastTimeout = false) { if (fastTimeout) source = source.replace('installer ? 120000 : 15000', '100'); // Expose lexical contracts only in this test VM. Production exports no policy, // verifier, command inventory, child executable or success switch. - source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES, publicInfoClient };'; + source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES, publicInfoClient, publicInfoInstallation, installed };'; const Module = require("node:module"); const instance = new Module(moduleFile, module); instance.filename = moduleFile; instance.paths = Module._nodeModulePaths(path.dirname(moduleFile)); @@ -90,6 +90,7 @@ function publicClientFixture(binding) { if (/^[0-9a-f]{40}$/.test(immutable)) value.package_revision.resolved_revision = immutable; } if (binding.affected_surfaces?.length) value.affected_surfaces = [...binding.affected_surfaces].sort(); + Object.assign(value, { receipt_reconciled: false, native_discovery_reconciled: false, native_identity_state: "indeterminate" }); return value; } const publicInfoChanges = { @@ -113,6 +114,13 @@ const publicInfoChanges = { "revision distribution": v => { v.package_revision.distribution_id = "invented"; }, "revision sequence": v => { v.package_revision.release_sequence = 1; }, "revision private evidence": v => { v.package_revision.catalog_evidence = {}; }, + "missing receipt": v => { delete v.receipt_reconciled; }, + "missing discovery": v => { delete v.native_discovery_reconciled; }, + "missing identity": v => { delete v.native_identity_state; }, + "all reconciliation omitted": v => { delete v.receipt_reconciled; delete v.native_discovery_reconciled; delete v.native_identity_state; }, + "receipt type": v => { v.receipt_reconciled = "false"; }, + "discovery type": v => { v.native_discovery_reconciled = "false"; }, + "identity type": v => { v.native_identity_state = false; }, "receipt claim": v => { v.receipt_reconciled = true; }, "discovery claim": v => { v.native_discovery_reconciled = true; }, "identity claim": v => { v.native_identity_state = "managed"; }, @@ -173,6 +181,8 @@ const publicInstallationChanges = {${Object.entries(publicInstallationChanges).m const publicInfoChanges = {${Object.entries(publicInfoChanges).map(([key, fn]) => JSON.stringify(key) + ":" + fn.toString()).join(",")}}; const profiles = [{"id": "agent-plugins/1.0.0", "revision": "ff8ab5e392cc87bd88d87c060815a87490e51003", "digest": "sha256:97a658b7dca3ce1b4c2266b95da300fa51d9dc4ade59d73168e5f9104272da18"}, {"id": "agent-skills/2026-09-06", "revision": "69ef37e9424c0a7ea9dd2293b559e43ec8176379", "digest": "sha256:b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220"}, {"id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "revision": "1.0.0", "digest": "sha256:0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883"}, {"id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "revision": "1.0.0", "digest": "sha256:6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb"}, {"id": "author-document-bounds/v1", "revision": "1", "digest": "sha256:4b8ab8fd50481ccd1a0b777dcbbfa06cf89516a5ea61ce09d56d6dd6a2c43004"}]; function output(value, status=0) { + if(scenario.startsWith('installation-rebind') && ['add','info','update'].includes(value.command) && !value.data.dry_run) + fs.writeFileSync(cfg.log+'.registration-'+value.command,fs.readFileSync(path.join(process.env.AGENTPLUGINS_HOME,'state-v2.json'))); if (scenario === 'invalid-utf8') process.stdout.write(Buffer.from([0xff])); else if (scenario === 'bad-json') process.stdout.write('{bad'); else process.stdout.write(JSON.stringify(value)+'\n'); @@ -313,9 +323,11 @@ const physical='skill-'+sha(installationID).slice(0,12), projection='managed/cli function registration(state,root){ const subject=identity(root),revision={version:'0.1.0',...subject}; const target=path.join(scenario==='wrong-target-locator'?'/not-the-owned-installer-root':state,projection),binding='client_'+sha([installationID,'codex','user',target].join('\0')).slice(0,24); - return {installation_id:installationID,declared_name:'skill',source:{tree_digest:subject.tree_digest,...(scenario.startsWith('installation-rebind')?{canonical_source:root}:{})}, + return {installation_id:installationID,declared_name:'skill',origin_mode:'direct', + source:{source_binding_id:'src_'+sha(root+'\0\0').slice(0,24),requested_source:root,canonical_source:root,resolved_revision:'',tree_digest:subject.tree_digest}, ...(scenario.startsWith('installation-rebind')?{needs_rebind:true}:{}), - package:{declared_name:'skill',version:'0.1.0',manifest_digest:subject.manifest_digest}, + package:{loader_kind:'agent_plugins',format_id:'agent-plugins/1.0.0',schema_uri:'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + declared_name:'skill',version:'0.1.0',manifest_digest:subject.manifest_digest}, clients:{[binding]:{client_binding_id:binding,client_id:'codex',scope:'user',materialization:'materialized',physical_artifact_id:physical, activation:'manual_activation_required',authentication:'not_checked',policy:'allowed',verification:'installation_verified', target_locator:target,package_revision:revision}}}; @@ -539,8 +551,8 @@ test("public installation replay rejects all coherently rehashed enclosing contr }); assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); }); -test("public installation reports registered needs_rebind with Go omission and exact stdout", async t => { - const f = fixture(); subprocessFixtures(t, f, "installation-rebind"); +test("public installation preserves Go omission and exact stdout with isolated true rebind info", async t => { + const f = fixture(); subprocessFixtures(t, f); // Deliberately noncanonical public bytes must survive both assertion paths. const script = path.join(f.sandbox, "child-fixture.js"), source = fs.readFileSync(script, "utf8"); const marker = "else process.stdout.write(JSON.stringify(value)+'\\n');"; @@ -551,26 +563,93 @@ test("public installation reports registered needs_rebind with Go omission and e assert.equal(fixtureReader(f.options.output, f.root, f.pins, expectations(f)).length, 2); const file = path.join(f.options.output, "transcripts.json"), transcripts = JSON.parse(fs.readFileSync(file)); const row = transcripts.installer.find(r => r.id === "info"), value = JSON.parse(row.stdout); - assert.equal(JSON.parse(row.before.state_document).installations[0].needs_rebind, true); + const reg = JSON.parse(row.before.state_document).installations[0]; + assert.equal(reg.needs_rebind, undefined); + // Truthful true-rebind info is valid in isolation, with no successful update. + reg.needs_rebind = true; value.data.needs_rebind = true; + const checkInfo = internal().test.publicInfoInstallation; + checkInfo(value.data, reg, Object.values(reg.clients)[0]); assert.deepEqual(Object.keys(value.data).sort(), ["installation_id", "name", "version", "source", "clients", "mixed_version", "needs_rebind"].sort()); assert.equal(value.data.needs_rebind, true); assert.equal(value.data.mixed_version, false); - assert.equal(row.stdout, " \n" + JSON.stringify(value, null, 2) + "\n\t"); + assert.equal(row.stdout, " \n" + JSON.stringify(JSON.parse(row.stdout), null, 2) + "\n\t"); assert.deepEqual(row.before, row.after); - const originals = Object.fromEntries(fs.readdirSync(f.options.output).map(n => [n, fs.readFileSync(path.join(f.options.output, n))])); for (const replacement of [undefined, false, "true"]) { const changed = structuredClone(value); if (replacement === undefined) delete changed.data.needs_rebind; else changed.data.needs_rebind = replacement; - row.stdout = JSON.stringify(changed) + "\n"; - fs.chmodSync(file, 0o600); fs.writeFileSync(file, c.encode(transcripts)); - for (const p of c.PRODUCTS) { - const name = p + "-terminal.json", terminal = JSON.parse(originals[name]); - for (const pin of terminal.evidence) Object.assign(pin, c.metadata(fs.readFileSync(path.join(f.options.output, pin.file)))); - fs.chmodSync(path.join(f.options.output, name), 0o600); fs.writeFileSync(path.join(f.options.output, name), c.encode(terminal)); - } - assert.throws(() => fixtureReader(f.options.output, f.root, f.pins, expectations(f)), /public info installation/); + assert.throws(() => checkInfo(changed.data, reg, Object.values(reg.clients)[0]), /public info installation/); } }); -for (const name of ["scope", "materialization", "revision", "target_locator", "physical_artifact_id", "client_binding_id"]) { +test("registered rebind child info succeeds but mandatory unchanged update rejects", async t => { + const f = fixture(); subprocessFixtures(t, f, "installation-rebind"); + await assert.rejects(internal(true).produce(f.options), /update requires a bound Agent Plugins installation/); + noTerminal(f); + const failure = JSON.parse(fs.readFileSync(path.join(f.options.output, "failure-transcripts.json"))).invocations.installer; + const info = failure.find(r => r.id === "info"), update = failure.find(r => r.id === "update"); + assert.equal(JSON.parse(info.stdout).data.needs_rebind, true); + assert.equal(update.status, 0); assert.equal(JSON.parse(update.stdout).data.result.no_change, true); + const raw = fs.readFileSync(f.log + ".registration-add"); + for (const verb of ["info", "update"]) assert.deepEqual(fs.readFileSync(f.log + ".registration-" + verb), raw); + const reg = JSON.parse(raw).installations[0]; + assert.equal(reg.needs_rebind, true); assert.equal(reg.package.loader_kind, "agent_plugins"); + internal().test.publicInfoInstallation(JSON.parse(info.stdout).data, reg, Object.values(reg.clients)[0]); +}); +test("update replay rejects ineligible registration with coherent state and all evidence pins", async t => { + const f = fixture(); subprocessFixtures(t, f); await internal(true).produce(f.options); + const root = f.options.output, expected = expectations(f), api = internal().test; + const originals = Object.fromEntries(fs.readdirSync(root).map(file => [file, fs.readFileSync(path.join(root, file))])); + const put = (file, bytes) => { fs.chmodSync(path.join(root, file), 0o600); fs.writeFileSync(path.join(root, file), bytes); }; + for (const scenario of ["needs_rebind", "legacy loader"]) await t.test(scenario, () => { + try { + const tr = JSON.parse(originals["transcripts.json"]); + // Change every registered snapshot from add through remove, including its + // raw byte pin. Info remains truthful and no command silently rebinds. + for (const row of tr.installer) for (const state of [row.before, row.after]) { + if (!state.state_document) continue; + const doc = JSON.parse(state.state_document); + if (!doc.installations.length) continue; + const reg = doc.installations[0]; + if (scenario === "needs_rebind") reg.needs_rebind = true; + else reg.package.loader_kind = "legacy"; + const bytes = c.encode(doc); state.state_document = bytes.toString("utf8"); + Object.assign(state.state.find(x => x.path === "state-v2.json"), c.metadata(bytes)); + state.state_document_source = c.metadata(bytes); + } + const info = tr.installer.find(r => r.id === "info"), value = JSON.parse(info.stdout); + if (scenario === "needs_rebind") value.data.needs_rebind = true; + info.stdout = JSON.stringify(value) + "\n"; + for (let i = 1; i < tr.installer.length; i++) assert.deepEqual(tr.installer[i].before, tr.installer[i - 1].after); + const update = tr.installer.find(r => r.id === "update"); assert.deepEqual(update.before, update.after); + const projects = JSON.parse(originals["trees.json"]).agentplugins; + api.installed(info, api.installationCommands().find(x => x.id === "info"), projects); + put("transcripts.json", c.encode(tr)); + for (const p of c.PRODUCTS) { + const file = p + "-terminal.json", terminal = JSON.parse(originals[file]); + for (const pin of terminal.evidence) Object.assign(pin, c.metadata(fs.readFileSync(path.join(root, pin.file)))); + put(file, c.encode(terminal)); + for (const pin of terminal.evidence) + assert.deepEqual(c.metadata(fs.readFileSync(path.join(root, pin.file))), { size: pin.size, sha256: pin.sha256 }); + } + assert.throws(() => fixtureReader(root, f.root, f.pins, expected), /update requires a bound Agent Plugins installation/); + // Keep the earlier true-registration omission/type replay negatives: each + // must reject at info before the independent update eligibility check. + if (scenario === "needs_rebind") for (const replacement of [undefined, false, "true"]) { + const reg = JSON.parse(info.before.state_document).installations[0], changed = structuredClone(value); + if (replacement === undefined) delete changed.data.needs_rebind; else changed.data.needs_rebind = replacement; + info.stdout = JSON.stringify(changed) + "\n"; put("transcripts.json", c.encode(tr)); + for (const p of c.PRODUCTS) { + const file = p + "-terminal.json", terminal = JSON.parse(originals[file]); + for (const pin of terminal.evidence) Object.assign(pin, c.metadata(fs.readFileSync(path.join(root, pin.file)))); + put(file, c.encode(terminal)); + } + assert.equal(reg.needs_rebind, true); + assert.throws(() => fixtureReader(root, f.root, f.pins, expected), /public info installation/); + } + } finally { for (const [file, bytes] of Object.entries(originals)) put(file, bytes); } + }); + assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); +}); +for (const name of ["scope", "materialization", "revision", "target_locator", "physical_artifact_id", "client_binding_id", + "all reconciliation omitted", "missing receipt", "missing discovery", "missing identity", "receipt type", "discovery type", "identity type"]) { test(`public info subprocess rejects ${name}`, async t => { const f = fixture(); subprocessFixtures(t, f, "public-info/" + name); await assert.rejects(internal(true).produce(f.options), /public info client matches checked registration/); From f900f72a04fe18a9f47821f606e09c84dc812011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D0=B8=D1=8F?= Date: Fri, 11 Sep 2026 10:01:18 +0300 Subject: [PATCH 7/8] feat(authoring): consolidate admission and paired staging checkpoints (#216) Consolidates native admission and paired staging checkpoints for the standard-first authoring engine: offline public evidence separation, native admission binding to checked provider artifacts, frozen input metadata codecs, paired npm pack validation, paired public stage byte contracts, native input provenance, paired input/stage workflows, provenance and paired staging consolidation, reviewed public v2 consumer integration, offline public evidence checkpoint, closed public journey intake contract, and public tool binding to frozen source provisioning. Independent exact-head CI (native offline, polyglot smoke) green prior to merge. Refs #216 Refs #208 --- .github/authoring-public-tools.json | 266 ++++ .../workflows/agentplugins-npm-publish.yml | 377 +++++- .github/workflows/agentplugins-release.yml | 365 ++++- .github/workflows/authoring-frozen-native.yml | 82 +- .../cmd/agentplugins/main_test.go | 134 ++ .../cmd/agentplugins/release_root_test.go | 37 + .../cmd/agentplugins/release_workflow_test.go | 531 +++++++- .../commands/packed_installer_test.go | 69 + .../scaffold/template_quoting_test.go | 14 +- .../lib/public-authoring-contract.js | 210 +++ .../lib/public-authoring-input.js | 121 ++ npm/agentplugins/lib/public-authoring.js | 183 ++- .../scripts/authoring-native-inputs.js | 247 ++++ .../scripts/authoring-native-qualification.js | 108 +- .../scripts/authoring-promotion.js | 346 ++++- .../scripts/npm-public-contract.js | 30 +- .../scripts/packed-installer-bridge.js | 84 +- .../scripts/packed-installer-bridge.md | 292 +++- .../scripts/packed-installer-bridge.test.js | 93 ++ .../scripts/public-authoring-acceptance.js | 1201 +++++++++++++++++ .../scripts/public-authoring-tools.js | 148 ++ .../scripts/stage-authoring-npm.js | 571 +++++++- .../scripts/stage-dual-authoring-npm.js | 44 +- .../test/authoring-native-inputs.test.js | 689 ++++++++++ .../authoring-native-qualification.test.js | 112 +- .../test/authoring-promotion.test.js | 498 ++++++- .../test/authoring-public-stage.test.js | 836 ++++++++++++ .../test/npm-public-contract.test.js | 180 ++- .../test/public-authoring-acceptance.test.js | 574 ++++++++ .../test/public-authoring-native.test.js | 23 +- .../test/public-authoring-pack.test.js | 49 +- .../test/public-authoring-tools.test.js | 153 +++ .../test/public-authoring-v2-pack.test.js | 177 +++ .../test/public-authoring-v2.test.js | 438 ++++++ .../test/public-authoring.test.js | 18 +- scripts/check-packed-ci.py | 287 +++- scripts/read-authoring-evidence-zip.py | 231 ++++ scripts/run-packed-ci.py | 74 +- scripts/test_packed_ci.py | 360 ++++- scripts/test_read_authoring_evidence_zip.py | 196 +++ 40 files changed, 10226 insertions(+), 222 deletions(-) create mode 100644 .github/authoring-public-tools.json create mode 100644 npm/agentplugins/lib/public-authoring-contract.js create mode 100644 npm/agentplugins/lib/public-authoring-input.js create mode 100644 npm/agentplugins/scripts/authoring-native-inputs.js create mode 100644 npm/agentplugins/scripts/public-authoring-acceptance.js create mode 100644 npm/agentplugins/scripts/public-authoring-tools.js create mode 100644 npm/agentplugins/test/authoring-native-inputs.test.js create mode 100644 npm/agentplugins/test/authoring-public-stage.test.js create mode 100644 npm/agentplugins/test/public-authoring-acceptance.test.js create mode 100644 npm/agentplugins/test/public-authoring-tools.test.js create mode 100644 npm/agentplugins/test/public-authoring-v2-pack.test.js create mode 100644 npm/agentplugins/test/public-authoring-v2.test.js create mode 100644 scripts/read-authoring-evidence-zip.py create mode 100644 scripts/test_read_authoring_evidence_zip.py diff --git a/.github/authoring-public-tools.json b/.github/authoring-public-tools.json new file mode 100644 index 00000000..df963298 --- /dev/null +++ b/.github/authoring-public-tools.json @@ -0,0 +1,266 @@ +{ + "schema": "authoring-public-tools/v1", + "controllers": { + "linux-amd64": { + "node": null, + "python": null, + "git": null, + "gh": null, + "tar": null + }, + "linux-arm64": { + "node": null, + "python": null, + "git": null, + "gh": null, + "tar": null + }, + "darwin-amd64": { + "node": null, + "python": null, + "git": null, + "gh": null, + "tar": null + }, + "darwin-arm64": { + "node": null, + "python": null, + "git": null, + "gh": null, + "tar": null + }, + "windows-amd64": { + "node": null, + "python": null, + "git": null, + "gh": null, + "tar": null + }, + "windows-arm64": { + "node": null, + "python": null, + "git": null, + "gh": null, + "tar": null + } + }, + "cells": { + "linux-amd64/kit-node18": { + "runner": null, + "image": null, + "controller": "linux-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "linux-amd64/pair-node22": { + "runner": null, + "image": null, + "controller": "linux-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "linux-amd64/pair-node24": { + "runner": null, + "image": null, + "controller": "linux-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "linux-arm64/kit-node18": { + "runner": null, + "image": null, + "controller": "linux-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "linux-arm64/pair-node22": { + "runner": null, + "image": null, + "controller": "linux-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "linux-arm64/pair-node24": { + "runner": null, + "image": null, + "controller": "linux-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "darwin-amd64/kit-node18": { + "runner": null, + "image": null, + "controller": "darwin-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "darwin-amd64/pair-node22": { + "runner": null, + "image": null, + "controller": "darwin-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "darwin-amd64/pair-node24": { + "runner": null, + "image": null, + "controller": "darwin-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "darwin-arm64/kit-node18": { + "runner": null, + "image": null, + "controller": "darwin-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "darwin-arm64/pair-node22": { + "runner": null, + "image": null, + "controller": "darwin-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "darwin-arm64/pair-node24": { + "runner": null, + "image": null, + "controller": "darwin-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "windows-amd64/kit-node18": { + "runner": null, + "image": null, + "controller": "windows-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "windows-amd64/pair-node22": { + "runner": null, + "image": null, + "controller": "windows-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "windows-amd64/pair-node24": { + "runner": null, + "image": null, + "controller": "windows-amd64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "windows-arm64/kit-node18": { + "runner": null, + "image": null, + "controller": "windows-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "windows-arm64/pair-node22": { + "runner": null, + "image": null, + "controller": "windows-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + }, + "windows-arm64/pair-node24": { + "runner": null, + "image": null, + "controller": "windows-arm64", + "npm_node": null, + "shim_node": null, + "npm": null, + "go": null, + "mod_cache": null, + "observer": null, + "installer_policy": null + } + }, + "reader": "linux-amd64" +} diff --git a/.github/workflows/agentplugins-npm-publish.yml b/.github/workflows/agentplugins-npm-publish.yml index 3dd1e0f9..1919af28 100644 --- a/.github/workflows/agentplugins-npm-publish.yml +++ b/.github/workflows/agentplugins-npm-publish.yml @@ -12,6 +12,28 @@ on: required: true default: false type: boolean + producer_mode: + description: Legacy publication or isolated prepublication pair staging + required: true + type: choice + default: legacy + options: [legacy, paired-stage] + source_sha: + description: Exact paired source and workflow SHA + type: string + required: false + plugin_kit_version: + description: Paired kit version, exactly 2.0.0 + type: string + required: false + native_inputs: + description: Exact retained canonical I UTF-8 bytes including final newline, comparison only + type: string + required: false + input_artifact: + description: Exact completed I locator JSON, run_id run_attempt artifact_id artifact_sha256 + type: string + required: false permissions: contents: read @@ -22,7 +44,51 @@ concurrency: cancel-in-progress: false jobs: + dispatch_contract: + runs-on: ubuntu-24.04 + timeout-minutes: 2 + permissions: + contents: read + env: + PRODUCER_MODE: ${{ inputs.producer_mode }} + TAG: ${{ inputs.tag }} + SOURCE_SHA: ${{ inputs.source_sha }} + KIT_VERSION: ${{ inputs.plugin_kit_version }} + PUBLISH: ${{ inputs.publish }} + NATIVE_INPUTS: ${{ inputs.native_inputs }} + INPUT_ARTIFACT: ${{ inputs.input_artifact }} + steps: + - name: C1 preflight + shell: bash + run: | + set -euo pipefail + export PATH="/usr/local/bin:${PATH}" + [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch ]] + case "${PRODUCER_MODE}" in + legacy) [[ -z "${SOURCE_SHA}${KIT_VERSION}${NATIVE_INPUTS}${INPUT_ARTIFACT}" ]]; exit 0 ;; + paired-stage) [[ "${PUBLISH}" == false ]] ;; + *) exit 1 ;; + esac + [[ "${GITHUB_ACTIONS}" == true && "${GITHUB_REPOSITORY}" == 777genius/universal-agent-plugins ]] + [[ "${TAG}" =~ ^agentplugins-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ && ${#TAG} -le 47 ]] + [[ "${KIT_VERSION}" == 2.0.0 && "${TAG}" != agentplugins-v2.0.0 ]] + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0+$ ]] + [[ "${GITHUB_SHA}" == "${SOURCE_SHA}" && "${GITHUB_WORKFLOW_SHA}" == "${SOURCE_SHA}" ]] + [[ "${GITHUB_REF}" == "refs/tags/${TAG}" ]] + [[ "${GITHUB_WORKFLOW_REF}" == "777genius/universal-agent-plugins/.github/workflows/agentplugins-npm-publish.yml@refs/tags/${TAG}" ]] + positive() { [[ "$1" =~ ^[1-9][0-9]{0,15}$ && "$1" -le 9007199254740991 ]]; } + positive "${GITHUB_RUN_ID}" + positive "${GITHUB_RUN_ATTEMPT}"; [[ "${GITHUB_RUN_ATTEMPT}" -le 1000 ]] + [[ ${#NATIVE_INPUTS} -gt 0 && ${#NATIVE_INPUTS} -le 32768 && "${NATIVE_INPUTS}" == *$'\n' ]] + locator='^[[:space:]]*\{[[:space:]]*"run_id":[[:space:]]*([1-9][0-9]{0,15}),[[:space:]]*"run_attempt":[[:space:]]*([1-9][0-9]{0,3}),[[:space:]]*"artifact_id":[[:space:]]*([1-9][0-9]{0,15}),[[:space:]]*"artifact_sha256":[[:space:]]*"([0-9a-f]{64})"[[:space:]]*\}[[:space:]]*$' + [[ "${INPUT_ARTIFACT}" =~ $locator ]] + run_id="${BASH_REMATCH[1]}"; artifact_id="${BASH_REMATCH[3]}" + positive "${run_id}"; positive "${artifact_id}" + [[ "${INPUT_ARTIFACT}" =~ $locator ]] + [[ "${BASH_REMATCH[2]}" -le 1000 && ! "${BASH_REMATCH[4]}" =~ ^0+$ ]] prepare: + needs: dispatch_contract + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'legacy' && needs.dispatch_contract.result == 'success' }} name: Verify release and stage npm package runs-on: ubuntu-24.04 timeout-minutes: 20 @@ -106,7 +172,7 @@ jobs: publish: name: Publish npm package with trusted provenance - if: ${{ inputs.publish == true }} + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'legacy' && inputs.publish == true && needs.prepare.result == 'success' }} needs: prepare runs-on: ubuntu-24.04 timeout-minutes: 10 @@ -155,7 +221,7 @@ jobs: verify-public: name: Verify public npm provenance and lifecycle - if: ${{ inputs.publish == true }} + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'legacy' && inputs.publish == true && needs.publish.result == 'success' }} needs: [prepare, publish] runs-on: ubuntu-24.04 timeout-minutes: 15 @@ -233,3 +299,310 @@ jobs: run_agentplugins repair context7 --target opencode --format json | jq -e '.result == "success" and .data.succeeded == 1 and .data.failed == 0 and .data.targets[0].target == "opencode" and .data.targets[0].output.result.mutated == true' >/dev/null jq -e '.mcp.context7' "${repair_file}" > /dev/null run_agentplugins remove context7 --target opencode --external-uninstalled --format json | jq -e '.result == "success" and .data.succeeded == 1 and .data.failed == 0' >/dev/null + + paired_stage: + name: paired_stage + needs: dispatch_contract + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'paired-stage' && inputs.publish == false && needs.dispatch_contract.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + actions: read + attestations: read + env: + PRODUCER_MODE: ${{ inputs.producer_mode }} + TAG: ${{ inputs.tag }} + SOURCE_SHA: ${{ inputs.source_sha }} + KIT_VERSION: ${{ inputs.plugin_kit_version }} + PUBLISH: ${{ inputs.publish }} + NATIVE_INPUTS: ${{ inputs.native_inputs }} + INPUT_ARTIFACT: ${{ inputs.input_artifact }} + GH_TOKEN: ${{ github.token }} + outputs: + stage_sha256: ${{ steps.stage.outputs.stage_sha256 }} + stage_artifact_id: ${{ steps.upload.outputs.artifact-id }} + stage_artifact_sha256: ${{ steps.upload_evidence.outputs.artifact_sha256 }} + steps: + - name: C1 preflight + shell: bash + run: | + set -euo pipefail + export PATH="/usr/local/bin:${PATH}" + [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch ]] + case "${PRODUCER_MODE}" in + legacy) [[ -z "${SOURCE_SHA}${KIT_VERSION}${NATIVE_INPUTS}${INPUT_ARTIFACT}" ]]; exit 0 ;; + paired-stage) [[ "${PUBLISH}" == false ]] ;; + *) exit 1 ;; + esac + [[ "${GITHUB_ACTIONS}" == true && "${GITHUB_REPOSITORY}" == 777genius/universal-agent-plugins ]] + [[ "${TAG}" =~ ^agentplugins-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ && ${#TAG} -le 47 ]] + [[ "${KIT_VERSION}" == 2.0.0 && "${TAG}" != agentplugins-v2.0.0 ]] + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0+$ ]] + [[ "${GITHUB_SHA}" == "${SOURCE_SHA}" && "${GITHUB_WORKFLOW_SHA}" == "${SOURCE_SHA}" ]] + [[ "${GITHUB_REF}" == "refs/tags/${TAG}" ]] + [[ "${GITHUB_WORKFLOW_REF}" == "777genius/universal-agent-plugins/.github/workflows/agentplugins-npm-publish.yml@refs/tags/${TAG}" ]] + positive() { [[ "$1" =~ ^[1-9][0-9]{0,15}$ && "$1" -le 9007199254740991 ]]; } + positive "${GITHUB_RUN_ID}" + positive "${GITHUB_RUN_ATTEMPT}"; [[ "${GITHUB_RUN_ATTEMPT}" -le 1000 ]] + [[ ${#NATIVE_INPUTS} -gt 0 && ${#NATIVE_INPUTS} -le 32768 && "${NATIVE_INPUTS}" == *$'\n' ]] + locator='^[[:space:]]*\{[[:space:]]*"run_id":[[:space:]]*([1-9][0-9]{0,15}),[[:space:]]*"run_attempt":[[:space:]]*([1-9][0-9]{0,3}),[[:space:]]*"artifact_id":[[:space:]]*([1-9][0-9]{0,15}),[[:space:]]*"artifact_sha256":[[:space:]]*"([0-9a-f]{64})"[[:space:]]*\}[[:space:]]*$' + [[ "${INPUT_ARTIFACT}" =~ $locator ]] + run_id="${BASH_REMATCH[1]}"; artifact_id="${BASH_REMATCH[3]}" + positive "${run_id}"; positive "${artifact_id}" + [[ "${INPUT_ARTIFACT}" =~ $locator ]] + [[ "${BASH_REMATCH[2]}" -le 1000 && ! "${BASH_REMATCH[4]}" =~ ^0+$ ]] + [[ "${GITHUB_JOB}" == paired_stage ]] + - name: C1 checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ inputs.source_sha }} + fetch-depth: 0 + persist-credentials: false + - name: C1 setup + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: "22.23.2" + package-manager-cache: false + - name: C1 stage + id: stage + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const i = require('./npm/agentplugins/scripts/authoring-native-inputs'); + const s = require('./npm/agentplugins/scripts/stage-authoring-npm'); + const e = process.env, selected = {tag: e.TAG, ref: `refs/tags/${e.TAG}`, source: e.SOURCE_SHA, + versions: {agentplugins: e.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': e.KIT_VERSION}}; + const root = fs.mkdtempSync(path.join(e.RUNNER_TEMP, 'c1-workflow-')); + const scratch = path.join(root, 'scratch'); fs.mkdirSync(scratch, {mode: 0o700}); + const options = {selected, workflow_sha: e.SOURCE_SHA}; + const input_file = path.join(root, 'native-inputs.json'); + const input = Buffer.from(e.NATIVE_INPUTS, 'utf8'); assert.ok(input.length <= 32768); i.decodeInputs(input); + fs.writeFileSync(input_file, input, {flag: 'wx', mode: 0o400}); + const npm = fs.realpathSync(path.join(path.dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')); + assert.equal(path.basename(npm), 'npm-cli.js'); c.readFile(npm); + Object.assign(options, {input_file, repo: process.cwd(), workParent: scratch, node: process.execPath, npm}); + Object.assign(options, {artifact: JSON.parse(e.INPUT_ARTIFACT), output: path.join(root, 'output'), + producer: {workflow: '.github/workflows/agentplugins-npm-publish.yml', source: e.SOURCE_SHA, ref: selected.ref, + run_id: Number(e.GITHUB_RUN_ID), run_attempt: Number(e.GITHUB_RUN_ATTEMPT)}}); + const file = path.join(root, 'options.json'); fs.writeFileSync(file, c.encode(options), {flag: 'wx', mode: 0o400}); + const result = s.main(['--stage-prepublication', file]); + c.keys(result, ['root', 'record', 'subjects', 'stage_sha256'], 'stage CLI result'); + const names = ['completion.json', ...c.PRODUCTS.map(p => result.record.packs[p].file)]; + assert.equal(result.subjects.length, names.length); + assert.deepEqual(result.subjects.map(r => path.relative(result.root, r.file)).sort(), [...names].sort()); + for (const row of result.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + const payload = [...names]; + assert.equal(payload.length, 3); + const pins = (r) => payload.map(n => ({file: n, sha256: c.digest(c.readFile(path.join(r, n), 128 * 1024 * 1024))})); + const actual = pins(result.root); + const result_file = path.join(root, 'result.json'); + fs.writeFileSync(result_file, c.encode({root: result.root, subjects: result.subjects, pins: actual}), {flag: 'wx', mode: 0o400}); + const output = (key, value) => fs.appendFileSync(e.GITHUB_OUTPUT, `${key}< r.file).join('\n')); + output('payload', payload.map(n => path.join(result.root, n)).join('\n')); + output('stage_sha256', c.digest(c.readFile(path.join(result.root, 'completion.json')))); + NODE + - name: C1 upload + id: upload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: authoring-public-stage-${{ inputs.source_sha }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.stage.outputs.payload }} + if-no-files-found: error + retention-days: 7 + - name: C1 upload evidence + id: upload_evidence + env: + ARTIFACT_ID: ${{ steps.upload.outputs.artifact-id }} + ARTIFACT_SHA256: ${{ steps.upload.outputs.artifact-digest }} + COMPLETION_SHA256: ${{ steps.stage.outputs.stage_sha256 }} + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), assert = require('node:assert/strict'), e = process.env; + assert.match(e.ARTIFACT_ID, /^[1-9][0-9]{0,15}$/); + const artifact_id = Number(e.ARTIFACT_ID); assert.ok(Number.isSafeInteger(artifact_id)); + const artifact_sha256 = e.ARTIFACT_SHA256.replace(/^sha256:/, '').toLowerCase(); + assert.match(artifact_sha256, /^[0-9a-f]{64}$/); assert.ok(!/^0+$/.test(artifact_sha256)); + fs.appendFileSync(e.GITHUB_OUTPUT, `artifact_sha256=${artifact_sha256}\n`); + console.log('C1_STAGE ' + JSON.stringify({operation: 'upload', artifact_id, artifact_sha256, stage_sha256: e.COMPLETION_SHA256})); + NODE + + paired_stage_attestation: + name: paired_stage_attestation + needs: paired_stage + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'paired-stage' && inputs.publish == false && needs.paired_stage.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: npm-agentplugins + permissions: + contents: read + actions: read + id-token: write + attestations: write + env: + PRODUCER_MODE: ${{ inputs.producer_mode }} + TAG: ${{ inputs.tag }} + SOURCE_SHA: ${{ inputs.source_sha }} + KIT_VERSION: ${{ inputs.plugin_kit_version }} + PUBLISH: ${{ inputs.publish }} + NATIVE_INPUTS: ${{ inputs.native_inputs }} + INPUT_ARTIFACT: ${{ inputs.input_artifact }} + GH_TOKEN: ${{ github.token }} + STAGE_ARTIFACT_ID: ${{ needs.paired_stage.outputs.stage_artifact_id }} + STAGE_ARTIFACT_SHA256: ${{ needs.paired_stage.outputs.stage_artifact_sha256 }} + STAGE_SHA256: ${{ needs.paired_stage.outputs.stage_sha256 }} + outputs: + stage_sha256: ${{ steps.recheck.outputs.stage_sha256 }} + steps: + - name: C1 preflight + shell: bash + run: | + set -euo pipefail + export PATH="/usr/local/bin:${PATH}" + [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch ]] + case "${PRODUCER_MODE}" in + legacy) [[ -z "${SOURCE_SHA}${KIT_VERSION}${NATIVE_INPUTS}${INPUT_ARTIFACT}" ]]; exit 0 ;; + paired-stage) [[ "${PUBLISH}" == false ]] ;; + *) exit 1 ;; + esac + [[ "${GITHUB_ACTIONS}" == true && "${GITHUB_REPOSITORY}" == 777genius/universal-agent-plugins ]] + [[ "${TAG}" =~ ^agentplugins-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ && ${#TAG} -le 47 ]] + [[ "${KIT_VERSION}" == 2.0.0 && "${TAG}" != agentplugins-v2.0.0 ]] + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0+$ ]] + [[ "${GITHUB_SHA}" == "${SOURCE_SHA}" && "${GITHUB_WORKFLOW_SHA}" == "${SOURCE_SHA}" ]] + [[ "${GITHUB_REF}" == "refs/tags/${TAG}" ]] + [[ "${GITHUB_WORKFLOW_REF}" == "777genius/universal-agent-plugins/.github/workflows/agentplugins-npm-publish.yml@refs/tags/${TAG}" ]] + positive() { [[ "$1" =~ ^[1-9][0-9]{0,15}$ && "$1" -le 9007199254740991 ]]; } + positive "${GITHUB_RUN_ID}" + positive "${GITHUB_RUN_ATTEMPT}"; [[ "${GITHUB_RUN_ATTEMPT}" -le 1000 ]] + [[ ${#NATIVE_INPUTS} -gt 0 && ${#NATIVE_INPUTS} -le 32768 && "${NATIVE_INPUTS}" == *$'\n' ]] + locator='^[[:space:]]*\{[[:space:]]*"run_id":[[:space:]]*([1-9][0-9]{0,15}),[[:space:]]*"run_attempt":[[:space:]]*([1-9][0-9]{0,3}),[[:space:]]*"artifact_id":[[:space:]]*([1-9][0-9]{0,15}),[[:space:]]*"artifact_sha256":[[:space:]]*"([0-9a-f]{64})"[[:space:]]*\}[[:space:]]*$' + [[ "${INPUT_ARTIFACT}" =~ $locator ]] + run_id="${BASH_REMATCH[1]}"; artifact_id="${BASH_REMATCH[3]}" + positive "${run_id}"; positive "${artifact_id}" + [[ "${INPUT_ARTIFACT}" =~ $locator ]] + [[ "${BASH_REMATCH[2]}" -le 1000 && ! "${BASH_REMATCH[4]}" =~ ^0+$ ]] + [[ "${GITHUB_JOB}" == paired_stage_attestation ]] + positive "${STAGE_ARTIFACT_ID}" + [[ "${STAGE_ARTIFACT_SHA256}" =~ ^[0-9a-f]{64}$ && ! "${STAGE_ARTIFACT_SHA256}" =~ ^0+$ ]] + [[ "${STAGE_SHA256}" =~ ^[0-9a-f]{64}$ && ! "${STAGE_SHA256}" =~ ^0+$ ]] + - name: C1 checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ inputs.source_sha }} + fetch-depth: 0 + persist-credentials: false + - name: C1 setup + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: "22.23.2" + package-manager-cache: false + - name: C1 stage + id: stage + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const i = require('./npm/agentplugins/scripts/authoring-native-inputs'); + const s = require('./npm/agentplugins/scripts/stage-authoring-npm'); + const e = process.env, selected = {tag: e.TAG, ref: `refs/tags/${e.TAG}`, source: e.SOURCE_SHA, + versions: {agentplugins: e.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': e.KIT_VERSION}}; + const root = fs.mkdtempSync(path.join(e.RUNNER_TEMP, 'c1-workflow-')); + const scratch = path.join(root, 'scratch'); fs.mkdirSync(scratch, {mode: 0o700}); + const options = {selected, workflow_sha: e.SOURCE_SHA}; + const input_file = path.join(root, 'native-inputs.json'); + const input = Buffer.from(e.NATIVE_INPUTS, 'utf8'); assert.ok(input.length <= 32768); i.decodeInputs(input); + fs.writeFileSync(input_file, input, {flag: 'wx', mode: 0o400}); + const npm = fs.realpathSync(path.join(path.dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')); + assert.equal(path.basename(npm), 'npm-cli.js'); c.readFile(npm); + Object.assign(options, {input_file, repo: process.cwd(), workParent: scratch, node: process.execPath, npm}); + Object.assign(options, {artifact: {run_id: Number(e.GITHUB_RUN_ID), run_attempt: Number(e.GITHUB_RUN_ATTEMPT), + artifact_id: Number(e.STAGE_ARTIFACT_ID), artifact_sha256: e.STAGE_ARTIFACT_SHA256}, stage_sha256: e.STAGE_SHA256}); + const file = path.join(root, 'options.json'); fs.writeFileSync(file, c.encode(options), {flag: 'wx', mode: 0o400}); + const result = s.main(['--validate-unsigned-stage', file]); + c.keys(result, ['root', 'record', 'subjects'], 'stage CLI result'); + const names = ['completion.json', ...c.PRODUCTS.map(p => result.record.packs[p].file)]; + assert.equal(result.subjects.length, names.length); + assert.deepEqual(result.subjects.map(r => path.relative(result.root, r.file)).sort(), [...names].sort()); + for (const row of result.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + const payload = [...names]; + assert.equal(payload.length, 3); + const pins = (r) => payload.map(n => ({file: n, sha256: c.digest(c.readFile(path.join(r, n), 128 * 1024 * 1024))})); + const actual = pins(result.root); + const result_file = path.join(root, 'result.json'); + fs.writeFileSync(result_file, c.encode({root: result.root, subjects: result.subjects, pins: actual}), {flag: 'wx', mode: 0o400}); + const output = (key, value) => fs.appendFileSync(e.GITHUB_OUTPUT, `${key}< r.file).join('\n')); + output('payload', payload.map(n => path.join(result.root, n)).join('\n')); + output('stage_sha256', c.digest(c.readFile(path.join(result.root, 'completion.json')))); + NODE + - name: C1 attest + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 + with: + subject-path: ${{ steps.stage.outputs.subjects }} + - name: C1 recheck + id: recheck + env: + PREVIOUS: ${{ steps.stage.outputs.result_file }} + SIGNING_ROOT: ${{ steps.stage.outputs.root }} + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const i = require('./npm/agentplugins/scripts/authoring-native-inputs'); + const s = require('./npm/agentplugins/scripts/stage-authoring-npm'); + const e = process.env, selected = {tag: e.TAG, ref: `refs/tags/${e.TAG}`, source: e.SOURCE_SHA, + versions: {agentplugins: e.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': e.KIT_VERSION}}; + const root = fs.mkdtempSync(path.join(e.RUNNER_TEMP, 'c1-workflow-')); + const scratch = path.join(root, 'scratch'); fs.mkdirSync(scratch, {mode: 0o700}); + const options = {selected, workflow_sha: e.SOURCE_SHA}; + const input_file = path.join(root, 'native-inputs.json'); + const input = Buffer.from(e.NATIVE_INPUTS, 'utf8'); assert.ok(input.length <= 32768); i.decodeInputs(input); + fs.writeFileSync(input_file, input, {flag: 'wx', mode: 0o400}); + const npm = fs.realpathSync(path.join(path.dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')); + assert.equal(path.basename(npm), 'npm-cli.js'); c.readFile(npm); + Object.assign(options, {input_file, repo: process.cwd(), workParent: scratch, node: process.execPath, npm}); + Object.assign(options, {artifact: {run_id: Number(e.GITHUB_RUN_ID), run_attempt: Number(e.GITHUB_RUN_ATTEMPT), + artifact_id: Number(e.STAGE_ARTIFACT_ID), artifact_sha256: e.STAGE_ARTIFACT_SHA256}, stage_sha256: e.STAGE_SHA256}); + const file = path.join(root, 'options.json'); fs.writeFileSync(file, c.encode(options), {flag: 'wx', mode: 0o400}); + const result = s.main(['--validate-unsigned-stage', file]); + c.keys(result, ['root', 'record', 'subjects'], 'stage CLI result'); + const names = ['completion.json', ...c.PRODUCTS.map(p => result.record.packs[p].file)]; + assert.equal(result.subjects.length, names.length); + assert.deepEqual(result.subjects.map(r => path.relative(result.root, r.file)).sort(), [...names].sort()); + for (const row of result.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + const payload = [...names]; + assert.equal(payload.length, 3); + const pins = (r) => payload.map(n => ({file: n, sha256: c.digest(c.readFile(path.join(r, n), 128 * 1024 * 1024))})); + const actual = pins(result.root); + const previous = JSON.parse(c.readFile(e.PREVIOUS, 1024 * 1024)); + c.keys(previous, ['root', 'subjects', 'pins'], 'original signing comparison'); + assert.equal(previous.root, e.SIGNING_ROOT); + assert.deepEqual([...previous.subjects].sort((a,b) => a.file.localeCompare(b.file)), names.map(n => ({file: path.join(e.SIGNING_ROOT, n), sha256: actual.find(row => row.file === n).sha256})).sort((a,b) => a.file.localeCompare(b.file))); + assert.deepEqual(previous.pins, actual); assert.deepEqual(pins(previous.root), actual); + for (const row of previous.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + // Original signing root is retained; independently reacquired bytes never replace it. + result.root = previous.root; result.subjects = previous.subjects; + const result_file = path.join(root, 'result.json'); + fs.writeFileSync(result_file, c.encode({root: result.root, subjects: result.subjects, pins: actual}), {flag: 'wx', mode: 0o400}); + const output = (key, value) => fs.appendFileSync(e.GITHUB_OUTPUT, `${key}< r.file).join('\n')); + output('payload', payload.map(n => path.join(result.root, n)).join('\n')); + output('stage_sha256', c.digest(c.readFile(path.join(result.root, 'completion.json')))); + NODE diff --git a/.github/workflows/agentplugins-release.yml b/.github/workflows/agentplugins-release.yml index 2b6a8799..6b5c4af6 100644 --- a/.github/workflows/agentplugins-release.yml +++ b/.github/workflows/agentplugins-release.yml @@ -15,6 +15,7 @@ on: - binary-only - paired-preparation - paired-promotion + - paired-input-provenance source_sha: description: Exact source and workflow SHA (required for paired preparation) required: false @@ -22,16 +23,16 @@ on: description: Explicit plugin-kit version (paired first cut requires 2.0.0) required: false preparation_run: - description: Exact completed preparation run ID (promotion only) + description: Exact completed preparation run ID (promotion or input provenance) required: false preparation_attempt: - description: Exact preparation run attempt (promotion only) + description: Exact preparation run attempt (promotion or input provenance) required: false preparation_artifact: - description: Exact preparation artifact ID (promotion only) + description: Exact preparation artifact ID (promotion or input provenance) required: false preparation_digest: - description: Independently selected preparation ZIP SHA256 (promotion only) + description: Independently selected preparation ZIP SHA256 (promotion or input provenance) required: false promotion_operation: description: Promote exact drafts or reconcile interrupted drafts/partial publication @@ -53,6 +54,47 @@ concurrency: cancel-in-progress: false jobs: + dispatch_contract: + runs-on: ubuntu-24.04 + timeout-minutes: 2 + permissions: + contents: read + env: + PRODUCER_MODE: ${{ inputs.producer_mode }} + TAG: ${{ inputs.tag }} + SOURCE_SHA: ${{ inputs.source_sha }} + KIT_VERSION: ${{ inputs.plugin_kit_version }} + PREPARATION_RUN: ${{ inputs.preparation_run }} + PREPARATION_ATTEMPT: ${{ inputs.preparation_attempt }} + PREPARATION_ARTIFACT: ${{ inputs.preparation_artifact }} + PREPARATION_DIGEST: ${{ inputs.preparation_digest }} + PROMOTION_OPERATION: ${{ inputs.promotion_operation }} + PROMOTION_RECORD: ${{ inputs.promotion_record }} + steps: + - name: C1 preflight + shell: bash + run: | + set -euo pipefail + export PATH="/usr/local/bin:${PATH}" + [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch ]] + case "${PRODUCER_MODE}" in + binary-only|paired-preparation|paired-promotion) exit 0 ;; + paired-input-provenance) [[ -z "${PROMOTION_RECORD}" && "${PROMOTION_OPERATION}" == promote ]] ;; + *) exit 1 ;; + esac + [[ "${GITHUB_ACTIONS}" == true && "${GITHUB_REPOSITORY}" == 777genius/universal-agent-plugins ]] + [[ "${TAG}" =~ ^agentplugins-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ && ${#TAG} -le 47 ]] + [[ "${KIT_VERSION}" == 2.0.0 && "${TAG}" != agentplugins-v2.0.0 ]] + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0+$ ]] + [[ "${GITHUB_SHA}" == "${SOURCE_SHA}" && "${GITHUB_WORKFLOW_SHA}" == "${SOURCE_SHA}" ]] + [[ "${GITHUB_REF}" == "refs/tags/${TAG}" ]] + [[ "${GITHUB_WORKFLOW_REF}" == "777genius/universal-agent-plugins/.github/workflows/agentplugins-release.yml@refs/tags/${TAG}" ]] + positive() { [[ "$1" =~ ^[1-9][0-9]{0,15}$ && "$1" -le 9007199254740991 ]]; } + positive "${GITHUB_RUN_ID}" + positive "${GITHUB_RUN_ATTEMPT}"; [[ "${GITHUB_RUN_ATTEMPT}" -le 1000 ]] + positive "${PREPARATION_RUN}"; positive "${PREPARATION_ARTIFACT}" + positive "${PREPARATION_ATTEMPT}"; [[ "${PREPARATION_ATTEMPT}" -le 1000 ]] + [[ "${PREPARATION_DIGEST}" =~ ^[0-9a-f]{64}$ && ! "${PREPARATION_DIGEST}" =~ ^0+$ ]] validate: if: ${{ inputs.producer_mode == 'binary-only' }} runs-on: ubuntu-latest @@ -542,6 +584,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + actions: read steps: - name: Validate promotion identity before checkout env: @@ -568,8 +611,13 @@ jobs: with: node-version: 22.21.1 package-manager-cache: false - - name: Reject missing native terminal contracts before protected effects + - name: Admit exact native evidence read-only before protected effects env: + GH_TOKEN: ${{ github.token }} + PREPARATION_RUN: ${{ inputs.preparation_run }} + PREPARATION_ATTEMPT: ${{ inputs.preparation_attempt }} + PREPARATION_ARTIFACT: ${{ inputs.preparation_artifact }} + PREPARATION_DIGEST: ${{ inputs.preparation_digest }} PROMOTION_RECORD: ${{ inputs.promotion_record }} TAG: ${{ inputs.tag }} WORKFLOW_REF: ${{ github.ref }} @@ -583,7 +631,15 @@ jobs: const selected = { tag: process.env.TAG, ref: process.env.WORKFLOW_REF, source: process.env.WORKFLOW_SHA, versions: { agentplugins: process.env.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': process.env.KIT_VERSION } }; const record = p.validateSelection(Buffer.from(input), selected); - p.requireNativeContracts(record.qualification?.lanes || []); + p.checkNativeContracts(record); // Syntax only; provider admission still follows. + const fs = require('node:fs'), path = require('node:path'); + const scratch = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP, 'read-only-admission-')); + p.admitNativeEvidence(record, { run_id: Number(process.env.PREPARATION_RUN), + run_attempt: Number(process.env.PREPARATION_ATTEMPT), artifact_id: Number(process.env.PREPARATION_ARTIFACT), + artifact_sha256: process.env.PREPARATION_DIGEST }, scratch); + // No public producer is registered. Twelve native receipts remain + // insufficient; this throws before the protected job can be scheduled. + p.requireNativeContracts(record.qualification.lanes); NODE paired-sign-and-promote: @@ -606,8 +662,8 @@ jobs: with: node-version: 22.21.1 package-manager-cache: false - # Admission currently always rejects: real terminal producer integration is - # required before any artifact download, attestation or release mutation. + # Public-packed admission remains closed. Reacquire and replay independently + # in this job; a prior read-only job is never a reusable authorization token. - name: Acquire exact frozen preparation after native admission env: GH_TOKEN: ${{ github.token }} @@ -628,27 +684,20 @@ jobs: const p = require('./npm/agentplugins/scripts/authoring-promotion'); const selected = { tag: process.env.TAG, ref: process.env.WORKFLOW_REF, source: process.env.WORKFLOW_SHA, versions: { agentplugins: process.env.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': process.env.KIT_VERSION } }; - const record = p.admitRecord(Buffer.from(process.env.PROMOTION_RECORD), selected); + const record = p.validateSelection(Buffer.from(process.env.PROMOTION_RECORD), selected); + p.checkNativeContracts(record); // Reject unsupported identifiers before scratch/provider effects. if (record.identity.commit !== process.env.SOURCE_SHA) throw Error('exact promotion source required'); const scratch = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP, 'paired-promotion-')); const preparation = { run_id: Number(process.env.PREPARATION_RUN), run_attempt: Number(process.env.PREPARATION_ATTEMPT), artifact_id: Number(process.env.PREPARATION_ARTIFACT), artifact_sha256: process.env.PREPARATION_DIGEST }; - p.acquireArtifact(preparation, p.WORKFLOW, record.identity.commit, scratch); + const acquired = p.acquirePreparation(preparation, record, scratch); const recordFile = path.join(scratch, 'authoring-promotion.json'); fs.writeFileSync(recordFile, p.encodeRecord(record), { flag: 'wx', mode: 0o400 }); fs.writeFileSync(path.join(scratch, 'options.json'), JSON.stringify({ record: recordFile, - root: path.join(scratch, 'frozen'), scratch: path.join(scratch, 'provider'), workflow_sha: process.env.SOURCE_SHA, preparation, selected }), { flag: 'wx' }); + root: acquired.root, scratch: path.join(scratch, 'provider'), workflow_sha: process.env.SOURCE_SHA, preparation, selected }), { flag: 'wx' }); fs.mkdirSync(path.join(scratch, 'provider'), { mode: 0o700 }); fs.appendFileSync(process.env.GITHUB_ENV, `PROMOTION_ROOT=${scratch}\n`); NODE - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ inputs.preparation_artifact }} - run-id: ${{ inputs.preparation_run }} - github-token: ${{ github.token }} - repository: 777genius/universal-agent-plugins - merge-multiple: true - path: ${{ env.PROMOTION_ROOT }}/frozen - name: Recheck admission and exact subject closure before signing id: subjects env: @@ -678,3 +727,281 @@ jobs: run: | case "${PROMOTION_OPERATION}" in promote|reconcile) ;; *) exit 1 ;; esac node npm/agentplugins/scripts/authoring-promotion.js "${PROMOTION_OPERATION}" "${PROMOTION_ROOT}/options.json" + + paired_input_admission: + name: paired_input_admission + needs: dispatch_contract + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'paired-input-provenance' && needs.dispatch_contract.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + actions: read + env: + PRODUCER_MODE: ${{ inputs.producer_mode }} + TAG: ${{ inputs.tag }} + SOURCE_SHA: ${{ inputs.source_sha }} + KIT_VERSION: ${{ inputs.plugin_kit_version }} + PREPARATION_RUN: ${{ inputs.preparation_run }} + PREPARATION_ATTEMPT: ${{ inputs.preparation_attempt }} + PREPARATION_ARTIFACT: ${{ inputs.preparation_artifact }} + PREPARATION_DIGEST: ${{ inputs.preparation_digest }} + PROMOTION_OPERATION: ${{ inputs.promotion_operation }} + PROMOTION_RECORD: ${{ inputs.promotion_record }} + GH_TOKEN: ${{ github.token }} + outputs: + input_sha256: ${{ steps.stage.outputs.input_sha256 }} + steps: + - name: C1 preflight + shell: bash + run: | + set -euo pipefail + export PATH="/usr/local/bin:${PATH}" + [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch ]] + case "${PRODUCER_MODE}" in + binary-only|paired-preparation|paired-promotion) exit 0 ;; + paired-input-provenance) [[ -z "${PROMOTION_RECORD}" && "${PROMOTION_OPERATION}" == promote ]] ;; + *) exit 1 ;; + esac + [[ "${GITHUB_ACTIONS}" == true && "${GITHUB_REPOSITORY}" == 777genius/universal-agent-plugins ]] + [[ "${TAG}" =~ ^agentplugins-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ && ${#TAG} -le 47 ]] + [[ "${KIT_VERSION}" == 2.0.0 && "${TAG}" != agentplugins-v2.0.0 ]] + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0+$ ]] + [[ "${GITHUB_SHA}" == "${SOURCE_SHA}" && "${GITHUB_WORKFLOW_SHA}" == "${SOURCE_SHA}" ]] + [[ "${GITHUB_REF}" == "refs/tags/${TAG}" ]] + [[ "${GITHUB_WORKFLOW_REF}" == "777genius/universal-agent-plugins/.github/workflows/agentplugins-release.yml@refs/tags/${TAG}" ]] + positive() { [[ "$1" =~ ^[1-9][0-9]{0,15}$ && "$1" -le 9007199254740991 ]]; } + positive "${GITHUB_RUN_ID}" + positive "${GITHUB_RUN_ATTEMPT}"; [[ "${GITHUB_RUN_ATTEMPT}" -le 1000 ]] + positive "${PREPARATION_RUN}"; positive "${PREPARATION_ARTIFACT}" + positive "${PREPARATION_ATTEMPT}"; [[ "${PREPARATION_ATTEMPT}" -le 1000 ]] + [[ "${PREPARATION_DIGEST}" =~ ^[0-9a-f]{64}$ && ! "${PREPARATION_DIGEST}" =~ ^0+$ ]] + [[ "${GITHUB_JOB}" == paired_input_admission ]] + - name: C1 checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ inputs.source_sha }} + fetch-depth: 0 + persist-credentials: false + - name: C1 setup + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: "22.23.2" + package-manager-cache: false + - name: C1 stage + id: stage + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const i = require('./npm/agentplugins/scripts/authoring-native-inputs'); + const s = require('./npm/agentplugins/scripts/stage-authoring-npm'); + const e = process.env, selected = {tag: e.TAG, ref: `refs/tags/${e.TAG}`, source: e.SOURCE_SHA, + versions: {agentplugins: e.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': e.KIT_VERSION}}; + const root = fs.mkdtempSync(path.join(e.RUNNER_TEMP, 'c1-workflow-')); + const scratch = path.join(root, 'scratch'); fs.mkdirSync(scratch, {mode: 0o700}); + const options = {selected, workflow_sha: e.SOURCE_SHA}; + Object.assign(options, {preparation: {run_id: Number(e.PREPARATION_RUN), run_attempt: Number(e.PREPARATION_ATTEMPT), + artifact_id: Number(e.PREPARATION_ARTIFACT), artifact_sha256: e.PREPARATION_DIGEST}, repo: process.cwd(), scratch}); + const file = path.join(root, 'options.json'); fs.writeFileSync(file, c.encode(options), {flag: 'wx', mode: 0o400}); + const result = i.main(['--produce-inputs', file]); + c.keys(result, ['root', 'input', 'subjects'], 'input CLI result'); + const names = ['candidate/candidate.json', 'pair-prepared.json', ...c.PRODUCTS.flatMap(p => [...c.TARGETS.map(t => `${p}/${result.input.products[p].assets[t].file}`), `${p}/release-manifest.json`, `${p}/checksums.txt`]), 'native-inputs.json']; + assert.equal(result.subjects.length, names.length); + assert.deepEqual(result.subjects.map(r => path.relative(result.root, r.file)).sort(), [...names].sort()); + for (const row of result.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + const payload = [...names, 'preparation-run.json', 'candidate-identity.json']; + assert.equal(payload.length, 21); + const pins = (r) => payload.map(n => ({file: n, sha256: c.digest(c.readFile(path.join(r, n), 128 * 1024 * 1024))})); + const actual = pins(result.root); + const result_file = path.join(root, 'result.json'); + fs.writeFileSync(result_file, c.encode({root: result.root, subjects: result.subjects, pins: actual}), {flag: 'wx', mode: 0o400}); + const output = (key, value) => fs.appendFileSync(e.GITHUB_OUTPUT, `${key}< r.file).join('\n')); + output('payload', payload.map(n => path.join(result.root, n)).join('\n')); + output('input_sha256', c.digest(i.encodeInputs(result.input))); + NODE + + paired_input_attestation: + name: paired_input_attestation + needs: paired_input_admission + if: ${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'paired-input-provenance' && needs.paired_input_admission.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + environment: agentplugins-release + permissions: + contents: read + actions: read + id-token: write + attestations: write + env: + PRODUCER_MODE: ${{ inputs.producer_mode }} + TAG: ${{ inputs.tag }} + SOURCE_SHA: ${{ inputs.source_sha }} + KIT_VERSION: ${{ inputs.plugin_kit_version }} + PREPARATION_RUN: ${{ inputs.preparation_run }} + PREPARATION_ATTEMPT: ${{ inputs.preparation_attempt }} + PREPARATION_ARTIFACT: ${{ inputs.preparation_artifact }} + PREPARATION_DIGEST: ${{ inputs.preparation_digest }} + PROMOTION_OPERATION: ${{ inputs.promotion_operation }} + PROMOTION_RECORD: ${{ inputs.promotion_record }} + GH_TOKEN: ${{ github.token }} + outputs: + input_sha256: ${{ steps.recheck.outputs.input_sha256 }} + input_artifact_id: ${{ steps.upload.outputs.artifact-id }} + input_artifact_sha256: ${{ steps.upload_evidence.outputs.artifact_sha256 }} + steps: + - name: C1 preflight + shell: bash + run: | + set -euo pipefail + export PATH="/usr/local/bin:${PATH}" + [[ "${GITHUB_EVENT_NAME}" == workflow_dispatch ]] + case "${PRODUCER_MODE}" in + binary-only|paired-preparation|paired-promotion) exit 0 ;; + paired-input-provenance) [[ -z "${PROMOTION_RECORD}" && "${PROMOTION_OPERATION}" == promote ]] ;; + *) exit 1 ;; + esac + [[ "${GITHUB_ACTIONS}" == true && "${GITHUB_REPOSITORY}" == 777genius/universal-agent-plugins ]] + [[ "${TAG}" =~ ^agentplugins-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ && ${#TAG} -le 47 ]] + [[ "${KIT_VERSION}" == 2.0.0 && "${TAG}" != agentplugins-v2.0.0 ]] + [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0+$ ]] + [[ "${GITHUB_SHA}" == "${SOURCE_SHA}" && "${GITHUB_WORKFLOW_SHA}" == "${SOURCE_SHA}" ]] + [[ "${GITHUB_REF}" == "refs/tags/${TAG}" ]] + [[ "${GITHUB_WORKFLOW_REF}" == "777genius/universal-agent-plugins/.github/workflows/agentplugins-release.yml@refs/tags/${TAG}" ]] + positive() { [[ "$1" =~ ^[1-9][0-9]{0,15}$ && "$1" -le 9007199254740991 ]]; } + positive "${GITHUB_RUN_ID}" + positive "${GITHUB_RUN_ATTEMPT}"; [[ "${GITHUB_RUN_ATTEMPT}" -le 1000 ]] + positive "${PREPARATION_RUN}"; positive "${PREPARATION_ARTIFACT}" + positive "${PREPARATION_ATTEMPT}"; [[ "${PREPARATION_ATTEMPT}" -le 1000 ]] + [[ "${PREPARATION_DIGEST}" =~ ^[0-9a-f]{64}$ && ! "${PREPARATION_DIGEST}" =~ ^0+$ ]] + [[ "${GITHUB_JOB}" == paired_input_attestation ]] + - name: C1 checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ inputs.source_sha }} + fetch-depth: 0 + persist-credentials: false + - name: C1 setup + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: "22.23.2" + package-manager-cache: false + - name: C1 stage + id: stage + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const i = require('./npm/agentplugins/scripts/authoring-native-inputs'); + const s = require('./npm/agentplugins/scripts/stage-authoring-npm'); + const e = process.env, selected = {tag: e.TAG, ref: `refs/tags/${e.TAG}`, source: e.SOURCE_SHA, + versions: {agentplugins: e.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': e.KIT_VERSION}}; + const root = fs.mkdtempSync(path.join(e.RUNNER_TEMP, 'c1-workflow-')); + const scratch = path.join(root, 'scratch'); fs.mkdirSync(scratch, {mode: 0o700}); + const options = {selected, workflow_sha: e.SOURCE_SHA}; + Object.assign(options, {preparation: {run_id: Number(e.PREPARATION_RUN), run_attempt: Number(e.PREPARATION_ATTEMPT), + artifact_id: Number(e.PREPARATION_ARTIFACT), artifact_sha256: e.PREPARATION_DIGEST}, repo: process.cwd(), scratch}); + const file = path.join(root, 'options.json'); fs.writeFileSync(file, c.encode(options), {flag: 'wx', mode: 0o400}); + const result = i.main(['--produce-inputs', file]); + c.keys(result, ['root', 'input', 'subjects'], 'input CLI result'); + const names = ['candidate/candidate.json', 'pair-prepared.json', ...c.PRODUCTS.flatMap(p => [...c.TARGETS.map(t => `${p}/${result.input.products[p].assets[t].file}`), `${p}/release-manifest.json`, `${p}/checksums.txt`]), 'native-inputs.json']; + assert.equal(result.subjects.length, names.length); + assert.deepEqual(result.subjects.map(r => path.relative(result.root, r.file)).sort(), [...names].sort()); + for (const row of result.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + const payload = [...names, 'preparation-run.json', 'candidate-identity.json']; + assert.equal(payload.length, 21); + const pins = (r) => payload.map(n => ({file: n, sha256: c.digest(c.readFile(path.join(r, n), 128 * 1024 * 1024))})); + const actual = pins(result.root); + const result_file = path.join(root, 'result.json'); + fs.writeFileSync(result_file, c.encode({root: result.root, subjects: result.subjects, pins: actual}), {flag: 'wx', mode: 0o400}); + const output = (key, value) => fs.appendFileSync(e.GITHUB_OUTPUT, `${key}< r.file).join('\n')); + output('payload', payload.map(n => path.join(result.root, n)).join('\n')); + output('input_sha256', c.digest(i.encodeInputs(result.input))); + NODE + - name: C1 attest + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 + with: + subject-path: ${{ steps.stage.outputs.subjects }} + - name: C1 recheck + id: recheck + env: + PREVIOUS: ${{ steps.stage.outputs.result_file }} + SIGNING_ROOT: ${{ steps.stage.outputs.root }} + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const i = require('./npm/agentplugins/scripts/authoring-native-inputs'); + const s = require('./npm/agentplugins/scripts/stage-authoring-npm'); + const e = process.env, selected = {tag: e.TAG, ref: `refs/tags/${e.TAG}`, source: e.SOURCE_SHA, + versions: {agentplugins: e.TAG.slice('agentplugins-v'.length), 'plugin-kit-ai': e.KIT_VERSION}}; + const root = fs.mkdtempSync(path.join(e.RUNNER_TEMP, 'c1-workflow-')); + const scratch = path.join(root, 'scratch'); fs.mkdirSync(scratch, {mode: 0o700}); + const options = {selected, workflow_sha: e.SOURCE_SHA}; + Object.assign(options, {preparation: {run_id: Number(e.PREPARATION_RUN), run_attempt: Number(e.PREPARATION_ATTEMPT), + artifact_id: Number(e.PREPARATION_ARTIFACT), artifact_sha256: e.PREPARATION_DIGEST}, repo: process.cwd(), scratch}); + const file = path.join(root, 'options.json'); fs.writeFileSync(file, c.encode(options), {flag: 'wx', mode: 0o400}); + const result = i.main(['--produce-inputs', file]); + c.keys(result, ['root', 'input', 'subjects'], 'input CLI result'); + const names = ['candidate/candidate.json', 'pair-prepared.json', ...c.PRODUCTS.flatMap(p => [...c.TARGETS.map(t => `${p}/${result.input.products[p].assets[t].file}`), `${p}/release-manifest.json`, `${p}/checksums.txt`]), 'native-inputs.json']; + assert.equal(result.subjects.length, names.length); + assert.deepEqual(result.subjects.map(r => path.relative(result.root, r.file)).sort(), [...names].sort()); + for (const row of result.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + const payload = [...names, 'preparation-run.json', 'candidate-identity.json']; + assert.equal(payload.length, 21); + const pins = (r) => payload.map(n => ({file: n, sha256: c.digest(c.readFile(path.join(r, n), 128 * 1024 * 1024))})); + const actual = pins(result.root); + const previous = JSON.parse(c.readFile(e.PREVIOUS, 1024 * 1024)); + c.keys(previous, ['root', 'subjects', 'pins'], 'original signing comparison'); + assert.equal(previous.root, e.SIGNING_ROOT); + assert.deepEqual([...previous.subjects].sort((a,b) => a.file.localeCompare(b.file)), names.map(n => ({file: path.join(e.SIGNING_ROOT, n), sha256: actual.find(row => row.file === n).sha256})).sort((a,b) => a.file.localeCompare(b.file))); + assert.deepEqual(previous.pins, actual); assert.deepEqual(pins(previous.root), actual); + for (const row of previous.subjects) assert.equal(c.digest(c.readFile(row.file, 128 * 1024 * 1024)), row.sha256); + // Original signing root is retained; independently reacquired bytes never replace it. + result.root = previous.root; result.subjects = previous.subjects; + const result_file = path.join(root, 'result.json'); + fs.writeFileSync(result_file, c.encode({root: result.root, subjects: result.subjects, pins: actual}), {flag: 'wx', mode: 0o400}); + const output = (key, value) => fs.appendFileSync(e.GITHUB_OUTPUT, `${key}< r.file).join('\n')); + output('payload', payload.map(n => path.join(result.root, n)).join('\n')); + output('input_sha256', c.digest(i.encodeInputs(result.input))); + NODE + - name: C1 upload + id: upload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: authoring-input-provenance-${{ inputs.source_sha }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.recheck.outputs.payload }} + if-no-files-found: error + retention-days: 7 + - name: C1 upload evidence + id: upload_evidence + env: + ARTIFACT_ID: ${{ steps.upload.outputs.artifact-id }} + ARTIFACT_SHA256: ${{ steps.upload.outputs.artifact-digest }} + COMPLETION_SHA256: ${{ steps.recheck.outputs.input_sha256 }} + shell: bash + run: | + set -euo pipefail + node <<'NODE' + const fs = require('node:fs'), assert = require('node:assert/strict'), e = process.env; + assert.match(e.ARTIFACT_ID, /^[1-9][0-9]{0,15}$/); + const artifact_id = Number(e.ARTIFACT_ID); assert.ok(Number.isSafeInteger(artifact_id)); + const artifact_sha256 = e.ARTIFACT_SHA256.replace(/^sha256:/, '').toLowerCase(); + assert.match(artifact_sha256, /^[0-9a-f]{64}$/); assert.ok(!/^0+$/.test(artifact_sha256)); + fs.appendFileSync(e.GITHUB_OUTPUT, `artifact_sha256=${artifact_sha256}\n`); + NODE diff --git a/.github/workflows/authoring-frozen-native.yml b/.github/workflows/authoring-frozen-native.yml index 5c350217..69ee9baf 100644 --- a/.github/workflows/authoring-frozen-native.yml +++ b/.github/workflows/authoring-frozen-native.yml @@ -1,6 +1,6 @@ name: Frozen Authoring Native Observations -# N1 only. No automatic event, source build, promotion, signing or publication. +# Frozen inputs only. No automatic event, source build, signing or publication. # Local closed receipts are not authenticated provider admission. Whole-OS # observation and real installer services remain required execution prerequisites. on: @@ -42,25 +42,27 @@ on: description: Exact two-product manifest and checksum pins as bounded JSON required: true type: string - host_go_sha256: - description: Independent Linux amd64 Go 1.25.13 executable digest + host_contract: + description: Exact JSON target, agent_version and go_sha256; six targets, excluded execution stays pending required: true type: string permissions: contents: read concurrency: - group: frozen-native-${{ inputs.source_sha }}-${{ inputs.artifact_id }} + group: frozen-native-${{ inputs.source_sha }}-${{ inputs.artifact_id }}-${{ fromJSON(inputs.host_contract).target }} cancel-in-progress: false jobs: linux-amd64: - if: ${{ github.event_name == 'workflow_dispatch' }} - runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' && (fromJSON(inputs.host_contract).target == 'linux-amd64' || fromJSON(inputs.host_contract).target == 'linux-arm64') }} + runs-on: ${{ fromJSON(inputs.host_contract).target == 'linux-arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} timeout-minutes: 25 permissions: contents: read actions: read env: PATH: /usr/local/bin:/usr/bin:/bin + NATIVE_TARGET: ${{ fromJSON(inputs.host_contract).target }} + AGENT_VERSION: ${{ fromJSON(inputs.host_contract).agent_version }} SOURCE_SHA: ${{ inputs.source_sha }} WORKFLOW_SHA: ${{ github.sha }} PREPARATION_RUN: ${{ inputs.preparation_run }} @@ -71,7 +73,8 @@ jobs: PAIR_SHA256: ${{ inputs.pair_sha256 }} CANDIDATE_SHA256: ${{ inputs.candidate_sha256 }} PROJECTION_PINS: ${{ inputs.projection_pins }} - HOST_GO_SHA256: ${{ inputs.host_go_sha256 }} + HOST_GO_SHA256: ${{ fromJSON(inputs.host_contract).go_sha256 }} + HOST_CONTRACT: ${{ inputs.host_contract }} steps: - name: Validate exact read-only selection before acquisition shell: bash @@ -80,6 +83,8 @@ jobs: test "${GITHUB_REPOSITORY}" = "777genius/universal-agent-plugins" [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ && ! "${SOURCE_SHA}" =~ ^0{40}$ ]] test "${SOURCE_SHA}" = "${WORKFLOW_SHA}" + [[ "${AGENT_VERSION}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] + case "${NATIVE_TARGET}" in linux-amd64|linux-arm64) ;; *) exit 1 ;; esac for value in "${PREPARATION_RUN}" "${PREPARATION_ATTEMPT}" "${ARTIFACT_ID}"; do [[ "${value}" =~ ^[1-9][0-9]{0,14}$ ]] done @@ -98,31 +103,44 @@ jobs: with: node-version: 22.21.1 package-manager-cache: false + - name: Validate the closed selected host contract + run: | + node <<'NODE' + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const text = process.env.HOST_CONTRACT; + if (Buffer.byteLength(text) > 512) throw Error('bounded host contract required'); + const value = JSON.parse(text); + c.keys(value, ['target', 'agent_version', 'go_sha256'], 'host contract'); + if (!c.TARGETS.includes(value.target) || !['linux-amd64', 'linux-arm64'].includes(value.target)) + throw Error('NATIVE_EXECUTION_PENDING'); + if (value.target !== process.env.NATIVE_TARGET || value.agent_version !== process.env.AGENT_VERSION || + value.go_sha256 !== process.env.HOST_GO_SHA256) throw Error('host contract binding'); + NODE - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: 1.25.13 cache: false - - name: Inspect exact preparation attempt and artifact metadata + - name: Acquire and extract the same checked preparation ZIP env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); const p = require('./npm/agentplugins/scripts/authoring-promotion'); - // Read-only consistency check, not accepted cryptographic custody. - p.inspectArtifact({ run_id: Number(process.env.PREPARATION_RUN), + const c = require('./npm/agentplugins/scripts/dual-authoring-candidate'); + const scratch = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP, 'native-acquisition-')); + const pin = { run_id: Number(process.env.PREPARATION_RUN), run_attempt: Number(process.env.PREPARATION_ATTEMPT), - artifact_id: Number(process.env.ARTIFACT_ID), artifact_sha256: process.env.ARTIFACT_SHA256 }, - '.github/workflows/agentplugins-release.yml', process.env.SOURCE_SHA, process.env.RUNNER_TEMP); + artifact_id: Number(process.env.ARTIFACT_ID), artifact_sha256: process.env.ARTIFACT_SHA256 }; + const file = p.acquireArtifact(pin, p.WORKFLOW, process.env.SOURCE_SHA, scratch); + const versions = { agentplugins: process.env.AGENT_VERSION, 'plugin-kit-ai': '2.0.0' }; + const files = ['preparation-run.json', 'candidate-identity.json', 'candidate/candidate.json', 'pair-prepared.json', + ...c.PRODUCTS.flatMap(product => [...c.TARGETS.map(target => `${product}/${c.assetName(product, versions[product], target)}`), + `${product}/checksums.txt`, `${product}/release-manifest.json`])]; + p.extractArtifact(file, pin, 'preparation', files, path.join(process.env.RUNNER_TEMP, 'frozen-inputs'), scratch); NODE - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - artifact-ids: ${{ inputs.artifact_id }} - run-id: ${{ inputs.preparation_run }} - github-token: ${{ github.token }} - repository: 777genius/universal-agent-plugins - merge-multiple: true - path: ${{ runner.temp }}/frozen-inputs - name: Inspect host Go and execute the fixed pair journey shell: bash run: | @@ -136,12 +154,12 @@ jobs: const pair = c.readFile(path.join(root, 'pair-prepared.json')); if (c.digest(pair) !== process.env.PAIR_SHA256) throw Error('pair pin'); const identity = JSON.parse(pair).identity; - if (identity.commit !== process.env.SOURCE_SHA) throw Error('source pin'); + if (identity.commit !== process.env.SOURCE_SHA || identity.versions.agentplugins !== process.env.AGENT_VERSION) throw Error('source/version pin'); const producer = { repository: process.env.GITHUB_REPOSITORY, workflow: '.github/workflows/authoring-frozen-native.yml', source: process.env.SOURCE_SHA, workflow_sha: process.env.WORKFLOW_SHA, run_id: Number(process.env.GITHUB_RUN_ID), run_attempt: Number(process.env.GITHUB_RUN_ATTEMPT) }; - const options = { root, pins: { identity, candidate_sha256: process.env.CANDIDATE_SHA256, + const options = { root, target: process.env.NATIVE_TARGET, pins: { identity, candidate_sha256: process.env.CANDIDATE_SHA256, pair_marker_sha256: process.env.PAIR_SHA256, products: JSON.parse(process.env.PROJECTION_PINS) }, preparation: { sha256: process.env.RECEIPT_SHA256, producer: { ...producer, workflow: '.github/workflows/agentplugins-release.yml', run_id: Number(process.env.PREPARATION_RUN), @@ -156,11 +174,21 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 with: - name: frozen-native-linux-amd64-${{ github.run_id }}-${{ github.run_attempt }} + name: frozen-native-${{ fromJSON(inputs.host_contract).target }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/frozen-native-evidence/* if-no-files-found: error retention-days: 7 -# N2 must replace metadata-plus-download with same-checked-ZIP intake before -# provider admission. This job does not claim that a supplied boolean verifies -# the attempt. N1 terminals bind embedded attempts and reject stale preparation. -# No OS observer is provisioned here: production retains failure diagnostics. + excluded-native-execution: + if: ${{ github.event_name == 'workflow_dispatch' && fromJSON(inputs.host_contract).target != 'linux-amd64' && fromJSON(inputs.host_contract).target != 'linux-arm64' }} + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Preserve excluded execution as an unresolved failure + run: | + echo 'NATIVE_EXECUTION_PENDING: Windows and writable macOS are not enabled by portable source.' >&2 + echo 'No terminal emitted. No excluded-platform runner, product invocation or diagnosis.' >&2 + exit 1 +# Every target still needs an actual supported observation boundary, scanner/feed +# custody and native execution. The original authoring-native security matrix is +# separate and unchanged. A failure or skipped job never supplies terminal proof. diff --git a/cli/plugin-kit-ai/cmd/agentplugins/main_test.go b/cli/plugin-kit-ai/cmd/agentplugins/main_test.go index 847c3f0d..dc7a85a6 100644 --- a/cli/plugin-kit-ai/cmd/agentplugins/main_test.go +++ b/cli/plugin-kit-ai/cmd/agentplugins/main_test.go @@ -1,17 +1,25 @@ package main import ( + "bytes" "context" + "crypto/sha256" "encoding/base64" "errors" + "fmt" + "io/fs" "net/http" "os" "path/filepath" + "reflect" + "runtime" "strings" "testing" "time" + "github.com/777genius/plugin-kit-ai/cli/internal/authoringcli" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/directoryv1" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) type failingRoundTripper struct{} @@ -112,3 +120,129 @@ func TestInvalidScopeAndTargetDoNotCreateDataRoot(t *testing.T) { }) } } + +// Serial transport fixture: intended requests are recorded and denied in memory. +// No socket, trusted assessment, scanner executable or cache entry is supplied. +type offlineSecurityTransport struct{ urls []string } + +func (r *offlineSecurityTransport) RoundTrip(req *http.Request) (*http.Response, error) { + r.urls = append(r.urls, req.URL.String()) + return nil, errors.New("offline-security-transport-sentinel") +} +func securityFixtureTree(t *testing.T, root string) map[string]string { + t.Helper() + entries := map[string]string{} + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + st, err := d.Info() + if err != nil { + return err + } + if !st.IsDir() && !st.Mode().IsRegular() { + return fmt.Errorf("unexpected fixture entry %s", p) + } + var b []byte + if !st.IsDir() { + b, err = os.ReadFile(p) + if err != nil { + return err + } + } + rel, err := filepath.Rel(root, p) + if err != nil { + return err + } + entries[rel] = fmt.Sprintf("%s/%x", st.Mode(), sha256.Sum256(b)) + return nil + }) + if err != nil { + t.Fatal(err) + } + return entries +} +func TestOfflinePublicInstallerSecurityBoundary(t *testing.T) { + home, source, state := t.TempDir(), t.TempDir(), t.TempDir() + client := filepath.Join(home, ".codex") + if err := os.Mkdir(client, 0700); err != nil { + t.Fatal(err) + } + for p, body := range map[string]string{filepath.Join(client, "config.toml"): "# synthetic client\n", + filepath.Join(source, "plugin.json"): `{"$schema":"` + domain.PluginSchemaV1 + `","name":"demo"}`} { + if err := os.WriteFile(p, []byte(body), 0600); err != nil { + t.Fatal(err) + } + } + t.Setenv("HOME", home) + t.Setenv("CODEX_HOME", client) + t.Setenv("AGENTPLUGINS_HOME", state) + t.Setenv("PATH", home) + t.Setenv("AGENTPLUGINS_SECURITY_ORIGIN", "") + beforeSource, beforeHome, beforeState := securityFixtureTree(t, source), securityFixtureTree(t, home), securityFixtureTree(t, state) + transport := &offlineSecurityTransport{} + oldTransport, oldArgs, oldOut, oldErr := http.DefaultTransport, os.Args, os.Stdout, os.Stderr + defer func() { http.DefaultTransport, os.Args, os.Stdout, os.Stderr = oldTransport, oldArgs, oldOut, oldErr }() + http.DefaultTransport = transport + stdout, err := os.CreateTemp(t.TempDir(), "stdout") + if err != nil { + t.Fatal(err) + } + defer stdout.Close() + stderr, err := os.CreateTemp(t.TempDir(), "stderr") + if err != nil { + t.Fatal(err) + } + defer stderr.Close() + os.Stdout, os.Stderr = stdout, stderr + argv := []string{"add", source, "--target=codex", "--scope=project", "--dry-run", "--format=json"} + os.Args = append([]string{"agentplugins"}, argv...) + var out, errout bytes.Buffer + err = executeRelease(context.Background(), argv, authoringcli.Streams{Out: &out, Err: &errout}, run) + if err == nil || out.Len() != 0 || errout.String() != "agentplugins: --scope project is not supported by the current client adapters; the public CLI supports user scope only\n" || len(transport.urls) != 0 { + t.Fatalf("preflight boundary: %v %q %q requests=%v", err, out.String(), errout.String(), transport.urls) + } + if !reflect.DeepEqual(beforeState, securityFixtureTree(t, state)) || !reflect.DeepEqual(beforeSource, securityFixtureTree(t, source)) || !reflect.DeepEqual(beforeHome, securityFixtureTree(t, home)) { + t.Fatal("preflight acquired or wrote state") + } + os.Args = []string{"agentplugins", "add", source, "--target=codex", "--dry-run", "--format=json"} + err = run() + if err == nil || !strings.Contains(err.Error(), "security assessment failed before installation") || !strings.Contains(err.Error(), "offline-security-transport-sentinel") { + t.Fatalf("security bypassed: %v", err) + } + if len(transport.urls) != 4 { + t.Fatalf("expected three index attempts then scanner acquisition: %v", transport.urls) + } + for _, url := range transport.urls[:3] { + if url != defaultSecurityOrigin+"latest.json" { + t.Fatalf("unexpected index URL %s", url) + } + } + if !strings.HasPrefix(transport.urls[3], "https://github.com/777genius/lintai/releases/download/v0.1.3/lintai-v0.1.3-") { + t.Fatal(transport.urls) + } + b, err := os.ReadFile(stdout.Name()) + if err != nil || len(b) != 0 { + t.Fatalf("published result: %s %v", b, err) + } + b, err = os.ReadFile(stderr.Name()) + if err != nil || string(b) != "Resolving and validating one Agent Plugin package for every selected target...\n" { + t.Fatalf("progress changed: %s %v", b, err) + } + if !reflect.DeepEqual(beforeSource, securityFixtureTree(t, source)) || !reflect.DeepEqual(beforeHome, securityFixtureTree(t, home)) { + t.Fatal("source/client mutation") + } + // Only empty scanner acquisition directories are permitted; no lifecycle state, + // assessment cache, executable bytes or leaked private source snapshot. + want := []string{".", "security", "security/lintai", "security/lintai/0.1.3", "security/lintai/0.1.3/" + runtime.GOOS + "-" + runtime.GOARCH} + entries := securityFixtureTree(t, state) + if len(entries) != len(want) { + t.Fatalf("unexpected installer effects: %v", entries) + } + for _, rel := range want { + st, err := os.Stat(filepath.Join(state, rel)) + if err != nil || !st.IsDir() { + t.Fatalf("unexpected scanner scratch %s %v", rel, err) + } + } +} diff --git a/cli/plugin-kit-ai/cmd/agentplugins/release_root_test.go b/cli/plugin-kit-ai/cmd/agentplugins/release_root_test.go index 935e9084..9a45d418 100644 --- a/cli/plugin-kit-ai/cmd/agentplugins/release_root_test.go +++ b/cli/plugin-kit-ai/cmd/agentplugins/release_root_test.go @@ -225,3 +225,40 @@ func TestReleaseCompletionProcessStderr(t *testing.T) { } } } + +func TestReleaseOfflineInstallerObservationDispatch(t *testing.T) { + sentinel := errors.New("valid production dry-run still requires installer") + for _, tc := range []struct { + args []string + calls int + }{ + {[]string{"add", "/disposable/generated skill", "--target=codex", "--dry-run", "--format=json"}, 1}, + {[]string{"add", "--help", "--format=json"}, 0}, + {[]string{"author", "--help", "--format=json"}, 0}, + {[]string{"author"}, 0}, + } { + var out, stderr bytes.Buffer + calls := 0 + err := executeRelease(context.Background(), tc.args, authoringcli.Streams{Out: &out, Err: &stderr}, func() error { calls++; return sentinel }) + if calls != tc.calls { + t.Fatalf("dispatch %v calls=%d", tc.args, calls) + } + if tc.calls == 1 { + if !errors.Is(err, sentinel) || out.Len() != 0 || stderr.String() != "agentplugins: "+sentinel.Error()+"\n" { + t.Fatal(err, out.String(), stderr.String()) + } + } else if err != nil || stderr.Len() != 0 { + t.Fatal(err, stderr.String()) + } + if tc.args[0] == "add" && tc.calls == 0 { + var got any + if err := json.Unmarshal(out.Bytes(), &got); err != nil { + t.Fatal(err) + } + want := `{"schema_version":1,"command":"help","result":"success","data":{"commands":null,"use":"agentplugins add"}}` + "\n" + if out.String() != want { + t.Fatalf("installer visibility JSON changed: %s", out.String()) + } + } + } +} diff --git a/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go b/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go index b2e41e12..d5865854 100644 --- a/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go +++ b/cli/plugin-kit-ai/cmd/agentplugins/release_workflow_test.go @@ -1,12 +1,14 @@ package main import ( + "fmt" "go/ast" "go/parser" "go/token" "os" "os/exec" "path/filepath" + "reflect" "regexp" "runtime" "strconv" @@ -83,25 +85,34 @@ type producerWorkflow struct { } `yaml:"workflow_run"` Dispatch struct { Inputs map[string]struct { - Default string `yaml:"default"` - Options []string `yaml:"options"` + Default string `yaml:"default"` + Type string `yaml:"type"` + Required bool `yaml:"required"` + Options []string `yaml:"options"` } `yaml:"inputs"` } `yaml:"workflow_dispatch"` } `yaml:"on"` Permissions map[string]string `yaml:"permissions"` Jobs map[string]struct { If string `yaml:"if"` + Name string `yaml:"name"` + Runner string `yaml:"runs-on"` + Timeout int `yaml:"timeout-minutes"` + Outputs map[string]string `yaml:"outputs"` Environment any `yaml:"environment"` Needs any `yaml:"needs"` Uses string `yaml:"uses"` Permissions map[string]string `yaml:"permissions"` Env map[string]string `yaml:"env"` Steps []struct { - Name string `yaml:"name"` - Run string `yaml:"run"` - Uses string `yaml:"uses"` - With map[string]any `yaml:"with"` - Env map[string]string `yaml:"env"` + Name string `yaml:"name"` + ID string `yaml:"id"` + If string `yaml:"if"` + Continue bool `yaml:"continue-on-error"` + Run string `yaml:"run"` + Uses string `yaml:"uses"` + With map[string]any `yaml:"with"` + Env map[string]string `yaml:"env"` } `yaml:"steps"` } `yaml:"jobs"` } @@ -174,16 +185,19 @@ func TestReleaseWorkflowRunnerCacheContext(t *testing.T) { func TestReleasePairedPreparationReadOnlyGraph(t *testing.T) { w := readProducerWorkflow(t, "agentplugins-release.yml") mode := w.On.Dispatch.Inputs["producer_mode"] - if mode.Default != "binary-only" || strings.Join(mode.Options, ",") != "binary-only,paired-preparation,paired-promotion" { + if mode.Default != "binary-only" || strings.Join(mode.Options, ",") != "binary-only,paired-preparation,paired-promotion,paired-input-provenance" { t.Fatal("default binary-only dispatch contract changed") } if len(w.Permissions) != 1 || w.Permissions["contents"] != "read" { t.Fatal("workflow must default to contents-read") } - if len(w.Jobs) != 8 { + if len(w.Jobs) != 11 { t.Fatal("review every new producer job for preparation reachability") } for name, job := range w.Jobs { + if name == "dispatch_contract" || strings.HasPrefix(name, "paired_input_") { + continue + } if name == "paired-promotion-admission" || name == "paired-sign-and-promote" { if job.If != "${{ github.event_name == 'workflow_dispatch' && inputs.producer_mode == 'paired-promotion' }}" { t.Fatalf("%s loses explicit promotion isolation", name) @@ -321,17 +335,35 @@ func TestReleasePairedRouteCannotTriggerDownstreamPublication(t *testing.T) { // Parse the restricted boolean expression grammar, rejecting unknown syntax. // Testing a failed event must evaluate the whole OR/AND graph, not find a token. func downstreamCondition(t *testing.T, expression, event, conclusion string) bool { + t.Helper() + return workflowCondition(t, expression, map[string]any{"github.event_name": event, "github.event.workflow_run.conclusion": conclusion, + "vars.NPM_PUBLISH_READY": "true", "vars.PYPI_TRUSTED_PUBLISHING_READY": "true", "inputs.producer_mode": "paired-promotion"}) +} +func workflowCondition(t *testing.T, expression string, values map[string]any) bool { t.Helper() expression = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(expression), "${{"), "}}")) - values := map[string]string{"github.event_name": event, "github.event.workflow_run.conclusion": conclusion, - "vars.NPM_PUBLISH_READY": "true", "vars.PYPI_TRUSTED_PUBLISHING_READY": "true", "inputs.producer_mode": "paired-promotion"} + if expression == "" { + expression = "success()" + } + for _, fn := range []string{"success", "always", "failure", "cancelled"} { + if value, ok := values[fn]; ok { + expression = strings.ReplaceAll(expression, fn+"()", strconv.FormatBool(value.(bool))) + } + } words := regexp.MustCompile(`'[^']*'|[a-zA-Z_][a-zA-Z0-9_.]*`) expression = words.ReplaceAllStringFunc(expression, func(s string) string { if strings.HasPrefix(s, "'") { return strconv.Quote(s[1 : len(s)-1]) } if value, ok := values[s]; ok { - return strconv.Quote(value) + switch v := value.(type) { + case string: + return strconv.Quote(v) + case bool: + return strconv.FormatBool(v) + default: + t.Fatal("unsupported typed context") + } } if s == "true" || s == "false" { return s @@ -420,13 +452,18 @@ func TestReleasePairedPromotionProtectedGraph(t *testing.T) { } } binding := strings.Index(step.Run, "Buffer.from(") - effect := strings.Index(step.Run, "p.acquireArtifact(") + contract := strings.Index(step.Run, "p.checkNativeContracts(record)") + scratch := strings.Index(step.Run, "fs.mkdtempSync(") + if contract <= binding || scratch <= contract { + t.Fatalf("%s must reject unsupported native identifiers before scratch/provider effects", step.Name) + } + effect := strings.Index(step.Run, "p.acquirePreparation(") if binding < 0 || !strings.Contains(step.Run, ", selected)") || (effect >= 0 && binding > effect) { t.Fatalf("%s must bind canonical record before native/provider effects", step.Name) } } } - if admission.Needs != nil || len(admission.Permissions) != 1 || admission.Permissions["contents"] != "read" || signing.Needs != "paired-promotion-admission" { + if admission.Needs != nil || len(admission.Permissions) != 2 || admission.Permissions["actions"] != "read" || admission.Permissions["contents"] != "read" || signing.Needs != "paired-promotion-admission" { t.Fatal("native admission must precede protected promotion") } if signing.Environment != "agentplugins-release" { @@ -496,14 +533,14 @@ func TestReleasePairedPromotionShellSyntax(t *testing.T) { func TestFrozenNativeReadOnlyWorkflowContract(t *testing.T) { w := readProducerWorkflow(t, "authoring-frozen-native.yml") - if len(w.Jobs) != 1 || len(w.On.Dispatch.Inputs) != 10 || len(w.On.Run.Workflows) != 0 { - t.Fatal("N1 requires one explicit Linux lane and ten bounded identity inputs") + if len(w.Jobs) != 2 || len(w.On.Dispatch.Inputs) != 10 || len(w.On.Run.Workflows) != 0 { + t.Fatal("N2 requires explicit selected native route, excluded failure route and ten bounded inputs") } if len(w.Permissions) != 1 || w.Permissions["contents"] != "read" || w.Concurrency.Cancel { t.Fatal("native producer must preserve read-only permissions and owned cancellation") } job, ok := w.Jobs["linux-amd64"] - if !ok || job.If != "${{ github.event_name == 'workflow_dispatch' }}" || job.Needs != nil || job.Environment != nil { + if !ok || job.If != "${{ github.event_name == 'workflow_dispatch' && (fromJSON(inputs.host_contract).target == 'linux-amd64' || fromJSON(inputs.host_contract).target == 'linux-arm64') }}" || job.Needs != nil || job.Environment != nil { t.Fatal("native route must remain independently dispatched without protected effects") } if len(job.Permissions) != 2 || job.Permissions["actions"] != "read" || job.Permissions["contents"] != "read" { @@ -524,18 +561,16 @@ func TestFrozenNativeReadOnlyWorkflowContract(t *testing.T) { t.Fatalf("%s: %v %s", step.Name, err, output) } } - if token, ok := step.Env["GH_TOKEN"]; ok && (step.Name != "Inspect exact preparation attempt and artifact metadata" || token != "${{ github.token }}") { + if token, ok := step.Env["GH_TOKEN"]; ok && (step.Name != "Acquire and extract the same checked preparation ZIP" || token != "${{ github.token }}") { t.Fatal("read token escaped acquisition") } if strings.HasPrefix(step.Uses, "actions/download-artifact@") { + t.Fatal("a second downloader can extract different bytes from the checked ZIP") + } + if strings.Contains(step.Run, "p.acquireArtifact(") { downloads++ - for k, v := range map[string]string{"artifact-ids": "${{ inputs.artifact_id }}", "run-id": "${{ inputs.preparation_run }}", "repository": "777genius/universal-agent-plugins"} { - if step.With[k] != v { - t.Fatalf("download lost exact %s", k) - } - } - if _, ok := step.With["name"]; ok { - t.Fatal("artifact name must not select frozen bytes") + if !strings.Contains(step.Run, "p.extractArtifact(file, pin, 'preparation', files,") { + t.Fatal("extract the same acquired file with the independently selected pin") } } if strings.HasPrefix(step.Uses, "actions/upload-artifact@") { @@ -544,7 +579,7 @@ func TestFrozenNativeReadOnlyWorkflowContract(t *testing.T) { t.Fatal("upload must exclude binaries, client state and compilation caches") } } - if step.Uses != "" && !regexp.MustCompile(`^actions/(checkout|setup-node|setup-go|download-artifact|upload-artifact)@[0-9a-f]{40}$`).MatchString(step.Uses) { + if step.Uses != "" && !regexp.MustCompile(`^actions/(checkout|setup-node|setup-go|upload-artifact)@[0-9a-f]{40}$`).MatchString(step.Uses) { t.Fatalf("unreviewed action %s", step.Uses) } } @@ -557,7 +592,7 @@ func TestFrozenNativeReadOnlyWorkflowContract(t *testing.T) { t.Fatalf("native route reaches forbidden operation %s", forbidden) } } - for _, required := range []string{"p.inspectArtifact", "run_attempt: Number(process.env.PREPARATION_ATTEMPT)", "authoring-native-qualification.js", "go_sha256: process.env.HOST_GO_SHA256", "pair_marker_sha256: process.env.PAIR_SHA256"} { + for _, required := range []string{"p.acquireArtifact", "p.extractArtifact", "run_attempt: Number(process.env.PREPARATION_ATTEMPT)", "authoring-native-qualification.js", "go_sha256: process.env.HOST_GO_SHA256", "pair_marker_sha256: process.env.PAIR_SHA256"} { if !strings.Contains(body, required) { t.Fatalf("native route lacks %s", required) } @@ -593,3 +628,445 @@ func TestFrozenPreparationReceiptOutsideProjectionBytes(t *testing.T) { t.Fatal("one byte-bound receipt before the existing upload required") } } + +func TestN2AdmissionAcquiresEvidenceBeforeOIDC(t *testing.T) { + w := readProducerWorkflow(t, "agentplugins-release.yml") + read := w.Jobs["paired-promotion-admission"] + if read.Environment != nil || len(read.Permissions) != 2 || read.Permissions["actions"] != "read" || read.Permissions["contents"] != "read" { + t.Fatal("native evidence intake must remain outside protected writes/OIDC") + } + step := read.Steps[len(read.Steps)-1] + selectAt := strings.Index(step.Run, "p.validateSelection(") + acquireAt := strings.Index(step.Run, "p.admitNativeEvidence(") + publicAt := strings.Index(step.Run, "p.requireNativeContracts(") + if selectAt < 0 || acquireAt <= selectAt || publicAt <= acquireAt { + t.Fatal("syntactic dispatch selection, real native admission, then mandatory public contract required") + } + for key, value := range map[string]string{ + "GH_TOKEN": "${{ github.token }}", "PREPARATION_RUN": "${{ inputs.preparation_run }}", + "PREPARATION_ATTEMPT": "${{ inputs.preparation_attempt }}", "PREPARATION_ARTIFACT": "${{ inputs.preparation_artifact }}", + "PREPARATION_DIGEST": "${{ inputs.preparation_digest }}", + } { + if step.Env[key] != value { + t.Fatalf("missing independent provider locator %s", key) + } + } + write := w.Jobs["paired-sign-and-promote"] + if write.Needs != "paired-promotion-admission" { + t.Fatal("protected job may not bypass read-only admission failure") + } + for _, step := range write.Steps { + if strings.HasPrefix(step.Uses, "actions/download-artifact@") { + t.Fatal("protected job must never independently download/extract unchecked ZIP bytes") + } + if strings.Contains(step.Run, "p.acquirePreparation(") && !strings.Contains(step.Run, "root: acquired.root") { + t.Fatal("promotion must use the checked extracted preparation root") + } + } +} + +func TestN2ExcludedNativeExecutionRemainsFailure(t *testing.T) { + w := readProducerWorkflow(t, "authoring-frozen-native.yml") + if _, ok := w.On.Dispatch.Inputs["host_contract"]; !ok { + t.Fatal("explicit target/version/host-tool pins required") + } + job := w.Jobs["excluded-native-execution"] + if !strings.Contains(job.If, "target != 'linux-amd64'") || !strings.Contains(job.If, "target != 'linux-arm64'") || + len(job.Permissions) != 1 || job.Permissions["contents"] != "read" || len(job.Steps) != 1 || job.Environment != nil { + t.Fatal("excluded Windows/writable macOS must retain an explicit non-protected failure route") + } + if job.Steps[0].Uses != "" || !strings.Contains(job.Steps[0].Run, "NATIVE_EXECUTION_PENDING") { + t.Fatal("excluded platforms must not download or execute products") + } + command := exec.Command("/bin/bash", "-e", "-c", job.Steps[0].Run) + command.Env = []string{"PATH=/usr/local/bin:/usr/bin:/bin"} + if output, err := command.CombinedOutput(); err == nil || !strings.Contains(string(output), "No terminal emitted") { + t.Fatalf("pending execution must fail, not become skipped qualification: %v %s", err, output) + } + _, source, _, _ := runtime.Caller(0) + body, err := os.ReadFile(filepath.Join(filepath.Dir(source), "../../../../.github/workflows/authoring-frozen-native.yml")) + if err != nil { + t.Fatal(err) + } + for _, forbidden := range []string{"runs-on: windows", "runs-on: macos", "continue-on-error:", "strategy:", "--accept-security-risk"} { + if strings.Contains(string(body), forbidden) { + t.Fatalf("excluded native route enables %s", forbidden) + } + } +} + +// Fixed C1 graph expectations; fixtures prove source reachability, not hosted +// permission enforcement, cryptographic acceptance or genuine provider custody. +func c1Needs(w producerWorkflow, name string) []string { + switch n := w.Jobs[name].Needs.(type) { + case nil: + return nil + case string: + return []string{n} + case []any: + result := []string{} + for _, v := range n { + result = append(result, v.(string)) + } + return result + default: + panic("unknown needs shape") + } +} +func c1Permissions(w producerWorkflow, name string) map[string]string { + if w.Jobs[name].Permissions != nil { + return w.Jobs[name].Permissions + } + return w.Permissions +} +func c1Scripts(w producerWorkflow, name string) string { + var s strings.Builder + for _, step := range w.Jobs[name].Steps { + s.WriteString(step.Run) + } + return s.String() +} +func c1Contract(w producerWorkflow, name string, stage, signer bool) error { + job, ok := w.Jobs[name] + if !ok { + return fmt.Errorf("missing %s", name) + } + need, mode, env, timeout := "dispatch_contract", "paired-input-provenance", "agentplugins-release", 20 + if stage { + mode, env = "paired-stage", "npm-agentplugins" + timeout = 30 + } + if signer { + timeout = 20 + if stage { + need = "paired_stage" + } else { + need = "paired_input_admission" + } + } + condition := "${{ success() && github.event_name == 'workflow_dispatch' && inputs.producer_mode == '" + mode + "'" + if stage { + condition += " && inputs.publish == false" + } + condition += " && needs." + need + ".result == 'success' }}" + if job.If != condition || !reflect.DeepEqual(c1Needs(w, name), []string{need}) { + return fmt.Errorf("%s mode/status/needs", name) + } + permissions := map[string]string{"contents": "read", "actions": "read"} + if signer { + permissions["id-token"] = "write" + permissions["attestations"] = "write" + } else if stage { + permissions["attestations"] = "read" + } + if !reflect.DeepEqual(c1Permissions(w, name), permissions) { + return fmt.Errorf("%s effective permissions", name) + } + if (signer && job.Environment != env) || (!signer && job.Environment != nil) || job.Runner != "ubuntu-24.04" || job.Timeout != timeout { + return fmt.Errorf("%s execution boundary", name) + } + if len(job.Steps) < 3 || job.Steps[0].Name != "C1 preflight" || job.Steps[0].Uses != "" { + return fmt.Errorf("%s preflight ordering", name) + } + attest, uploads, admission, recheck := -1, 0, -1, -1 + for index, step := range job.Steps { + if step.Continue || (step.If != "" && step.If != "${{ success() }}") { + return fmt.Errorf("%s step bypass", name) + } + if step.ID == "stage" { + admission = index + } + if step.ID == "recheck" { + recheck = index + } + if strings.HasPrefix(step.Uses, "actions/attest@") { + attest = index + if step.Uses != "actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6" || step.With["subject-path"] != "${{ steps.stage.outputs.subjects }}" { + return fmt.Errorf("exact subject signing") + } + } + if strings.HasPrefix(step.Uses, "actions/upload-artifact@") { + uploads++ + if step.Uses != "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" || step.ID != "upload" || step.With["if-no-files-found"] != "error" || step.With["retention-days"] != 7 || step.With["overwrite"] != nil { + return fmt.Errorf("immutable upload") + } + id := "stage" + if signer { + id = "recheck" + } + if step.With["path"] != "${{ steps."+id+".outputs.payload }}" { + return fmt.Errorf("fixed upload payload") + } + } + } + if signer && (attest <= admission || admission < 0 || recheck <= attest) { + return fmt.Errorf("independent admission/sign/recheck order") + } + if !signer && attest != -1 { + return fmt.Errorf("unexpected signing") + } + expectedUploads := 0 + if stage != signer { + expectedUploads = 1 + } + if uploads != expectedUploads { + return fmt.Errorf("upload lifecycle") + } + body := c1Scripts(w, name) + for _, forbidden := range []string{"npm publish", "npm install", "--read-stage", "allow_incomplete", "needs.paired_input_admission.outputs", "needs.paired_stage.outputs.accepted"} { + if strings.Contains(body, forbidden) { + return fmt.Errorf("forbidden C1 effect or trust substitution: %s", forbidden) + } + } + if signer && (!strings.Contains(body, "assert.equal(previous.root, e.SIGNING_ROOT)") || !strings.Contains(body, "pins(previous.root)") || !strings.Contains(body, "result.root = previous.root") || !strings.Contains(body, "fs.mkdtempSync")) { + return fmt.Errorf("original signing root recheck") + } + return nil +} +func TestC1InputProvenanceWorkflowContract(t *testing.T) { + w := readProducerWorkflow(t, "agentplugins-release.yml") + if len(w.Jobs) != 11 || len(w.On.Dispatch.Inputs) != 10 || w.On.Dispatch.Inputs["producer_mode"].Type != "choice" || !w.On.Dispatch.Inputs["producer_mode"].Required { + t.Fatal("closed release inputs/jobs") + } + for _, name := range []string{"paired_input_admission", "paired_input_attestation"} { + if err := c1Contract(w, name, false, name == "paired_input_attestation"); err != nil { + t.Fatal(err) + } + body := c1Scripts(w, name) + for _, text := range []string{"--produce-inputs", "preparation:", "workflow_sha: e.SOURCE_SHA", "assert.equal(payload.length, 21)", "'native-inputs.json'", "'preparation-run.json', 'candidate-identity.json'"} { + if !strings.Contains(body, text) { + t.Fatalf("%s missing %s", name, text) + } + } + } + if len(w.Jobs["paired_input_admission"].Outputs) != 1 || len(w.Jobs["paired_input_attestation"].Outputs) != 3 { + t.Fatal("selector-only outputs") + } +} +func TestC1PublicStageWorkflowContract(t *testing.T) { + w := readProducerWorkflow(t, "agentplugins-npm-publish.yml") + if len(w.Jobs) != 6 || len(w.On.Dispatch.Inputs) != 7 || strings.Join(w.On.Dispatch.Inputs["producer_mode"].Options, ",") != "legacy,paired-stage" || w.On.Dispatch.Inputs["producer_mode"].Default != "legacy" || w.On.Dispatch.Inputs["publish"].Type != "boolean" { + t.Fatal("closed stage inputs/jobs") + } + for _, name := range []string{"paired_stage", "paired_stage_attestation"} { + if err := c1Contract(w, name, true, name == "paired_stage_attestation"); err != nil { + t.Fatal(err) + } + body := c1Scripts(w, name) + if !strings.Contains(body, "assert.equal(payload.length, 3)") || !strings.Contains(body, "input_file") || !strings.Contains(body, "Buffer.from(e.NATIVE_INPUTS, 'utf8')") { + t.Fatal("three retained files and Buffer transport") + } + } + if strings.Count(c1Scripts(w, "paired_stage"), "--stage-prepublication") != 1 || strings.Count(c1Scripts(w, "paired_stage_attestation"), "--validate-unsigned-stage") != 2 { + t.Fatal("pack once and independent same-run validation") + } + for name, expected := range map[string][]string{"prepare": {"dispatch_contract"}, "publish": {"prepare"}, "verify-public": {"prepare", "publish"}} { + if !reflect.DeepEqual(c1Needs(w, name), expected) { + t.Fatalf("legacy needs changed %s", name) + } + } + for _, name := range []string{"prepare", "publish", "verify-public"} { + if !strings.Contains(w.Jobs[name].If, "inputs.producer_mode == 'legacy'") || !strings.Contains(w.Jobs[name].If, "github.event_name == 'workflow_dispatch'") || len(c1Needs(w, name)) == 0 { + t.Fatal("legacy isolation", name) + } + } +} +func TestC1WorkflowFailureReachability(t *testing.T) { + for _, file := range []string{"agentplugins-release.yml", "agentplugins-npm-publish.yml"} { + w := readProducerWorkflow(t, file) + modes := []string{"binary-only", "paired-preparation", "paired-promotion", "paired-input-provenance", "legacy", "paired-stage", "unknown"} + legacyPermissions := map[string]map[string]string{ + "dispatch_contract": {"contents": "read"}, "validate": {"checks": "read", "contents": "read", "pull-requests": "read"}, + "build": {"contents": "read"}, "stage-draft": {"contents": "write", "id-token": "write", "attestations": "write", "artifact-metadata": "write"}, + "platform-proof": {"contents": "read", "attestations": "read"}, "promote-release": {"contents": "write", "attestations": "read"}, + "paired-preparation": {"contents": "read"}, "paired-promotion-admission": {"contents": "read", "actions": "read"}, + "paired-sign-and-promote": {"contents": "write", "actions": "read", "id-token": "write", "attestations": "write", "artifact-metadata": "write"}, + "prepare": {"contents": "read", "attestations": "read"}, "publish": {"contents": "read", "id-token": "write"}, + "verify-public": {"contents": "read", "attestations": "read"}, + } + for name, job := range w.Jobs { + if expected, ok := legacyPermissions[name]; ok && !reflect.DeepEqual(c1Permissions(w, name), expected) { + t.Fatalf("effective legacy permissions %s", name) + } + if name == "dispatch_contract" { + continue + } + for _, mode := range modes { + for _, event := range []string{"workflow_dispatch", "workflow_run"} { + for _, status := range []string{"success", "failure", "cancelled", "skipped", ""} { + for _, publish := range []bool{true, false} { + values := map[string]any{"github.event_name": event, "inputs.producer_mode": mode, "inputs.publish": publish, + "success": status == "success", "failure": status == "failure", "cancelled": status == "cancelled", "always": true} + for dependency := range w.Jobs { + values["needs."+dependency+".result"] = status + } + // GitHub's implicit success() applies when no status function is present. + reachable := workflowCondition(t, job.If, values) + if !strings.Contains(job.If, "success()") { + reachable = reachable && status == "success" + } + for _, dep := range c1Needs(w, name) { + reachable = reachable && values["needs."+dep+".result"] == "success" + } + if (status != "success") && reachable { + t.Fatalf("%s reachable after %s", name, status) + } + if strings.HasPrefix(name, "paired_input_") || strings.HasPrefix(name, "paired_stage") { + expected := status == "success" && event == "workflow_dispatch" && ((strings.HasPrefix(name, "paired_input_") && mode == "paired-input-provenance") || (strings.HasPrefix(name, "paired_stage") && mode == "paired-stage" && !publish)) + if reachable != expected { + t.Fatalf("%s reachability %s %s %s %v", name, mode, event, status, publish) + } + for _, step := range job.Steps { + for _, state := range []string{"failure", "cancelled", "skipped", ""} { + stopped := map[string]any{"success": false, "failure": state == "failure", "cancelled": state == "cancelled", "always": true} + if workflowCondition(t, step.If, stopped) { + t.Fatalf("sensitive step survives %s", state) + } + } + + if workflowCondition(t, step.If, values) && reachable && step.Continue { + t.Fatal("sensitive step tolerates failure") + } + } + if err := c1Contract(w, name, strings.HasPrefix(name, "paired_stage"), strings.HasSuffix(name, "attestation")); err != nil { + t.Fatal(err) + } + } + if file == "agentplugins-npm-publish.yml" && (name == "prepare" || name == "publish" || name == "verify-public") { + expected := status == "success" && event == "workflow_dispatch" && mode == "legacy" && (name == "prepare" || publish) + if reachable != expected { + t.Fatalf("legacy reachability %s %s %s %s %v", name, mode, event, status, publish) + } + } + if mode == "paired-stage" && (name == "prepare" || name == "publish" || name == "verify-public") && reachable { + t.Fatal("paired stage reaches legacy") + } + } + } + } + } + } + name := "paired_input_attestation" + if file == "agentplugins-npm-publish.yml" { + name = "paired_stage_attestation" + } + original := w.Jobs[name] + for _, mutation := range []string{"mode", "or true", "always", "needs", "output authorization", "permissions", "continue"} { + job := original + job.Steps = append(job.Steps[:0:0], job.Steps...) + switch mutation { + case "mode": + job.If = "${{ success() }}" + case "or true": + job.If = strings.TrimSuffix(job.If, " }}") + " || true }}" + case "always": + job.If = "${{ always() }}" + case "needs": + job.Needs = nil + case "output authorization": + job.If = "${{ needs.paired_stage.outputs.accepted == 'true' }}" + case "permissions": + job.Permissions = map[string]string{"contents": "write"} + case "continue": + job.Steps[0].Continue = true + } + w.Jobs[name] = job + if c1Contract(w, name, file == "agentplugins-npm-publish.yml", true) == nil { + t.Fatal("mutation accepted", mutation) + } + } + w.Jobs[name] = original + } +} +func TestC1WorkflowPreflightNoEffects(t *testing.T) { + for _, file := range []string{"agentplugins-release.yml", "agentplugins-npm-publish.yml"} { + w := readProducerWorkflow(t, file) + stage := file == "agentplugins-npm-publish.yml" + mode := "paired-input-provenance" + if stage { + mode = "paired-stage" + } + good := map[string]string{"PRODUCER_MODE": mode, "TAG": "agentplugins-v0.1.54", "KIT_VERSION": "2.0.0", "SOURCE_SHA": strings.Repeat("a", 40), + "GITHUB_EVENT_NAME": "workflow_dispatch", "GITHUB_ACTIONS": "true", "GITHUB_REPOSITORY": "777genius/universal-agent-plugins", + "GITHUB_SHA": strings.Repeat("a", 40), "GITHUB_WORKFLOW_SHA": strings.Repeat("a", 40), "GITHUB_REF": "refs/tags/agentplugins-v0.1.54", + "GITHUB_WORKFLOW_REF": "777genius/universal-agent-plugins/.github/workflows/" + file + "@refs/tags/agentplugins-v0.1.54", + "GITHUB_RUN_ID": "21", "GITHUB_RUN_ATTEMPT": "2", "PUBLISH": "false", "NATIVE_INPUTS": "{}\n", + "INPUT_ARTIFACT": `{"run_id":11,"run_attempt":1,"artifact_id":31,"artifact_sha256":"` + strings.Repeat("b", 64) + `"}`, + "PREPARATION_RUN": "11", "PREPARATION_ATTEMPT": "1", "PREPARATION_ARTIFACT": "31", "PREPARATION_DIGEST": strings.Repeat("b", 64), "PROMOTION_RECORD": "", "PROMOTION_OPERATION": "promote"} + names := []string{"dispatch_contract", "paired_input_admission", "paired_input_attestation"} + if stage { + names = []string{"dispatch_contract", "paired_stage", "paired_stage_attestation"} + } + for _, name := range names { + good["GITHUB_JOB"] = name + good["STAGE_ARTIFACT_ID"] = "901" + good["STAGE_ARTIFACT_SHA256"] = strings.Repeat("c", 64) + good["STAGE_SHA256"] = strings.Repeat("d", 64) + job := w.Jobs[name] + if len(job.Steps) == 0 { + t.Fatal("missing preflight") + } + for _, step := range job.Steps { + if step.Run != "" { + cmd := exec.Command("/bin/bash", "-n") + cmd.Stdin = strings.NewReader(step.Run) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("shell syntax %s: %v %s", name, err, out) + } + } + } + cases := []map[string]string{{}} + for _, key := range []string{"PRODUCER_MODE", "TAG", "KIT_VERSION", "SOURCE_SHA", "GITHUB_EVENT_NAME", "GITHUB_ACTIONS", "GITHUB_REPOSITORY", "GITHUB_SHA", "GITHUB_WORKFLOW_SHA", "GITHUB_REF", "GITHUB_WORKFLOW_REF", "GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT"} { + for _, bad := range []string{"", "invalid", "$(touch injected)", "bad\nvalue"} { + cases = append(cases, map[string]string{key: bad}) + } + } + if stage { + for _, bad := range []map[string]string{{"PUBLISH": "true"}, {"INPUT_ARTIFACT": "{}"}, {"INPUT_ARTIFACT": strings.ReplaceAll(good["INPUT_ARTIFACT"], `"run_attempt":1`, `"run_attempt":1001`)}, {"NATIVE_INPUTS": ""}, {"PRODUCER_MODE": "legacy"}} { + cases = append(cases, bad) + } + } else { + for _, key := range []string{"PREPARATION_RUN", "PREPARATION_ATTEMPT", "PREPARATION_ARTIFACT", "PREPARATION_DIGEST", "PROMOTION_RECORD", "PROMOTION_OPERATION"} { + cases = append(cases, map[string]string{key: "invalid"}) + } + } + if name != "dispatch_contract" { + cases = append(cases, map[string]string{"GITHUB_JOB": "publish"}) + } + if name == "paired_stage_attestation" { + for _, key := range []string{"STAGE_ARTIFACT_ID", "STAGE_ARTIFACT_SHA256", "STAGE_SHA256"} { + cases = append(cases, map[string]string{key: "invalid"}) + } + } + for index, changes := range cases { + dir := t.TempDir() + bin := filepath.Join(dir, "bin") + if err := os.Mkdir(bin, 0700); err != nil { + t.Fatal(err) + } + for _, tool := range []string{"node", "git", "npm", "gh", "tar", "python3", "curl"} { + if err := os.WriteFile(filepath.Join(bin, tool), []byte("#!/bin/bash\necho effect >> \"$MARKER\"\nexit 93\n"), 0700); err != nil { + t.Fatal(err) + } + } + cmd := exec.Command("/bin/bash", "-c", job.Steps[0].Run) + cmd.Dir = dir + cmd.Env = []string{"PATH=/usr/local/bin:" + bin + ":/usr/bin:/bin", "MARKER=" + filepath.Join(dir, "effect")} + for key, value := range good { + if replacement, ok := changes[key]; ok { + value = replacement + } + cmd.Env = append(cmd.Env, key+"="+value) + } + output, err := cmd.CombinedOutput() + if (err == nil) != (index == 0) { + t.Fatalf("%s preflight case %v: %v %s", name, changes, err, output) + } + entries, err := os.ReadDir(dir) + if err != nil || len(entries) != 1 || entries[0].Name() != "bin" { + t.Fatalf("preflight produced effects: %v %v", entries, err) + } + } + } + } +} diff --git a/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go b/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go index 84d809f6..1734e226 100644 --- a/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go +++ b/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go @@ -253,3 +253,72 @@ func TestPackedInstallerSourceHarness(t *testing.T) { }) } } + +// Assessment values here are deliberate test inputs, never lintai output/cache. +type boundaryAssessment struct { + *packedScanner + fail bool + assessment domain.SecurityAssessment +} + +func (s *boundaryAssessment) Evaluate(ctx context.Context, in domain.SecurityEvaluationInput) (domain.SecurityAssessment, error) { + a, err := s.packedScanner.Evaluate(ctx, in) + if err != nil { + return a, err + } + if s.fail { + return a, fmt.Errorf("injected assessment failure") + } + a.Outcome = domain.SecurityBlockingFindings + a.Counts = domain.SecurityCounts{Blocking: 1, Total: 1} + a.Findings = []domain.SecurityFinding{{Code: "TEST-BLOCKING", Disposition: "block", Severity: "high", Path: "plugin.json", Message: "Deliberate test input"}} + s.assessment = a + return a, nil +} +func TestPackedInstallerAssessmentBoundaries(t *testing.T) { + for _, fail := range []bool{true, false} { + t.Run(fmt.Sprintf("assessment-error-%t", fail), func(t *testing.T) { + source := filepath.Join(t.TempDir(), "demo") + author := commands.App{Projects: project.Service{Scratch: t.TempDir()}, Revision: publicRevision, PublicContract: true} + if _, code, out := publicRun(t, author, []string{"init", source, "--name=demo", "--template=skill", "--format=json"}, false); code != 0 { + t.Fatal(out) + } + fixture, scratch := t.TempDir(), t.TempDir() + beforeSource, beforeFixture, beforeScratch := packedTree(t, source), packedTree(t, fixture), packedTree(t, scratch) + registry, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + detector := &fixtureDetector{clients: []domain.DetectedClient{{ClientID: domain.ClientCodex, Status: domain.DetectionDetected, ConfigRoot: fixture}}} + scanner := &boundaryAssessment{packedScanner: &packedScanner{fixtureScanner: fixtureScanner{t: t}, source: source, scratch: scratch, tree: beforeSource}, fail: fail} + app := agentpluginscli.App{UserHome: fixture, ManagedRoot: filepath.Join(fixture, "managed"), Detector: detector, StateStore: noEffectState{}, + SourceAcquirer: sourceacquisition.Acquirer{TempRoot: scratch}, PackageLoader: loader.Loader{Registry: registry}, NativePackageLoader: loader.OpenAILoader{Loader: loader.Loader{Registry: registry}}, SecurityEvaluator: scanner, + Lifecycle: usecase.Service{Stager: noEffectStager{}, Activator: noEffectActivator{}}} + var out, stderr bytes.Buffer + err = authoringcli.Factory(func() (*cobra.Command, error) { return agentpluginscli.NewRoot(app), nil }).Execute(context.Background(), + []string{"add", source, "--target=codex", "--dry-run", "--format=json"}, authoringcli.Streams{Out: &out, Err: &stderr}) + if fail { + if err == nil || !strings.Contains(err.Error(), "injected assessment failure") || out.Len() != 0 { + t.Fatal("assessment error published result", err, out.String()) + } + } else { + var report struct { + Result string + Data struct { + DryRun bool `json:"dry_run"` + Security domain.SecurityAssessment + } + } + if err != nil || json.Unmarshal(out.Bytes(), &report) != nil || report.Result != "success" || !report.Data.DryRun || !reflect.DeepEqual(report.Data.Security, scanner.assessment) { + t.Fatal("blocking findings lost", err, out.String()) + } + } + if stderr.Len() != 0 || detector.calls != 1 || scanner.calls != 1 { + t.Fatal("wrong seam or output", stderr.String(), detector.calls, scanner.calls) + } + if !reflect.DeepEqual(beforeSource, packedTree(t, source)) || !reflect.DeepEqual(beforeFixture, packedTree(t, fixture)) || !reflect.DeepEqual(beforeScratch, packedTree(t, scratch)) { + t.Fatal("assessment path mutated fixture or leaked acquisition") + } + }) + } +} diff --git a/cli/plugin-kit-ai/internal/authoring/scaffold/template_quoting_test.go b/cli/plugin-kit-ai/internal/authoring/scaffold/template_quoting_test.go index 4aaa9677..c73dc10d 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/template_quoting_test.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/template_quoting_test.go @@ -100,7 +100,9 @@ func checkTemplateJavaScriptSyntax(t *testing.T, source []byte) { if err := os.WriteFile(path, source, 0600); err != nil { t.Fatal(err) } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + // Allow bounded headroom for slow CI process startup, including Windows. + const timeout = 60 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Parse only a newly generated temporary fixture. Do not import or execute // the module, load the SDK, install dependencies, or inherit Node preload flags. @@ -111,7 +113,13 @@ func checkTemplateJavaScriptSyntax(t *testing.T, source []byte) { volume := filepath.VolumeName(dir) cmd.Env = append(cmd.Env, "SystemRoot="+os.Getenv("SystemRoot"), "HOMEDRIVE="+volume, "HOMEPATH="+strings.TrimPrefix(dir, volume)) } - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("node --check: %v\n%s", err, out) + // Bound output-pipe waits as well as process execution. OS process startup + // itself may delay cancellation, so this is not a hard wall-clock guarantee. + cmd.WaitDelay = 5 * time.Second + started := time.Now() + out, err := cmd.CombinedOutput() + ctxErr := ctx.Err() + if err != nil || ctxErr != nil { + t.Fatalf("node --check: elapsed=%s timeout=%s context=%v process=%v output=%q", time.Since(started), timeout, ctxErr, err, out) } } diff --git a/npm/agentplugins/lib/public-authoring-contract.js b/npm/agentplugins/lib/public-authoring-contract.js new file mode 100644 index 00000000..a01b2e01 --- /dev/null +++ b/npm/agentplugins/lib/public-authoring-contract.js @@ -0,0 +1,210 @@ +"use strict"; + +// Pure structural codec; authentication and custody remain external. +const c = require("../scripts/dual-authoring-candidate"); +const { TextDecoder } = require("node:util"); + +const INPUT_SCHEMA = "authoring-native-inputs/v1"; +const DESCRIPTOR_SCHEMA = "dual-authoring-public-npm/v2"; +const INPUT_FILE = "native-inputs.json"; +const MODE = "release-cli-contract-v1"; +const SCOPE = "six-platform-pair"; +const WORKFLOW = ".github/workflows/agentplugins-release.yml"; +const MAX_INPUT_BYTES = 1024 * 1024; +const MAX_DESCRIPTOR_BYTES = 64 * 1024; +const MAX_NATIVE_BYTES = 128 * 1024 * 1024; +const PACKAGES = Object.freeze({ agentplugins: "universal-agent-plugins", "plugin-kit-ai": "plugin-kit-ai" }); + +function fail(label) { throw new Error(`structural consistency only: ${label}`); } +function fixed(value, expected, label) { + if (value !== expected) fail(`${label} mismatch`); + return value; +} +function hash(value, label) { + if (typeof value !== "string" || value.length !== 64 || !/^[0-9a-f]{64}$/.test(value) || /^0+$/.test(value)) fail(`${label} SHA256`); + return value; +} +function positive(value, maximum, label) { + if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) fail(`${label} positive bounded integer`); + return value; +} + +// Only ordinary own enumerable data fields. Do not silently discard symbols, +// hidden claims or custom prototypes, or invoke accessors/toJSON while encoding. +function fields(value, names, label) { + if (!value || typeof value !== "object" || + ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail(`${label} data object`); + const own = Reflect.ownKeys(value); + if (own.length !== names.length || own.some(key => typeof key !== "string" || !names.includes(key))) { + fail(`${label} unexpected or missing fields`); + } + for (const key of own) { + const d = Object.getOwnPropertyDescriptor(value, key); + if (!d.enumerable || !("value" in d)) fail(`${label} own enumerable data fields`); + } + c.keys(value, names, label); +} + +function identity(value) { + fields(value, ["repository", "commit", "engine_revision", "versions"], "identity"); + fields(value.versions, c.PRODUCTS, "versions"); + // Bound strings before calling the existing candidate version validator. + for (const product of c.PRODUCTS) { + if (typeof value.versions[product] !== "string" || value.versions[product].length > MAX_INPUT_BYTES || + /[\r\n]/.test(value.versions[product])) { + fail("version string limit/type"); + } + } + // Candidate's $-anchored regex also matches before a final newline; this + // contract requires exactly forty source characters and stable versions. + if (typeof value.commit !== "string" || value.commit.length !== 40) fail("exact source revision length/type"); + c.identity(value); + if (/^0+$/.test(value.commit)) fail("nonzero source revision required"); + fixed(value.versions["plugin-kit-ai"], "2.0.0", "kit version"); + return { repository: value.repository, commit: value.commit, engine_revision: value.engine_revision, + versions: { agentplugins: value.versions.agentplugins, "plugin-kit-ai": value.versions["plugin-kit-ai"] } }; +} + +function pin(value, file, label) { + fields(value, ["file", "sha256", "size"], label); + return { file: fixed(value.file, file, `${label} file`), sha256: hash(value.sha256, label), + size: positive(value.size, MAX_NATIVE_BYTES, `${label} size`) }; +} + +function inputs(value) { + fields(value, ["schema", "identity", "authoring_mode", "asset_scope", "candidate_sha256", + "pair_marker_sha256", "products", "preparation", "producer"], "inputs"); + fixed(value.schema, INPUT_SCHEMA, "input schema"); + const id = identity(value.identity); + fixed(value.authoring_mode, MODE, "authoring mode"); + fixed(value.asset_scope, SCOPE, "asset scope"); + fields(value.products, c.PRODUCTS, "products"); + const products = {}, binaries = new Set(); + for (const product of c.PRODUCTS) { + const p = value.products[product], assets = {}; + fields(p, ["tag", "manifest_sha256", "checksums_sha256", "assets"], "product"); + const tag = (product === "agentplugins" ? "agentplugins-v" : "v") + id.versions[product]; + fixed(p.tag, tag, "product tag"); + fields(p.assets, c.TARGETS, "assets"); + for (const target of c.TARGETS) { + const a = p.assets[target]; + fields(a, ["file", "sha256", "size", "binary"], "asset"); + const file = c.assetName(product, id.versions[product], target); + const binary = pin(a.binary, c.executableName(product, target), "binary"); + const outer = pin({ file: a.file, sha256: a.sha256, size: a.size }, file, "asset"); + if (product === "agentplugins" && (outer.sha256 !== binary.sha256 || outer.size !== binary.size)) { + fail("raw agent outer/inner pins disagree"); + } + if (binaries.has(binary.sha256)) fail("twelve distinct binary hashes required"); + binaries.add(binary.sha256); + assets[target] = { file: outer.file, sha256: outer.sha256, size: outer.size, binary }; + } + products[product] = { tag, manifest_sha256: hash(p.manifest_sha256, "manifest"), + checksums_sha256: hash(p.checksums_sha256, "checksums"), assets }; + } + const prep = value.preparation, producer = value.producer; + fields(prep, ["sha256", "artifact"], "preparation"); + fields(prep.artifact, ["run_id", "run_attempt", "artifact_id", "artifact_sha256"], "preparation artifact"); + const a = prep.artifact; + fields(producer, ["workflow", "source", "run_id", "run_attempt"], "producer"); + return { schema: INPUT_SCHEMA, identity: id, authoring_mode: MODE, asset_scope: SCOPE, + candidate_sha256: hash(value.candidate_sha256, "candidate"), pair_marker_sha256: hash(value.pair_marker_sha256, "pair marker"), + products, preparation: { sha256: hash(prep.sha256, "preparation"), artifact: { + run_id: positive(a.run_id, Number.MAX_SAFE_INTEGER, "preparation run"), + run_attempt: positive(a.run_attempt, 1000, "preparation attempt"), + artifact_id: positive(a.artifact_id, Number.MAX_SAFE_INTEGER, "preparation artifact ID"), + artifact_sha256: hash(a.artifact_sha256, "preparation artifact") } }, + producer: { workflow: fixed(producer.workflow, WORKFLOW, "producer workflow"), + source: fixed(producer.source, id.commit, "producer source"), + run_id: positive(producer.run_id, Number.MAX_SAFE_INTEGER, "producer run"), + run_attempt: positive(producer.run_attempt, 1000, "producer attempt") } }; +} + +function bytes(value, maximum) { + if (!Buffer.isBuffer(value) || value.length === 0 || value.length > maximum) fail("nonempty bounded Buffer required"); + return value; +} +function encoded(value, maximum) { + return bytes(c.encode(value), maximum); +} +function parsed(body, maximum) { + bytes(body, maximum); + const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body); + // Both fixed contracts have objects only, at most six levels deep. Bound + // nesting before JSON.parse; canonical re-encoding rejects duplicate keys, + // escapes, alternate number spellings, whitespace, BOM and trailing data. + 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 === "[") fail("arrays are outside the fixed contracts"); + else if (ch === "{" && ++depth > 6) fail("object depth limit"); + else if (ch === "}") depth--; + } + return JSON.parse(text); +} + +/** Encode a complete I object in fixed order. Structural consistency only. */ +function encodeInputs(value) { return encoded(inputs(value), MAX_INPUT_BYTES); } + +/** Decode canonical I bytes to a fresh data object. Structural consistency only. */ +function decodeInputs(body) { + const value = inputs(parsed(body, MAX_INPUT_BYTES)); + if (!body.equals(encoded(value, MAX_INPUT_BYTES))) fail("noncanonical input bytes"); + return value; +} + +function descriptor(value, inputBytes, product) { + if (!c.PRODUCTS.includes(product)) fail("explicit selected product required"); + const input = decodeInputs(inputBytes); + fields(value, ["schema", "product", "npm_package", "identity", "authoring_mode", "asset_scope", + "candidate_sha256", "release_manifest_sha256", "input_binding"], "descriptor"); + fixed(value.schema, DESCRIPTOR_SCHEMA, "descriptor schema"); + fixed(value.product, product, "selected product"); + fixed(value.npm_package, PACKAGES[product], "npm package"); + const id = identity(value.identity); + if (!c.encode(id).equals(c.encode(input.identity))) fail("descriptor/input identity mismatch"); + fields(value.input_binding, ["file", "sha256"], "input binding"); + return { schema: DESCRIPTOR_SCHEMA, product, npm_package: PACKAGES[product], identity: id, + authoring_mode: fixed(value.authoring_mode, MODE, "descriptor mode"), + asset_scope: fixed(value.asset_scope, SCOPE, "descriptor scope"), + candidate_sha256: fixed(value.candidate_sha256, input.candidate_sha256, "descriptor candidate"), + release_manifest_sha256: fixed(value.release_manifest_sha256, input.products[product].manifest_sha256, "descriptor manifest"), + input_binding: { file: fixed(value.input_binding.file, INPUT_FILE, "input binding file"), + sha256: fixed(value.input_binding.sha256, c.digest(inputBytes), "exact input bytes digest") } }; +} + +/** Encode complete v2 data against exact canonical I bytes and a required product. + * Structural consistency only; this does not generate or qualify a package. */ +function encodeDescriptor(value, inputBytes, product) { + return encoded(descriptor(value, inputBytes, product), MAX_DESCRIPTOR_BYTES); +} + +/** Decode v2 with the same required I bytes/product. Structural consistency only. + * No authentication, signing, acquisition, eligibility or acceptance is implied. */ +function decodeDescriptor(body, inputBytes, product) { + const value = descriptor(parsed(body, MAX_DESCRIPTOR_BYTES), inputBytes, product); + if (!body.equals(encoded(value, MAX_DESCRIPTOR_BYTES))) fail("noncanonical descriptor bytes"); + return value; +} + +// Fixed schema-3 projection, shared with staging. This reconstructs consistency, +// not custody of the absent original candidate, preparation or pair marker. +function projectionBytes(input, product) { + if (!c.PRODUCTS.includes(product)) fail("explicit selected product required"); + const id = input.identity; + const manifest = c.encode({ schema_version: 3, status: "CANDIDATE", product, repository: id.repository, + tag: input.products[product].tag, version: id.versions[product], commit: id.commit, engine_revision: id.engine_revision, + versions: id.versions, candidate_sha256: input.candidate_sha256, authoring_mode: input.authoring_mode, + asset_scope: input.asset_scope, assets: input.products[product].assets, + release_eligible: false, platform_acceptance: false, attested: false }); + const checksums = Buffer.from([...Object.values(input.products[product].assets).map(a => `${a.sha256} ${a.file}`), + `${c.digest(manifest)} release-manifest.json`].join("\n") + "\n"); + return { manifest, checksums }; +} + +module.exports = Object.freeze({ encodeInputs, decodeInputs, encodeDescriptor, decodeDescriptor, projectionBytes, INPUT_SCHEMA, DESCRIPTOR_SCHEMA, INPUT_FILE, MODE, SCOPE, WORKFLOW, PACKAGES, MAX_INPUT_BYTES, MAX_DESCRIPTOR_BYTES, MAX_NATIVE_BYTES, + checks: Object.freeze({ fail, fixed, hash, positive, fields }) }); diff --git a/npm/agentplugins/lib/public-authoring-input.js b/npm/agentplugins/lib/public-authoring-input.js new file mode 100644 index 00000000..00055d01 --- /dev/null +++ b/npm/agentplugins/lib/public-authoring-input.js @@ -0,0 +1,121 @@ +"use strict"; + +// Bounded observations in an owned, quiescent namespace. These path checks do +// not exclude hostile same-UID writers, mount replacement or every host race. +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); +const digest = bytes => crypto.createHash("sha256").update(bytes).digest("hex"); +const fail = message => { throw new Error(`public snapshot: ${message}`); }; +const directoryKeys = ["dev", "ino", "mode", "uid", "gid"]; +const fileKeys = [...directoryKeys, "size", "nlink", "mtimeMs", "ctimeMs"]; +const same = (a, b, keys) => keys.every(key => a[key] === b[key]); +const within = (a, b) => { const r = path.relative(b, a); return r === "" || (!r.startsWith(`..${path.sep}`) && r !== ".." && !path.isAbsolute(r)); }; +const overlaps = (a, b) => within(a, b) || within(b, a); + +function canonical(file) { + if (typeof file !== "string" || !file || file.includes("\0") || !path.isAbsolute(file) || + path.resolve(file) !== file || file.split(/[\\/]/).some(p => p === "." || p === "..")) fail("canonical absolute path required"); + return file; +} +function cancelled(signal) { + if (signal && signal.aborted) { const error = new Error("public snapshot cancelled"); error.code = "ABORT_ERR"; throw error; } +} +function uid() { + if (typeof process.geteuid !== "function" || !fs.constants.O_NOFOLLOW) fail("host ownership/no-follow support required"); + return process.geteuid(); +} +function directory(name, owner) { + const stat = fs.lstatSync(name); + if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(name) !== name || + (stat.uid !== 0 && stat.uid !== owner) || ((stat.mode & 0o022) && !(stat.mode & 0o1000))) fail("unsafe ancestor"); + return stat; +} +function ancestors(name, owner, missing = false) { + canonical(name); + let current = path.parse(name).root; + const pins = [[current, directory(current, owner)]]; + for (const part of name.slice(current.length).split(path.sep).filter(Boolean)) { + current = path.join(current, part); + try { pins.push([current, directory(current, owner)]); } + catch (error) { if (missing && error.code === "ENOENT") break; throw error; } + } + return pins; +} +function recheckDirectories(pins, owner) { + for (const [name, pin] of pins) if (!same(directory(name, owner), pin, directoryKeys)) fail("ancestor changed"); +} + +// Validate the entire configured cache boundary without creating it. Existing +// ancestor aliases are rejected even if the final cache directory is absent. +function publicPlacement(packageRoot, cacheRoot) { + canonical(packageRoot); canonical(cacheRoot); + if (overlaps(packageRoot, cacheRoot)) fail("cache overlaps package"); + const owner = uid(), packages = ancestors(packageRoot, owner), cache = ancestors(cacheRoot, owner, true); + const p = packages[packages.length - 1], q = cache[cache.length - 1]; + if (q[0] === cacheRoot && p[1].dev === q[1].dev && p[1].ino === q[1].ino) fail("cache aliases package"); + return { recheck() { recheckDirectories(packages, owner); recheckDirectories(cache, owner); } }; +} + +function snapshotPublicFile(file, { kind, maximum, exactSize, protectedRoots = [], signal } = {}) { + cancelled(signal); canonical(file); + if (!["metadata", "local asset"].includes(kind) || !Number.isSafeInteger(maximum) || maximum <= 0 || + maximum > (kind === "metadata" ? 1024 * 1024 : 128 * 1024 * 1024) || + (exactSize !== undefined && (!Number.isSafeInteger(exactSize) || exactSize <= 0 || exactSize > maximum))) fail("fixed bounded policy required"); + const owner = uid(), parent = path.dirname(file), parents = ancestors(parent, owner); + const extra = []; + if (kind === "local asset") { + const direct = parents[parents.length - 1][1]; + if (direct.uid !== owner || (direct.mode & 0o7777) !== 0o700) fail("owned private custody parent required"); + for (const root of protectedRoots) { + canonical(root); + if (overlaps(parent, root)) fail("custody overlaps protected root"); + const pins = ancestors(root, owner, true), last = pins[pins.length - 1]; + if (last[0] === root && direct.dev === last[1].dev && direct.ino === last[1].ino) fail("custody aliases protected root"); + extra.push(...pins); + } + } + const before = fs.lstatSync(file); + const regular = stat => stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && stat.size > 0 && stat.size <= maximum && + (exactSize === undefined || stat.size === exactSize) && + (kind !== "local asset" || (stat.uid === owner && !(stat.mode & 0o7022))); + if (!regular(before)) fail("regular nonempty bounded single-link file required"); + let fd, closed = false; + function close() { if (fd !== undefined && !closed) { closed = true; fs.closeSync(fd); } } + function identities() { + cancelled(signal); + if (closed) fail("closed descriptor"); + recheckDirectories([...parents, ...extra], owner); + const named = fs.lstatSync(file), opened = fs.fstatSync(fd); + if (!regular(named) || !regular(opened) || !same(before, named, fileKeys) || !same(before, opened, fileKeys)) fail("file identity or metadata changed"); + } + function read() { + // Allocate only the admitted initial size plus one overflow detection byte; + // positional reads also make repeated rechecks independent of fd offsets. + const body = Buffer.alloc(before.size + 1); + let offset = 0; + while (offset < body.length) { + cancelled(signal); + const count = fs.readSync(fd, body, offset, Math.min(64 * 1024, body.length - offset), offset); + if (!Number.isSafeInteger(count) || count < 0 || count > Math.min(64 * 1024, body.length - offset)) fail("invalid bounded read result"); + if (count === 0) break; + offset += count; + } + if (offset !== before.size) fail("short read or growth"); + return body.subarray(0, offset); + } + try { + fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); + identities(); + const bytes = read(); + identities(); + const pin = digest(bytes); + return { get bytes() { return Buffer.from(bytes); }, close, + recheck() { identities(); const current = read(); identities(); if (digest(current) !== pin) fail("file contents changed"); } }; + } catch (primary) { + try { close(); } catch (error) { throw new AggregateError([primary, error], "public snapshot read and close failed"); } + throw primary; + } +} + +module.exports = { snapshotPublicFile, publicPlacement }; diff --git a/npm/agentplugins/lib/public-authoring.js b/npm/agentplugins/lib/public-authoring.js index 9262a5e9..f915dc7a 100644 --- a/npm/agentplugins/lib/public-authoring.js +++ b/npm/agentplugins/lib/public-authoring.js @@ -3,6 +3,7 @@ // Package consistency, not a signature verifier. The future protected public // stager must authenticate the signed pair before emitting a binding. Preparation // emits null. No runtime environment/CLI option can qualify a preparation pack. +const fs = require("node:fs"); const fsp = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); @@ -48,24 +49,32 @@ function qualification(descriptor) { } } -function loadRelease(product, packageRoot, target) { +function validateRelease(product, packageRoot, target, read = json, inputBytes) { if (!Object.hasOwn(PACKAGES, product) || !c.TARGETS.includes(target)) throw new Error("unsupported public product/target"); c.safeDirectory(packageRoot); - const { value: d, bytes: descriptorBytes } = json(path.join(packageRoot, "public-release.json")); + const { value: d, bytes: descriptorBytes } = read(path.join(packageRoot, "public-release.json")); + const v2 = inputBytes !== undefined; + const contract = v2 ? require("./public-authoring-contract") : null; + if (v2) contract.decodeDescriptor(descriptorBytes, inputBytes, product); c.keys(d, ["schema", "product", "npm_package", "identity", "authoring_mode", "asset_scope", - "candidate_sha256", "release_manifest_sha256", "qualification"], "public release"); + "candidate_sha256", "release_manifest_sha256", v2 ? "input_binding" : "qualification"], "public release"); c.identity(d.identity); - if (d.schema !== SCHEMA || d.product !== product || d.npm_package !== PACKAGES[product] || + if (d.schema !== (v2 ? contract.DESCRIPTOR_SCHEMA : SCHEMA) || d.product !== product || d.npm_package !== PACKAGES[product] || /^0{40}$/.test(d.identity.commit) || d.identity.versions["plugin-kit-ai"] !== "2.0.0" || d.authoring_mode !== MODE || d.asset_scope !== SCOPE || !hash(d.candidate_sha256) || !hash(d.release_manifest_sha256)) { throw new Error("public release identity mismatch"); } - const { value: pkg } = json(path.join(packageRoot, "package.json")); + const { value: pkg } = read(path.join(packageRoot, "package.json")); c.keys(pkg, ["name", "version", "description", "license", "homepage", "repository", "bugs", "keywords", "engines", "publishConfig", "files", "bin", "scripts", "private", ...(product === "agentplugins" ? ["os", "cpu"] : [])], "public package"); const closure = ["LICENSE", "README.md", "package.json", `bin/${product}.js`, "bin/package.json", "lib/package.json", "lib/platform.js", "lib/verifier.js", "lib/public-authoring.js", "scripts/package.json", "scripts/dual-authoring-candidate.js", product === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js", "public-release.json", "release-manifest.json"]; + if (v2) closure.push("native-inputs.json", "lib/public-authoring-contract.js", "lib/public-authoring-input.js"); + if (v2 && (pkg.private !== false || (product === "agentplugins" && + (!equal(pkg.os, ["darwin", "linux", "win32"]) || !equal(pkg.cpu, ["x64", "arm64"]))))) { + throw new Error("public v2 package private/OS/CPU mismatch"); + } if (typeof pkg.private !== "boolean" || !Array.isArray(pkg.files) || !equal(pkg.files, closure.sort())) { throw new Error("public package closure mismatch"); } @@ -75,7 +84,7 @@ function loadRelease(product, packageRoot, target) { if (pkg.name !== PACKAGES[product] || pkg.version !== d.identity.versions[product] || pkg.bin[product] !== `bin/${product}.js` || pkg.engines.node !== (product === "agentplugins" ? ">=22" : ">=18") || !equal(pkg.scripts, scripts)) throw new Error("public npm package binding mismatch"); - const { value: m, bytes } = json(path.join(packageRoot, "release-manifest.json")); + const { value: m, bytes } = read(path.join(packageRoot, "release-manifest.json")); c.keys(m, ["schema_version", "status", "product", "repository", "tag", "version", "commit", "engine_revision", "versions", "candidate_sha256", "authoring_mode", "asset_scope", "assets", "release_eligible", "platform_acceptance", "attested"], "producer projection"); if (c.digest(bytes) !== d.release_manifest_sha256 || m.schema_version !== 3 || m.status !== "CANDIDATE" || @@ -99,19 +108,101 @@ function loadRelease(product, packageRoot, target) { } seen.add(a.binary.sha256); } - qualification(d); // Always before effects, including warm cache lookup. + if (v2) { + const input = contract.decodeInputs(inputBytes); + for (const peer of c.PRODUCTS) { + const projection = contract.projectionBytes(input, peer), pins = input.products[peer]; + if (c.digest(projection.manifest) !== pins.manifest_sha256 || c.digest(projection.checksums) !== pins.checksums_sha256 || + (peer === product && !bytes.equals(projection.manifest))) throw new Error("public pair projection mismatch"); + } + } else qualification(d); // Always before effects, including warm cache lookup. return { descriptor: d, manifest: m, asset: m.assets[target], version: pkg.version, binding: c.digest(descriptorBytes), tag: m.tag }; } +// Bounded schema dispatch lives in the existing shipped runtime so v1 packages +// do not import either new helper. Actual v1 validation retains its old reader. +function descriptorSchema(file) { + const named = fs.lstatSync(file); + if (!named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.size <= 0 || named.size > 1024 * 1024) { + throw new Error("invalid bounded public descriptor"); + } + const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); + let primary, schema; + try { + const bytes = Buffer.alloc(named.size + 1); + let offset = 0, count; + while (offset < bytes.length) { + const length = Math.min(65536, bytes.length - offset); + count = fs.readSync(fd, bytes, offset, length, offset); + if (!Number.isSafeInteger(count) || count < 0 || count > length) throw new Error("invalid bounded descriptor read"); + if (count === 0) break; + offset += count; + } + const opened = fs.fstatSync(fd); + if (offset !== named.size || ["dev", "ino", "size", "mode", "nlink", "uid", "gid", "mtimeMs", "ctimeMs"].some(k => named[k] !== opened[k])) { + throw new Error("public descriptor changed"); + } + schema = JSON.parse(bytes.subarray(0, offset)).schema; + } catch (error) { primary = error; } + try { fs.closeSync(fd); } catch (error) { throw v.failures(primary, [error]); } + if (primary) throw primary; + return schema; +} + +function snapshotRelease(product, packageRoot, target, signal) { + if (!Object.hasOwn(PACKAGES, product) || !c.TARGETS.includes(target)) throw new Error("unsupported public product/target"); + c.safeDirectory(packageRoot); + const schema = descriptorSchema(path.join(packageRoot, "public-release.json")); + if (schema === SCHEMA) { + const release = validateRelease(product, packageRoot, target); + return { release, close() {}, recheck() { + if (validateRelease(product, packageRoot, target).binding !== release.binding) throw new Error("public release changed during acquisition"); + } }; + } + if (schema !== "dual-authoring-public-npm/v2") throw new Error("unsupported public release schema"); + const { snapshotPublicFile } = require("./public-authoring-input"); + const contract = require("./public-authoring-contract"), snapshots = new Map(); + function close() { + const errors = []; + for (const snapshot of snapshots.values()) try { snapshot.close(); } catch (error) { errors.push(error); } + if (errors.length) throw v.failures(null, errors); + } + try { + for (const [name, maximum] of [["public-release.json", contract.MAX_DESCRIPTOR_BYTES], [contract.INPUT_FILE, contract.MAX_INPUT_BYTES], + ["release-manifest.json", 1024 * 1024], ["package.json", 1024 * 1024]]) { + snapshots.set(path.join(packageRoot, name), snapshotPublicFile(path.join(packageRoot, name), { kind: "metadata", maximum, signal })); + } + const read = file => { + const bytes = snapshots.get(file).bytes, value = JSON.parse(bytes); + if (!bytes.equals(c.encode(value))) throw new Error("noncanonical public JSON"); + return { bytes, value }; + }; + const release = validateRelease(product, packageRoot, target, read, snapshots.get(path.join(packageRoot, contract.INPUT_FILE)).bytes); + const recheck = () => { for (const snapshot of snapshots.values()) snapshot.recheck(); }; + recheck(); + return { release, close, recheck }; + } catch (primary) { + try { close(); } catch (error) { throw v.failures(primary, [error]); } + throw primary; + } +} + +function loadRelease(product, packageRoot, target) { + const snapshot = snapshotRelease(product, packageRoot, target); + snapshot.close(); + return snapshot.release; +} +const releaseNamespace = release => release.descriptor.schema === SCHEMA ? "public-authoring-v1" : "public-authoring-v2"; + function cachePath(root, product, target, release) { - return path.join(root, "public-authoring-v1", MODE, release.descriptor.identity.commit, + return path.join(root, releaseNamespace(release), MODE, release.descriptor.identity.commit, release.descriptor.candidate_sha256, product, release.version, target, release.asset.binary.sha256, release.asset.binary.file); } // Create only missing directories; never chmod an existing historical cache. // PR167 validates every ancestor before the next child can be created. -async function namespace(root, io) { +async function namespace(root, io, name = "public-authoring-v1") { if (typeof root !== "string" || !path.isAbsolute(root) || path.resolve(root) !== root || root.includes("\0") || root.split(/[\\/]/).includes("..")) throw new Error("unsafe public cache root"); let current = path.parse(root).root; @@ -127,7 +218,7 @@ async function namespace(root, io) { throw new Error("public cache requires safe ancestors"); } } - const owned = path.join(root, "public-authoring-v1"); + const owned = path.join(root, name); try { await io.mkdir(owned, { mode: 0o700 }); } catch (e) { if (e.code !== "EEXIST") throw e; } await v.privateDirectory(owned, owned, io); @@ -145,12 +236,50 @@ async function ensureBinary(product, options = {}) { if (options.target !== undefined && options.target !== target) throw new Error("unexpected public native target"); const packageRoot = options.packageRoot || path.resolve(__dirname, ".."); v.cancelled(options.signal); - const release = loadRelease(product, packageRoot, target); + const snapshot = snapshotRelease(product, packageRoot, target, options.signal); + let assetSnapshot, primary, result; + try { + const release = snapshot.release; + const root = options.cacheRoot || path.join(os.homedir(), ".cache", "universal-agent-plugins"); + let placement, localBinary; + if (release.descriptor.schema !== SCHEMA) { + const input = require("./public-authoring-input"); + placement = input.publicPlacement(packageRoot, root); + const environment = options.environment || process.env; + if (Object.hasOwn(environment, "UAP_PUBLIC_AUTHORING_ASSET_FILE")) { + const file = environment.UAP_PUBLIC_AUTHORING_ASSET_FILE; + if (typeof file !== "string" || !file || path.basename(file) !== release.asset.file) throw new Error("invalid public asset locator basename"); + assetSnapshot = input.snapshotPublicFile(file, { kind: "local asset", maximum: release.asset.size, + exactSize: release.asset.size, protectedRoots: [packageRoot, root], signal: options.signal }); + localBinary = checkedBinary(product, assetSnapshot.bytes, release.asset); + } + } + const recheck = () => { snapshot.recheck(); if (placement) placement.recheck(); if (assetSnapshot) assetSnapshot.recheck(); }; + if (release.descriptor.schema !== SCHEMA) recheck(); + result = await acquireBinary(product, options, platform, target, packageRoot, release, localBinary, recheck); + // Acquisition finalization awaits cleanup and unlock while snapshots remain held. + v.cancelled(options.signal); + recheck(); + } catch (error) { primary = error; } + const errors = []; + for (const held of [assetSnapshot, snapshot]) if (held) try { held.close(); } catch (error) { errors.push(error); } + if (primary || errors.length) throw v.failures(primary, errors); + return result; +} + +function checkedBinary(product, bytes, asset) { + if (bytes.length !== asset.size || c.digest(bytes) !== asset.sha256) throw new Error("outer public asset mismatch"); + const binary = product === "plugin-kit-ai" ? c.unpack(bytes, asset.binary.file) : bytes; + if (!Buffer.isBuffer(binary) || binary.length !== asset.binary.size || c.digest(binary) !== asset.binary.sha256) throw new Error("inner public binary mismatch"); + return Buffer.from(binary); +} + +async function acquireBinary(product, options, platform, target, packageRoot, release, localBinary, recheck) { // No historical repository/version/cache override is consulted on this path. const root = options.cacheRoot || path.join(os.homedir(), ".cache", "universal-agent-plugins"); if (inside(root, packageRoot) || inside(packageRoot, root)) throw new Error("public cache overlaps package"); const io = options.io || fsp; - const owned = await namespace(root, io); + const owned = await namespace(root, io, releaseNamespace(release)); const binaryPath = cachePath(root, product, target, release); await v.privateDirectory(owned, path.dirname(binaryPath), io); const lockRoot = path.join(owned, ".locks"); @@ -171,24 +300,30 @@ async function ensureBinary(product, options = {}) { await unchangedDirectories(); const hit = await v.strictCachedBinary(binaryPath, release.asset.binary, settings); if (!hit) { - temporary = await io.mkdtemp(path.join(owned, ".download-")); - temporaryStat = await io.lstat(temporary); - await v.privateDirectory(owned, temporary, io); - const file = path.join(temporary, "asset"); - await v.downloadFile(`https://github.com/${c.REPOSITORY}/releases/download/${release.tag}/${release.asset.file}`, - file, release.asset, { request: options.request, signal: options.signal, onOpen: stat => { downloadStat = stat; } }); - v.cancelled(options.signal); - const bytes = c.readFile(file); - if (bytes.length !== release.asset.size || c.digest(bytes) !== release.asset.sha256) throw new Error("outer public asset mismatch"); - const binary = product === "plugin-kit-ai" ? c.unpack(bytes, release.asset.binary.file) : bytes; - if (binary.length !== release.asset.binary.size || c.digest(binary) !== release.asset.binary.sha256) throw new Error("inner public binary mismatch"); + let binary = localBinary; + if (!binary) { + temporary = await io.mkdtemp(path.join(owned, ".download-")); + temporaryStat = await io.lstat(temporary); + await v.privateDirectory(owned, temporary, io); + const file = path.join(temporary, "asset"); + await v.downloadFile(`https://github.com/${c.REPOSITORY}/releases/download/${release.tag}/${release.asset.file}`, + file, release.asset, { request: options.request, signal: options.signal, onOpen: stat => { downloadStat = stat; } }); + v.cancelled(options.signal); + const bytes = c.readFile(file); + binary = checkedBinary(product, bytes, release.asset); + } + if (release.descriptor.schema !== SCHEMA) recheck(); await unchangedDirectories(); await v.commitVerifiedBinary(binary, binaryPath, release.asset.binary, settings); } v.cancelled(options.signal); // A concurrent metadata edit cannot turn this operation into a warm bypass. - if (loadRelease(product, packageRoot, target).binding !== release.binding) throw new Error("public release changed during acquisition"); + recheck(); await unchangedDirectories(); + if (release.descriptor.schema !== SCHEMA && !await v.strictCachedBinary(binaryPath, release.asset.binary, settings)) { + throw new Error("final public binary verification failed"); + } + if (release.descriptor.schema !== SCHEMA) recheck(); result = { binaryPath, version: release.version, tag: release.tag, product, cacheHit: hit, publicAuthoring: true, repository: c.REPOSITORY }; } catch (e) { primary = e; } diff --git a/npm/agentplugins/scripts/authoring-native-inputs.js b/npm/agentplugins/scripts/authoring-native-inputs.js new file mode 100644 index 00000000..64ae5e3a --- /dev/null +++ b/npm/agentplugins/scripts/authoring-native-inputs.js @@ -0,0 +1,247 @@ +"use strict"; + +// Codecs and subject enumeration are structural only. The bounded producer +// prepares unsigned I; only readInputs invokes the fixed signature boundary. +// Neither operation grants qualification, publication or execution permission. +const c = require("./dual-authoring-candidate"); +const fs = require("node:fs"); +const path = require("node:path"); +const { isDeepStrictEqual: equal } = require("node:util"); + +const { encodeInputs, decodeInputs, encodeDescriptor, decodeDescriptor, INPUT_SCHEMA, DESCRIPTOR_SCHEMA, INPUT_FILE, MODE, SCOPE, WORKFLOW, PACKAGES, MAX_INPUT_BYTES, MAX_DESCRIPTOR_BYTES, MAX_NATIVE_BYTES, checks: { fail, fixed, hash, positive, fields } } = require("../lib/public-authoring-contract"); + +const agree = (a, b, label) => { if (!equal(a, b)) throw new Error(`C1 provenance ${label} mismatch`); }; +function artifact(value) { + fields(value, ["run_id", "run_attempt", "artifact_id", "artifact_sha256"], "input artifact"); + return { run_id: positive(value.run_id, Number.MAX_SAFE_INTEGER, "input run"), + run_attempt: positive(value.run_attempt, 1000, "input attempt"), + artifact_id: positive(value.artifact_id, Number.MAX_SAFE_INTEGER, "input artifact ID"), + artifact_sha256: hash(value.artifact_sha256, "input artifact") }; +} +function operationOptions(value, reading) { + fields(value, ["input", "selected", "workflow_sha", "scratch", ...(reading ? ["artifact"] : [])], "provenance options"); + const input = decodeInputs(value.input), body = Buffer.from(value.input); + fields(value.selected, ["tag", "ref", "source", "versions"], "selected inputs"); + fields(value.selected.versions, c.PRODUCTS, "selected versions"); + const selected = { tag: input.products.agentplugins.tag, ref: `refs/tags/${input.products.agentplugins.tag}`, + source: input.identity.commit, versions: input.identity.versions }; + agree(value.selected, selected, "selected source/ref/versions"); + fixed(value.workflow_sha, input.identity.commit, "integrated workflow source F"); + if (Object.values(input.identity.versions).some(v => v.length > 32)) fail("bounded provider versions required"); + if (input.producer.run_id === input.preparation.artifact.run_id) fail("separate provenance and preparation runs required"); + const pin = reading ? artifact(value.artifact) : null; + if (reading) { + agree([pin.run_id, pin.run_attempt], [input.producer.run_id, input.producer.run_attempt], "I artifact producer attempt"); + if (pin.artifact_id === input.preparation.artifact.artifact_id) fail("I and preparation artifacts must differ"); + } + c.safeDirectory(value.scratch); + return { body, input, selected, workflow_sha: value.workflow_sha, scratch: value.scratch, artifact: pin }; +} +function projectionPins(input) { + return { identity: input.identity, candidate_sha256: input.candidate_sha256, pair_marker_sha256: input.pair_marker_sha256, + products: Object.fromEntries(c.PRODUCTS.map(p => [p, { + manifest_sha256: input.products[p].manifest_sha256, checksums_sha256: input.products[p].checksums_sha256 }])) }; +} +function preparationSnapshot(root, body) { + const input = decodeInputs(body); + const verified = require("./authoring-release").verifyProjectedPair(root, projectionPins(input)); + agree(verified.subjects.length, 18, "original subject count"); + for (const p of c.PRODUCTS) agree(verified.manifest.products[p].assets, input.products[p].assets, "outer/inner pins"); + const prepared = require("./authoring-promotion").readInputPreparation(root, body); + return { subjects: verified.subjects, preparation: prepared.preparation, + metadata_sha256: c.digest(c.readFile(path.join(root, "candidate-identity.json"), MAX_INPUT_BYTES)) }; +} + +/** Structural enumeration of exactly 18 original subjects plus exact I bytes. + * Keep rows, including both manifest/checksum basenames; this is NOT admission. */ +function inputSubjects(root, inputBytes) { + decodeInputs(inputBytes); + const snapshot = preparationSnapshot(root, inputBytes); + const file = path.join(root, INPUT_FILE); + agree(c.readFile(file, MAX_INPUT_BYTES), inputBytes, "retained I bytes"); + return [...snapshot.subjects, { file, sha256: c.digest(inputBytes) }]; +} + +/** Acquire the exact completed preparation and prepare unsigned I for the + * future protected provenance job. No signatures, OIDC or upload are performed. + * Its returned nineteen rows are signing candidates, never authenticated proof. */ +function produceInputs(value) { + const o = operationOptions(value, false), p = require("./authoring-promotion"); + const pin = o.input.preparation.artifact; + p.checkInputTags(o.body, o.scratch); + const before = p.inspectArtifact(pin, WORKFLOW, o.input.identity.commit, o.scratch); + const prepared = p.acquireInputPreparation(o.body, o.scratch); + const snapshot = preparationSnapshot(prepared.root, o.body); + p.checkInputTags(o.body, o.scratch); + agree(p.inspectArtifact(pin, WORKFLOW, o.input.identity.commit, o.scratch), before, "preparation provider changed"); + agree(preparationSnapshot(prepared.root, o.body), snapshot, "preparation changed before I completion"); + agree(operationOptions(value, false), o, "caller inputs changed before I completion"); + const file = path.join(prepared.root, INPUT_FILE); + fs.writeFileSync(file, o.body, { flag: "wx", mode: 0o444 }); + const subjects = inputSubjects(prepared.root, o.body); + agree(preparationSnapshot(prepared.root, o.body), snapshot, "preparation changed at I completion"); + agree(operationOptions(value, false), o, "caller inputs changed at I completion"); + return { root: prepared.root, input: o.input, subjects }; +} + +/** Source wiring for a completed provenance artifact. Positive integrated use + * is UNAVAILABLE: the existing checked reader supports native/preparation only. + * A separately accepted 21-entry input-provenance interface is required. Never + * substitute preparation kind, append files, or fall back to another extractor. */ +function readInputs(value) { + const o = operationOptions(value, true), p = require("./authoring-promotion"); + p.checkInputTags(o.body, o.scratch); + const before = p.inspectArtifact(o.artifact, WORKFLOW, o.input.identity.commit, o.scratch); + const work = fs.mkdtempSync(path.join(o.scratch, "input-provenance-")); + const file = p.acquireArtifact(o.artifact, WORKFLOW, o.input.identity.commit, work); + const files = ["preparation-run.json", "candidate-identity.json", "candidate/candidate.json", "pair-prepared.json", + ...c.PRODUCTS.flatMap(p => [...c.TARGETS.map(t => `${p}/${o.input.products[p].assets[t].file}`), + `${p}/release-manifest.json`, `${p}/checksums.txt`]), INPUT_FILE]; + const root = p.extractArtifact(file, o.artifact, "input-provenance", files, path.join(work, "frozen"), work); + agree(c.readFile(path.join(root, INPUT_FILE), MAX_INPUT_BYTES), o.body, "independently pinned I bytes"); + const snapshot = preparationSnapshot(root, o.body); + const prepPin = o.input.preparation.artifact; + const prepBefore = p.inspectArtifact(prepPin, WORKFLOW, o.input.identity.commit, o.scratch); + p.checkPreparationRef(prepPin, o.selected, o.scratch); + p.checkPreparationRef(o.artifact, o.selected, o.scratch); + const prepared = p.acquireInputPreparation(o.body, o.scratch); + const original = preparationSnapshot(prepared.root, o.body); + const relative = (s, base) => ({ ...s, + subjects: s.subjects.map(row => ({ file: path.relative(base, row.file), sha256: row.sha256 })) }); + agree(relative(snapshot, root), relative(original, prepared.root), "original preparation custody"); + const subjects = inputSubjects(root, o.body); + const multiset = subjects.map(s => ({ name: path.basename(s.file), digest: { sha256: s.sha256 } })); + for (const subject of subjects) p.verifySubject(subject.file, { + name: path.basename(subject.file), sha256: subject.sha256, source: o.input.identity.commit, + workflow_sha: o.workflow_sha, ref: o.selected.ref, run_id: o.input.producer.run_id, + run_attempt: o.input.producer.run_attempt, subjects: multiset + }, o.scratch); + p.checkInputTags(o.body, o.scratch); + agree(p.inspectArtifact(o.artifact, WORKFLOW, o.input.identity.commit, o.scratch), before, "I provider changed"); + agree(p.inspectArtifact(prepPin, WORKFLOW, o.input.identity.commit, o.scratch), prepBefore, "preparation provider changed"); + agree(preparationSnapshot(prepared.root, o.body), original, "original preparation changed"); + agree(preparationSnapshot(root, o.body), snapshot, "provenance preparation changed"); + agree(inputSubjects(root, o.body), subjects, "provenance subjects changed"); + p.checkPreparationRef(prepPin, o.selected, o.scratch); + p.checkPreparationRef(o.artifact, o.selected, o.scratch); + agree(operationOptions(value, true), o, "caller inputs changed during authentication"); + return { root, input: o.input, subjects }; +} + +/** Derive I only from the checked original preparation and independently bound + * current provenance caller. Both fixed jobs repeat this operation. */ +function produceInputsFromPreparation(value) { + fields(value, ["selected", "workflow_sha", "preparation", "repo", "scratch"], "preparation producer options"); + const p = require("./authoring-promotion"), packing = require("./stage-dual-authoring-npm"); + const selected = p.workflowSelection(value.selected, value.workflow_sha); + const pin = artifact(value.preparation); + for (const name of ["repo", "scratch"]) c.safeDirectory(value[name]); + const executing = path.resolve(__dirname, "../../.."); + for (const root of [value.repo, executing]) { + if (root === value.scratch || root.startsWith(value.scratch + path.sep) || value.scratch.startsWith(root + path.sep)) { + fail("provenance source/scratch overlap"); + } + } + const env = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: value.scratch, LC_ALL: "C.UTF-8" }; + const toolFiles = [process.execPath, "/usr/bin/git", "/usr/bin/gh", fs.realpathSync("/usr/bin/python3")]; + const tools = () => toolFiles.map(file => ({ file, sha256: c.digest(c.readFile(file)) })); + const toolPins = tools(), callerArgs = [...process.execArgv]; + const source = packing.blobs(value.repo, selected.source, env, "stage"); + const caller = p.inspectInputCaller(selected, value.workflow_sha, value.scratch); + if (caller.run_id === pin.run_id) fail("separate original preparation required"); + const before = p.inspectArtifact(pin, WORKFLOW, selected.source, value.scratch); + p.checkPreparationRef(pin, selected, value.scratch); + const work = fs.mkdtempSync(path.join(value.scratch, "derive-inputs-")); + const archive = p.acquireArtifact(pin, WORKFLOW, selected.source, work); + const files = ["preparation-run.json", "candidate-identity.json", "candidate/candidate.json", "pair-prepared.json", + ...c.PRODUCTS.flatMap(product => [...c.TARGETS.map(target => + `${product}/${c.assetName(product, selected.versions[product], target)}`), + `${product}/release-manifest.json`, `${product}/checksums.txt`])]; + const root = p.extractArtifact(archive, pin, "preparation", files, path.join(work, "frozen"), work); + const snapshot = () => files.map(file => ({ file, sha256: c.digest(c.readFile(path.join(root, file), MAX_NATIVE_BYTES)) })); + const originals = snapshot(); + const candidateBytes = c.readFile(path.join(root, "candidate/candidate.json"), MAX_INPUT_BYTES); + const candidate = JSON.parse(candidateBytes); + const id = identity({ repository: c.REPOSITORY, commit: selected.source, engine_revision: selected.source, versions: selected.versions }); + const tentative = { schema: INPUT_SCHEMA, identity: id, authoring_mode: MODE, asset_scope: SCOPE, + candidate_sha256: c.digest(candidateBytes), pair_marker_sha256: c.digest(c.readFile(path.join(root, "pair-prepared.json"), MAX_INPUT_BYTES)), + products: Object.fromEntries(c.PRODUCTS.map(product => [product, { + tag: (product === "agentplugins" ? "agentplugins-v" : "v") + selected.versions[product], + manifest_sha256: c.digest(c.readFile(path.join(root, product, "release-manifest.json"), MAX_INPUT_BYTES)), + checksums_sha256: c.digest(c.readFile(path.join(root, product, "checksums.txt"), MAX_INPUT_BYTES)), + assets: candidate.products?.[product]?.assets }])), + preparation: { sha256: c.digest(c.readFile(path.join(root, "preparation-run.json"), MAX_INPUT_BYTES)), artifact: pin }, + producer: caller }; + const body = encodeInputs(tentative); + preparationSnapshot(root, body); // candidate, projections, metadata and original receipt + const result = produceInputs({ input: body, selected, workflow_sha: value.workflow_sha, scratch: value.scratch }); + const retained = [...inputSubjects(result.root, body), ...["preparation-run.json", "candidate-identity.json"].map(file => + ({ file: path.join(result.root, file), sha256: c.digest(c.readFile(path.join(result.root, file), MAX_INPUT_BYTES)) }))]; + agree(retained.filter(row => path.basename(row.file) !== INPUT_FILE).map(row => + ({ file: path.relative(result.root, row.file), sha256: row.sha256 })).sort((a, b) => a.file.localeCompare(b.file)), + [...originals].sort((a, b) => a.file.localeCompare(b.file)), "derived versus revalidated original payload"); + agree(snapshot(), originals, "derivation root changed"); + agree(p.inspectArtifact(pin, WORKFLOW, selected.source, value.scratch), before, "original provider changed"); + p.checkPreparationRef(pin, selected, value.scratch); + agree(p.inspectInputCaller(selected, value.workflow_sha, value.scratch), caller, "current provenance caller"); + agree(packing.blobs(value.repo, selected.source, env, "stage"), source, "provenance source changed"); + agree(p.workflowSelection(value.selected, value.workflow_sha), selected, "provenance selection changed"); + agree(tools(), toolPins, "provenance tools changed"); + agree(process.execArgv, callerArgs, "provenance interpreter arguments"); + return result; +} + +// Fixed file transport for the two C1 modules. Options remain comparison pins; +// reading a file never turns its I bytes into authenticated input provenance. +function inputFileOptions(file, names) { + if (typeof file !== "string" || !path.isAbsolute(file) || path.resolve(file) !== file || /[\x00-\x1f]/.test(file)) { + fail("normalized absolute options file required"); + } + const raw = c.readFile(file, MAX_INPUT_BYTES); + const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw)); + fields(value, names, "CLI options"); + // Canonical options prevent duplicate keys and ambiguous transport spelling. + if (!raw.equals(c.encode(value))) fail("canonical options required"); + let input, inputFile; + if (names.includes("input_file")) { + inputFile = value.input_file; + if (typeof inputFile !== "string" || !path.isAbsolute(inputFile) || path.resolve(inputFile) !== inputFile || + /[\x00-\x1f]/.test(inputFile) || inputFile === file) fail("normalized distinct input_file required"); + input = c.readFile(inputFile, MAX_INPUT_BYTES); + decodeInputs(input); + delete value.input_file; + value.input = input; + } + const files = [file, ...(inputFile ? [inputFile] : [])]; + for (const root of [path.resolve(__dirname, "../../.."), value.repo, + ...[value.node, value.npm].filter(v => typeof v === "string").map(v => path.dirname(v))].filter(Boolean)) { + for (const candidate of files) if (candidate === root || candidate.startsWith(root + path.sep)) fail("CLI transport overlaps source/tools"); + } + return { value, recheck() { + agree(c.readFile(file, MAX_INPUT_BYTES), raw, "CLI options bytes"); + if (inputFile) agree(c.readFile(inputFile, MAX_INPUT_BYTES), input, "CLI comparison I bytes"); + } }; +} + +function main(args) { + if (args.length !== 2 || !["--produce-inputs", "--read-inputs"].includes(args[0])) { + fail("usage: authoring-native-inputs.js --produce-inputs|--read-inputs "); + } + const producing = args[0] === "--produce-inputs"; + const transport = inputFileOptions(args[1], producing ? + ["selected", "workflow_sha", "preparation", "repo", "scratch"] : + ["input_file", "selected", "workflow_sha", "scratch", "artifact"]); + const result = producing ? produceInputsFromPreparation(transport.value) : readInputs(transport.value); + transport.recheck(); + return result; +} + +module.exports = Object.freeze({ encodeInputs, decodeInputs, encodeDescriptor, decodeDescriptor, + produceInputs, readInputs, inputSubjects, produceInputsFromPreparation, inputFileOptions, main, + INPUT_SCHEMA, DESCRIPTOR_SCHEMA, INPUT_FILE, MODE, SCOPE, WORKFLOW, PACKAGES, + MAX_INPUT_BYTES, MAX_DESCRIPTOR_BYTES, MAX_NATIVE_BYTES }); + +if (require.main === module) { + try { process.stdout.write(c.encode(main(process.argv.slice(2)))); } + catch (error) { process.stderr.write(`C1 inputs: ${error.message}\n`); process.exitCode = 1; } +} diff --git a/npm/agentplugins/scripts/authoring-native-qualification.js b/npm/agentplugins/scripts/authoring-native-qualification.js index 8bf6feb8..d15fda09 100644 --- a/npm/agentplugins/scripts/authoring-native-qualification.js +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -1,8 +1,8 @@ #!/usr/bin/env node "use strict"; -// N1 observes one frozen pair. This is not provider admission or qualification -// signing. No promotion caller accepts these local records in this checkpoint. +// Fixed paired observations, separately acquired and replayed by N2 admission. +// Portable contracts do not provision observation or enable excluded execution. const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); @@ -17,7 +17,31 @@ const { lifecycleResult } = require("./platform-proof"); const SCHEMA = "authoring-frozen-native/v1"; const WORKFLOW = ".github/workflows/authoring-frozen-native.yml"; const PREPARATION = ".github/workflows/agentplugins-release.yml"; -const TARGET = "linux-amd64"; +const TARGET = "linux-amd64"; // Retain the accepted N1 default and encoding. +const HOSTS = Object.freeze(Object.fromEntries([ + ["linux-amd64", "linux", "x64", "x86_64", "amd64", true], + ["linux-arm64", "linux", "arm64", "aarch64", "arm64", true], + ["darwin-amd64", "darwin", "x64", "x86_64", "amd64", false], + ["darwin-arm64", "darwin", "arm64", "arm64", "arm64", false], + ["windows-amd64", "win32", "x64", "x86_64", "amd64", false], + ["windows-arm64", "win32", "arm64", "aarch64", "arm64", false] +].map(([target, platform, architecture, machine, goarch, executable]) => [target, Object.freeze({ + target, platform, architecture, machine, goos: target.split("-")[0], goarch, executable, + read_profile: `packageview-local-${target.split("-")[0]}-v1`, + client_root: "home/.codex", state_root: "state", project_root: "projects", + argv: "direct-array-no-shell", binary_suffix: platform === "win32" ? ".exe" : "", + modes: platform === "win32" ? "pending-native-acl-evidence" : "posix-exact", + cancellation: platform === "win32" ? "pending-owned-job-cleanup" : "owned-process-group-sigkill" +})]))); +function hostContract(target) { + assert.ok(Object.hasOwn(HOSTS, target), "unsupported native target contract"); + return HOSTS[target]; +} +function executableHost(target) { + const host = hostContract(target); + assert.ok(host.executable, "NATIVE_EXECUTION_PENDING: Windows and writable macOS remain excluded; portable source cannot admit a skipped observation"); + return host; +} const MODE = "release-cli-contract-v1"; const LIMIT = 1024 * 1024; const CASES = Object.freeze(["skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"]); @@ -99,8 +123,9 @@ function invocation(v, workflow, source) { sha(source, 40); positive(v.run_id); positive(v.run_attempt); assert.ok(v.run_attempt <= 1000); return v; } -function subject(manifest, product) { - return { product, target: TARGET, ...manifest.products[product].assets[TARGET] }; +function subject(manifest, product, target = TARGET) { + hostContract(target); + return { product, target, ...manifest.products[product].assets[target] }; } function preparationRecord(root, pins, producer) { const verified = verifyProjectedPair(root, pins); @@ -270,7 +295,7 @@ function envelope(row, expected) { exact(v.schema_version, 1); exact(v.result, expected.status === 0 ? "success" : "failure"); assert.ok(v.data && !Array.isArray(v.data)); return v; } -function authorResult(row, spec, product, identity) { +function authorResult(row, spec, product, identity, target = TARGET) { const r = envelope(row, spec); if (!r) return; if (!spec.author) { @@ -297,7 +322,7 @@ function authorResult(row, spec, product, identity) { else exact(d.committed, false); if (!spec.lane || /\/(existing)$/.test(spec.id) || spec.id === "installer-flag") return; assert.match(d.identity.tree_digest, /^sha256:[0-9a-f]{64}$/); - exact(d.identity.read_profile, "packageview-local-linux-v1"); + exact(d.identity.read_profile, hostContract(target).read_profile); exact(d.profiles, PROFILES, "exact conformance profiles"); exact(d.schema_ids, (spec.lane === "skill" ? [SCHEMAS[0].id] : SCHEMAS.map(x => x.id).sort()), "accepted package schema inventory"); exact(d.loadability.status, "pass"); @@ -446,29 +471,33 @@ function scanEvidence(r, source, project) { assert.match(security.report_digest, /^sha256:[0-9a-f]{64}$/); return security; } -function acquisition(state, bodies) { +function acquisition(state, bodies, target = TARGET) { + const platforms = target === TARGET ? [TARGET, "linux-amd64-musl"] : [target]; + const scannerRoots = platforms.map(t => `security/lintai/0.1.3/${t}`); const root = { path: ".", mode: 448, kind: "directory" }; assert.ok(Array.isArray(state.acquisition)); treeShape([root, ...state.acquisition]); for (const item of state.acquisition) { - const directory = /^(?:security(?:\/(?:lintai|assessments))?|security\/lintai\/0\.1\.3(?:\/linux-amd64(?:-musl)?)?)$/.test(item.path); - const file = /^(?:security\/lintai\/0\.1\.3\/linux-amd64(?:-musl)?\/lintai|security\/assessments\/[0-9a-f]{64}\.json|(?:directory|discovery)-v1-cache\.json)$/.test(item.path); + const directory = ["security", "security/lintai", "security/assessments", "security/lintai/0.1.3", ...scannerRoots].includes(item.path); + const file = scannerRoots.some(root => item.path === `${root}/lintai${hostContract(target).binary_suffix}`) || + /^(?:security\/assessments\/[0-9a-f]{64}\.json|(?:directory|discovery)-v1-cache\.json)$/.test(item.path); assert.ok(directory || file, "fixed acquisition paths"); exact(item.kind, directory ? "directory" : "file"); } return capturedBytes(state.acquisition, Object.fromEntries(state.acquisition.filter(x => x.kind === "file") .map(x => [x.path, bodies[x.sha256]]))); } -function acquisitionClosure(rows, bodies) { +function acquisitionClosure(rows, bodies, target = TARGET) { const files = rows.flatMap(row => [row.before, row.after].flatMap(state => state.acquisition.filter(x => x.kind === "file"))); const pins = [...new Map(files.map(x => [x.sha256, x])).values()]; c.keys(bodies, pins.map(x => x.sha256), "closed acquisition byte subjects"); let total = 0; for (const item of pins) { total += item.size; assert.ok(total <= 16 * LIMIT, "total acquisition byte bound"); } - for (const row of rows) for (const state of [row.before, row.after]) acquisition(state, bodies); + for (const row of rows) for (const state of [row.before, row.after]) acquisition(state, bodies, target); } // Fixed ReleaseScanner release.go pins, never supplied by an evidence caller. // HTTP bodies are observer evidence, not files invented in the scanner cache. const SCANNER_RELEASES = Object.freeze({ "linux-amd64": { name: "lintai-v0.1.3-x86_64-unknown-linux-gnu.tar.gz", sha256: "2b3d176db752433b904a4b42375543ff398f4841d22e48f7d4f23ded925b72da" }, + "linux-arm64": { name: "lintai-v0.1.3-aarch64-unknown-linux-gnu.tar.gz", sha256: "132a37610575bd251ecaf0be4c6090dad144dd1397c99aad989a3944c63c3d4a" }, "linux-amd64-musl": { name: "lintai-v0.1.3-x86_64-unknown-linux-musl.tar.gz", sha256: "3da60f749c61e2caca029a44a9ce422d570aef8c57f82ce51c411c8cec12f61b" } }); function scannerArchive(scan, executable) { @@ -512,7 +541,7 @@ function scannerArchive(scan, executable) { assert.ok(binary?.length, "release archive contains lintai"); exact(binary, executable, "scanner executable is the pinned archive member"); } -function scanReplay(rows, projects, scans, bodies) { +function scanReplay(rows, projects, scans, bodies, target = TARGET) { assert.ok(Array.isArray(scans)); exact(scans.length, 3, "three observed fresh scans and report bytes"); const fresh = new Map(); let executable; for (let i = 0; i < 4; i++) { @@ -520,7 +549,7 @@ function scanReplay(rows, projects, scans, bodies) { const assessment = scanEvidence(jsonDocument(row.stdout), i < 3 ? "local_scan" : "cache", projects[lane]); const key = c.digest(Buffer.from([assessment.subject.tree_digest, assessment.subject.manifest_digest, "lintai", "0.1.3", POLICY.id, String(POLICY.version), POLICY.digest].join("\0"))); - const file = `security/assessments/${key}.json`, before = acquisition(row.before, bodies), after = acquisition(row.after, bodies); + const file = `security/assessments/${key}.json`, before = acquisition(row.before, bodies, target), after = acquisition(row.after, bodies, target); const cached = { ...assessment }; delete cached.evidence_source; assert.ok(after[file], "required assessment cache bytes"); exact(jsonDocument(after[file].toString("utf8")), cached); if (i < 3) { @@ -528,7 +557,8 @@ function scanReplay(rows, projects, scans, bodies) { const scan = scans[i]; c.keys(scan, ["id", "args", "subject", "executable", "archive", "report"], "observed scanner call"); exact([scan.id, scan.args, scan.subject], [row.id, ["scan-agent-plugin", `/${lane}`], assessment.subject]); c.keys(scan.executable, ["path", "sha256"], "observed scanner executable"); - assert.match(scan.executable.path, /^security\/lintai\/0\.1\.3\/linux-amd64(?:-musl)?\/lintai$/); + assert.ok((target === TARGET ? [TARGET, "linux-amd64-musl"] : [target]) + .some(t => scan.executable.path === `security/lintai/0.1.3/${t}/lintai`), "scanner matches selected host target"); assert.ok(after[scan.executable.path], "required scanner acquisition bytes"); exact(c.digest(after[scan.executable.path]), sha(scan.executable.sha256)); scannerArchive(scan, after[scan.executable.path]); @@ -790,7 +820,8 @@ function custodyCoverage(entries, preparation) { const identity = `${v.dev}:${v.ino}`; assert.ok(!inodes.has(identity)); inodes.add(identity); }); } -function verifyJourney(e, pins) { +function verifyJourney(e, pins, target = TARGET) { + executableHost(target); c.keys(e, FILES, "evidence bundle"); const transcript = e["transcripts.json"], projects = e["trees.json"]; c.keys(transcript, [...c.PRODUCTS, "installer"], "transcripts"); c.keys(projects, c.PRODUCTS, "trees"); @@ -800,7 +831,7 @@ function verifyJourney(e, pins) { const row = rows[i], spec = specs[i]; c.keys(row, ["id", "args", "status", "stdout", "stderr", "before", "after"], "command transcript"); treeShape(row.before); treeShape(row.after); - exact([row.id, row.args], [spec.id, spec.args]); authorResult(row, spec, p, pins.identity); + exact([row.id, row.args], [spec.id, spec.args]); authorResult(row, spec, p, pins.identity, target); if (!/\/(init|extra-skill)$/.test(spec.id)) exact(row.before, row.after, "read/rejection preservation"); } c.keys(projects[p], CASES, "five templates"); @@ -827,7 +858,7 @@ function verifyJourney(e, pins) { } treeShape(state.client); treeShape(state.state); capturedBytes(state.state.filter(x => /\/(?:\.codex-plugin\/plugin|\.agents\/plugins\/marketplace)\.json$/.test(x.path)), state.projection_documents); - acquisition(state, e["acquisition.json"]); stateDocument(state); + acquisition(state, e["acquisition.json"], target); stateDocument(state); } if (i) exact(row.before, transcript.installer[i - 1].after, "installer state continuity"); else { exact(row.before.acquisition, []); exact(stateDocument(row.before).installations, []); } @@ -836,8 +867,8 @@ function verifyJourney(e, pins) { exact(row.after.client.find(x => x.path === original.path), original, "preexisting client preservation"); exact([row.id, row.args], [specs[i].id, specs[i].args]); installed(row, specs[i], projects.agentplugins); } - acquisitionClosure(transcript.installer, e["acquisition.json"]); - scanReplay(transcript.installer, projects.agentplugins, e["scans.json"], e["acquisition.json"]); + acquisitionClosure(transcript.installer, e["acquisition.json"], target); + scanReplay(transcript.installer, projects.agentplugins, e["scans.json"], e["acquisition.json"], target); const preserve = e["preservation.json"]; c.keys(preserve, ["inputs_before", "inputs_after", "projects_before", "projects_after", "author_homes_before", "author_homes_after", "custody_before", "custody_after"], "preservation"); for (const name of ["inputs", "projects", "author_homes", "custody"]) exact(preserve[`${name}_before`], preserve[`${name}_after`]); @@ -854,14 +885,16 @@ function verifyJourney(e, pins) { installer: specs.map(x => x.id), parity: "both-products", preservation: "inputs-projects-client-state", runtime: "not_evaluated" }; } function terminal(p, pins, manifest, options, evidence, tools) { - return { schema: SCHEMA, lane: `${p}/${TARGET}`, identity: pins.identity, candidate_sha256: pins.candidate_sha256, - pair_marker_sha256: pins.pair_marker_sha256, projection_pins: pins.products, subject: subject(manifest, p), - peer_subject: subject(manifest, c.PRODUCTS.find(x => x !== p)), preparation: options.preparation, - producer: options.producer, host: evidence["host.json"], tools, assertions: verifyJourney(evidence, pins), + const target = Object.hasOwn(options, "target") ? options.target : TARGET; + return { schema: SCHEMA, lane: `${p}/${target}`, identity: pins.identity, candidate_sha256: pins.candidate_sha256, + pair_marker_sha256: pins.pair_marker_sha256, projection_pins: pins.products, subject: subject(manifest, p, target), + peer_subject: subject(manifest, c.PRODUCTS.find(x => x !== p), target), preparation: options.preparation, + producer: options.producer, host: evidence["host.json"], tools, assertions: verifyJourney(evidence, pins, target), evidence: FILES.map(file => ({ file, ...c.metadata(c.encode(evidence[file])) })) }; } function readTerminals(root, inputRoot, pins, expected) { - c.safeDirectory(root); c.keys(expected, ["producer", "preparation", "tools"], "terminal expectations"); + c.safeDirectory(root); c.keys(expected, ["producer", "preparation", "tools", ...(Object.hasOwn(expected, "target") ? ["target"] : [])], "terminal expectations"); + const target = Object.hasOwn(expected, "target") ? expected.target : TARGET, contract = executableHost(target); invocation(expected.producer, WORKFLOW, pins.identity.commit); const prepared = readPreparation(inputRoot, pins, expected.preparation); const { manifest } = verifyProjectedPair(inputRoot, pins); @@ -870,9 +903,9 @@ function readTerminals(root, inputRoot, pins, expected) { const evidence = Object.fromEntries(FILES.map(file => [file, canonical(c.readFile(path.join(root, file), 32 * LIMIT), 32 * LIMIT)])); exact(evidence["preparation.json"], prepared); const build = evidence["build-info.json"]; c.keys(build, c.PRODUCTS, "selected build info"); - for (const p of c.PRODUCTS) buildInfo(build[p], p, TARGET, pins.identity, MODE); + for (const p of c.PRODUCTS) buildInfo(build[p], p, target, pins.identity, MODE); const host = evidence["host.json"]; - exact(host, { platform: "linux", architecture: "x64", machine: "x86_64", target: TARGET, + exact(host, { platform: contract.platform, architecture: contract.architecture, machine: contract.machine, target, observation: "whole-descendant-authoring-no-process-network/1" }); c.keys(expected.tools, ["go", "node"], "host tools"); for (const [name, version] of [["go", "go1.25.13"], ["node", "v22.21.1"]]) { @@ -889,10 +922,11 @@ function readTerminals(root, inputRoot, pins, expected) { return result; } async function produce(options, signal) { - c.keys(options, ["root", "pins", "preparation", "producer", "go", "go_sha256", "output", "work"], "native options"); + c.keys(options, ["root", "pins", "preparation", "producer", "go", "go_sha256", "output", "work", ...(Object.hasOwn(options, "target") ? ["target"] : [])], "native options"); const { root, pins } = options; + const target = Object.hasOwn(options, "target") ? options.target : TARGET, contract = executableHost(target); invocation(options.producer, WORKFLOW, pins.identity.commit); - exact([process.platform, process.arch, os.machine()], ["linux", "x64", "x86_64"], "actual Linux amd64 host"); + exact([process.platform, process.arch, os.machine()], [contract.platform, contract.architecture, contract.machine], "actual native host OS/architecture; no emulation"); exact(process.version, "v22.21.1"); sha(options.go_sha256); assert.ok(path.isAbsolute(options.go)); exact(c.digest(c.readFile(options.go)), options.go_sha256); // Host Go has its own pin; a different-platform builder executable need not hash identically. @@ -910,15 +944,15 @@ async function produce(options, signal) { const tool = context(path.join(options.work, "host-tool")); const go = await subprocess(options.go, ["env", "-json", "GOVERSION", "GOHOSTOS", "GOHOSTARCH"], tool, signal); exact(go.status, 0); exact(go.stderr, ""); - exact(JSON.parse(go.stdout), { GOVERSION: "go1.25.13", GOHOSTOS: "linux", GOHOSTARCH: "amd64" }); + exact(JSON.parse(go.stdout), { GOVERSION: "go1.25.13", GOHOSTOS: contract.goos, GOHOSTARCH: contract.goarch }); for (const p of c.PRODUCTS) { contexts[p] = context(path.join(options.work, p)); - const a = frozen.manifest.products[p].assets[TARGET], bytes = c.readFile(path.join(root, p, a.file)); + const a = frozen.manifest.products[p].assets[target], bytes = c.readFile(path.join(root, p, a.file)); const binary = p === "plugin-kit-ai" ? c.unpack(bytes, a.binary.file) : bytes; binaries[p] = path.join(contexts[p].root, p); fs.writeFileSync(binaries[p], binary, { flag: "wx", mode: 0o500 }); exact(c.metadata(c.readFile(binaries[p])), { sha256: a.binary.sha256, size: a.binary.size }); const r = await subprocess(options.go, ["version", "-m", "-json", binaries[p]], tool, signal); - exact(r.status, 0); exact(r.stderr, ""); build[p] = JSON.parse(r.stdout); buildInfo(build[p], p, TARGET, pins.identity, MODE); + exact(r.status, 0); exact(r.stderr, ""); build[p] = JSON.parse(r.stdout); buildInfo(build[p], p, target, pins.identity, MODE); } const protectedHomes = () => Object.fromEntries(c.PRODUCTS.map(p => [p, Object.fromEntries(["home", "state", "config", "cache", "data", "tmp"].map(n => [n, tree(path.join(contexts[p].root, n))]))])); const homesBefore = protectedHomes(); @@ -931,7 +965,7 @@ async function produce(options, signal) { } const before = tree(projectRoot), r = await subprocess(binaries[p], spec.args, ctx, signal); const row = { id: spec.id, args: spec.args, ...r, before, after: tree(projectRoot) }; - rows[p].push(row); authorResult(row, spec, p, pins.identity); + rows[p].push(row); authorResult(row, spec, p, pins.identity, target); if (!/\/(init|extra-skill)$/.test(spec.id)) exact(row.before, row.after); if (spec.id === "malformed-skill") { fs.unlinkSync(path.join(malformed, "SKILL.md")); fs.rmdirSync(malformed); } } @@ -982,7 +1016,7 @@ async function produce(options, signal) { const after = verifyProjectedPair(root, pins); readPreparation(root, pins, options.preparation); exact(c.digest(c.readFile(options.go)), options.go_sha256); for (const p of c.PRODUCTS) { - exact(c.digest(c.readFile(binaries[p])), frozen.manifest.products[p].assets[TARGET].binary.sha256); + exact(c.digest(c.readFile(binaries[p])), frozen.manifest.products[p].assets[target].binary.sha256); exact(fs.lstatSync(binaries[p]).mode & 0o777, 0o500, "selected executable mode preserved"); } normalizeInstaller(rows.installer, install.env.AGENTPLUGINS_HOME); @@ -996,9 +1030,9 @@ async function produce(options, signal) { // ReleaseScanner keeps neither its raw report stdout nor acquisition HTTP // bytes. No scan record can be inferred from a cached assessment. A future // reviewed observer must close that custody; production has no positive seam. - e["host.json"] = { platform: process.platform, architecture: process.arch, machine: os.machine(), target: TARGET, + e["host.json"] = { platform: process.platform, architecture: process.arch, machine: os.machine(), target, observation: "whole-descendant-authoring-no-process-network/1" }; - verifyJourney(e, pins); + verifyJourney(e, pins, target); const tools = { go: { sha256: options.go_sha256, version: "go1.25.13" }, node: { sha256: c.digest(c.readFile(process.execPath)), version: process.version } }; for (const file of FILES) fs.writeFileSync(path.join(options.output, file), c.encode(e[file]), { flag: "wx", mode: 0o400 }); if (signal?.aborted) fail("cancelled before terminal"); @@ -1008,7 +1042,7 @@ async function produce(options, signal) { try { fs.writeFileSync(fd, c.encode(terminal(p, pins, frozen.manifest, options, e, tools))); } finally { fs.closeSync(fd); } } - return readTerminals(options.output, root, pins, { producer: options.producer, preparation: options.preparation, tools }); + return readTerminals(options.output, root, pins, { producer: options.producer, preparation: options.preparation, tools, ...(options.target ? { target } : {}) }); } catch (error) { for (const file of ownedTerminals) fs.unlinkSync(file); fs.writeFileSync(path.join(options.output, "failure-transcripts.json"), c.encode({ diff --git a/npm/agentplugins/scripts/authoring-promotion.js b/npm/agentplugins/scripts/authoring-promotion.js index aa6384e0..6c271247 100644 --- a/npm/agentplugins/scripts/authoring-promotion.js +++ b/npm/agentplugins/scripts/authoring-promotion.js @@ -20,6 +20,10 @@ const SCHEMA = "authoring-promotion/v1"; const SLSA = "https://slsa.dev/provenance/v1"; const LIMIT = 1024 * 1024; const LANES = Object.freeze([...c.PRODUCTS.flatMap(p => c.TARGETS.map(t => `${p}/${t}`)), "public-packed-pair"]); +const NATIVE_SCHEMA = "authoring-frozen-native/v1"; +const NATIVE_WORKFLOW = ".github/workflows/authoring-frozen-native.yml"; +const NATIVE_FILES = Object.freeze(["transcripts.json", "trees.json", "build-info.json", "preservation.json", + "preparation.json", "host.json", "scans.json", "acquisition.json", ...c.PRODUCTS.map(p => `${p}-terminal.json`)]); const fail = message => { throw new Error(message); }; const exact = (a, b, label) => { if (!equal(a, b)) fail(`${label}: binding mismatch`); }; const sha = (v, n = 64) => { @@ -119,12 +123,14 @@ function requireNativeContracts(lanes) { seen.add(value.lane); } const missing = LANES.filter(x => !seen.has(x)); - // Deliberately NO caller-supplied adapter, issuer, success boolean or policy. - // Native owners must deliver reviewed terminal schemas AND their producer - // source asserting the concrete native/installer and public packed gates. - const unsupported = lanes.map(x => `${x.lane}:${String(x.schema).slice(0, 100)}`); + // This is only the registered-contract inventory, never evidence admission. + // Public packed has no accepted producer. Even twelve admitted native lanes + // cannot authorize the thirteen-lane global gate or a protected effect. + const unsupported = lanes.filter(x => x.lane === "public-packed-pair" || + x.schema !== NATIVE_SCHEMA || x.workflow !== NATIVE_WORKFLOW) + .map(x => `${x.lane}:${String(x.schema).slice(0, 100)}`); fail(`NATIVE_EVIDENCE_INTEGRATION_REQUIRED: missing lanes [${missing.join(", ")}]; unsupported contracts [${unsupported.join(", ")}]. ` + - "No accepted frozen-pair all-target terminal producer/schema is integrated. dual-authoring-public-native/v1 is Linux fixture evidence with false release claims; private-packed or SLSA build success cannot qualify this pair. Signing and promotion are disabled."); + "Public-packed producer/schema remains unsupported. dual-authoring-public-native/v1 is Linux fixture evidence with false release claims; private-packed or SLSA build success cannot qualify this pair. Signing and promotion are disabled."); } function validateSelection(body, selected) { const record = decodeRecord(body); @@ -180,21 +186,310 @@ function inspectArtifact(pin, workflow, source, cwd) { function acquireArtifact(pin, workflow, source, cwd) { cliVersion(cwd); const before = inspectArtifact(pin, workflow, source, cwd); + return downloadArtifactBytes(pin, before.item, cwd, () => + exact(inspectArtifact(pin, workflow, source, cwd), before, "artifact changed during acquisition")); +} +// Same private bounded byte path after distinct completed/current admission. +function downloadArtifactBytes(pin, item, cwd, recheck) { const file = path.join(cwd, `artifact-${pin.artifact_id}.zip`); if (fs.existsSync(file)) fail("artifact destination already exists"); - // gh api follows the provider's supported artifact ZIP redirect. No free URL. - // Binary output is bounded in memory, never extracted or executed here. const zip = gh(["api", "--hostname", "github.com", `repos/${REPOSITORY}/actions/artifacts/${pin.artifact_id}/zip`], cwd, 2 * 1024 * LIMIT, null); - if (zip.length !== before.item.size_in_bytes || c.digest(zip) !== pin.artifact_sha256) fail("artifact ZIP digest/size mismatch"); - exact(inspectArtifact(pin, workflow, source, cwd), before, "artifact changed during acquisition"); + if (zip.length !== item.size_in_bytes || c.digest(zip) !== pin.artifact_sha256) fail("artifact ZIP digest/size mismatch"); + recheck(); fs.writeFileSync(file, zip, { flag: "wx", mode: 0o400 }); return file; } +// Fixed C1 provider adapters. Log mapping and runner/process custody require +// independent genuine acceptance before positive execution. These checks do not +// confer that acceptance, and unsupported checked-ZIP kinds remain closed. +const STAGE_WORKFLOW = ".github/workflows/agentplugins-npm-publish.yml"; +function workflowSelection(value, workflowSha) { + c.keys(value, ["tag", "ref", "source", "versions"], "workflow selection"); + c.keys(value.versions, c.PRODUCTS, "selected versions"); + const identityValue = { repository: REPOSITORY, commit: value.source, engine_revision: value.source, versions: value.versions }; + identity(identityValue); sha(value.source, 40); + if (value.source.length !== 40 || Object.values(value.versions).some(v => typeof v !== "string" || v.length > 32 || /[\r\n]/.test(v))) fail("bounded workflow identity required"); + exact(value.versions["plugin-kit-ai"], "2.0.0", "first kit version"); + exact([value.tag, value.ref, workflowSha], [tag(identityValue, "agentplugins"), `refs/tags/${tag(identityValue, "agentplugins")}`, value.source], "selected workflow ref/source"); + return { tag: value.tag, ref: value.ref, source: value.source, versions: { ...value.versions } }; +} +function callerNumber(name, maximum = Number.MAX_SAFE_INTEGER) { + const text = process.env[name]; + if (typeof text !== "string" || !/^[1-9][0-9]{0,15}$/.test(text)) fail("exact caller integer required"); + return integer(Number(text), maximum); +} +function currentCaller(selected, workflow, jobs) { + const expected = { GITHUB_ACTIONS: "true", GITHUB_EVENT_NAME: "workflow_dispatch", GITHUB_REPOSITORY: REPOSITORY, + GITHUB_SHA: selected.source, GITHUB_WORKFLOW_SHA: selected.source, GITHUB_REF: selected.ref, + GITHUB_WORKFLOW_REF: `${REPOSITORY}/${workflow}@${selected.ref}` }; + exact(Object.fromEntries(Object.keys(expected).map(k => [k, process.env[k]])), expected, "current workflow caller"); + if (!jobs.includes(process.env.GITHUB_JOB)) fail("fixed C1 caller job required"); + return { workflow, source: selected.source, run_id: callerNumber("GITHUB_RUN_ID"), run_attempt: callerNumber("GITHUB_RUN_ATTEMPT", 1000) }; +} +function attemptAtRef(pin, workflow, selected, cwd, status) { + const run = api(`actions/runs/${integer(pin.run_id)}/attempts/${integer(pin.run_attempt, 1000)}`, cwd); + exact([run.id, run.run_attempt, run.repository?.full_name, run.head_repository?.full_name, + run.head_sha, run.path, run.event, run.head_branch, run.status, run.conclusion], + [pin.run_id, pin.run_attempt, REPOSITORY, REPOSITORY, selected.source, workflow, + "workflow_dispatch", selected.tag, status, status === "completed" ? "success" : null], "provider invocation/ref"); + return run; +} +function attemptJobs(pin, selected, cwd) { + const response = api(`actions/runs/${pin.run_id}/attempts/${pin.run_attempt}/jobs?per_page=100`, cwd); + if (!Array.isArray(response.jobs) || response.jobs.length > 100 || response.total_count !== response.jobs.length) fail("complete bounded attempt jobs required"); + for (const job of response.jobs) { + integer(job.id); + exact([job.run_id, job.run_attempt, job.head_sha, job.head_branch], + [pin.run_id, pin.run_attempt, selected.source, selected.tag], "provider job attempt/ref"); + } + if (new Set(response.jobs.map(j => j.id)).size !== response.jobs.length) fail("duplicate provider job ID"); + return response.jobs; +} +function oneJob(jobs, name, status) { + const found = jobs.filter(job => job.name === name); + if (found.length !== 1 || found[0].status !== status || found[0].conclusion !== (status === "completed" ? "success" : null)) { + fail("exact fixed successful producer/current job required"); + } + return found[0]; +} +function checkPreparationRef(pin, selected, cwd) { + return attemptAtRef(pin, WORKFLOW, selected, cwd, "completed"); +} +function inspectInputCaller(selected, workflowSha, cwd) { + selected = workflowSelection(selected, workflowSha); + const caller = currentCaller(selected, WORKFLOW, ["paired_input_admission", "paired_input_attestation"]); + cliVersion(cwd); + attemptAtRef(caller, WORKFLOW, selected, cwd, "in_progress"); + const jobs = attemptJobs(caller, selected, cwd); + oneJob(jobs, process.env.GITHUB_JOB, "in_progress"); + if (process.env.GITHUB_JOB === "paired_input_attestation") oneJob(jobs, "paired_input_admission", "completed"); + for (const p of c.PRODUCTS) checkTag({ identity: { commit: selected.source, versions: selected.versions } }, p, cwd); + return caller; +} +function inspectStageCaller(selected, workflowSha, cwd) { + selected = workflowSelection(selected, workflowSha); + const caller = currentCaller(selected, STAGE_WORKFLOW, ["paired_stage"]); + cliVersion(cwd); + attemptAtRef(caller, STAGE_WORKFLOW, selected, cwd, "in_progress"); + oneJob(attemptJobs(caller, selected, cwd), "paired_stage", "in_progress"); + for (const p of c.PRODUCTS) checkTag({ identity: { commit: selected.source, versions: selected.versions } }, p, cwd); + return { ...caller, ref: selected.ref }; +} +function stageJobEvidence(pin, selected, cwd) { + const job = oneJob(attemptJobs(pin, selected, cwd), "paired_stage", "completed"); + // Names are fixed in YAML; provider records expose step names, not YAML IDs. + const names = ["C1 preflight", "C1 checkout", "C1 setup", "C1 stage", "C1 upload", "C1 upload evidence"]; + if (!Array.isArray(job.steps)) fail("retained producer steps required"); + const allowed = ["Set up job", ...names, "Post C1 setup", "Post C1 checkout", "Complete job"]; + if (job.steps.some(step => !allowed.includes(step.name))) fail("unreviewed producer step"); + let last = 0; + for (const name of names) { + const rows = job.steps.filter(step => step.name === name); + if (rows.length !== 1 || rows[0].status !== "completed" || rows[0].conclusion !== "success" || + !Number.isSafeInteger(rows[0].number) || rows[0].number <= last) fail("fixed ordered successful stage steps required"); + last = rows[0].number; + } + const log = gh(["api", "--hostname", "github.com", `repos/${REPOSITORY}/actions/jobs/${job.id}/logs`], cwd, 4 * LIMIT); + const records = []; + for (const line of log.split("\n")) { + // Only standalone timestamped log records; echoed command source is not evidence. + const match = /^\d{4}-\d\d-\d\dT[0-9:.]+Z C1_STAGE (.*)\r?$/.exec(line); + if (match) records.push(JSON.parse(match[1])); + } + if (records.length !== 5) fail("five ordered retained stage operation records required"); + const [start, agent, kit, completion, upload] = records; + c.keys(start, ["operation", "source", "ref", "run_id", "run_attempt", "input_sha256", "input_artifact"], "stage start transcript"); + exact([start.operation, start.source, start.ref, start.run_id, start.run_attempt], + ["start", selected.source, selected.ref, pin.run_id, pin.run_attempt], "stage start invocation"); + sha(start.input_sha256); locator(start.input_artifact); + for (const [i, row] of [agent, kit].entries()) { + c.keys(row, ["operation", "product", "pack"], "stage pack transcript"); + exact([row.operation, row.product], ["pack", c.PRODUCTS[i]], "ordered two packs"); + } + c.keys(completion, ["operation", "stage_sha256"], "stage completion transcript"); + exact(completion.operation, "completion", "completed stage operation"); sha(completion.stage_sha256); + c.keys(upload, ["operation", "artifact_id", "artifact_sha256", "stage_sha256"], "stage upload transcript"); + exact(upload, { operation: "upload", artifact_id: pin.artifact_id, artifact_sha256: pin.artifact_sha256, + stage_sha256: completion.stage_sha256 }, "retained upload selector"); + return { job, records }; +} +function inspectCurrentStage(value) { + c.keys(value, ["artifact", "selected", "workflow_sha", "scratch"], "current stage options"); + const pin = locator(value.artifact), selected = workflowSelection(value.selected, value.workflow_sha); + c.safeDirectory(value.scratch); + const caller = currentCaller(selected, STAGE_WORKFLOW, ["paired_stage_attestation"]); + exact([pin.run_id, pin.run_attempt], [caller.run_id, caller.run_attempt], "current stage locator"); + cliVersion(value.scratch); + attemptAtRef(pin, STAGE_WORKFLOW, selected, value.scratch, "in_progress"); + oneJob(attemptJobs(pin, selected, value.scratch), "paired_stage_attestation", "in_progress"); + const evidence = stageJobEvidence(pin, selected, value.scratch); + const item = api(`actions/artifacts/${pin.artifact_id}`, value.scratch); + exact([item.id, item.expired, item.digest, item.workflow_run?.id, item.workflow_run?.head_sha, item.name], + [pin.artifact_id, false, `sha256:${pin.artifact_sha256}`, pin.run_id, selected.source, + `authoring-public-stage-${selected.source}-${pin.run_id}-${pin.run_attempt}`], "current stage artifact custody"); + integer(item.size_in_bytes, 2 * 1024 * LIMIT); + const created = Date.parse(item.created_at), started = Date.parse(evidence.job.started_at), ended = Date.parse(evidence.job.completed_at); + if (![created, started, ended].every(Number.isFinite) || created < started || created > ended) fail("artifact outside producer upload interval"); + // Incidental timestamps and growing unrelated jobs are deliberately omitted. + return { item: { id: item.id, digest: item.digest, size_in_bytes: item.size_in_bytes, name: item.name }, + job_id: evidence.job.id, records: evidence.records }; +} +function acquireCurrentStage(value) { + const before = inspectCurrentStage(value); + return downloadArtifactBytes(value.artifact, before.item, value.scratch, () => + exact(inspectCurrentStage(value), before, "current stage custody changed")); +} +function checkStageEvidence(artifact, selected, workflowSha, record, stageSha, cwd) { + selected = workflowSelection(selected, workflowSha); locator(artifact); + const evidence = stageJobEvidence(artifact, selected, cwd); + const expected = [ + { operation: "start", source: record.producer.source, ref: record.producer.ref, run_id: record.producer.run_id, + run_attempt: record.producer.run_attempt, input_sha256: record.native_inputs.sha256, input_artifact: record.native_inputs.artifact }, + ...c.PRODUCTS.map(product => ({ operation: "pack", product, pack: record.packs[product] })), + { operation: "completion", stage_sha256: stageSha }, + { operation: "upload", artifact_id: artifact.artifact_id, artifact_sha256: artifact.artifact_sha256, stage_sha256: stageSha }]; + exact(evidence.records, expected, "retained operation transcript versus exact S"); + return { job_id: evidence.job.id, records: evidence.records }; +} + +// Extract only the file already checked by acquireArtifact. Python opens once, +// snapshots and rehashes it, then parses/extracts that same immutable byte array. +// There is no second provider download, archive-name selection or unzip command. +function extractArtifact(file, pin, kind, files, output, cwd) { + locator(pin); c.safeDirectory(cwd); + if (path.basename(file) !== `artifact-${pin.artifact_id}.zip`) fail("exact acquired ZIP path required"); + const st = fs.lstatSync(file); + if (!st.isFile() || st.nlink !== 1) fail("regular unaliased ZIP required"); + const result = cp.spawnSync("/usr/bin/python3", ["-B", path.resolve(__dirname, "../../../scripts/read-authoring-evidence-zip.py"), + "--archive", file, "--sha256", pin.artifact_sha256, "--size", String(st.size), + "--kind", kind, "--files", JSON.stringify(files), "--output", output], { + cwd, env: { PATH: "/usr/bin:/bin", HOME: cwd, LC_ALL: "C.UTF-8", PYTHONNOUSERSITE: "1" }, + encoding: "utf8", timeout: 120000, killSignal: "SIGKILL", maxBuffer: LIMIT, shell: false + }); + if (result.error || result.signal || result.status !== 0) fail("checked authoring ZIP extraction rejected; no admission or protected effect"); + exact(JSON.parse(result.stdout), { archive_sha256: pin.artifact_sha256, files: [...files].sort() }, "extracted archive closure"); + return output; +} +function preparationFiles(record) { + return ["preparation-run.json", "candidate-identity.json", "candidate/candidate.json", "pair-prepared.json", + ...c.PRODUCTS.flatMap(p => [...c.TARGETS.map(t => `${p}/${record.products[p].assets[t].file}`), + `${p}/release-manifest.json`, `${p}/checksums.txt`])]; +} +function invocationFor(pin, workflow, source) { + return { repository: REPOSITORY, workflow, source, workflow_sha: source, + run_id: pin.run_id, run_attempt: pin.run_attempt }; +} +function projectedPins(record) { + return { identity: record.identity, candidate_sha256: record.candidate_sha256, + pair_marker_sha256: record.pair_marker_sha256, products: Object.fromEntries(c.PRODUCTS.map(p => [p, { + manifest_sha256: record.products[p].manifest_sha256, checksums_sha256: record.products[p].checksums_sha256 }])) }; +} +function acquirePreparation(pin, record, scratch) { + return acquirePreparationBinding(pin, recordShape(record), scratch); +} +// Shared preparation intake below Q validation. Only the Q wrapper above and +// the canonical I adapter below supply this private binding; no synthetic Q. +function acquirePreparationBinding(pin, record, scratch, receiptSha256) { + c.safeDirectory(scratch); locator(pin); + const work = fs.mkdtempSync(path.join(scratch, "preparation-")); + const file = acquireArtifact(pin, WORKFLOW, record.identity.commit, work); + const root = extractArtifact(file, pin, "preparation", preparationFiles(record), path.join(work, "frozen"), work); + return readPreparationBinding(root, pin, record, receiptSha256); +} +function readPreparationBinding(root, pin, record, receiptSha256) { + frozenSubjects(root, record); + const metadataBody = c.readFile(path.join(root, "candidate-identity.json"), LIMIT); + const metadata = JSON.parse(metadataBody); + exact(metadataBody, c.encode(metadata), "canonical preparation identity metadata"); + c.keys(metadata, ["identity", "status", "manifest_sha256", "output", "local_build_evidence", + "release_eligible", "platform_acceptance", "attested"], "preparation identity metadata"); + exact(metadata.identity, record.identity, "preparation metadata identity"); + exact([metadata.status, metadata.manifest_sha256, metadata.release_eligible, metadata.platform_acceptance, metadata.attested], + ["CANDIDATE", record.candidate_sha256, false, false, false], "preparation metadata claims"); + // Historical builder locations are descriptive strings only. Never resolve, + // follow or read them in the consumer's namespace, including during recovery. + for (const name of ["output", "local_build_evidence"]) { + const value = metadata[name]; + if (typeof value !== "string" || value.length > 4096 || !value.startsWith("/") || /[\x00-\x1f]/.test(value)) + fail("bounded preparation location metadata required"); + } + const preparation = { sha256: c.digest(c.readFile(path.join(root, "preparation-run.json"), LIMIT)), + producer: invocationFor(pin, WORKFLOW, record.identity.commit) }; + if (receiptSha256 !== undefined) exact(preparation.sha256, receiptSha256, "exact I preparation receipt"); + require("./authoring-native-qualification").readPreparation(root, projectedPins(record), preparation); + // Metadata cannot substitute for the receipt or the frozen subject pins. + // Independently acquired provider bytes, not this file's claims, bind attempt. + return { root, preparation }; +} +// These are fixed structural/custody adapters, not signature admission. The +// existing Q wrapper retains its recordShape requirement and return encoding. +function acquireInputPreparation(inputBytes, scratch) { + const input = require("./authoring-native-inputs").decodeInputs(inputBytes); + return acquirePreparationBinding(input.preparation.artifact, input, scratch, input.preparation.sha256); +} +function readInputPreparation(root, inputBytes) { + const input = require("./authoring-native-inputs").decodeInputs(inputBytes); + return readPreparationBinding(root, input.preparation.artifact, input, input.preparation.sha256); +} +function checkInputTags(inputBytes, cwd) { + const input = require("./authoring-native-inputs").decodeInputs(inputBytes); + c.safeDirectory(cwd); cliVersion(cwd); + for (const product of c.PRODUCTS) checkTag(input, product, cwd); +} +function nativeContract(lane) { + if (!LANES.slice(0, 12).includes(lane.lane) || lane.schema !== NATIVE_SCHEMA || lane.workflow !== NATIVE_WORKFLOW) + fail(`NATIVE_EVIDENCE_INTEGRATION_REQUIRED: unsupported contracts [${lane.lane}:${lane.schema}]`); +} +// Registered identifiers are a syntactic precondition only. No success value, +// provider assertion or authorization is returned by this inexpensive check. +function checkNativeContracts(record) { + recordShape(record).qualification.lanes.slice(0, 12).forEach(nativeContract); +} +function admitNativeEvidence(record, preparationPin, scratch) { + // Full canonical record validation precedes every acquisition. This return + // represents only twelve native observations, never global authorization. + record = recordShape(record); + const lanes = record.qualification.lanes.slice(0, 12); + lanes.forEach(nativeContract); + const prepared = acquirePreparation(preparationPin, record, scratch); + const receipts = []; + const seenArtifacts = new Set(); + for (const target of ["linux-amd64", ...c.TARGETS.filter(t => t !== "linux-amd64")]) { + const pair = c.PRODUCTS.map(p => lanes.find(x => x.lane === `${p}/${target}`)); + exact(pair[0].artifact, pair[1].artifact, "paired terminals must share one exact artifact"); + const pin = pair[0].artifact; + if (seenArtifacts.has(pin.artifact_id) || pin.artifact_id === preparationPin.artifact_id) fail("native artifact reused across targets or preparation"); + seenArtifacts.add(pin.artifact_id); + const work = fs.mkdtempSync(path.join(scratch, `native-${target}-`)); + const file = acquireArtifact(pin, NATIVE_WORKFLOW, record.identity.commit, work); + const root = extractArtifact(file, pin, "native", NATIVE_FILES, path.join(work, "evidence"), work); + for (let i = 0; i < pair.length; i++) { + const body = c.readFile(path.join(root, `${c.PRODUCTS[i]}-terminal.json`), LIMIT); + exact(c.digest(body), pair[i].sha256, "independently pinned terminal digest"); + } + // The accepted reader validates the complete paired journey and evidence, + // not merely producer metadata, success booleans, or the selected subject. + const terminal = JSON.parse(c.readFile(path.join(root, "agentplugins-terminal.json"), LIMIT)); + const observed = require("./authoring-native-qualification").readTerminals(root, prepared.root, projectedPins(record), { + producer: invocationFor(pin, NATIVE_WORKFLOW, record.identity.commit), preparation: prepared.preparation, + tools: terminal.tools, target + }); + exact(observed.map(x => x.lane), pair.map(x => x.lane), "admitted target lanes"); + receipts.push(...observed); + } + return { ...prepared, receipts }; +} + // Mapping for fresh `gh attestation verify --format json` results. This function // is structural; only verifySubject calls the cryptographic boundary. B must // never promote supplied JSON into authenticated proof by calling this mapper. function mapVerifiedOutput(output, expected) { + return mapWorkflowOutput(output, expected, WORKFLOW); +} +// Private policy selection only. Cryptographic output fields and their mapping +// are unchanged; callers cannot supply a workflow or a verifier. +function mapWorkflowOutput(output, expected, workflow) { if (typeof output !== "string" || Buffer.byteLength(output) > 4 * LIMIT) fail("bounded verifier output required"); const results = JSON.parse(output); if (!Array.isArray(results) || results.length !== 1) fail("one verified attestation required"); @@ -217,7 +512,7 @@ function mapVerifiedOutput(output, expected) { exact(order(statement.subject), order(normalized), "verified subject set"); const build = statement.predicate?.buildDefinition; if (build?.buildType !== "https://actions.github.io/buildtypes/workflow/v1") fail("verified Actions build type mismatch"); - exact(build.externalParameters?.workflow, { ref: expected.ref, repository: URL, path: WORKFLOW }, "verified workflow"); + exact(build.externalParameters?.workflow, { ref: expected.ref, repository: URL, path: workflow }, "verified workflow"); exact(build.resolvedDependencies, [{ uri: `git+${URL}@${expected.ref}`, digest: { gitCommit: expected.source } }], "verified source"); const run = statement.predicate?.runDetails; exact(run?.metadata?.invocationId, `${URL}/actions/runs/${expected.run_id}/attempts/${expected.run_attempt}`, "verified invocation"); @@ -225,6 +520,17 @@ function mapVerifiedOutput(output, expected) { return statement; } function verifySubject(file, expected, cwd) { + return verifyWorkflowSubject(file, expected, cwd, WORKFLOW); +} +function verifyStageSubject(file, expected, cwd) { + exact(expected.workflow_sha, expected.source, "stage signer revision F"); + if (!Array.isArray(expected.subjects) || expected.subjects.length !== 3 || + expected.subjects.filter(s => s.name === "completion.json").length !== 1 || + expected.subjects.filter(s => /^universal-agent-plugins-[0-9]+\.[0-9]+\.[0-9]+\.tgz$/.test(s.name)).length !== 1 || + expected.subjects.filter(s => s.name === "plugin-kit-ai-2.0.0.tgz").length !== 1) fail("exact three stage subjects required"); + return verifyWorkflowSubject(file, expected, cwd, ".github/workflows/agentplugins-npm-publish.yml"); +} +function verifyWorkflowSubject(file, expected, cwd, workflow) { c.keys(expected, ["name", "sha256", "source", "workflow_sha", "ref", "run_id", "run_attempt", "subjects"], "verification expectations"); sha(expected.sha256); sha(expected.source, 40); sha(expected.workflow_sha, 40); integer(expected.run_id); integer(expected.run_attempt, 1000); @@ -232,11 +538,11 @@ function verifySubject(file, expected, cwd) { if (c.digest(c.readFile(file)) !== expected.sha256) fail("subject changed before signature verification"); cliVersion(cwd); const output = gh(["attestation", "verify", file, "--repo", REPOSITORY, - "--signer-workflow", SIGNER, "--signer-digest", expected.workflow_sha, + "--signer-workflow", workflow === WORKFLOW ? SIGNER : `github.com/${REPOSITORY}/${workflow}`, "--signer-digest", expected.workflow_sha, "--source-digest", expected.source, "--source-ref", expected.ref, "--cert-oidc-issuer", "https://token.actions.githubusercontent.com", "--deny-self-hosted-runners", "--predicate-type", SLSA, "--format", "json"], cwd); - const statement = mapVerifiedOutput(output, expected); + const statement = mapWorkflowOutput(output, expected, workflow); if (c.digest(c.readFile(file)) !== expected.sha256) fail("subject changed after signature verification"); return statement; } @@ -311,13 +617,11 @@ function options(v) { } function admittedInputs(input) { const o = options(input); - const record = admitRecord(c.readFile(o.record, LIMIT), o.selected); // Before gh, output or attestation. + const record = validateSelection(c.readFile(o.record, LIMIT), o.selected); exact(o.workflow_sha, record.identity.commit, "integrated workflow source"); - cliVersion(o.scratch); - inspectArtifact(o.preparation, WORKFLOW, record.identity.commit, o.scratch); - // Integration must acquire/validate the accepted native terminal artifacts - // here, including actual asserted gates and exact attempt/subject bindings. - // requireNativeContracts remains unconditional until that implementation lands. + const native = admitNativeEvidence(record, o.preparation, o.scratch); + require("./authoring-native-qualification").readPreparation(o.root, projectedPins(record), native.preparation); + requireNativeContracts(record.qualification.lanes); // Public adapter still unavailable; zero effects. const subjects = frozenSubjects(o.root, record); subjects.push({ file: o.record, sha256: c.digest(encodeRecord(record)) }); return { o, record, subjects }; @@ -417,5 +721,7 @@ if (require.main === module) { try { process.stdout.write(JSON.stringify(main(process.argv.slice(2))) + "\n"); } catch (error) { process.stderr.write(`authoring promotion: ${error.message}\n`); process.exitCode = 1; } } -module.exports = { SCHEMA, WORKFLOW, GH_VERSION, LANES, encodeRecord, decodeRecord, validateSelection, admitRecord, requireNativeContracts, - inspectArtifact, acquireArtifact, mapVerifiedOutput, verifySubject, frozenSubjects, releasePins, inspectPair, promote }; +module.exports = { inspectStageCaller, workflowSelection, inspectInputCaller, checkPreparationRef, acquireCurrentStage, inspectCurrentStage, checkStageEvidence, SCHEMA, WORKFLOW, GH_VERSION, LANES, encodeRecord, decodeRecord, validateSelection, admitRecord, requireNativeContracts, + inspectArtifact, acquireArtifact, extractArtifact, acquirePreparation, checkNativeContracts, admitNativeEvidence, + acquireInputPreparation, readInputPreparation, checkInputTags, + mapVerifiedOutput, verifySubject, verifyStageSubject, frozenSubjects, releasePins, inspectPair, promote }; diff --git a/npm/agentplugins/scripts/npm-public-contract.js b/npm/agentplugins/scripts/npm-public-contract.js index 6735d907..70710370 100644 --- a/npm/agentplugins/scripts/npm-public-contract.js +++ b/npm/agentplugins/scripts/npm-public-contract.js @@ -41,28 +41,47 @@ function validateExpected(version, integrity, shasum) { if (!SHASUM.test(shasum)) fail("shasum must be an exact lowercase SHA-1 value"); } -function validatePackJSON(value, version) { +// Fixed packing identities only; registry/provenance policy remains agent-only. +function validateProductPackJSON(value, product, version) { + const name = product === "agentplugins" ? PACKAGE_NAME : + product === "plugin-kit-ai" ? "plugin-kit-ai" : null; + if (!name) fail("unknown fixed npm product"); + if (typeof version !== "string" || version.match(VERSION)?.[0] !== version) { + fail("version must be an exact stable semantic version"); + } let record; if (Array.isArray(value)) { if (value.length !== 1) fail("npm pack JSON must contain exactly one record"); [record] = value; } else if (value && typeof value === "object") { const keys = Object.keys(value); - if (keys.length !== 1 || keys[0] !== PACKAGE_NAME) { + if (keys.length !== 1 || keys[0] !== name) { fail("npm pack JSON must contain exactly one package-named record"); } - record = value[PACKAGE_NAME]; + record = value[name]; } else { fail("npm pack JSON must contain exactly one record"); } - if (!record || record.name !== PACKAGE_NAME || record.version !== version || - record.filename !== `${PACKAGE_NAME}-${version}.tgz`) { + if (!record || typeof record !== "object" || Array.isArray(record) || record.name !== name || + record.version !== version || + record.filename !== `${name}-${version}.tgz`) { fail("npm pack JSON package identity does not match the release"); } + if (typeof record.integrity !== "string" || typeof record.shasum !== "string") { + fail("npm pack JSON integrity and shasum must be strings"); + } + if (record.shasum.length !== 40) fail("shasum must be an exact lowercase SHA-1 value"); validateExpected(version, record.integrity, record.shasum); + if ("sha512-" + Buffer.from(record.integrity.slice(7), "base64").toString("base64") !== record.integrity) { + fail("npm pack JSON integrity must be canonical SHA-512 SRI"); + } return record; } +function validatePackJSON(value, version) { + return validateProductPackJSON(value, "agentplugins", version); +} + function validatePublicMetadata(metadata, version, integrity, shasum) { validateExpected(version, integrity, shasum); if (Array.isArray(metadata)) { @@ -261,6 +280,7 @@ module.exports = { validateDownloadedTarball, validateAuditSignatures, validatePackJSON, + validateProductPackJSON, validatePublicMetadata, validateSLSAAttestation }; diff --git a/npm/agentplugins/scripts/packed-installer-bridge.js b/npm/agentplugins/scripts/packed-installer-bridge.js index d87ee840..a81a05dc 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.js @@ -158,7 +158,18 @@ function publicInit(lane) { ...(lane.endsWith("remote") ? ["--url=https://docs.example.com/mcp"] : lane.endsWith("stdio") ? ["--runtime=node"] : []), ...(template === "hybrid" ? ["--mcp-template=mcp-" + lane.split("-")[1]] : [])]; } -function publicEvidence(cfg, native, configPath) { +function installerBoundary(projectRoot) { + return { executable_observation: "help-and-preflight-rejection", valid_add_dry_run: "not_evaluated", + argv: ["add", path.join(projectRoot, "skill"), "--target=codex", "--dry-run", "--format=json"], + reason: "production-security-inputs-not-offline" }; +} +function installerHelp(value) { + assert.deepEqual(value, { schema_version: 1, command: "help", result: "success", + data: { use: "agentplugins add", commands: null } }, "installer help visibility only"); +} +function publicEvidence(cfg, native, configPath, intake = "public-fixture/v1") { + assert.ok(["public-fixture/v1", "public-fixture/v2"].includes(intake), "explicit public schema required"); + const v2 = intake === "public-fixture/v2"; c.keys(cfg, ["prepare", "completionDigest", "evidenceOutput"], "public config"); const o = cfg.prepare, v = o.candidate; c.keys(o, ["candidate", "repo", "node", "npm", "output", "projectionPins", "pairMarkerDigest"], "public preparation"); @@ -169,8 +180,8 @@ function publicEvidence(cfg, native, configPath) { "pair_marker_sha256", "projection_pins", "fixtureRoot", "target", "packs", "binaries", "installations", "tools", "invocations", "invocations_sha256", "downloads_sha256", "result_sha256", "projects", "trees", "fixture_acquisition_execution", "qualification", "signed_promotion", "public_eligible", "release_eligible", - "platform_acceptance", "attested", "runtime_evidence"], "public terminal"); - assert.equal(native.schema, "dual-authoring-public-native/v1"); assert.equal(native.status, "completed"); + "platform_acceptance", "attested", "runtime_evidence", ...(v2 ? ["installer_boundary"] : [])], "public terminal"); + assert.equal(native.schema, v2 ? "dual-authoring-public-native/v2" : "dual-authoring-public-native/v1"); assert.equal(native.status, "completed"); falseClaims(native); assert.equal(native.signed_promotion, false); assert.equal(native.public_eligible, false); assert.equal(native.qualification, null); assert.equal(native.fixture_acquisition_execution, true); assert.equal(native.runtime_evidence, "not_evaluated"); assert.equal(native.target, "linux-amd64"); @@ -262,12 +273,20 @@ function publicEvidence(cfg, native, configPath) { } } command("plugin-kit-ai", ["update", "--all", "--format=json"], 2); - command("agentplugins", ["add", path.join(native.projects.agentplugins, "skill"), "--target=codex", "--dry-run", "--format=json"]); + if (v2) { + assert.deepEqual(native.installer_boundary, installerBoundary(native.projects.agentplugins), "outstanding production add requirement"); + command("agentplugins", ["add", "--help", "--format=json"]); + command("agentplugins", ["add", path.join(native.projects.agentplugins, "skill"), "--target=codex", "--scope=project", "--dry-run", "--format=json"], 1); + } else command("agentplugins", ["add", path.join(native.projects.agentplugins, "skill"), "--target=codex", "--dry-run", "--format=json"]); assert.equal(native.invocations, expected.length); assert.equal(invocations.length, expected.length); invocations.forEach((row, i) => { c.keys(row, ["product", "argv", "status", "signal", "stdout", "stderr"], "public invocation"); const want = expected[i]; assert.equal(row.product, want.product); assert.deepEqual(row.argv, want.argv); - assert.equal(row.status, want.status); assert.equal(row.signal, null); assert.equal(row.stderr, ""); assert.equal(typeof row.stdout, "string"); + assert.equal(row.status, want.status); assert.equal(row.signal, null); assert.equal(typeof row.stdout, "string"); + const rejection = v2 && i === 70; + assert.equal(row.stderr, rejection ? "agentplugins: --scope project is not supported by the current client adapters; the public CLI supports user scope only\n" : ""); + if (rejection) assert.equal(row.stdout, ""); + if (v2 && i === 69) installerHelp(JSON.parse(row.stdout)); if (want.author) { const result = JSON.parse(row.stdout); assert.equal(result.result, "success"); assert.equal(result.schema_version, 1); assert.equal(result.data.revision, v.identity.commit); assert.equal(result.data.engine, "standard-first-slice/1"); @@ -286,27 +305,57 @@ function publicEvidence(cfg, native, configPath) { return { identity: v.identity, repo: o.repo, candidate_sha256: v.manifestDigest, packs: native.packs, projects, snapshots: [...roots.slice(1).map(root => snapshot(root)), snapshot(native.fixtureRoot, true)], public_evidence: { schema: native.schema, signed_promotion: false, public_eligible: false, qualification: null, - pair_marker_sha256: native.pair_marker_sha256, tools: native.tools, binaries: native.binaries } }; + 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.equal(request.intake, "public-fixture/v1"); assert.equal(request.disposableEvidence, true); + assert.ok(["public-fixture/v1", "public-fixture/v2"].includes(request.intake)); assert.equal(request.disposableEvidence, true); pin(request.nativeConfig, request.nativeConfigSha256); const cfg = json(request.nativeConfig); const terminal = path.join(cfg.evidenceOutput, "public-native-completion.json"); pin(terminal, request.nativeCompletionSha256); const native = json(terminal); assert.equal(native.identity.commit, request.expectedCommit); assert.equal(native.fixtureRoot, request.fixtureRoot); - return publicEvidence(cfg, native, request.nativeConfig); + return publicEvidence(cfg, native, request.nativeConfig, request.intake); } 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); @@ -315,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"); @@ -326,11 +380,21 @@ 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))); } else throw new Error("usage: node packed-installer-bridge.js seal REQUEST OUTPUT | verify CONFIG SHA256 COMMIT"); } catch (error) { process.stderr.write(`packed installer bridge: ${error.message}\n`); process.exitCode = 1; } } -module.exports = { LANES, PRODUCTS, intake, seal, verify, snapshot, publicInit, publicEvidence, publishSeal }; +module.exports = { LANES, PRODUCTS, intake, seal, verify, snapshot, publicInit, publicEvidence, publishSeal, installerBoundary, installerHelp }; diff --git a/npm/agentplugins/scripts/packed-installer-bridge.md b/npm/agentplugins/scripts/packed-installer-bridge.md index ca52892f..cfbfd2a9 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.md +++ b/npm/agentplugins/scripts/packed-installer-bridge.md @@ -180,9 +180,9 @@ and attestation remain false. Local synthetic/source tests are not this run. Public preparation is **not** private native completion. The public producer `test/public-authoring-native.test.js` now emits an exclusive -`public-native-completion.json` (`dual-authoring-public-native/v1`) only after +`public-native-completion.json` (`dual-authoring-public-native/v2`) only after both products finish five lanes, `extra-skill`, static parity, isolation, cache -recovery, and lifecycle checks. Its 70 authoring/installer invocation records +recovery, and lifecycle checks. Its 71 authoring/installer invocation records are ordered and complete. Five successful npm installations separately bind actual fixture tarball paths and SHA256 values (independent prefixes, shared prefix, and reinstall). Preparation tarballs remain separate, unqualified @@ -200,7 +200,7 @@ read-only byte consistency; it does not authenticate an arbitrary supplied log. Use the existing six request fields (`expectedCommit`, `nativeConfig`, `nativeConfigSha256`, `nativeCompletionSha256`, `fixtureRoot`, `disposableEvidence`) with the additional exact field -`"intake": "public-fixture/v1"`. The completion digest must independently pin +`"intake": "public-fixture/v2"`. The completion digest must independently pin `public-native-completion.json`. Omitting `intake` still selects the unchanged strict private schema. Unknown intake values and public/private substitution fail closed. Seal with the same `packed-installer-bridge.js seal REQUEST OUTPUT` @@ -233,3 +233,289 @@ The final integrated source (including the independently owned preparation initialization fix) still needs owner-scheduled public native execution and packed planner acceptance. These focused synthetic tests do not satisfy that E2E or the external release/required-check prerequisites. + +Public v2 explicitly narrows executable installer evidence to command visibility +and preflight rejection. Rows 1–69 retain the original authoring sequence; row 70 +is `agentplugins add --help --format=json` (one successful help JSON document, +empty stderr); row 71 adds `--scope=project` to the original generated Skill +vector and requires exit 1, no signal, empty stdout and the exact user-scope-only +error. Totals are 69 zero exits, one retired-command exit 2, and one preflight +exit 1. Both project trees, client and installer roots retain entries/bytes/modes. +The test-only helper rejects valid installer vectors before spawning; it does +not establish process-level network denial for Go descendants. + +The required `installer_boundary` is sealed into `public_evidence` and the +public summary: `executable_observation: help-and-preflight-rejection`, +`valid_add_dry_run: not_evaluated`, `reason: production-security-inputs-not-offline`, +and `argv` retains the original valid add vector without the rejected scope. +Strict v1 intake remains explicitly selectable and cannot accept v2; private +schemas/counts remain unchanged. `check_public(..., require_valid_add=True)` +rejects this evidence as insufficient for successful production add. + +The existing ten-project/thirty-plan acceptance still runs the real Go CLI and +planner with injected detector/security/effects. It does not execute production +main, Security Index or lintai. Help, rejection and injected assessments cannot +close Milestone A's still-required successful distributed installer dry-run on +generated projects. That separate legitimate installer acceptance must bind +actual public launcher/native bytes, genuine security inputs, structured plans, +source/client preservation and separately accounted scanner/cache/acquisition +effects. It remains outstanding after v2 core success, independent review and +fresh successor E2E; no historical candidate bytes may be relabelled. + +### C2 production acquisition boundary + +The same public launchers and kit postinstall accept the fixed v2 descriptor +and complete `native-inputs.json` binding. V1 qualification/null behavior stays +unchanged. V2 alone recognizes `UAP_PUBLIC_AUTHORING_ASSET_FILE` as an untrusted +absolute locator in an owned private custody directory. Every supplied locator +is checked before cache effects, including on warm hits; invalid input never +falls back to download. Outer and inner pins come only from package metadata. +Local and absent-locator anonymous acquisition converge on the existing locked, +verified cache under `public-authoring-v2`. The supplied file is never executed. + +Bounded retained metadata/asset snapshots are rechecked before commit and return; +close or cleanup uncertainty returns failure. The child environment removes the +locator and existing proof controls. These checks assume an owned, quiescent +namespace, without hostile same-UID, mount-replacement or all-host guarantees. +Callers must separately keep project/client/evidence outputs outside custody. + +C2 source fixtures establish structural and interface behavior only. They do +not authenticate C1 custody, qualify native inputs, or change either existing +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`. +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 +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. + +### C3b first invariant: source-frozen public tools + +The controller now reads only `.github/authoring-public-tools.json` beside the +executing trusted checkout. `expectedCommit`, receipt pins, PATH and environment +cannot select that root or an interpreter. `options.node` is comparison only. +`authoring-public-tools/v1` fixes six controller keys, eighteen cell keys and the +Linux-amd64 reader. The closed, ordered JSON encoding is two-space indentation +plus one LF, using the existing encoding/checked-file interface in JavaScript +and minimal standard-library checks in Python. Limits are 1 MiB and depth eight; +unknown, missing, duplicate, reordered and alternate-encoded fields fail. + +All fields remain present. `null` means approved provision is unavailable; +there are no approved pins in this checkout. Controller rows contain +`node,python,git,gh,tar`, each eventually `{path,version,sha256}`. Cell +`runner,image,observer,installer_policy` are immutable `{id,sha256}` bindings +supplied by approved provisioning, not guessed runner labels. `npm_node` and +`shim_node` must match the cell major. npm additionally requires +`closure:{root,files:[{path,sha256}]}`: the sorted exhaustive regular-file closure, +including its CLI and dependencies. `mod_cache` uses that same closure shape. +Closures allow at most 4096 files, 8192 entries and 256 MiB; links and path aliases +fail. Go and module cache are required only for Linux-amd64/pair-node22; +installer policy is required for pair cells. Other null capabilities fail with +`PUBLIC_PROVISIONING_REQUIRED::` before execution. +Provisioned versions are frozen metadata bound to bytes, not inferred by running +a tool. Provisioning owners must supply authentic supply-chain instructions and +immutable host/image identities before any real cell can run. + +`readProvisioning()` takes no root argument. `requireController(key)` returns a +verified fixed Node path; `requireCellTools(key)` checks that cell's provision. +Python `require_authenticated_controller()` accepts no caller input. Its initial +TCB is trusted checkout plus trusted Python/workflow OS. Prepared subprocess paths +recheck tools and trusted source before and after invocation, using an environment +that excludes interpreter injection settings. The external provision contract +must keep paths immutable throughout execution; hashes do not prevent hostile +same-UID replacement between checks. + +A verified controller does **not** open authenticated summary success. A separate +explicit `C3b execution incomplete` gate remains before receipt reads/effects: +full result/installer/observer validation and independent invocation authority +are still missing. Synthetic unit fixtures mock that later capability and child +execution only; harmless tool bytes are never executed and establish no authentic +acceptance. Full producer/validators/facades, workflow, J/E/P, genuine matrix and +remaining release gates remain mandatory next-lane work. + +### C3b delivery step 2: fixed scenarios and producer + +This checkpoint implements `scenarioContract`, `verifyNpmLifecycle`, +`verifyCacheProcess`, `verifyResults` and `produceJourney` in the authentic +reader module. The original 55 kit / 127 pair core rows, including all eighteen +installer rows per pair, remain unchanged. A separate immutable inventory adds +five kit or 34 pair npm lifecycle rows, cache acquisition/repair/invalid-locator +rows, simultaneous requests, literal arguments and host cancellation scenarios. +Core command indices link observed processes to `commands.json`; they do not +replace its inventory. No caller supplies commands, validators or policy. + +The producer uses the accepted source-frozen tools API and closed +`authoring-public-produce/v1` request. It authenticates I/S before installing, +compares both original packs and all native subject pins, derives fresh disjoint +roots, invokes actual npm and its installed shims, finalizes the supported +observer, rechecks source/tools/custody and then writes J plus local admission. +Kit postinstall must use the selected npm Node. POSIX invokes the installed +shim directly; Windows uses fixed `.cmd` version invocations and PowerShell +`.ps1` calls with separately quoted literal arguments. npm install retains +`--offline --ignore-scripts=false --foreground-scripts --no-audit --no-fund`; +uninstall retains the corresponding fixed flags and exact package name. + +The three new records are closed, ordered schema/cell/row tables. Process rows +bind ID/core index, actual argv/cwd/environment, executable/runtime identities, +stdout/stderr size and SHA256, exit/signal, before/after project/prefix/cache/ +client/state/input identities, observed boundaries/counts, postinstall and +monotonic intervals and the pinned literal-argument manifest effect. Boundary +events are chronological: reached native/waiter boundary before cancellation, +then final reaping; a cancelled waiter never launches native code. Repair +records the exact intentional corruption before checked recovery. Cache +evidence also binds descendant finalization; +installer evidence binds assessment and eighteen readbacks. Raw observations +and assessment are pinned `sidecars/` files, checked exhaustively with the +128 MiB aggregate ceiling. The three scenario record files carry +`rows: {shards: [{path,size,sha256}]}` with fixed ordered +`sidecars/-rows-.json` transcript names. Each process row +and envelope stays within 1 MiB; each ordered transcript shard stays within +16 MiB. Readers verify pins, canonical partitioning and the aggregate before +accepting expanded rows. Long workspace paths do not require omitting rows. +Core transcripts retain 16 MiB and each output retains 1 MiB limits. Overflow +prevents completion. Equality failures report a bounded assertion label rather +than constructing potentially enormous diagnostic object diffs. + +Result validation checks engine/product versions, embedded schema and profile +pins, command and client inventories, read profile, independent policy states, +exact components and names, mutation effects, manifest identities and pair JSON +and tree equality. Only explicit product/version and displayed invocation +prefix differences are normalized. Directory and file modes and empty +directories remain evidence. Cache checks require real repair, warm zero new +acquisition/commit/download, four overlapping cold requests plus two warm, +peer namespace preservation and reached cancellation/waiter boundaries. + +**Execution prerequisites remain absent on this source.** The fixed modules +must be supplied and reviewed by their existing owners: + +- `public-authoring-custody.js`: `readPublicInputs` returning authenticated + `{stage,input}` with original retained subjects and checked pack closure. +- `public-process-observation.js`: `openPublicObservation` and + `verifyPublicObservation`. Its session supplies `run`, `cancel`, `finish`. +- `public-installer-evidence.js`: `requirePublicInstaller` and + `verifyPublicInstaller`, covering actual clean Codex detection, all three + lifecycle sources, genuine assessment/services and state/client readbacks. + +The process port's concrete return protocol for this consumer is +`run -> {row,stdout,stderr}` and +`finish -> {finalization,assessment,readbacks}`. The observer owns pinned raw +sidecars in the supplied evidence root. `cancel({id,event})` uses the fixed +scenario's reached boundary; it cannot silently pass an unreached signal. +`verifyPublicObservation` returns checked `{rows,finalization}`; +`verifyPublicInstaller` returns checked `{assessment,readbacks}`. Boolean +success is rejected. These are required integration contracts, not supplied +observer/security implementations or authentic execution evidence. + +Missing exports fail before npm/native effects with their exact module/export +names. Post-admission setup, process, cancellation and finalization failures +retain bounded diagnostics, including nested primary causes, without writing J. +Synthetic unit fixtures establish semantic and orchestration controls +only. The same-live-root bridge receives the producer's original ten projects; +it must still run and independently verify its ten leaves/thirty plans in the +later authorized integrated invocation. No Go gate is executed by this writer. +Completed remote J/E, aggregation, E2, workflow and P remain mandatory step 3; +`readAcceptance` stays closed. Full phases 0–11, genuine E2E, native/N2, release +qualification and distribution gates remain open. This patch awaits independent +review and does not constitute authentic public acceptance. + +### R1/R2 bounded source correction (2026-09-10) + +The authentic public validator now requires the exact five fixed `publicInit` +closures: original and extra Skills, manifest, README, .gitignore, remote MCP +references and the complete generated Node stdio sources/package/lock bytes. +File and directory inventories and host modes are checked independently for each +product. Captured file sizes/hashes bind the expected bytes before ordinary +`agentplugins-tree-sha256-v1` framing is recomputed and compared with every +retained read identity. The root is omitted from that engine digest; directory +entries are included. The snapshot still seals the root and all empty directories. +No additional empty directory is generated by these fixed commands. Existing +live snapshot, bridge, evidence bounds and command inventories remain mandatory. + +Successful observer acquisition enters finalization scope before session method +validation. An available finish is called exactly once even when run/cancel is +missing; validation and finalizer errors are retained together, with no J. +The observation owner must release all resources if open throws or returns no +usable finalizer. The caller cannot finalize an absent method; its failure receipt +is not evidence of quiescence. Only synthetic owner-interface tests cover this +correction; no real observer experiment or substitute observer was performed. + +This correction does not authenticate execution or accept E2E/release. Genuine +facades/provision, step 3 and full phases 0–11 remain open. N2 cyber refusal is +NOT ACCEPTED. Refused security/crypto/ZIP work must not be retried, rerouted or +replaced. Quarantined Windows parent-sharing/concurrency reproducers, ptrace or +alternate observers, denied localhost/private-network/raw-download probes, +network/auth/download/native execution and provisioning remain excluded. diff --git a/npm/agentplugins/scripts/packed-installer-bridge.test.js b/npm/agentplugins/scripts/packed-installer-bridge.test.js index 1f2f763a..eaed11da 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.test.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.test.js @@ -167,3 +167,96 @@ test('SYNTHETIC public intake: exact lanes, executed packs, pins and schema sepa } finally {c.frozenCandidate=original;} assert.throws(()=>bridge.seal(f.request),'unstubbed candidate rejects synthetic bytes'); }); + +// v1 remains independently accepted; v2 must be explicitly selected. +test('SYNTHETIC v2 help/preflight boundary cannot stand for production add', posixFixture, t => { + const original = c.frozenCandidate, f = publicFixture(t); + c.frozenCandidate = () => ({manifest:{products:f.assets,build:{go_sha256:hash(f.tool)}}}); + const inv = path.join(path.dirname(f.nativePath), 'invocations.json'); + const oldRows = JSON.parse(fs.readFileSync(inv)); + const help = {product:'agentplugins', argv:['add','--help','--format=json'], status:0, signal:null, stderr:'', + stdout:JSON.stringify({schema_version:1,command:'help',result:'success',data:{use:'agentplugins add',commands:null}})}; + const rejection = {...help, argv:['add',path.join(f.projects.agentplugins,'skill'),'--target=codex','--scope=project','--dry-run','--format=json'], + status:1, stdout:'', stderr:'agentplugins: --scope project is not supported by the current client adapters; the public CLI supports user scope only\n'}; + const rows = [...oldRows.slice(0,69), help, rejection]; + const boundary = {executable_observation:'help-and-preflight-rejection',valid_add_dry_run:'not_evaluated', + argv:oldRows[69].argv,reason:'production-security-inputs-not-offline'}; + const terminal = {...f.terminal,schema:'dual-authoring-public-native/v2',installer_boundary:boundary}; + function seal(changes = {}, observations = rows, intake = 'public-fixture/v2') { + write(inv, observations); + write(f.nativePath, {...terminal,invocations:observations.length,invocations_sha256:hash(inv),...changes}); + return bridge.seal({...f.request,intake,nativeCompletionSha256:hash(f.nativePath)}); + } + try { + const accepted = seal(); + assert.equal(rows.length,71); + assert.deepEqual(rows.reduce((n,r)=>(n[r.status]=(n[r.status]||0)+1,n),{}),{0:69,1:1,2:1}); + assert.deepEqual(accepted.inputs.public_evidence.installer_boundary,boundary); + assert.throws(()=>seal({},rows,'public-fixture/v1')); + assert.throws(()=>seal({schema:'dual-authoring-public-native/v1'})); + for (const installer_boundary of [undefined,{},true,{...boundary,valid_add_dry_run:true}, + {...boundary,valid_add_dry_run:'success'},{...boundary,reason:undefined},{...boundary,argv:rejection.argv}, + {...boundary,executable_observation:'successful-add'},{...boundary,accepted:true}]) { + assert.throws(()=>seal({installer_boundary})); + } + for (const bad of [oldRows, rows.slice(0,70), [...rows.slice(0,69),rejection], + [...rows.slice(0,69),rejection,help], [...rows.slice(0,69),help,help]]) assert.throws(()=>seal({},bad)); + for (const i of [69,70]) for (const change of [{status:0},{status:2},{signal:'SIGTERM'}, + {stdout:'{}'},{stdout:help.stdout+'{}'},{stderr:'arbitrary'},{stderr:''}, + {argv:oldRows[69].argv}]) { + if (Object.entries(change).every(([k,v])=>JSON.stringify(rows[i][k])===JSON.stringify(v))) continue; + assert.throws(()=>seal({},rows.map((r,j)=>j===i?{...r,...change}:r))); + } + for (const value of [{}, {schema_version:1,command:'help',result:'failure',data:{use:'agentplugins add',commands:null}}, + {schema_version:1,command:'help',result:'success',data:{use:'agentplugins',commands:null}}]) { + assert.throws(()=>seal({},rows.map((r,i)=>i===69?{...r,stdout:JSON.stringify(value)}:r))); + } + assert.deepEqual(seal().inputs.public_evidence.installer_boundary,boundary); + } 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..bf642979 --- /dev/null +++ b/npm/agentplugins/scripts/public-authoring-acceptance.js @@ -0,0 +1,1201 @@ +"use strict"; + +// C3 local custody, fixed producer and semantic evidence contracts. Completed E +// admission belongs to step 3; installer/observation/custody engines stay external. +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: PUBLIC_FACADE_REQUIRED:public-authoring-custody.js#readPublicInputs,public-process-observation.js#openPublicObservation,public-process-observation.js#verifyPublicObservation,public-installer-evidence.js#requirePublicInstaller,public-installer-evidence.js#verifyPublicInstaller"; +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]) })))); +// Compare without asking node:assert to render an unbounded object/Buffer diff. +// Rejected transcript-sized values must remain cheap to report under memory limits. +const agree = (a, b, label) => assert.ok(require('node:util').isDeepStrictEqual(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"); hostAbsolute(t.path, selected.key); 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) hostAbsolute(value.projects[p], selected.key); + 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 = {}, budget = { size: j.evidence.reduce((n, row) => n + row.size, 0) }; + 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] = expandEvidence(row.path, bounded(bytes, row.path === "commands.json" ? TRANSCRIPT_LIMIT : LIMIT), a.journey_root, budget); + } + 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)] }; +} +// C3b step 2. These pure contracts are replayed against authenticated observations; +// neither a caller assertion nor a synthetic unit fixture authenticates execution. +const AGGREGATE_LIMIT = 128 * LIMIT; +const PRODUCE_FIELDS = ['schema', 'selected', 'workflow_sha', 'stage', 'input_file', 'repo', 'work_parent', 'output', 'cell', 'tools', 'producer']; +const FACADES = Object.freeze({ + 'public-authoring-custody': ['readPublicInputs'], + 'public-process-observation': ['openPublicObservation', 'verifyPublicObservation'], + 'public-installer-evidence': ['requirePublicInstaller', 'verifyPublicInstaller'] +}); +function requireFacades(key) { + const result = {}, missing = []; + for (const [name, exports] of Object.entries(FACADES)) { + if (name === 'public-installer-evidence' && cell(key).node === 18) continue; + const file = path.join(__dirname, name + '.js'); + if (!fs.existsSync(file)) { missing.push(...exports.map(e => `${name}.js#${e}`)); continue; } + const api = require(file); + for (const e of exports) if (typeof api[e] !== 'function') missing.push(`${name}.js#${e}`); + result[name] = api; + } + assert.equal(missing.length, 0, `C3b required: PUBLIC_FACADE_REQUIRED:${missing.join(',')}`); + return result; +} +const hostPath = key => key.startsWith('windows-') ? path.win32 : path.posix; +function hostAbsolute(value, key) { + const p = hostPath(key); + assert.ok(typeof value === 'string' && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) && p.isAbsolute(value) && + p.normalize(value) === value && value !== p.parse(value).root && !value.startsWith('\\\\'), 'canonical recorded host path'); + return value; +} +function freeze(value) { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); } return value; } +function list(value, count, label) { + assert.ok(Array.isArray(value) && value.length === count, label); + agree(Reflect.ownKeys(value), [...value.map((_, i) => String(i)), 'length'], `${label} plain array`); +} +function nonnegative(value, maximum, label) { assert.ok(Number.isSafeInteger(value) && value >= 0 && value <= maximum, label); } +function textValue(value, maximum = LIMIT) { assert.ok(typeof value === 'string' && Buffer.byteLength(value) <= maximum && !value.includes('\0'), 'bounded text'); } +function digestID(value) { assert.match(value, /^sha256:[0-9a-f]{64}$/); hash(value.slice(7), 'digest identity'); } +function sidecar(value) { + fields(value, ['path', 'size', 'sha256'], 'observation sidecar'); + assert.match(value.path, /^sidecars\/[a-z0-9][a-z0-9._-]{0,150}$/); + positive(value.size, TRANSCRIPT_LIMIT, 'sidecar size'); hash(value.sha256, 'sidecar'); +} +/** Supplementary rows have a separate identity/count; core55/core127 never change. */ +function scenarioContract(key) { + const selected = cell(key), npm = [], cache = [], installer = []; + const add = (into, product, kind, prefix, cacheName, suffix = kind, group = null, command = null, event = null) => + into.push({ id: `${product}/${prefix}/${suffix}`, product, kind, prefix, cache: cacheName, group, command, event }); + for (const p of selected.products) { + const prefix = `alone-${p}`; + for (const k of ['install', 'probe', 'uninstall', 'reinstall', 'probe-reinstalled']) add(npm, p, k, prefix, prefix); + } + if (selected.products.length === 2) for (const order of [selected.products, [...selected.products].reverse()]) { + const prefix = `shared-${order[0]}`; + for (const p of order) { add(npm, p, 'install', prefix, prefix); add(npm, p, 'probe', prefix, prefix); } + for (const p of order) { + const peer = order.find(x => x !== p); + add(npm, p, 'uninstall', prefix, prefix); + add(npm, peer, 'probe-peer', prefix, prefix, `peer-after-${p}`); + add(npm, p, 'reinstall', prefix, prefix); + add(npm, p, 'probe-reinstalled', prefix, prefix); + } + } + for (const p of selected.products) { + const prefix = `alone-${p}`; + for (const k of ['cold', 'warm', 'repair', 'invalid-cold', 'invalid-warm']) + add(cache, p, k, prefix, k === 'invalid-cold' ? `invalid-${p}` : `serial-${p}`); + for (let i = 0; i < 4; i++) add(cache, p, 'concurrent-cold', prefix, `concurrent-${p}`, `concurrent-cold-${i}`, `cold-${p}`); + for (let i = 0; i < 2; i++) add(cache, p, 'concurrent-warm', prefix, `concurrent-${p}`, `concurrent-warm-${i}`); + add(cache, p, 'literal-argv', prefix, `serial-${p}`); + if (key.startsWith('windows-')) { + add(cache, p, 'cancel', prefix, `serial-${p}`, 'console-cancel', null, null, 'CTRL_C_EVENT'); + add(cache, p, 'cancel', prefix, `serial-${p}`, 'process-cancel', null, null, 'TerminateProcess'); + } else for (const event of ['SIGINT', 'SIGTERM']) add(cache, p, 'cancel', prefix, `serial-${p}`, event, null, null, event); + add(cache, p, 'waiter-owner', prefix, `waiter-${p}`, 'waiter-owner', `waiter-${p}`); + add(cache, p, 'waiter-cancel', prefix, `waiter-${p}`, 'waiter-cancel', `waiter-${p}`, null, + key.startsWith('windows-') ? 'CTRL_C_EVENT' : 'SIGINT'); + } + if (selected.products.length === 2) for (const p of selected.products) + add(cache, p, 'peer-overlap', `alone-${p}`, 'peer-overlap', 'peer-overlap', 'peer-overlap'); + commandContract(key).forEach((r, i) => add(r.id.startsWith('installer/') ? installer : cache, r.product, 'core', + `alone-${r.product}`, `alone-${r.product}`, `core-${r.id}`, null, i)); + return freeze({ npm, cache, installer }); +} +function rootsFor(output, key) { + const p = hostPath(key); hostAbsolute(output, key); + return Object.fromEntries(['input', 'stage', 'admission', 'projects', 'npm', 'cache', 'client', 'state', 'evidence', 'scenarios'] + .map(n => [n, p.join(output, n)])); +} +function scopePaths(j, scenario, roots) { + const p = hostPath(j.cell), name = scenario.cache; + return { prefix: p.join(roots.npm, scenario.prefix, 'prefix'), home: p.join(roots.cache, name), + cwd: scenario.kind === 'core' ? commandCwd(j, commandContract(j.cell)[scenario.command]) : + p.join(roots.scenarios, scenario.product, scenario.kind === 'literal-argv' ? "cwd spaces ü 'quotes' $literal ; &" : scenario.prefix), + userconfig: p.join(roots.npm, scenario.prefix, 'user.npmrc'), globalconfig: p.join(roots.npm, scenario.prefix, 'global.npmrc'), + npmCache: p.join(roots.npm, scenario.prefix, 'npm-cache') }; +} +function commandCwd(j, want) { + const p = hostPath(j.cell), parent = j.projects[want.product]; + return want.scenario === 'projects' ? parent : p.join(p.dirname(parent), `${want.product} malformed-skill ü`); +} +const LITERAL_DESCRIPTION = 'Use spaces ü "double" \'single\' $HOME $(literal) `literal` ; & | < > %PATH% !literal!'; +function plannedInvocation(j, scenario, roots) { + const p = hostPath(j.cell), s = scopePaths(j, scenario, roots), windows = j.cell.startsWith('windows-'); + const asset = p.join(roots.input, j.subjects[scenario.product][cell(j.cell).target].file); + const env = { HOME: s.home, TMPDIR: p.join(s.home, 'tmp'), + PATH: p.dirname(j.tools[['install', 'reinstall', 'uninstall'].includes(scenario.kind) ? 'npm_node' : 'shim_node'].path), LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', + NPM_CONFIG_USERCONFIG: s.userconfig, NPM_CONFIG_GLOBALCONFIG: s.globalconfig, NPM_CONFIG_CACHE: s.npmCache, + NPM_CONFIG_OFFLINE: 'true', NPM_CONFIG_AUDIT: 'false', NPM_CONFIG_FUND: 'false', + UAP_PUBLIC_AUTHORING_ASSET_FILE: scenario.kind.startsWith('invalid-') ? p.join(roots.input, 'absent-invalid-locator') : asset, + CODEX_HOME: p.join(roots.client, 'codex'), XDG_CONFIG_HOME: roots.client, XDG_STATE_HOME: roots.state, + XDG_DATA_HOME: p.join(roots.state, 'data'), XDG_CACHE_HOME: p.join(s.home, '.cache') }; + if (windows) { + // Frozen OS destinations, never inherited COMSPEC/PowerShell or caller shell. + env.USERPROFILE = s.home; env.TEMP = env.TMP = p.join(s.home, 'tmp'); + env.SystemRoot = 'C:\\Windows'; env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'; + env.PATHEXT = '.COM;.EXE;.BAT;.CMD'; env.LOCALAPPDATA = p.join(s.home, 'AppData', 'Local'); + } + let argv; + if (['install', 'reinstall', 'uninstall'].includes(scenario.kind)) { + const uninstall = scenario.kind === 'uninstall'; + argv = [j.tools.npm_node.path, j.tools.npm.path, uninstall ? 'uninstall' : 'install', '--global', '--prefix', s.prefix, + '--offline', '--ignore-scripts=false', ...(!uninstall ? ['--foreground-scripts'] : []), '--no-audit', '--no-fund', + uninstall ? contract.PACKAGES[scenario.product] : p.join(roots.stage, j.packs[scenario.product].file)]; + } else { + let args = scenario.kind === 'core' ? [...commandContract(j.cell)[scenario.command].argv] : + scenario.kind === 'literal-argv' ? [...(scenario.product === 'agentplugins' ? ['author'] : []), 'init', 'literal project ü', + '--name', 'literal-project', '--template=skill', '--description', LITERAL_DESCRIPTION, '--format=json'] : ['version', '--format=json']; + if (scenario.kind === 'core') { + const core = commandContract(j.cell)[scenario.command]; + if (core.id.startsWith('installer/') && /\/(dry-run|add)$/.test(core.id)) args[1] = p.join(j.projects[core.product], core.lane); + } + const shim = p.join(s.prefix, ...(windows ? [] : ['bin']), scenario.product); + // PowerShell's call operator with independently single-quoted array elements + // preserves metacharacters. .cmd is exercised separately on core version rows. + if (windows && scenario.kind === 'core' && commandContract(j.cell)[scenario.command].id === 'product-version') { + const quote = v => { assert.ok(!/["%\r\n!]/.test(v), 'cmd fixed version tokens'); return '"' + v + '"'; }; + argv = [env.ComSpec, '/d', '/s', '/c', '"' + [shim + '.cmd', ...args].map(quote).join(' ') + '"']; + } else if (windows) { + const quote = v => "'" + v.replaceAll("'", "''") + "'"; + argv = ['C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', '-NoLogo', '-NoProfile', '-NonInteractive', + '-Command', '& ' + [shim + '.ps1', ...args].map(quote).join(' ') + '; exit $LASTEXITCODE']; + } else argv = [shim, ...args]; + } + return { id: scenario.id, argv, cwd: s.cwd, env }; +} +function tree(value, key, expectedRoot) { + fields(value, ['root', 'sha256', 'entries'], 'tree snapshot'); hostAbsolute(value.root, key); + if (expectedRoot !== undefined) agree(value.root, expectedRoot, 'snapshot root'); + assert.ok(Array.isArray(value.entries) && value.entries.length > 0 && value.entries.length <= 8192, 'tree entries bound'); + hash(value.sha256, 'tree'); agree(value.sha256, c.digest(c.encode(value.entries)), 'tree digest'); + const seen = new Set(); + for (const e of value.entries) { + fields(e, e.kind === 'directory' ? ['path', 'mode', 'kind'] : ['path', 'mode', 'kind', 'size', 'sha256'], 'project tree entry'); + assert.ok(e.path === '.' || typeof e.path === 'string' && e.path.length <= 4096 && !e.path.startsWith('/') && + !e.path.includes('\\') && !e.path.split('/').some(x => !x || x === '.' || x === '..'), 'relative project entry'); + assert.ok(!seen.has(e.path.toLowerCase()), 'unique tree path'); seen.add(e.path.toLowerCase()); + assert.ok(['file', 'directory'].includes(e.kind), 'project regular file/directory'); + nonnegative(e.mode, 0o777, 'exact host mode'); + if (e.kind === 'file') { nonnegative(e.size, LIMIT, 'file bound'); hash(e.sha256, 'file'); } + assert.ok(!/(^|\/)(hooks|plugin\.yaml)(\/|$)/.test(e.path), 'no generated legacy manifest/root hooks'); + } + agree(value.entries[0].path, '.', 'tree root entry'); + for (const e of value.entries.slice(1)) { + const parent = path.posix.dirname(e.path); assert.ok(value.entries.some(x => x.path === parent && x.kind === 'directory'), 'explicit parent including empty directories'); + } + return value; +} +const CORRUPTION_BYTES = Buffer.from('C3 intentional owned cache corruption\n'); +function binary(value, j, product, corrupted = false) { + if (value === null) return; + fields(value, ['path', 'sha256', 'size', 'mode'], 'observed binary'); hostAbsolute(value.path, j.cell); + const expected = corrupted ? { sha256: c.digest(CORRUPTION_BYTES), size: CORRUPTION_BYTES.length } : j.subjects[product][cell(j.cell).target].binary; + agree(value.sha256, expected.sha256, 'I inner binary bytes'); agree(value.size, expected.size, 'I inner binary size'); + agree(value.mode, j.cell.startsWith('windows-') ? 0o666 : 0o755, 'native executable host mode'); +} +function observedState(value, j, corruptedProduct = null) { + fields(value, ['projects', 'prefix', 'cache', 'client', 'state', 'inputs'], 'observed state'); + for (const k of ['projects', 'client', 'state', 'inputs']) hash(value[k], k); + fields(value.prefix, cell(j.cell).products, 'prefix products'); fields(value.cache, cell(j.cell).products, 'cache products'); + for (const product of cell(j.cell).products) { + const pkg = value.prefix[product]; + if (pkg !== null) { + fields(pkg, ['tree', 'shims'], 'installed package'); hash(pkg.tree, 'package tree'); + const kinds = j.cell.startsWith('windows-') ? ['posix', 'cmd', 'powershell'] : ['posix']; list(pkg.shims, kinds.length, 'actual npm shim inventory'); + pkg.shims.forEach((shim, i) => { + fields(shim, ['kind', 'path', 'sha256', 'mode', 'target'], 'npm shim'); agree(shim.kind, kinds[i], 'shim kind'); + hostAbsolute(shim.path, j.cell); hash(shim.sha256, 'shim'); nonnegative(shim.mode, 0o777, 'shim mode'); + if (j.cell.startsWith('windows-')) { agree(shim.target, null, 'Windows regular shim'); agree(shim.mode, 0o666, 'Windows shim mode'); } + else { textValue(shim.target, 4096); assert.ok(shim.target.endsWith(`/bin/${product}.js`), 'npm link to product bin'); agree(shim.mode, 0o777, 'POSIX npm symlink'); } + }); + } + binary(value.cache[product], j, product, product === corruptedProduct); + } +} +function expectedCachePath(j, roots, scenario, product) { + const p = hostPath(j.cell), native = j.subjects[product][cell(j.cell).target].binary; + return p.join(scopePaths(j, scenario, roots).home, '.cache', 'universal-agent-plugins', 'public-authoring-v2', contract.MODE, + j.identity.commit, j.candidate_sha256, product, j.identity.versions[product], cell(j.cell).target, native.sha256, native.file); +} +function verifyObservedRow(row, scenario, j, roots, commands) { + fields(row, ['id', 'command', 'argv', 'cwd', 'env', 'executable', 'runtime', 'stdout', 'stderr', 'status', 'signal', + 'before', 'after', 'observation', 'events', 'acquisitions', 'commits', 'downloads', 'native_launches', 'postinstall', 'interval', 'literal'], 'observed process row'); + agree(row.id, scenario.id, 'fixed scenario ID'); agree(row.command, scenario.command, 'core reference'); + const want = plannedInvocation(j, scenario, roots); + for (const k of ['argv', 'cwd', 'env']) agree(row[k], want[k], `actual ${k}`); + fields(row.executable, ['path', 'sha256'], 'observed executable'); agree(row.executable.path, want.argv[0], 'actual executable'); hash(row.executable.sha256, 'executable'); + const npm = ['install', 'reinstall', 'uninstall'].includes(scenario.kind); + agree(row.runtime, npm ? j.tools.npm_node : j.tools.shim_node, 'observed runtime, not controller'); + if (npm) agree(row.executable.sha256, j.tools.npm_node.sha256, 'npm executing runtime hash'); + else if (!j.cell.startsWith('windows-')) { + const pkg = row.before.prefix[scenario.product]; assert.ok(pkg, 'installed public shim required'); + agree(row.executable.sha256, pkg.shims[0].sha256, 'observed installed shim bytes'); + } + for (const k of ['stdout', 'stderr']) { fields(row[k], ['size', 'sha256'], 'pinned output'); nonnegative(row[k].size, LIMIT, 'output size'); hash(row[k].sha256, 'output hash'); } + if (scenario.kind === 'literal-argv') { + fields(row.literal, ['root', 'description', 'manifest_sha256'], 'literal argv filesystem effect'); + agree(row.literal.root, hostPath(j.cell).join(want.cwd, 'literal project ü'), 'actual cwd-relative destination'); + agree(row.literal.description, LITERAL_DESCRIPTION, 'literal shell metacharacters preserved'); hash(row.literal.manifest_sha256, 'literal manifest'); + } else agree(row.literal, null, 'no invented literal effect'); + sidecar(row.observation); observedState(row.before, j, scenario.kind === 'repair' ? scenario.product : null); observedState(row.after, j); + for (const state of [row.before, row.after]) for (const product of cell(j.cell).products) { + if (state.cache[product]) agree(state.cache[product].path, expectedCachePath(j, roots, scenario, product), 'exact owned product cache path'); + if (state.prefix[product]) state.prefix[product].shims.forEach(shim => { + const p = hostPath(j.cell), windows = j.cell.startsWith('windows-'); + agree(shim.path, p.join(scopePaths(j, scenario, roots).prefix, ...(windows ? [] : ['bin']), product + ({ posix: '', cmd: '.cmd', powershell: '.ps1' }[shim.kind])), 'exact installed npm shim path'); + }); + } + for (const k of ['acquisitions', 'commits', 'downloads', 'native_launches']) nonnegative(row[k], 32, 'process effect count'); + list(row.interval, 2, 'observed monotonic interval'); row.interval.forEach(x => nonnegative(x, Number.MAX_SAFE_INTEGER, 'monotonic time')); + assert.ok(row.interval[1] >= row.interval[0] && row.interval[1] - row.interval[0] <= 120000, 'fixed 120s scenario deadline'); + assert.ok(Array.isArray(row.events) && row.events.length <= 32, 'bounded observed boundaries'); + row.events.forEach(x => assert.ok(['shim', 'native', 'cache-waiter', 'lock-owner', 'reaped', 'cancel-delivered', 'repair-before-launch', 'locator-rejected'].includes(x), 'fixed boundary')); + agree(new Set(row.events).size, row.events.length, 'unique boundaries'); agree(row.events.at(-1), 'reaped', 'descendants reaped after all observed boundaries'); + if (!npm) { agree(row.events[0], 'shim', 'shim is the first execution boundary'); + if (row.events.includes('native')) assert.ok(row.events.indexOf('shim') < row.events.indexOf('native'), 'shim before native'); + } + agree(row.downloads, 0, 'no native download/fallback'); + for (const k of ['inputs', 'projects', 'client', 'state']) { + const mutation = scenario.kind === 'core' && /\/(init|extra-skill)$/.test(commandContract(j.cell)[scenario.command].id); + const installer = scenario.command !== null && commandContract(j.cell)[scenario.command].id.startsWith('installer/'); + if (!(k === 'projects' && mutation) && !(installer && ['client', 'state'].includes(k))) agree(row.after[k], row.before[k], `preserved ${k}`); + } + const canceled = ['cancel', 'waiter-cancel'].includes(scenario.kind); + const status = scenario.command !== null ? commandContract(j.cell)[scenario.command].status : scenario.kind.startsWith('invalid-') ? 1 : 0; + if (canceled) { + assert.ok(row.events.includes('cancel-delivered') && row.events.includes(scenario.kind === 'waiter-cancel' ? 'cache-waiter' : 'native'), 'reached genuine cancellation boundary'); + const boundary = scenario.kind === 'waiter-cancel' ? 'cache-waiter' : 'native'; + assert.ok(row.events.indexOf(boundary) < row.events.indexOf('cancel-delivered'), 'reached boundary before cancellation delivery'); + agree(row.native_launches, scenario.kind === 'waiter-cancel' ? 0 : 1, 'cancelled waiter never launches native'); + if (scenario.kind === 'waiter-cancel') agree([row.acquisitions, row.commits], [0, 0], 'cancelled waiter never acquires or commits'); + const codes = { SIGINT: 130, SIGTERM: 143, CTRL_C_EVENT: 130, TerminateProcess: 1 }; + agree(row.status, codes[scenario.event], 'fixed cancellation exit mapping'); agree(row.signal, null, 'wrapper maps cancellation exit'); + } else { agree(row.status, status, 'fixed exit'); agree(row.signal, null, 'no unexpected signal'); } + if (!npm && !canceled && !scenario.kind.startsWith('invalid-')) { + assert.ok(row.events.includes('shim') && row.events.includes('native'), 'real shim/native boundaries'); + agree(row.native_launches, 1, 'one native process'); assert.ok(row.after.cache[scenario.product], 'native ran from checked cache'); + if (scenario.kind === 'core') { agree(row.after.cache, row.before.cache, 'core warm cache preserved'); agree([row.acquisitions, row.commits], [0, 0], 'core warm no acquisition/commit'); } + } + if (scenario.command !== null) { + const core = commands[scenario.command]; + for (const [k, value] of Object.entries({ cwd: row.cwd, status: row.status, signal: row.signal })) agree(core[k], value, `core observed ${k}`); + for (const k of ['stdout', 'stderr']) agree(row[k], { size: Buffer.byteLength(core[k]), sha256: c.digest(Buffer.from(core[k])) }, 'core exact observed output pin'); + } + return row; +} +function recordRows(value, schema, j, expected, roots, commands) { + fixed(value.schema, schema, 'scenario schema'); agree(value.cell, j.cell, 'scenario cell'); list(value.rows, expected.length, 'complete ordered scenario rows'); + value.rows.forEach((r, i) => verifyObservedRow(r, expected[i], j, roots, commands)); return value.rows; +} +function verifyNpmLifecycle(j, value, roots, commands = []) { + fields(value, ['schema', 'cell', 'rows'], 'npm lifecycle'); + const expected = scenarioContract(j.cell).npm, rows = recordRows(value, 'authoring-public-npm-lifecycle/v1', j, expected, roots, commands); + const previous = new Map(), originals = new Map(); + rows.forEach((r, i) => { + const s = expected[i], product = s.product, peer = cell(j.cell).products.find(p => p !== product); + if (previous.has(s.prefix)) agree(r.before, previous.get(s.prefix), 'continuous prefix lifecycle history'); + else { agree(Object.values(r.before.prefix), cell(j.cell).products.map(() => null), 'fresh prefix'); agree(Object.values(r.before.cache), cell(j.cell).products.map(() => null), 'cold prefix cache'); } + previous.set(s.prefix, r.after); + if (peer) { agree(r.after.prefix[peer], r.before.prefix[peer], 'peer package/shims/modes unchanged'); agree(r.after.cache[peer], r.before.cache[peer], 'peer cache/binary unchanged'); } + const key = `${s.prefix}/${product}`; + if (['install', 'reinstall'].includes(s.kind)) { + agree(r.before.prefix[product], null, 'install into missing package'); assert.ok(r.after.prefix[product], 'installed package/shims'); + if (s.kind === 'install') originals.set(key, r.after.prefix[product]); else agree(r.after.prefix[product], originals.get(key), 'same tarball reinstall bytes/modes'); + if (product === 'plugin-kit-ai') { + fields(r.postinstall, ['argv', 'runtime', 'acquisitions', 'commits', 'observation'], 'real kit postinstall'); + agree(r.postinstall.argv, [j.tools.npm_node.path, './lib/install.js'], 'genuine npm postinstall command'); + agree(r.postinstall.runtime, j.tools.npm_node, 'selected kit postinstall Node'); sidecar(r.postinstall.observation); + const cold = r.before.cache[product] === null; + agree(r.postinstall.acquisitions, cold ? 1 : 0, 'cold/warm postinstall acquisition'); agree(r.postinstall.commits, cold ? 1 : 0, 'postinstall cache commit'); + assert.ok(r.after.cache[product], 'postinstall checked cache'); + } else agree(r.postinstall, null, 'agent no install script'); + } else { + agree(r.postinstall, null, 'no hidden postinstall'); + if (s.kind === 'uninstall') { assert.ok(r.before.prefix[product], 'installed before removal'); agree(r.after.prefix[product], null, 'removed package and all shims absent'); agree(r.after.cache, r.before.cache, 'uninstall preserves native cache'); } + else { agree(r.after.prefix, r.before.prefix, 'probe read-only package'); assert.ok(r.events.includes('native') && r.native_launches === 1, 'peer/standalone real native command'); } + } + }); + return { npm_lifecycle: true }; +} +function verifyCacheProcess(j, value, roots, commands) { + fields(value, ['schema', 'cell', 'rows', 'finalization'], 'cache/process'); + const expected = scenarioContract(j.cell).cache, rows = recordRows(value, 'authoring-public-cache-process/v1', j, expected, roots, commands); + const groups = new Map(), last = new Map(); + rows.forEach((r, i) => { + const s = expected[i], p = s.product; + agree(r.postinstall, null, 'shim has no npm postinstall'); + agree(r.after.prefix, r.before.prefix, 'shim preserves installed trees'); + for (const peer of cell(j.cell).products.filter(x => x !== p)) { + if (s.kind === 'peer-overlap') { agree(r.before.cache[peer], null, 'both peer namespaces start cold'); if (r.after.cache[peer]) binary(r.after.cache[peer], j, peer); } + else agree(r.after.cache[peer], r.before.cache[peer], 'cache namespace independence'); + } + if (s.kind.startsWith('invalid-')) { + agree(r.native_launches, 0, 'invalid locator never launches native'); agree(r.acquisitions, 0, 'invalid locator never acquires'); agree(r.commits, 0, 'invalid locator never commits'); + agree(r.after.cache, r.before.cache, 'invalid locator does not fallback even warm'); assert.ok(r.events.includes('locator-rejected'), 'locator rejection observed'); + if (s.kind === 'invalid-cold') agree(r.before.cache[p], null, 'invalid genuinely cold'); else assert.ok(r.before.cache[p], 'invalid genuinely warm'); + } else if (['cold', 'warm', 'repair', 'concurrent-cold', 'concurrent-warm', 'peer-overlap', 'waiter-owner'].includes(s.kind)) { + assert.ok(r.after.cache[p], 'checked cache result'); + if (['cold', 'peer-overlap'].includes(s.kind)) { agree(r.before.cache[p], null, 'fresh cold acquisition'); agree(r.acquisitions, 1, 'one cold acquisition'); agree(r.commits, 1, 'one checked commit'); } + if (['warm', 'concurrent-warm'].includes(s.kind)) { assert.ok(r.before.cache[p], 'warm cache present'); agree(r.after.cache, r.before.cache, 'warm exact cache identity'); agree([r.acquisitions, r.commits], [0, 0], 'warm zero acquisition/commit'); } + if (s.kind === 'repair') { assert.ok(r.before.cache[p], 'observed fixed corruption before repair'); assert.ok(r.events.includes('repair-before-launch') && r.events.indexOf('repair-before-launch') < r.events.indexOf('native'), 'owned corruption repaired before any execution'); agree([r.acquisitions, r.commits], [1, 1], 'checked repair acquisition/commit'); } + agree(r.native_launches, 1, 'one actual native command'); + } + if (s.group) { if (!groups.has(s.group)) groups.set(s.group, []); groups.get(s.group).push(r); } + if (!s.group && s.kind !== 'core') { + if (last.has(s.cache)) { + const before = s.kind === 'repair' ? { ...r.before.cache, [p]: r.after.cache[p] } : r.before.cache; + agree(before, last.get(s.cache), 'continuous serial cache history around fixed owned corruption'); + } + last.set(s.cache, r.after.cache); + } + }); + for (const [id, group] of groups) { + assert.ok(Math.max(...group.map(r => r.interval[0])) < Math.min(...group.map(r => r.interval[1])), `real overlapping requests:${id}`); + if (id.startsWith('cold-')) { + list(group, 4, 'four simultaneous cold requests'); + group.forEach(r => agree(Object.values(r.before.cache), cell(j.cell).products.map(() => null), 'all four start from the same cold state')); agree(group.reduce((n, r) => n + r.acquisitions, 0), 1, 'one concurrent acquisition'); + agree(group.reduce((n, r) => n + r.commits, 0), 1, 'one concurrent commit'); + group.forEach(r => agree(r.after.cache, group[0].after.cache, 'one valid resulting cache')); + } + if (id === 'peer-overlap') { list(group, 2, 'both overlapping peer requests'); agree(group.map(r => r.id.split('/')[0]), cell(j.cell).products, 'distinct overlapping products'); } + if (id.startsWith('waiter-')) assert.ok(group.some(r => r.events.includes('lock-owner')) && group.some(r => r.events.includes('cache-waiter')), 'genuine owner/waiter overlap'); + } + fields(value.finalization, ['rows', 'descendants', 'locks', 'late_errors', 'observation'], 'descendant finalization'); + agree(value.finalization.rows, [...scenarioContract(j.cell).npm, ...expected, ...scenarioContract(j.cell).installer].map(s => s.id), 'finalization complete process inventory'); + for (const k of ['descendants', 'locks', 'late_errors']) agree(value.finalization[k], [], `no remaining ${k}`); + sidecar(value.finalization.observation); + return { cache_process: true, children_reaped: true }; +} +const PLUGIN_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'; +const MCP_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json'; +const PROFILES = freeze([ + ['agent-plugins/1.0.0', 'ff8ab5e392cc87bd88d87c060815a87490e51003', '97a658b7dca3ce1b4c2266b95da300fa51d9dc4ade59d73168e5f9104272da18'], + ['agent-skills/2026-09-06', '69ef37e9424c0a7ea9dd2293b559e43ec8176379', 'b9079c0c10b7930e8c6a20ff2bc10cda2a3343c55185120e3f1116a1a529b220'], + [PLUGIN_SCHEMA, '1.0.0', '0a4aad95ce337878ad38802ebf0daa3fde76abe3f65400c86bcbb1ec0b3ab883'], + [MCP_SCHEMA, '1.0.0', '6539175bfcdf43085855183e86da40ea94b166547a72b47ae9a0a390516d3acb'], + ['author-document-bounds/v1', '1', '4b8ab8fd50481ccd1a0b777dcbbfa06cf89516a5ea61ce09d56d6dd6a2c43004'] +].map(([id, revision, digest]) => ({ id, revision, digest: 'sha256:' + digest }))); +const SURFACE = freeze(['capabilities', 'compat', 'doctor', 'init', 'inspect', 'skills.init', 'skills.validate', 'test', 'validate', 'version'].map(x => 'author.' + x)); +function clientFacts() { + return [ + ['chatgpt', 'compatibility_projection', 'manual', 'projected', 'unsupported', 'projected', 'unsupported'], + ['claude', 'compatibility_projection', 'automatic', 'projected', 'projected', 'unsupported', 'unsupported'], + ['cline', 'native', 'automatic', 'native', 'native', 'unsupported', 'unsupported'], + ['codex', 'compatibility_projection', 'manual', 'projected', 'projected', 'unsupported', 'unsupported'], + ['copilot', 'native', 'manual', 'native', 'native', 'unsupported', 'native'], + ['cursor', 'native', 'manual', 'native', 'native', 'unsupported', 'native'], + ['gemini', 'native', 'manual', 'native', 'native', 'unsupported', 'unsupported'], + ['kiro', 'native', 'manual', 'native', 'native', 'unsupported', 'unsupported'], + ['opencode', 'prepared_package', 'automatic', 'prepared', 'prepared', 'unsupported', 'unsupported'], + ['vscode', 'prepared_package', 'manual', 'prepared', 'prepared', 'unsupported', 'prepared'], + ['windsurf', 'prepared_package', 'manual', 'prepared', 'prepared', 'unsupported', 'prepared'] + ].map(([client_id, package_mode, activation_mode, skill_support, mcp, app_support, extension_support]) => + ({ client_id, package_mode, activation_mode, scopes: ['user'], skill_support, mcp_transports: { stdio: mcp, 'streamable-http': mcp, sse: mcp }, app_support, extension_support })); +} +function optionalFields(v, required, optional, label) { + assert.ok(v && typeof v === 'object', label); + fields(v, [...required, ...optional.filter(k => Object.hasOwn(v, k))], label); +} +function outputJSON(text) { + textValue(text); assert.ok(text.length > 0, 'one JSON output'); + // Tokenize only to detect duplicate object keys/depth. JSON.parse owns syntax. + const stack = []; let match; + const tokens = /"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|[{}\[\]]/g; + while ((match = tokens.exec(text))) { + const token = match[0]; + if (token === '{' || token === '[') { stack.push(token === '{' ? new Set() : null); assert.ok(stack.length <= 16, 'output depth'); } + else if (token === '}' || token === ']') stack.pop(); + else if (/^\s*:/.test(text.slice(tokens.lastIndex))) { + const keys = stack.at(-1), key = JSON.parse(token); assert.ok(keys && !keys.has(key), 'duplicate JSON result key'); keys.add(key); + } + } + return JSON.parse(text); +} +function componentFacts(lane, extra = true, malformed = false) { + const rows = [], skill = lane === 'skill' || lane.startsWith('hybrid-'); + const add = (type, name, requirements = [], status = 'pass') => rows.push({ id: 'sha256:' + c.digest(Buffer.from(`${type === 'skill' ? 'skill' : 'mcp'}:${name}`)), type, status, requirements }); + if (skill) add('skill', lane); + if (extra) add('skill', 'extra-skill', [], malformed ? 'fail' : 'pass'); + if (lane !== 'skill') add(lane.endsWith('stdio') ? 'mcp_stdio' : 'mcp_streamable-http', lane, + lane.endsWith('stdio') ? ['executable_unresolved', 'executable_path'] : ['remote_endpoint_uncontacted']); + return rows.sort((a, b) => a.id.localeCompare(b.id)); +} +const ASSESSMENTS = ['compatibility', 'toolchain', 'loadability', 'normative_conformance', 'host_safety', 'authoring_readiness', 'release_policy', 'runtime_evidence']; +function authorResult(v, want, j) { + const d = v.data, version = want.id === 'engine-version' || want.id === 'product-version'; + const optional = ['help', 'root', 'inspection', 'commands', 'withheld_path_ids', 'error', 'clients', 'capabilities', 'doctor_checks', 'product', 'product_version']; + optionalFields(d, [...ASSESSMENTS, 'schema', 'engine', 'revision', 'command', 'mode', 'identity', 'coverage', 'profiles', 'schema_ids', 'findings', + 'components', 'checks', 'committed', 'affected_paths', 'authoring_schema_version', 'engine_version', 'requested', 'effects', 'next_actions'], optional, 'author result fields'); + agree(d.schema, 'agentplugins-authoring-report/v1', 'report schema'); agree(d.engine, 'standard-first-slice/1', 'engine'); + agree(d.engine_version, d.engine, 'engine version'); agree(d.revision, j.identity.commit, 'engine F'); agree(d.authoring_schema_version, 1, 'author schema'); + const args = want.argv.slice(want.product === 'agentplugins' ? 1 : 0); + const op = want.id === 'retired-v1' ? 'author' : args[0] === '--help' ? 'author' : `author.${args[0]}${args[0] === 'skills' ? '.' + args[1] : ''}`; + agree(v.command, op, 'operation'); agree(d.command, op, 'report operation'); + const mutation = op === 'author.init' || op === 'author.skills.init'; + agree(d.mode, mutation ? 'local_mutation' : 'read', 'operation mode'); agree(d.requested, { operation: op, mode: d.mode }, 'requested operation'); + const committed = /\/(init|extra-skill)$/.test(want.id); + agree(d.committed, committed, 'commit boundary'); fields(d.effects, ['attempted', 'committed'], 'effects'); + agree(d.effects.committed, committed, 'public effects'); assert.equal(typeof d.effects.attempted, 'boolean'); + assert.ok(Array.isArray(d.findings) && d.findings.length <= 256, 'bounded findings'); + const ids = new Set(); + for (const f of d.findings) { + optionalFields(f, ['id', 'code', 'layer', 'rule', 'severity'], ['location', 'item_id'], 'finding'); + digestID(f.id); assert.ok(!ids.has(f.id), 'unique diagnostics'); ids.add(f.id); + Object.values(f).forEach(x => textValue(x, 4096)); assert.ok(['error', 'warning', 'info'].includes(f.severity), 'diagnostic severity'); + } + for (const name of ASSESSMENTS) { + fields(d[name], ['status', 'finding_ids'], 'separate policy assessment'); + assert.ok(['pass', 'fail', 'not_evaluated'].includes(d[name].status), 'policy state'); + assert.ok(Array.isArray(d[name].finding_ids) && d[name].finding_ids.every(id => ids.has(id)), 'assessment references real findings'); + agree([...new Set(d[name].finding_ids)].sort(), d[name].finding_ids, 'canonical finding references'); + } + agree(d.runtime_evidence, { status: 'not_evaluated', finding_ids: [] }, 'offline runtime not evaluated'); + agree(d.release_policy, { status: 'not_evaluated', finding_ids: [] }, 'release policy not inferred'); + fields(d.coverage, ['components_requested', 'skills_enumerated', 'inventory_complete', 'tree_complete', 'plugin', 'mcp', 'skills', 'filesystem', 'facts_complete'], 'coverage'); + for (const k of ['components_requested', 'skills_enumerated', 'inventory_complete', 'tree_complete', 'facts_complete']) assert.equal(typeof d.coverage[k], 'boolean'); + for (const k of ['plugin', 'mcp', 'skills', 'filesystem']) assert.ok(['pass', 'fail', 'not_evaluated'].includes(d.coverage[k]), 'coverage state'); + optionalFields(d.identity, ['scope_algorithm', 'read_profile', 'tree_exclusions'], ['scope_digest', 'tree_algorithm', 'tree_digest', 'manifest_digest'], 'project identity'); + const project = want.lane && !want.id.endsWith('/existing') && want.id !== 'installer-flag'; + if (project) { + agree(d.effects.attempted, true, 'entered actual project operation'); + agree(d.identity.scope_algorithm, 'agentplugins-captured-input-sha256-v1', 'scope algorithm'); + agree(d.identity.tree_algorithm, 'agentplugins-tree-sha256-v1', 'tree algorithm'); + agree(d.identity.read_profile, `packageview-local-${cell(j.cell).target.split('-')[0]}-v1`, 'recorded host read profile'); + agree(d.identity.tree_exclusions, ['root .git', 'root non-directory .plugin-kit-ai.lock'], 'exact tree exclusions'); + for (const k of ['scope_digest', 'tree_digest', 'manifest_digest']) digestID(d.identity[k]); + agree(d.profiles, PROFILES, 'embedded profiles'); + agree(d.schema_ids, (want.lane === 'skill' ? [PLUGIN_SCHEMA] : [MCP_SCHEMA, PLUGIN_SCHEMA]).sort(), 'exact schema inventory'); + for (const k of ['components_requested', 'skills_enumerated', 'inventory_complete', 'tree_complete', 'facts_complete']) agree(d.coverage[k], true, 'complete captured facts'); + const malformed = want.id === 'malformed-skill'; + for (const k of ['plugin', 'filesystem']) agree(d.coverage[k], 'pass', 'complete core/filesystem coverage'); + agree(d.coverage.mcp, want.lane === 'skill' ? 'not_evaluated' : 'pass', 'MCP coverage'); + agree(d.coverage.skills, 'pass', 'complete Skills capture separate from conformance'); + agree(d.loadability.status, 'pass', 'valid Skill sibling remains loadable'); + agree(d.normative_conformance.status, malformed ? 'fail' : 'pass', 'normative conformance'); + agree(d.host_safety.status, 'pass', 'host safety separate'); + agree(d.authoring_readiness.status, malformed ? 'fail' : 'pass', 'authoring readiness'); + agree(d.components, componentFacts(want.lane, !want.id.endsWith('/init'), malformed), 'exact Skill/MCP components and boundary'); + fields(d.inspection, ['name', 'version', 'schema', 'components'], 'inspection'); + agree([d.inspection.name, d.inspection.version, d.inspection.schema], [want.lane, '0.1.0', PLUGIN_SCHEMA], 'package identity'); + const names = d.inspection.components.map(x => { + optionalFields(x, ['id', 'type'], ['name', 'namespace', 'executable', 'executable_kind'], 'display component'); + digestID(x.id); Object.values(x).forEach(v => textValue(v, 256)); return [x.id, x.type, x.name]; + }); + agree(names, d.components.map(x => [x.id, x.type, x.id === 'sha256:' + c.digest(Buffer.from('skill:extra-skill')) ? 'extra-skill' : want.lane]), 'component display names'); + } else { + agree(d.profiles, [], 'no invented project profiles'); agree(d.schema_ids, [], 'no invented project schemas'); + agree(d.components, [], 'no project components'); + } + if (d.doctor_checks) { assert.ok(Array.isArray(d.doctor_checks) && d.doctor_checks.length <= 256); d.doctor_checks.forEach(x => { optionalFields(x, ['id', 'status', 'action'], ['item_id'], 'doctor check'); Object.values(x).forEach(v => textValue(v, 8192)); assert.ok(['pass', 'fail', 'not_evaluated'].includes(x.status)); }); } + if (d.withheld_path_ids) { assert.ok(Array.isArray(d.withheld_path_ids) && d.withheld_path_ids.length <= 256); d.withheld_path_ids.forEach(digestID); } + if (d.root !== undefined) { textValue(d.root, 4096); assert.fail('absolute/root disclosure was not requested'); } + if (want.id.endsWith('/doctor')) agree(d.toolchain.status, want.lane === 'skill' ? 'pass' : 'not_evaluated', 'doctor offline boundary'); + else agree(d.toolchain.status, 'not_evaluated', 'no implicit toolchain proof'); + if (want.id.endsWith('/compat')) { + agree(d.compatibility.status, 'pass', 'static compatibility'); list(d.clients, 2, 'two explicit clients'); + d.clients.forEach((client, i) => { + fields(client, ['client_id', 'capabilities', 'components', 'limitations'], 'compat client'); agree(client.client_id, ['claude', 'codex'][i], 'client order'); + agree(client.capabilities, clientFacts().find(x => x.client_id === client.client_id), 'client capability facts'); + const counts = { skill: 0, mcp_server: 0 }; + const components = d.components.map(x => ({ kind: x.type === 'skill' ? 'skill' : 'mcp_server', index: 0, support: 'projected' })) + .sort((a, b) => a.kind.localeCompare(b.kind)).map(x => ({ ...x, index: ++counts[x.kind] })); + agree(client.components.map(({ kind, index, support }) => ({ kind, index, support })), components, 'compat exact components'); + client.components.forEach(x => optionalFields(x, ['kind', 'index', 'support'], ['limitations'], 'compat component')); + agree(client.limitations, ['static_adapter_support_only', 'installation_not_checked', 'authentication_not_checked', 'runtime_not_checked', 'client_version_not_checked', 'catalog_publication_not_checked', ...(i === 1 ? ['manual_activation_required'] : [])], 'compat evidence limits'); + }); + } else agree(d.compatibility.status, 'not_evaluated', 'no implicit compatibility'); + if (want.id === 'capabilities') { + fields(d.capabilities, ['schemas', 'profiles', 'clients', 'commands', 'evidence_limits'], 'capability inventory'); + agree(d.capabilities.schemas, PROFILES.slice(2, 4).map(({ id, digest }) => ({ id, digest })), 'embedded schema pins'); + agree(d.capabilities.profiles, PROFILES, 'capability profiles'); agree(d.capabilities.clients, clientFacts(), 'complete client inventory'); + agree(d.capabilities.commands, SURFACE, 'complete capability commands'); + agree(d.capabilities.evidence_limits, ['static_only', 'no_path_lookup', 'no_executable_version_probe', 'no_runtime_or_oauth_evidence', 'native_files_metadata_only'], 'capability limits'); + } + if (['author-help', 'capabilities', 'engine-version', 'product-version'].includes(want.id)) agree(d.commands, SURFACE, 'implemented command surface'); + if (version) agree([d.product, d.product_version], [want.product, j.identity.versions[want.product]], 'kit product version'); + if (d.help) { fields(d.help, ['use', 'flags', 'guidance'], 'help'); textValue(d.help.use, 4096); textValue(d.help.guidance, 8192); assert.ok(Array.isArray(d.help.flags) && d.help.flags.every(x => typeof x === 'string'), 'help flags'); } + if (d.error) { fields(d.error, ['code', 'action'], 'operation error'); textValue(d.error.code, 256); textValue(d.error.action, 8192); } + for (const k of ['affected_paths', 'next_actions', 'checks']) assert.ok(Array.isArray(d[k]) && d[k].length <= 256, 'bounded result lists'); + d.affected_paths.forEach(x => { textValue(x, 4096); assert.ok(!x.startsWith('/') && !x.split('/').includes('..'), 'relative affected path'); }); + if (!committed) agree(d.affected_paths, [], 'read/failed operation no affected files'); else assert.ok(d.affected_paths.length > 0, 'committed actual paths'); + d.next_actions.forEach(x => { optionalFields(x, ['code', 'message'], ['operation'], 'next action'); Object.values(x).forEach(v => textValue(v, 8192)); }); + d.checks.forEach(x => { fields(x, ['id', 'status', 'finding_ids'], 'static check'); textValue(x.id, 256); assert.ok(['pass', 'fail', 'not_evaluated'].includes(x.status)); assert.ok(x.finding_ids.every(id => ids.has(id))); }); + if (want.id.endsWith('/test')) agree(d.checks.map(x => [x.id, x.status]), [['portable_configuration', 'pass'], ['package_hygiene', 'pass'], ['static_skills', 'pass'], ['static_mcp', want.lane === 'skill' ? 'not_evaluated' : 'pass'], ['runtime', 'not_evaluated']], 'complete static checks'); +} +function normalizeResult(v) { + const copy = structuredClone(v); + // Only product/version fields and displayed invocation prefix are allowed to differ. + delete copy.data.product; delete copy.data.product_version; + if (copy.data.help) copy.data.help.use = copy.data.help.use.replace(/^(plugin-kit-ai|agentplugins author)(?= |$)/, 'AUTHOR'); + return copy; +} +// Scenario records stay below 1MiB; ordered process rows use bounded transcript +// sidecars. Long supported workspace paths must not inflate the record envelope. +function evidenceFiles(name, value) { + const files = {}; + if (['npm-lifecycle.json', 'cache-process.json', 'installer.json'].includes(name) && Array.isArray(value.rows)) { + const refs = []; let shard = [], size = 3; + const flush = () => { + if (!shard.length) return; + const file = `sidecars/${name.slice(0, -5)}-rows-${refs.length}.json`, bytes = c.encode(shard); + assert.ok(bytes.length <= TRANSCRIPT_LIMIT, '16MiB transcript shard'); files[file] = bytes; + refs.push({ path: file, size: bytes.length, sha256: c.digest(bytes) }); shard = []; size = 3; + }; + for (const row of value.rows) { + const bytes = c.encode(row); assert.ok(bytes.length <= LIMIT, '1MiB process record'); + if (size + bytes.length + 1 > TRANSCRIPT_LIMIT) flush(); + shard.push(row); size += bytes.length + 1; + } + flush(); value = { ...value, rows: { shards: refs } }; + } + const bytes = c.encode(value); + assert.ok(bytes.length <= (name === 'commands.json' ? TRANSCRIPT_LIMIT : LIMIT), 'bounded evidence file'); + files[name] = bytes; return files; +} +function expandEvidence(name, value, root, budget = { size: 0 }) { + if (!['npm-lifecycle.json', 'cache-process.json', 'installer.json'].includes(name) || value.rows === undefined || Array.isArray(value.rows)) return value; + fields(value.rows, ['shards'], 'ordered row transcript index'); + assert.ok(Array.isArray(value.rows.shards) && value.rows.shards.length <= 128, 'bounded shard inventory'); + const rows = []; + value.rows.shards.forEach((ref, i) => { + sidecar(ref); agree(ref.path, `sidecars/${name.slice(0, -5)}-rows-${i}.json`, 'fixed row shard name'); + budget.size += ref.size; assert.ok(budget.size <= AGGREGATE_LIMIT, '128MiB transcript aggregate'); + const bytes = pin(path.join(root, ref.path), ref.sha256, TRANSCRIPT_LIMIT); agree(bytes.length, ref.size, 'row shard size'); + const shard = bounded(bytes, TRANSCRIPT_LIMIT); assert.ok(Array.isArray(shard) && shard.length > 0, 'nonempty row shard'); + for (const row of shard) { assert.ok(c.encode(row).length <= LIMIT, '1MiB process record'); rows.push(row); } + }); + const expanded = { ...value, rows }; + agree(c.encode(value), evidenceFiles(name, expanded)[name], 'canonical row shard partition'); return expanded; +} +// Fixed publicInit lanes only: exact scaffold bytes, not a configurable template engine. +function generatedFiles(lane) { + assert.ok(LANES.includes(lane), 'fixed generated lane'); + const hybrid = lane.startsWith('hybrid-'), stdio = lane.endsWith('stdio'), remote = lane.endsWith('remote'); + const description = hybrid ? 'An Agent Plugins package with a Skill and an MCP server.' : lane === 'skill' ? 'A Skill for documentation and task guidance.' : remote ? 'An Agent Plugins package with a remote MCP server.' : 'An Agent Plugins package with a local Node MCP server.'; + const json = value => JSON.stringify(value, null, 2) + '\n'; + const files = { + 'plugin.json': json({ $schema: PROFILES[2].id, description, name: lane, version: '0.1.0' }), + '.gitignore': 'node_modules/\n.DS_Store\n', + 'README.md': `# ${lane}\n\n${description}\n\nThis package uses Agent Plugins 1.0: \`plugin.json\`, with portable components in \`skills/\` and/or \`mcp.json\`.\n` + + (stdio ? '\nThe stdio server requires Node >=22 and the official MCP SDK pinned in package-lock.json. Dependency installation and runtime execution are separate, explicit author actions. Creation performs neither; runtime behavior has not been tested.\n' : remote ? '\nThe remote MCP URL is configuration only. Creation does not contact the endpoint or verify authentication or runtime behavior.\n' : ''), + 'skills/extra-skill/SKILL.md': '---\nname: "extra-skill"\ndescription: "Use for extra documentation requests"\n---\n\n# extra-skill\n\nUse for extra documentation requests\n' + }; + if (hybrid || lane === 'skill') files[`skills/${lane}/SKILL.md`] = `---\nname: ${lane}\ndescription: ${JSON.stringify(description)}\n---\n\n# ${lane}\n\n${description}\n\nUse this skill when the request matches its description. Clarify missing requirements before taking action and report the result.\n`; + if (remote || stdio) files['mcp.json'] = json({ $schema: PROFILES[3].id, mcpServers: { [lane]: stdio ? { args: ['${PLUGIN_ROOT}/src/server.mjs'], command: 'node', type: 'stdio' } : { type: 'streamable-http', url: 'https://docs.example.com/mcp' } } }); + if (stdio) { + // Existing source-frozen embedded npm fixtures; never install or resolve them. + for (const name of ['package.json', 'package-lock.json']) { + const bytes = c.readFile(path.resolve(__dirname, '../../../cli/plugin-kit-ai/internal/authoring/scaffold/templates', name), LIMIT).toString('utf8'); + const needle = name === 'package.json' ? '"name":"agent-plugin-template"' : '"name": "agent-plugin-template"'; + agree(bytes.split(needle).length - 1, name === 'package.json' ? 1 : 2, 'fixed embedded root names'); + files[name] = bytes.split(needle).join(needle.replace('agent-plugin-template', lane)); + } + files['src/server.mjs'] = `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nconst server = new McpServer({ name: "${lane}", version: '0.1.0' });\nserver.registerTool('hello', { description: 'Return a greeting', inputSchema: {} }, async () => ({\n content: [{ type: 'text', text: "Hello from ${lane}!" }],\n}));\nawait server.connect(new StdioServerTransport());\n`; + } + return Object.fromEntries(Object.entries(files).map(([name, body]) => [name, Buffer.from(body)])); +} +function generatedTreeIdentity(entries, lane, key) { + const files = generatedFiles(lane), directories = new Set(['.']); + for (const name of Object.keys(files)) for (let dir = path.posix.dirname(name); dir !== '.'; dir = path.posix.dirname(dir)) directories.add(dir); + const actual = entries.filter(e => e.path === lane || e.path.startsWith(lane + '/')).map(e => ({ ...e, path: e.path === lane ? '.' : e.path.slice(lane.length + 1) })); + agree(actual.map(e => e.path).sort(), [...directories, ...Object.keys(files)].sort(), 'fixed generated file/directory closure'); + const windows = cell(key).target.startsWith('windows-'); + for (const e of actual) { + const directory = directories.has(e.path); + agree(e.kind, directory ? 'directory' : 'file', 'generated entry type'); + // Node's Windows stat reports writable files 0666 and directories 0777; + // packageview uses that host read profile, including file execute bits. + agree(e.mode, windows ? directory ? 0o777 : 0o666 : directory ? 0o755 : 0o644, 'generated host mode'); + if (!directory) { agree(e.size, files[e.path].length, 'generated file size'); agree(e.sha256, c.digest(files[e.path]), 'generated file content'); } + } + // Ordinary agentplugins-tree-sha256-v1 framing (DigestCaptured). Root is + // excluded; directories, including empty ones, are entries. Bytes below are + // bound to captured size/hash above; claimed report digests are never inputs. + const chunks = [], length = n => { const b = Buffer.alloc(8); b.writeBigUInt64BE(BigInt(n)); return b; }; + const frame = value => { const b = Buffer.from(value); chunks.push(length(b.length), b); }; + frame('agentplugins.package-tree\0sha256\0v1'); + for (const e of actual.filter(e => e.path !== '.').sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) { + const body = e.kind === 'file' ? files[e.path] : Buffer.alloc(0); + for (const field of ['entry', e.path, e.kind, e.kind === 'directory' ? '040000' : e.mode & 0o111 ? '100755' : '100644', '']) frame(field); + chunks.push(length(body.length), body); + } + return 'sha256:' + c.digest(Buffer.concat(chunks)); +} +function verifyResults(j, evidence) { + for (const name of EVIDENCE) if (Object.hasOwn(evidence, name)) evidenceFiles(name, evidence[name]); + const expected = commandContract(j.cell), rows = evidence['commands.json']; list(rows, expected.length, 'exact ordered C3 core command rows'); + const payloads = new Map(), identityByProject = new Map(); + rows.forEach((row, i) => { + const want = expected[i]; fields(row, ['product', 'id', 'argv', 'cwd', 'status', 'signal', 'stdout', 'stderr'], 'C3 command result'); + for (const k of ['product', 'id', 'argv', 'status']) agree(row[k], want[k], `command ${k}`); + agree(row.cwd, commandCwd(j, want), 'fixed cwd'); agree(row.signal, null, 'complete command'); agree(row.stderr, '', 'clean stderr'); textValue(row.stdout); + if (want.id === 'product-help') { assert.ok(row.stdout.includes(want.product) && row.stdout.length > 50, 'real product help'); return; } + const v = outputJSON(row.stdout); fields(v, ['schema_version', 'command', 'result', 'data'], 'one output envelope'); + agree(v.schema_version, 1, 'JSON schema'); agree(v.result, want.status === 0 ? 'success' : 'failure', 'result status'); + if (want.id.startsWith('installer/')) return; // Checked by the mandatory fixed public installer facade below. + if (want.author || want.product === 'plugin-kit-ai') authorResult(v, want, j); + else { agree(v.command, 'version', 'agent product version operation'); assert.ok(v.data && typeof v.data === 'object'); agree(v.data.version, j.identity.versions.agentplugins, 'agent product version'); } + if (want.lane && !/\/(init|existing)$/.test(want.id) && !['malformed-skill', 'installer-flag'].includes(want.id)) { + const key = `${want.product}/${want.lane}`; + if (identityByProject.has(key)) agree(v.data.identity, identityByProject.get(key), 'unchanged read-only project identity'); + else identityByProject.set(key, v.data.identity); + } + payloads.set(`${want.product}/${want.id}`, v); + }); + fields(evidence['projects.json'], cell(j.cell).products, 'final canonical projects'); + for (const p of cell(j.cell).products) { + const snapshot = tree(evidence['projects.json'][p], j.cell, j.projects[p]); + assert.ok(snapshot.entries.every(e => e.path === '.' || LANES.some(lane => e.path === lane || e.path.startsWith(lane + '/'))), 'only generated lane entries'); + agree(snapshot.entries.filter(e => e.kind === 'directory' && e.path !== '.' && !e.path.includes('/')).map(e => e.path).sort(), [...LANES].sort(), 'exact five canonical projects'); + for (const lane of LANES) { + const capturedDigest = generatedTreeIdentity(snapshot.entries, lane, j.cell); + const identity = identityByProject.get(`${p}/${lane}`); assert.ok(identity, 'project read identity present'); + const manifest = snapshot.entries.find(e => e.path === `${lane}/plugin.json`); + agree(identity.tree_digest, capturedDigest, 'captured generated tree identity'); + agree(identity.manifest_digest, 'sha256:' + manifest.sha256, 'observed manifest identity'); + } + } + if (cell(j.cell).products.length === 2) { + for (const want of expected.filter(r => r.product === 'agentplugins' && r.author)) + agree(normalizeResult(payloads.get(`agentplugins/${want.id}`)), normalizeResult(payloads.get(`plugin-kit-ai/${want.id}`)), 'pair JSON parity including diagnostics/readiness/digests'); + agree(evidence['projects.json'].agentplugins.entries, evidence['projects.json']['plugin-kit-ai'].entries, 'pair generated trees bytes/modes/empty directories'); + } + return { fixed_commands: true, pair_parity: true, projects_preserved: true }; +} +function verifyInstaller(j, value, roots, commands, observation, facade) { + fields(value, ['schema', 'cell', 'rows', 'assessment', 'readbacks'], 'installer evidence'); + recordRows(value, 'authoring-public-installer/v1', j, scenarioContract(j.cell).installer, roots, commands); + if (cell(j.cell).node === 18) { agree(value.assessment, null, 'kit has no installer assessment'); agree(value.readbacks, [], 'kit has no installer readbacks'); return { production_installer: true }; } + // Assessment/readback internals belong exclusively to the reviewed owner facade. + // They are bounded opaque sidecars here, not caller booleans or copied security logic. + sidecar(value.assessment); list(value.readbacks, 18, 'all eighteen installer readbacks'); value.readbacks.forEach(sidecar); + const checked = facade.verifyPublicInstaller({ cell: j.cell, identity: j.identity, subjects: j.subjects, + commands: value.rows, assessment: value.assessment, readbacks: value.readbacks, observation }); + agree(checked, { assessment: value.assessment, readbacks: value.readbacks }, 'checked installer evidence, never boolean success'); + return { production_installer: true }; +} +function journeyRoots(j) { + const p = hostPath(j.cell), output = p.dirname(p.dirname(j.projects[cell(j.cell).products[0]])); + return rootsFor(output, j.cell); +} +function verifyJourney(local) { + const { record: j, evidence } = local, roots = journeyRoots(j); + const result = verifyResults(j, evidence); + Object.assign(result, verifyNpmLifecycle(j, evidence['npm-lifecycle.json'], roots, evidence['commands.json']), + verifyCacheProcess(j, evidence['cache-process.json'], roots, evidence['commands.json'])); + const api = requireFacades(j.cell), all = [...evidence['npm-lifecycle.json'].rows, ...evidence['cache-process.json'].rows, ...evidence['installer.json'].rows]; + // All core effects share the same original project roots. Bind observed history + // to the retained final snapshots, including every read and installer row. + const coreRows = all.filter(r => r.command !== null).sort((a, b) => { + const order = [...scenarioContract(j.cell).cache, ...scenarioContract(j.cell).installer].map(s => s.command); + return order.indexOf(a.command) - order.indexOf(b.command); + }); + let projectState; + for (const r of coreRows) { + if (projectState !== undefined) agree(r.before.projects, projectState, 'continuous original project history'); + const w = commandContract(j.cell)[r.command]; + if (/\/(init|extra-skill)$/.test(w.id)) assert.notEqual(r.before.projects, r.after.projects, 'actual authoring mutation changed tree'); + projectState = r.after.projects; + } + agree(projectState, c.digest(c.encode(evidence['projects.json'])), 'final original project snapshots'); + for (const scenario of [...scenarioContract(j.cell).npm, ...scenarioContract(j.cell).cache]) { + if (['install', 'uninstall', 'reinstall', 'literal-argv', 'cancel', 'waiter-cancel', 'invalid-cold', 'invalid-warm', 'core'].includes(scenario.kind)) continue; + const row = all.find(r => r.id === scenario.id), version = evidence['commands.json'].find(r => r.product === scenario.product && r.id === 'product-version'); + agree(row.stdout, { size: Buffer.byteLength(version.stdout), sha256: c.digest(Buffer.from(version.stdout)) }, 'supplementary native version result'); + agree(row.stderr, { size: 0, sha256: c.digest(Buffer.alloc(0)) }, 'supplementary clean stderr'); + } + const finalization = evidence['cache-process.json'].finalization; + const observed = api['public-process-observation'].verifyPublicObservation({ cell: j.cell, tools: j.tools, subjects: j.subjects, rows: all, finalization }); + agree(observed, { rows: all, finalization }, 'checked observation evidence, never boolean success'); + Object.assign(result, verifyInstaller(j, evidence['installer.json'], roots, evidence['commands.json'], finalization, api['public-installer-evidence'])); + agree(Object.fromEntries(ASSERTIONS.map(k => [k, result[k]])), j.assertions, 'all seven assertions recomputed'); + return local; +} +function sourceSeal(repo, source, provision) { + const git = provision.controllers[cell(provision.key).target].git.path; + const run = args => require('node:child_process').execFileSync(git, args, { cwd: repo, env: { PATH: path.dirname(git), LANG: 'C', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null' }, maxBuffer: TRANSCRIPT_LIMIT }).toString(); + agree(run(['rev-parse', 'HEAD']).trim(), source, 'exact executing source F'); agree(run(['status', '--porcelain=v1', '--untracked-files=all']), '', 'clean exact source'); + return run(['ls-files', '-z']).split('\0').filter(Boolean).map(name => { + const file = path.join(repo, name), st = fs.lstatSync(file); + assert.ok(st.isFile() || st.isSymbolicLink(), 'source file/link'); + return { name, mode: st.mode, sha256: c.digest(st.isSymbolicLink() ? Buffer.from(fs.readlinkSync(file)) : fs.readFileSync(file)) }; + }); +} +function admitProducerInputs(r, api) { + const inputBytes = c.readFile(r.input_file, LIMIT), input = contract.decodeInputs(inputBytes); + agree(r.selected, { tag: input.products.agentplugins.tag, ref: `refs/tags/${input.products.agentplugins.tag}`, source: input.identity.commit, versions: input.identity.versions }, 'producer selected I'); + agree(r.workflow_sha, input.identity.commit, 'producer F'); producer(r.producer, input); + const admitted = api.readPublicInputs({ input: inputBytes, selected: r.selected, workflow_sha: r.workflow_sha, stage: r.stage, + repo: r.repo, work_parent: r.work_parent, tools: r.tools }); + fields(admitted, ['stage', 'input'], 'authenticated cross-host intake'); + agree(admitted.input.input, input, 'authenticated I'); + const stageBytes = pin(path.join(admitted.stage.root, 'completion.json'), r.stage.sha256), stages = require('./stage-authoring-npm'); + const stage = stages.decodeStage(stageBytes, inputBytes); agree(admitted.stage.record, stage, 'authenticated S'); + agree(stage.native_inputs.sha256, c.digest(inputBytes), 'S binds original I'); + agree([stage.producer.run_id, stage.producer.run_attempt], [r.stage.artifact.run_id, r.stage.artifact.run_attempt], 'exact stage attempt'); + assert.ok(![stage.producer.run_id, input.producer.run_id, input.preparation.artifact.run_id].includes(r.producer.run_id), 'separate public producer'); + const retained = []; + for (const [admission, count, kind] of [[admitted.stage, 3, 'stage'], [admitted.input, 19, 'input']]) { + c.safeDirectory(admission.root); list(admission.subjects, count, 'original subject multiset'); + const seen = new Set(); + for (const row of admission.subjects) { + const relative = path.relative(admission.root, row.file); + assert.ok(relative && !relative.startsWith('..') && !path.isAbsolute(relative) && !seen.has(relative), 'distinct contained original subject'); seen.add(relative); + const bytes = pin(row.file, row.sha256, contract.MAX_NATIVE_BYTES); + retained.push({ kind, relative, file: row.file, sha256: row.sha256, mode: fs.statSync(row.file).mode & 0o777, bytes }); + } + } + assert.ok(retained.some(x => x.kind === 'input' && x.relative === contract.INPUT_FILE && x.bytes.equals(inputBytes)), 'original I subject'); + for (const product of c.PRODUCTS) { + agree(Object.keys(stage.generated[product]).length, 17, 'both authenticated exact seventeen-entry packs'); + const pack = retained.find(x => x.kind === 'stage' && x.relative === stage.packs[product].file); + assert.ok(pack, 'both original pack subjects'); agree(pack.sha256, stage.packs[product].sha256, 'pack SHA256'); agree(pack.bytes.length, stage.packs[product].size, 'pack size'); + const crypto = require('node:crypto'); + agree('sha512-' + crypto.createHash('sha512').update(pack.bytes).digest('base64'), stage.packs[product].integrity, 'pack SRI'); + agree(crypto.createHash('sha1').update(pack.bytes).digest('hex'), stage.packs[product].shasum, 'pack SHA1'); + for (const target of c.TARGETS) assert.ok(retained.some(x => x.kind === 'input' && x.relative === input.products[product].assets[target].file && + x.sha256 === input.products[product].assets[target].sha256), 'all twelve original native outer subjects'); + } + return { inputBytes, input, stageBytes, stage, retained, admitted }; +} +function actualState(j, roots, scenario) { + const bridge = require('./packed-installer-bridge'), scope = scopePaths(j, scenario, roots), p = hostPath(j.cell); + const snap = root => bridge.snapshot(root, true).sha256, prefix = {}, cache = {}; + for (const product of cell(j.cell).products) { + const packageRoot = p.join(scope.prefix, ...(j.cell.startsWith('windows-') ? [] : ['lib']), 'node_modules', contract.PACKAGES[product]); + const kinds = j.cell.startsWith('windows-') ? [['posix', ''], ['cmd', '.cmd'], ['powershell', '.ps1']] : [['posix', '']]; + const shims = kinds.map(([kind, suffix]) => p.join(scope.prefix, ...(j.cell.startsWith('windows-') ? [] : ['bin']), product + suffix)); + if (!fs.existsSync(packageRoot)) { assert.ok(shims.every(file => { try { fs.lstatSync(file); return false; } catch (e) { if (e.code === 'ENOENT') return true; throw e; } }), 'removed all shims, including dangling links'); prefix[product] = null; } + else prefix[product] = { tree: snap(packageRoot), shims: kinds.map(([kind], i) => { + const file = shims[i], st = fs.lstatSync(file), target = st.isSymbolicLink() ? fs.readlinkSync(file) : null; + const resolved = fs.realpathSync(file); assert.ok(resolved.startsWith(packageRoot + p.sep) || resolved === file, 'contained npm shim'); + return { kind, path: file, sha256: c.digest(fs.readFileSync(file)), mode: st.mode & 0o777, target }; + }) }; + const release = { descriptor: { schema: contract.DESCRIPTOR_SCHEMA, identity: j.identity, candidate_sha256: j.candidate_sha256 }, + version: j.identity.versions[product], asset: j.subjects[product][cell(j.cell).target] }; + const file = require('../lib/public-authoring').cachePath(p.join(scope.home, '.cache', 'universal-agent-plugins'), product, cell(j.cell).target, release); + cache[product] = fs.existsSync(file) ? { path: file, sha256: c.digest(c.readFile(file, contract.MAX_NATIVE_BYTES)), size: fs.statSync(file).size, mode: fs.statSync(file).mode & 0o777 } : null; + } + return { projects: c.digest(c.encode(Object.fromEntries(cell(j.cell).products.map(p => [p, bridge.snapshot(j.projects[p])])))), prefix, cache, client: snap(roots.client), state: snap(roots.state), + inputs: c.digest(c.encode([snap(roots.input), snap(roots.stage)])) }; +} +function verifySidecars(evidence, root) { + const rows = [...evidence['npm-lifecycle.json'].rows, ...evidence['cache-process.json'].rows, ...evidence['installer.json'].rows]; + const refs = rows.flatMap(r => [r.observation, ...(r.postinstall ? [r.postinstall.observation] : [])]); + refs.push(evidence['cache-process.json'].finalization.observation); + if (evidence['installer.json'].assessment) refs.push(evidence['installer.json'].assessment, ...evidence['installer.json'].readbacks); + const names = new Map(); let total = 0; + for (const name of EVIDENCE) for (const [file, bytes] of Object.entries(evidenceFiles(name, evidence[name]))) { + if (file !== name) refs.push({ path: file, size: bytes.length, sha256: c.digest(bytes) }); + else total += bytes.length; + } + for (const ref of refs) { + sidecar(ref); + if (names.has(ref.path)) { agree(names.get(ref.path), ref, 'same sidecar pin'); continue; } + names.set(ref.path, ref); total += ref.size; assert.ok(total <= AGGREGATE_LIMIT, '128MiB complete evidence closure'); + const body = pin(path.join(root, ref.path), ref.sha256, TRANSCRIPT_LIMIT); agree(body.length, ref.size, 'sidecar size'); + } + assert.ok(total <= AGGREGATE_LIMIT, '128MiB complete evidence closure'); + agree(fs.readdirSync(path.join(root, 'sidecars')).sort(), [...names.keys()].map(n => n.slice('sidecars/'.length)).sort(), 'exhaustive sidecar closure'); +} +function failureText(error) { + if (!error) return null; + const pending = [error], seen = new Set(); let text = '', count = 0; + while (pending.length && text.length < 120000 && count++ < 64) { + const current = pending.shift(); if (seen.has(current)) continue; seen.add(current); + text += String(current && current.stack || current).slice(0, 8192) + '\n'; + if (current && Array.isArray(current.errors)) pending.push(...current.errors.slice(0, 16)); + if (current && current.cause) pending.push(current.cause); + } + return text.slice(0, 120000) + (pending.length ? '\n[additional failure details bounded; journey failed]\n' : ''); +} +async function produceJourney(value) { + // Provision authority is checked before any untrusted record or output effect. + const provisioning = require('./public-authoring-tools'); + const controller = provisioning.requireController(process.platform === 'win32' ? `windows-${process.arch === 'x64' ? 'amd64' : 'arm64'}` : `${process.platform}-${process.arch === 'x64' ? 'amd64' : 'arm64'}`); + agree(controller, process.execPath, 'independently provisioned executing controller'); + fields(value, PRODUCE_FIELDS, 'closed producer request'); const r = value; + fixed(r.schema, 'authoring-public-produce/v1', 'producer schema'); cell(r.cell); tools(r.tools, cell(r.cell)); locator(r.stage); + fields(r.selected, ['tag', 'ref', 'source', 'versions'], 'selected source'); + for (const k of ['input_file', 'repo', 'work_parent', 'output']) absolute(r[k]); + agree(r.repo, path.resolve(__dirname, '../../..'), 'executing checkout'); + agree(r.tools.host, { platform: process.platform, arch: process.arch }, 'actual native host'); + const provision = provisioning.requireCellTools(r.cell), manifest = provisioning.readProvisioning(); + agree(r.tools.orchestrator_node, { path: controller, version: process.version, sha256: c.digest(c.readFile(controller)) }, 'controller comparison'); + for (const k of ['npm_node', 'shim_node', 'npm', 'go']) agree(r.tools[k], provision[k] === null ? null : Object.fromEntries(['path', 'sha256', 'version'].map(n => [n, provision[k][n]])), 'source-frozen cell tools'); + const api = requireFacades(r.cell), source = sourceSeal(r.repo, r.workflow_sha, { ...manifest, key: r.cell }); + c.safeDirectory(r.work_parent); c.safeDirectory(path.dirname(r.output)); assert.ok(!fs.existsSync(r.output), 'new owned output'); + disjoint([r.output, r.work_parent, r.repo, path.dirname(r.input_file)]); + const intake = admitProducerInputs(r, api['public-authoring-custody']), roots = rootsFor(r.output, r.cell); + disjoint([r.output, intake.admitted.input.root, intake.admitted.stage.root]); + for (const t of Object.values(r.tools).filter(t => t && t.path)) disjoint([r.output, t.path]); + disjoint([r.output, provision.npm.closure.root]); if (provision.mod_cache) disjoint([r.output, provision.mod_cache.root]); + const j = { schema: SCHEMA, status: 'completed', ...Object.fromEntries(['identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256', 'native_inputs', 'packs'].map(k => [k, intake.stage[k]])), + stage: r.stage, producer: r.producer, cell: r.cell, tools: r.tools, command_contract_sha256: c.digest(c.encode(commandContract(r.cell))), + subjects: Object.fromEntries(c.PRODUCTS.map(p => [p, intake.input.products[p].assets])), + projects: Object.fromEntries(cell(r.cell).products.map(p => [p, path.join(roots.projects, `${p} projects ü`)])), evidence: [], assertions: Object.fromEntries(ASSERTIONS.map(k => [k, true])) }; + // Installer availability is required before making output, npm or native effects. + if (api['public-installer-evidence']) api['public-installer-evidence'].requirePublicInstaller({ cell: r.cell, tools: r.tools, roots }); + fs.mkdirSync(r.output, { mode: 0o700 }); + try { + for (const root of Object.values(roots)) fs.mkdirSync(root, { mode: 0o700 }); + for (const retained of intake.retained) { + const file = path.join(roots[retained.kind], retained.relative); fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); + fs.writeFileSync(file, retained.bytes, { flag: 'wx', mode: retained.mode }); fs.chmodSync(file, retained.mode); + } + Object.values(j.projects).forEach(dir => fs.mkdirSync(dir, { mode: 0o700 })); + const scenarios = scenarioContract(r.cell), all = [...scenarios.npm, ...scenarios.cache, ...scenarios.installer]; + for (const scenario of all) { + const s = scopePaths(j, scenario, roots); + for (const dir of [s.prefix, s.home, path.join(s.home, 'tmp'), s.npmCache, s.cwd]) fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + for (const file of [s.userconfig, s.globalconfig]) if (!fs.existsSync(file)) fs.writeFileSync(file, '', { flag: 'wx', mode: 0o600 }); + } + const observed = new Map(), commands = new Array(commandContract(r.cell).length); let primary, terminal, finalError; + const recheck = () => { + provisioning.requireCellTools(r.cell); agree(sourceSeal(r.repo, r.workflow_sha, { ...manifest, key: r.cell }), source, 'source preserved'); + intake.retained.forEach(x => { for (const file of [x.file, path.join(roots[x.kind], x.relative)]) { + pin(file, x.sha256, contract.MAX_NATIVE_BYTES); agree(fs.statSync(file).mode & 0o777, x.mode, 'original and copied custody modes'); + } }); + }; + async function run(s, before) { + if (s.kind === 'repair') { + const b = before.cache[s.product]; assert.ok(b, 'owned exact repair target'); binary(b, j, s.product); + assert.ok(b.path.startsWith(scopePaths(j, s, roots).home + path.sep), 'owned corruption target only'); + fs.writeFileSync(b.path, CORRUPTION_BYTES, { flag: 'w' }); + before = actualState(j, roots, s); binary(before.cache[s.product], j, s.product, true); + } + if (s.kind === 'core' && commandContract(j.cell)[s.command].id === 'malformed-skill') { + const destination = path.join(scopePaths(j, s, roots).cwd, 'skill'); + fs.cpSync(path.join(j.projects[s.product], 'skill'), destination, { recursive: true, errorOnExist: true, force: false }); + fs.writeFileSync(path.join(destination, 'skills/extra-skill/SKILL.md'), '---\nname: [invalid\n---\n', { flag: 'w' }); + } + const pending = Promise.resolve(session.run(plannedInvocation(j, s, roots))).then(value => ({ value }), error => ({ error })); + let cancellationError; + if (s.event) { try { await session.cancel({ id: s.id, event: s.event }); } catch (error) { cancellationError = error; } } + const outcome = await pending; + if (cancellationError || outcome.error) throw new AggregateError([cancellationError, outcome.error].filter(Boolean), 'observed command/cancellation failed'); + const returned = outcome.value; fields(returned, ['row', 'stdout', 'stderr'], 'observed run result'); + const row = returned.row; for (const k of ['stdout', 'stderr']) { textValue(returned[k]); agree(row[k], { size: Buffer.byteLength(returned[k]), sha256: c.digest(Buffer.from(returned[k])) }, 'observed raw output'); } + agree(row.before, before, 'observed before state matches original live roots'); agree(row.after, actualState(j, roots, s), 'observed after state matches original live roots'); + if (s.kind === 'literal-argv') { + const root = path.join(scopePaths(j, s, roots).cwd, 'literal project ü'), bytes = c.readFile(path.join(root, 'plugin.json'), LIMIT); + const manifest = outputJSON(bytes.toString('utf8')); + agree(manifest.description, LITERAL_DESCRIPTION, 'literal argv bytes/cwd effects through real shim'); + agree(row.literal, { root, description: manifest.description, manifest_sha256: c.digest(bytes) }, 'observed literal effect matches original bytes'); + } + if (s.command !== null) { + const core = commandContract(j.cell)[s.command]; commands[s.command] = { product: core.product, id: core.id, argv: core.argv, cwd: row.cwd, + status: row.status, signal: row.signal, stdout: returned.stdout, stderr: returned.stderr }; + } + verifyObservedRow(row, s, j, roots, commands); observed.set(s.id, row); + } + const session = await api['public-process-observation'].openPublicObservation({ cell: r.cell, tools: r.tools, roots }); + try { + for (const method of ['finish', 'run', 'cancel']) assert.equal(typeof session?.[method], 'function', `PUBLIC_FACADE_REQUIRED:public-process-observation.js#session.${method}`); + for (let i = 0; i < all.length;) { + const s = all[i], group = [s]; i++; + if (s.group) while (i < all.length && all[i].group === s.group) group.push(all[i++]); + recheck(); + const before = group.map(s => actualState(j, roots, s)); + // Admit the entire group before launching any member; no source scan or + // synchronous cache walk serializes its four actual requests. + const outcomes = await Promise.allSettled(group.map((s, i) => run(s, before[i]))); + const errors = outcomes.filter(x => x.status === 'rejected').map(x => x.reason); + try { recheck(); } catch (error) { errors.push(error); } + if (errors.length) throw new AggregateError(errors, 'C3 fixed scenario failed'); + } + } catch (e) { primary = e; } + finally { try { if (typeof session?.finish === 'function') terminal = await session.finish(); } catch (e) { finalError = e; } } + if (primary || finalError) { + fs.writeFileSync(path.join(roots.evidence, 'failure.json'), c.encode({ primary: failureText(primary), finalization: failureText(finalError) }), { flag: 'wx', mode: 0o600 }); + throw new AggregateError([primary, finalError].filter(Boolean), 'C3 journey incomplete; no J'); + } + fields(terminal, ['finalization', 'assessment', 'readbacks'], 'observation terminal evidence'); + const evidence = { 'commands.json': commands, + 'projects.json': Object.fromEntries(cell(r.cell).products.map(p => [p, require('./packed-installer-bridge').snapshot(j.projects[p])])), + 'npm-lifecycle.json': { schema: 'authoring-public-npm-lifecycle/v1', cell: r.cell, rows: scenarios.npm.map(s => observed.get(s.id)) }, + 'cache-process.json': { schema: 'authoring-public-cache-process/v1', cell: r.cell, rows: scenarios.cache.map(s => observed.get(s.id)), finalization: terminal.finalization }, + 'installer.json': { schema: 'authoring-public-installer/v1', cell: r.cell, rows: scenarios.installer.map(s => observed.get(s.id)), assessment: terminal.assessment, readbacks: terminal.readbacks } }; + verifyJourney({ record: j, evidence }); recheck(); + const late = admitProducerInputs(r, api['public-authoring-custody']); agree(late.stageBytes, intake.stageBytes, 'late authenticated S'); agree(late.inputBytes, intake.inputBytes, 'late authenticated I'); + agree(late.retained.map(({ kind, relative, sha256, mode }) => ({ kind, relative, sha256, mode })), intake.retained.map(({ kind, relative, sha256, mode }) => ({ kind, relative, sha256, mode })), 'late complete custody multiset'); recheck(); + for (const name of EVIDENCE) { + const files = evidenceFiles(name, evidence[name]); + for (const [file, bytes] of Object.entries(files)) fs.writeFileSync(path.join(roots.evidence, file), bytes, { flag: 'wx', mode: 0o600 }); + const bytes = files[name]; j.evidence.push({ path: name, size: bytes.length, sha256: c.digest(bytes) }); + } + verifySidecars(evidence, roots.evidence); recheck(); + const bytes = encodeJourney(j, intake.inputBytes, intake.stageBytes); + const admission = { schema: 'authoring-public-local-inputs/v1', selected: r.selected, workflow_sha: r.workflow_sha, input_file: path.join(roots.input, contract.INPUT_FILE), stage: r.stage, + repo: r.repo, work_parent: r.work_parent, stage_root: roots.stage, input_root: roots.input, journey_root: roots.evidence, fixture_root: roots.projects, cell: r.cell, tools: r.tools, producer: r.producer }; + const admissionFile = path.join(roots.admission, 'local-inputs.json'), admissionBytes = c.encode(admission); + fs.writeFileSync(admissionFile, admissionBytes, { flag: 'wx', mode: 0o600 }); recheck(); + fs.writeFileSync(path.join(roots.evidence, 'public-journey.json'), bytes, { flag: 'wx', mode: 0o600 }); + return { record: j, request: { intake: INTAKE, expectedCommit: r.workflow_sha, journey: path.join(roots.evidence, 'public-journey.json'), + journeySha256: c.digest(bytes), admission: admissionFile, admissionSha256: c.digest(admissionBytes), fixtureRoot: roots.projects } }; + } catch (error) { + const diagnostic = path.join(fs.existsSync(roots.evidence) ? roots.evidence : r.output, 'failure.json'); + if (!fs.existsSync(diagnostic)) { + try { fs.writeFileSync(diagnostic, c.encode({ primary: failureText(error), finalization: null }), { flag: 'wx', mode: 0o600 }); } + catch (diagnosticError) { throw new AggregateError([error, diagnosticError], 'C3 incomplete; failure receipt could not be written'); } + } + throw error; + } +} + +function readJourney(value) { + const r = request(value), admission = bounded(pin(r.admission, r.admissionSha256), LIMIT); + requireFacades(admission.cell); + const provision = require('./public-authoring-tools'), selected = cell(admission.cell); + agree(provision.requireController(selected.target), process.execPath, 'trusted local controller'); provision.requireCellTools(selected.key); + const frozen = { ...provision.readProvisioning(), key: selected.key }, before = sourceSeal(admission.repo, r.expectedCommit, frozen); + const local = readJourneyInputs(value); verifyJourney(local); verifySidecars(local.evidence, path.dirname(value.journey)); + provision.requireCellTools(selected.key); agree(sourceSeal(admission.repo, r.expectedCommit, frozen), before, 'late source closure'); return 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) { + if (args.length === 2 && args[0] === "--produce-journey") return produceJourney(fileJSON(args[1])); + 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 = { generatedFiles, generatedTreeIdentity, evidenceFiles, expandEvidence, scenarioContract, plannedInvocation, rootsFor, requireFacades, verifyNpmLifecycle, verifyCacheProcess, verifyResults, produceJourney, + expectedCachePath, PROFILES, SURFACE, clientFacts, componentFacts, LITERAL_DESCRIPTION, outputJSON, matrix, commandContract, encodeJourney, decodeJourney, readJourneyInputs, verifyJourney, readJourney, + readAcceptance, request, fileJSON, disjoint, main, LIMIT, SCHEMA, MATRIX_SCHEMA, INTAKE, WORKFLOW, MISSING }; +if (require.main === module) { + Promise.resolve().then(() => main(process.argv.slice(2))).then(result => process.stdout.write(c.encode(result))).catch(error => { process.stderr.write(`C3 public journey: ${error.message}\n`); process.exitCode = 1; }); +} diff --git a/npm/agentplugins/scripts/public-authoring-tools.js b/npm/agentplugins/scripts/public-authoring-tools.js new file mode 100644 index 00000000..dc3eb084 --- /dev/null +++ b/npm/agentplugins/scripts/public-authoring-tools.js @@ -0,0 +1,148 @@ +"use strict"; +// Source checkout + independently immutable provision are authority, never receipts. +const fs = require("node:fs"); +const path = require("node:path"); +const assert = require("node:assert/strict"); +const c = require("./dual-authoring-candidate"); +const { fields: checkedFields, hash } = require("../lib/public-authoring-contract").checks; +const TARGETS = ["linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64", "windows-amd64", "windows-arm64"]; +const CELLS = TARGETS.flatMap(t => ["kit-node18", "pair-node22", "pair-node24"].map(l => `${t}/${l}`)); +const TOOLS = ["node", "python", "git", "gh", "tar"]; +const CELL_FIELDS = ["runner", "image", "controller", "npm_node", "shim_node", "npm", "go", "mod_cache", "observer", "installer_policy"]; +const ROOT = path.resolve(__dirname, "../../.."); +const MANIFEST = path.join(ROOT, ".github/authoring-public-tools.json"); +const LIMIT = 1024 * 1024; +function fields(v, names, label) { + if (v && typeof v === "object" && !Array.isArray(v)) for (const name of names) { + if (!Object.hasOwn(v, name)) absent(["six controllers", "eighteen cells"].includes(label) ? `${name}:entry` : `${label}:${name}`); + } + checkedFields(v, names, label); assert.deepEqual(Object.keys(v), names, "ordered provision fields"); +} +function absent(label) { throw new Error(`PUBLIC_PROVISIONING_REQUIRED:${label}`); } +function text(v) { assert.ok(typeof v === "string" && /^[\x21-\x7e]{1,256}$/.test(v), "bounded provision identity"); } +// Absolute path limit: 4096 Unicode code points in both decoders. +function absolute(v, target) { + assert.ok(typeof v === "string" && Array.from(v).length <= 4096 && !/[\x00-\x1f\x7f]/.test(v), "provision path"); + const p = target.startsWith("windows-") ? path.win32 : path.posix; + assert.ok(p.isAbsolute(v) && p.normalize(v) === v && v !== p.parse(v).root && + !v.startsWith("\\\\") && !v.startsWith("//") && !v.endsWith(p.sep), "canonical provision path"); +} +function identity(v) { fields(v, ["id", "sha256"], "provision identity"); text(v.id); hash(v.sha256, "identity"); } +function closure(v, target) { + fields(v, ["root", "files"], "complete provision closure"); absolute(v.root, target); + assert.ok(Array.isArray(v.files) && v.files.length > 0 && v.files.length <= 4096, "bounded provision closure"); + let last = ""; + for (const row of v.files) { + fields(row, ["path", "sha256"], "closure file"); hash(row.sha256, "closure file"); + assert.ok(typeof row.path === "string" && /^[A-Za-z0-9_.@+-]+(?:\/[A-Za-z0-9_.@+-]+)*$/.test(row.path) && + row.path.length <= 4096 && !row.path.split("/").some(x => x === "." || x === "..") && + row.path > last, "ordered unique relative closure files"); last = row.path; + } +} +function tool(v, target, npm = false) { + fields(v, npm ? ["path", "version", "sha256", "closure"] : ["path", "version", "sha256"], "provision tool"); + absolute(v.path, target); text(v.version); hash(v.sha256, "tool"); + if (npm) { + closure(v.closure, target); + const p = target.startsWith("windows-") ? path.win32 : path.posix; + assert.ok(v.closure.files.some(f => p.join(v.closure.root, ...f.path.split("/")) === v.path && f.sha256 === v.sha256), "npm CLI in complete closure"); + } +} +function decode(body) { + assert.ok(body.length > 0 && body.length <= LIMIT, "bounded provision manifest"); + const s = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body); + let depth = 0, quoted = false, escaped = false; + for (const ch of s) { + 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 <= 8, "provision depth"); + else if (ch === "}" || ch === "]") depth--; + } + const v = JSON.parse(s); + assert.deepEqual(body, c.encode(v), "canonical provision JSON: duplicates/alternate encoding rejected"); + fields(v, ["schema", "controllers", "cells", "reader"], "provision manifest"); + assert.equal(v.schema, "authoring-public-tools/v1"); assert.equal(v.reader, "linux-amd64"); + fields(v.controllers, TARGETS, "six controllers"); fields(v.cells, CELLS, "eighteen cells"); + for (const target of TARGETS) { + fields(v.controllers[target], TOOLS, target); + for (const t of TOOLS) if (v.controllers[target][t] !== null) tool(v.controllers[target][t], target); + } + for (const key of CELLS) { + const row = v.cells[key], target = key.split("/")[0], major = key.match(/node(\d+)$/)[1]; + fields(row, CELL_FIELDS, key); assert.equal(row.controller, target); + for (const t of ["runner", "image", "observer", "installer_policy"]) if (row[t] !== null) identity(row[t]); + for (const t of ["npm_node", "shim_node", "npm", "go"]) if (row[t] !== null) { + tool(row[t], target, t === "npm"); + if (t.endsWith("_node")) assert.match(row[t].version, new RegExp(`^v${major}\\.[0-9]+\\.[0-9]+$`)); + } + if (row.mod_cache !== null) closure(row.mod_cache, target); + } + return v; +} +function freeze(v) { if (v && typeof v === "object") { Object.values(v).forEach(freeze); Object.freeze(v); } return v; } +function readProvisioning() { + assert.equal(arguments.length, 0, "provision root is trusted source only"); + try { return freeze(decode(c.readFile(MANIFEST, LIMIT))); } + catch (e) { if (e.code === "ENOENT") absent("linux-amd64:manifest"); throw e; } +} +function checkTool(t, label) { + if (t === null) absent(label); + let bytes; + try { bytes = c.readFile(t.path, 256 * LIMIT); } + catch (e) { if (e.code === "ENOENT") absent(label); throw e; } + assert.equal(c.digest(bytes), t.sha256, `source-frozen provision pin mismatch:${label}`); +} +// Closure members alone may be empty; manifests and tools retain c.readFile. +function readClosureFile(file, maximum) { + c.safeDirectory(path.dirname(file)); + const before = fs.lstatSync(file, { bigint: true }); + assert.ok(before.isFile() && before.nlink === 1n && before.size >= 0n && before.size <= BigInt(maximum), + "closure file must be regular, bounded and unaliased"); + const same = st => ["dev", "ino", "mode", "nlink", "uid", "gid", "size", "mtimeNs", "ctimeNs"].every(k => st[k] === before[k]); + const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); + try { + assert.ok(same(fs.fstatSync(fd, { bigint: true })), "closure file changed"); + const body = Buffer.alloc(Number(before.size) + 1); + let length = 0, count; + while (length < body.length && (count = fs.readSync(fd, body, length, body.length - length, null)) > 0) length += count; + assert.ok(BigInt(length) === before.size && same(fs.fstatSync(fd, { bigint: true })) && + same(fs.lstatSync(file, { bigint: true })), "closure file changed"); + c.safeDirectory(path.dirname(file)); + return body.subarray(0, length); + } finally { fs.closeSync(fd); } +} +function checkClosure(value, label) { + if (value === null) absent(label); + const found = []; let entries = 0, total = 0; + function walk(dir, relative) { + c.safeDirectory(dir); + for (const name of fs.readdirSync(dir).sort()) { + assert.ok(++entries <= 8192, "closure entry bound"); + const file = path.join(dir, name), rel = relative ? `${relative}/${name}` : name, st = fs.lstatSync(file); + total += st.isFile() ? st.size : 0; assert.ok(total <= 256 * LIMIT, "closure byte bound"); + if (st.isDirectory()) walk(file, rel); + else { assert.ok(found.length < 4096, "closure bound"); found.push({ path: rel, sha256: c.digest(readClosureFile(file, 256 * LIMIT)) }); } + } + } + try { walk(value.root, ""); } catch (e) { if (e.code === "ENOENT") absent(label); throw e; } + found.sort((a, b) => a.path < b.path ? -1 : 1); + assert.deepEqual(found, value.files, `complete source-frozen closure mismatch:${label}`); +} +function requireController(key = "linux-amd64") { + assert.ok(arguments.length <= 1 && TARGETS.includes(key), "fixed controller key"); + const provision = readProvisioning(); + for (const t of TOOLS) checkTool(provision.controllers[key][t], `${key}:${t}`); + return provision.controllers[key].node.path; +} +function requireCellTools(key) { + assert.ok(arguments.length === 1 && CELLS.includes(key), "fixed cell key"); + const provision = readProvisioning(), row = provision.cells[key]; + // Availability checks precede any tool access or future cell scheduling. + const required = CELL_FIELDS.filter(n => !(["go", "mod_cache"].includes(n) && key !== "linux-amd64/pair-node22") && !(n === "installer_policy" && key.endsWith("kit-node18"))); + for (const name of required) if (row[name] === null) absent(`${key}:${name}`); + requireController(row.controller); + for (const name of ["npm_node", "shim_node", "npm", "go"]) if (required.includes(name)) checkTool(row[name], `${key}:${name}`); + checkClosure(row.npm.closure, `${key}:npm`); if (required.includes("mod_cache")) checkClosure(row.mod_cache, `${key}:mod_cache`); + return row; +} +module.exports = { readProvisioning, requireController, requireCellTools }; diff --git a/npm/agentplugins/scripts/stage-authoring-npm.js b/npm/agentplugins/scripts/stage-authoring-npm.js index e42c3eca..19ac6718 100644 --- a/npm/agentplugins/scripts/stage-authoring-npm.js +++ b/npm/agentplugins/scripts/stage-authoring-npm.js @@ -1,22 +1,41 @@ #!/usr/bin/env node "use strict"; -// Preparation only. B must authenticate real signed promotion inputs before it -// can introduce public staging. This entrypoint never generates qualification. +// Preparation and C1 stage source interfaces. Neither qualifies packs. +// Positive stage execution requires separately accepted artifact/workflow tools. const fs = require("node:fs"); const path = require("node:path"); const c = require("./dual-authoring-candidate"); const adapter = require("./authoring-release"); const packing = require("./stage-dual-authoring-npm"); const runtime = require("../lib/public-authoring"); +const inputs = require("./authoring-native-inputs"); +const { projectionBytes } = require("../lib/public-authoring-contract"); +const crypto = require("node:crypto"); +const { TextDecoder } = require("node:util"); const PREFIX = "npm/agentplugins/"; const COMMON = Object.freeze(["lib/verifier.js", "lib/public-authoring.js", "scripts/dual-authoring-candidate.js"]); +const STAGE_COMMON = Object.freeze([...COMMON, "lib/public-authoring-contract.js", "lib/public-authoring-input.js"]); const ownFiles = product => ["LICENSE", "README.md", "package.json", `bin/${product}.js`, "lib/platform.js", product === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js"]; const ALLOWLIST = Object.freeze([...new Set([...COMMON.map(n => PREFIX + n), ...c.PRODUCTS.flatMap(p => ownFiles(p).map(n => `npm/${p}/${n}`)), ...["stage-authoring-npm.js", "stage-dual-authoring-npm.js", "stage-dual-authoring-candidate.js", "authoring-release.js"].map(n => PREFIX + "scripts/" + n)])]); +// Separate future stage provenance inventory. Never extend the legacy +// preparation wrapper_blobs receipt or ship these producer helpers in a pack. +const STAGE_ALLOWLIST = Object.freeze([...ALLOWLIST, + ...STAGE_COMMON.filter(n => !COMMON.includes(n)).map(n => PREFIX + n), + ...["authoring-native-inputs.js", "authoring-promotion.js", "authoring-native-qualification.js", + "platform-proof.js", "npm-public-contract.js"].map(n => PREFIX + "scripts/" + n), + "scripts/read-authoring-evidence-zip.py", ".github/workflows/agentplugins-release.yml", + ".github/workflows/agentplugins-npm-publish.yml"]); +const STAGE_SCHEMA = "dual-authoring-public-stage/v1"; +const STAGE_WORKFLOW = ".github/workflows/agentplugins-npm-publish.yml"; +const MAX_STAGE_BYTES = 1024 * 1024; +const ASSERTIONS = Object.freeze(["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"]); const write = (file, bytes, mode = 0o644) => { fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); fs.writeFileSync(file, bytes, { flag: "wx", mode }); @@ -40,6 +59,524 @@ function packageFiles(product, source, manifestBytes, candidate) { return files; } +// These fixed pure contracts establish byte/shape consistency ONLY. In +// particular, parsing S's assertions cannot establish that any assertion is true. +// Authentication belongs to the separate integrated operations below. +function stageFields(value, names, label) { + if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) { + throw new Error(`${label}: ordinary data object required`); + } + const own = Reflect.ownKeys(value); + if (own.length !== names.length || own.some(k => typeof k !== "string" || !names.includes(k))) { + throw new Error(`${label}: unexpected or missing fields`); + } + for (const key of own) { + const d = Object.getOwnPropertyDescriptor(value, key); + if (!d.enumerable || !("value" in d)) throw new Error(`${label}: enumerable data fields required`); + } + c.keys(value, names, label); +} +function stageEqual(value, expected, label) { + if (value !== expected) throw new Error(`${label}: stage binding mismatch`); + return value; +} +function stageHash(value, length = 64) { + if (typeof value !== "string" || value.length !== length || !/^[0-9a-f]+$/.test(value) || /^0+$/.test(value)) { + throw new Error("stage nonzero lowercase digest required"); + } + return value; +} +function stageInteger(value, maximum = Number.MAX_SAFE_INTEGER) { + if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) throw new Error("stage positive bounded integer required"); + return value; +} +function stageBytes(value, maximum) { + if (!Buffer.isBuffer(value) || value.length === 0 || value.length > maximum) throw new Error("stage nonempty bounded Buffer required"); + return value; +} +const stageClosure = product => [...ownFiles(product), ...STAGE_COMMON, "bin/package.json", "lib/package.json", + "scripts/package.json", "public-release.json", "release-manifest.json", inputs.INPUT_FILE].sort(); + +function stageDescriptors(input, inputBytes) { + // Preflight BOTH 64 KiB limits, even when I itself fits its 1 MiB contract. + // Stable versions keep the accepted I policy; no artificial version ceiling. + return Object.fromEntries(c.PRODUCTS.map(product => [product, inputs.encodeDescriptor({ + schema: inputs.DESCRIPTOR_SCHEMA, product, npm_package: inputs.PACKAGES[product], identity: input.identity, + authoring_mode: input.authoring_mode, asset_scope: input.asset_scope, candidate_sha256: input.candidate_sha256, + release_manifest_sha256: input.products[product].manifest_sha256, + input_binding: { file: inputs.INPUT_FILE, sha256: c.digest(inputBytes) } + }, inputBytes, product)])); +} + +function stageBlob(value, withBytes) { + stageFields(value, ["git_blob", "mode", "sha256", ...(withBytes ? ["bytes"] : [])], "stage blob"); + const pin = { git_blob: stageHash(value.git_blob, 40), mode: value.mode, sha256: stageHash(value.sha256) }; + if (!["100644", "100755"].includes(pin.mode)) throw new Error("stage regular Git mode required"); + if (withBytes) { + const body = stageBytes(value.bytes, inputs.MAX_NATIVE_BYTES); + stageEqual(c.digest(body), pin.sha256, "source SHA256"); + const blob = crypto.createHash("sha1").update(`blob ${body.length}\0`).update(body).digest("hex"); + stageEqual(blob, pin.git_blob, "source Git blob"); + } + return pin; +} + +/** Pure final-format pair constructor. Supplied blobs and canonical projections + * are byte contracts, NOT authenticated source/provenance. The integrated C1 + * producer must authenticate I/all eighteen inputs and committed F first, then + * reuse packPackage once per product and completeRecord after revalidation. */ +function pairedPackageFiles(source, manifests, inputBytes) { + const input = inputs.decodeInputs(inputBytes); + const descriptors = stageDescriptors(input, inputBytes); + stageFields(source, STAGE_ALLOWLIST, "stage source closure"); + for (const name of STAGE_ALLOWLIST) stageBlob(source[name], true); + for (const name of STAGE_COMMON.filter(n => !COMMON.includes(n))) { + stageEqual(source[PREFIX + name].mode, "100644", "v2 helper mode"); + } + stageFields(manifests, c.PRODUCTS, "paired manifest bytes"); + for (const product of c.PRODUCTS) { + const body = stageBytes(manifests[product], inputs.MAX_INPUT_BYTES); + const { manifest: expected, checksums } = projectionBytes(input, product); + if (!body.equals(expected)) throw new Error("stage projection bytes differ from I"); + stageEqual(c.digest(body), input.products[product].manifest_sha256, "selected manifest hash"); + stageEqual(c.digest(checksums), input.products[product].checksums_sha256, "projection checksums hash"); + const baseBytes = source[`npm/${product}/package.json`].bytes; + const base = JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(baseBytes)); + if (!base || Array.isArray(base) || typeof base !== "object") throw new Error("source package object required"); + stageEqual(base.name, inputs.PACKAGES[product], "source package name"); + c.keys(base.bin, [product], "source bin"); + stageEqual(base.bin[product], `bin/${product}.js`, "source bin"); + c.keys(base.engines, ["node"], "source engines"); + stageEqual(base.engines.node, product === "agentplugins" ? ">=22" : ">=18", "source Node support"); + const scripts = product === "agentplugins" ? { test: "node --test" } : { postinstall: "node ./lib/install.js" }; + c.keys(base.scripts, Object.keys(scripts), "source scripts"); + for (const name of Object.keys(scripts)) stageEqual(base.scripts[name], scripts[name], "source script"); + } + const pair = {}; + for (const product of c.PRODUCTS) { + const files = packageFiles(product, source, manifests[product], { + identity: input.identity, manifestDigest: input.candidate_sha256 }); + files["public-release.json"] = descriptors[product]; + files[inputs.INPUT_FILE] = inputBytes; + for (const name of STAGE_COMMON) files[name] = source[PREFIX + name].bytes; + files["package.json"] = c.encode({ ...JSON.parse(files["package.json"]), private: false, files: stageClosure(product) }); + // Own the returned buffers, including each product's I and shared runtime. + pair[product] = Object.fromEntries(Object.entries(files).map(([name, body]) => [name, Buffer.from(body)])); + } + return pair; +} + +function stageRecord(value, inputBytes) { + const input = inputs.decodeInputs(inputBytes); + stageFields(value, ["schema", "identity", "authoring_mode", "asset_scope", "candidate_sha256", "pair_marker_sha256", + "projection_pins", "native_inputs", "wrapper_blobs", "generated", "packs", "tools", "producer", "assertions"], "stage"); + // Reuse the accepted descriptor identity validation (including closed nested + // fields) instead of relaxing candidate identity or introducing a new policy. + const descriptors = stageDescriptors({ ...input, identity: value.identity }, inputBytes); + stageEqual(value.schema, STAGE_SCHEMA, "schema"); + stageEqual(value.authoring_mode, input.authoring_mode, "mode"); + stageEqual(value.asset_scope, input.asset_scope, "scope"); + stageEqual(value.candidate_sha256, input.candidate_sha256, "candidate"); + stageEqual(value.pair_marker_sha256, input.pair_marker_sha256, "pair marker"); + stageFields(value.projection_pins, c.PRODUCTS, "projection pins"); + const projection_pins = {}; + for (const product of c.PRODUCTS) { + const pin = value.projection_pins[product]; + stageFields(pin, ["manifest_sha256", "checksums_sha256"], "projection pin"); + projection_pins[product] = { + manifest_sha256: stageEqual(pin.manifest_sha256, input.products[product].manifest_sha256, "manifest"), + checksums_sha256: stageEqual(pin.checksums_sha256, input.products[product].checksums_sha256, "checksums") }; + } + const native = value.native_inputs; + stageFields(native, ["sha256", "artifact"], "native inputs"); + stageFields(native.artifact, ["run_id", "run_attempt", "artifact_id", "artifact_sha256"], "input artifact"); + const a = native.artifact; + const native_inputs = { sha256: stageEqual(native.sha256, c.digest(inputBytes), "I bytes"), artifact: { + run_id: stageEqual(a.run_id, input.producer.run_id, "I producer run"), + run_attempt: stageEqual(a.run_attempt, input.producer.run_attempt, "I producer attempt"), + artifact_id: stageInteger(a.artifact_id), artifact_sha256: stageHash(a.artifact_sha256) } }; + stageFields(value.wrapper_blobs, STAGE_ALLOWLIST, "stage wrapper blobs"); + const wrapper_blobs = Object.fromEntries(STAGE_ALLOWLIST.map(n => [n, stageBlob(value.wrapper_blobs[n], false)])); + stageFields(value.generated, c.PRODUCTS, "generated pair"); + const generated = {}; + for (const product of c.PRODUCTS) { + const g = value.generated[product]; + stageFields(g, stageClosure(product), "generated closure"); + generated[product] = Object.fromEntries(stageClosure(product).map(n => [n, stageHash(g[n])])); + for (const name of ownFiles(product).filter(n => n !== "package.json")) { + stageEqual(g[name], wrapper_blobs[`npm/${product}/${name}`].sha256, "generated source file"); + } + for (const name of STAGE_COMMON) stageEqual(g[name], wrapper_blobs[PREFIX + name].sha256, "shared runtime"); + for (const dir of ["bin", "lib", "scripts"]) { + stageEqual(g[`${dir}/package.json`], c.digest(c.encode({ type: "commonjs" })), "CommonJS scope"); + } + stageEqual(g[inputs.INPUT_FILE], native_inputs.sha256, "generated I"); + stageEqual(g["release-manifest.json"], projection_pins[product].manifest_sha256, "generated manifest"); + stageEqual(g["public-release.json"], c.digest(descriptors[product]), "generated descriptor"); + } + stageFields(value.packs, c.PRODUCTS, "stage packs"); + const packs = {}; + for (const product of c.PRODUCTS) { + const p = value.packs[product]; + stageFields(p, ["file", "sha256", "size", "integrity", "shasum"], "pack"); + if (typeof p.integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(p.integrity) || + p.integrity.length !== 95 || Buffer.from(p.integrity.slice(7), "base64").toString("base64") !== p.integrity.slice(7)) { + throw new Error("canonical SHA512 SRI required"); + } + packs[product] = { file: stageEqual(p.file, `${inputs.PACKAGES[product]}-${input.identity.versions[product]}.tgz`, "tarball name"), + sha256: stageHash(p.sha256), size: stageInteger(p.size, inputs.MAX_NATIVE_BYTES), + integrity: p.integrity, shasum: stageHash(p.shasum, 40) }; + } + stageFields(value.tools, ["node", "npm", "git", "tar", "gh"], "stage tools"); + const tools = {}; + for (const name of ["node", "npm", "git", "tar", "gh"]) { + const t = value.tools[name]; + stageFields(t, ["version", "sha256"], "tool"); + if (typeof t.version !== "string" || !t.version.length || t.version.length > MAX_STAGE_BYTES || + t.version.trim() !== t.version || /[\x00-\x1f\x7f]/.test(t.version)) throw new Error("stage tool version string required"); + tools[name] = { version: t.version, sha256: stageHash(t.sha256) }; + } + const p = value.producer; + stageFields(p, ["workflow", "source", "ref", "run_id", "run_attempt"], "stage producer"); + const producer = { workflow: stageEqual(p.workflow, STAGE_WORKFLOW, "stage workflow"), + source: stageEqual(p.source, input.identity.commit, "stage source"), + ref: stageEqual(p.ref, `refs/tags/${input.products.agentplugins.tag}`, "stage ref"), + run_id: stageInteger(p.run_id), run_attempt: stageInteger(p.run_attempt, 1000) }; + stageFields(value.assertions, ASSERTIONS, "stage assertions"); + const assertions = Object.fromEntries(ASSERTIONS.map(n => [n, stageEqual(value.assertions[n], true, "assertion syntax")])); + return { schema: STAGE_SCHEMA, identity: input.identity, authoring_mode: input.authoring_mode, asset_scope: input.asset_scope, + candidate_sha256: input.candidate_sha256, pair_marker_sha256: input.pair_marker_sha256, projection_pins, + native_inputs, wrapper_blobs, generated, packs, tools, producer, assertions }; +} + +/** Canonical S codec only. Does not attest/assert truth or authorize effects. */ +function encodeStage(value, inputBytes) { + return stageBytes(c.encode(stageRecord(value, inputBytes)), MAX_STAGE_BYTES); +} + +/** Canonical S codec only, NOT authenticated readStage. Retained tarballs, + * source at F, tool provision, signatures and operation evidence are external. */ +function decodeStage(body, inputBytes) { + stageBytes(body, MAX_STAGE_BYTES); + 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 === "[") throw new Error("stage arrays are outside the fixed contract"); + else if (ch === "{" && ++depth > 4) throw new Error("stage object depth limit"); + else if (ch === "}") depth--; + } + const value = stageRecord(JSON.parse(text), inputBytes); + if (!body.equals(stageBytes(c.encode(value), MAX_STAGE_BYTES))) throw new Error("noncanonical stage bytes"); + return value; +} + +// Integrated source operations below use only fixed external boundaries. The +// checked artifact reader's input-provenance/public-stage kinds and protected +// workflows remain separately required before authentic positive execution. +const promotion = require("./authoring-promotion"); +const cp = require("node:child_process"); +const { isDeepStrictEqual: stageSame } = require("node:util"); +const agreeStage = (a, b, label) => { + if (!stageSame(a, b)) throw new Error(`C1 stage ${label} changed or mismatched`); +}; +function stageLocator(value) { + stageFields(value, ["run_id", "run_attempt", "artifact_id", "artifact_sha256"], "stage artifact locator"); + return { run_id: stageInteger(value.run_id), run_attempt: stageInteger(value.run_attempt, 1000), + artifact_id: stageInteger(value.artifact_id), artifact_sha256: stageHash(value.artifact_sha256) }; +} +function stageInvocation(value, input) { + stageFields(value, ["workflow", "source", "ref", "run_id", "run_attempt"], "stage invocation"); + const result = { workflow: STAGE_WORKFLOW, source: input.identity.commit, + ref: `refs/tags/${input.products.agentplugins.tag}`, run_id: stageInteger(value.run_id), + run_attempt: stageInteger(value.run_attempt, 1000) }; + agreeStage(value, result, "producer source/ref/workflow"); + if ([input.producer.run_id, input.preparation.artifact.run_id].includes(result.run_id)) { + throw new Error("separate stage and input/preparation runs required"); + } + return result; +} +function stageCaller(producer) { + const expected = { GITHUB_ACTIONS: "true", GITHUB_REPOSITORY: c.REPOSITORY, GITHUB_SHA: producer.source, + GITHUB_REF: producer.ref, GITHUB_RUN_ID: String(producer.run_id), GITHUB_RUN_ATTEMPT: String(producer.run_attempt), + GITHUB_WORKFLOW_SHA: producer.source, GITHUB_WORKFLOW_REF: `${c.REPOSITORY}/${STAGE_WORKFLOW}@${producer.ref}` }; + agreeStage(Object.fromEntries(Object.keys(expected).map(k => [k, process.env[k]])), expected, "workflow caller"); + return expected; +} +function stageOptions(value, reading) { + stageFields(value, ["input", "selected", "workflow_sha", "artifact", "repo", "workParent", "node", "npm", + ...(reading ? ["stage_sha256"] : ["producer", "output"])], "stage operation options"); + const body = Buffer.from(stageBytes(value.input, inputs.MAX_INPUT_BYTES)), input = inputs.decodeInputs(body); + stageDescriptors(input, body); // both bounded descriptors before any effects + // Match the existing readInputs provider boundary without changing pure I/S + // or descriptor limits. Provider-incompatible versions fail before effects. + if (Object.values(input.identity.versions).some(v => v.length > 32)) throw new Error("bounded provider versions required"); + stageFields(value.selected, ["tag", "ref", "source", "versions"], "stage selection"); + stageFields(value.selected.versions, c.PRODUCTS, "stage versions"); + const selected = { tag: input.products.agentplugins.tag, ref: `refs/tags/${input.products.agentplugins.tag}`, + source: input.identity.commit, versions: input.identity.versions }; + agreeStage(value.selected, selected, "selected identity"); + stageEqual(value.workflow_sha, input.identity.commit, "workflow revision F"); + const artifact = stageLocator(value.artifact); + if (input.producer.run_id === input.preparation.artifact.run_id) throw new Error("separate provenance run required"); + if (!reading) { + agreeStage([artifact.run_id, artifact.run_attempt], [input.producer.run_id, input.producer.run_attempt], "input attempt"); + if (artifact.artifact_id === input.preparation.artifact.artifact_id) throw new Error("separate input artifact required"); + } + if (reading && [input.producer.run_id, input.preparation.artifact.run_id].includes(artifact.run_id)) { + throw new Error("separate completed stage run required"); + } + const producer = reading ? null : stageInvocation(value.producer, input); + const stage_sha256 = reading ? stageHash(value.stage_sha256) : null; + for (const key of ["repo", "workParent"]) c.safeDirectory(value[key]); + const executing = path.resolve(__dirname, "../../.."); + for (const root of [value.repo, executing]) { + if (root === value.workParent || root.startsWith(value.workParent + path.sep) || value.workParent.startsWith(root + path.sep)) { + throw new Error("stage source/scratch roots overlap"); + } + } + for (const key of ["node", "npm"]) { + if (typeof value[key] !== "string" || !path.isAbsolute(value[key]) || path.resolve(value[key]) !== value[key]) { + throw new Error("absolute trusted stage tool required"); + } + c.readFile(value[key]); + } + if (!reading) { + stageCaller(producer); + // Existence is checked separately at reservation, so caller rechecks work + // after the output has been reserved without weakening initial placement. + const output = value.output; + if (typeof output !== "string" || !path.isAbsolute(output) || path.resolve(output) !== output || + !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(path.basename(output))) throw new Error("safe stage output required"); + c.safeDirectory(path.dirname(output)); + for (const root of [value.repo, executing, value.workParent, path.dirname(value.node), path.dirname(value.npm)]) { + if (output === root || output.startsWith(root + path.sep) || root.startsWith(output + path.sep)) throw new Error("stage output overlaps input"); + } + } + return { body, input, selected, workflow_sha: value.workflow_sha, artifact, producer, stage_sha256, + repo: value.repo, workParent: value.workParent, node: value.node, npm: value.npm, output: reading ? null : value.output }; +} +function stageTools(o, context) { + const command = (exe, args) => cp.execFileSync(exe, args, { env: context.env, cwd: context.root, + timeout: 30000, maxBuffer: MAX_STAGE_BYTES }).toString().trim(); + const paths = { node: o.node, npm: o.npm, git: "/usr/bin/git", tar: "/usr/bin/tar", gh: "/usr/bin/gh" }; + const tools = Object.fromEntries(Object.entries(paths).map(([name, file]) => [name, { + version: (name === "npm" ? command(o.node, [o.npm, "--version"]) : command(file, ["--version"])).split("\n")[0], + sha256: c.digest(c.readFile(file)) }])); + if (!tools.gh.version.startsWith(`gh version ${promotion.GH_VERSION} (`)) throw new Error("fixed stage gh provision required"); + return tools; +} +function toolSnapshot(o) { + // Invocation-local pins supplement S's portable five-tool fields. Include the + // running interpreter and the fixed checked-reader interpreter; no extra S keys. + return Object.fromEntries([...new Set([o.node, o.npm, process.execPath, "/usr/bin/git", "/usr/bin/tar", "/usr/bin/gh", + fs.realpathSync("/usr/bin/python3")])].map(file => [file, c.digest(c.readFile(file))])); +} +function sourcePins(source) { + return Object.fromEntries(Object.entries(source).map(([n, { bytes, ...pin }]) => [n, pin])); +} +function inputFiles(input) { + return ["preparation-run.json", "candidate-identity.json", "candidate/candidate.json", "pair-prepared.json", + ...c.PRODUCTS.flatMap(p => [...c.TARGETS.map(t => `${p}/${input.products[p].assets[t].file}`), + `${p}/release-manifest.json`, `${p}/checksums.txt`]), inputs.INPUT_FILE]; +} +function stageInputSnapshot(root, body) { + const input = inputs.decodeInputs(body); + const subjects = inputs.inputSubjects(root, body); // existing projected + receipt reader + if (subjects.length !== 19) throw new Error("exact nineteen input subjects required"); + const files = Object.fromEntries(inputFiles(input).map(n => [n, c.readFile(path.join(root, n), inputs.MAX_NATIVE_BYTES)])); + agreeStage(files[inputs.INPUT_FILE], body, "same retained I bytes"); + return { files, subjects: subjects.map(s => ({ file: path.relative(root, s.file), sha256: s.sha256 })) }; +} +function manifestsFrom(snapshot) { + return Object.fromEntries(c.PRODUCTS.map(p => [p, snapshot.files[`${p}/release-manifest.json`]])); +} +function generatedPins(pair) { + for (const name of [...STAGE_COMMON, inputs.INPUT_FILE]) agreeStage(pair.agentplugins[name], pair["plugin-kit-ai"][name], "shared generated bytes"); + return Object.fromEntries(c.PRODUCTS.map(p => [p, + Object.fromEntries(Object.entries(pair[p]).map(([n, b]) => [n, c.digest(b)]))])); +} +function retainedPack(root, product, input) { + const file = `${inputs.PACKAGES[product]}-${input.identity.versions[product]}.tgz`; + const bytes = c.readFile(path.join(root, file), inputs.MAX_NATIVE_BYTES); + return { file, ...c.metadata(bytes), integrity: "sha512-" + crypto.createHash("sha512").update(bytes).digest("base64"), + shasum: crypto.createHash("sha1").update(bytes).digest("hex") }; +} +function stageProviders(o, inputArtifact, cwd) { + promotion.checkInputTags(o.body, cwd); + return [promotion.inspectArtifact(inputArtifact, inputs.WORKFLOW, o.input.identity.commit, cwd), + promotion.inspectArtifact(o.input.preparation.artifact, inputs.WORKFLOW, o.input.identity.commit, cwd)]; +} +function stageReadInputs(o, artifact, scratch) { + return inputs.readInputs({ input: o.body, selected: o.selected, workflow_sha: o.workflow_sha, artifact, scratch }); +} +function checkGeneratedRoot(root, files) { + const walk = (dir, prefix = "") => fs.readdirSync(dir).flatMap(n => { + const relative = prefix + n, file = path.join(dir, n), st = fs.lstatSync(file); + if (st.isDirectory() && !st.isSymbolicLink()) return walk(file, relative + "/"); + return [relative]; + }); + agreeStage(walk(root).sort(), Object.keys(files).sort(), "generated file inventory"); + for (const [name, body] of Object.entries(files)) { + agreeStage(c.readFile(path.join(root, name)), body, "generated file bytes"); + stageEqual(fs.lstatSync(path.join(root, name)).mode & 0o777, /^bin\/[^/]+\.js$/.test(name) ? 0o755 : 0o644, "generated mode"); + } +} + +/** Produce unsigned S only after authenticated I and two verified packs. Never + * signs, publishes, qualifies or launches a native input. Failed work is retained. + * Protected positive execution is unavailable until the fixed reader/workflows + * and genuine tool/verifier prerequisites have been independently accepted. */ +function stagePrepublication(value) { + const o = stageOptions(value, false); + c.outputPlacement(o.output, [o.repo, o.workParent, path.resolve(__dirname, "../../.."), path.dirname(o.node), path.dirname(o.npm)]); + const context = packing.npmContext(o.workParent); + context.env.PATH = "/usr/local/bin:/usr/bin:/bin"; + const source = packing.blobs(o.repo, o.input.identity.commit, context.env, "stage"); + const toolPins = toolSnapshot(o), tools = stageTools(o, context), callerArgs = [...process.execArgv]; + agreeStage(promotion.inspectStageCaller(o.selected, o.workflow_sha, context.root), o.producer, "provider stage caller"); + const providers = stageProviders(o, o.artifact, context.root); + const admitted = stageReadInputs(o, o.artifact, context.root); + const before = stageInputSnapshot(admitted.root, o.body); + const snapshot = path.join(context.root, "stage-inputs"); + c.outputPlacement(snapshot, [admitted.root, o.repo]); + fs.mkdirSync(snapshot, { mode: 0o700 }); + for (const [n, b] of Object.entries(before.files)) write(path.join(snapshot, n), b, 0o444); + agreeStage(stageInputSnapshot(snapshot, o.body), before, "owned input snapshot"); + const pair = pairedPackageFiles(source, manifestsFrom(before), o.body), generated = generatedPins(pair); + // Authentication may have changed caller/source/tools; no pack until recheck. + agreeStage(stageOptions(value, false), o, "caller before packing"); + agreeStage(packing.blobs(o.repo, o.input.identity.commit, context.env, "stage"), source, "source before packing"); + agreeStage(toolSnapshot(o), toolPins, "tools before packing"); + fs.mkdirSync(o.output, { mode: 0o700 }); + process.stderr.write("C1_STAGE " + JSON.stringify({ operation: "start", source: o.producer.source, ref: o.producer.ref, + run_id: o.producer.run_id, run_attempt: o.producer.run_attempt, input_sha256: c.digest(o.body), input_artifact: o.artifact }) + "\n"); + const packs = {}; + for (const product of c.PRODUCTS) { + const root = path.join(o.output, product); fs.mkdirSync(root, { mode: 0o700 }); + for (const [n, b] of Object.entries(pair[product])) write(path.join(root, n), b, /^bin\/[^/]+\.js$/.test(n) ? 0o755 : 0o644); + const packed = packing.packPackage(product, pair[product], root, { node: o.node, npm: o.npm, + output: o.output, identity: o.input.identity }, context); + const retained = retainedPack(o.output, product, o.input), { shasum, ...legacy } = retained; + agreeStage(packed, legacy, "pack return versus retained bytes"); + packs[product] = retained; // actual SHA1, without changing v1 pack return/receipts + process.stderr.write("C1_STAGE " + JSON.stringify({ operation: "pack", product, pack: retained }) + "\n"); + } + for (const product of c.PRODUCTS) { + checkGeneratedRoot(path.join(o.output, product), pair[product]); + packing.verifyPack(path.join(o.output, packs[product].file), pair[product], path.join(context.root, `retained-${product}`), context.env); + } + agreeStage(stageProviders(o, o.artifact, context.root), providers, "input providers"); + agreeStage(stageInputSnapshot(admitted.root, o.body), before, "authenticated inputs after packing"); + agreeStage(stageInputSnapshot(snapshot, o.body), before, "snapshot after packing"); + agreeStage(packing.blobs(o.repo, o.input.identity.commit, context.env, "stage"), source, "source after packing"); + agreeStage(toolSnapshot(o), toolPins, "tools after packing"); + agreeStage(process.execArgv, callerArgs, "caller interpreter arguments"); + agreeStage(stageOptions(value, false), o, "caller before completion"); + agreeStage(promotion.inspectStageCaller(o.selected, o.workflow_sha, context.root), o.producer, "provider stage completion"); + for (const product of c.PRODUCTS) { + checkGeneratedRoot(path.join(o.output, product), pair[product]); + agreeStage(retainedPack(o.output, product, o.input), packs[product], "retained pair before completion"); + } + const record = { schema: STAGE_SCHEMA, identity: o.input.identity, authoring_mode: o.input.authoring_mode, + asset_scope: o.input.asset_scope, candidate_sha256: o.input.candidate_sha256, pair_marker_sha256: o.input.pair_marker_sha256, + projection_pins: Object.fromEntries(c.PRODUCTS.map(p => [p, { manifest_sha256: o.input.products[p].manifest_sha256, + checksums_sha256: o.input.products[p].checksums_sha256 }])), + native_inputs: { sha256: c.digest(o.body), artifact: o.artifact }, wrapper_blobs: sourcePins(source), generated, packs, tools, + producer: o.producer, assertions: Object.fromEntries(ASSERTIONS.map(n => [n, true])) }; + const completed = decodeStage(encodeStage(record, o.body), o.body); + packing.completeRecord(o.output, completed); + process.stderr.write("C1_STAGE " + JSON.stringify({ operation: "completion", + stage_sha256: c.digest(c.readFile(path.join(o.output, "completion.json"), MAX_STAGE_BYTES)) }) + "\n"); + return completed; +} + +/** Authenticate an independently pinned completed S artifact, then retrieve its + * referenced I through readInputs. The required input Buffer is a comparison + * pin, never an authentication flag. Check both retained packs without packing. + * Returns staging evidence only, never qualification or execution permission. */ +function retainedContext(value) { + const o = stageOptions(value, true), context = packing.npmContext(o.workParent); + context.env.PATH = "/usr/local/bin:/usr/bin:/bin"; + return { o, context, source: packing.blobs(o.repo, o.input.identity.commit, context.env, "stage"), toolPins: toolSnapshot(o) }; +} +function recheckSubjects(result) { + for (const row of result.subjects) pinFile(row.file, row.sha256, 128 * 1024 * 1024); +} +function readStage(value) { + const { o, context, source, toolPins } = retainedContext(value); + const before = promotion.inspectArtifact(o.artifact, STAGE_WORKFLOW, o.input.identity.commit, context.root); + const archive = promotion.acquireArtifact(o.artifact, STAGE_WORKFLOW, o.input.identity.commit, context.root); + const retained = openRetainedStage(o, context, archive); + const { record, subjects } = retained; + const multiset = subjects.map(s => ({ name: path.basename(s.file), digest: { sha256: s.sha256 } })); + for (const subject of subjects) promotion.verifyStageSubject(subject.file, { name: path.basename(subject.file), + sha256: subject.sha256, source: record.producer.source, workflow_sha: o.workflow_sha, ref: record.producer.ref, + run_id: record.producer.run_id, run_attempt: record.producer.run_attempt, subjects: multiset }, context.root); + const result = validateRetainedStage(value, o, context, source, toolPins, retained); + agreeStage(promotion.inspectArtifact(o.artifact, STAGE_WORKFLOW, o.input.identity.commit, context.root), before, "completed stage provider"); + agreeStage(packing.blobs(o.repo, o.input.identity.commit, context.env, "stage"), source, "source after signatures"); + agreeStage(toolSnapshot(o), toolPins, "tools after signatures"); + agreeStage(stageOptions(value, true), o, "caller after signatures"); + recheckSubjects(result); + return result; +} +/** Fixed same-run unsigned acquisition, only for paired_stage_attestation. + * It cannot weaken the completed reader or accept a caller's staging root. */ +function validateUnsignedStage(value) { + const { o, context, source, toolPins } = retainedContext(value); + const custody = { artifact: o.artifact, selected: o.selected, workflow_sha: o.workflow_sha, scratch: context.root }; + const before = promotion.inspectCurrentStage(custody); + const archive = promotion.acquireCurrentStage(custody); + const result = validateRetainedStage(value, o, context, source, toolPins, openRetainedStage(o, context, archive)); + stageCaller(result.record.producer); + agreeStage(promotion.inspectCurrentStage(custody), before, "current stage provider"); + recheckSubjects(result); + return result; +} +// Both entrypoints share precisely the retained byte checks. This private +// function has no completed/authenticated switches or injected verifier. +function openRetainedStage(o, context, archive) { + const names = ["completion.json", ...c.PRODUCTS.map(p => `${inputs.PACKAGES[p]}-${o.input.identity.versions[p]}.tgz`)]; + const root = promotion.extractArtifact(archive, o.artifact, "public-stage", names, path.join(context.root, "stage"), context.root); + const body = pinFile(path.join(root, "completion.json"), o.stage_sha256, MAX_STAGE_BYTES), record = decodeStage(body, o.body); + const invocation = stageInvocation(record.producer, o.input); + agreeStage([invocation.run_id, invocation.run_attempt], [o.artifact.run_id, o.artifact.run_attempt], "stage artifact attempt"); + if ([record.native_inputs.artifact.artifact_id, o.input.preparation.artifact.artifact_id].includes(o.artifact.artifact_id)) { + throw new Error("separate stage artifact required"); + } + const subjects = names.map(name => ({ file: path.join(root, name), sha256: name === "completion.json" ? o.stage_sha256 : + record.packs[c.PRODUCTS.find(p => record.packs[p].file === name)].sha256 })); + return { root, record, subjects, body }; +} +function validateRetainedStage(value, o, context, source, toolPins, retained) { + const { root, record, subjects, body } = retained; + const evidence = promotion.checkStageEvidence(o.artifact, o.selected, o.workflow_sha, record, o.stage_sha256, context.root); + const providers = stageProviders(o, record.native_inputs.artifact, context.root); + const admitted = stageReadInputs(o, record.native_inputs.artifact, context.root); + const snapshot = stageInputSnapshot(admitted.root, o.body); + const pair = pairedPackageFiles(source, manifestsFrom(snapshot), o.body); + agreeStage(record.wrapper_blobs, sourcePins(source), "authenticated source closure"); + agreeStage(record.generated, generatedPins(pair), "authenticated generated closures"); + for (const product of c.PRODUCTS) { + agreeStage(retainedPack(root, product, o.input), record.packs[product], "authenticated retained pack"); + packing.verifyPack(path.join(root, record.packs[product].file), pair[product], path.join(context.root, `read-${product}`), context.env); + } + agreeStage(promotion.checkStageEvidence(o.artifact, o.selected, o.workflow_sha, record, o.stage_sha256, context.root), evidence, "stage operation evidence"); + agreeStage(stageProviders(o, record.native_inputs.artifact, context.root), providers, "input providers"); + agreeStage(stageInputSnapshot(admitted.root, o.body), snapshot, "reader inputs"); + agreeStage(packing.blobs(o.repo, o.input.identity.commit, context.env, "stage"), source, "reader source"); + agreeStage(toolSnapshot(o), toolPins, "reader tools"); + agreeStage(stageOptions(value, true), o, "reader caller"); + agreeStage(c.readFile(path.join(root, "completion.json"), MAX_STAGE_BYTES), body, "retained S"); + for (const product of c.PRODUCTS) agreeStage(retainedPack(root, product, o.input), record.packs[product], "reader retained pair"); + return { root, record, subjects }; +} + function pinFile(file, expected, maximum = 1024 * 1024) { if (typeof expected !== "string" || !/^[0-9a-f]{64}$/.test(expected)) throw new Error("independent preparation digest required"); const bytes = c.readFile(file, maximum); @@ -119,12 +656,32 @@ function prepare(options) { packing.completeRecord(options.output, record); return record; } +function main(args) { + if (args.length !== 2) throw new Error("one operation and absolute options file required"); + if (args[0] === "--prepare") { + if (!path.isAbsolute(args[1])) throw new Error("absolute preparation options required"); + return prepare(JSON.parse(c.readFile(args[1], 1024 * 1024))); + } + if (!["--stage-prepublication", "--read-stage", "--validate-unsigned-stage"].includes(args[0])) throw new Error("unknown C1 stage operation"); + const producing = args[0] === "--stage-prepublication"; + const transport = inputs.inputFileOptions(args[1], ["input_file", "selected", "workflow_sha", "artifact", "repo", "workParent", "node", "npm", + ...(producing ? ["producer", "output"] : ["stage_sha256"])]); + let result; + if (producing) { + const record = stagePrepublication(transport.value), root = transport.value.output; + const stage_sha256 = c.digest(c.readFile(path.join(root, "completion.json"), MAX_STAGE_BYTES)); + const subjects = [{ file: path.join(root, "completion.json"), sha256: stage_sha256 }, + ...c.PRODUCTS.map(product => ({ file: path.join(root, record.packs[product].file), sha256: record.packs[product].sha256 }))]; + result = { root, record, subjects, stage_sha256 }; + } else result = args[0] === "--read-stage" ? readStage(transport.value) : validateUnsignedStage(transport.value); + transport.recheck(); + return result; +} // Public blob verification re-enters this module while the CLI is preparing. -module.exports = { prepare, packageFiles, ALLOWLIST, COMMON }; +module.exports = { prepare, packageFiles, ALLOWLIST, COMMON, + encodeStage, decodeStage, pairedPackageFiles, STAGE_ALLOWLIST, stagePrepublication, readStage, validateUnsignedStage, main }; if (require.main === module) { - try { - if (process.argv.length !== 4 || process.argv[2] !== "--prepare" || !path.isAbsolute(process.argv[3])) throw new Error("usage: stage-authoring-npm.js --prepare "); - process.stdout.write(c.encode(prepare(JSON.parse(c.readFile(process.argv[3], 1024 * 1024))))); - } catch (e) { process.stderr.write(`public npm preparation: ${e.message}\n`); process.exitCode = 1; } + try { process.stdout.write(c.encode(main(process.argv.slice(2)))); } + catch (e) { process.stderr.write(`public npm preparation: ${e.message}\n`); process.exitCode = 1; } } diff --git a/npm/agentplugins/scripts/stage-dual-authoring-npm.js b/npm/agentplugins/scripts/stage-dual-authoring-npm.js index cf679a0c..231b3a40 100644 --- a/npm/agentplugins/scripts/stage-dual-authoring-npm.js +++ b/npm/agentplugins/scripts/stage-dual-authoring-npm.js @@ -8,6 +8,7 @@ const cp = require("node:child_process"); const crypto = require("node:crypto"); const c = require("./dual-authoring-candidate"); const producer = require("./stage-dual-authoring-candidate"); +const { validateProductPackJSON } = require("./npm-public-contract"); const MODE = "release-cli-contract-v1"; const PACKAGES = { agentplugins: "universal-agent-plugins", "plugin-kit-ai": "plugin-kit-ai" }; const COMMON = ["lib/verifier.js", "scripts/dual-authoring-candidate.js", @@ -39,14 +40,18 @@ function npmContext(workParent) { } function blobs(repo, commit, env, closure = "private") { - if (!["private", "public"].includes(closure)) throw new Error("unknown fixed npm closure"); - const allowlist = closure === "private" ? ALLOWLIST : require("./stage-authoring-npm").ALLOWLIST; + if (!["private", "public", "stage"].includes(closure)) throw new Error("unknown fixed npm closure"); + const allowlist = closure === "private" ? ALLOWLIST : require("./stage-authoring-npm")[closure === "stage" ? "STAGE_ALLOWLIST" : "ALLOWLIST"]; c.safeDirectory(repo); if (run("/usr/bin/git", ["rev-parse", "HEAD"], env, repo).toString().trim() !== commit) { throw new Error("expected source must equal checkout HEAD"); } const result = {}; - for (const name of allowlist) { + // Check the newly executing helper at the same commit without extending the + // historical private/public preparation wrapper_blobs receipt inventories. + const packHelper = PREFIX + "scripts/npm-public-contract.js"; + const codecHelper = PREFIX + "lib/public-authoring-contract.js"; + for (const name of [...new Set([...allowlist, packHelper, ...(closure === "public" ? [codecHelper] : [])])]) { const entry = run("/usr/bin/git", ["ls-tree", "-z", commit, "--", name], env, repo).toString(); const match = /^(100644|100755) blob ([0-9a-f]{40})\t([^\0]+)\0$/.exec(entry); if (!match || match[3] !== name) throw new Error(`required regular Git blob missing: ${name}`); @@ -55,14 +60,32 @@ function blobs(repo, commit, env, closure = "private") { } // The code doing verification/generation must itself be this committed code. // A dirty caller may not manufacture an exact-source claim using old blobs. - for (const name of ["scripts/stage-dual-authoring-npm.js", "scripts/stage-dual-authoring-candidate.js", + for (const name of ["scripts/npm-public-contract.js", "scripts/stage-dual-authoring-npm.js", "scripts/stage-dual-authoring-candidate.js", "scripts/dual-authoring-candidate.js", ...(closure === "public" ? - ["scripts/stage-authoring-npm.js", "scripts/authoring-release.js", "lib/public-authoring.js"] : [])]) { + ["scripts/stage-authoring-npm.js", "scripts/authoring-release.js", "lib/public-authoring.js", "lib/public-authoring-contract.js"] : [])]) { if (!c.readFile(path.resolve(__dirname, "..", name)).equals(result[PREFIX + name].bytes)) { throw new Error(`executing stager differs from committed source: ${name}`); } } - return result; + if (closure === "public") { + const file = path.resolve(__dirname, "../lib/public-authoring-contract.js"); + if ((fs.lstatSync(file).mode & 0o777) !== (result[codecHelper].mode === "100755" ? 0o755 : 0o644)) { + throw new Error("executing codec mode differs from committed source"); + } + } + if (closure === "stage") { + // Every listed checkout byte AND every executing-tree byte must be F. Keep + // legacy preparation inventories/checks unchanged; stage has its own set. + const executing = path.resolve(__dirname, "../../.."); + for (const root of new Set([repo, executing])) for (const name of allowlist) { + const file = path.join(root, name), pin = result[name]; + if (!c.readFile(file).equals(pin.bytes) || + (fs.lstatSync(file).mode & 0o777) !== (pin.mode === "100755" ? 0o755 : 0o644)) { + throw new Error(`stage source differs from committed F: ${name}`); + } + } + } + return Object.fromEntries(allowlist.map(name => [name, result[name]])); } function packageFiles(product, source, manifestBytes, options) { @@ -108,14 +131,17 @@ function verifyPack(tarball, files, destination, env) { // One exact pack algorithm for both fixed closures; private defaults are intact. function packPackage(product, files, root, options, context) { + if (!c.PRODUCTS.includes(product)) throw new Error("unknown fixed npm product"); const result = JSON.parse(run(options.node, [options.npm, "pack", "--ignore-scripts", "--offline", "--json", "--pack-destination", options.output], context.env, root)); const filename = `${PACKAGES[product]}-${options.identity.versions[product]}.tgz`; - if (result.length !== 1 || result[0].filename !== filename) throw new Error("unexpected npm pack result"); + const record = validateProductPackJSON(result, product, options.identity.versions[product]); const tarball = path.join(options.output, filename), bytes = c.readFile(tarball); - verifyPack(tarball, files, path.join(context.root, product), context.env); const integrity = "sha512-" + crypto.createHash("sha512").update(bytes).digest("base64"); - if (result[0].integrity !== integrity) throw new Error("npm integrity differs from actual pack"); + const shasum = crypto.createHash("sha1").update(bytes).digest("hex"); + if (record.integrity !== integrity) throw new Error("npm integrity differs from actual pack"); + if (record.shasum !== shasum) throw new Error("npm shasum differs from actual pack"); + verifyPack(tarball, files, path.join(context.root, product), context.env); return { file: filename, ...c.metadata(bytes), integrity }; } diff --git a/npm/agentplugins/test/authoring-native-inputs.test.js b/npm/agentplugins/test/authoring-native-inputs.test.js new file mode 100644 index 00000000..f8bd8f50 --- /dev/null +++ b/npm/agentplugins/test/authoring-native-inputs.test.js @@ -0,0 +1,689 @@ +"use strict"; + +// All fixture positives prove structural consistency only, NOT authenticated +// provenance, signing, acquisition, eligibility, acceptance or authorization. +const test = require("node:test"); +const assert = require("node:assert/strict"); +const c = require("../scripts/dual-authoring-candidate"); +const contract = require("../scripts/authoring-native-inputs"); +const { encodeInputs, decodeInputs, encodeDescriptor, decodeDescriptor } = contract; +const products = ["agentplugins", "plugin-kit-ai"]; +const targets = ["darwin-amd64", "darwin-arm64", "linux-amd64", "linux-arm64", "windows-amd64", "windows-arm64"]; +const sha = n => n.toString(16).padStart(64, "0"); +const json = value => Buffer.from(JSON.stringify(value, null, 2) + "\n"); +const copy = value => structuredClone(value); + +function fixture(version = "0.1.99") { + const value = { schema: "authoring-native-inputs/v1", identity: { + repository: "777genius/universal-agent-plugins", commit: "a".repeat(40), engine_revision: "a".repeat(40), + versions: { agentplugins: version, "plugin-kit-ai": "2.0.0" } }, + authoring_mode: "release-cli-contract-v1", asset_scope: "six-platform-pair", + candidate_sha256: sha(30), pair_marker_sha256: sha(31), products: {}, + preparation: { sha256: sha(32), artifact: { run_id: 101, run_attempt: 2, artifact_id: 301, artifact_sha256: sha(33) } }, + producer: { workflow: ".github/workflows/agentplugins-release.yml", source: "a".repeat(40), run_id: 201, run_attempt: 3 } }; + products.forEach((product, pi) => { + const v = value.identity.versions[product]; + const p = value.products[product] = { tag: pi ? "v2.0.0" : `agentplugins-v${v}`, + manifest_sha256: sha(40 + pi), checksums_sha256: sha(50 + pi), assets: {} }; + targets.forEach((target, ti) => { + const extension = target.startsWith("windows-") ? ".exe" : ""; + const binary = { file: product + extension, sha256: sha(1 + pi * 6 + ti), size: 100 + ti }; + p.assets[target] = { file: `${product}_${v}_${target.replace("-", "_")}${pi ? ".tar.gz" : extension}`, + sha256: pi ? sha(60 + ti) : binary.sha256, size: pi ? 200 + ti : binary.size, binary }; + }); + }); + return value; +} + +function descriptor(input, inputBytes, product) { + return { schema: "dual-authoring-public-npm/v2", product, + npm_package: product === "agentplugins" ? "universal-agent-plugins" : "plugin-kit-ai", + identity: copy(input.identity), authoring_mode: "release-cli-contract-v1", asset_scope: "six-platform-pair", + candidate_sha256: input.candidate_sha256, release_manifest_sha256: input.products[product].manifest_sha256, + input_binding: { file: "native-inputs.json", sha256: c.digest(inputBytes) } }; +} +function rejectInput(mutate) { + const value = fixture(); + mutate(value); + assert.throws(() => encodeInputs(value)); + assert.throws(() => decodeInputs(json(value))); +} +function objectPaths(value, prefix = []) { + return [prefix, ...Object.entries(value).flatMap(([key, v]) => + v && typeof v === "object" ? objectPaths(v, [...prefix, key]) : [])]; +} +const at = (value, keys) => keys.reduce((v, key) => v[key], value); +function reversed(value) { + if (!value || typeof value !== "object") return value; + return Object.fromEntries(Object.entries(value).reverse().map(([k, v]) => [k, reversed(v)])); +} + +test("structural consistency only: I roundtrip and exact fixed inventories/names", () => { + const f = fixture(), body = encodeInputs(f), decoded = decodeInputs(body); + assert.deepEqual(decoded, f); + assert.notStrictEqual(decoded, f); + assert.deepEqual(Object.keys(decoded), ["schema", "identity", "authoring_mode", "asset_scope", "candidate_sha256", + "pair_marker_sha256", "products", "preparation", "producer"]); + assert.deepEqual(Object.keys(decoded.products), products); + for (const product of products) { + assert.deepEqual(Object.keys(decoded.products[product].assets), targets); + for (const target of targets) { + const a = decoded.products[product].assets[target]; + assert.equal(a.file, c.assetName(product, f.identity.versions[product], target)); + assert.equal(a.binary.file, c.executableName(product, target)); + } + } + assert.equal(new Set(products.flatMap(p => targets.map(t => decoded.products[p].assets[t].binary.sha256))).size, 12); + decoded.producer.run_id++; + assert.deepEqual(decodeInputs(body), f); +}); + +test("structural consistency only: constructors impose canonical ordering at every object", () => { + const f = fixture(), body = json(f); + assert.deepEqual(encodeInputs(reversed(f)), body); + assert.deepEqual(encodeInputs(decodeInputs(body)), body); + assert.throws(() => decodeInputs(json(reversed(f))), /noncanonical/); + for (const product of products) { + const d = descriptor(f, body, product); + assert.deepEqual(encodeDescriptor(reversed(d), body, product), json(d)); + assert.deepEqual(decodeDescriptor(json(d), body, product), d); + assert.deepEqual(encodeDescriptor(decodeDescriptor(json(d), body, product), body, product), json(d)); + assert.throws(() => decodeDescriptor(json(reversed(d)), body, product), /noncanonical/); + } +}); + +test("structural consistency only: missing and extra fields rejected at every I object", () => { + for (const path of objectPaths(fixture())) { + for (const key of Object.keys(at(fixture(), path))) { + rejectInput(f => { delete at(f, path)[key]; }); + } + rejectInput(f => { at(f, path).unexpected = true; }); + for (const replacement of [null, [], "object", 1, true]) { + if (path.length) rejectInput(f => { at(f, path.slice(0, -1))[path.at(-1)] = replacement; }); + else assert.throws(() => encodeInputs(replacement)); + } + } +}); + +test("structural consistency only: all descriptor inventories are closed on both interfaces", () => { + const f = fixture(), body = encodeInputs(f), d = descriptor(f, body, "agentplugins"); + const reject = value => { + assert.throws(() => encodeDescriptor(value, body, "agentplugins")); + assert.throws(() => decodeDescriptor(json(value), body, "agentplugins")); + }; + for (const path of objectPaths(d)) { + for (const key of Object.keys(at(d, path))) { + const changed = copy(d); delete at(changed, path)[key]; reject(changed); + } + const changed = copy(d); at(changed, path).unexpected = false; reject(changed); + } + for (const value of [null, [], "object", 1, true]) reject(value); +}); + +test("structural consistency only: no claim injection, even false, hidden or accessor claims", () => { + const claims = ["qualification", "signed_subject", "attested", "authenticated", "authenticated_provenance", + "eligibility", "release_eligible", "acceptance", "platform_acceptance", "assertions", "tarball_sha256", + "stage_sha256", "execution_sha256", "self_sha256", "artifact", "workflow_sha", "ref"]; + const f = fixture(), body = encodeInputs(f), d = descriptor(f, body, "plugin-kit-ai"); + for (const claim of claims) { + for (const value of [true, false, null]) { + rejectInput(x => { x[claim] = value; }); + const injected = { ...d, [claim]: value }; + assert.throws(() => encodeDescriptor(injected, body, "plugin-kit-ai")); + assert.throws(() => decodeDescriptor(json(injected), body, "plugin-kit-ai")); + } + } + for (const path of objectPaths(f)) { + let calls = 0; + const accessor = copy(f), node = at(accessor, path), key = Object.keys(node)[0]; + Object.defineProperty(node, key, { enumerable: true, get() { calls++; return f[key]; } }); + assert.throws(() => encodeInputs(accessor)); + assert.equal(calls, 0); + const hidden = copy(f); Object.defineProperty(at(hidden, path), "accepted", { value: true }); + assert.throws(() => encodeInputs(hidden)); + const symbolic = copy(f); at(symbolic, path)[Symbol("trusted")] = true; + assert.throws(() => encodeInputs(symbolic)); + const inherited = copy(f); Object.setPrototypeOf(at(inherited, path), { accepted: true }); + assert.throws(() => encodeInputs(inherited)); + } +}); + +test("structural consistency only: fixed source, versions, workflows, tags, mode and scope", () => { + for (const repository of ["777genius/plugin-kit-ai", "fork/universal-agent-plugins", null]) { + rejectInput(f => { f.identity.repository = repository; }); + } + for (const commit of ["0".repeat(40), "A".repeat(40), "a".repeat(39), "g".repeat(40), 123, + ...["\n", "\r", "\r\n"].map(suffix => "a".repeat(40) + suffix)]) { + rejectInput(f => { f.identity.commit = f.identity.engine_revision = f.producer.source = commit; }); + } + rejectInput(f => { f.identity.engine_revision = "b".repeat(40); }); + rejectInput(f => { f.producer.source = "b".repeat(40); }); + for (const version of ["2.0.0", "0.1.1-beta", "01.1.1", "0.1.1+build", "v0.1.1", 1, null, + "0.1.99\n", "0.1.99\r", "0.1.99\r\n"]) { + rejectInput(f => { f.identity.versions.agentplugins = version; }); + } + rejectInput(f => { f.identity.versions["plugin-kit-ai"] = "2.0.1"; }); + for (const workflow of [".github/workflows/authoring-frozen-native.yml", ".github/workflows/agentplugins-npm-publish.yml", null]) { + rejectInput(f => { f.producer.workflow = workflow; }); + } + rejectInput(f => { f.products.agentplugins.tag = "v0.1.99"; }); + rejectInput(f => { f.products["plugin-kit-ai"].tag = "v2.0.1"; }); + rejectInput(f => { f.authoring_mode = "vertical-slice-v1"; }); + rejectInput(f => { f.asset_scope = "host-pair"; }); + rejectInput(f => { f.schema = "authoring-native-inputs/v2"; }); +}); + +test("structural consistency only: strict hash types and nonzero lowercase digests everywhere", () => { + const f = fixture(); + for (const path of objectPaths(f)) { + for (const key of Object.keys(at(f, path)).filter(k => k.endsWith("sha256"))) { + for (const value of [null, true, 123, "0".repeat(64), "A".repeat(64), "g".repeat(64), "1".repeat(63), "1".repeat(65), + ...["\n", "\r", "\r\n"].map(suffix => "1".repeat(64) + suffix)]) { + rejectInput(x => { at(x, path)[key] = value; }); + } + } + } +}); + +test("structural consistency only: positive safe IDs, attempt bounds and native size bounds", () => { + for (const path of [["preparation", "artifact"], ["producer"]]) { + for (const key of Object.keys(at(fixture(), path)).filter(k => k === "run_id" || k === "run_attempt" || k === "artifact_id")) { + for (const value of [0, -1, 1.5, "1", true, null, Number.MAX_SAFE_INTEGER + 1, NaN, Infinity]) { + rejectInput(f => { at(f, path)[key] = value; }); + } + for (const value of [1, key === "run_attempt" ? 1000 : Number.MAX_SAFE_INTEGER]) { + const f = fixture(); at(f, path)[key] = value; assert.deepEqual(decodeInputs(encodeInputs(f)), f); + } + } + rejectInput(f => { at(f, path).run_attempt = 1001; }); + } + for (const product of products) for (const target of targets) { + for (const inner of [false, true]) { + for (const size of [0, -1, 1.5, "1", null, true, contract.MAX_NATIVE_BYTES + 1]) { + rejectInput(f => { const a = f.products[product].assets[target]; (inner ? a.binary : a).size = size; }); + } + } + for (const size of [1, contract.MAX_NATIVE_BYTES]) { + const f = fixture(), a = f.products[product].assets[target]; a.size = a.binary.size = size; + assert.deepEqual(decodeInputs(encodeInputs(f)), f); + } + } +}); + +test("structural consistency only: filenames, raw pins and all twelve binary identities", () => { + for (const product of products) for (const target of targets) { + for (const file of ["../asset", "asset.exe", "plugin-kit-ai", null]) { + rejectInput(f => { f.products[product].assets[target].file = file; }); + } + rejectInput(f => { f.products[product].assets[target].binary.file = "../binary"; }); + rejectInput(f => { + const a = f.products[product].assets[target]; + const other = product === "agentplugins" ? "plugin-kit-ai" : "agentplugins"; + a.binary.sha256 = f.products[other].assets[target].binary.sha256; + if (product === "agentplugins") a.sha256 = a.binary.sha256; + }); + if (product === "agentplugins") { + rejectInput(f => { f.products[product].assets[target].sha256 = sha(99); }); + rejectInput(f => { f.products[product].assets[target].size++; }); + } + } +}); + +test("structural consistency only: v2 requires exact I bytes and selected product on both interfaces", () => { + const f = fixture(), body = encodeInputs(f); + for (const product of products) { + const d = descriptor(f, body, product), encoded = encodeDescriptor(d, body, product); + for (const selection of [undefined, null, "other", products.find(p => p !== product)]) { + assert.throws(() => encodeDescriptor(d, body, selection)); + assert.throws(() => decodeDescriptor(encoded, body, selection)); + } + const changes = [x => x.identity.commit = x.identity.engine_revision = "b".repeat(40), + x => x.identity.versions.agentplugins = "0.1.98", x => x.candidate_sha256 = sha(99), + x => x.release_manifest_sha256 = f.products[products.find(p => p !== product)].manifest_sha256, + x => x.input_binding.sha256 = sha(99), x => x.input_binding.file = "../native-inputs.json", + x => x.input_binding = null, x => x.npm_package = "wrong", x => x.authoring_mode = "vertical-slice-v1", + x => x.asset_scope = "host-pair", x => x.schema = "dual-authoring-public-npm/v1"]; + for (const mutate of changes) { + const changed = copy(d); mutate(changed); + assert.throws(() => encodeDescriptor(changed, body, product)); + assert.throws(() => decodeDescriptor(json(changed), body, product)); + } + for (const mutate of [x => x.preparation.artifact.artifact_id++, x => x.producer.run_attempt++, + x => x.products["plugin-kit-ai"].checksums_sha256 = sha(99), x => x.pair_marker_sha256 = sha(99)]) { + const changed = copy(f); mutate(changed); const changedBytes = encodeInputs(changed); + assert.throws(() => encodeDescriptor(d, changedBytes, product)); + assert.throws(() => decodeDescriptor(encoded, changedBytes, product)); + } + const noncanonical = json(reversed(f)), misleading = copy(d); + misleading.input_binding.sha256 = c.digest(noncanonical); + assert.throws(() => encodeDescriptor(misleading, noncanonical, product)); + assert.throws(() => decodeDescriptor(json(misleading), noncanonical, product)); + } +}); + +test("structural consistency only: bounded canonical UTF-8 bytes, duplicates and spellings", () => { + const f = fixture(), input = encodeInputs(f), d = descriptor(f, input, "agentplugins"); + for (const [body, decode, maximum] of [[input, decodeInputs, contract.MAX_INPUT_BYTES], + [json(d), b => decodeDescriptor(b, input, "agentplugins"), contract.MAX_DESCRIPTOR_BYTES]]) { + const text = body.toString(); + for (const bad of [body.toString(), new Uint8Array(body), null, {}, Buffer.alloc(0), Buffer.alloc(maximum + 1), + Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), body]), Buffer.concat([body, Buffer.from([0xff])]), + Buffer.from(text.trim()), Buffer.from(text + "\n"), Buffer.from(text + "{}"), + Buffer.from(text.replace('"schema":', '"schema": null, "schema":')), + Buffer.from(text.replace('"schema"', '"sch\\u0065ma"')), + Buffer.from(text.replace('"repository":', '"repository": null, "repository":')), + Buffer.from(text.replaceAll("\n", "\r\n")), Buffer.from('{"x":'.repeat(7) + '0' + '}'.repeat(7))]) { + assert.throws(() => decode(bad)); + } + } + assert.throws(() => decodeInputs(Buffer.from(input.toString().replace('"run_id": 101', '"run_id": 1.01e2')))); + assert.throws(() => decodeInputs(Buffer.from(input.toString().replace('"run_id": 101', '"run_id": 101.0')))); +}); + +test("structural consistency only: constructor and decoder byte limits include exact boundaries", () => { + const base = encodeInputs(fixture("1.0.0")).length; + const n = Math.floor((contract.MAX_INPUT_BYTES - base) / 8); + const near = fixture("1" + "0".repeat(n) + ".0.0"); + // Version appears in identity, tag and six outer filenames. Adjust the run ID + // digit count to exercise exactly 1 MiB without introducing unknown fields. + const remaining = contract.MAX_INPUT_BYTES - encodeInputs(near).length; + near.preparation.artifact.run_id = Number("1" + "0".repeat(2 + remaining)); + const limitBytes = encodeInputs(near); + assert.equal(limitBytes.length, contract.MAX_INPUT_BYTES); + assert.deepEqual(decodeInputs(limitBytes), near); + assert.throws(() => encodeInputs(fixture("1" + "0".repeat(n + 1) + ".0.0"))); + assert.throws(() => encodeInputs(fixture("1".repeat(contract.MAX_INPUT_BYTES + 1)))); + + const f = fixture("1.0.0"), body = encodeInputs(f); + const overhead = json(descriptor(f, body, "agentplugins")).length; + const large = fixture("1" + "0".repeat(contract.MAX_DESCRIPTOR_BYTES - overhead) + ".0.0"); + const largeBytes = encodeInputs(large), d = descriptor(large, largeBytes, "agentplugins"); + const exact = encodeDescriptor(d, largeBytes, "agentplugins"); + assert.equal(exact.length, contract.MAX_DESCRIPTOR_BYTES); + assert.deepEqual(decodeDescriptor(exact, largeBytes, "agentplugins"), d); + const over = fixture(large.identity.versions.agentplugins.replace("1", "10")), overBytes = encodeInputs(over); + assert.throws(() => encodeDescriptor(descriptor(over, overBytes, "agentplugins"), overBytes, "agentplugins")); +}); + +test("structural consistency only: four codecs stay pure beside separately named custody operations", () => { + assert.deepEqual(Object.entries(contract).filter(([, v]) => typeof v === "function").map(([k]) => k), + ["encodeInputs", "decodeInputs", "encodeDescriptor", "decodeDescriptor", "produceInputs", "readInputs", "inputSubjects", "produceInputsFromPreparation", "inputFileOptions", "main"]); + assert.ok(Object.isFrozen(contract)); + const f = fixture(), snapshot = json(f), input = encodeInputs(f); + const d = descriptor(f, input, "agentplugins"), before = json(d); + encodeDescriptor(d, input, "agentplugins"); decodeInputs(input); + assert.deepEqual(json(f), snapshot); assert.deepEqual(json(d), before); assert.deepEqual(input, snapshot); +}); + + +// C1 tests are orchestration unit evidence only. Provider acquisition and +// signatures are stubbed module operations; these fixtures never authenticate. +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const cp = require("node:child_process"); +const promotion = require("../scripts/authoring-promotion"); +const release = require("../scripts/authoring-release"); +const qualification = require("../scripts/authoring-native-qualification"); + +function c1Fixture(t) { + t.mock.method(cp, "spawnSync", () => assert.fail("C1 tests must never launch a subprocess")); + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), "provenance-")); + const root = path.join(sandbox, "prepared"), provenance = path.join(sandbox, "provenance"), scratch = path.join(sandbox, "scratch"); + for (const dir of [root, provenance, scratch]) fs.mkdirSync(dir); + const input = fixture(), inner = new Map(); + const write = (rel, bytes) => { + const file = path.join(root, rel); fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, bytes); return c.metadata(bytes); + }; + const manifest = { schema: c.SCHEMA, status: "CANDIDATE", identity: input.identity, asset_scope: input.asset_scope, + build: { method: "controlled-git-archive-go-build/v1", go_version: "go1.25.13", go_sha256: sha(80), + source_archive_sha256: sha(81), authoring_mode: input.authoring_mode }, products: {}, release_eligible: false }; + for (const product of products) { + for (const target of targets) { + const a = input.products[product].assets[target]; + const binary = Buffer.from("NONEXECUTABLE UNIT FIXTURE " + product + "/" + target); + const outer = product === "agentplugins" ? binary : Buffer.from("OPAQUE OUTER FIXTURE " + target); + a.binary = { file: a.binary.file, ...c.metadata(binary) }; + Object.assign(a, write(product + "/" + a.file, outer)); + inner.set(c.digest(outer), { file: a.binary.file, binary }); + } + manifest.products[product] = { version: input.identity.versions[product], assets: input.products[product].assets }; + } + // Stub the existing unpack operation only. No ZIP/tar internals are tested. + t.mock.method(c, "unpack", (bytes, name) => { + const row = inner.get(c.digest(bytes)); assert.ok(row); assert.equal(row.file, name); return row.binary; + }); + input.candidate_sha256 = write("candidate/candidate.json", c.encode(manifest)).sha256; + const pins = { identity: input.identity, candidate_sha256: input.candidate_sha256, pair_marker_sha256: "", products: {} }; + for (const product of products) { + const p = input.products[product]; + const m = { schema_version: 3, status: "CANDIDATE", product, repository: c.REPOSITORY, tag: p.tag, + version: input.identity.versions[product], commit: input.identity.commit, engine_revision: input.identity.commit, + versions: input.identity.versions, candidate_sha256: input.candidate_sha256, authoring_mode: input.authoring_mode, + asset_scope: input.asset_scope, assets: p.assets, release_eligible: false, platform_acceptance: false, attested: false }; + p.manifest_sha256 = write(product + "/release-manifest.json", c.encode(m)).sha256; + const checks = Buffer.from([...Object.values(p.assets).map(a => a.sha256 + " " + a.file), + p.manifest_sha256 + " release-manifest.json"].join("\n") + "\n"); + p.checksums_sha256 = write(product + "/checksums.txt", checks).sha256; + pins.products[product] = { manifest_sha256: p.manifest_sha256, checksums_sha256: p.checksums_sha256 }; + } + const marker = { schema: "authoring-release-pair/v1", status: "CANDIDATE", identity: input.identity, + candidate_sha256: input.candidate_sha256, authoring_mode: input.authoring_mode, asset_scope: input.asset_scope, + products: pins.products, release_eligible: false, platform_acceptance: false, attested: false }; + pins.pair_marker_sha256 = input.pair_marker_sha256 = write("pair-prepared.json", c.encode(marker)).sha256; + const invocation = { repository: c.REPOSITORY, workflow: contract.WORKFLOW, source: input.identity.commit, + workflow_sha: input.identity.commit, run_id: input.preparation.artifact.run_id, run_attempt: input.preparation.artifact.run_attempt }; + input.preparation.sha256 = qualification.writePreparation(root, pins, invocation).sha256; + write("candidate-identity.json", c.encode({ identity: input.identity, status: "CANDIDATE", + manifest_sha256: input.candidate_sha256, output: "/historical/not-opened", local_build_evidence: "/historical/not-opened-either", + release_eligible: false, platform_acceptance: false, attested: false })); + const original = release.verifyProjectedPair(root, pins).subjects; + const files = [...original.map(s => path.relative(root, s.file)), "preparation-run.json", "candidate-identity.json"]; + for (const file of files) { + fs.mkdirSync(path.dirname(path.join(provenance, file)), { recursive: true }); + fs.copyFileSync(path.join(root, file), path.join(provenance, file)); + } + const body = encodeInputs(input); + fs.writeFileSync(path.join(provenance, contract.INPUT_FILE), body); + const selected = { tag: input.products.agentplugins.tag, ref: "refs/tags/" + input.products.agentplugins.tag, + source: input.identity.commit, versions: copy(input.identity.versions) }; + const options = { input: body, selected, workflow_sha: input.identity.commit, scratch }; + const artifact = { run_id: input.producer.run_id, run_attempt: input.producer.run_attempt, + artifact_id: 501, artifact_sha256: sha(90) }; + const reading = { ...options, artifact }; + const calls = []; + t.mock.method(promotion, "checkPreparationRef", () => ({fixture_only: "completed original ref"})); + t.mock.method(promotion, "checkInputTags", (bytes, cwd) => { + calls.push("tags"); assert.deepEqual(bytes, body); assert.equal(cwd, scratch); + }); + t.mock.method(promotion, "inspectArtifact", (pin, workflow, source, cwd) => { + calls.push("inspect"); assert.equal(workflow, contract.WORKFLOW); assert.equal(source, input.identity.commit); + assert.equal(cwd, scratch); assert.ok([artifact.artifact_id, input.preparation.artifact.artifact_id].includes(pin.artifact_id)); + return { fixture_only: copy(pin) }; + }); + t.mock.method(promotion, "acquireInputPreparation", (bytes, cwd) => { + calls.push("preparation"); assert.equal(cwd, scratch); return promotion.readInputPreparation(root, bytes); + }); + t.mock.method(promotion, "acquireArtifact", (pin, workflow, source, cwd) => { + calls.push("acquire"); assert.deepEqual(pin, artifact); assert.equal(workflow, contract.WORKFLOW); + assert.equal(source, input.identity.commit); return path.join(cwd, "artifact-501.zip"); + }); + t.mock.method(promotion, "extractArtifact", (file, pin, kind, closure, output, cwd) => { + calls.push("extract"); assert.equal(file, path.join(cwd, "artifact-501.zip")); assert.deepEqual(pin, artifact); + assert.equal(kind, "input-provenance"); assert.equal(output, path.join(cwd, "frozen")); + assert.deepEqual([...closure].sort(), [...files, contract.INPUT_FILE].sort()); assert.equal(closure.length, 21); + // A simulated future interface, NOT the current checked reader. + return provenance; + }); + t.mock.method(promotion, "verifySubject", (file, expected, cwd) => { + calls.push({ file, expected: copy(expected) }); assert.equal(cwd, scratch); + assert.equal(c.digest(fs.readFileSync(file)), expected.sha256); + assert.equal(expected.source, input.identity.commit); assert.equal(expected.workflow_sha, input.identity.commit); + assert.equal(expected.ref, selected.ref); assert.equal(expected.run_id, input.producer.run_id); + assert.equal(expected.run_attempt, input.producer.run_attempt); + }); + return { root, provenance, scratch, input, body, options, reading, original, files, pins, calls }; +} + +test("C1 provenance producer and reader agree with honest simulated custody, never authentic admission", t => { + const f = c1Fixture(t), produced = contract.produceInputs(f.options); + assert.deepEqual(produced.input, f.input); assert.equal(produced.subjects.length, 19); + assert.equal(f.calls.filter(x => typeof x === "object").length, 0, "producer does not verify or sign unsigned I"); + assert.deepEqual(fs.readFileSync(path.join(f.root, contract.INPUT_FILE)), f.body); + const read = contract.readInputs(f.reading); + assert.deepEqual(read.input, produced.input); + const multiset = list => list.map(s => ({ name: path.basename(s.file), digest: { sha256: s.sha256 } })); + assert.deepEqual(multiset(read.subjects), multiset(produced.subjects)); + const signatures = f.calls.filter(x => typeof x === "object"); + assert.equal(signatures.length, 19); + for (const call of signatures) assert.deepEqual(call.expected.subjects, multiset(read.subjects)); + for (const name of ["release-manifest.json", "checksums.txt"]) { + const rows = signatures[0].expected.subjects.filter(s => s.name === name); + assert.equal(rows.length, 2); assert.notEqual(rows[0].digest.sha256, rows[1].digest.sha256); + } + assert.ok(!signatures[0].expected.subjects.some(s => s.name === "authoring-promotion.json")); + assert.deepEqual(Object.keys(read), ["root", "input", "subjects"]); +}); + +test("C1 provenance rejects malformed closed options and independent identity disagreements before effects", t => { + const f = c1Fixture(t); + const mutations = [o => o.input = null, o => o.input = Buffer.from('{}\n'), o => o.input = Buffer.concat([o.input, Buffer.from('\n')]), + o => o.verifier = () => true, o => o.success = true, o => o.selected.source = "b".repeat(40), + o => o.selected.ref = "refs/heads/main", o => o.selected.tag = "v2.0.0", o => o.workflow_sha = "b".repeat(40), + o => o.selected.versions.agentplugins = "0.1.98", o => o.scratch = "relative"]; + for (const reading of [false, true]) for (const mutate of mutations) { + const o = { ...f.options, input: Buffer.from(f.body), selected: copy(f.options.selected), ...(reading ? { artifact: copy(f.reading.artifact) } : {}) }; + mutate(o); assert.throws(() => (reading ? contract.readInputs : contract.produceInputs)(o)); + } + for (const mutate of [o => o.artifact.run_attempt++, o => o.artifact.run_id++, o => o.artifact.artifact_id = 0, + o => o.artifact.artifact_sha256 = "0".repeat(64), o => o.artifact.artifact_id = f.input.preparation.artifact.artifact_id, + o => o.artifact.claim = true]) { + const o = { ...f.reading, artifact: copy(f.reading.artifact) }; mutate(o); assert.throws(() => contract.readInputs(o)); + } + for (const mutate of [i => i.identity.repository = "fork/repo", i => i.producer.workflow = ".github/workflows/other.yml", + i => i.producer.source = "b".repeat(40), i => i.products["plugin-kit-ai"].tag = "v2.0.1", + i => delete i.products.agentplugins.assets["linux-amd64"], i => i.qualification = null, + i => i.preparation.artifact.run_attempt = 1001, i => i.authoring_mode = "wrong", i => i.asset_scope = "wrong"]) { + const input = copy(f.input); mutate(input); + assert.throws(() => contract.produceInputs({ ...f.options, input: json(input) })); + assert.throws(() => contract.readInputs({ ...f.reading, input: json(input) })); + } + assert.deepEqual(f.calls, []); assert.equal(fs.existsSync(path.join(f.root, contract.INPUT_FILE)), false); + assert.deepEqual(fs.readdirSync(f.scratch), []); +}); + +test("C1 provenance checked-artifact dependency rejects with no fallback or signature calls", t => { + const f = c1Fixture(t); + t.mock.method(promotion, "extractArtifact", (_file, _pin, kind, files) => { + assert.equal(kind, "input-provenance"); assert.equal(files.length, 21); + throw Error("simulated current checked interface rejects unsupported kind"); + }); + assert.throws(() => contract.readInputs(f.reading), /unsupported kind/); + assert.deepEqual(f.calls, ["tags", "inspect", "acquire"]); +}); + +for (const kind of ["receipt", "attempt", "metadata", "outer", "inner", "I"]) { + test("C1 provenance rejects " + kind + " disagreement before signatures or I output", t => { + const f = c1Fixture(t); + if (kind === "receipt") { + f.input.preparation.sha256 = sha(97); f.options.input = f.reading.input = encodeInputs(f.input); + // Identity-only stub: receipt validation itself stays the real reader. + t.mock.method(promotion, "checkInputTags", () => {}); + } else if (kind === "attempt") { + // Keep the existing read-only receipt intact; select a disagreeing attempt. + f.input.preparation.artifact.run_attempt++; f.options.input = encodeInputs(f.input); + t.mock.method(promotion, "checkInputTags", () => {}); + } else if (kind === "metadata") { + const file = path.join(f.root, "candidate-identity.json"), metadata = JSON.parse(fs.readFileSync(file)); + metadata.attested = true; fs.writeFileSync(file, c.encode(metadata)); + } else if (kind === "outer") fs.appendFileSync(f.original[3].file, "CHANGED"); + else if (kind === "inner") { + f.input.products["plugin-kit-ai"].assets["linux-amd64"].binary.sha256 = sha(99); + f.options.input = encodeInputs(f.input); t.mock.method(promotion, "checkInputTags", () => {}); + } else fs.appendFileSync(path.join(f.provenance, contract.INPUT_FILE), "\n"); + if (kind === "I") assert.throws(() => contract.readInputs(f.reading), /I bytes/); + else assert.throws(() => contract.produceInputs(f.options)); + assert.equal(fs.existsSync(path.join(f.root, contract.INPUT_FILE)), false); + assert.equal(f.calls.filter(x => typeof x === "object").length, 0); + }); +} + +for (const operation of ["produce", "read"]) for (const changed of ["caller", "source", "subject", "metadata", "provider", "tag"]) { + test("C1 provenance " + operation + " rejects late " + changed + " changes", t => { + const f = c1Fixture(t); let tags = 0, inspections = 0; + const selectedOptions = operation === "read" ? f.reading : f.options; + t.mock.method(promotion, "checkInputTags", () => { + if (++tags !== 2) return; + if (changed === "caller") selectedOptions.input[1] ^= 1; + if (changed === "source") selectedOptions.workflow_sha = "b".repeat(40); + if (changed === "subject") fs.appendFileSync(operation === "read" ? path.join(f.provenance, f.files[0]) : f.original[0].file, "changed"); + if (changed === "metadata") { + const file = path.join(operation === "read" ? f.provenance : f.root, "candidate-identity.json"); + const value = JSON.parse(fs.readFileSync(file)); value.output = "/different-history"; fs.writeFileSync(file, c.encode(value)); + } + if (changed === "tag") throw Error("simulated moved release tag"); + }); + t.mock.method(promotion, "inspectArtifact", pin => ({ pin: copy(pin), observation: changed === "provider" && ++inspections > (operation === "read" ? 2 : 1) ? "changed" : "same" })); + assert.throws(() => (operation === "read" ? contract.readInputs : contract.produceInputs)(selectedOptions)); + if (operation === "produce") assert.equal(fs.existsSync(path.join(f.root, contract.INPUT_FILE)), false); + }); +} + +test("C1 provenance unsigned verifier rejection never returns reader admission", t => { + const f = c1Fixture(t); let calls = 0; + t.mock.method(promotion, "verifySubject", () => { calls++; throw Error("unsigned fixture rejected"); }); + assert.throws(() => contract.readInputs(f.reading), /unsigned fixture rejected/); assert.equal(calls, 1); +}); + +test("C1 provenance rechecks earlier subjects and I after the last signature operation", t => { + const f = c1Fixture(t); let calls = 0; + t.mock.method(promotion, "verifySubject", () => { + if (++calls === 19) fs.appendFileSync(path.join(f.provenance, f.files[0]), "changed after first verification"); + }); + assert.throws(() => contract.readInputs(f.reading)); assert.equal(calls, 19); +}); + +test("C1 provenance reader rechecks exact I bytes after all nineteen simulated verifications", t => { + const f = c1Fixture(t); let calls = 0; + t.mock.method(promotion, "verifySubject", () => { + if (++calls === 19) fs.appendFileSync(path.join(f.provenance, contract.INPUT_FILE), "\n"); + }); + assert.throws(() => contract.readInputs(f.reading), /retained I bytes/); assert.equal(calls, 19); +}); + +test("C1 provenance producer checks unchanged preparation after writing I without returning completion", t => { + const f = c1Fixture(t), write = fs.writeFileSync; + t.mock.method(fs, "writeFileSync", (file, bytes, options) => { + write(file, bytes, options); + if (file === path.join(f.root, contract.INPUT_FILE)) fs.appendFileSync(f.original[0].file, "late fixture change"); + }); + assert.throws(() => contract.produceInputs(f.options)); + // A failed local candidate is retained; it is never an uploaded completed run. + assert.equal(f.calls.filter(x => typeof x === "object").length, 0); +}); + +test("C1 provenance reader compares preparation metadata from independently acquired bytes", t => { + const f = c1Fixture(t), file = path.join(f.provenance, "candidate-identity.json"); + const metadata = JSON.parse(fs.readFileSync(file)); metadata.output = "/different-historical-location"; + fs.writeFileSync(file, c.encode(metadata)); + assert.throws(() => contract.readInputs(f.reading), /original preparation custody/); + assert.equal(f.calls.filter(x => typeof x === "object").length, 0); +}); + +test("C1 provenance enumeration rejects missing original row and changed I without authenticating", t => { + const f = c1Fixture(t), verify = release.verifyProjectedPair; + t.mock.method(release, "verifyProjectedPair", (root, pins) => { + const result = verify(root, pins); return { ...result, subjects: result.subjects.slice(1) }; + }); + assert.throws(() => contract.inputSubjects(f.provenance, f.body), /original subject count/); + assert.deepEqual(f.calls, []); +}); + +test("C1 provenance producer completion collision preserves the existing file", t => { + const f = c1Fixture(t), file = path.join(f.root, contract.INPUT_FILE), previous = Buffer.from("existing output"); + fs.writeFileSync(file, previous); + assert.throws(() => contract.produceInputs(f.options), /EEXIST/); + assert.deepEqual(fs.readFileSync(file), previous); +}); + +function workflowPreparation(t) { + const f = c1Fixture(t), packing = require('../scripts/stage-dual-authoring-npm'); + const originalRead = c.readFile; + const tools = new Set([process.execPath, '/usr/bin/git', '/usr/bin/gh', fs.realpathSync('/usr/bin/python3')]); + t.mock.method(c, 'readFile', (file, max) => tools.has(file) ? Buffer.from(`tool fixture ${file}`) : originalRead(file, max)); + const repo = path.join(path.dirname(f.scratch), 'repo'); fs.mkdirSync(repo); + const derivation = path.join(path.dirname(f.scratch), 'derivation'); fs.mkdirSync(derivation); + for (const name of f.files) { + fs.mkdirSync(path.dirname(path.join(derivation, name)), {recursive: true}); + fs.copyFileSync(path.join(f.root, name), path.join(derivation, name)); + } + const options = {selected: f.options.selected, workflow_sha: f.options.workflow_sha, + preparation: f.input.preparation.artifact, repo, scratch: f.scratch}; + t.mock.method(packing, 'blobs', () => ({fixture_only: 'source pins'})); + t.mock.method(promotion, 'inspectInputCaller', () => copy(f.input.producer)); + t.mock.method(promotion, 'checkPreparationRef', () => ({fixture_only: 'original tag invocation'})); + t.mock.method(promotion, 'acquireArtifact', (pin, workflow, source, work) => { + assert.deepEqual(pin, options.preparation); assert.equal(workflow, contract.WORKFLOW); + assert.equal(source, options.selected.source); return path.join(work, `artifact-${pin.artifact_id}.zip`); + }); + t.mock.method(promotion, 'extractArtifact', (archive, pin, kind, files) => { + assert.equal(kind, 'preparation'); assert.deepEqual([...files].sort(), [...f.files].sort()); + assert.equal(path.basename(archive), `artifact-${pin.artifact_id}.zip`); return derivation; + }); + return {...f, derivation, producerOptions: options}; +} +test('C1 workflow derives I from checked original preparation and revalidates without Q', t => { + const f = workflowPreparation(t), file = path.join(f.scratch, 'options.json'); + fs.writeFileSync(file, c.encode(f.producerOptions)); + const result = contract.main(['--produce-inputs', file]); + assert.deepEqual(result.input, f.input); assert.equal(result.subjects.length, 19); + assert.deepEqual(fs.readFileSync(path.join(result.root, 'native-inputs.json')), f.body); + assert.deepEqual(Object.keys(result).sort(), ['input', 'root', 'subjects']); + assert.equal(f.calls.filter(x => typeof x === 'object').length, 0); +}); +for (const name of ['candidate/candidate.json', 'candidate-identity.json', 'pair-prepared.json', 'preparation-run.json', + 'agentplugins/release-manifest.json', 'plugin-kit-ai/checksums.txt']) { + test(`C1 workflow contradictory original ${name} rejects derivation`, t => { + const f = workflowPreparation(t); + // Preparation receipts are immutable; substitute contradictory comparison + // bytes at the existing read seam, never overwrite a read-only receipt. + const read = c.readFile; + t.mock.method(c, 'readFile', (file, max) => { + const body = read(file, max); + return file === path.join(f.derivation, name) ? Buffer.concat([body, Buffer.from('\n')]) : body; + }); + assert.throws(() => contract.produceInputsFromPreparation(f.producerOptions)); + assert.equal(fs.existsSync(path.join(f.root, 'native-inputs.json')), false); + }); +} +test('C1 workflow file transport passes exact Buffer to completed I authentication', t => { + const f = c1Fixture(t), input_file = path.join(f.scratch, 'comparison.json'), file = path.join(f.scratch, 'options.json'); + fs.writeFileSync(input_file, f.body); + const {input, ...options} = f.reading; + fs.writeFileSync(file, c.encode({...options, input_file})); + const result = contract.main(['--read-inputs', file]); + assert.deepEqual(result.input, f.input); + assert.equal(f.calls.filter(x => typeof x === 'object').length, 19); +}); +for (const defect of ['object', 'Buffer JSON', 'unknown field', 'relative file', 'duplicate key', 'unknown flag', 'extra flag']) { + test(`C1 workflow ${defect} transport fails before provider effects`, t => { + const f = c1Fixture(t), file = path.join(f.scratch, 'options.json'); + const {input, ...options} = f.reading; + const value = {...options, input_file: path.join(f.scratch, 'comparison.json')}; + fs.writeFileSync(value.input_file, f.body); + if (defect === 'object') value.input_file = f.input; + if (defect === 'Buffer JSON') {delete value.input_file; value.input = f.body;} + if (defect === 'unknown field') value.authenticated = true; + if (defect === 'relative file') value.input_file = 'comparison.json'; + let bytes = c.encode(value); + if (defect === 'duplicate key') bytes = Buffer.from(bytes.toString().replace('{', '{"input_file":"ignored",')); + fs.writeFileSync(file, bytes); + const args = [defect === 'unknown flag' ? '--produce' : '--read-inputs', file]; + if (defect === 'extra flag') args.push('--read-inputs'); + assert.throws(() => contract.main(args)); assert.equal(f.calls.length, 0); + }); +} +test('C1 workflow comparison file change during authentication rejects CLI success', t => { + const f = c1Fixture(t), input_file = path.join(f.scratch, 'comparison.json'), file = path.join(f.scratch, 'options.json'); + fs.writeFileSync(input_file, f.body); const {input, ...options} = f.reading; + fs.writeFileSync(file, c.encode({...options, input_file})); + t.mock.method(promotion, 'verifySubject', () => fs.writeFileSync(input_file, Buffer.from('changed'))); + assert.throws(() => contract.main(['--read-inputs', file]), /CLI comparison/); +}); + +for (const defect of ['source', 'caller', 'tools', 'original ref', 'reacquired original']) { + test(`C1 workflow derivation rejects changed ${defect} before a usable result`, t => { + const f = workflowPreparation(t); let checks = 0; + if (defect === 'source') t.mock.method(require('../scripts/stage-dual-authoring-npm'), 'blobs', () => ({fixture_only: ++checks})); + if (defect === 'caller') t.mock.method(promotion, 'inspectInputCaller', () => ({...f.input.producer, run_attempt: f.input.producer.run_attempt + checks++})); + if (defect === 'original ref') t.mock.method(promotion, 'checkPreparationRef', () => {throw Error('foreign original tag ref');}); + if (defect === 'tools') { + const original = c.readFile; + t.mock.method(c, 'readFile', (file, max) => file === '/usr/bin/gh' ? Buffer.from(`changed tool ${checks++}`) : original(file,max)); + } + if (defect === 'reacquired original') t.mock.method(promotion, 'acquireInputPreparation', () => {throw Error('original preparation contradiction');}); + assert.throws(() => contract.produceInputsFromPreparation(f.producerOptions)); + assert.equal(f.calls.filter(v => typeof v === 'object').length, 0, 'no signing or authentication result from producer fixture'); + }); +} diff --git a/npm/agentplugins/test/authoring-native-qualification.test.js b/npm/agentplugins/test/authoring-native-qualification.test.js index e09507e2..9777fdc5 100644 --- a/npm/agentplugins/test/authoring-native-qualification.test.js +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -61,11 +61,11 @@ function internal(observation = false, fastTimeout = false) { if (observation) source = source.replace(' e["scans.json"] = observationGate(); //', ' e["scans.json"] = JSON.parse(fs.readFileSync(path.join(install.env.TMPDIR, "fixture-scans.json"))); // Test-only captured child fixtures.'); // Only this isolated test module pins the tiny synthetic tar. Unmodified // production readers must reject it; no runtime pin parameter is introduced. - if (observation) source = source.replace("2b3d176db752433b904a4b42375543ff398f4841d22e48f7d4f23ded925b72da", c.digest(scannerFixtureArchive)); + if (observation) for (const pin of ["2b3d176db752433b904a4b42375543ff398f4841d22e48f7d4f23ded925b72da", "132a37610575bd251ecaf0be4c6090dad144dd1397c99aad989a3944c63c3d4a"]) source = source.replace(pin, c.digest(scannerFixtureArchive)); if (fastTimeout) source = source.replace('installer ? 120000 : 15000', '100'); // Expose lexical contracts only in this test VM. Production exports no policy, // verifier, command inventory, child executable or success switch. - source += '\nmodule.exports.test = { commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES, publicInfoClient, publicInfoInstallation, installed };'; + source += '\nmodule.exports.test = { HOSTS, hostContract, executableHost, subject, acquisition, commands, installationCommands, verifyJourney, terminal, tree, canonical, context, subprocess, observationGate, jsonDocument, treeShape, packageIdentity, capabilities, PROFILES, POLICY, scannerArchive, SCANNER_RELEASES, publicInfoClient, publicInfoInstallation, installed };'; const Module = require("node:module"); const instance = new Module(moduleFile, module); instance.filename = moduleFile; instance.paths = Module._nodeModulePaths(path.dirname(moduleFile)); @@ -1196,3 +1196,111 @@ test("scanner archive parsing fails closed on bounded malformed test archives", bytes: body.toString("base64") } }, scannerFixture), name); }); }); + + +test("N2 six closed host contracts preserve excluded execution and exact roots", async t => { + const api = internal().test; + assert.deepEqual(Object.keys(api.HOSTS).sort(), [...c.TARGETS].sort()); + const known = { + 'linux-amd64': ['linux', 'x64', 'x86_64', 'amd64'], + 'linux-arm64': ['linux', 'arm64', 'aarch64', 'arm64'], + 'darwin-amd64': ['darwin', 'x64', 'x86_64', 'amd64'], + 'darwin-arm64': ['darwin', 'arm64', 'arm64', 'arm64'], + 'windows-amd64': ['win32', 'x64', 'x86_64', 'amd64'], + 'windows-arm64': ['win32', 'arm64', 'aarch64', 'arm64'] + }; + for (const [target, expected] of Object.entries(known)) await t.test(target, async () => { + const host = api.hostContract(target); + assert.deepEqual([host.platform, host.architecture, host.machine, host.goarch], expected); + assert.equal(host.client_root, 'home/.codex'); assert.equal(host.state_root, 'state'); + assert.equal(host.project_root, 'projects'); assert.equal(host.argv, 'direct-array-no-shell'); + assert.equal(host.binary_suffix, target.startsWith('windows-') ? '.exe' : ''); + assert.equal(host.modes, target.startsWith('windows-') ? 'pending-native-acl-evidence' : 'posix-exact'); + assert.equal(host.cancellation, target.startsWith('windows-') ? 'pending-owned-job-cleanup' : 'owned-process-group-sigkill'); + const f = fixture(); f.options.target = target; + if (!target.startsWith('linux-')) { + await assert.rejects(native.produce(f.options), /NATIVE_EXECUTION_PENDING/); + assert.throws(() => native.readTerminals(f.root, f.root, f.pins, { ...expectations(f), target }), /NATIVE_EXECUTION_PENDING/); + noTerminal(f); assert.equal(fs.existsSync(f.options.work), false); + } + }); + for (const target of ['linux-x64', 'linux-386', 'windows-x64', '__proto__', '', null]) { + assert.throws(() => api.hostContract(target), /unsupported native target contract/); + } +}); + +test("N2 native host mismatch cannot execute a selected foreign architecture", async t => { + const f = fixture(); f.options.target = 'linux-arm64'; + let calls = 0; t.mock.method(cp, 'spawn', () => { calls++; throw Error('must not execute'); }); + await assert.rejects(native.produce(f.options), /actual native host OS\/architecture/); + assert.equal(calls, 0); noTerminal(f); assert.equal(fs.existsSync(f.options.work), false); +}); + +test("N2 arm64 replay binds both selected subjects, scanner platform and unchanged full custody", async t => { + const f = fixture(); subprocessFixtures(t, f); + await internal(true).produce(f.options); + const api = internal(true).test, root = f.options.output; + for (const name of fs.readdirSync(root)) fs.chmodSync(path.join(root, name), 0o600); + const files = ['transcripts.json', 'trees.json', 'build-info.json', 'preservation.json', + 'preparation.json', 'host.json', 'scans.json', 'acquisition.json']; + const evidence = Object.fromEntries(files.map(name => [name, JSON.parse(fs.readFileSync(path.join(root, name)))])); + const custody = structuredClone(evidence['preservation.json']); + for (const row of evidence['transcripts.json'].installer) for (const state of [row.before, row.after]) + for (const entry of state.acquisition) entry.path = entry.path.replace('linux-amd64', 'linux-arm64'); + for (const scan of evidence['scans.json']) { + scan.executable.path = scan.executable.path.replace('linux-amd64', 'linux-arm64'); + scan.archive.url = scan.archive.url.replace('x86_64-unknown-linux-gnu', 'aarch64-unknown-linux-gnu'); + } + for (const info of Object.values(evidence['build-info.json'])) { + const setting = info.Settings.find(x => x.Key === 'GOARCH'); assert.ok(setting); setting.Value = 'arm64'; + } + Object.assign(evidence['host.json'], { architecture: 'arm64', machine: 'aarch64', target: 'linux-arm64' }); + const expected = { ...expectations(f), target: 'linux-arm64' }; + const manifest = JSON.parse(fs.readFileSync(path.join(f.root, 'candidate/candidate.json'))); + const seal = () => { + for (const name of files) fs.writeFileSync(path.join(root, name), c.encode(evidence[name])); + for (const product of c.PRODUCTS) fs.writeFileSync(path.join(root, `${product}-terminal.json`), + c.encode(api.terminal(product, f.pins, manifest, expected, evidence, expected.tools))); + }; + seal(); + const result = fixtureReader(root, f.root, f.pins, expected); + assert.deepEqual(result.map(x => x.lane), c.PRODUCTS.map(p => `${p}/linux-arm64`)); + assert.deepEqual(evidence['preservation.json'], custody); + for (const product of c.PRODUCTS) { + const terminal = result.find(x => x.subject.product === product); + assert.deepEqual(terminal.subject, { product, target: 'linux-arm64', ...manifest.products[product].assets['linux-arm64'] }); + } + // Independently change each bound subject/host/invocation; the selected lane + // does not authorize trusting an embedded target or successful status field. + for (const mutate of [ + x => { x.subject.target = 'linux-amd64'; }, + x => { x.peer_subject.binary.sha256 = 'b'.repeat(64); }, + x => { x.producer.run_attempt++; }, + x => { x.host.machine = 'x86_64'; }, + x => { x.assertions.runtime = 'pass'; }, + x => { x.evidence.pop(); } + ]) { + const value = structuredClone(result[0]); mutate(value); + fs.writeFileSync(path.join(root, 'agentplugins-terminal.json'), c.encode(value)); + assert.throws(() => fixtureReader(root, f.root, f.pins, expected)); + seal(); + } + assert.throws(() => fixtureReader(root, f.root, f.pins, expectations(f))); + assert.equal(fixtureReader(root, f.root, f.pins, expected).length, 2); + // Production still rejects the synthetic scanner archive; replay above is + // exclusively fixture control flow and never actual arm64 qualification. + assert.throws(() => native.readTerminals(root, f.root, f.pins, expected), /independently pinned scanner release archive/); +}); + +test('N2 explicit invalid target never falls back to accepted N1 default', async t => { + const f = fixture(); let processes = 0; + t.mock.method(cp, 'spawn', () => { processes++; throw Error('no process permitted'); }); + for (const target of [null, undefined, '', 'linux-x64', 'linux-386', 'darwin-x64', 'windows-x64', '__proto__', 1, true, {}, []]) { + f.options.target = target; + await assert.rejects(native.produce(f.options), /unsupported native target contract/); + assert.throws(() => native.readTerminals(f.root, f.root, f.pins, + { ...expectations(f), target }), /unsupported native target contract/); + noTerminal(f); assert.equal(fs.existsSync(f.options.work), false); + } + assert.equal(processes, 0); +}); diff --git a/npm/agentplugins/test/authoring-promotion.test.js b/npm/agentplugins/test/authoring-promotion.test.js index 73686733..658e2326 100644 --- a/npm/agentplugins/test/authoring-promotion.test.js +++ b/npm/agentplugins/test/authoring-promotion.test.js @@ -122,9 +122,10 @@ else process.stdout.write(typeof r.body==='string'?r.body:JSON.stringify(r.body) `); const spawn = cp.spawnSync; t.mock.method(cp, "spawnSync", function(executable, args, options) { + if (executable === "/usr/bin/python3") return spawn(executable, args, options); assert.equal(executable, "/usr/bin/gh"); assert.equal(options.shell, false); assert.equal(options.timeout, 30000); assert.equal(options.killSignal, "SIGKILL"); assert.equal(options.env.PATH, "/usr/local/bin:/usr/bin:/bin"); - assert.equal(options.env.HOME, f.scratch); assert.equal(options.env.GH_CONFIG_DIR, f.scratch); + assert.ok(options.env.HOME === f.scratch || options.env.HOME.startsWith(f.scratch + path.sep)); assert.equal(options.env.GH_CONFIG_DIR, options.env.HOME); assert.equal(options.env.GH_TOKEN, undefined); assert.equal(options.env.NODE_OPTIONS, undefined); return spawn(process.execPath, [script, ...args], options); }); @@ -413,7 +414,7 @@ test("actual preflight and record shells bind dispatch before native acquisition const values={...env,...(changed ? {TAG:"agentplugins-v0.1.55",WORKFLOW_REF:"refs/tags/agentplugins-v0.1.55"} : {})}; const run=name=>cp.spawnSync("/bin/bash",["-e","-o","pipefail","-s"],{cwd,env:values,input:script(name),encoding:"utf8",timeout:10000}); assert.equal(run("Validate promotion identity before checkout").status,0); - for(const name of ["Reject missing native terminal contracts before protected effects","Acquire exact frozen preparation after native admission"]) { + for(const name of ["Admit exact native evidence read-only before protected effects","Acquire exact frozen preparation after native admission"]) { const result=run(name); assert.equal(result.status,1); assert.equal(result.stdout,""); assert.match(result.stderr,changed ? /selected promotion identity/ : /NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); if(changed) assert.doesNotMatch(result.stderr,/NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); @@ -525,3 +526,496 @@ test("verified own creates advance expected presence without an extra upload", t assert.deepEqual(b.seq.promotePair(b.recheck,true).public_readback,["public","public"]); assert.equal(b.writes().length,before); }); + + +// Exercise the actual standard-library reader in a separate bounded process. +// ZIP contents here are synthetic control-flow evidence, never qualification. +function evidenceZIP(files) { + const result = cp.spawnSync('/usr/bin/python3', ['-B', '-c', + "import sys,json,base64,zipfile,io; b=io.BytesIO(); z=zipfile.ZipFile(b,'w',zipfile.ZIP_DEFLATED); " + + "[(z.writestr(n,base64.b64decode(v))) for n,v in json.load(sys.stdin).items()]; z.close(); sys.stdout.buffer.write(b.getvalue())"], { + input: JSON.stringify(Object.fromEntries(Object.entries(files).map(([name, bytes]) => [name, Buffer.from(bytes).toString('base64')]))), + env: { PATH: '/usr/bin:/bin', PYTHONNOUSERSITE: '1' }, timeout: 30000, maxBuffer: 64 * 1024 * 1024 + }); + assert.equal(result.status, 0, String(result.stderr)); return result.stdout; +} +function identityMetadata(f) { + return c.encode({ identity: ID, status: 'CANDIDATE', manifest_sha256: f.record.candidate_sha256, + output: '/prior-builder/candidate', local_build_evidence: '/prior-builder/evidence', + release_eligible: false, platform_acceptance: false, attested: false }); +} +function preparationZIP(f, mutate = () => {}) { + const n = require('../scripts/authoring-native-qualification'); + const pins = { identity: ID, candidate_sha256: f.record.candidate_sha256, pair_marker_sha256: f.record.pair_marker_sha256, + products: Object.fromEntries(c.PRODUCTS.map(product => [product, { + manifest_sha256: f.record.products[product].manifest_sha256, checksums_sha256: f.record.products[product].checksums_sha256 }])) }; + n.writePreparation(f.root, pins, { repository: c.REPOSITORY, workflow: p.WORKFLOW, source: ID.commit, + workflow_sha: ID.commit, run_id: 21, run_attempt: 2 }); + const names = ['candidate/candidate.json', 'pair-prepared.json', 'preparation-run.json', + ...c.PRODUCTS.flatMap(product => fs.readdirSync(path.join(f.root, product)).map(name => `${product}/${name}`))]; + const files = Object.fromEntries(names.map(name => [name, fs.readFileSync(path.join(f.root, name))])); + files['candidate-identity.json'] = identityMetadata(f); + mutate(files); return evidenceZIP(files); +} +function zipRoutes(body, artifact, workflow = p.WORKFLOW) { + return { + [endpoint(`actions/runs/${artifact.run_id}/attempts/${artifact.run_attempt}`)]: { body: { + id: artifact.run_id, run_attempt: artifact.run_attempt, status: 'completed', conclusion: 'success', + repository: { full_name: c.REPOSITORY }, head_repository: { full_name: c.REPOSITORY }, head_sha: ID.commit, path: workflow } }, + [endpoint(`actions/artifacts/${artifact.artifact_id}`)]: { body: { + id: artifact.artifact_id, expired: false, digest: `sha256:${artifact.artifact_sha256}`, name: 'synthetic-only', + workflow_run: { id: artifact.run_id, head_sha: ID.commit }, size_in_bytes: body.length } }, + [endpoint(`actions/artifacts/${artifact.artifact_id}/zip`)]: { binary: body.toString('base64') } + }; +} +function registeredNativeRecord(f) { + for (const lane of f.record.qualification.lanes.filter(x => x.lane !== 'public-packed-pair')) { + lane.schema = 'authoring-frozen-native/v1'; lane.workflow = '.github/workflows/authoring-frozen-native.yml'; + const index = c.TARGETS.indexOf(lane.lane.split('/')[1]); + lane.artifact = { run_id: 100 + index, run_attempt: 2, artifact_id: 200 + index, artifact_sha256: hash(`fixture zip ${index}`) }; + } +} +function promoteEvidence(f, preparation) { + fs.writeFileSync(f.recordFile, p.encodeRecord(f.record)); + return p.promote({ record: f.recordFile, root: f.root, scratch: f.scratch, + workflow_sha: ID.commit, preparation, selected }); +} +function noProtectedCalls(calls) { + for (const call of calls()) { + assert.ok(!['release', 'attestation'].includes(call.args[0]), 'rejection must cause zero protected effects'); + assert.ok(!call.args.includes('POST') && !call.args.includes('PATCH') && !call.args.includes('DELETE')); + } +} + +test('N2 independently acquired preparation ZIP closes receipt attempt and eighteen frozen subjects', t => { + const f = fixture(), bytes = preparationZIP(f), loc = { ...pin, artifact_sha256: c.digest(bytes) }; + const calls = provider(t, f, zipRoutes(bytes, loc)); + const acquired = p.acquirePreparation(loc, f.record, f.scratch); + assert.equal(acquired.preparation.producer.run_attempt, 2); + assert.equal(p.frozenSubjects(acquired.root, f.record).length, 18); + assert.equal(fs.readFileSync(path.join(acquired.root, 'candidate/candidate.json')).equals(fs.readFileSync(path.join(f.root, 'candidate/candidate.json'))), true); + noProtectedCalls(calls); +}); + +for (const [name, mutate, error] of [ + ['false preparation metadata claim', files => { const v = JSON.parse(files['candidate-identity.json']); v.attested = true; files['candidate-identity.json'] = c.encode(v); }, /preparation metadata claims/], + ['foreign preparation metadata identity', files => { const v = JSON.parse(files['candidate-identity.json']); v.identity.commit = 'b'.repeat(40); files['candidate-identity.json'] = c.encode(v); }, /preparation metadata identity/], + ['stale embedded preparation attempt', files => { const v = JSON.parse(files['preparation-run.json']); v.producer.run_attempt = 1; files['preparation-run.json'] = c.encode(v); }, /eighteen preparation/], + ['stale embedded preparation source', files => { const v = JSON.parse(files['preparation-run.json']); v.producer.source = 'b'.repeat(40); files['preparation-run.json'] = c.encode(v); }, /eighteen preparation/], + ['missing receipt subject', files => { const v = JSON.parse(files['preparation-run.json']); v.subjects.pop(); files['preparation-run.json'] = c.encode(v); }, /eighteen preparation/], + ['missing ZIP subject', files => { delete files['pair-prepared.json']; }, /ZIP extraction rejected/], + ['unexpected ZIP subject', files => { files['unreviewed.json'] = Buffer.from('{}'); }, /ZIP extraction rejected/], + ['changed frozen bytes', files => { files['candidate/candidate.json'] = Buffer.from('{}'); }, /candidate/] +]) test(`N2 ${name} has no protected effects`, t => { + const f = fixture(), body = preparationZIP(f, mutate), loc = { ...pin, artifact_sha256: c.digest(body) }; + registeredNativeRecord(f); + const calls = provider(t, f, zipRoutes(body, loc)); + assert.throws(() => promoteEvidence(f, loc), error); + noProtectedCalls(calls); +}); + +test('N2 checked ZIP cannot be swapped at extraction and unsupported public never authorizes effects', t => { + const f = fixture(), body = preparationZIP(f), loc = { ...pin, artifact_sha256: c.digest(body) }; + const calls = provider(t, f, zipRoutes(body, loc)); + const file = p.acquireArtifact(loc, p.WORKFLOW, ID.commit, f.scratch); + const extracted = path.join(f.scratch, 'swapped'); + const names = ['transcripts.json', 'trees.json', 'build-info.json', 'preservation.json', + 'preparation.json', 'host.json', 'scans.json', 'acquisition.json', 'agentplugins-terminal.json', 'plugin-kit-ai-terminal.json']; + const replacement = evidenceZIP(Object.fromEntries(names.map(name => [name, c.encode({ fixture: true })]))); + fs.chmodSync(file, 0o600); fs.writeFileSync(file, replacement); + assert.throws(() => p.extractArtifact(file, loc, 'native', names, extracted, f.scratch), /ZIP extraction rejected/); + assert.equal(fs.existsSync(extracted), false); + registeredNativeRecord(f); + const bodyRecord = p.encodeRecord(f.record); + assert.equal(p.checkNativeContracts(f.record), undefined); // Registered syntax grants no authorization. + assert.doesNotThrow(() => p.validateSelection(bodyRecord, selected)); + assert.throws(() => p.admitRecord(bodyRecord, selected), /public-packed-pair/); + assert.throws(() => p.requireNativeContracts(f.record.qualification.lanes.slice(0, 12)), /missing lanes \[public-packed-pair\]/); + noProtectedCalls(calls); +}); + +test('N2 native artifact metadata success cannot replace a valid paired terminal', t => { + const f = fixture(), prepared = preparationZIP(f), loc = { ...pin, artifact_sha256: c.digest(prepared) }; + registeredNativeRecord(f); + const files = Object.fromEntries(['transcripts.json', 'trees.json', 'build-info.json', 'preservation.json', + 'preparation.json', 'host.json', 'scans.json', 'acquisition.json', 'agentplugins-terminal.json', 'plugin-kit-ai-terminal.json'] + .map(name => [name, c.encode({ fixture: true, status: 'success', name })])); + const nativeZIP = evidenceZIP(files); + const lanes = f.record.qualification.lanes.filter(x => x.lane.endsWith('/linux-amd64')); + for (const lane of lanes) { + lane.artifact.artifact_sha256 = c.digest(nativeZIP); + lane.sha256 = c.digest(files[`${lane.lane.split('/')[0]}-terminal.json`]); + } + const calls = provider(t, f, { ...zipRoutes(prepared, loc), + ...zipRoutes(nativeZIP, lanes[0].artifact, '.github/workflows/authoring-frozen-native.yml') }); + assert.throws(() => promoteEvidence(f, loc), /eighteen|preparation|Expected/); + noProtectedCalls(calls); + assert.ok(calls().some(x => x.args.at(-1) === endpoint(`actions/artifacts/${lanes[0].artifact.artifact_id}/zip`)), 'native bytes actually acquired'); +}); + +test('N2 provider-bound native semantic mutations reject with zero protected effects', async t => { + // Reuse executed child fixtures, not a fabricated passing terminal generator. + // Only the test-local reader substitutes the synthetic scanner archive pin. + const Module = require('node:module'); + const file = path.join(__dirname, 'authoring-native-qualification.test.js'); + const source = fs.readFileSync(file, 'utf8'); + const instance = new Module(file, module); + instance.filename = file; instance.paths = Module._nodeModulePaths(__dirname); + instance._compile(source.slice(0, source.indexOf('test("shared verifier')) + + '\nmodule.exports = { fixture, subprocessFixtures, internal };', file); + const fixtureAPI = instance.exports, f = fixtureAPI.fixture(); + f.options.producer.run_id = 43; + fixtureAPI.subprocessFixtures(t, f); + await fixtureAPI.internal(true).produce(f.options); + registeredNativeRecord(f); + const preparationFiles = ['candidate/candidate.json', 'pair-prepared.json', 'preparation-run.json', + ...c.PRODUCTS.flatMap(product => fs.readdirSync(path.join(f.root, product)).map(name => `${product}/${name}`))]; + const prepared = evidenceZIP({ ...Object.fromEntries(preparationFiles.map(name => [name, fs.readFileSync(path.join(f.root, name))])), + 'candidate-identity.json': identityMetadata(f) }); + const loc = { ...pin, run_id: 42, run_attempt: 2, artifact_sha256: c.digest(prepared) }; + const original = Object.fromEntries(fs.readdirSync(f.options.output).map(name => [name, fs.readFileSync(path.join(f.options.output, name))])); + const lanes = f.record.qualification.lanes.filter(x => x.lane.endsWith('/linux-amd64')); + for (const lane of lanes) lane.artifact.run_id = 43; + const calls = provider(t, f, {}); + const realReader = require('../scripts/authoring-native-qualification'); + t.mock.method(realReader, 'readTerminals', fixtureAPI.internal(true).readTerminals); + const routesFile = path.join(f.sandbox, 'routes.json'); + const reseal = (files, changed = null) => { + if (changed) for (const product of c.PRODUCTS) { + const name = `${product}-terminal.json`, terminal = JSON.parse(files[name]); + for (const entry of terminal.evidence) if (files[entry.file]) Object.assign(entry, c.metadata(files[entry.file])); + files[name] = c.encode(terminal); + } + const bytes = evidenceZIP(files); + for (const lane of lanes) { + lane.artifact.artifact_sha256 = c.digest(bytes); + lane.sha256 = c.digest(files[`${lane.lane.split('/')[0]}-terminal.json`]); + } + return { ...zipRoutes(prepared, loc), ...zipRoutes(bytes, lanes[0].artifact, '.github/workflows/authoring-frozen-native.yml') }; + }; + const cases = [ + ['embedded stale native source', files => { const x = JSON.parse(files['agentplugins-terminal.json']); x.producer.source = 'b'.repeat(40); files['agentplugins-terminal.json'] = c.encode(x); }, /source|producer|Expected/], + ['embedded stale native attempt', files => { const x = JSON.parse(files['agentplugins-terminal.json']); x.producer.run_attempt = 1; files['agentplugins-terminal.json'] = c.encode(x); }, /producer|run_attempt|Expected/], + ['missing signed peer subject', files => { const x = JSON.parse(files['agentplugins-terminal.json']); delete x.peer_subject; files['agentplugins-terminal.json'] = c.encode(x); }, /closed native terminal/], + ['wrong peer binary', files => { const x = JSON.parse(files['agentplugins-terminal.json']); x.peer_subject.binary.sha256 = 'b'.repeat(64); files['agentplugins-terminal.json'] = c.encode(x); }, /peer_subject|Expected/], + ['success-shaped runtime claim', files => { const x = JSON.parse(files['agentplugins-terminal.json']); x.assertions.runtime = 'pass'; files['agentplugins-terminal.json'] = c.encode(x); }, /runtime|Expected/], + ['omitted mandatory command', files => { const x = JSON.parse(files['transcripts.json']); x.agentplugins.pop(); files['transcripts.json'] = c.encode(x); }, /command|Expected/], + ['rehashed grouped reconciliation omission', files => { + const x = JSON.parse(files['transcripts.json']); + const row = x.installer.find(r => r.id === 'info'); + assert.ok(row, 'mandatory info command present'); + const stdout = JSON.parse(row.stdout), client = stdout.data.clients[0]; + delete client.receipt_reconciled; delete client.native_discovery_reconciled; delete client.native_identity_state; + row.stdout = JSON.stringify(stdout); files['transcripts.json'] = c.encode(x); + }, /public info client matches checked registration/] + ]; + for (const [label, mutate, error] of cases) await t.test(label, () => { + const files = Object.fromEntries(Object.entries(original).map(([name, bytes]) => [name, Buffer.from(bytes)])); + mutate(files); fs.writeFileSync(routesFile, JSON.stringify(reseal(files, true))); + assert.throws(() => promoteEvidence(f, loc), error); + noProtectedCalls(calls); + }); + // A coherent accepted Linux pair reaches the next required target, whose + // missing provider route fails. Linux success does not silently shrink lanes. + fs.writeFileSync(routesFile, JSON.stringify(reseal(original))); + assert.throws(() => promoteEvidence(f, loc), /trusted gh failed/); + const next = c.TARGETS.find(target => target !== 'linux-amd64'); + const missing = f.record.qualification.lanes.find(lane => lane.lane === `agentplugins/${next}`).artifact; + assert.ok(calls().some(call => call.args.at(-1) === endpoint(`actions/runs/${missing.run_id}/attempts/${missing.run_attempt}`))); + noProtectedCalls(calls); +}); + + +// C1 interface-only tests. Load a private test copy to stub existing module +// operations below the public wrappers. No production injection API is added, +// and no subprocess, artifact acquisition or signature is executed. +function c1PromotionInterface(t) { + t.mock.method(cp, "spawnSync", () => assert.fail("C1 interface test cannot spawn")); + const filename = require.resolve("../scripts/authoring-promotion"); + const Module = require("node:module"), local = new Module(filename, module); + local.filename = filename; local.paths = module.paths; + local._compile(fs.readFileSync(filename, "utf8") + String.raw` +const c1Calls = []; +const c1Responses = new Map(); +acquireArtifact = (pin, workflow, source, cwd) => { + c1Calls.push({operation:"acquire",pin,workflow,source,cwd}); + return path.join(cwd,"artifact-"+pin.artifact_id+".zip"); +}; +extractArtifact = (file,pin,kind,files,output,cwd) => { + c1Calls.push({operation:"extract",file,pin,kind,files,output,cwd}); return output; +}; +readPreparationBinding = (root,pin,record,receiptSha256) => { + c1Calls.push({operation:"read",root,pin,record,receiptSha256}); + return {root,preparation:{sha256:receiptSha256 ?? "Q-computed-receipt",producer:invocationFor(pin,WORKFLOW,record.identity.commit)}}; +}; +cliVersion = cwd => c1Calls.push({operation:"version",cwd}); +api = (endpoint,cwd) => { c1Calls.push({operation:"api",endpoint,cwd}); return c1Responses.get(endpoint) ?? {sha:"a".repeat(40)}; }; +module.exports.c1Calls = c1Calls; +module.exports.c1Responses = c1Responses; +`, filename); + const scratch = fs.mkdtempSync(path.join(os.tmpdir(),"q-interface-")); + const input = { schema:"authoring-native-inputs/v1",identity:structuredClone(ID),authoring_mode:"release-cli-contract-v1", + asset_scope:"six-platform-pair",candidate_sha256:hash("candidate"),pair_marker_sha256:hash("pair"),products:{}, + preparation:{sha256:hash("receipt"),artifact:structuredClone(pin)}, + producer:{workflow:p.WORKFLOW,source:ID.commit,run_id:41,run_attempt:3} }; + for (const product of c.PRODUCTS) { + const assets = {}; + for (const target of c.TARGETS) { + const binary = {file:c.executableName(product,target),sha256:hash(product+target),size:10}; + assets[target] = {file:c.assetName(product,ID.versions[product],target), + sha256:product === "agentplugins" ? binary.sha256 : hash("outer"+target),size:10,binary}; + } + input.products[product] = {tag:(product === "agentplugins" ? "agentplugins-v" : "v")+ID.versions[product], + manifest_sha256:hash(product+"manifest"),checksums_sha256:hash(product+"checksums"),assets}; + } + const {preparation:_prep,...common} = input; + const record = {...common,schema:p.SCHEMA,qualification:{lanes:p.LANES.map((lane,i) => { + const pairs = lane === "public-packed-pair" ? c.PRODUCTS.flatMap(product => c.TARGETS.map(target => [product,target])) : [lane.split("/")]; + return {lane,schema:"fixture-terminal/v1",sha256:hash("terminal"+i),workflow:".github/workflows/fixture-only.yml", + source:ID.commit,artifact:{...pin,artifact_id:100+i},subjects:pairs.map(([product,target]) => ({product,target, + sha256:input.products[product].assets[target].sha256,binary_sha256:input.products[product].assets[target].binary.sha256}))}; + })}}; + return {adapter:local.exports,scratch,input,record,body:require("../scripts/authoring-native-inputs").encodeInputs(input)}; +} + +test("C1 provenance preparation adapters share exact existing twenty-entry same-byte interface", t => { + const f = c1PromotionInterface(t), a = f.adapter; + const result = a.acquireInputPreparation(f.body,f.scratch); + assert.deepEqual(a.c1Calls.map(c => c.operation),["acquire","extract","read"]); + const [acquired,extracted,read] = a.c1Calls; + assert.deepEqual(acquired.pin,f.input.preparation.artifact); + assert.equal(acquired.workflow,p.WORKFLOW); assert.equal(acquired.source,ID.commit); + assert.equal(extracted.file,path.join(acquired.cwd,"artifact-31.zip")); + assert.equal(extracted.cwd,acquired.cwd); assert.equal(extracted.kind,"preparation"); + assert.equal(extracted.files.length,20); assert.equal(new Set(extracted.files).size,20); + assert.ok(extracted.files.includes("candidate/candidate.json")); + assert.ok(!extracted.files.includes("native-inputs.json")); assert.ok(!extracted.files.includes("authoring-promotion.json")); + assert.equal(read.root,extracted.output); assert.deepEqual(read.record,f.input); + assert.equal(read.receiptSha256,f.input.preparation.sha256); + assert.deepEqual(Object.keys(result),["root","preparation"]); + assert.deepEqual(result.preparation,{sha256:f.input.preparation.sha256,producer:{repository:c.REPOSITORY, + workflow:p.WORKFLOW,source:ID.commit,workflow_sha:ID.commit,run_id:pin.run_id,run_attempt:pin.run_attempt}}); +}); + +test("C1 provenance Q wrapper preserves full record validation, bytes and receipt return contract", t => { + const f = c1PromotionInterface(t), before = p.encodeRecord(f.record), a = f.adapter; + const result = a.acquirePreparation(pin,f.record,f.scratch); + const read = a.c1Calls.at(-1); + assert.deepEqual(read.record,p.decodeRecord(before)); assert.equal(read.receiptSha256,undefined); + assert.deepEqual(p.encodeRecord(f.record),before); + assert.deepEqual(result,{root:read.root,preparation:{sha256:"Q-computed-receipt",producer:{repository:c.REPOSITORY, + workflow:p.WORKFLOW,source:ID.commit,workflow_sha:ID.commit,run_id:pin.run_id,run_attempt:pin.run_attempt}}}); + a.c1Calls.length = 0; + assert.throws(() => a.acquirePreparation(pin,f.input,f.scratch)); + const missing = structuredClone(f.record); missing.qualification.lanes.pop(); + assert.throws(() => a.acquirePreparation(pin,missing,f.scratch),/missing required lanes/); + assert.deepEqual(a.c1Calls,[]); + assert.equal(p.LANES.length,13); + assert.throws(() => p.requireNativeContracts(f.record.qualification.lanes),/NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); + assert.throws(() => p.requireNativeContracts(f.record.qualification.lanes.slice(0,12)),/public-packed-pair/); + for (const product of c.PRODUCTS) { + assert.ok(p.releasePins(f.record,product).some(row => row.name === "authoring-promotion.json")); + assert.ok(!p.releasePins(f.record,product).some(row => row.name === "native-inputs.json")); + } +}); + +test("C1 provenance fixed tag adapter reuses both existing derived release-tag endpoints", t => { + const f = c1PromotionInterface(t); f.adapter.checkInputTags(f.body,f.scratch); + assert.deepEqual(f.adapter.c1Calls,[{operation:"version",cwd:f.scratch}, + {operation:"api",endpoint:"commits/agentplugins-v0.1.54",cwd:f.scratch}, + {operation:"api",endpoint:"commits/v2.0.0",cwd:f.scratch}]); +}); + +test("C1 provenance malformed preparation adapter input rejects before any intake operation", t => { + const f = c1PromotionInterface(t); + for (const bytes of [null,Buffer.from("{}\n"),Buffer.concat([f.body,Buffer.from("\n")])]) { + assert.throws(() => f.adapter.acquireInputPreparation(bytes,f.scratch)); + assert.throws(() => f.adapter.readInputPreparation(f.scratch,bytes)); + assert.throws(() => f.adapter.checkInputTags(bytes,f.scratch)); + } + assert.deepEqual(f.adapter.c1Calls,[]); assert.deepEqual(fs.readdirSync(f.scratch),[]); +}); + +test("C1 provenance fixed completed-attempt inspector rejects foreign or stale provider metadata at interface level", t => { + const f = c1PromotionInterface(t), a = f.adapter; + const run = { id:pin.run_id,run_attempt:pin.run_attempt,status:"completed",conclusion:"success", + repository:{full_name:c.REPOSITORY},head_repository:{full_name:c.REPOSITORY},head_sha:ID.commit,path:p.WORKFLOW }; + const item = { id:pin.artifact_id,expired:false,digest:"sha256:"+pin.artifact_sha256, + workflow_run:{id:pin.run_id,head_sha:ID.commit},name:"fixture-only",size_in_bytes:123 }; + const select = (r,i) => { + a.c1Responses.set("actions/runs/21/attempts/2",r); + a.c1Responses.set("actions/artifacts/31",i); a.c1Calls.length = 0; + }; + select(run,item); + assert.deepEqual(a.inspectArtifact(pin,p.WORKFLOW,ID.commit,f.scratch),{run,item}); + assert.deepEqual(a.c1Calls.map(c => c.endpoint),["actions/runs/21/attempts/2","actions/artifacts/31"]); + for (const mutate of [r => r.id++,r => r.run_attempt++,r => r.status = "in_progress",r => r.conclusion = "failure", + r => r.repository.full_name = "fork/repo",r => r.head_repository.full_name = "fork/repo", + r => r.head_sha = "b".repeat(40),r => r.path = ".github/workflows/other.yml"]) { + const changed = structuredClone(run); mutate(changed); select(changed,item); + assert.throws(() => a.inspectArtifact(pin,p.WORKFLOW,ID.commit,f.scratch),/exact successful/); + assert.equal(a.c1Calls.length,1); + } + for (const mutate of [i => i.id++,i => i.expired = true,i => i.digest = "sha256:"+hash("different"), + i => i.workflow_run.id++,i => i.workflow_run.head_sha = "b".repeat(40),i => i.size_in_bytes = 0]) { + const changed = structuredClone(item); mutate(changed); select(run,changed); + assert.throws(() => a.inspectArtifact(pin,p.WORKFLOW,ID.commit,f.scratch)); + assert.ok(a.c1Calls.every(c => c.operation === "api")); + } + assert.deepEqual(fs.readdirSync(f.scratch),[]); +}); + +test("C1 provenance fixed tag adapter rejects a moved second product tag", t => { + const f = c1PromotionInterface(t); + f.adapter.c1Responses.set("commits/v2.0.0",{sha:"b".repeat(40)}); + assert.throws(() => f.adapter.checkInputTags(f.body,f.scratch),/moved release tag/); + assert.ok(f.adapter.c1Calls.every(c => ["version","api"].includes(c.operation))); +}); + +// New fixed-stage adapter tests mock the existing process interface IN MEMORY. +// No verifier execution or authentic signature compatibility is claimed. +test("C1 stage integration fixed npm signer uses existing verification interface with exact three subjects", t => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "c1-stage-signer-")); + const rows = ["completion.json", "universal-agent-plugins-0.1.54.tgz", "plugin-kit-ai-2.0.0.tgz"].map(name => { + const file = path.join(root, name), body = Buffer.from(`unsigned interface fixture ${name}`); + fs.writeFileSync(file, body); return { name, file, digest: { sha256: c.digest(body) } }; + }); + let workflow = ".github/workflows/agentplugins-npm-publish.yml", calls = [], active; + t.mock.method(cp, "spawnSync", (exe, args, options) => { + assert.equal(exe, "/usr/bin/gh"); assert.equal(options.env.PATH, "/usr/local/bin:/usr/bin:/bin"); + calls.push(args); + if (args[0] === "--version") return { status: 0, stdout: `gh version ${p.GH_VERSION} (interface fixture)\n` }; + assert.deepEqual(args.slice(0, 3), ["attestation", "verify", active.file]); + assert.equal(args[args.indexOf("--signer-workflow") + 1], `github.com/${c.REPOSITORY}/.github/workflows/agentplugins-npm-publish.yml`); + assert.equal(args[args.indexOf("--signer-digest") + 1], ID.commit); + assert.equal(args[args.indexOf("--source-digest") + 1], ID.commit); + assert.equal(args[args.indexOf("--source-ref") + 1], selected.ref); + const statement = verified(active.expected); // existing output fixture shape only + statement[0].verificationResult.statement.predicate.buildDefinition.externalParameters.workflow.path = workflow; + return { status: 0, stdout: JSON.stringify(statement) }; + }); + for (const row of rows) { + active = { file: row.file, expected: { name: row.name, sha256: row.digest.sha256, source: ID.commit, + workflow_sha: ID.commit, ref: selected.ref, run_id: 501, run_attempt: 4, + subjects: rows.map(({ name, digest }) => ({ name, digest })) } }; + assert.equal(p.verifyStageSubject(active.file, active.expected, root)._type, "https://in-toto.io/Statement/v1"); + } + assert.equal(calls.filter(a => a[0] === "attestation").length, 3); + workflow = p.WORKFLOW; + assert.throws(() => p.verifyStageSubject(active.file, active.expected, root), /verified workflow/); + for (const mutate of [e => e.workflow_sha = "b".repeat(40), e => e.subjects.pop(), + e => e.subjects[0].name = "authoring-promotion.json", e => e.workflow = p.WORKFLOW, + e => e.ref = "refs/heads/main", e => e.sha256 = hash("different bytes")]) { + const expected = structuredClone(active.expected); mutate(expected); const before = calls.length; + assert.throws(() => p.verifyStageSubject(active.file, expected, root)); assert.equal(calls.length, before); + } +}); + +// New fixed workflow interfaces only. Provider/log bytes below are explicitly +// mocked orchestration evidence, never genuine custody or N2 acceptance. +function c1WorkflowProvider(t) { + const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'c1-workflow-provider-')); + const artifact = {run_id: 701, run_attempt: 3, artifact_id: 801, artifact_sha256: hash('opaque checked byte fixture')}; + const workflow = '.github/workflows/agentplugins-npm-publish.yml'; + const fields = {GITHUB_ACTIONS: 'true', GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: c.REPOSITORY, + GITHUB_SHA: selected.source, GITHUB_WORKFLOW_SHA: selected.source, GITHUB_REF: selected.ref, + GITHUB_WORKFLOW_REF: `${c.REPOSITORY}/${workflow}@${selected.ref}`, GITHUB_RUN_ID: '701', GITHUB_RUN_ATTEMPT: '3', + GITHUB_JOB: 'paired_stage_attestation'}; + for (const [key, value] of Object.entries(fields)) { + const prior = process.env[key]; process.env[key] = value; + t.after(() => {if (prior === undefined) delete process.env[key]; else process.env[key] = prior;}); + } + const run = {id: 701, run_attempt: 3, status: 'in_progress', conclusion: null, event: 'workflow_dispatch', + repository: {full_name: c.REPOSITORY}, head_repository: {full_name: c.REPOSITORY}, head_sha: selected.source, + path: workflow, head_branch: selected.tag}; + const job = {id: 901, run_id: 701, run_attempt: 3, head_sha: selected.source, head_branch: selected.tag, + name: 'paired_stage', status: 'completed', conclusion: 'success', started_at: '2026-09-09T01:00:00Z', + completed_at: '2026-09-09T01:10:00Z', steps: ['C1 preflight', 'C1 checkout', 'C1 setup', 'C1 stage', 'C1 upload', 'C1 upload evidence'] + .map((name, i) => ({name, number: i + 2, status: 'completed', conclusion: 'success'}))}; + const signing = {...job, id: 902, name: 'paired_stage_attestation', status: 'in_progress', conclusion: null}; + const jobs = {total_count: 2, jobs: [job, signing]}; + const item = {id: 801, expired: false, digest: `sha256:${artifact.artifact_sha256}`, + workflow_run: {id: 701, head_sha: selected.source}, size_in_bytes: Buffer.byteLength('opaque checked byte fixture'), + name: `authoring-public-stage-${selected.source}-701-3`, created_at: '2026-09-09T01:05:00Z'}; + const records = [ + {operation: 'start', source: selected.source, ref: selected.ref, run_id: 701, run_attempt: 3, + input_sha256: hash('I'), input_artifact: {run_id: 601, run_attempt: 2, artifact_id: 701, artifact_sha256: hash('I zip')}}, + {operation: 'pack', product: 'agentplugins', pack: {fixture_only: 'agent'}}, + {operation: 'pack', product: 'plugin-kit-ai', pack: {fixture_only: 'kit'}}, + {operation: 'completion', stage_sha256: hash('S')}, + {operation: 'upload', artifact_id: 801, artifact_sha256: artifact.artifact_sha256, stage_sha256: hash('S')}]; + const calls = []; + t.mock.method(cp, 'spawnSync', (exe, args, options) => { + assert.equal(exe, '/usr/bin/gh'); calls.push(args); + let stdout; + const endpoint = args.at(-1); + if (args[0] === '--version') stdout = `gh version ${p.GH_VERSION} (mocked)\n`; + else if (endpoint.endsWith('/zip')) stdout = Buffer.from('opaque checked byte fixture'); + else if (endpoint.endsWith('/logs')) stdout = records.map(r => `2026-09-09T01:06:00.000Z C1_STAGE ${JSON.stringify(r)}\n`).join(''); + else if (endpoint.endsWith('/jobs?per_page=100')) stdout = JSON.stringify(jobs); + else if (endpoint.includes('/attempts/')) stdout = JSON.stringify(run); + else if (endpoint.includes('/commits/')) stdout = JSON.stringify({sha: selected.source}); + else if (endpoint.endsWith('/artifacts/801')) stdout = JSON.stringify(item); + else assert.fail(`unexpected provider request ${endpoint}`); + return {status: 0, stdout}; + }); + return {scratch, artifact, workflow, run, job, signing, jobs, item, records, calls, + options: {artifact, selected, workflow_sha: selected.source, scratch}}; +} +test('C1 workflow current unsigned custody downloads exact same checked bytes; completed reader stays closed', t => { + const f = c1WorkflowProvider(t); + assert.throws(() => p.inspectArtifact(f.artifact, f.workflow, selected.source, f.scratch), /successful workflow/); + const file = p.acquireCurrentStage(f.options); + assert.deepEqual(fs.readFileSync(file), Buffer.from('opaque checked byte fixture')); + assert.equal(f.calls.filter(args => args.at(-1).endsWith('/zip')).length, 1); + assert.throws(() => p.acquireCurrentStage(f.options), /already exists/); + assert.equal(f.calls.filter(args => args.at(-1).endsWith('/zip')).length, 1); +}); +for (const defect of ['failed', 'cancelled', 'skipped', 'incomplete', 'old attempt', 'foreign run', 'foreign ref', + 'unknown caller', 'ambiguous producer', 'artifact ID', 'artifact name', 'artifact digest', 'expired', 'upload time', + 'missing log', 'duplicate pack', 'reordered packs', 'wrong upload', 'wrong completion', 'extra step', 'step failed', 'partial jobs']) { + test(`C1 workflow fixed current-stage rejects ${defect} before download`, t => { + const f = c1WorkflowProvider(t); + if (['failed', 'cancelled', 'skipped'].includes(defect)) f.job.conclusion = defect; + if (defect === 'incomplete') f.job.status = 'in_progress'; + if (defect === 'old attempt') f.run.run_attempt--; + if (defect === 'foreign run') f.options.artifact = {...f.artifact, run_id: 700}; + if (defect === 'foreign ref') f.run.head_branch = 'main'; + if (defect === 'unknown caller') process.env.GITHUB_JOB = 'publish'; + if (defect === 'ambiguous producer') {f.jobs.jobs.push({...f.job, id: 903}); f.jobs.total_count++;} + if (defect === 'artifact ID') f.item.id++; + if (defect === 'artifact name') f.item.name += '-other'; + if (defect === 'artifact digest') f.item.digest = `sha256:${hash('other')}`; + if (defect === 'expired') f.item.expired = true; + if (defect === 'upload time') f.item.created_at = '2026-09-10T00:00:00Z'; + if (defect === 'missing log') f.records.pop(); + if (defect === 'duplicate pack') f.records.splice(2, 0, f.records[1]); + if (defect === 'reordered packs') [f.records[1], f.records[2]] = [f.records[2], f.records[1]]; + if (defect === 'wrong upload') f.records[4].artifact_id++; + if (defect === 'wrong completion') f.records[4].stage_sha256 = hash('different'); + if (defect === 'extra step') f.job.steps.push({name: 'npm publish', number: 20}); + if (defect === 'step failed') f.job.steps[3].conclusion = 'failure'; + if (defect === 'partial jobs') f.jobs.total_count++; + assert.throws(() => p.acquireCurrentStage(f.options)); + assert.equal(f.calls.filter(args => args.at(-1).endsWith('/zip')).length, 0); + assert.deepEqual(fs.readdirSync(f.scratch), []); + }); +} +test('C1 workflow current-stage cannot accept completed toggle, caller root or verifier', t => { + const f = c1WorkflowProvider(t); + for (const key of ['allow_incomplete', 'authenticated', 'root', 'verify', 'producer']) { + assert.throws(() => p.acquireCurrentStage({...f.options, [key]: true})); + } + assert.equal(f.calls.length, 0); +}); +test('C1 workflow provenance signer independently requires live provider caller and successful admission', t => { + const f = c1WorkflowProvider(t); + process.env.GITHUB_JOB = 'paired_input_attestation'; + process.env.GITHUB_WORKFLOW_REF = `${c.REPOSITORY}/${p.WORKFLOW}@${selected.ref}`; + f.run.path = p.WORKFLOW; f.job.name = 'paired_input_admission'; f.signing.name = 'paired_input_attestation'; + const caller = p.inspectInputCaller(selected, selected.source, f.scratch); + assert.deepEqual(caller, {workflow: p.WORKFLOW, source: selected.source, run_id: 701, run_attempt: 3}); + f.job.conclusion = 'failure'; + assert.throws(() => p.inspectInputCaller(selected, selected.source, f.scratch)); + assert.equal(f.calls.filter(args => args.at(-1).endsWith('/zip')).length, 0); +}); diff --git a/npm/agentplugins/test/authoring-public-stage.test.js b/npm/agentplugins/test/authoring-public-stage.test.js new file mode 100644 index 00000000..b2ea7d28 --- /dev/null +++ b/npm/agentplugins/test/authoring-public-stage.test.js @@ -0,0 +1,836 @@ +"use strict"; + +// Pure contracts and mocked source orchestration only. No authentic custody, +// signatures, npm pack, native launch, network or qualification is tested. +const test = require("node:test"); +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const c = require("../scripts/dual-authoring-candidate"); +const inputs = require("../scripts/authoring-native-inputs"); +const stage = require("../scripts/stage-authoring-npm"); +const runtime = require("../lib/public-authoring"); +const products = ["agentplugins", "plugin-kit-ai"]; +const targets = ["darwin-amd64", "darwin-arm64", "linux-amd64", "linux-arm64", "windows-amd64", "windows-arm64"]; +const prefix = "npm/agentplugins/"; +const sha = n => n.toString(16).padStart(64, "0"); +const json = v => Buffer.from(JSON.stringify(v, null, 2) + "\n"); +const clone = v => JSON.parse(JSON.stringify(v)); +const inventory = p => ["LICENSE", "README.md", "package.json", `bin/${p}.js`, "bin/package.json", + "lib/package.json", "lib/platform.js", "lib/verifier.js", "lib/public-authoring.js", + p === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js", "scripts/package.json", + "scripts/dual-authoring-candidate.js", "public-release.json", "release-manifest.json", "native-inputs.json", "lib/public-authoring-contract.js", "lib/public-authoring-input.js"].sort(); +const assertions = ["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"]; + +function blob(bytes, mode = "100644") { + return { bytes, git_blob: crypto.createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex"), + mode, sha256: c.digest(bytes) }; +} +function fixture(version = "0.1.99") { + const input = { schema: "authoring-native-inputs/v1", identity: { + repository: "777genius/universal-agent-plugins", commit: "a".repeat(40), engine_revision: "a".repeat(40), + versions: { agentplugins: version, "plugin-kit-ai": "2.0.0" } }, + authoring_mode: "release-cli-contract-v1", asset_scope: "six-platform-pair", + candidate_sha256: sha(30), pair_marker_sha256: sha(31), products: {}, + preparation: { sha256: sha(32), artifact: { run_id: 101, run_attempt: 2, artifact_id: 301, artifact_sha256: sha(33) } }, + producer: { workflow: ".github/workflows/agentplugins-release.yml", source: "a".repeat(40), run_id: 201, run_attempt: 3 } }; + const manifests = {}; + products.forEach((product, pi) => { + const v = input.identity.versions[product]; + const p = input.products[product] = { tag: pi ? "v2.0.0" : `agentplugins-v${v}`, + manifest_sha256: sha(40 + pi), checksums_sha256: sha(50 + pi), assets: {} }; + targets.forEach((target, ti) => { + const extension = target.startsWith("windows-") ? ".exe" : ""; + const binary = { file: product + extension, sha256: sha(1 + pi * 6 + ti), size: 100 + ti }; + p.assets[target] = { file: `${product}_${v}_${target.replace("-", "_")}${pi ? ".tar.gz" : extension}`, + sha256: pi ? sha(60 + ti) : binary.sha256, size: pi ? 200 + ti : binary.size, binary }; + }); + manifests[product] = json({ schema_version: 3, status: "CANDIDATE", product, + repository: input.identity.repository, tag: p.tag, version: v, commit: input.identity.commit, + engine_revision: input.identity.engine_revision, versions: clone(input.identity.versions), + candidate_sha256: input.candidate_sha256, authoring_mode: input.authoring_mode, asset_scope: input.asset_scope, + assets: clone(p.assets), release_eligible: false, platform_acceptance: false, attested: false }); + p.manifest_sha256 = c.digest(manifests[product]); + p.checksums_sha256 = c.digest(Buffer.from([...Object.values(p.assets).map(a => `${a.sha256} ${a.file}`), + `${p.manifest_sha256} release-manifest.json`].join("\n") + "\n")); + }); + const source = Object.fromEntries(stage.STAGE_ALLOWLIST.map(n => [n, blob(Buffer.from(`unit source: ${n}\n`), + n.includes("/bin/") ? "100755" : "100644")])); + for (const p of products) { + source[`npm/${p}/package.json`] = blob(json({ name: inputs.PACKAGES[p], version: "0.0.0-development", + description: `unit metadata ${p}`, license: "Apache-2.0", homepage: "https://example.invalid/unit", + repository: { type: "git", url: "https://example.invalid/unit.git" }, keywords: ["preserve", p], + engines: { node: p === "agentplugins" ? ">=22" : ">=18" }, publishConfig: { access: "public" }, + files: ["old-file"], bin: { [p]: `bin/${p}.js` }, + scripts: p === "agentplugins" ? { test: "node --test" } : { postinstall: "node ./lib/install.js" } })); + } + return { input, source, manifests, inputBytes: inputs.encodeInputs(input) }; +} +function stageFixture() { + const f = fixture(); + const pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + const value = { schema: "dual-authoring-public-stage/v1", identity: clone(f.input.identity), + authoring_mode: f.input.authoring_mode, asset_scope: f.input.asset_scope, candidate_sha256: f.input.candidate_sha256, + pair_marker_sha256: f.input.pair_marker_sha256, + projection_pins: Object.fromEntries(products.map(p => [p, { + manifest_sha256: f.input.products[p].manifest_sha256, checksums_sha256: f.input.products[p].checksums_sha256 }])), + native_inputs: { sha256: c.digest(f.inputBytes), artifact: { run_id: 201, run_attempt: 3, + artifact_id: 401, artifact_sha256: sha(71) } }, + wrapper_blobs: Object.fromEntries(Object.entries(f.source).map(([n, { bytes, ...pin }]) => [n, pin])), + generated: Object.fromEntries(products.map(p => [p, Object.fromEntries(inventory(p).map(n => [n, c.digest(pair[p][n])]))])), + packs: Object.fromEntries(products.map((p, i) => [p, { file: `${inputs.PACKAGES[p]}-${f.input.identity.versions[p]}.tgz`, + sha256: sha(80 + i), size: 300 + i, integrity: "sha512-" + Buffer.alloc(64, i + 1).toString("base64"), + shasum: (90 + i).toString(16).padStart(40, "0") }])), + tools: Object.fromEntries(["node", "npm", "git", "tar", "gh"].map((n, i) => [n, { version: `unit-${i}.0`, sha256: sha(100 + i) }])), + producer: { workflow: ".github/workflows/agentplugins-npm-publish.yml", source: f.input.identity.commit, + ref: `refs/tags/agentplugins-v${f.input.identity.versions.agentplugins}`, run_id: 501, run_attempt: 1 }, + assertions: Object.fromEntries(assertions.map(n => [n, true])) }; + return { ...f, pair, value }; +} +function objectPaths(v, prefix = []) { + return [prefix, ...Object.entries(v).flatMap(([k, child]) => + child && typeof child === "object" ? objectPaths(child, [...prefix, k]) : [])]; +} +const at = (v, keys) => keys.reduce((obj, key) => obj[key], v); +function reverse(v) { + return v && typeof v === "object" ? Object.fromEntries(Object.entries(v).reverse().map(([k, x]) => [k, reverse(x)])) : v; +} +function rejectStage(f, mutate) { + const v = clone(f.value); mutate(v); + assert.throws(() => stage.encodeStage(v, f.inputBytes)); + assert.throws(() => stage.decodeStage(json(v), f.inputBytes)); +} + +test("C1 pure pair: exact two-product closure, metadata and identical I/runtime bytes", () => { + const f = fixture(), pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + assert.deepEqual(Object.keys(pair), products); + for (const p of products) { + assert.deepEqual(Object.keys(pair[p]).sort(), inventory(p)); + const pkg = JSON.parse(pair[p]["package.json"]), base = JSON.parse(f.source[`npm/${p}/package.json`].bytes); + assert.deepEqual(pkg, { ...base, version: f.input.identity.versions[p], private: false, files: inventory(p) }); + const d = inputs.decodeDescriptor(pair[p]["public-release.json"], f.inputBytes, p); + assert.equal(d.input_binding.sha256, c.digest(f.inputBytes)); + assert.deepEqual(pair[p]["release-manifest.json"], f.manifests[p]); + assert.deepEqual(pair[p]["native-inputs.json"], f.inputBytes); + for (const n of ["LICENSE", "README.md", `bin/${p}.js`, "lib/platform.js", + p === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js"]) { + assert.deepEqual(pair[p][n], f.source[`npm/${p}/${n}`].bytes); + } + for (const dir of ["bin", "lib", "scripts"]) assert.deepEqual(pair[p][`${dir}/package.json`], json({ type: "commonjs" })); + for (const claim of ["qualification", "signed_subject", "self_sha256", "stage_sha256", "execution_sha256"]) { + assert.equal(Object.hasOwn(d, claim), false); + assert.equal(Object.hasOwn(JSON.parse(pair[p]["native-inputs.json"]), claim), false); + } + } + for (const n of [...stage.COMMON, "native-inputs.json"]) { + assert.deepEqual(pair.agentplugins[n], pair["plugin-kit-ai"][n]); + assert.notStrictEqual(pair.agentplugins[n], pair["plugin-kit-ai"][n]); + } +}); + +test("C1 pure pair: caller bytes and objects unchanged and returned buffers independently owned", () => { + const f = fixture(), before = json(f); + const pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + assert.deepEqual(json(f), before); + for (const p of products) for (const bytes of Object.values(pair[p])) bytes.fill(0); + assert.deepEqual(json(f), before); + const fresh = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + assert.deepEqual(fresh.agentplugins["native-inputs.json"], f.inputBytes); +}); + +test("C1 pure pair: validate both manifests, selected hashes, assets and checksum pins", () => { + for (const p of products) { + for (const mutate of [m => m.product = "wrong", m => m.commit = "b".repeat(40), m => m.version = "2.0.1", + m => m.assets["linux-amd64"].binary.sha256 = sha(500), m => m.release_eligible = true, + m => m.qualification = null, m => delete m.attested]) { + const f = fixture(), m = JSON.parse(f.manifests[p]); mutate(m); f.manifests[p] = json(m); + f.input.products[p].manifest_sha256 = c.digest(f.manifests[p]); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, inputs.encodeInputs(f.input))); + } + for (const key of ["manifest_sha256", "checksums_sha256"]) { + const f = fixture(); f.input.products[p][key] = sha(500); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, inputs.encodeInputs(f.input)), /hash/); + } + for (const replacement of [null, "{}", new Uint8Array([123, 125]), Buffer.alloc(0), Buffer.alloc(1024 * 1024 + 1)]) { + const f = fixture(); f.manifests[p] = replacement; + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes), /Buffer/); + } + const f = fixture(); f.manifests[p] = Buffer.from(f.manifests[p].toString().trim()); + f.input.products[p].manifest_sha256 = c.digest(f.manifests[p]); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, inputs.encodeInputs(f.input)), /projection/); + } + for (const field of ["agentplugins", "plugin-kit-ai", "extra"]) { + const f = fixture(); + if (field === "extra") f.manifests.extra = Buffer.from("extra"); else delete f.manifests[field]; + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes)); + } +}); + +test("C1 pure pair: exact source inventory and byte/pin/metadata contracts", () => { + for (const name of stage.STAGE_ALLOWLIST) { + const f = fixture(); delete f.source[name]; + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes)); + } + const path = "npm/plugin-kit-ai/package.json"; + for (const mutate of [s => s.extra = s[path], s => s[path].sha256 = sha(900), + s => s[path].git_blob = "b".repeat(40), s => s[path].mode = "120000", s => s[path].bytes = "{}", + s => s[path].bytes = new Uint8Array([1]), s => s[path].bytes = Buffer.alloc(0), + s => s[path].accepted = true]) { + const f = fixture(); mutate(f.source); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes)); + } + for (const p of products) for (const mutate of [b => b.name = "other", b => b.engines.node = ">=16", + b => b.scripts.postinstall = "run something", b => b.bin[p] = "wrong.js", b => b.bin.other = "bin/other.js"]) { + const f = fixture(), n = `npm/${p}/package.json`, base = JSON.parse(f.source[n].bytes); mutate(base); + f.source[n] = blob(json(base)); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes)); + } + for (const body of [Buffer.from("null\n"), Buffer.from("[]\n"), Buffer.from([0xff]), Buffer.from("{broken")]) { + const f = fixture(); f.source[path] = blob(body); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes)); + } +}); + +test("C1 pure pair: valid 1 MiB I still fails descriptor overflow before touching source", () => { + const base = fixture("1.0.0").inputBytes.length; + const n = Math.floor((inputs.MAX_INPUT_BYTES - base) / 8); + const f = fixture("1" + "0".repeat(n) + ".0.0"); + const remaining = inputs.MAX_INPUT_BYTES - f.inputBytes.length; + f.input.preparation.artifact.run_id = Number("1" + "0".repeat(2 + remaining)); + f.inputBytes = inputs.encodeInputs(f.input); + assert.equal(f.inputBytes.length, inputs.MAX_INPUT_BYTES); + assert.deepEqual(inputs.decodeInputs(f.inputBytes), f.input); + let reads = 0; + const source = new Proxy({}, { ownKeys() { reads++; throw new Error("source touched"); } }); + assert.throws(() => stage.pairedPackageFiles(source, f.manifests, f.inputBytes), /bounded Buffer/); + assert.equal(reads, 0); +}); + +test("C1 pure pair: both descriptor boundaries, including a pair where only agent overflows", () => { + // Agent's npm package spelling makes its descriptor nine bytes larger. + const f = fixture("1.0.0"), small = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + const overhead = small.agentplugins["public-release.json"].length; + const large = fixture("1" + "0".repeat(inputs.MAX_DESCRIPTOR_BYTES - overhead) + ".0.0"); + const d = JSON.parse(small.agentplugins["public-release.json"]); + d.identity = large.input.identity; d.release_manifest_sha256 = large.input.products.agentplugins.manifest_sha256; + d.input_binding.sha256 = c.digest(large.inputBytes); + assert.equal(inputs.encodeDescriptor(d, large.inputBytes, "agentplugins").length, inputs.MAX_DESCRIPTOR_BYTES); + const kit = JSON.parse(small["plugin-kit-ai"]["public-release.json"]); + kit.identity = large.input.identity; kit.release_manifest_sha256 = large.input.products["plugin-kit-ai"].manifest_sha256; + kit.input_binding.sha256 = c.digest(large.inputBytes); + assert.equal(inputs.encodeDescriptor(kit, large.inputBytes, "plugin-kit-ai").length, inputs.MAX_DESCRIPTOR_BYTES - 9); + const pair = stage.pairedPackageFiles(large.source, large.manifests, large.inputBytes); + assert.equal(pair.agentplugins["public-release.json"].length, inputs.MAX_DESCRIPTOR_BYTES); + assert.equal(pair["plugin-kit-ai"]["public-release.json"].length, inputs.MAX_DESCRIPTOR_BYTES - 9); + const over = fixture(large.input.identity.versions.agentplugins.replace("1", "10")); + kit.identity = over.input.identity; kit.release_manifest_sha256 = over.input.products["plugin-kit-ai"].manifest_sha256; + kit.input_binding.sha256 = c.digest(over.inputBytes); + assert.equal(inputs.encodeDescriptor(kit, over.inputBytes, "plugin-kit-ai").length, inputs.MAX_DESCRIPTOR_BYTES - 8); + assert.throws(() => stage.pairedPackageFiles(over.source, over.manifests, over.inputBytes), /bounded Buffer/); +}); + +test("C1 pure S: canonical two-product roundtrip, explicit inventory and no mutation", () => { + const f = stageFixture(), before = json(f.value), encoded = stage.encodeStage(f.value, f.inputBytes); + assert.deepEqual(encoded, before); + assert.deepEqual(stage.decodeStage(encoded, f.inputBytes), f.value); + assert.deepEqual(stage.encodeStage(reverse(f.value), f.inputBytes), encoded); + assert.throws(() => stage.decodeStage(json(reverse(f.value)), f.inputBytes), /noncanonical/); + assert.deepEqual(Object.keys(f.value), ["schema", "identity", "authoring_mode", "asset_scope", "candidate_sha256", + "pair_marker_sha256", "projection_pins", "native_inputs", "wrapper_blobs", "generated", "packs", "tools", "producer", "assertions"]); + assert.deepEqual(Object.keys(f.value.assertions), assertions); + const decoded = stage.decodeStage(encoded, f.inputBytes); decoded.identity.versions.agentplugins = "9.0.0"; + assert.deepEqual(json(f.value), before); + assert.deepEqual(stage.decodeStage(encoded, f.inputBytes), f.value); +}); + +test("C1 pure S: every object rejects missing, unknown, hidden, inherited and accessor fields", () => { + const f = stageFixture(); + for (const path of objectPaths(f.value)) { + for (const key of Object.keys(at(f.value, path))) rejectStage(f, v => { delete at(v, path)[key]; }); + rejectStage(f, v => { at(v, path).unexpected = true; }); + for (const value of [null, [], "object", 1, false]) { + if (path.length) rejectStage(f, v => { at(v, path.slice(0, -1))[path.at(-1)] = value; }); + else assert.throws(() => stage.encodeStage(value, f.inputBytes)); + } + for (const add of [o => Object.defineProperty(o, "trusted", { value: true }), + o => o[Symbol("trusted")] = true, o => Object.setPrototypeOf(o, { accepted: true })]) { + const v = clone(f.value); add(at(v, path)); assert.throws(() => stage.encodeStage(v, f.inputBytes)); + } + const v = clone(f.value), o = at(v, path), key = Object.keys(o)[0]; + let calls = 0; + Object.defineProperty(o, key, { enumerable: true, get() { calls++; return true; } }); + assert.throws(() => stage.encodeStage(v, f.inputBytes)); assert.equal(calls, 0); + } +}); + +test("C1 pure S: fixed I identity, projection, artifact attempt, workflow/source/ref bindings", () => { + const f = stageFixture(); + for (const mutate of [v => v.schema = "dual-authoring-public-preparation/v1", v => v.authoring_mode = "vertical-slice-v1", + v => v.asset_scope = "host-pair", v => v.identity.repository = "fork/repo", v => v.identity.commit = "b".repeat(40), + v => v.identity.engine_revision = "b".repeat(40), v => v.identity.versions.agentplugins = "0.1.98", + v => v.identity.versions["plugin-kit-ai"] = "2.0.1", v => v.candidate_sha256 = sha(999), v => v.pair_marker_sha256 = sha(999), + v => v.native_inputs.sha256 = sha(999), v => v.native_inputs.artifact.run_id++, v => v.native_inputs.artifact.run_attempt++, + v => v.producer.workflow = inputs.WORKFLOW, v => v.producer.source = "b".repeat(40), + v => v.producer.ref = "refs/heads/main", v => v.producer.ref = "refs/tags/v2.0.0"]) rejectStage(f, mutate); + for (const p of products) for (const k of ["manifest_sha256", "checksums_sha256"]) { + rejectStage(f, v => { v.projection_pins[p][k] = sha(999); }); + } + for (const mutate of [i => i.preparation.sha256 = sha(999), i => i.preparation.artifact.artifact_id++, + i => i.products["plugin-kit-ai"].checksums_sha256 = sha(999), i => i.pair_marker_sha256 = sha(999), + i => i.producer.run_attempt++]) { + const input = clone(f.input); mutate(input); const bytes = inputs.encodeInputs(input); + assert.throws(() => stage.encodeStage(f.value, bytes)); + assert.throws(() => stage.decodeStage(json(f.value), bytes)); + } + for (const bad of [null, f.input, f.inputBytes.toString(), json(reverse(f.input)), Buffer.alloc(0)]) { + assert.throws(() => stage.encodeStage(f.value, bad)); + assert.throws(() => stage.decodeStage(json(f.value), bad)); + assert.throws(() => stage.pairedPackageFiles(f.source, f.manifests, bad)); + } +}); + +test("C1 pure S: exact generated set and source/I/manifest/descriptor/shared scope digests", () => { + const f = stageFixture(); + for (const p of products) { + for (const n of inventory(p).filter(n => n !== "package.json")) rejectStage(f, v => { v.generated[p][n] = sha(999); }); + for (const n of ["../escape", "qualification.json", "authoring-promotion.json", "completion.json", "extra"]) { + rejectStage(f, v => { v.generated[p][n] = sha(999); }); + } + rejectStage(f, v => { v.generated[p]["public-release.json"] = f.value.generated[products.find(x => x !== p)]["public-release.json"]; }); + } + rejectStage(f, v => { v.wrapper_blobs[prefix + "lib/verifier.js"].sha256 = sha(999); }); + rejectStage(f, v => { v.wrapper_blobs[prefix + "lib/verifier.js"].mode = "120000"; }); +}); + +test("C1 pure S: digest syntax, positive safe IDs, attempts, tarball names and canonical SRI", () => { + const f = stageFixture(); + const digestPaths = objectPaths(f.value).flatMap(p => Object.keys(at(f.value, p)) + .filter(k => k.endsWith("sha256") || k === "git_blob" || k === "shasum").map(k => [...p, k])); + for (const path of digestPaths) { + const n = at(f.value, path).length; + for (const bad of [null, 1, "0".repeat(n), "A".repeat(n), "f".repeat(n - 1), "f".repeat(n) + "\n"]) { + rejectStage(f, v => { at(v, path.slice(0, -1))[path.at(-1)] = bad; }); + } + } + for (const path of [["producer", "run_id"], ["producer", "run_attempt"], ["native_inputs", "artifact", "artifact_id"], + ...products.map(p => ["packs", p, "size"])]) { + for (const bad of [0, -1, 1.5, "1", true, null, Number.MAX_SAFE_INTEGER + 1]) { + rejectStage(f, v => { at(v, path.slice(0, -1))[path.at(-1)] = bad; }); + } + } + rejectStage(f, v => v.producer.run_attempt = 1001); + for (const p of products) { + rejectStage(f, v => v.packs[p].size = inputs.MAX_NATIVE_BYTES + 1); + for (const file of ["../package.tgz", `${p}-9.0.0.tgz`, "package.tar.gz", null]) rejectStage(f, v => v.packs[p].file = file); + const good = f.value.packs[p].integrity; + for (const integrity of [null, "sha256-" + good.slice(7), good + "\n", good.slice(0, -1), good + " sha512-other", + "sha512-" + "A".repeat(85) + "B=="]) rejectStage(f, v => v.packs[p].integrity = integrity); + } + for (const path of [["producer", "run_id"], ["native_inputs", "artifact", "artifact_id"]]) { + const v = clone(f.value); at(v, path.slice(0, -1))[path.at(-1)] = Number.MAX_SAFE_INTEGER; + assert.deepEqual(stage.decodeStage(stage.encodeStage(v, f.inputBytes), f.inputBytes), v); + } + const v = clone(f.value); v.producer.run_attempt = 1000; + assert.deepEqual(stage.decodeStage(stage.encodeStage(v, f.inputBytes), f.inputBytes), v); +}); + +test("C1 pure S: closed tools and assertions; syntax does not establish authenticated truth", () => { + const f = stageFixture(); + for (const name of ["node", "npm", "git", "tar", "gh"]) { + for (const version of [null, 123, "", " ", "version\n", "a\0b"]) rejectStage(f, v => v.tools[name].version = version); + rejectStage(f, v => v.tools[name].path = "/usr/bin/tool"); + } + for (const name of assertions) for (const bad of [false, null, 1, "true"]) rejectStage(f, v => v.assertions[name] = bad); + for (const name of ["qualification", "attested", "release_eligible", "platform_acceptance", "execution", "self_sha256", + "artifact", "workflow", "verifier", "authenticatedInputs"]) rejectStage(f, v => v[name] = true); + // No bytes here establish these unobservable assertions or hash claims. A + // different well-formed pack digest can pass this codec; only readStage with + // retained packs/authenticated custody may validate it in subsequent C1. + const v = clone(f.value); v.packs.agentplugins.sha256 = sha(999); + assert.deepEqual(stage.decodeStage(stage.encodeStage(v, f.inputBytes), f.inputBytes), v); + assert.equal(typeof stage.readStage, "function"); + assert.equal(typeof stage.stagePrepublication, "function"); +}); + +test("C1 pure S: canonical UTF-8, duplicate keys, nesting, spelling and exact 1 MiB boundary", () => { + const f = stageFixture(), body = stage.encodeStage(f.value, f.inputBytes), text = body.toString(); + for (const bad of [text, new Uint8Array(body), null, Buffer.alloc(0), Buffer.alloc(1024 * 1024 + 1), + Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), body]), Buffer.concat([body, Buffer.from([0xff])]), + Buffer.from(text.trim()), Buffer.from(text + "\n"), Buffer.from(text + "{}"), + Buffer.from(text.replace('"schema":', '"schema": null, "schema":')), + Buffer.from(text.replace('"schema"', '"sch\\u0065ma"')), + Buffer.from(text.replace('"run_id": 501', '"run_id": 5.01e2')), + Buffer.from(text.replaceAll("\n", "\r\n")), Buffer.from('{"x":'.repeat(5) + '0' + '}'.repeat(5))]) { + assert.throws(() => stage.decodeStage(bad, f.inputBytes)); + } + const v = clone(f.value); + v.tools.node.version += "x".repeat(1024 * 1024 - body.length); + const exact = stage.encodeStage(v, f.inputBytes); assert.equal(exact.length, 1024 * 1024); + assert.deepEqual(stage.decodeStage(exact, f.inputBytes), v); + v.tools.node.version += "x"; + assert.throws(() => stage.encodeStage(v, f.inputBytes), /bounded Buffer/); + assert.throws(() => stage.decodeStage(json(v), f.inputBytes), /bounded Buffer/); +}); + +test("C1 pure v1 regression: same descriptor, metadata, null/private and all other bytes", () => { + const f = fixture(), pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + for (const p of products) { + const v1 = stage.packageFiles(p, f.source, f.manifests[p], { identity: f.input.identity, manifestDigest: f.input.candidate_sha256 }); + assert.deepEqual(Object.keys(v1).sort(), inventory(p).filter(n => !["native-inputs.json", "lib/public-authoring-contract.js", "lib/public-authoring-input.js"].includes(n))); + assert.deepEqual(v1["public-release.json"], json({ schema: "dual-authoring-public-npm/v1", product: p, + npm_package: inputs.PACKAGES[p], identity: f.input.identity, authoring_mode: "release-cli-contract-v1", + asset_scope: "six-platform-pair", candidate_sha256: f.input.candidate_sha256, + release_manifest_sha256: c.digest(f.manifests[p]), qualification: null })); + const base = JSON.parse(f.source[`npm/${p}/package.json`].bytes); + assert.deepEqual(v1["package.json"], json({ ...base, version: f.input.identity.versions[p], private: true, + files: inventory(p).filter(n => !["native-inputs.json", "lib/public-authoring-contract.js", "lib/public-authoring-input.js"].includes(n)) })); + for (const n of Object.keys(v1).filter(n => !["package.json", "public-release.json"].includes(n))) assert.deepEqual(v1[n], pair[p][n]); + } +}); + +test("C1 pure inventories: separate exact stage additions and unchanged legacy exports", () => { + const own = p => ["LICENSE", "README.md", "package.json", `bin/${p}.js`, "lib/platform.js", + p === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js"].map(n => `npm/${p}/${n}`); + assert.deepEqual(stage.COMMON, ["lib/verifier.js", "lib/public-authoring.js", "scripts/dual-authoring-candidate.js"]); + const legacy = [...stage.COMMON.map(n => prefix + n), ...products.flatMap(own), + ...["stage-authoring-npm.js", "stage-dual-authoring-npm.js", "stage-dual-authoring-candidate.js", "authoring-release.js"] + .map(n => prefix + "scripts/" + n)]; + assert.deepEqual(stage.ALLOWLIST, legacy); + assert.deepEqual(stage.STAGE_ALLOWLIST, [...legacy, + prefix + "lib/public-authoring-contract.js", prefix + "lib/public-authoring-input.js", + ...["authoring-native-inputs.js", "authoring-promotion.js", "authoring-native-qualification.js", "platform-proof.js", + "npm-public-contract.js"].map(n => prefix + "scripts/" + n), "scripts/read-authoring-evidence-zip.py", + ".github/workflows/agentplugins-release.yml", ".github/workflows/agentplugins-npm-publish.yml"]); + assert.ok(Object.isFrozen(stage.STAGE_ALLOWLIST)); + assert.deepEqual(Object.keys(stage), ["prepare", "packageFiles", "ALLOWLIST", "COMMON", + "encodeStage", "decodeStage", "pairedPackageFiles", "STAGE_ALLOWLIST", "stagePrepublication", "readStage", "validateUnsignedStage", "main"]); +}); + +test("C1 pure runtime regression: loadRelease accepts structural v2 and rejects v1/null using only in-memory reads", t => { + const f = fixture(), pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + const { memory } = require("./public-authoring-v2.test"); + for (const p of products) { + for (const v2 of [true, false]) { + const files = v2 ? pair[p] : stage.packageFiles(p, f.source, f.manifests[p], { identity: f.input.identity, manifestDigest: f.input.candidate_sha256 }); + const pkg = JSON.parse(files["package.json"]); pkg.bugs = { url: "https://example.invalid/unit" }; + if (p === "agentplugins") { pkg.os = ["darwin", "linux", "win32"]; pkg.cpu = ["x64", "arm64"]; } + files["package.json"] = json(pkg); + const m = memory(t, files); + t.mock.method(c, "readFile", file => files[file.slice("/unit-package/".length)]); + if (v2) assert.deepEqual(runtime.loadRelease(p, m.root, "linux-amd64").asset, f.input.products[p].assets["linux-amd64"]); + else assert.throws(() => runtime.loadRelease(p, m.root, "linux-amd64"), /not qualified: preparation package/); + assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + } +}); + +// SOURCE orchestration fixtures: every acquisition/signature/pack/tool seam is +// mocked. Only fresh os.tmpdir roots receive files; none is authentic admission. +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const cp = require("node:child_process"); +const Module = require("node:module"); +const packing = require("../scripts/stage-dual-authoring-npm"); +const promotion = require("../scripts/authoring-promotion"); +function fixtureEnv(t, name, value) { + const prior = process.env[name]; process.env[name] = value; + t.after(() => { if (prior === undefined) delete process.env[name]; else process.env[name] = prior; }); +} +function integrationFixture(t, hook = () => {}) { + const f = fixture(), base = fs.mkdtempSync(path.join(os.tmpdir(), "c1-stage-integration-")); + const paths = Object.fromEntries(["repo", "scratch", "tools", "incoming"].map(n => { + const dir = path.join(base, n); fs.mkdirSync(dir); return [n, dir]; + })); + const put = (root, n, b, mode = 0o644) => { + const file = path.join(root, n); fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, b, { mode }); return file; + }; + const fixedFiles = { "native-inputs.json": f.inputBytes, "preparation-run.json": Buffer.from("receipt fixture"), + "candidate-identity.json": Buffer.from("metadata fixture"), "candidate/candidate.json": Buffer.from("candidate fixture"), + "pair-prepared.json": Buffer.from("pair fixture") }; + for (const p of products) { + fixedFiles[`${p}/release-manifest.json`] = f.manifests[p]; + fixedFiles[`${p}/checksums.txt`] = Buffer.from("checksum fixture"); + for (const a of Object.values(f.input.products[p].assets)) fixedFiles[`${p}/${a.file}`] = Buffer.from(`NOT NATIVE: ${p}/${a.file}`); + } + for (const [n, b] of Object.entries(fixedFiles)) put(paths.incoming, n, b); + const calls = [], options = { input: Buffer.from(f.inputBytes), selected: { tag: f.input.products.agentplugins.tag, + ref: `refs/tags/${f.input.products.agentplugins.tag}`, source: f.input.identity.commit, versions: clone(f.input.identity.versions) }, + workflow_sha: f.input.identity.commit, artifact: { run_id: 201, run_attempt: 3, artifact_id: 401, artifact_sha256: sha(71) }, + repo: paths.repo, workParent: paths.scratch, node: put(paths.tools, "node", Buffer.from("node fixture")), + npm: put(paths.tools, "npm", Buffer.from("npm fixture")), output: path.join(base, "output"), + producer: { workflow: ".github/workflows/agentplugins-npm-publish.yml", source: f.input.identity.commit, + ref: `refs/tags/${f.input.products.agentplugins.tag}`, run_id: 501, run_attempt: 4 } }; + for (const [k, v] of Object.entries({ GITHUB_ACTIONS: "true", GITHUB_REPOSITORY: c.REPOSITORY, + GITHUB_SHA: options.producer.source, GITHUB_REF: options.producer.ref, GITHUB_RUN_ID: "501", GITHUB_RUN_ATTEMPT: "4", + GITHUB_WORKFLOW_SHA: options.producer.source, + GITHUB_WORKFLOW_REF: `${c.REPOSITORY}/${options.producer.workflow}@${options.producer.ref}` })) fixtureEnv(t, k, v); + const originalRead = c.readFile; + const fixedTools = new Set(["/usr/bin/git", "/usr/bin/tar", "/usr/bin/gh", fs.realpathSync("/usr/bin/python3"), process.execPath]); + t.mock.method(c, "readFile", (file, max) => fixedTools.has(file) ? Buffer.from(`tool fixture: ${file}`) : originalRead(file, max)); + t.mock.method(cp, "execFileSync", (exe, args) => { + calls.push(["tool", exe, args]); assert.deepEqual(args.at(-1), "--version"); + return Buffer.from(exe === "/usr/bin/gh" ? `gh version ${promotion.GH_VERSION} (fixture)\n` : "fixture version\n"); + }); + t.mock.method(cp, "spawnSync", () => { throw new Error("unexpected external process"); }); + const event = (name, data) => { calls.push([name, data]); hook(name, data, { f, paths, options, calls, put }); }; + t.mock.method(packing, "blobs", (repo, commit, env, closure) => { + assert.equal(repo, paths.repo); assert.equal(commit, f.input.identity.commit); assert.equal(closure, "stage"); + assert.equal(env.PATH, "/usr/local/bin:/usr/bin:/bin"); event("blobs"); + return Object.fromEntries(Object.entries(f.source).map(([n, pin]) => [n, { ...pin, bytes: Buffer.from(pin.bytes) }])); + }); + t.mock.method(promotion, "checkInputTags", body => { assert.deepEqual(body, f.inputBytes); event("tags"); }); + t.mock.method(promotion, "inspectArtifact", (pin, workflow, source) => { + assert.equal(source, f.input.identity.commit); event("inspect", { pin, workflow }); return clone({ pin, workflow, source }); + }); + t.mock.method(promotion, "inspectStageCaller", () => { event("stage-caller"); return clone(options.producer); }); + t.mock.method(promotion, "checkStageEvidence", () => { event("stage-evidence"); return {fixture_only: "ordered provider transcript"}; }); + t.mock.method(promotion, "inspectCurrentStage", o => { event("current-custody", o); return {fixture_only: clone(o.artifact)}; }); + t.mock.method(promotion, "acquireCurrentStage", o => { + event("acquire-current", o); return put(o.scratch, `artifact-${o.artifact.artifact_id}.zip`, Buffer.from("checked current ZIP fixture")); + }); + t.mock.method(promotion, "acquireArtifact", (pin, workflow, source, cwd) => { + assert.equal(workflow, options.producer.workflow); assert.equal(source, f.input.identity.commit); + event("acquire-stage", pin); return put(cwd, `artifact-${pin.artifact_id}.zip`, Buffer.from("checked ZIP interface fixture")); + }); + t.mock.method(promotion, "extractArtifact", (file, pin, kind, names, output, cwd) => { + assert.equal(file, path.join(cwd, `artifact-${pin.artifact_id}.zip`)); assert.equal(kind, "public-stage"); + assert.deepEqual(names, ["completion.json", ...products.map(p => `${inputs.PACKAGES[p]}-${f.input.identity.versions[p]}.tgz`)]); + fs.mkdirSync(output); for (const n of names) put(output, n, fs.readFileSync(path.join(options.output, n))); + event("extract-stage", output); return output; + }); + t.mock.method(promotion, "verifyStageSubject", (file, expected) => { + assert.equal(expected.workflow_sha, f.input.identity.commit); assert.equal(expected.source, f.input.identity.commit); + assert.equal(expected.ref, options.producer.ref); assert.equal(expected.run_id, 501); assert.equal(expected.run_attempt, 4); + assert.equal(expected.subjects.length, 3); assert.equal(c.digest(originalRead(file)), expected.sha256); + event("signer", { file, expected }); + }); + const adapter = { ...inputs, readInputs(o) { + assert.deepEqual(o.input, f.inputBytes); assert.deepEqual(o.artifact, options.artifact); + assert.deepEqual(o.selected, options.selected); assert.equal(o.workflow_sha, f.input.identity.commit); + event("read-I", o); return { root: paths.incoming, input: inputs.decodeInputs(o.input), subjects: [] }; + }, inputSubjects(root, body) { + assert.deepEqual(body, f.inputBytes); + for (const [n, b] of Object.entries(fixedFiles)) assert.deepEqual(originalRead(path.join(root, n)), b, `input fixture changed: ${n}`); + event("input-snapshot", root); + return Object.keys(fixedFiles).filter(n => !["preparation-run.json", "candidate-identity.json"].includes(n)) + .map(n => ({ file: path.join(root, n), sha256: c.digest(fixedFiles[n]) })); + } }; + // Override existing external imports in a test-only module instance. Production + // exports/options contain no injection API; the accepted frozen I exports stay frozen. + const filename = require.resolve("../scripts/stage-authoring-npm"), loaded = new Module(filename, module); + loaded.filename = filename; loaded.paths = Module._nodeModulePaths(path.dirname(filename)); + const normalRequire = loaded.require.bind(loaded); + loaded.require = name => name === "./authoring-native-inputs" ? adapter : normalRequire(name); + loaded._compile(fs.readFileSync(filename, "utf8"), filename); + const api = loaded.exports, pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + t.mock.method(packing, "packPackage", (product, files, root, o, context) => { + assert.deepEqual(files, pair[product]); assert.equal(o.node, options.node); assert.equal(o.npm, options.npm); + assert.equal(context.env.npm_config_ignore_scripts, "true"); assert.equal(context.env.npm_config_offline, "true"); + const body = Buffer.from(`RETAINED PACK FIXTURE: ${product}`), file = `${inputs.PACKAGES[product]}-${f.input.identity.versions[product]}.tgz`; + put(o.output, file, body); event("pack", { product, files, root }); + return { file, ...c.metadata(body), integrity: "sha512-" + crypto.createHash("sha512").update(body).digest("base64") }; + }); + t.mock.method(packing, "verifyPack", (file, files, dest) => { + const p = file.includes("universal-agent-plugins-") ? "agentplugins" : "plugin-kit-ai"; + assert.deepEqual(files, pair[p]); assert.ok(dest.startsWith(paths.scratch + path.sep)); event("verify-pack", { file, files, dest }); + }); + const readOptions = () => ({ input: Buffer.from(f.inputBytes), selected: clone(options.selected), workflow_sha: options.workflow_sha, + artifact: { run_id: 501, run_attempt: 4, artifact_id: 601, artifact_sha256: sha(80) }, repo: paths.repo, + workParent: paths.scratch, node: options.node, npm: options.npm, + stage_sha256: c.digest(fs.readFileSync(path.join(options.output, "completion.json"))) }); + return { ...f, base, paths, options, api, calls, put, readOptions, pair }; +} + +test("C1 stage integration producer and reader agree on I, three subjects and retained SHA1 without reader repack", t => { + const f = integrationFixture(t), record = f.api.stagePrepublication(f.options); + assert.deepEqual(stage.decodeStage(fs.readFileSync(path.join(f.options.output, "completion.json")), f.inputBytes), record); + assert.equal(f.calls.filter(x => x[0] === "pack").length, 2); + assert.deepEqual(f.calls.filter(x => x[0] === "pack").map(x => x[1].product), products); + assert.ok(f.calls.findIndex(x => x[0] === "read-I") < f.calls.findIndex(x => x[0] === "pack")); + for (const p of products) { + const bytes = fs.readFileSync(path.join(f.options.output, record.packs[p].file)); + assert.equal(record.packs[p].shasum, crypto.createHash("sha1").update(bytes).digest("hex")); + assert.equal(record.packs[p].sha256, c.digest(bytes)); assert.equal(record.packs[p].size, bytes.length); + } + const result = f.api.readStage(f.readOptions()); assert.deepEqual(result.record, record); + assert.equal(result.subjects.length, 3); assert.equal(f.calls.filter(x => x[0] === "signer").length, 3); + assert.equal(f.calls.filter(x => x[0] === "read-I").length, 2); + assert.equal(f.calls.filter(x => x[0] === "pack").length, 2); + const signer = f.calls.findIndex(x => x[0] === "signer"), readerI = f.calls.findLastIndex(x => x[0] === "read-I"); + assert.ok(signer < readerI); +}); + +test("C1 stage integration malformed options fail before scratch, authentication or packing", t => { + const f = integrationFixture(t); + for (const mutate of [o => o.trusted = true, o => o.input = null, o => o.input = Buffer.from("{}\n"), + o => o.selected.source = "b".repeat(40), o => o.selected.versions.agentplugins = "9.0.0", + o => o.selected.ref = "refs/heads/main", o => o.workflow_sha = "b".repeat(40), + o => o.artifact.run_attempt++, o => o.artifact.artifact_sha256 = "0".repeat(64), + o => o.producer.workflow = inputs.WORKFLOW, o => o.producer.source = "b".repeat(40), + o => o.producer.ref = "refs/tags/v2.0.0", o => o.producer.run_id = 201, o => o.producer.run_attempt = 1001, + o => o.output = o.repo, o => o.workParent = o.repo, o => o.node = "relative", o => o.verifier = () => true]) { + const o = { ...clone(f.options), input: Buffer.from(f.inputBytes) }; mutate(o); + assert.throws(() => f.api.stagePrepublication(o)); assert.equal(f.calls.length, 0); + assert.deepEqual(fs.readdirSync(f.paths.scratch), []); assert.equal(fs.existsSync(f.options.output), false); + } + for (const mutate of [o => o.stage_sha256 = null, o => o.artifact.run_attempt = 1001, o => o.selected.ref = "refs/heads/main", + o => o.authenticated = true]) { + const { output, producer, ...o } = { ...clone(f.options), input: Buffer.from(f.inputBytes), stage_sha256: sha(92) }; + mutate(o); assert.throws(() => f.api.readStage(o)); assert.equal(f.calls.length, 0); + } +}); + +test("C1 stage integration producer binds actual workflow caller before effects", t => { + const f = integrationFixture(t); fixtureEnv(t, "GITHUB_WORKFLOW_SHA", "b".repeat(40)); + assert.throws(() => f.api.stagePrepublication(f.options), /workflow caller/); assert.equal(f.calls.length, 0); +}); + +for (const defect of ["auth", "source-before", "source-after", "input", "I", "snapshot", "tool", "caller", "arguments", "half-pair", + "second-modified", "first-late", "verified-late", "generated-late", "generated", "generated-mode", "generated-extra", "provider", "collision"]) { + test(`C1 stage integration producer rejects ${defect} with no new accepted S`, t => { + let fired = false, blobs = 0; + const f = integrationFixture(t, (event, data, state) => { + const { options, paths, put } = state; + if (event === "blobs") blobs++; + if (defect === "auth" && event === "read-I") throw new Error("fixture authentication rejection"); + if ((defect === "source-before" && event === "blobs" && blobs === 1) || + (defect === "source-after" && event === "blobs" && blobs === 3)) throw new Error("source closure changed fixture"); + if (event === "pack" && data.product === "plugin-kit-ai" && !fired) { + fired = true; + if (defect === "input") put(paths.incoming, "pair-prepared.json", Buffer.from("changed input")); + if (defect === "I") put(paths.incoming, "native-inputs.json", Buffer.from("changed I")); + if (defect === "snapshot") { + const snap = state.calls.find(x => x[0] === "input-snapshot" && x[1].endsWith("stage-inputs"))[1]; + // Do not overwrite sealed fixture files: introduce an unexpected entry; + // the mocked existing input interface detects it in the hook below. + put(snap, "unexpected", Buffer.from("changed snapshot")); + } + if (defect === "tool") fs.appendFileSync(options.npm, "changed"); + if (defect === "caller") options.selected.source = "b".repeat(40); + if (defect === "arguments") { + const prior = process.execArgv; process.execArgv = ["--changed-fixture"]; t.after(() => { process.execArgv = prior; }); + } + if (defect === "half-pair") throw new Error("failed second pack fixture"); + if (defect === "second-modified") fs.appendFileSync(path.join(options.output, "plugin-kit-ai-2.0.0.tgz"), "changed"); + if (defect === "first-late") fs.appendFileSync(path.join(options.output, `universal-agent-plugins-${f.input.identity.versions.agentplugins}.tgz`), "changed"); + if (defect === "generated") fs.appendFileSync(path.join(data.root, "README.md"), "changed"); + if (defect === "generated-mode") fs.chmodSync(path.join(data.root, "README.md"), 0o755); + if (defect === "generated-extra") put(data.root, "unexpected", Buffer.from("extra")); + if (defect === "collision") put(options.output, "completion.json", Buffer.from("existing owner bytes")); + } + if (event === "verify-pack" && data.file.endsWith("plugin-kit-ai-2.0.0.tgz")) { + if (defect === "verified-late") fs.appendFileSync(path.join(options.output, `universal-agent-plugins-${f.input.identity.versions.agentplugins}.tgz`), "late"); + if (defect === "generated-late") fs.appendFileSync(path.join(options.output, "agentplugins/README.md"), "late"); + } + if (defect === "snapshot" && event === "input-snapshot" && fired && data.endsWith("stage-inputs")) { + assert.ok(fs.existsSync(path.join(data, "unexpected"))); throw new Error("changed snapshot fixture"); + } + if (defect === "provider" && event === "inspect" && fired) throw new Error("changed completed attempt fixture"); + }); + assert.throws(() => f.api.stagePrepublication(f.options)); + const marker = path.join(f.options.output, "completion.json"); + if (defect === "collision") assert.equal(fs.readFileSync(marker, "utf8"), "existing owner bytes"); + else assert.equal(fs.existsSync(marker), false); + assert.ok(f.calls.filter(x => x[0] === "pack").length <= 2); + }); +} + +for (const defect of ["digest", "attempt", "signature", "source-pins", "generated", "S-I", "I-custody", "input-change", "source-change", + "tarball", "late-tarball", "late-S", "caller-change", "S-canonical", "stage-provider"]) { + test(`C1 stage integration reader rejects ${defect} without repacking`, t => { + let reading = false, verified = 0, readerOptions, readerRoot; + const f = integrationFixture(t, (event, data, state) => { + if (!reading) return; + if (event === "extract-stage") readerRoot = data; + if (defect === "signature" && event === "signer") throw new Error("fixed signer rejected fixture"); + if (defect === "I-custody" && event === "read-I") throw new Error("I custody rejected fixture"); + if (event === "verify-pack") { + verified++; + if (verified === 2) { + if (defect === "late-tarball") fs.appendFileSync(path.join(readerRoot, `universal-agent-plugins-${f.input.identity.versions.agentplugins}.tgz`), "late"); + if (defect === "late-S") fs.appendFileSync(path.join(readerRoot, "completion.json"), "late"); + if (defect === "input-change") state.put(state.paths.incoming, "candidate-identity.json", Buffer.from("late input")); + if (defect === "caller-change") readerOptions.selected.source = "b".repeat(40); + } + } + if (defect === "source-change" && event === "blobs" && verified === 2) throw new Error("reader source changed fixture"); + if (defect === "stage-provider" && event === "inspect" && verified === 2) throw new Error("completed stage changed fixture"); + }); + const record = f.api.stagePrepublication(f.options); + const marker = path.join(f.options.output, "completion.json"); + // Producer completion is read-only. Reader mutations use a distinct owned + // artifact fixture, never chmod/overwrite that completion or prior receipts. + if (["source-pins", "generated", "S-I", "S-canonical"].includes(defect)) { + const original = promotion.extractArtifact; + t.mock.method(promotion, "extractArtifact", (...args) => { + const output = original(...args), changed = clone(record); + if (defect === "source-pins") changed.wrapper_blobs[prefix + "scripts/authoring-promotion.js"].sha256 = sha(991); + if (defect === "generated") changed.generated.agentplugins["package.json"] = sha(992); + if (defect === "S-I") changed.native_inputs.sha256 = sha(993); + const bytes = defect === "S-canonical" ? Buffer.from(JSON.stringify(changed)) : json(changed); + fs.writeFileSync(path.join(output, "completion.json"), bytes); readerOptions.stage_sha256 = c.digest(bytes); + return output; + }); + } + readerOptions = f.readOptions(); + if (defect === "digest") readerOptions.stage_sha256 = sha(999); + if (defect === "attempt") readerOptions.artifact.run_attempt++; + if (defect === "tarball") fs.appendFileSync(path.join(f.options.output, record.packs.agentplugins.file), "changed pack"); + reading = true; assert.throws(() => f.api.readStage(readerOptions)); + assert.equal(f.calls.filter(x => x[0] === "pack").length, 2); + assert.deepEqual(fs.readFileSync(marker), stage.encodeStage(record, f.inputBytes)); + }); +} + +test("C1 stage integration existing blobs checks every committed, checkout and executing entry including mode and HEAD", t => { + const f = fixture(), root = fs.mkdtempSync(path.join(os.tmpdir(), "c1-stage-blobs-")); + const executing = path.resolve(__dirname, "../../.."), normalRead = c.readFile, normalStat = fs.lstatSync; + let changed, absent, changedMode, head = f.input.identity.commit, reads = [], commands = []; + t.mock.method(cp, "execFileSync", (exe, args, options) => { + assert.equal(exe, "/usr/bin/git"); assert.equal(options.cwd, root); commands.push(args); + if (args[0] === "rev-parse") return Buffer.from(head + "\n"); + if (args[0] === "ls-tree") { + const n = args.at(-1), pin = f.source[n]; + return Buffer.from(n === absent ? "" : `${pin.mode} blob ${pin.git_blob}\t${n}\0`); + } + assert.equal(args[0], "cat-file"); + return Object.values(f.source).find(pin => pin.git_blob === args.at(-1)).bytes; + }); + t.mock.method(c, "readFile", (file, max) => { + const base = file.startsWith(root + path.sep) ? root : file.startsWith(executing + path.sep) ? executing : null; + const n = base && path.relative(base, file); + if (!n || !Object.hasOwn(f.source, n)) return normalRead(file, max); + reads.push(file); return file === changed ? Buffer.from("changed fixture") : Buffer.from(f.source[n].bytes); + }); + t.mock.method(fs, "lstatSync", (...args) => { + const file = args[0], base = file.startsWith(root + path.sep) ? root : file.startsWith(executing + path.sep) ? executing : null; + const n = base && path.relative(base, file); + if (n && Object.hasOwn(f.source, n)) return { mode: file === changedMode ? 0o600 : f.source[n].mode === "100755" ? 0o755 : 0o644 }; + return normalStat(...args); + }); + assert.deepEqual(packing.blobs(root, head, {}, "stage"), f.source); + for (const n of stage.STAGE_ALLOWLIST) for (const base of [root, executing]) assert.ok(reads.includes(path.join(base, n))); + for (const n of stage.STAGE_ALLOWLIST) { + absent = n; assert.throws(() => packing.blobs(root, head, {}, "stage"), /required regular Git blob missing/); absent = undefined; + for (const base of [root, executing]) { + changed = path.join(base, n); assert.throws(() => packing.blobs(root, head, {}, "stage"), /differs from committed/); changed = undefined; + changedMode = path.join(base, n); assert.throws(() => packing.blobs(root, head, {}, "stage"), /differs from committed/); changedMode = undefined; + } + } + head = "b".repeat(40); commands = []; + assert.throws(() => packing.blobs(root, f.input.identity.commit, {}, "stage"), /checkout HEAD/); + assert.equal(commands.length, 1); +}); + +test("C1 stage integration existing pack engine validates entries, modes, bytes, response SRI and SHA1 without receipt changes", t => { + const f = fixture(), pair = stage.pairedPackageFiles(f.source, f.manifests, f.inputBytes); + const root = fs.mkdtempSync(path.join(os.tmpdir(), "c1-stage-pack-engine-")); + let current, flaw, count = 0; + const output = path.join(root, "output"); fs.mkdirSync(output); + const context = { root: path.join(root, "verify"), env: { PATH: "/usr/local/bin:/usr/bin:/bin" } }; fs.mkdirSync(context.root); + t.mock.method(cp, "execFileSync", (exe, args) => { + if (exe === "/fixture-node") { + assert.deepEqual(args, ["/fixture-npm", "pack", "--ignore-scripts", "--offline", "--json", "--pack-destination", output]); + count++; + const bytes = Buffer.from(`PACK INTERFACE ${current} ${count}`), name = inputs.PACKAGES[current], version = f.input.identity.versions[current]; + const filename = `${name}-${version}.tgz`; fs.writeFileSync(path.join(output, filename), bytes); + const row = { id: `${name}@${version}`, name, version, filename, size: bytes.length, + integrity: "sha512-" + crypto.createHash("sha512").update(bytes).digest("base64"), + shasum: crypto.createHash("sha1").update(bytes).digest("hex") }; + if (flaw === "SRI") row.integrity = "sha512-" + Buffer.alloc(64, 1).toString("base64"); + if (flaw === "SHA1") row.shasum = "a".repeat(40); + if (flaw === "identity") row.name = "wrong-product"; + return json(count % 2 ? [row] : { [name]: row }); + } + assert.equal(exe, "/usr/bin/tar"); + const entries = inventory(current).map(n => "package/" + n); + if (args[0] === "-tzf") return Buffer.from([...entries, ...(flaw === "entries" ? ["package/extra"] : [])].join("\n") + "\n"); + if (args[0] === "-tvzf") return Buffer.from(entries.map(n => + (flaw === "mode" ? "lrwxrwxrwx " : /^package\/bin\/[^/]+\.js$/.test(n) ? "-rwxr-xr-x " : "-rw-r--r-- ") + n).join("\n") + "\n"); + assert.equal(args[0], "-xzf"); const destination = args[args.indexOf("-C") + 1]; + for (const [n, b] of Object.entries(pair[current])) { + const file = path.join(destination, "package", n); fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, flaw === "bytes" && n === "README.md" ? Buffer.from("wrong bytes") : b, + { mode: /^bin\/[^/]+\.js$/.test(n) ? 0o755 : 0o644 }); + } + return Buffer.alloc(0); + }); + for (const product of products) for (const defect of [null, "SRI", "SHA1", "identity", "entries", "mode", "bytes"]) { + current = product; flaw = defect; + // verifyPack uses exclusive extraction directories; every trial owns a new context. + context.root = fs.mkdtempSync(path.join(root, "trial-")); + const run = () => packing.packPackage(product, pair[product], root, + { node: "/fixture-node", npm: "/fixture-npm", output, identity: f.input.identity }, context); + if (defect) assert.throws(run); + else { + const packed = run(); assert.deepEqual(Object.keys(packed), ["file", "sha256", "size", "integrity"]); + assert.equal(packed.sha256, c.digest(fs.readFileSync(path.join(output, packed.file)))); + } + } + assert.equal(count, 14); +}); + +function workflowTransport(f, options, basename) { + const {input, ...rest} = options; + const input_file = f.put(f.base, `${basename}-I.json`, input); + return f.put(f.base, `${basename}.json`, c.encode({...rest, input_file})); +} +test('C1 workflow CLI producer emits closed result and unsigned validator never packs or checks S signatures', t => { + const f = integrationFixture(t); + const produced = f.api.main(['--stage-prepublication', workflowTransport(f, f.options, 'produce')]); + assert.deepEqual(Object.keys(produced), ['root', 'record', 'subjects', 'stage_sha256']); + assert.equal(produced.subjects.length, 3); assert.equal(f.calls.filter(([n]) => n === 'pack').length, 2); + const result = f.api.main(['--validate-unsigned-stage', workflowTransport(f, f.readOptions(), 'unsigned')]); + assert.deepEqual(result.record, produced.record); assert.equal(result.subjects.length, 3); + assert.equal(f.calls.filter(([n]) => n === 'pack').length, 2); + assert.equal(f.calls.filter(([n]) => n === 'signer').length, 0); + assert.ok(f.calls.some(([n]) => n === 'read-I')); assert.ok(f.calls.some(([n]) => n === 'stage-evidence')); + const completed = f.api.main(['--read-stage', workflowTransport(f, f.readOptions(), 'completed')]); + assert.deepEqual(completed.record, result.record); + assert.equal(f.calls.filter(([n]) => n === 'signer').length, 3); + assert.equal(f.calls.filter(([n]) => n === 'pack').length, 2); +}); +for (const defect of ['current custody', 'I signature', 'operation evidence', 'completion.json', 'agent pack', 'kit pack', 'generated pin']) { + test(`C1 workflow unsigned validator rejects ${defect} without pack or S signing`, t => { + let reading = false; + const f = integrationFixture(t, (name) => { + if (reading && ((defect === 'I signature' && name === 'read-I') || + (defect === 'operation evidence' && name === 'stage-evidence') || + (defect === 'current custody' && name === 'current-custody'))) throw Error(defect); + }); + f.api.stagePrepublication(f.options); const options = f.readOptions(); reading = true; + if (defect.endsWith('pack')) { + const product = defect === 'agent pack' ? 'agentplugins' : 'plugin-kit-ai'; + fs.appendFileSync(path.join(f.options.output, `${inputs.PACKAGES[product]}-${f.input.identity.versions[product]}.tgz`), 'changed'); + } + if (defect === 'completion.json' || defect === 'generated pin') { + const original = c.readFile; + const retained = fs.readFileSync(path.join(f.options.output, 'completion.json')); + const record = JSON.parse(retained); record.generated.agentplugins['README.md'] = sha(19); + const changed = defect === 'completion.json' ? Buffer.concat([retained, Buffer.from('changed')]) : c.encode(record); + if (defect === 'generated pin') options.stage_sha256 = c.digest(changed); + t.mock.method(c, 'readFile', (file, max) => path.basename(file) === 'completion.json' ? changed : original(file, max)); + } + assert.throws(() => f.api.validateUnsignedStage(options)); + assert.equal(f.calls.filter(([n]) => n === 'pack').length, 2); + assert.equal(f.calls.filter(([n]) => n === 'signer').length, 0); + }); +} +test('C1 workflow stage CLI rejects object/path transport, unknown operations and input mutation', t => { + const f = integrationFixture(t), file = workflowTransport(f, f.options, 'options'); + for (const args of [['--unknown', file], ['--read-stage', file, file], ['--read-stage', 'relative']]) assert.throws(() => f.api.main(args)); + const options = JSON.parse(fs.readFileSync(file)); options.input_file = {type: 'Buffer', data: [1]}; + fs.writeFileSync(file, c.encode(options)); assert.throws(() => f.api.main(['--stage-prepublication', file])); + assert.equal(f.calls.length, 0); +}); +test('C1 workflow second pack failure cannot emit completion transcript or S', t => { + const transcript = []; + t.mock.method(process.stderr, 'write', chunk => {transcript.push(String(chunk)); return true;}); + const f = integrationFixture(t, (name, data) => {if (name === 'pack' && data.product === 'plugin-kit-ai') throw Error('second pack failed');}); + assert.throws(() => f.api.stagePrepublication(f.options), /second pack/); + assert.equal(fs.existsSync(path.join(f.options.output, 'completion.json')), false); + assert.equal(transcript.filter(line => line.includes('"operation":"completion"')).length, 0); +}); diff --git a/npm/agentplugins/test/npm-public-contract.test.js b/npm/agentplugins/test/npm-public-contract.test.js index 031278b9..881de637 100644 --- a/npm/agentplugins/test/npm-public-contract.test.js +++ b/npm/agentplugins/test/npm-public-contract.test.js @@ -2,6 +2,7 @@ const assert = require("node:assert/strict"); const crypto = require("node:crypto"); +const cp = require("node:child_process"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -11,6 +12,7 @@ const { validateAuditSignatures, validateDownloadedTarball, validatePackJSON, + validateProductPackJSON, validatePublicMetadata, validateSLSAAttestation } = require("../scripts/npm-public-contract"); @@ -128,7 +130,7 @@ test("public npm metadata and downloaded pack bind the staged package identity", /publisher identity is not GitHub Actions/ ); const root = fs.mkdtempSync(path.join(os.tmpdir(), "npm-public-contract-")); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); + t.after(() => cp.execFileSync("rm", ["-r", "--", root])); fs.writeFileSync(path.join(root, value.pack[0].filename), value.body); assert.equal( validateDownloadedTarball(value.pack, root, value.version, value.integrity, value.shasum), @@ -141,7 +143,7 @@ test("npm 12 object-shaped pack JSON preserves the exact package identity", (t) const npm12 = { "universal-agent-plugins": structuredClone(value.pack[0]) }; assert.equal(validatePackJSON(npm12, value.version), npm12["universal-agent-plugins"]); const root = fs.mkdtempSync(path.join(os.tmpdir(), "npm-public-contract-npm12-")); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); + t.after(() => cp.execFileSync("rm", ["-r", "--", root])); fs.writeFileSync(path.join(root, value.pack[0].filename), value.body); assert.equal( validateDownloadedTarball(npm12, root, value.version, value.integrity, value.shasum), @@ -276,7 +278,7 @@ test("public npm metadata fails closed for every reviewed identity field", () => test("downloaded public npm bytes and pack JSON reject staged digest mismatches", (t) => { const value = fixture(); const root = fs.mkdtempSync(path.join(os.tmpdir(), "npm-public-contract-negative-")); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); + t.after(() => cp.execFileSync("rm", ["-r", "--", root])); fs.writeFileSync(path.join(root, value.pack[0].filename), Buffer.from("tampered")); assert.throws( () => validateDownloadedTarball(value.pack, root, value.version, value.integrity, value.shasum), @@ -289,3 +291,175 @@ test("downloaded public npm bytes and pack JSON reject staged digest mismatches" /pack identity/ ); }); + +const products = { agentplugins: "universal-agent-plugins", "plugin-kit-ai": "plugin-kit-ai" }; +const responseForms = { + array: record => [record], + object: record => ({ [record.name]: record }) +}; +function productRecord(product) { + const record = fixture().pack[0]; + record.name = products[product]; + record.filename = `${record.name}-${record.version}.tgz`; + return record; +} + +for (const product of Object.keys(products)) for (const [form, wrap] of Object.entries(responseForms)) { + test(`C1 fixed pack ${product} ${form}: identity, shape and digest syntax`, () => { + const record = productRecord(product); + // npm carries additional informational fields; these are not a new schema. + record.files = [{ path: "package.json", size: 123, mode: 420 }]; + assert.equal(validateProductPackJSON(wrap(record), product, record.version), record); + if (product === "agentplugins") assert.equal(validatePackJSON(wrap(record), record.version), record); + else assert.throws(() => validatePackJSON(wrap(record), record.version)); + for (const mutation of [ + x => { x.name = "lookalike"; }, + x => { x.version = "1.2.4"; }, + x => { x.filename = "../" + x.filename; }, + x => { x.filename = x.filename.replace("1.2.3", "1.2.4"); }, + x => { x.integrity = undefined; }, + x => { x.integrity = 42; }, + x => { x.integrity = "sha256-" + "a".repeat(86) + "=="; }, + x => { x.integrity += "\n"; }, + x => { x.integrity = "sha512-" + "A".repeat(85) + "B=="; }, + x => { x.shasum = undefined; }, + x => { x.shasum = 42; }, + x => { x.shasum = "A".repeat(40); }, + x => { x.shasum = "a".repeat(39); }, + x => { x.shasum += "\n"; } + ]) { + const bad = structuredClone(record); mutation(bad); + assert.throws(() => validateProductPackJSON(wrap(bad), product, record.version)); + } + for (const value of [null, false, "pack", [], [record, record], [null], {}, + { lookalike: record }, { [record.name]: record, extra: record }, + { [record.name]: [record] }]) { + assert.throws(() => validateProductPackJSON(value, product, record.version)); + } + for (const version of [null, 123, "01.2.3", "1.2.3-beta.1", "1.2.3+build", "1.2.3\n"]) { + assert.throws(() => validateProductPackJSON(wrap(record), product, version)); + } + for (const unknown of ["universal-agent-plugins", "other", "toString", null]) { + assert.throws(() => validateProductPackJSON(wrap(record), unknown, record.version), /unknown fixed npm product/); + } + }); +} + +test("C1 blob checks bind the helper while preserving both historical receipt inventories", t => { + const packing = require("../scripts/stage-dual-authoring-npm"); + const publicPacking = require("../scripts/stage-authoring-npm"); + const c = require("../scripts/dual-authoring-candidate"); + const repo = path.resolve(__dirname, "../../.."); + const helper = "npm/agentplugins/scripts/npm-public-contract.js"; + const commit = "a".repeat(40); + const readFile = c.readFile; + let fault; + const requested = []; + // Simulated committed-source interface, not an accepted Git successor. + t.mock.method(cp, "execFileSync", (exe, args) => { + assert.equal(exe, "/usr/bin/git"); + if (args[0] === "rev-parse") return Buffer.from(commit + "\n"); + if (args[0] === "ls-tree") { + const name = args.at(-1); requested.push(name); + if (fault === "missing" && name === helper) return Buffer.from(""); + const bytes = fs.readFileSync(path.join(repo, name)); + const hash = crypto.createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex"); + bodies.set(hash, bytes); + return Buffer.from(`100644 blob ${hash}\t${name}\0`); + } + assert.equal(args[0], "cat-file"); return bodies.get(args[2]); + }); + const bodies = new Map(); + t.mock.method(c, "readFile", (file, ...rest) => fault === "dirty" && file === path.join(repo, helper) ? + Buffer.from("changed executing helper") : readFile(file, ...rest)); + for (const [closure, allowlist] of [["private", packing.ALLOWLIST], ["public", publicPacking.ALLOWLIST]]) { + assert.equal(allowlist.includes(helper), false); + const source = packing.blobs(repo, commit, {}, closure); + assert.deepEqual(Object.keys(source), [...allowlist]); + assert.ok(requested.includes(helper)); + for (const name of allowlist) assert.deepEqual(source[name].bytes, fs.readFileSync(path.join(repo, name))); + for (fault of ["missing", "dirty"]) { + assert.throws(() => packing.blobs(repo, commit, {}, closure), /required regular Git blob missing|executing stager differs/); + } + fault = undefined; + assert.throws(() => packing.blobs(repo, "b".repeat(40), {}, closure), /checkout HEAD/); + } +}); + +// Explicit offline tool provision only. Retain small fixtures under TMPDIR for +// evidence; never import the native-building private-npm fixture suite. +test("C1 packPackage real offline packs: both forms/products, same receipts, rejection before completion", { + skip: !process.env.UAP_C1_PACK_NPM +}, t => { + const packing = require("../scripts/stage-dual-authoring-npm"); + const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "c1-pack-consumer-")); + const npm = process.env.UAP_C1_PACK_NPM; + assert.ok(path.isAbsolute(npm)); + const execFile = cp.execFileSync; + const actualRecords = {}, productBytes = {}; + for (const product of Object.keys(products)) for (const [form, wrap] of Object.entries(responseForms)) { + const base = path.join(scratch, `${product}-${form}`); fs.mkdirSync(base); + const context = packing.npmContext(base); + const root = path.join(base, "package"); fs.mkdirSync(root); + const output = path.join(base, "output"); fs.mkdirSync(output); + const files = { "package.json": Buffer.from(JSON.stringify({ + name: products[product], version: "1.2.3", private: true, + scripts: { prepack: "exit 91", prepare: "exit 92", postpack: "exit 93", postinstall: "exit 94" } + }) + "\n"), "README.md": Buffer.from("Offline fixed pack consumer fixture.\n") }; + for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(root, name), body, { mode: 0o644 }); + let calls = 0; + const mock = t.mock.method(cp, "execFileSync", (exe, args, options) => { + if (exe !== process.execPath || args[0] !== npm) return execFile(exe, args, options); + calls++; + assert.deepEqual(args.slice(1), ["pack", "--ignore-scripts", "--offline", "--json", "--pack-destination", output]); + const response = JSON.parse(execFile(exe, args, options)); + const record = validateProductPackJSON(response, product, "1.2.3"); + actualRecords[product] = record; + return Buffer.from(JSON.stringify(wrap(record))); + }); + const options = { node: process.execPath, npm, output, identity: { versions: { [product]: "1.2.3" } } }; + const receipt = packing.packPackage(product, files, root, options, context); + mock.mock.restore(); + assert.equal(calls, 1); + const body = fs.readFileSync(path.join(output, receipt.file)); + assert.deepEqual(receipt, { file: `${products[product]}-1.2.3.tgz`, + sha256: crypto.createHash("sha256").update(body).digest("hex"), size: body.length, + integrity: "sha512-" + crypto.createHash("sha512").update(body).digest("base64") }); + if (productBytes[product]) assert.deepEqual(body, productBytes[product]); + productBytes[product] = body; + assert.equal(actualRecords[product].shasum, crypto.createHash("sha1").update(body).digest("hex")); + assert.deepEqual(fs.readFileSync(path.join(context.root, product, "package/README.md")), files["README.md"]); + // Same bytes and real response, with one field corrupted. No extraction or + // downstream completion is allowed even when the other digest is correct. + for (const field of ["name", "version", "filename", "integrity", "shasum"]) { + const bad = { ...actualRecords[product], [field]: field === "integrity" ? + "sha512-" + Buffer.alloc(64).toString("base64") : field === "shasum" ? "0".repeat(40) : "wrong" }; + let tarCalls = 0; + const rejection = t.mock.method(cp, "execFileSync", (exe, args) => { + if (exe === process.execPath && args[0] === npm) return Buffer.from(JSON.stringify(wrap(bad))); + tarCalls++; throw new Error("unexpected downstream tool"); + }); + const marker = path.join(output, "completion.json"); + assert.throws(() => { + const pack = packing.packPackage(product, files, root, options, context); + packing.completeRecord(output, { pack }); + }, /package identity|package-named record|differs from actual pack/); + rejection.mock.restore(); + assert.equal(tarCalls, 0); assert.equal(fs.existsSync(marker), false); + } + fs.writeFileSync(path.join(output, receipt.file), Buffer.from("changed tarball bytes")); + const changed = t.mock.method(cp, "execFileSync", (exe, args) => { + assert.equal(exe, process.execPath); assert.equal(args[0], npm); + return Buffer.from(JSON.stringify(wrap(actualRecords[product]))); + }); + assert.throws(() => packing.packPackage(product, files, root, options, context), /integrity differs from actual pack/); + changed.mock.restore(); + } +}); + +test("C1 packPackage rejects unknown products before invoking npm", t => { + const packing = require("../scripts/stage-dual-authoring-npm"); + const run = t.mock.method(cp, "execFileSync", () => { throw new Error("unexpected tool"); }); + assert.throws(() => packing.packPackage("toString", {}, "", {}, {}), /unknown fixed npm product/); + assert.equal(run.mock.callCount(), 0); +}); 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..967c6376 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-acceptance.test.js @@ -0,0 +1,574 @@ +"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, { mode: 0o755 }); + for (const [name, bytes] of Object.entries(a.generatedFiles(lane))) { + const file = path.join(dir, name); fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o755 }); + fs.writeFileSync(file, bytes, { mode: 0o644 }); + } + } + } + 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); for (const [relative, bytes] of Object.entries(a.evidenceFiles(name, value))) { const target = path.join(journeyRoot, relative); fs.mkdirSync(path.dirname(target), { recursive: true }); write(target, bytes); } 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(); } +} +// Every value below is explicitly SYNTHETIC. No pack, native process, scanner, +// custody or observer implementation is executed by these semantic fixtures. +function semanticFixture(f) { + const j = f.j, commands = a.commandContract(j.cell), roots = a.rootsFor(f.root, j.cell); + const snapshots = f.evidence['projects.json']; + const assessment = status => ({ status, finding_ids: [] }); + const rows = commands.map(w => { + const productVersion = w.id === 'product-version' && w.product === 'agentplugins'; + const args = w.argv.slice(w.product === 'agentplugins' && w.author ? 1 : 0); + const operation = w.id === 'retired-v1' ? 'author' : args[0] === '--help' ? 'author' : `author.${args[0]}${args[0] === 'skills' ? '.' + args[1] : ''}`; + const mutation = ['author.init', 'author.skills.init'].includes(operation), committed = /\/(init|extra-skill)$/.test(w.id); + const project = w.lane && !w.id.endsWith('/existing') && w.id !== 'installer-flag' && !w.id.startsWith('installer/'); + const malformed = w.id === 'malformed-skill'; + const data = Object.fromEntries(['compatibility', 'toolchain', 'loadability', 'normative_conformance', 'host_safety', 'authoring_readiness', 'release_policy', 'runtime_evidence'].map(k => [k, assessment('not_evaluated')])); + Object.assign(data, { schema: 'agentplugins-authoring-report/v1', engine: 'standard-first-slice/1', revision: j.identity.commit, command: operation, + mode: mutation ? 'local_mutation' : 'read', identity: { scope_algorithm: '', read_profile: '', tree_exclusions: null }, + coverage: { components_requested: !!project, skills_enumerated: !!project, inventory_complete: !!project, tree_complete: !!project, + plugin: project ? 'pass' : 'not_evaluated', mcp: project && w.lane !== 'skill' ? 'pass' : 'not_evaluated', skills: project ? 'pass' : 'not_evaluated', filesystem: project ? 'pass' : 'not_evaluated', facts_complete: !!project }, + profiles: project ? a.PROFILES : [], schema_ids: project ? a.PROFILES.slice(w.lane === 'skill' ? 2 : 2, w.lane === 'skill' ? 3 : 4).map(x => x.id).sort() : [], + findings: [], components: project ? a.componentFacts(w.lane, !w.id.endsWith('/init'), malformed) : [], checks: [], committed, + affected_paths: committed ? ['plugin.json'] : [], authoring_schema_version: 1, engine_version: 'standard-first-slice/1', + requested: { operation, mode: mutation ? 'local_mutation' : 'read' }, effects: { attempted: !!project, committed }, next_actions: [] }); + if (project) { + const manifest = snapshots[w.product].entries.find(x => x.path === `${w.lane}/plugin.json`); + data.identity = { scope_algorithm: 'agentplugins-captured-input-sha256-v1', scope_digest: 'sha256:' + H(w.lane), tree_algorithm: 'agentplugins-tree-sha256-v1', + tree_digest: a.generatedTreeIdentity(snapshots[w.product].entries, w.lane, j.cell), manifest_digest: 'sha256:' + manifest.sha256, + read_profile: `packageview-local-${a.matrix.find(c => c.key === j.cell).target.split('-')[0]}-v1`, tree_exclusions: ['root .git', 'root non-directory .plugin-kit-ai.lock'] }; + for (const k of ['loadability', 'normative_conformance', 'host_safety', 'authoring_readiness']) data[k] = assessment(malformed && ['normative_conformance', 'authoring_readiness'].includes(k) ? 'fail' : 'pass'); + data.inspection = { name: w.lane, version: '0.1.0', schema: a.PROFILES[2].id, components: data.components.map(x => + ({ id: x.id, type: x.type, name: x.id === 'sha256:' + H('skill:extra-skill') ? 'extra-skill' : w.lane })) }; + } + if (w.id.endsWith('/doctor')) data.toolchain = assessment(w.lane === 'skill' ? 'pass' : 'not_evaluated'); + if (w.id.endsWith('/compat')) { + data.compatibility = assessment('pass'); data.clients = ['claude', 'codex'].map((client_id, i) => { + const counts = { skill: 0, mcp_server: 0 }; + return { client_id, capabilities: a.clientFacts().find(c => c.client_id === client_id), components: data.components.map(x => ({ kind: x.type === 'skill' ? 'skill' : 'mcp_server' })) + .sort((a, b) => a.kind.localeCompare(b.kind)).map(x => ({ ...x, index: ++counts[x.kind], support: 'projected' })), + limitations: ['static_adapter_support_only', 'installation_not_checked', 'authentication_not_checked', 'runtime_not_checked', 'client_version_not_checked', 'catalog_publication_not_checked', ...(i ? ['manual_activation_required'] : [])] }; + }); + } + if (w.id.endsWith('/test')) data.checks = [['portable_configuration', 'pass'], ['package_hygiene', 'pass'], ['static_skills', 'pass'], ['static_mcp', w.lane === 'skill' ? 'not_evaluated' : 'pass'], ['runtime', 'not_evaluated']].map(([id, status]) => ({ id, ...assessment(status) })); + if (['author-help', 'capabilities', 'engine-version', 'product-version'].includes(w.id)) data.commands = a.SURFACE; + if (w.id === 'capabilities') data.capabilities = { schemas: a.PROFILES.slice(2, 4).map(({ id, digest }) => ({ id, digest })), profiles: a.PROFILES, + clients: a.clientFacts(), commands: a.SURFACE, evidence_limits: ['static_only', 'no_path_lookup', 'no_executable_version_probe', 'no_runtime_or_oauth_evidence', 'native_files_metadata_only'] }; + if (w.id === 'engine-version' || w.product === 'plugin-kit-ai' && w.id === 'product-version') Object.assign(data, { product: w.product, product_version: j.identity.versions[w.product] }); + const result = { schema_version: 1, command: productVersion ? 'version' : operation, result: w.status ? 'failure' : 'success', data: productVersion ? { version: j.identity.versions.agentplugins } : data }; + return { product: w.product, id: w.id, argv: w.argv, cwd: w.scenario === 'projects' ? j.projects[w.product] : path.join(path.dirname(j.projects[w.product]), `${w.product} malformed-skill ü`), + status: w.status, signal: null, stdout: w.id === 'product-help' ? `SYNTHETIC ${w.product} help `.repeat(10) : JSON.stringify(result), stderr: '' }; + }); + f.evidence['commands.json'] = rows; + const inventory = a.scenarioContract(j.cell), prefixes = new Map(), caches = new Map(), selected = a.matrix.find(x => x.key === j.cell); + const empty = () => Object.fromEntries(selected.products.map(p => [p, null])); + const asset = (p, name) => ({ path: a.expectedCachePath(j, roots, { cache: name, prefix: 'unused', product: p, kind: 'cold' }, p), ...Object.fromEntries(['sha256', 'size'].map(k => [k, j.subjects[p][selected.target].binary[k]])), mode: j.cell.startsWith('windows-') ? 0o666 : 0o755 }); + const pkg = (p, prefix) => ({ tree: H(p + 'tree'), shims: (j.cell.startsWith('windows-') ? ['posix', 'cmd', 'powershell'] : ['posix']).map(kind => ({ kind, + path: path.join(roots.npm, prefix, 'prefix', ...(j.cell.startsWith('windows-') ? [] : ['bin']), p + ({ posix: '', cmd: '.cmd', powershell: '.ps1' }[kind])), + sha256: H(p + 'shim'), mode: j.cell.startsWith('windows-') ? 0o666 : 0o777, target: j.cell.startsWith('windows-') ? null : `../lib/node_modules/${ic.PACKAGES[p]}/bin/${p}.js` })) }); + let projectState = H('initial-projects'); const lastMutation = inventory.cache.filter(s => s.command !== null && /\/(init|extra-skill)$/.test(commands[s.command].id)).at(-1).id; + let next = 0; const groups = new Map(); + const ref = () => ({ path: 'sidecars/synthetic-observation.json', size: 10, sha256: H('SYNTHETIC\n') }); + function observation(s) { + if (!prefixes.has(s.prefix)) prefixes.set(s.prefix, empty()); if (!caches.has(s.cache)) caches.set(s.cache, empty()); + const before = { projects: projectState, prefix: structuredClone(prefixes.get(s.prefix)), cache: structuredClone(caches.get(s.cache)), client: H('client'), state: H('state'), inputs: H('inputs') }; + const after = structuredClone(before), p = s.product; + if (s.command !== null && /\/(init|extra-skill)$/.test(commands[s.command].id)) projectState = s.id === lastMutation ? c.digest(c.encode(snapshots)) : H(s.id); + after.projects = projectState; + const npm = ['install', 'reinstall', 'uninstall'].includes(s.kind); + let acquisitions = 0, commits = 0, launches = npm ? 0 : 1; + if (['install', 'reinstall'].includes(s.kind)) after.prefix[p] = pkg(p, s.prefix); + if (s.kind === 'uninstall') after.prefix[p] = null; + if (s.kind.startsWith('invalid-') || s.kind === 'waiter-cancel') launches = 0; + else if (s.kind !== 'uninstall' && (p === 'plugin-kit-ai' || !npm)) { + if (!before.cache[p] || s.kind === 'repair') acquisitions = commits = 1; + after.cache[p] = asset(p, s.cache); + } + if (s.kind === 'concurrent-cold' && !s.id.endsWith('-0')) acquisitions = commits = 0; + if (s.kind === 'repair') before.cache[p] = { ...before.cache[p], sha256: H('C3 intentional owned cache corruption\n'), size: Buffer.byteLength('C3 intentional owned cache corruption\n') }; + if (s.kind === 'concurrent-cold' || s.kind === 'waiter-cancel') before.cache[p] = null; + const planned = a.plannedInvocation(j, s, roots), core = s.command === null ? null : rows[s.command]; + const stdout = core ? core.stdout : npm || s.kind === 'literal-argv' || s.event || s.kind.startsWith('invalid-') ? 'SYNTHETIC process output' : rows.find(r => r.product === p && r.id === 'product-version').stdout; + const row = { id: s.id, command: s.command, argv: planned.argv, cwd: planned.cwd, env: planned.env, + executable: { path: planned.argv[0], sha256: npm ? j.tools.npm_node.sha256 : H(p + 'shim') }, runtime: npm ? j.tools.npm_node : j.tools.shim_node, + stdout: { size: Buffer.byteLength(stdout), sha256: H(stdout) }, stderr: { size: 0, sha256: H('') }, status: core ? core.status : s.kind.startsWith('invalid-') ? 1 : 0, signal: null, + before, after, observation: ref(), events: !npm ? ['shim', ...(launches ? ['native'] : [])] : [], + acquisitions, commits, downloads: 0, native_launches: launches, postinstall: null, interval: [next, next + 10], + literal: s.kind === 'literal-argv' ? { root: path.join(planned.cwd, 'literal project ü'), description: a.LITERAL_DESCRIPTION, manifest_sha256: H('synthetic literal manifest') } : null }; + if (s.group) { if (!groups.has(s.group)) groups.set(s.group, next); row.interval = [groups.get(s.group), groups.get(s.group) + 10]; } next += 20; + if (s.kind === 'waiter-cancel') row.events.push('cache-waiter'); + if (s.kind === 'waiter-owner') row.events.splice(1, 0, 'lock-owner'); + if (s.kind === 'repair') row.events.splice(1, 0, 'repair-before-launch'); + if (s.kind.startsWith('invalid-')) row.events.push('locator-rejected'); + if (s.event) { row.events.push('cancel-delivered'); row.status = ({ SIGINT: 130, SIGTERM: 143, CTRL_C_EVENT: 130, TerminateProcess: 1 })[s.event]; } + row.events.push('reaped'); + if (['install', 'reinstall'].includes(s.kind) && p === 'plugin-kit-ai') row.postinstall = { argv: [j.tools.npm_node.path, './lib/install.js'], runtime: j.tools.npm_node, acquisitions, commits, observation: ref() }; + prefixes.set(s.prefix, after.prefix); caches.set(s.cache, after.cache); return row; + } + const npm = inventory.npm.map(observation), cache = inventory.cache.map(observation), installer = inventory.installer.map(observation); + // Both peer starts were cold in an overlapping group, with disjoint namespaces. + const peers = cache.filter(r => r.id.endsWith('/peer-overlap')); + for (const row of peers) for (const p of selected.products.filter(x => x !== row.id.split('/')[0])) { row.before.cache[p] = null; row.after.cache[p] = asset(p, 'peer-overlap'); } + f.evidence['npm-lifecycle.json'] = { schema: 'authoring-public-npm-lifecycle/v1', cell: j.cell, rows: npm }; + f.evidence['cache-process.json'] = { schema: 'authoring-public-cache-process/v1', cell: j.cell, rows: cache, finalization: { + rows: [...inventory.npm, ...inventory.cache, ...inventory.installer].map(s => s.id), descendants: [], locks: [], late_errors: [], observation: ref() } }; + f.evidence['installer.json'] = { schema: 'authoring-public-installer/v1', cell: j.cell, rows: installer, assessment: selected.node === 18 ? null : ref(), readbacks: inventory.installer.map(ref) }; + const sidecars = path.join(f.admission.journey_root, 'sidecars'); fs.mkdirSync(sidecars, { recursive: true }); fs.writeFileSync(path.join(sidecars, 'synthetic-observation.json'), 'SYNTHETIC\n'); + f.repin(); return f; +} +function withFacades(t, run) { + const exists = fs.existsSync, Module = require('node:module'), load = Module._load; + const api = { + 'public-authoring-custody': { readPublicInputs() { throw new Error('SYNTHETIC custody not provided'); } }, + 'public-process-observation': { openPublicObservation() { throw new Error('SYNTHETIC session not provided'); }, verifyPublicObservation({ rows, finalization }) { return { rows, finalization }; } }, + 'public-installer-evidence': { requirePublicInstaller() { return undefined; }, verifyPublicInstaller({ assessment, readbacks }) { return { assessment, readbacks }; } } + }; + fs.existsSync = file => Object.keys(api).some(n => file === path.join(repo, 'npm/agentplugins/scripts', n + '.js')) || exists(file); + Module._load = function(name, ...args) { + const key = Object.keys(api).find(n => name === path.join(repo, 'npm/agentplugins/scripts', n + '.js')); + return key ? api[key] : load.call(this, name, ...args); + }; + const restore = () => { fs.existsSync = exists; Module._load = load; }; + try { const result = run(api); if (result && typeof result.then === 'function') return result.finally(restore); restore(); return result; } catch (e) { restore(); throw e; } +} + +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, () => { + semanticFixture(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), /PUBLIC_FACADE_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, () => { + semanticFixture(f); const local = a.readJourneyInputs(f.request); + assert.throws(() => a.verifyJourney(local), /PUBLIC_FACADE_REQUIRED/); + assert.deepEqual(a.verifyResults(f.j, f.evidence), { fixed_commands: true, pair_parity: true, projects_preserved: true }); + const resultCases = [ + ['schema', d => { d.schema = 'other'; }], ['profile', d => { d.profiles[0].digest = 'sha256:' + H('wrong'); }], + ['read profile', d => { d.identity.read_profile = 'other-host'; }], ['component name', d => { d.inspection.components[0].name = 'wrong'; }], + ['component type', d => { d.inspection.components[0].type = 'wrong'; }], + ['coverage', d => { d.coverage.filesystem = 'not_evaluated'; }], + ['component omitted', d => d.components.pop()], ['readiness', d => { d.authoring_readiness.status = 'not_evaluated'; }], + ['runtime claim', d => { d.runtime_evidence.status = 'pass'; }], ['release claim', d => { d.release_policy.status = 'pass'; }], + ['diagnostic parity', d => { d.next_actions.push({ code: 'different', message: 'different' }); }], + ['unknown nested', d => { d.identity.extra = true; }] + ]; + for (const [name, mutate] of resultCases) { + const evidence = structuredClone(f.evidence), row = evidence['commands.json'].find(r => r.product === 'agentplugins' && r.id === 'skill/inspect'); + const value = JSON.parse(row.stdout); mutate(value.data); row.stdout = JSON.stringify(value); + assert.throws(() => a.verifyResults(f.j, evidence), name); t.diagnostic(name); + } + assert.throws(() => a.outputJSON('{"a":1,"a":2}'), /duplicate/); + withFacades(t, () => assert.equal(a.verifyJourney(local), local)); + + 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("PUBLIC_FACADE_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 fixed generated closure and captured identity', t => { + const f = semanticFixture(fixture(t)); + assert.ok(a.verifyResults(f.j, f.evidence).projects_preserved); + const extraRoot = structuredClone({ 'commands.json': f.evidence['commands.json'], 'projects.json': f.evidence['projects.json'] }); + for (const snapshot of Object.values(extraRoot['projects.json'])) { + snapshot.entries.push({ path: 'unexpected', kind: 'file', mode: 0o644, size: 0, sha256: H('') }); + snapshot.sha256 = c.digest(c.encode(snapshot.entries)); + } + assert.throws(() => a.verifyResults(f.j, extraRoot), /only generated lane entries/); + // Independently rendered scaffold fixtures and ordinary v1 framing pins. + const golden = {"skill": "sha256:383668d78b12a5ad868c895183a6d86b3441c6fedc833371250eecd8e40d9754", "mcp-remote": "sha256:2aeaa18441a1e66f8b31c9beee438f88205c9dd24a4d73dd3fc24ebfce50c005", "mcp-stdio": "sha256:39908919898e31f6bd170623b8f70581aa5604f21910c334466d1fc527028e39", "hybrid-remote": "sha256:66fa98b22663011eb77e8ffbe4b73f056eb94b2706fe9f9a58244d32e0935bdb", "hybrid-stdio": "sha256:1ebd460c3872e672a6378226265f89d861160f29932a0b15ae8e376a3c576f6b"}; + for (const lane of bridge.LANES) { + const entries = f.evidence['projects.json'].agentplugins.entries; + for (const cell of a.matrix) { + const hostEntries = structuredClone(entries); + if (cell.target.startsWith('windows-')) for (const e of hostEntries) e.mode = e.kind === 'directory' ? 0o777 : 0o666; + assert.equal(a.generatedTreeIdentity(hostEntries, lane, cell.key), golden[lane]); + } + for (const entry of entries.filter(e => e.path.startsWith(lane + '/'))) { + for (const mutation of ['missing', 'mode', ...(entry.kind === 'file' ? ['content'] : [])]) { + const bad = structuredClone({ 'commands.json': f.evidence['commands.json'], 'projects.json': f.evidence['projects.json'] }); + for (const snapshot of Object.values(bad['projects.json'])) { + const e = snapshot.entries.find(e => e.path === entry.path); + if (mutation === 'missing') snapshot.entries = snapshot.entries.filter(x => x.path !== e.path); + else if (mutation === 'mode') e.mode ^= 0o100; + else e.sha256 = H('symmetric changed content'); + snapshot.sha256 = c.digest(c.encode(snapshot.entries)); + } + for (const snapshot of Object.values(bad['projects.json'])) + assert.throws(() => a.generatedTreeIdentity(snapshot.entries, lane, f.j.cell), e => e.message.length < 1024, `${lane} ${mutation} ${entry.path}`); + if (entry.path === lane + '/plugin.json') assert.throws(() => a.verifyResults(f.j, bad), e => e.message.length < 1024); + } + } + for (const kind of ['file', 'directory']) { + const bad = structuredClone({ 'commands.json': f.evidence['commands.json'], 'projects.json': f.evidence['projects.json'] }); + for (const snapshot of Object.values(bad['projects.json'])) { + snapshot.entries.push({ path: lane + '/unexpected-empty', mode: kind === 'file' ? 0o644 : 0o755, kind, + ...(kind === 'file' ? { size: 0, sha256: H('') } : {}) }); + snapshot.sha256 = c.digest(c.encode(snapshot.entries)); + } + assert.throws(() => a.verifyResults(f.j, bad), e => e.message.length < 1024); + } + const bad = structuredClone({ 'commands.json': f.evidence['commands.json'], 'projects.json': f.evidence['projects.json'] }); + for (const row of bad['commands.json'].filter(r => r.id.startsWith(lane + '/'))) { + const v = JSON.parse(row.stdout); + if (v.data.identity.tree_digest) v.data.identity.tree_digest = 'sha256:' + H('unbound identical claim'); + row.stdout = JSON.stringify(v); + } + assert.throws(() => a.verifyResults(f.j, bad), /captured generated tree identity/); + } + }); + test("C3 unit production installer evidence is independently required", t => { + const f = fixture(t); withReaders(t, f, () => { + semanticFixture(f); const local = a.readJourneyInputs(f.request); + assert.throws(() => a.verifyJourney(local), /PUBLIC_FACADE_REQUIRED/); + assert.throws(() => bridge.publishSeal(f.request, path.join(f.root, "must-not-exist")), /PUBLIC_FACADE_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(["--assemble", 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)); + }); + test('C3 unit npm shims postinstall and peer lifecycle contract', async t => { + const f = semanticFixture(fixture(t)), j = f.j, roots = a.rootsFor(f.root, j.cell), original = f.evidence['npm-lifecycle.json']; + assert.deepEqual(a.verifyNpmLifecycle(j, original, roots), { npm_lifecycle: true }); + const cases = [ + ['omitted npm row', x => x.rows.pop()], ['wrong argv', x => x.rows[0].argv.push('--ignore-scripts')], + ['wrong cwd', x => { x.rows[0].cwd = f.root; }], ['extra env', x => { x.rows[0].env.NODE_OPTIONS = '--require evil'; }], + ['wrong npm runtime', x => { x.rows[0].runtime = structuredClone(j.tools.orchestrator_node); x.rows[0].runtime.version = 'v18.0.0'; }], + ['missing postinstall', x => { x.rows.find(r => r.postinstall).postinstall = null; }], + ['postinstall wrong node', x => { x.rows.find(r => r.postinstall).postinstall.runtime.version = 'v18.0.0'; }], + ['retained removed shim', x => { const r = x.rows.find(r => r.id.endsWith('/uninstall')); r.after.prefix.agentplugins = r.before.prefix.agentplugins; }], + ['peer damaged', x => { const r = x.rows.find(r => r.id.includes('/shared-agentplugins/uninstall')); r.after.prefix['plugin-kit-ai'].tree = H('changed'); }], + ['reinstall changed bytes', x => { const r = x.rows.find(r => r.id.endsWith('/reinstall')); r.after.prefix.agentplugins.tree = H('changed'); }], + ['native missing', x => { x.rows.find(r => r.id.endsWith('/probe')).native_launches = 0; }] + ]; + for (const [name, mutate] of cases) { const bad = structuredClone(original); mutate(bad); assert.throws(() => a.verifyNpmLifecycle(j, bad, roots), name); t.diagnostic(name); } + for (const cell of a.matrix) { + const contract = a.scenarioContract(cell.key); assert.ok(Object.isFrozen(contract) && Object.isFrozen(contract.npm)); + assert.equal(contract.npm.length, cell.node === 18 ? 5 : 34); assert.equal(contract.installer.length, cell.node === 18 ? 0 : 18); + const fake = structuredClone(j); fake.cell = cell.key; + if (cell.target.startsWith('windows-')) { + fake.projects = Object.fromEntries(cell.products.map(p => [p, `C:\\C3\\projects\\${p} projects ü`])); + for (const k of ['npm_node', 'shim_node', 'npm']) fake.tools[k].path = `C:\\tools\\${k}.exe`; + const r = a.rootsFor('C:\\C3', cell.key), literal = contract.cache.find(s => s.kind === 'literal-argv'); + const invocation = a.plannedInvocation(fake, literal, r); + assert.match(invocation.argv[0], /powershell\.exe$/); assert.ok(invocation.argv.at(-1).includes('.ps1')); + assert.ok(invocation.argv.at(-1).includes("''single''")); assert.ok(invocation.argv.at(-1).includes('$(literal)')); + const version = contract.cache.find(s => s.command !== null && a.commandContract(cell.key)[s.command].id === 'product-version'); + assert.ok(a.plannedInvocation(fake, version, r).argv.at(-1).includes('.cmd')); + } else { + const installation = a.plannedInvocation(fake, contract.npm[0], roots); + assert.deepEqual(installation.argv.slice(2, -1), ['install', '--global', '--prefix', path.join(roots.npm, contract.npm[0].prefix, 'prefix'), '--offline', '--ignore-scripts=false', '--foreground-scripts', '--no-audit', '--no-fund']); + const literal = a.plannedInvocation(fake, contract.cache.find(s => s.kind === 'literal-argv'), roots); + assert.ok(literal.argv.includes(a.LITERAL_DESCRIPTION)); assert.ok(!literal.argv[0].endsWith('.js')); + } + } + // The real producer must close before any output/npm/native effect when its + // source-frozen provision is absent; no input chooses a callback or command. + const tools = require('../scripts/public-authoring-tools'); let checks = 0; + t.mock.method(tools, 'requireController', () => { checks++; throw new Error('PUBLIC_PROVISIONING_REQUIRED:synthetic'); }); + const before = fs.readdirSync(f.root); + await assert.rejects(a.produceJourney({ command: ['arbitrary'], success() {} }), /PUBLIC_PROVISIONING_REQUIRED/); + assert.equal(checks, 1); assert.deepEqual(fs.readdirSync(f.root), before); t.mock.restoreAll(); + assert.throws(() => a.requireFacades(j.cell), /public-authoring-custody.js#readPublicInputs.*public-process-observation.js#openPublicObservation.*public-installer-evidence.js#requirePublicInstaller/); + // Real producer control flow, with opaque synthetic owner interfaces. No + // npm/native/tool fixture is executed and no synthetic J can be emitted. + const request = { schema: 'authoring-public-produce/v1', selected: f.admission.selected, workflow_sha: j.identity.commit, + stage: j.stage, input_file: f.admission.input_file, repo, work_parent: f.admission.work_parent, + output: path.join(f.root, 'producer-control'), cell: j.cell, tools: j.tools, producer: j.producer }; + const frozen = { ...j.tools, npm: { ...j.tools.npm, closure: { root: path.dirname(j.tools.npm.path) } }, mod_cache: null }; + const history = []; + t.mock.method(tools, 'requireController', () => process.execPath); + t.mock.method(tools, 'requireCellTools', () => frozen); + t.mock.method(tools, 'readProvisioning', () => ({ controllers: { 'linux-amd64': { git: j.tools.go } } })); + t.mock.method(require('node:child_process'), 'execFileSync', (file, args) => { + assert.equal(file, j.tools.go.path); history.push('source:' + args[0]); + if (args[0] === 'rev-parse') return Buffer.from(j.identity.commit); + if (args[0] === 'status') return Buffer.alloc(0); + assert.deepEqual(args, ['ls-files', '-z']); return Buffer.from('npm/agentplugins/scripts/public-authoring-acceptance.js\0'); + }); + await withFacades(t, async api => { + api['public-authoring-custody'].readPublicInputs = () => { history.push('custody'); throw new Error('SYNTHETIC custody denial'); }; + await assert.rejects(a.produceJourney(request), /custody denial/); + assert.equal(fs.existsSync(request.output), false); + assert.ok(history.indexOf('source:ls-files') < history.indexOf('custody')); + const subjects = [f.inputResult.subjects[0], ...f.inputResult.subjects.slice(1, 7)]; + for (const product of c.PRODUCTS) for (const target of c.TARGETS) { + const file = path.join(f.inputResult.root, j.subjects[product][target].file); + write(file, Buffer.from((product === 'agentplugins' ? '' : 'outer') + product + target)); + subjects.push({ file, sha256: hash(file) }); + } + api['public-authoring-custody'].readPublicInputs = () => ({ stage: f.stageResult, input: { ...f.inputResult, subjects } }); + api['public-installer-evidence'].requirePublicInstaller = () => { throw new Error('SYNTHETIC installer unavailable'); }; + await assert.rejects(a.produceJourney(request), /installer unavailable/); + assert.equal(fs.existsSync(request.output), false); + api['public-installer-evidence'].requirePublicInstaller = () => history.push('installer-ready'); + let runs = 0, finishes = 0; + api['public-process-observation'].openPublicObservation = ({ roots }) => { + history.push('observation-open'); + assert.equal(fs.readFileSync(path.join(roots.npm, 'alone-agentplugins/user.npmrc'), 'utf8'), ''); + return { + run(invocation) { runs++; assert.deepEqual(invocation, a.plannedInvocation(j, a.scenarioContract(j.cell).npm[0], roots)); throw new Error('SYNTHETIC primary run failure'); }, + cancel() { assert.fail('no cancellation scenario reached'); }, + finish() { finishes++; throw new Error('SYNTHETIC late finalization failure'); } + }; + }; + await assert.rejects(a.produceJourney(request), /journey incomplete/); + assert.equal(runs, 1); assert.equal(finishes, 1); + assert.ok(history.indexOf('installer-ready') < history.indexOf('observation-open')); + const failure = JSON.parse(fs.readFileSync(path.join(request.output, 'evidence/failure.json'))); + assert.match(failure.primary, /C3 fixed scenario failed/); assert.match(failure.primary, /primary run failure/); assert.match(failure.finalization, /late finalization failure/); + assert.equal(fs.existsSync(path.join(request.output, 'evidence/public-journey.json')), false); + for (const failFinish of [false, true]) { + request.output = path.join(f.root, `malformed-observer-${failFinish}`); + let finished = 0, ran = 0; + api['public-process-observation'].openPublicObservation = () => ({ + run() { ran++; }, + finish() { finished++; if (failFinish) throw new Error('SYNTHETIC malformed finalizer failure'); } + }); + await assert.rejects(a.produceJourney(request), /journey incomplete/); + assert.equal(finished, 1); assert.equal(ran, 0); + const receipt = JSON.parse(fs.readFileSync(path.join(request.output, 'evidence/failure.json'))); + assert.match(receipt.primary, /session.cancel/); + if (failFinish) assert.match(receipt.finalization, /malformed finalizer failure/); + else assert.equal(receipt.finalization, null); + assert.equal(fs.existsSync(path.join(request.output, 'evidence/public-journey.json')), false); + } + for (const session of [null, { run() {}, cancel() {} }]) { + request.output = path.join(f.root, `no-finalizer-${session === null}`); + api['public-process-observation'].openPublicObservation = () => session; + await assert.rejects(a.produceJourney(request), /journey incomplete/); + const receipt = JSON.parse(fs.readFileSync(path.join(request.output, 'evidence/failure.json'))); + assert.match(receipt.primary, /session.finish/); assert.equal(receipt.finalization, null); + assert.equal(fs.existsSync(path.join(request.output, 'evidence/public-journey.json')), false); + } + request.output = path.join(f.root, 'observer-open-failure'); + api['public-process-observation'].openPublicObservation = () => { throw new Error('SYNTHETIC observation unavailable'); }; + await assert.rejects(a.produceJourney(request), /observation unavailable/); + assert.match(JSON.parse(fs.readFileSync(path.join(request.output, 'evidence/failure.json'))).primary, /observation unavailable/); + assert.equal(fs.existsSync(path.join(request.output, 'evidence/public-journey.json')), false); + }); + t.mock.restoreAll(); + + }); + test('C3 unit cache process failure and cancellation ordering', t => { + const f = semanticFixture(fixture(t)), j = f.j, roots = a.rootsFor(f.root, j.cell), original = f.evidence['cache-process.json']; + const verify = value => a.verifyCacheProcess(j, value, roots, f.evidence['commands.json']); + assert.deepEqual(verify(original), { cache_process: true, children_reaped: true }); + // Transport regression: full long-path observations exceed a 1MiB record, + // without dropping rows or relaxing record/output/shard/aggregate limits. + const transport = a.evidenceFiles('cache-process.json', original); + assert.ok(transport['cache-process.json'].length <= a.LIMIT); + assert.deepEqual(a.expandEvidence('cache-process.json', JSON.parse(transport['cache-process.json']), f.admission.journey_root), original); + const padded = { ...original, rows: original.rows.map(r => ({ ...r, diagnostic: 'x'.repeat(130000) })) }; + const large = a.evidenceFiles('cache-process.json', padded); + assert.ok(Object.keys(large).filter(n => n.startsWith('sidecars/')).length > 1); + for (const [name, bytes] of Object.entries(large)) assert.ok(bytes.length <= (name.startsWith('sidecars/') ? 16 * a.LIMIT : a.LIMIT)); + const shardRoot = path.join(f.root, 'transport'); fs.mkdirSync(path.join(shardRoot, 'sidecars'), { recursive: true }); + for (const [name, bytes] of Object.entries(large)) write(path.join(shardRoot, name), bytes); + assert.ok(require('node:util').isDeepStrictEqual(a.expandEvidence('cache-process.json', JSON.parse(large['cache-process.json']), shardRoot), padded)); + assert.throws(() => a.evidenceFiles('cache-process.json', { ...original, rows: [{ data: 'x'.repeat(a.LIMIT) }] }), /1MiB process record/); + const index = JSON.parse(transport['cache-process.json']); + assert.throws(() => a.expandEvidence('cache-process.json', index, f.admission.journey_root, { size: 128 * a.LIMIT }), /128MiB/); + const badIndex = structuredClone(index); badIndex.rows.shards[0].sha256 = H('changed'); + assert.throws(() => a.expandEvidence('cache-process.json', badIndex, f.admission.journey_root), /pin/); + const find = (x, kind) => x.rows.find(r => r.id.endsWith('/' + kind)); + const cases = [ + ['warm reacquisition', x => { find(x, 'warm').acquisitions++; }], + ['warm wrong bytes', x => { find(x, 'warm').after.cache.agentplugins.sha256 = H('wrong'); }], + ['invalid cold launches', x => { find(x, 'invalid-cold').native_launches = 1; }], + ['invalid warm fallback', x => { find(x, 'invalid-warm').downloads = 1; }], + ['uncorrupted repair input', x => { find(x, 'repair').before.cache.agentplugins = structuredClone(find(x, 'repair').after.cache.agentplugins); }], + ['unchecked repair', x => { const r = find(x, 'repair'); r.events = r.events.filter(x => x !== 'repair-before-launch'); }], + ['repair wrong mode', x => { find(x, 'repair').after.cache.agentplugins.mode = 0o644; }], + ['nonoverlapping concurrency', x => { find(x, 'concurrent-cold-3').interval = [100000, 100001]; }], + ['two concurrent commits', x => { find(x, 'concurrent-cold-2').commits++; }], + ['missing fourth request', x => { x.rows.splice(x.rows.findIndex(r => r.id.endsWith('/concurrent-cold-3')), 1); }], + ['signal before boundary', x => { find(x, 'SIGINT').events = ['cancel-delivered', 'reaped']; }], + ['cancel before native', x => { find(x, 'SIGINT').events = ['shim', 'cancel-delivered', 'native', 'reaped']; }], + ['early reap', x => { find(x, 'SIGINT').events = ['shim', 'reaped', 'native', 'cancel-delivered']; }], + ['waiter native launch', x => { find(x, 'waiter-cancel').native_launches = 1; }], + ['wrong cancel exit', x => { find(x, 'SIGTERM').status = 0; }], + ['not a cache waiter', x => { const r = find(x, 'waiter-cancel'); r.events = r.events.filter(x => x !== 'cache-waiter'); }], + ['leaked descendant', x => x.finalization.descendants.push(42)], ['owned lock leak', x => x.finalization.locks.push('lock')], + ['late observer error', x => x.finalization.late_errors.push('denied')], ['omitted final row', x => x.finalization.rows.pop()], + ['oversized observation', x => { x.rows[0].observation.size = 16 * a.LIMIT + 1; }], + ['literal effect missing', x => { find(x, 'literal-argv').literal = null; }], + ['literal expansion', x => { find(x, 'literal-argv').literal.description = 'expanded shell values'; }], + ['literal wrong cwd effect', x => { find(x, 'literal-argv').literal.root = f.root; }], + ['direct-bin substitution', x => { x.rows[0].argv = [j.tools.shim_node.path, 'bin/agentplugins.js']; }] + ]; + for (const [name, mutate] of cases) { const bad = structuredClone(original); mutate(bad); assert.throws(() => verify(bad), name); t.diagnostic(name); } + withFacades(t, api => { + assert.equal(a.verifyJourney({ record: j, evidence: f.evidence }).record, j); + api['public-process-observation'].verifyPublicObservation = () => true; + assert.throws(() => a.verifyJourney({ record: j, evidence: f.evidence }), /never boolean success/); + }); + }); + +} diff --git a/npm/agentplugins/test/public-authoring-native.test.js b/npm/agentplugins/test/public-authoring-native.test.js index 4a6dccd9..4e5a17bd 100644 --- a/npm/agentplugins/test/public-authoring-native.test.js +++ b/npm/agentplugins/test/public-authoring-native.test.js @@ -14,7 +14,7 @@ const bridge = require("../scripts/packed-installer-bridge"); const adapter = require("../scripts/authoring-release"); const packing = require("../scripts/stage-dual-authoring-npm"); const stager = require("../scripts/stage-authoring-npm"); -const { preload, run, ok, environment } = require("./public-authoring-pack.test"); +const { preload, run, ok, environment, nativeObservation } = require("./public-authoring-pack.test"); function tree(root) { const result = {}; @@ -122,10 +122,10 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and const root = path.join(context.root, `${p} projects ü`); fs.mkdirSync(root); return [p, root]; })); function invoke(p, argv, status = 0, json = true) { - const r = run(o.node, [bin(p), ...argv], { ...transportEnv, PATH: "/absent-peer-binary-path" }, projects[p]); + const r = nativeObservation(p, o.node, bin(p), argv, { ...transportEnv, PATH: "/absent-peer-binary-path" }, projects[p]); invocations.push({ product: p, argv, status: r.status, signal: r.signal, stdout: r.stdout, stderr: r.stderr }); fs.writeFileSync(path.join(cfg.evidenceOutput, "invocations.json"), c.encode(invocations)); - assert.equal(r.status, status, r.stdout + r.stderr); assert.equal(r.stderr, ""); + assert.equal(r.status, status, r.stdout + r.stderr); return json ? JSON.parse(r.stdout) : r.stdout; } function author(p, argv, status = 0, compare = true) { @@ -170,9 +170,16 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and const retired = invoke("plugin-kit-ai", ["update", "--all", "--format=json"], 2); assert.equal(retired.data.error.code, "v1_operation_unavailable"); assert.deepEqual(tree(projects["plugin-kit-ai"]), before); - // Installer dry-run uses an explicitly isolated synthetic client root/home. - const dry = invoke("agentplugins", ["add", path.join(projects.agentplugins, "skill"), "--target=codex", "--dry-run", "--format=json"]); - assert.ok(dry); assert.deepEqual(tree(projects["plugin-kit-ai"]), before); + // Genuine executable visibility/preflight only; valid production add remains required. + const preservedRoots = [...Object.values(projects), client, installer]; + const preserved = preservedRoots.map(root => bridge.snapshot(root)); + const help = invoke("agentplugins", ["add", "--help", "--format=json"]); + bridge.installerHelp(help); + assert.deepEqual(preservedRoots.map(root => bridge.snapshot(root)), preserved); + assert.equal(invoke("agentplugins", ["add", path.join(projects.agentplugins, "skill"), "--target=codex", + "--scope=project", "--dry-run", "--format=json"], 1, false), ""); + assert.deepEqual(preservedRoots.map(root => bridge.snapshot(root)), preserved); + const installer_boundary = bridge.installerBoundary(projects.agentplugins); const runtime = require("../lib/public-authoring"); for (const p of c.PRODUCTS) { const packageRoot = path.resolve(bin(p), "../.."); @@ -197,7 +204,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and fs.writeFileSync(path.join(cfg.evidenceOutput, "result.json"), c.encode({ source: candidate.identity.commit, fixture_acquisition_execution: true, signed_promotion: false, public_eligible: false, runtime_evidence: "not_evaluated" }), { flag: "wx" }); for (const p of c.PRODUCTS) assert.deepEqual(bridge.LANES.map(lane => tree(path.join(projects[p], lane))), trees[p], "all generated projects preserved through lifecycle"); - const terminal = { schema: "dual-authoring-public-native/v1", status: "completed", + const terminal = { schema: "dual-authoring-public-native/v2", installer_boundary, status: "completed", identity: candidate.identity, candidate_sha256: candidate.manifestDigest, completion_sha256: cfg.completionDigest, config_sha256: c.digest(c.readFile(config)), pair_marker_sha256: o.pairMarkerDigest, projection_pins: o.projectionPins, fixtureRoot: context.root, target, packs, binaries, installations, @@ -210,7 +217,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and release_eligible: false, platform_acceptance: false, attested: false, runtime_evidence: "not_evaluated" }; // Validate the full contract before exclusive terminal publication. No success // marker exists if a lane, pin, or lifecycle assertion above failed. - bridge.publicEvidence(cfg, terminal, config); + bridge.publicEvidence(cfg, terminal, config, "public-fixture/v2"); const terminalPath = path.join(cfg.evidenceOutput, "public-native-completion.json"); fs.writeFileSync(terminalPath, c.encode(terminal), { flag: "wx", mode: 0o600 }); t.diagnostic("public native completion: " + JSON.stringify({ file: terminalPath, sha256: c.digest(c.readFile(terminalPath)), source: candidate.identity.commit })); diff --git a/npm/agentplugins/test/public-authoring-pack.test.js b/npm/agentplugins/test/public-authoring-pack.test.js index 409c14de..45d432be 100644 --- a/npm/agentplugins/test/public-authoring-pack.test.js +++ b/npm/agentplugins/test/public-authoring-pack.test.js @@ -38,6 +38,53 @@ https.get = options => { function run(exe, args, env, cwd) { return cp.spawnSync(exe, args, { env, cwd, encoding: "utf8", timeout: 30000, maxBuffer: 8 * 1024 * 1024 }); } +// Driver-only intent guard; this does not observe networking in native children. +const PREFLIGHT_STDERR = "agentplugins: --scope project is not supported by the current client adapters; the public CLI supports user scope only\n"; +function nativeObservation(product, exe, bin, argv, env, cwd, spawn = run) { + const bridge = require("../scripts/packed-installer-bridge"); + const rejected = ["add", path.join(cwd, "skill"), "--target=codex", "--scope=project", "--dry-run", "--format=json"]; + const same = args => JSON.stringify(args) === JSON.stringify(argv); + if (product === "agentplugins") { + const allowed = [["version", "--format=json"], ["--help"], ["add", "--help", "--format=json"], rejected]; + const author = [["version"], ["--help"]]; + for (const lane of bridge.LANES) { + const source = path.join(cwd, lane); + author.push(bridge.publicInit(lane), ["skills", "init", "extra-skill", source, "--description=Disposable fixture."], + ["skills", "validate", source], ...["validate", "inspect", "test"].map(n => [n, source])); + } + allowed.push(...author.map(args => ["author", ...args, "--format=json"])); + assert.ok(allowed.some(same), "offline native observation denied before spawn: production-security-inputs-not-offline"); + } + const result = spawn(exe, [bin, ...argv], env, cwd); + assert.equal(result.signal, null); + assert.equal(result.stderr, product === "agentplugins" && same(rejected) ? PREFLIGHT_STDERR : ""); + return result; +} +if (require.main === module) test("offline native guard rejects original valid add before spawn effects", () => { + let calls = 0; + const source = "/disposable/agentplugins projects ü"; + const argv = ["add", path.join(source, "skill"), "--target=codex", "--dry-run", "--format=json"]; + for (const args of [argv, ["install", ...argv.slice(1)], ["--target=codex", ...argv], + [...argv, "--help"], [...argv, "--scope=project"], ["author", "publish", "--format=json"]]) { + assert.throws(() => nativeObservation("agentplugins", NODE, "/unused/bin", args, {}, source, + () => { calls++; throw new Error("spawn effect"); }), /denied before spawn/); + } + assert.equal(calls, 0); +}); +if (require.main === module) test("offline native guard allows only designated installer results", () => { + const cwd = "/disposable/agentplugins projects ü", bin = "/unused/bin"; + for (const argv of [["add", "--help", "--format=json"], + ["add", path.join(cwd, "skill"), "--target=codex", "--scope=project", "--dry-run", "--format=json"], + ["author", "test", path.join(cwd, "skill"), "--format=json"]]) { + const stderr = argv.includes("--scope=project") ? PREFLIGHT_STDERR : ""; + const invoke = result => nativeObservation("agentplugins", NODE, bin, argv, {}, cwd, (exe, args) => { + assert.equal(exe, NODE); assert.deepEqual(args, [bin, ...argv]); return result; + }); + assert.equal(invoke({ signal: null, stderr }).stderr, stderr); + assert.throws(() => invoke({ signal: null, stderr: stderr + "unexpected output" })); + assert.throws(() => invoke({ signal: "SIGTERM", stderr })); + } +}); function ok(result) { assert.equal(result.status, 0, result.stderr || result.stdout); return result.stdout; } function packageBytes(root) { const files = {}; @@ -212,7 +259,7 @@ packing.blobs = (...args) => { assert.throws(() => packing.blobs(REPO, head, context.env, "arbitrary"), /fixed/); }); } -module.exports = { preload, run, ok, packageBytes, environment }; +module.exports = { preload, run, ok, packageBytes, environment, nativeObservation, PREFLIGHT_STDERR }; if (require.main === module) test("shared completion helper preserves a collision and rolls back an uncertain marker", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "public completion-")); diff --git a/npm/agentplugins/test/public-authoring-tools.test.js b/npm/agentplugins/test/public-authoring-tools.test.js new file mode 100644 index 00000000..57ce5d19 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-tools.test.js @@ -0,0 +1,153 @@ +"use strict"; +// SYNTHETIC provision only. Harmless bytes are hashed, never executed. +const test = require("node:test"), assert = require("node:assert/strict"); +const fs = require("node:fs"), path = require("node:path"), os = require("node:os"), vm = require("node:vm"); +const { createRequire } = require("node:module"); +const source = path.resolve(__dirname, "../scripts/public-authoring-tools.js"), local = createRequire(source); +const c = local("./dual-authoring-candidate"), shipped = local("./public-authoring-tools"); +const base = shipped.readProvisioning(), manifestPath = path.resolve(__dirname, "../../../.github/authoring-public-tools.json"); +function fixture(body = c.encode(base), read = c.readFile, filesystem = fs) { + const context = { module: { exports: {} }, Buffer, TextDecoder, __dirname: path.dirname(source), + require: id => id === "./dual-authoring-candidate" ? { ...c, readFile: (f, cap) => f === manifestPath ? body : read(f, cap) } : id === "node:fs" ? filesystem : local(id) }; + vm.runInThisContext("(function(require,module,__dirname){" + fs.readFileSync(source, "utf8") + "\n})", { filename: source })(context.require, context.module, context.__dirname); return context.module.exports; +} +const fresh = () => JSON.parse(c.encode(base)); +function pin(file) { return { path: file, version: "v22.21.1", sha256: c.digest(fs.readFileSync(file)) }; } +function prepared() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "C3b-tools-SYNTHETIC-")); + const file = path.join(root, "node"); fs.writeFileSync(file, "SYNTHETIC TOOL\n"); + const value = fresh(); for (const key of Object.keys(value.controllers["linux-amd64"])) value.controllers["linux-amd64"][key] = pin(file); + return { root, file, value }; +} +test("C3b tools frozen six controllers eighteen cells and unavailable provision", () => { + assert.equal(Object.keys(base.controllers).length, 6); assert.equal(Object.keys(base.cells).length, 18); + assert.equal(base.reader, "linux-amd64"); assert.ok(Object.isFrozen(base.cells)); + assert.throws(() => shipped.requireController(), /PUBLIC_PROVISIONING_REQUIRED:linux-amd64:node/); + for (const key of Object.keys(base.cells)) assert.throws(() => shipped.requireCellTools(key), new RegExp(`PUBLIC_PROVISIONING_REQUIRED:${key}:runner`)); + for (const fn of [() => shipped.readProvisioning("/receipt"), () => shipped.requireController("unknown"), + () => shipped.requireController("linux-amd64", base), () => shipped.requireCellTools("unknown")]) assert.throws(fn); +}); +test("C3b tools literal canonical fixture and closed malformed table", () => { + const literal = '{\n "path": "/synthetic/node",\n "version": "v22.21.1",\n "sha256": "' + c.digest(Buffer.from("SYNTHETIC TOOL\n")) + '"\n}'; + const value = fresh(); value.controllers["linux-amd64"].node = JSON.parse(literal); + assert.equal(JSON.stringify(fixture(c.encode(value)).readProvisioning().controllers["linux-amd64"].node, null, 2), literal); + const missing = fresh(); delete missing.cells["linux-arm64/kit-node18"]; + assert.throws(() => fixture(c.encode(missing)).readProvisioning(), /PUBLIC_PROVISIONING_REQUIRED:linux-arm64\/kit-node18:entry/); + const mutations = [v => delete v.cells["linux-arm64/kit-node18"], v => v.controllers.extra = {}, + v => delete v.controllers["linux-amd64"].tar, v => v.cells["linux-amd64/kit-node18"].extra = null, + v => v.reader = "windows-amd64", v => v.cells["linux-amd64/kit-node18"].controller = "linux-arm64", + v => v.controllers["linux-amd64"].node.sha256 = "0".repeat(64), v => v.controllers["linux-amd64"].node.path = "/synthetic/../node", + v => v.controllers["linux-amd64"].node.version = "x".repeat(257), + v => v.controllers["linux-amd64"].node.extra = true]; + for (const mutate of mutations) { const bad = JSON.parse(c.encode(value)); mutate(bad); assert.throws(() => fixture(c.encode(bad)).readProvisioning()); } + for (const body of [Buffer.from(JSON.stringify(value)), Buffer.from(c.encode(value).toString().replace(' "schema":', ' "reader": "linux-amd64",\n "schema":')), + Buffer.from('[['.repeat(10)), Buffer.alloc(1024 * 1024 + 1, 32), Buffer.from([255]), Buffer.from('null\n')]) { + assert.throws(() => fixture(body).readProvisioning()); + } +}); +test("C3b tools controller pins missing tools links and late replacement", () => { + const { file, value, root } = prepared(); const api = fixture(c.encode(value)); + assert.equal(api.requireController(), file); + fs.writeFileSync(file, "CHANGED TOOL\n"); assert.throws(() => api.requireController(), /pin mismatch/); + fs.writeFileSync(file, "SYNTHETIC TOOL\n"); + const link = path.join(root, "link"); fs.symlinkSync(file, link); + value.controllers["linux-amd64"].node.path = link; assert.throws(() => fixture(c.encode(value)).requireController()); + value.controllers["linux-amd64"].node.path = path.join(root, "missing"); + assert.throws(() => fixture(c.encode(value)).requireController(), /PUBLIC_PROVISIONING_REQUIRED:linux-amd64:node/); + value.controllers["linux-amd64"].node = pin(file); value.controllers["linux-amd64"].gh = null; + assert.throws(() => fixture(c.encode(value)).requireController(), /PUBLIC_PROVISIONING_REQUIRED:linux-amd64:gh/); + fs.linkSync(file, path.join(root, "hardlink")); assert.throws(() => api.requireController()); +}); +test("C3b tools complete npm closure and cell capabilities", () => { + const { file, value, root } = prepared(), key = "linux-amd64/pair-node22", row = value.cells[key]; + const npmRoot = path.join(root, "npm"); fs.mkdirSync(npmRoot); const cli = path.join(npmRoot, "npm-cli.js"); fs.writeFileSync(cli, "SYNTHETIC NPM\n"); + const files = [{ path: "npm-cli.js", sha256: pin(cli).sha256 }]; + for (const name of ["runner", "image", "observer", "installer_policy"]) row[name] = { id: "synthetic-only", sha256: pin(file).sha256 }; + for (const name of ["npm_node", "shim_node", "go"]) row[name] = pin(file); + row.npm = { ...pin(cli), closure: { root: npmRoot, files } }; row.mod_cache = { root: npmRoot, files }; + assert.equal(fixture(c.encode(value)).requireCellTools(key).npm.path, cli); + for (const name of ["image", "npm_node", "shim_node", "npm", "go", "mod_cache", "observer", "installer_policy"]) { + const bad = JSON.parse(c.encode(value)); bad.cells[key][name] = null; + assert.throws(() => fixture(c.encode(bad)).requireCellTools(key), new RegExp(`PUBLIC_PROVISIONING_REQUIRED:${key}:${name}`)); + } + const empty = path.join(npmRoot, "empty.js"); fs.writeFileSync(empty, ""); + assert.throws(() => fixture(c.encode(value)).requireCellTools(key), /closure mismatch/); + files.unshift({ path: "empty.js", sha256: pin(empty).sha256 }); + assert.equal(fixture(c.encode(value)).requireCellTools(key).mod_cache.files[0].sha256, c.digest(Buffer.alloc(0))); + // Empty members remain exhaustive and cannot be aliased or change mid-read. + const hard = path.join(root, "empty-hardlink"); fs.linkSync(empty, hard); + assert.throws(() => fixture(c.encode(value)).requireCellTools(key), /unaliased/); + // Retain fixtures without cleanup: use a fresh empty member after the alias case. + const nextRoot = path.join(root, "next-npm"); fs.mkdirSync(nextRoot); + fs.writeFileSync(path.join(nextRoot, "empty.js"), ""); fs.writeFileSync(path.join(nextRoot, "npm-cli.js"), "SYNTHETIC NPM\n"); + row.npm.path = path.join(nextRoot, "npm-cli.js"); row.npm.closure.root = nextRoot; row.mod_cache.root = nextRoot; + const target = path.join(nextRoot, "empty.js"); + let changed = false; + const filesystem = { ...fs, readSync(fd, ...args) { + if (!changed) { changed = true; fs.writeFileSync(target, "x"); } + return fs.readSync(fd, ...args); + } }; + assert.throws(() => fixture(c.encode(value), c.readFile, filesystem).requireCellTools(key), /closure file changed/); + fs.writeFileSync(target, ""); + const aliasRoot = path.join(root, "linked-npm"); fs.symlinkSync(nextRoot, aliasRoot); + const linked = JSON.parse(c.encode(value)); linked.cells[key].mod_cache.root = aliasRoot; + assert.throws(() => fixture(c.encode(linked)).requireCellTools(key), /symlink/); + const replacedDescriptor = { ...fs, fstatSync(fd, options) { + const st = fs.fstatSync(fd, options); return { ...st, ino: st.ino + 1n }; + } }; + assert.throws(() => fixture(c.encode(value), c.readFile, replacedDescriptor).requireCellTools(key), /closure file changed/); + const emptyTool = JSON.parse(c.encode(value)); emptyTool.controllers["linux-amd64"].node = pin(target); + assert.throws(() => fixture(c.encode(emptyTool)).requireController(), /nonempty/); + assert.throws(() => fixture(Buffer.alloc(0)).readProvisioning()); + const bad = JSON.parse(c.encode(value)); bad.cells[key].npm.closure.files.push(files[0]); + assert.throws(() => fixture(c.encode(bad)).readProvisioning(), /ordered unique/); + fs.writeFileSync(path.join(row.npm.closure.root, "unlisted.js"), "UNLISTED\n"); + assert.throws(() => fixture(c.encode(value)).requireCellTools(key), /complete source-frozen closure mismatch/); +}); +test("C3b tools manifest absence and source root cannot be substituted", () => { + const error = Object.assign(new Error("missing"), { code: "ENOENT" }); + const context = { module: { exports: {} }, Buffer, TextDecoder, __dirname: path.dirname(source), + require: id => id === "./dual-authoring-candidate" ? { ...c, readFile: f => { assert.equal(f, manifestPath); throw error; } } : local(id) }; + vm.runInThisContext("(function(require,module,__dirname){" + fs.readFileSync(source, "utf8") + "\n})")(context.require, context.module, context.__dirname); + assert.throws(() => context.module.exports.readProvisioning(), /PUBLIC_PROVISIONING_REQUIRED:linux-amd64:manifest/); + assert.throws(() => context.module.exports.readProvisioning(base), /trusted source only/); +}); +test("C3b tools JS Python agree on identical canonical fixture bytes", () => { + const { spawnSync } = require("node:child_process"); + const { root, value } = prepared(), bodies = [c.encode(base), c.encode(value)]; + for (const mutate of [v => v.reader = "darwin-amd64", v => delete v.cells["windows-arm64/pair-node24"], + v => v.controllers["linux-amd64"].node.extra = true, v => v.controllers["linux-amd64"].node.sha256 = "0".repeat(64), + v => v.controllers["linux-amd64"].node.path = "/synthetic/../node", v => v.controllers["linux-amd64"].node.path = "/synthetic/node/", + v => v.controllers["linux-amd64"].node = { version: "v22.21.1", path: "/synthetic/node", sha256: v.controllers["linux-amd64"].node.sha256 }]) { + const bad = JSON.parse(c.encode(value)); mutate(bad); bodies.push(c.encode(bad)); + } + bodies.push(Buffer.from(JSON.stringify(value)), Buffer.from(c.encode(value).toString().replace(' "schema":', ' "reader": "linux-amd64",\n "schema":')), + Buffer.from('['.repeat(9)), Buffer.alloc(1024 * 1024 + 1, 32), Buffer.from([255])); + const expected = bodies.map(body => { try { fixture(body).readProvisioning(); return true; } catch { return false; } }); + assert.deepEqual(expected, [true, true, ...Array(bodies.length - 2).fill(false)]); + // Identical canonical bytes at 4095/4096/4097 code points, including supplementary Unicode. + for (const [suffix, accepted] of [["", true], ["a", true], ["ab", false]]) { + const unicode = JSON.parse(c.encode(value)); + unicode.controllers["linux-amd64"].node.path = "/" + "😀".repeat(4094) + suffix; + const body = c.encode(unicode); bodies.push(body); expected.push(accepted); + if (accepted) assert.doesNotThrow(() => fixture(body).readProvisioning()); + else assert.throws(() => fixture(body).readProvisioning(), /provision path/); + } + bodies.forEach((body, i) => fs.writeFileSync(path.join(root, `${i}.json`), body)); + const code = `import importlib.util,json,pathlib,sys +spec=importlib.util.spec_from_file_location('provision',sys.argv[1]); p=importlib.util.module_from_spec(spec); spec.loader.exec_module(p) +root=pathlib.Path(sys.argv[2]); (root/'.github').mkdir(); p.__file__=str(root/'scripts/check-packed-ci.py'); results=[] +for i in range(int(sys.argv[3])): + (root/'.github/authoring-public-tools.json').write_bytes((root/(str(i)+'.json')).read_bytes()) + try: p.read_provisioning(); results.append(True) + except (ValueError,TypeError): results.append(False) +print(json.dumps(results)) +`; + const argv = ["-B", "-c", code, path.resolve(__dirname, "../../../scripts/check-packed-ci.py"), root, String(bodies.length)]; + const env = Object.fromEntries(["HOME", "TMPDIR", "TMP", "TEMP", "XDG_CACHE_HOME"].map(k => [k, process.env[k]])); + Object.assign(env, { PATH: "/usr/local/bin:/usr/bin:/bin", LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }); + const result = spawnSync("/usr/bin/python3", argv, { env, cwd: root, encoding: "utf8" }); + fs.writeFileSync(path.join(root, "agreement-receipt.json"), JSON.stringify({ argv: ["/usr/bin/python3", ...argv], env, cwd: root, + exit: result.status, stdout: result.stdout, stderr: result.stderr, fixtures: bodies.map(c.digest), expected }, null, 2) + "\n"); + assert.equal(result.status, 0, result.stderr); assert.equal(result.stderr, ""); assert.deepEqual(JSON.parse(result.stdout), expected); +}); diff --git a/npm/agentplugins/test/public-authoring-v2-pack.test.js b/npm/agentplugins/test/public-authoring-v2-pack.test.js new file mode 100644 index 00000000..ba4494b6 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-v2-pack.test.js @@ -0,0 +1,177 @@ +"use strict"; + +// In-memory package closure and actual JavaScript launcher interfaces only. +const test = require("node:test"); +const assert = require("node:assert/strict"); +const vm = require("node:vm"); +const path = require("node:path"); +const fs = require("node:fs"); +const cp = require("node:child_process"); +const { EventEmitter } = require("node:events"); +const c = require("../scripts/dual-authoring-candidate"); +const codec = require("../lib/public-authoring-contract"); +const facade = require("../scripts/authoring-native-inputs"); +const stage = require("../scripts/stage-authoring-npm"); +const packing = require("../scripts/stage-dual-authoring-npm"); +const { fixture, source, LOCATOR } = require("./public-authoring-v2.test"); +const root = path.resolve(__dirname, "../../.."); +const inventory = p => ["LICENSE", "README.md", "package.json", `bin/${p}.js`, "bin/package.json", "lib/package.json", + "lib/platform.js", "lib/verifier.js", "lib/public-authoring.js", "lib/public-authoring-contract.js", "lib/public-authoring-input.js", + p === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js", "scripts/package.json", "scripts/dual-authoring-candidate.js", + "public-release.json", "release-manifest.json", "native-inputs.json"].sort(); + +function loader(files, overrides = {}, processValue = process) { + const cache = new Map(), loaded = [], builtins = new Set(["fs", "fs/promises", "os", "path", "crypto", "util", "child_process", "https", "http", "zlib", "stream", "stream/promises", "url"]); + function load(name, main = false) { + if (cache.has(name)) return cache.get(name).exports; + assert.ok(Object.hasOwn(files, name), `require escaped the fixed package closure: ${name}`); + const module = { exports: {} }; cache.set(name, module); loaded.push(name); + function localRequire(request) { + const builtin = request.replace(/^node:/, ""); + if (!request.startsWith(".")) { + assert.ok(builtins.has(builtin), `unapproved external module ${request}`); + return Object.hasOwn(overrides, builtin) ? overrides[builtin] : require(`node:${builtin}`); + } + let target = path.posix.normalize(path.posix.join(path.posix.dirname(name), request)); + if (!path.posix.extname(target)) target += ".js"; + return load(target); + } + localRequire.main = main ? module : null; + if (name.endsWith(".json")) module.exports = JSON.parse(files[name]); + else { + const code = files[name].toString().replace(/^#![^\n]*\n/, ""); + const fn = vm.runInNewContext(`(function(require,module,exports,__filename,__dirname){${code}\n})`, + { Buffer, process: processValue, console, setTimeout, clearTimeout, URL, AbortController }, { filename: name }); + fn(localRequire, module, module.exports, `/packed/${name}`, `/packed/${path.posix.dirname(name)}`); + } + return module.exports; + } + return { load, loaded }; +} + +async function launchers(f) { + for (const p of c.PRODUCTS) for (const failed of [false, true]) for (const postinstall of p === "plugin-kit-ai" ? [false, true] : [false]) { + const calls = [], child = new EventEmitter(); + const env = { KEEP: "yes", [LOCATOR]: "/custody/untrusted", NODE_OPTIONS: "preload", UAP_PRIVATE_NPM_ROOT: "private", AGENTPLUGINS_INTERNAL_PROOF_BINARY: "private" }; + const proc = { env, argv: ["node", "launcher", "argument with spaces", "--format", "json"], pid: 12, + platform: "linux", arch: "x64", cwd: () => "/caller", stderr: { write: text => calls.push(["stderr", text]) }, + exit: code => { proc.exitCode = code; }, kill: (pid, signal) => calls.push(["kill", pid, signal]) }; + const fakeFS = { ...fs, lstatSync: () => ({ isFile: () => true }), + readFileSync: file => { assert.equal(file, "/packed/package.json"); return f.pair[p]["package.json"]; } }; + const l = loader(f.pair[p], { fs: fakeFS, child_process: { spawn: (file, args, options) => { + calls.push(["spawn", file, args, options]); return child; + } } }, proc); + const api = l.load("lib/public-authoring.js"); + api.ensureBinary = async (product, options) => { + calls.push(["runtime", product, options]); assert.equal(product, p); assert.equal(options.packageRoot, "/packed"); + if (failed) throw new Error("synthetic runtime rejection"); + return { binaryPath: "/checked/cache/binary", publicAuthoring: true }; + }; + l.load(postinstall ? "lib/install.js" : `bin/${p}.js`, postinstall); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(calls.filter(c => c[0] === "runtime").length, 1); + const spawned = calls.find(c => c[0] === "spawn"); + if (failed || postinstall) assert.equal(spawned, undefined); + else { + assert.equal(spawned[1], "/checked/cache/binary"); assert.deepEqual([...spawned[2]], proc.argv.slice(2)); + assert.equal(spawned[3].cwd, undefined); assert.equal(spawned[3].stdio, "inherit"); + assert.deepEqual({ ...spawned[3].env }, { KEEP: "yes" }); + const exit = child.listeners("exit")[0]; + child.emit("exit", 7, null); assert.equal(proc.exitCode, 7); + exit(null, "SIGTERM"); assert.deepEqual(calls.at(-1), ["kill", 12, "SIGTERM"]); + child.emit("error", new Error("mock spawn")); assert.equal(proc.exitCode, 1); + } + if (failed) assert.equal(proc.exitCode, 1); + } +} + +if (require.main === module) { +test("C2 closure both public packages remain self contained", async () => { + const f = fixture(); + for (const p of c.PRODUCTS) { + const files = f.pair[p]; assert.deepEqual(Object.keys(files).sort(), inventory(p)); assert.equal(Object.keys(files).length, 17); + const pkg = JSON.parse(files["package.json"]), base = JSON.parse(source[`npm/${p}/package.json`].bytes); + assert.deepEqual(pkg, { ...base, version: f.input.identity.versions[p], private: false, files: inventory(p) }); + for (const dir of ["bin", "lib", "scripts"]) assert.deepEqual(JSON.parse(files[`${dir}/package.json`]), { type: "commonjs" }); + const modes = Object.fromEntries(Object.keys(files).map(n => [n, n === `bin/${p}.js` ? 0o755 : 0o644])); + assert.equal(Object.values(modes).filter(n => n === 0o755).length, 1); + const l = loader(files); + for (const n of ["lib/public-authoring.js", "lib/public-authoring-contract.js", "lib/public-authoring-input.js", p === "agentplugins" ? "lib/bootstrap.js" : "lib/install.js"]) l.load(n); + assert.ok(l.loaded.includes("scripts/dual-authoring-candidate.js")); + assert.equal(l.loaded.some(n => /promotion|native-inputs\.js|provider|workflow/.test(n)), false); + const missing = { ...files }; delete missing["lib/public-authoring-contract.js"]; + assert.throws(() => loader(missing).load("lib/public-authoring-contract.js"), /closure/); + } + for (const n of ["native-inputs.json", "lib/public-authoring-contract.js", "lib/public-authoring-input.js", "lib/public-authoring.js"]) { + assert.deepEqual(f.pair.agentplugins[n], f.pair["plugin-kit-ai"][n]); assert.notStrictEqual(f.pair.agentplugins[n], f.pair["plugin-kit-ai"][n]); + } + await launchers(f); +}); + +test("C2 closure executing helpers are pinned at F", t => { + const f = fixture(), commit = f.input.identity.commit; + for (const problem of [null, "HEAD", "missing", "blob", "mode", "checkout", "executing"]) { + const helper = "npm/agentplugins/lib/public-authoring-contract.js"; + const byBlob = new Map(Object.values(source).map(pin => [pin.git_blob, pin.bytes])); + t.mock.method(c, "safeDirectory", value => value); + t.mock.method(cp, "execFileSync", (exe, args) => { + assert.equal(exe, "/usr/bin/git"); + if (args[0] === "rev-parse") return Buffer.from(problem === "HEAD" ? "b".repeat(40) : commit); + if (args[0] === "ls-tree") { + const name = args.at(-1), pin = source[name]; + if (name === helper && problem === "missing") return Buffer.alloc(0); + return Buffer.from(`${name === helper && problem === "mode" ? "120000" : pin.mode} blob ${pin.git_blob}\t${name}\0`); + } + assert.equal(args[0], "cat-file"); + if (problem === "blob" && args[2] === source[helper].git_blob) return Buffer.from("drift"); + return byBlob.get(args[2]); + }); + t.mock.method(c, "readFile", file => { + const name = file.startsWith("/checkout/") ? file.slice(10) : path.relative(root, file); + if (name === helper && ((problem === "checkout" && file.startsWith("/checkout/")) || (problem === "executing" && !file.startsWith("/checkout/")))) return Buffer.from("drift"); + return source[name].bytes; + }); + t.mock.method(fs, "lstatSync", file => ({ mode: source[file.startsWith("/checkout/") ? file.slice(10) : path.relative(root, file)]?.mode === "100755" ? 0o755 : 0o644 })); + if (problem) assert.throws(() => packing.blobs("/checkout", commit, {}, "stage")); + else { + assert.deepEqual(Object.keys(packing.blobs("/checkout", commit, {}, "stage")), stage.STAGE_ALLOWLIST); + assert.deepEqual(Object.keys(packing.blobs("/checkout", commit, {}, "public")), stage.ALLOWLIST); + } + t.mock.restoreAll(); + } + for (const helper of ["lib/public-authoring-contract.js", "lib/public-authoring-input.js"]) { + const name = "npm/agentplugins/" + helper; + for (const mutate of [s => { delete s[name]; }, s => { s.extra = s[name]; }, s => { s[name] = { ...s[name], sha256: "f".repeat(64) }; }, s => { s[name] = { ...s[name], mode: "100755" }; }, + s => { s[name] = { ...s[name], bytes: Buffer.from("changed") }; }]) { + const altered = { ...source }; mutate(altered); assert.throws(() => stage.pairedPackageFiles(altered, f.manifests, f.inputBytes)); + } + } +}); + +test("C2 closure producer codecs and v1 bytes stay compatible", () => { + const f = fixture(); + for (const name of ["encodeInputs", "decodeInputs", "encodeDescriptor", "decodeDescriptor"]) assert.strictEqual(facade[name], codec[name]); + assert.equal(Object.hasOwn(facade, "checks"), false); + for (const name of ["produceInputs", "readInputs", "inputSubjects"]) assert.equal(Object.hasOwn(codec, name), false); + assert.equal(codec.MAX_INPUT_BYTES, 1024 * 1024); assert.equal(codec.MAX_DESCRIPTOR_BYTES, 64 * 1024); + const before = codec.encodeInputs(f.input); assert.deepEqual(codec.decodeInputs(before), f.input); assert.deepEqual(codec.encodeInputs(f.input), before); + for (const p of c.PRODUCTS) { + const v1 = stage.packageFiles(p, source, f.manifests[p], { identity: f.input.identity, manifestDigest: f.input.candidate_sha256 }); + assert.deepEqual(Object.keys(v1).sort(), inventory(p).filter(n => !["native-inputs.json", "lib/public-authoring-contract.js", "lib/public-authoring-input.js"].includes(n))); + const l = loader(v1); l.load("lib/public-authoring.js"); + assert.equal(l.loaded.some(n => /public-authoring-(contract|input)/.test(n)), false); + assert.equal(JSON.parse(v1["public-release.json"]).qualification, null); + for (const name of Object.keys(v1).filter(n => !["public-release.json", "package.json"].includes(n))) assert.deepEqual(v1[name], f.pair[p][name]); + const descriptor = f.pair[p]["public-release.json"]; + assert.deepEqual(codec.encodeDescriptor(codec.decodeDescriptor(descriptor, before, p), before, p), descriptor); + for (const malformed of [Buffer.alloc(0), Buffer.concat([descriptor, Buffer.from(" ")]), Buffer.from("{}")]) { + let first, second; + try { facade.decodeDescriptor(malformed, before, p); } catch (error) { first = error.message; } + try { codec.decodeDescriptor(malformed, before, p); } catch (error) { second = error.message; } + assert.ok(first); assert.equal(first, second); + } + } +}); + +} +module.exports = { loader, launchers }; diff --git a/npm/agentplugins/test/public-authoring-v2.test.js b/npm/agentplugins/test/public-authoring-v2.test.js new file mode 100644 index 00000000..1d97aa48 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-v2.test.js @@ -0,0 +1,438 @@ +"use strict"; + +// Source/unit/interface evidence only: harmless bytes, deterministic fs and +// external-engine outcomes. No archive, native process, transport or custody proof. +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); +const c = require("../scripts/dual-authoring-candidate"); +const codec = require("../lib/public-authoring-contract"); +const snapshots = require("../lib/public-authoring-input"); +const runtime = require("../lib/public-authoring"); +const v = require("../lib/verifier"); +const stage = require("../scripts/stage-authoring-npm"); +const realpath = fs.realpathSync; +require("../lib/platform"); +const ROOT = path.resolve(__dirname, "../../.."); +const source = Object.fromEntries(stage.STAGE_ALLOWLIST.map(name => { + const bytes = fs.readFileSync(path.join(ROOT, name)); + return [name, { bytes, mode: (fs.lstatSync(path.join(ROOT, name)).mode & 0o111) ? "100755" : "100644", + sha256: c.digest(bytes), git_blob: crypto.createHash("sha1").update(`blob ${bytes.length}\0`).update(bytes).digest("hex") }]; +})); +const clone = value => structuredClone(value); +const hash = n => String(n).padStart(64, "0"); +const LOCATOR = "UAP_PUBLIC_AUTHORING_ASSET_FILE"; +const native = (product, target) => Buffer.from(`harmless ${product} ${target}\n`); +function fixture() { + const input = { schema: codec.INPUT_SCHEMA, identity: { repository: c.REPOSITORY, commit: "a".repeat(40), + engine_revision: "a".repeat(40), versions: { agentplugins: "0.1.99", "plugin-kit-ai": "2.0.0" } }, + authoring_mode: codec.MODE, asset_scope: codec.SCOPE, candidate_sha256: hash(30), pair_marker_sha256: hash(31), products: {}, + preparation: { sha256: hash(32), artifact: { run_id: 101, run_attempt: 2, artifact_id: 301, artifact_sha256: hash(33) } }, + producer: { workflow: codec.WORKFLOW, source: "a".repeat(40), run_id: 201, run_attempt: 3 } }; + const manifests = {}, outers = {}; + for (const product of c.PRODUCTS) { + const assets = {}, version = input.identity.versions[product]; + for (const target of c.TARGETS) { + const binary = native(product, target), outer = product === "agentplugins" ? binary : Buffer.concat([Buffer.from("mock outer\n"), binary]); + assets[target] = { file: c.assetName(product, version, target), ...c.metadata(outer), + binary: { file: c.executableName(product, target), ...c.metadata(binary) } }; + outers[assets[target].file] = outer; + } + input.products[product] = { tag: (product === "agentplugins" ? "agentplugins-v" : "v") + version, + manifest_sha256: hash(40), checksums_sha256: hash(41), assets }; + // Independent literal oracle: no call to the shared projection constructor. + manifests[product] = Buffer.from(JSON.stringify({ schema_version: 3, status: "CANDIDATE", product, + repository: "777genius/universal-agent-plugins", tag: input.products[product].tag, version, + commit: "a".repeat(40), engine_revision: "a".repeat(40), versions: { agentplugins: "0.1.99", "plugin-kit-ai": "2.0.0" }, + candidate_sha256: hash(30), authoring_mode: "release-cli-contract-v1", asset_scope: "six-platform-pair", assets, + release_eligible: false, platform_acceptance: false, attested: false }, null, 2) + "\n"); + const p = input.products[product]; p.manifest_sha256 = c.digest(manifests[product]); + p.checksums_sha256 = c.digest(Buffer.from(c.TARGETS.map(t => `${assets[t].sha256} ${assets[t].file}\n`).join("") + `${p.manifest_sha256} release-manifest.json\n`)); + } + const inputBytes = codec.encodeInputs(input), pair = stage.pairedPackageFiles(source, manifests, inputBytes); + return { input, inputBytes, manifests, pair, outers }; +} + +function memory(t, files, root = "/unit-package") { + const nodes = new Map(), handles = new Map(), calls = [], owner = process.geteuid(); + let serial = 1, fd = 200; + function put(name, body, overrides = {}) { + const dir = body === null; + const node = { body, dev: 1, ino: serial++, mode: dir ? 0o40700 : 0o100644, uid: owner, gid: owner, + nlink: 1, size: dir ? 0 : body.length, mtimeMs: 1, ctimeMs: 1, type: dir ? "directory" : "file", ...overrides }; + nodes.set(name, node); return node; + } + function parents(name) { + let current = path.dirname(name); + while (!nodes.has(current)) { put(current, null, { mode: 0o40755 }); if (current === "/") break; current = path.dirname(current); } + } + for (const [name, bytes] of Object.entries(files)) { const full = path.join(root, name); parents(full); put(full, Buffer.from(bytes)); } + const fault = { action: () => {} }; + const stat = node => ({ ...node, isFile: () => node.type === "file", isDirectory: () => node.type === "directory", isSymbolicLink: () => node.type === "link" }); + function get(name) { const node = nodes.get(name); if (!node) throw Object.assign(new Error(`missing ${name}`), { code: "ENOENT" }); return node; } + for (const [name, fn] of Object.entries({ + lstatSync: name => stat(get(name)), realpathSync: name => { if (name.startsWith(ROOT + "/npm/")) return realpath(name); get(name); return name; }, + openSync: (name, flags) => { assert.equal(flags, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); const id = fd++; handles.set(id, get(name)); return id; }, + fstatSync: id => stat(handles.get(id)), + readSync: (id, buffer, offset, length, position) => { + assert.ok(length <= 65536); return handles.get(id).body.copy(buffer, offset, position, position + length); + }, + closeSync: id => { assert.ok(handles.has(id), "descriptor closed exactly once"); handles.delete(id); } + })) t.mock.method(fs, name, (...args) => { calls.push([name, ...args]); fault.action(name, args); return fn(...args); }); + return { nodes, handles, calls, fault, put, parents, root }; +} + +function engine(t, f, product, warm = false) { + const calls = [], asset = f.input.products[product].assets["linux-amd64"]; + let valid = warm; + const fault = { action: () => {} }; + const invoke = name => { calls.push(name); fault.action(name); }; + const stat = { dev: 2, ino: 2, uid: process.geteuid(), mode: 0o40700, nlink: 1, + isDirectory: () => true, isSymbolicLink: () => false, isFile: () => true }; + const io = Object.fromEntries(["mkdir", "lstat", "mkdtemp", "unlink", "rmdir"].map(name => [name, async () => { + invoke(name); return name === "lstat" ? { ...stat } : name === "mkdtemp" ? "/unit-cache/public-authoring-v2/.download-unit" : undefined; + }])); + t.mock.method(v, "privateDirectory", async () => invoke("directory")); + t.mock.method(v, "acquireLock", async () => { invoke("lock"); return async () => invoke("unlock"); }); + t.mock.method(v, "strictCachedBinary", async (file, pin) => { invoke("strict"); assert.deepEqual(pin, asset.binary); return valid; }); + t.mock.method(v, "commitVerifiedBinary", async (bytes, file, pin) => { + invoke("commit"); assert.deepEqual(bytes, native(product, "linux-amd64")); assert.deepEqual(pin, asset.binary); valid = true; + }); + t.mock.method(v, "downloadFile", async (url, file, pin, options) => { + invoke("download"); assert.equal(url, `https://github.com/${c.REPOSITORY}/releases/download/${f.input.products[product].tag}/${asset.file}`); + assert.deepEqual(pin, asset); assert.deepEqual(Object.keys(options).sort(), ["onOpen", "request", "signal"]); + }); + t.mock.method(c, "readFile", () => { invoke("read-download"); return Buffer.from(f.outers[asset.file]); }); + t.mock.method(c, "unpack", (bytes, name) => { invoke("unpack"); assert.deepEqual(bytes, f.outers[asset.file]); assert.equal(name, asset.binary.file); return native(product, "linux-amd64"); }); + return { calls, fault, stat, options: { packageRoot: "/unit-package", cacheRoot: "/unit-cache", platform: "linux", arch: "x64", io, environment: {} } }; +} +function local(m, f, product) { + const a = f.input.products[product].assets["linux-amd64"], parent = "/custody space ü", file = `${parent}/${a.file}`; + m.put(parent, null); m.put(file, Buffer.from(f.outers[a.file])); return file; +} +function noEffects(e) { assert.deepEqual(e.calls, []); } + +if (require.main === module) { + test("C2 schema both products and six targets", t => { + const f = fixture(); + for (const p of c.PRODUCTS) { + const oracle = codec.projectionBytes(f.input, p); + assert.deepEqual(oracle.manifest, f.manifests[p]); + assert.equal(c.digest(oracle.checksums), f.input.products[p].checksums_sha256); + const m = memory(t, f.pair[p]); + for (const target of c.TARGETS) { + t.diagnostic(`schema ${p}/${target}`); + const r = runtime.loadRelease(p, m.root, target); + assert.deepEqual(r.asset, f.input.products[p].assets[target]); assert.equal(r.descriptor.identity.commit, f.input.identity.commit); + assert.equal(m.handles.size, 0); + } + t.mock.restoreAll(); + } + }); + + test("C2 schema closed metadata and immutable bindings", async t => { + const f = fixture(); + // The 13 existing structural tables cover every nested key/type/canonical + // codec case; here exercise their integration and all package-only fields. + for (const p of c.PRODUCTS) for (const field of ["name", "version", "bin", "engines", "scripts", "private", "files", ...(p === "agentplugins" ? ["os", "cpu"] : [])]) { + t.diagnostic(`package mismatch ${p}/${field}`); + const files = { ...f.pair[p] }, pkg = JSON.parse(files["package.json"]); pkg[field] = "wrong"; files["package.json"] = c.encode(pkg); + const m = memory(t, files), e = engine(t, f, p); + await assert.rejects(runtime.ensureBinary(p, e.options)); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const name of ["native-inputs.json", "public-release.json", "release-manifest.json", "package.json"]) { + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins", true); + e.fault.action = phase => { if (phase === "strict") m.nodes.get(`${m.root}/${name}`).ctimeMs++; }; + await assert.rejects(runtime.ensureBinary("agentplugins", e.options), /changed/); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const p of c.PRODUCTS) { + const input = clone(f.input); input.products[p].checksums_sha256 = hash(99); + const body = codec.encodeInputs(input), files = { ...f.pair.agentplugins, "native-inputs.json": body }; + const d = JSON.parse(files["public-release.json"]); d.input_binding.sha256 = c.digest(body); files["public-release.json"] = codec.encodeDescriptor(d, body, "agentplugins"); + const m = memory(t, files), e = engine(t, f, "agentplugins"); + await assert.rejects(runtime.ensureBinary("agentplugins", e.options), /projection/); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + }); + + test("C2 locator validates before cold and warm effects", async t => { + const f = fixture(); + const bad = [undefined, null, "", " ", 17, "relative", "file:///custody/asset", "~/asset", "/missing", "/a\0b"]; + for (const warm of [false, true]) { + for (const value of bad) { + t.diagnostic(`locator warm=${warm} value=${JSON.stringify(value)}`); + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins", warm); e.options.environment[LOCATOR] = value; + await assert.rejects(runtime.ensureBinary("agentplugins", e.options)); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const change of [ + (m, file) => file.replace("/custody space ü/", "/custody space ü/./"), + (m, file) => file.replace("/custody space ü/", "/custody space ü/../custody space ü/"), + (m, file) => { m.nodes.delete(file); return file; }, + ...["link", "directory", "fifo", "socket", "device"].map(type => (m, file) => { m.nodes.get(file).type = type; return file; }), + ...[{ nlink: 2 }, { uid: process.geteuid() + 1 }, { mode: 0o100666 }, { mode: 0o104644 }, { size: 0 }].map(x => (m, file) => { Object.assign(m.nodes.get(file), x); return file; }), + ...[{ mode: 0o40755 }, { mode: 0o41700 }, { uid: process.geteuid() + 1 }, { type: "link" }].map(x => (m, file) => { Object.assign(m.nodes.get(path.dirname(file)), x); return file; }), + (m, file, e) => { e.options.cacheRoot = "/"; return file; }, + (m, file, e) => { e.options.cacheRoot = path.dirname(file); return file; }, + (m, file, e) => { e.options.cacheRoot = `${path.dirname(file)}/output`; return file; }, + (m, file, e) => { e.options.cacheRoot = m.root; return file; }, + (m, file) => { const dest = `${m.root}/${path.basename(file)}`; m.nodes.set(dest, m.nodes.get(file)); return dest; } + ]) { + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins", warm), file = local(m, f, "agentplugins"); + e.options.environment[LOCATOR] = change(m, file, e); + await assert.rejects(runtime.ensureBinary("agentplugins", e.options)); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + } + for (const warm of [false, true]) { + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins", warm), file = local(m, f, "agentplugins"); + const old = fs.realpathSync; + t.mock.method(fs, "realpathSync", name => name === path.dirname(file) ? "/alias" : old(name)); + e.options.environment[LOCATOR] = file; + await assert.rejects(runtime.ensureBinary("agentplugins", e.options), /ancestor/); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + + }); + + test("C2 snapshot bounds identity and closure", t => { + const body = Buffer.from("bounded fixture"); + for (const delta of [0, 1, -1]) { + const m = memory(t, { file: body }); + if (delta === 0) { const s = snapshots.snapshotPublicFile(`${m.root}/file`, { kind: "metadata", maximum: body.length }); assert.deepEqual(s.bytes, body); s.bytes.fill(0); s.recheck(); s.close(); s.close(); } + else assert.throws(() => snapshots.snapshotPublicFile(`${m.root}/file`, { kind: "metadata", maximum: body.length + delta, exactSize: body.length + delta })); + assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const field of ["ino", "dev", "mode", "uid", "gid", "size", "nlink", "mtimeMs", "ctimeMs", "content", "ancestor", "replacement", "missing"]) { + t.diagnostic(`snapshot recheck ${field}`); + const m = memory(t, { file: body }), file = `${m.root}/file`, s = snapshots.snapshotPublicFile(file, { kind: "metadata", maximum: 100 }); + const node = m.nodes.get(file); + if (field === "content") node.body = Buffer.alloc(body.length, 120); + else if (field === "ancestor") m.nodes.get(m.root).ino++; + else if (field === "replacement") m.put(file, body); + else if (field === "missing") m.nodes.delete(file); + else node[field]++; + assert.throws(() => s.recheck()); s.close(); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const fault of ["openSync", "readSync", "fstatSync", "lstatSync", "closeSync", "growth", "short"]) { + t.diagnostic(`snapshot fault ${fault}`); + const m = memory(t, { file: body }); let fired = false; + m.fault.action = (name, args) => { + if (!fired && name === (fault === "growth" || fault === "short" ? "readSync" : fault)) { + fired = true; + if (fault === "growth" || fault === "short") m.nodes.get(`${m.root}/file`).body = fault === "growth" ? Buffer.concat([body, body]) : body.subarray(1); + else if (fault === "closeSync") { m.handles.delete(args[0]); throw new Error("mock close failure"); } + else throw new Error(`mock ${fault}`); + } + }; + assert.throws(() => { const s = snapshots.snapshotPublicFile(`${m.root}/file`, { kind: "metadata", maximum: 100 }); s.close(); }); + assert.equal(fired, true); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const phase of ["initial", "recheck"]) { + const m = memory(t, { file: body }), controller = new AbortController(); + if (phase === "initial") controller.abort(); + let held; + assert.throws(() => { + held = snapshots.snapshotPublicFile(`${m.root}/file`, { kind: "metadata", maximum: 100, signal: controller.signal }); + controller.abort(); held.recheck(); + }, /cancelled/); + if (held) held.close(); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + + }); + + test("C2 local acquisition uses the shared checked engine", async t => { + const f = fixture(); + for (const p of c.PRODUCTS) for (const warm of [false, true]) { + t.diagnostic(`local ${p} warm=${warm}`); + const m = memory(t, f.pair[p]), e = engine(t, f, p, warm), file = local(m, f, p); + e.options.environment[LOCATOR] = file; + const result = await runtime.ensureBinary(p, e.options); + assert.equal(result.cacheHit, warm); assert.notEqual(result.binaryPath, file); assert.equal(e.calls.includes("download"), false); + assert.equal(e.calls.filter(n => n === "unpack").length, p === "plugin-kit-ai" ? 1 : 0); + assert.equal(e.calls.includes("commit"), !warm); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const failure of ["outer", "inner", "unpack"]) for (const warm of [false, true]) { + t.diagnostic(`local pin failure ${failure} warm=${warm}`); + const p = "plugin-kit-ai", m = memory(t, f.pair[p]), e = engine(t, f, p, warm), file = local(m, f, p); + e.options.environment[LOCATOR] = file; + if (failure === "outer") m.nodes.get(file).body = Buffer.alloc(m.nodes.get(file).size, 120); + else t.mock.method(c, "unpack", () => { if (failure === "unpack") throw new Error("mock unpack"); return Buffer.from("wrong"); }); + await assert.rejects(runtime.ensureBinary(p, e.options)); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + }); + + test("C2 anonymous acquisition keeps canonical transport", async t => { + const f = fixture(); + for (const p of c.PRODUCTS) { + const m = memory(t, f.pair[p]), e = engine(t, f, p); + e.options.environment = { PLUGIN_KIT_AI_REPOSITORY: "wrong", PLUGIN_KIT_AI_VERSION: "wrong", UAP_PUBLIC_AUTHORING_VERIFIED: "true" }; + const result = await runtime.ensureBinary(p, e.options); + assert.equal(result.repository, c.REPOSITORY); assert.ok(e.calls.indexOf("download") < e.calls.indexOf("commit")); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + }); + + test("C2 v2 cache isolation and lock ordering", async t => { + const f = fixture(), paths = new Set(); + for (const p of c.PRODUCTS) for (const target of c.TARGETS) { + const m = memory(t, f.pair[p]), r = runtime.loadRelease(p, m.root, target); + const pathname = runtime.cachePath("/cache", p, target, r); paths.add(pathname); + assert.ok(pathname.startsWith("/cache/public-authoring-v2/")); + assert.notEqual(pathname, runtime.cachePath("/cache", p, target, { ...r, descriptor: { ...r.descriptor, schema: runtime.SCHEMA } })); + for (const changed of [{ ...r, version: "3.0.0" }, { ...r, descriptor: { ...r.descriptor, identity: { ...r.descriptor.identity, commit: "b".repeat(40) } } }, + { ...r, asset: { ...r.asset, binary: { ...r.asset.binary, sha256: hash(99) } } }]) assert.notEqual(pathname, runtime.cachePath("/cache", p, target, changed)); + t.mock.restoreAll(); + } + assert.equal(paths.size, 12); + let expected; + for (const supplied of [false, true]) for (const warm of [false, true]) { + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins", warm); + if (supplied) e.options.environment[LOCATOR] = local(m, f, "agentplugins"); + const r = await runtime.ensureBinary("agentplugins", e.options); + if (expected) assert.equal(r.binaryPath, expected); expected = r.binaryPath; + assert.ok(e.calls.indexOf("lock") < e.calls.indexOf("strict")); assert.equal(e.calls.at(-1), "unlock"); + assert.equal(e.calls.filter(n => n === "strict").length, 2); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const problem of ["timeout", "unsafe", "directory replacement", "final binary"]) { + t.diagnostic(`cache fault ${problem}`); + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins", true); + let strict = 0; + e.fault.action = phase => { + if (problem === "timeout" && phase === "lock") throw Object.assign(new Error("mock lock timeout"), { code: "ETIMEDOUT" }); + if (problem === "unsafe" && phase === "strict") throw new Error("mock unsafe cached object"); + if (problem === "directory replacement" && phase === "strict") e.stat.ino++; + }; + if (problem === "final binary") t.mock.method(v, "strictCachedBinary", async () => ++strict === 1); + await assert.rejects(runtime.ensureBinary("agentplugins", e.options), /timeout|unsafe|replaced|final public/); + assert.equal(e.calls.filter(n => n === "lock").length, 1); assert.equal(e.calls.includes("commit"), false); + assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + + }); + + test("C2 failures cancel and preserve caller state", async t => { + const f = fixture(); + for (const phase of ["mkdir", "directory", "lock", "strict", "download", "commit", "unlock", "cancel", "precommit", "final", "close"]) { + t.diagnostic(`acquisition failure ${phase}`); + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins"), controller = new AbortController(); e.options.signal = controller.signal; + const original = Buffer.from(m.nodes.get(`${m.root}/package.json`).body); let fired = false; + if (phase === "close") m.fault.action = (name, args) => { if (name === "closeSync" && e.calls.includes("commit")) { m.handles.delete(args[0]); throw new Error("mock close failure"); } }; + else e.fault.action = name => { + if (phase === "cancel" && name === "lock") controller.abort(); + if (phase === "precommit" && name === "read-download") m.nodes.get(`${m.root}/package.json`).ino++; + if (phase === "final" && name === "commit") m.nodes.get(`${m.root}/package.json`).ctimeMs++; + if (name === phase && !fired) { fired = true; throw new Error(`mock ${phase}`); } + }; + await assert.rejects(runtime.ensureBinary("agentplugins", e.options)); + assert.deepEqual(m.nodes.get(`${m.root}/package.json`).body, original); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + for (const warm of [false, true]) for (const checkpoint of warm ? ["unpack", "lock", "strict"] : ["unpack", "lock", "strict", "commit"]) { + for (const field of ["ino", "uid", "mode", "nlink", "mtimeMs", "ctimeMs", "content", "parent"]) { + t.diagnostic(`custody recheck warm=${warm} checkpoint=${checkpoint} field=${field}`); + const p = "plugin-kit-ai", m = memory(t, f.pair[p]), e = engine(t, f, p, warm), file = local(m, f, p); + e.options.environment[LOCATOR] = file; let changed = false; + e.fault.action = phase => { + if (phase !== checkpoint || changed) return; changed = true; + if (field === "content") m.nodes.get(file).body = Buffer.alloc(m.nodes.get(file).size, 120); + else if (field === "parent") m.nodes.get(path.dirname(file)).ino++; + else m.nodes.get(file)[field]++; + }; + await assert.rejects(runtime.ensureBinary(p, e.options), /changed|unsafe|regular|custody/); + assert.equal(changed, true); assert.equal(m.handles.size, 0); + if (checkpoint === "unpack") assert.deepEqual(e.calls, ["unpack"]); + t.mock.restoreAll(); + } + } + for (const checkpoint of ["lock", "download", "commit"]) { + const m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins"); + const primary = new Error(`mock primary ${checkpoint}`), cleanup = new Error("mock unlock uncertainty"); + e.fault.action = phase => { if (phase === checkpoint) throw primary; if (phase === "unlock") throw cleanup; }; + let caught; + try { await runtime.ensureBinary("agentplugins", e.options); } catch (error) { caught = error; } + assert.ok(caught); + if (checkpoint === "lock") assert.strictEqual(caught, primary); + else { assert.ok(caught.message.includes(primary.message)); assert.ok(caught.message.includes(cleanup.message)); } + assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + + }); + + test("C2 terminal cancellation rejects after successful awaited unlock", async t => { + const f = fixture(); + for (const p of c.PRODUCTS) for (const warm of [false, true]) for (const supplied of [false, true]) { + t.diagnostic(`terminal cancel ${p} warm=${warm} local=${supplied}`); + const m = memory(t, f.pair[p]), e = engine(t, f, p, warm), controller = new AbortController(); + e.options.signal = controller.signal; + if (supplied) e.options.environment[LOCATOR] = local(m, f, p); + t.mock.method(v, "acquireLock", async () => { + e.calls.push("lock"); + return async () => { await Promise.resolve(); assert.ok(m.handles.size >= 4); e.calls.push("unlock"); controller.abort(); }; + }); + await assert.rejects(runtime.ensureBinary(p, e.options), /cancelled/); + assert.equal(controller.signal.aborted, true); assert.equal(e.calls.at(-1), "unlock"); + assert.equal(e.calls.includes("commit"), !warm); assert.equal(m.handles.size, 0); + // A late rejection may leave the verified cache entry for a fresh operation. + e.options.signal = undefined; + e.fault.action = () => {}; + t.mock.method(v, "acquireLock", async () => async () => {}); + assert.equal((await runtime.ensureBinary(p, e.options)).cacheHit, true); + assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + }); + + test("C2 terminal retained inputs and placement reject after successful awaited unlock", async t => { + const f = fixture(); + for (const p of c.PRODUCTS) for (const warm of [false, true]) for (const field of ["package.json", "native-inputs.json", "public-release.json", "release-manifest.json", "locator", "placement"]) { + t.diagnostic(`terminal replacement ${p} warm=${warm} field=${field}`); + const m = memory(t, f.pair[p]), e = engine(t, f, p, warm), file = local(m, f, p); + e.options.environment[LOCATOR] = file; + if (field === "placement") m.put("/unit-cache", null); + let changed = false; + t.mock.method(v, "acquireLock", async () => { + e.calls.push("lock"); + return async () => { + await Promise.resolve(); assert.equal(m.handles.size, 5); e.calls.push("unlock"); + if (field === "placement") m.put("/unit-cache", null, { type: "link" }); + else { const name = field === "locator" ? file : `${m.root}/${field}`; m.put(name, Buffer.from(m.nodes.get(name).body)); } + changed = true; + }; + }); + await assert.rejects(runtime.ensureBinary(p, e.options), /changed|unsafe ancestor/); + assert.equal(changed, true); assert.equal(e.calls.at(-1), "unlock"); + assert.equal(e.calls.includes("commit"), !warm); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + }); + + test("C2 terminal primary and snapshot close errors aggregate", async t => { + const f = fixture(), m = memory(t, f.pair.agentplugins), e = engine(t, f, "agentplugins"), controller = new AbortController(); + e.options.signal = controller.signal; + e.fault.action = phase => { if (phase === "unlock") controller.abort(); }; + m.fault.action = (name, args) => { + if (name === "closeSync" && e.calls.includes("unlock")) { m.handles.delete(args[0]); throw new Error("terminal close uncertainty"); } + }; + await assert.rejects(runtime.ensureBinary("agentplugins", e.options), error => { + assert.match(error.message, /cancelled/); assert.match(error.message, /terminal close uncertainty/); return true; + }); + assert.equal(m.handles.size, 0); + }); + + test("C2 v1 null and legacy contracts remain unchanged", async t => { + const f = fixture(); + for (const p of c.PRODUCTS) { + const files = stage.packageFiles(p, source, f.manifests[p], { identity: f.input.identity, manifestDigest: f.input.candidate_sha256 }); + assert.equal(Object.keys(files).length, 14); assert.equal(JSON.parse(files["package.json"]).private, true); + for (const locator of ["/anything", ""]) { + const m = memory(t, files), e = engine(t, f, p); + t.mock.method(c, "readFile", file => files[path.basename(file)]); + e.options.environment[LOCATOR] = locator; + await assert.rejects(runtime.ensureBinary(p, e.options), /not qualified/); noEffects(e); assert.equal(m.handles.size, 0); t.mock.restoreAll(); + } + } + }); + + test("C2 child environment removes the locator", async () => { + await require("./public-authoring-v2-pack.test").launchers(fixture()); + assert.deepEqual(runtime.childEnvironment({ [LOCATOR]: "/untrusted", NODE_OPTIONS: "preload", UAP_PUBLIC_AUTHORING_TEST: "true", + UAP_PRIVATE_NPM_ROOT: "private", AGENTPLUGINS_INTERNAL_PROOF_BINARY: "private", PLUGIN_KIT_AI_VERSION: "wrong", + PLUGIN_KIT_AI_REPOSITORY: "wrong", PLUGIN_KIT_AI_RELEASE_BASE_URL: "wrong", KEEP: "yes", PATH: "/ordinary" }), { KEEP: "yes", PATH: "/ordinary" }); + }); +} +module.exports = { fixture, memory, engine, source, local, LOCATOR, native }; diff --git a/npm/agentplugins/test/public-authoring.test.js b/npm/agentplugins/test/public-authoring.test.js index e29634a7..210d051b 100644 --- a/npm/agentplugins/test/public-authoring.test.js +++ b/npm/agentplugins/test/public-authoring.test.js @@ -57,6 +57,15 @@ function fixture(product, binaryFor = (p, t) => Buffer.from(`fixture ${p} ${t}\n }; return { root, packageRoot, cacheRoot, descriptor, manifest, bodies, files, identity, qualify, save }; } +function mockNativeBytes(t) { + const prefix = Buffer.from("synthetic outer\n"); + t.mock.method(c, "archive", (bytes, name) => Buffer.concat([prefix, Buffer.from(name + "\n"), bytes])); + t.mock.method(c, "unpack", (bytes, name) => { + const header = Buffer.concat([prefix, Buffer.from(name + "\n")]); + assert.deepEqual(bytes.subarray(0, header.length), header); + return bytes.subarray(header.length); + }); +} function transport(f, seen = [], alter = res => res) { return (url, options) => { seen.push(url.toString()); @@ -81,7 +90,8 @@ const target = `${process.platform === "win32" ? "windows" : process.platform}-$ if (require.main === module) { for (const p of c.PRODUCTS) { - test(`${p}: preparation and forged bypass reject before any cache/download effect`, async () => { + test(`${p}: preparation and forged bypass reject before any cache/download effect`, async t => { + mockNativeBytes(t); const f = fixture(p); const before = fs.readdirSync(f.cacheRoot); let requests = 0; @@ -91,7 +101,8 @@ if (require.main === module) { assert.equal(requests, 0); assert.deepEqual(fs.readdirSync(f.cacheRoot), before); } finally { delete process.env.UAP_PUBLIC_AUTHORING_VERIFIED; } }); - test(`${p}: cold, offline warm, corrupt recovery and isolation`, async () => { + test(`${p}: cold, offline warm, corrupt recovery and isolation`, async t => { + mockNativeBytes(t); const f = fixture(p); f.qualify(); const seen = []; const cold = await publicAPI.ensureBinary(p, options(f, { request: transport(f, seen) })); assert.equal(cold.cacheHit, false); assert.equal(seen.length, 1); @@ -139,7 +150,8 @@ if (require.main === module) { await assert.rejects(publicAPI.ensureBinary(p, options(f, { request: () => assert.fail("invalid metadata downloaded") }))); } }); - test(`${p}: canonical JSON, package binding and unsafe metadata files`, async () => { + test(`${p}: canonical JSON, package binding and unsafe metadata files`, async t => { + mockNativeBytes(t); for (const name of ["public-release.json", "release-manifest.json", "package.json"]) { for (const change of [b => Buffer.from(b.toString().replace("{", '{"duplicate":1,"duplicate":2,')), b => Buffer.concat([b, Buffer.from([0xff])]), b => Buffer.from(b.toString().trim())]) { diff --git a/scripts/check-packed-ci.py b/scripts/check-packed-ci.py index f498381b..4d4a356f 100644 --- a/scripts/check-packed-ci.py +++ b/scripts/check-packed-ci.py @@ -269,11 +269,26 @@ def walk(file, relative): require(actual == expected, 'sealed tree changed') -def check_public(root, sha): +def public_boundary(native, intake, require_valid_add=False): + require(intake in ('public-fixture/v1', 'public-fixture/v2'), 'explicit public intake') + v2 = intake == 'public-fixture/v2' + require(native['schema'] == 'dual-authoring-public-native/' + ('v2' if v2 else 'v1'), 'public schema substitution') + if v2: + expected = dict(executable_observation='help-and-preflight-rejection', valid_add_dry_run='not_evaluated', + argv=['add', str(Path(native['projects']['agentplugins']) / 'skill'), '--target=codex', '--dry-run', '--format=json'], + reason='production-security-inputs-not-offline') + require(native.get('installer_boundary') == expected, 'outstanding production add requirement') + else: + require('installer_boundary' not in native, 'v2 boundary in v1') + require(not require_valid_add, 'insufficient evidence for successful production add: separate installer acceptance required') + return native.get('installer_boundary') + + +def check_public(root, sha, require_valid_add=False, require_summary=True): run = read(root / 'public-run.json'); false_claims(run) require(run['schema'] == 'public-packed-run/v1' and run['head'] == sha and re.fullmatch('[0-9a-f]{40}', sha), 'public run identity') options = run['options']; request = options['request'] - require(request['intake'] == 'public-fixture/v1' and request['expectedCommit'] == sha, 'public request identity') + require(request['intake'] in ('public-fixture/v1', 'public-fixture/v2') and request['expectedCommit'] == sha, 'public request identity') require(digest(options['nativeTap']) == options['nativeTapSha256'], 'changed public TAP') public_tap(data(options['nativeTap']).decode(), request) require(digest(request['nativeConfig']) == request['nativeConfigSha256'], 'changed public config') @@ -281,11 +296,17 @@ def check_public(root, sha): terminal_path = Path(cfg['evidenceOutput']) / 'public-native-completion.json' require(digest(terminal_path) == request['nativeCompletionSha256'], 'changed public terminal') native = read(terminal_path); false_claims(native, (*CLAIMS, 'signed_promotion', 'public_eligible')) - require(native['schema'] == 'dual-authoring-public-native/v1' and native['status'] == 'completed' and + boundary = public_boundary(native, request['intake'], require_valid_add) + require(native['status'] == 'completed' and native['qualification'] is None and native['identity']['commit'] == native['identity']['engine_revision'] == sha, 'public terminal contract') require(digest(Path(cfg['evidenceOutput']) / 'invocations.json') == native['invocations_sha256'], 'public invocation pin') sealed_path = root / 'bridge-config/sealed.json'; sealed = read(sealed_path); false_claims(sealed) require(sealed['schema'] == 'packed-installer-bridge/v1' and sealed['request'] == request == read(root / 'bridge-config/request.json'), 'public sealed request') + evidence = sealed['inputs'].get('public_evidence', {}) + if boundary is not None: + require(evidence.get('schema') == native['schema'] and evidence.get('installer_boundary') == boundary, 'sealed installer boundary') + else: + require('installer_boundary' not in evidence, 'v2 sealed boundary in v1') repo = Path(__file__).resolve().parent.parent; bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' require(sealed['verifier_sha256'] == digest(bridge) and sealed['helper_sha256'] == digest(bridge.with_name('dual-authoring-candidate.js')), 'public verifier changed') for key in ('go', 'node'): @@ -322,10 +343,268 @@ def check_public(root, sha): len({p['source'] for p in projects}) == 10, 'public ten distinct projects') plans(result) require(log('head').strip() == sha and log('clean') == log('terminal-clean') == '', 'public exact clean checkout') + if boundary is not None and require_summary: + summary = read(root / 'summary.json'); false_claims(summary, (*CLAIMS, 'signed_promotion', 'public_eligible')) + require(summary == dict(status='passed', intake=request['intake'], head=sha, projects=10, plans=30, + scope='public-authoring-help-preflight-and-injected-planner', installer_boundary=boundary, + 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 + + +PROVISION_TARGETS = ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') +PROVISION_CELLS = tuple(t + '/' + lane for t in PROVISION_TARGETS for lane in ('kit-node18', 'pair-node22', 'pair-node24')) +PROVISION_TOOLS = ('node', 'python', 'git', 'gh', 'tar') +PROVISION_FIELDS = ('runner', 'image', 'controller', 'npm_node', 'shim_node', 'npm', 'go', 'mod_cache', 'observer', 'installer_policy') + + +def provision_fields(value, names, label='manifest'): + if type(value) is dict: + for name in names: + missing = name + ':entry' if label in ('controllers', 'cells') else label + ':' + name + require(name in value, 'PUBLIC_PROVISIONING_REQUIRED:' + missing) + require(type(value) is dict and list(value) == list(names), 'closed ordered provision fields') + + +def provision_bytes(file, maximum=1024 * 1024): + import os + file = Path(file) + require(str(file) == os.path.abspath(file) and file.resolve() == file, 'canonical provision path') + before = file.lstat() + require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and 0 < before.st_size <= maximum, + 'bounded regular unaliased provision file') + with open(file, 'rb') as stream: + opened = os.fstat(stream.fileno()) + require((opened.st_dev, opened.st_ino) == (before.st_dev, before.st_ino), 'provision file changed') + body = stream.read(maximum + 1) + after = os.fstat(stream.fileno()) + # Reading may update atime (e.g. relatime on a fresh checkout). Compare + # identity and mutation metadata explicitly, retaining nanosecond precision. + def identity(st): + return (st.st_dev, st.st_ino, st.st_mode, st.st_nlink, st.st_uid, st.st_gid, + st.st_size, st.st_mtime_ns, st.st_ctime_ns) + require(identity(before) == identity(opened) == identity(after) == identity(file.lstat()) and + len(body) == before.st_size, 'provision file changed') + return body + + +def read_provisioning(): + # No caller root, expectedCommit, receipt, PATH or environment selection. + file = Path(__file__).absolute().parent.parent / '.github/authoring-public-tools.json' + try: body = provision_bytes(file) + except FileNotFoundError: raise ValueError('PUBLIC_PROVISIONING_REQUIRED:linux-amd64:manifest') from None + text = body.decode('utf-8'); depth = 0; quoted = escaped = False + for ch in text: + if quoted: + if escaped: escaped = False + elif ch == '\\': escaped = True + elif ch == '"': quoted = False + elif ch == '"': quoted = True + elif ch in '{[': + depth += 1; require(depth <= 8, 'provision depth') + elif ch in '}]': depth -= 1 + value = json.loads(text) + require(body == (json.dumps(value, indent=2, ensure_ascii=False) + '\n').encode(), 'canonical provision JSON') + def pin(v): require(type(v) is str and re.fullmatch('[0-9a-f]{64}', v) and v != '0' * 64, 'provision pin') + def label(v): require(type(v) is str and re.fullmatch('[!-~]{1,256}', v), 'bounded provision identity') + # Absolute path limit: 4096 Unicode code points, shared with JS. + def absolute(v, target): + import ntpath, posixpath + p = ntpath if target.startswith('windows-') else posixpath + require(type(v) is str and 0 < len(v) <= 4096 and not re.search('[\x00-\x1f\x7f]', v) and + p.isabs(v) and p.normpath(v) == v and v not in ('/', p.splitdrive(v)[0] + '\\') and + not v.startswith(('\\\\', '//')), 'canonical provision path') + def closure(v, target): + provision_fields(v, ('root', 'files')); absolute(v['root'], target) + require(type(v['files']) is list and 0 < len(v['files']) <= 4096, 'bounded provision closure') + last = '' + for row in v['files']: + provision_fields(row, ('path', 'sha256')); pin(row['sha256']); name = row['path'] + require(type(name) is str and len(name) <= 4096 and re.fullmatch(r'[A-Za-z0-9_.@+-]+(?:/[A-Za-z0-9_.@+-]+)*', name) and + not set(name.split('/')) & {'.', '..'} and name > last, 'ordered unique relative closure files') + last = name + def tool(v, target, npm=False): + provision_fields(v, ('path', 'version', 'sha256', 'closure') if npm else ('path', 'version', 'sha256')) + absolute(v['path'], target); label(v['version']); pin(v['sha256']) + if npm: + import ntpath, posixpath + closure(v['closure'], target); p = ntpath if target.startswith('windows-') else posixpath + require(any(p.join(v['closure']['root'], *f['path'].split('/')) == v['path'] and f['sha256'] == v['sha256'] + for f in v['closure']['files']), 'npm CLI in complete closure') + provision_fields(value, ('schema', 'controllers', 'cells', 'reader')) + require(value['schema'] == 'authoring-public-tools/v1' and value['reader'] == 'linux-amd64', 'fixed provision reader/schema') + provision_fields(value['controllers'], PROVISION_TARGETS, 'controllers'); provision_fields(value['cells'], PROVISION_CELLS, 'cells') + for target, row in value['controllers'].items(): + provision_fields(row, PROVISION_TOOLS, target) + for v in row.values(): + if v is not None: tool(v, target) + for key, row in value['cells'].items(): + target = key.split('/')[0]; provision_fields(row, PROVISION_FIELDS, key) + require(row['controller'] == target, 'fixed cell controller') + for name in ('runner', 'image', 'observer', 'installer_policy'): + if row[name] is not None: + provision_fields(row[name], ('id', 'sha256')); label(row[name]['id']); pin(row[name]['sha256']) + for name in ('npm_node', 'shim_node', 'npm', 'go'): + if row[name] is not None: + tool(row[name], target, name == 'npm') + if name.endswith('_node'): + require(re.fullmatch('v' + key.split('node')[1] + r'\.[0-9]+\.[0-9]+', row[name]['version']), 'selected Node major') + if row['mod_cache'] is not None: closure(row['mod_cache'], target) + return value + + +def require_authenticated_controller(): + value = read_provisioning() + for name, tool in value['controllers']['linux-amd64'].items(): + message = 'PUBLIC_PROVISIONING_REQUIRED:linux-amd64:' + name + require(tool is not None, message) + try: body = provision_bytes(tool['path'], 256 * 1024 * 1024) + except FileNotFoundError: raise ValueError(message) from None + require(hashlib.sha256(body).hexdigest() == tool['sha256'], 'source-frozen provision pin mismatch:' + name) + return value['controllers']['linux-amd64']['node']['path'] + + +def require_authenticated_execution(): + raise ValueError('C3b execution incomplete: result/installer/observer validators and independent invocation authority required') + + +def authenticated_source(): + # Snapshot trusted source, never receipt-selected source. External provisioning + # keeps this namespace immutable; before/after hashing is not same-UID isolation. + repo = Path(__file__).absolute().parent.parent + files = [] + for directory in ('.github', 'scripts', 'npm/agentplugins/scripts', 'npm/agentplugins/lib', 'npm/plugin-kit-ai/lib'): + def walk(folder): + require(folder.resolve() == folder and stat.S_ISDIR(folder.lstat().st_mode), 'trusted source directory') + for file in sorted(folder.iterdir()): + require(not file.is_symlink(), 'trusted source link') + if file.is_dir(): walk(file) + else: files.append(file) + require(len(files) <= 4096, 'trusted source closure bound') + walk(repo / directory) + return {str(f): hashlib.sha256(provision_bytes(f, 16 * 1024 * 1024)).hexdigest() for f in files} + + +def authenticated_verify(node, argv): + controller = require_authenticated_controller() + require(str(node) == controller, 'source-frozen controller comparison mismatch') + require_authenticated_execution() + import subprocess + repo = Path(__file__).absolute().parent.parent + bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' + source = authenticated_source() + require(require_authenticated_controller() == controller and authenticated_source() == source, 'trusted source/controller changed') + try: + result = subprocess.run([controller, 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) + finally: + require(require_authenticated_controller() == controller and authenticated_source() == source, 'trusted source/controller changed') + 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') + controller = require_authenticated_controller() + require_authenticated_execution() + 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']; require(options['node'] == controller, 'source-frozen controller comparison mismatch') + 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/read-authoring-evidence-zip.py b/scripts/read-authoring-evidence-zip.py new file mode 100644 index 00000000..aadd5107 --- /dev/null +++ b/scripts/read-authoring-evidence-zip.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Bounded intake of a single provider authoring artifact; no trust inference. + +The caller independently acquires the provider digest and exact file closure. +One immutable byte snapshot supplies both the digest check and every ZIP read. +Successful extraction is not native admission, attempt binding or qualification. +""" +import argparse +import hashlib +import io +import json +import os +from pathlib import Path +import re +import stat +import struct +import sys +import zipfile +import zlib + +MAX_ARCHIVE = 2 * 1024**3 +MAX_FILE = 128 * 1024**2 +MAX_TOTAL = 2 * 1024**3 +MAX_ENTRIES = 64 +MAX_RATIO = 200 +NATIVE_FILES = { + "transcripts.json", "trees.json", "build-info.json", "preservation.json", + "preparation.json", "host.json", "scans.json", "acquisition.json", + "agentplugins-terminal.json", "plugin-kit-ai-terminal.json", +} + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def safe_name(name): + require(isinstance(name, str) and 0 < len(name) <= 240, "bounded member path required") + parts = name.split("/") + require(len(parts) <= 3, "member path depth") + for part in parts: + require(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,150}", part) is not None, + "noncanonical member path") + require(not part.endswith(".") and part not in (".", ".."), "member alias") + require(part.split(".")[0].upper() not in { + "CON", "PRN", "AUX", "NUL", *(f"COM{i}" for i in range(10)), + *(f"LPT{i}" for i in range(10)), + }, "reserved member path") + return name + + +def closure(kind, names): + require(isinstance(names, list) and len(names) <= MAX_ENTRIES, "bounded file closure") + for name in names: + safe_name(name) + require(len(set(n.lower() for n in names)) == len(names), "duplicate closure or alias") + if kind == "native": + require(set(names) == NATIVE_FILES, "exact native artifact closure") + else: + require(kind == "preparation" and len(names) == 20, "exact preparation artifact closure") + require(set(n for n in names if not n.startswith(("agentplugins/", "plugin-kit-ai/"))) == { + "candidate/candidate.json", "candidate-identity.json", "pair-prepared.json", "preparation-run.json", + }, "preparation metadata closure") + for product in ("agentplugins", "plugin-kit-ai"): + members = [n for n in names if n.startswith(product + "/")] + require(len(members) == 8 and all(n.count("/") == 1 for n in members), "eight product subjects") + require({product + "/checksums.txt", product + "/release-manifest.json"} <= set(members), + "product metadata closure") + return set(names) + + +def safe_directory(directory): + require(directory.is_absolute(), "absolute directory required") + for entry in (*reversed(directory.parents), directory): + require(stat.S_ISDIR(entry.lstat().st_mode), "real directory ancestors required") + + +def checked_bytes(file, digest, size): + require(re.fullmatch(r"[0-9a-f]{64}", digest) is not None and digest != "0" * 64, "exact ZIP SHA256") + require(type(size) is int and 0 < size <= MAX_ARCHIVE, "ZIP size bound") + file = Path(file) + safe_directory(file.parent) + before = file.lstat() + require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1, "regular unaliased ZIP required") + with os.fdopen(os.open(file, os.O_RDONLY | os.O_NOFOLLOW), "rb") as handle: + opened = os.fstat(handle.fileno()) + require((opened.st_dev, opened.st_ino) == (before.st_dev, before.st_ino), "ZIP identity changed") + require(opened.st_size == size, "ZIP size mismatch") + body = handle.read(size + 1) + identity = lambda s: (s.st_dev, s.st_ino, s.st_size, s.st_mode, s.st_nlink, s.st_mtime_ns, s.st_ctime_ns) + require(identity(os.fstat(handle.fileno())) == identity(opened) and identity(file.lstat()) == identity(before), + "ZIP changed during read") + require(len(body) == size and hashlib.sha256(body).hexdigest() == digest, "ZIP digest/size mismatch") + return body + + +def check_expansion(body, start, info): + # ZipExtFile truncates reads to declared file_size. Independently count the + # complete deflate stream so a forged size/CRC cannot conceal an expansion. + if info.compress_type == zipfile.ZIP_STORED: + require(info.compress_size == info.file_size, "stored ZIP size mismatch") + return + decoder = zlib.decompressobj(-15) + end, cursor, total, crc, pending = start + info.compress_size, start, 0, 0, b"" + while cursor < end or pending: + if not pending: + next_cursor = min(cursor + 65536, end) + pending = memoryview(body)[cursor:next_cursor] + cursor = next_cursor + chunk = decoder.decompress(pending, min(1024 * 1024, info.file_size - total + 1)) + total += len(chunk) + require(total <= info.file_size, "actual ZIP expansion exceeds declared size") + crc = zlib.crc32(chunk, crc) + pending = decoder.unconsumed_tail + require(not decoder.unused_data, "trailing compressed ZIP data") + require(decoder.eof and total == info.file_size and crc == info.CRC, "complete ZIP expansion and CRC required") + + +def extract(file, digest, size, output, kind, names): + expected = closure(kind, names) + body = checked_bytes(file, digest, size) + # Bound central-directory allocation BEFORE ZipFile constructs its entry list. + # Provider artifacts here require ordinary single-disk ZIP, no ZIP64/comments. + require(len(body) >= 22 and body[-22:-18] == b"PK\x05\x06", "ordinary complete ZIP terminator required") + disk, start_disk, count_disk, count, cd_size, cd_offset, comment = struct.unpack_from("<4H2LH", body, len(body) - 18) + require(disk == start_disk == comment == 0 and count_disk == count and 0 < count <= MAX_ENTRIES, + "ZIP entry count or disk bound") + require(cd_size <= 65536 and cd_offset + cd_size == len(body) - 22, "exact bounded ZIP central directory") + output = Path(output) + safe_directory(output.parent) + require(not os.path.lexists(output), "exclusive extraction destination required") + with zipfile.ZipFile(io.BytesIO(body)) as archive: + entries = archive.infolist() + require(len(entries) == count, "ZIP central entry count mismatch") + seen, files, directories, intervals = set(), set(), set(), [] + total = 0 + for info in entries: + directory = info.is_dir() + raw_name = info.filename[:-1] if directory else info.filename + name = safe_name(raw_name) + require(info.orig_filename == info.filename and name.lower() not in seen, "duplicate ZIP entry or alias") + seen.add(name.lower()) + require(not info.extra and not info.comment, "ZIP extra metadata or alias unsupported") + require(info.flag_bits & ~0x808 == 0 and info.compress_type in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED), + "encrypted or unsupported ZIP encoding") + mode = info.external_attr >> 16 + require(not mode & 0o7000 and stat.S_IFMT(mode) in (0, stat.S_IFDIR if directory else stat.S_IFREG), + "ZIP links or special file type") + require(info.external_attr & 0x400 == 0, "ZIP reparse point") + require(not (info.external_attr & 0x10) or directory, "ZIP directory alias") + require(0 <= info.file_size <= MAX_FILE and info.file_size <= MAX_RATIO * max(1, info.compress_size), + "ZIP member size or ratio bound") + total += info.file_size + require(total <= MAX_TOTAL, "ZIP total size bound") + if directory: + require(info.file_size == 0 and any(n.startswith(name + "/") for n in expected), "unexpected ZIP directory") + directories.add(name) + else: + require(name in expected and info.file_size > 0, "unexpected or empty ZIP subject") + files.add(name) + offset = info.header_offset + require(0 <= offset and body[offset:offset + 4] == b"PK\x03\x04", "ZIP local header") + flags, method = struct.unpack_from("