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 85fab3a4..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 @@ -490,11 +532,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 @@ -508,6 +584,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + actions: read steps: - name: Validate promotion identity before checkout env: @@ -534,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 }} @@ -549,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: @@ -572,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 }} @@ -594,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: @@ -644,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 new file mode 100644 index 00000000..69ee9baf --- /dev/null +++ b/.github/workflows/authoring-frozen-native.yml @@ -0,0 +1,194 @@ +name: Frozen Authoring Native Observations + +# 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: + 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_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 }}-${{ fromJSON(inputs.host_contract).target }} + cancel-in-progress: false +jobs: + linux-amd64: + 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 }} + 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: ${{ fromJSON(inputs.host_contract).go_sha256 }} + HOST_CONTRACT: ${{ inputs.host_contract }} + 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}" + [[ "${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 + 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 + - 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: 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'); + 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 }; + 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 + - 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 || 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, 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), + 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-${{ 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 + 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/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml new file mode 100644 index 00000000..63ec96e8 --- /dev/null +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -0,0 +1,149 @@ +name: Authoring Milestone A E2E + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: authoring-milestone-a-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prepare: + name: Exact candidate package + runs-on: ubuntu-24.04 + timeout-minutes: 20 + env: + EXPECTED_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + NODE_OPTIONS: --max-old-space-size=384 + GOTOOLCHAIN: local + GOMAXPROCS: '2' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version: '1.25.13' + cache: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '22.23.2' + check-latest: false + - name: Assemble sealed exact-head native and npm candidate + shell: bash + run: | + set -euo pipefail + root="$RUNNER_TEMP/milestone-a-prepare-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + test ! -e "$root" + mkdir -m 700 "$root" + mkdir -m 700 "$root/evidence" + printf 'MILESTONE_A_BUNDLE=%s\n' "$root" >> "$GITHUB_ENV" + export MILESTONE_A_FAILURE="$root/evidence/workflow-failure.json" + trap 'status=$?; if (( status != 0 )) && [[ ! -e "$MILESTONE_A_FAILURE" ]]; then node -e '\''require("fs").writeFileSync(process.argv[1],JSON.stringify({schema:"milestone-a-e2e-workflow-failure/v1",status:Number(process.argv[2]),publication:false},null,2)+"\\n",{flag:"wx",mode:0o600})'\'' "$MILESTONE_A_FAILURE" "$status" || true; fi' EXIT + config="$RUNNER_TEMP/milestone-a-prepare.json" + modcache="$RUNNER_TEMP/milestone-a-modcache-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + test ! -e "$modcache" + mkdir -m 700 "$modcache" + # Go dependency acquisition precedes local exact-candidate packing. + # Runtime package acquisition uses only the resulting local tarballs; + # production security checks retain credential-free read-only access + # to their pinned public feed and scanner-release endpoints. + GOMODCACHE="$modcache" go mod download + node -e 'const fs=require("fs"),h=require("./npm/agentplugins/scripts/milestone-a-e2e"),tools=h.resolveTrustedTools(process.argv[3],process.argv[5]); fs.writeFileSync(process.argv[1],JSON.stringify({root:process.argv[2],precreatedRoot:true,repo:process.env.GITHUB_WORKSPACE,expectedHead:process.env.EXPECTED_HEAD,...tools,modCache:process.argv[4]}))' \ + "$config" "$root" "$(command -v go)" "$modcache" "$(command -v npm)" + node npm/agentplugins/scripts/milestone-a-e2e.js prepare "$config" + - name: Upload exact candidate bundle and failure evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: milestone-a-exact-candidate-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ env.MILESTONE_A_BUNDLE }} + include-hidden-files: true + if-no-files-found: error + retention-days: 14 + + e2e: + name: Milestone A / ${{ matrix.platform }}-${{ matrix.arch }} + needs: prepare + strategy: + fail-fast: false + matrix: + include: + - {runner: ubuntu-24.04, platform: linux, arch: amd64} + - {runner: windows-2022, platform: windows, arch: amd64} + - {runner: macos-14, platform: darwin, arch: arm64} + runs-on: ${{ matrix.runner }} + timeout-minutes: 12 + env: + NODE_OPTIONS: --max-old-space-size=384 + EXPECTED_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '22.23.2' + check-latest: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: milestone-a-exact-candidate-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/milestone-a-input + - name: Restore sealed candidate modes + if: runner.os != 'Windows' + shell: bash + run: chmod -R a-w "${RUNNER_TEMP}/milestone-a-input/candidate" + - name: Place candidate on the Darwin packageview read-only profile + if: runner.os == 'macOS' + shell: bash + run: | + set -euo pipefail + image="$RUNNER_TEMP/milestone-a-candidate-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT.dmg" + mount="$RUNNER_TEMP/milestone-a-candidate-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + test ! -e "$image" + test ! -e "$mount" + mkdir -m 700 "$mount" + hdiutil create -quiet -srcfolder "$RUNNER_TEMP/milestone-a-input/candidate" -fs APFS -format UDRO "$image" + hdiutil attach -quiet -readonly -nobrowse -mountpoint "$mount" "$image" + printf 'MILESTONE_A_CANDIDATE=%s\n' "$mount" >> "$GITHUB_ENV" + - name: Run both packaged public entrypoints in fresh roots + shell: bash + run: | + set -euo pipefail + outer="$RUNNER_TEMP/milestone-a-outer-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + test ! -e "$outer" + root="$outer/run" + native_root="$(node -e 'process.stdout.write(require("./npm/agentplugins/scripts/milestone-a-e2e").nativeAbsolute(process.argv[1]))' "$root")" + native_outer="$(node -e 'process.stdout.write(require("./npm/agentplugins/scripts/milestone-a-e2e").nativeAbsolute(process.argv[1]))' "$outer")" + native_input="$(node -e 'process.stdout.write(require("./npm/agentplugins/scripts/milestone-a-e2e").nativeAbsolute(process.argv[1]))' "$RUNNER_TEMP/milestone-a-input")" + native_candidate="$(node -e 'process.stdout.write(require("./npm/agentplugins/scripts/milestone-a-e2e").nativeAbsolute(process.argv[1]))' "${MILESTONE_A_CANDIDATE:-$RUNNER_TEMP/milestone-a-input/candidate}")" + npm_cli="$(node -e 'process.stdout.write(require("./npm/agentplugins/scripts/milestone-a-e2e").resolveNpmCLI())')" + printf 'MILESTONE_A_EVIDENCE=%s\n' "$(node -e 'process.stdout.write(require("path").join(process.argv[1],"evidence"))' "$native_root")" >> "$GITHUB_ENV" + node -e 'require("./npm/agentplugins/scripts/milestone-a-e2e").precreate(process.argv[1])' "$native_root" + config="$RUNNER_TEMP/milestone-a-run.json" + native_config="$(node -e 'process.stdout.write(require("./npm/agentplugins/scripts/milestone-a-e2e").nativeAbsolute(process.argv[1]))' "$config")" + node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],precreatedRoot:true,outerRoot:process.argv[3],input:process.argv[4],candidateRoot:process.argv[5],npm:process.argv[6],platform:process.argv[7],arch:process.argv[8],expectedHead:process.env.EXPECTED_HEAD}))' \ + "$native_config" "$native_root" "$native_outer" "$native_input" "$native_candidate" "$npm_cli" '${{ matrix.platform }}' '${{ matrix.arch }}' + node npm/agentplugins/scripts/milestone-a-e2e.js run "$native_config" + test -z "$(git status --porcelain)" + - name: Detach Darwin packageview candidate + if: always() && runner.os == 'macOS' + shell: bash + run: | + if [[ -n "${MILESTONE_A_CANDIDATE:-}" ]]; then + hdiutil detach -quiet "$MILESTONE_A_CANDIDATE" + fi + - name: Upload run evidence, including failures + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: milestone-a-${{ matrix.platform }}-${{ matrix.arch }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ env.MILESTONE_A_EVIDENCE }} + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/authoring-public-packed.yml b/.github/workflows/authoring-public-packed.yml new file mode 100644 index 00000000..b8fb9e7f --- /dev/null +++ b/.github/workflows/authoring-public-packed.yml @@ -0,0 +1,507 @@ +name: Public Packed Authoring +# R1 produce -> completed R1 -> R2 assemble -> completed R2 -> R3 attest +# -> completed R3 -> R4 check. No automatic trigger or publication route. +on: + workflow_dispatch: + inputs: + mode: + description: Separate public invocation + required: true + type: choice + options: [produce, assemble, attest, check] + selected: + description: "Exact tag/ref/source/versions JSON" + required: true + type: string + input: + description: "Original I digest and artifact locator JSON" + required: true + type: string + stage: + description: "Original S digest and artifact locator JSON" + required: true + type: string + journeys: + description: "R2 only: eighteen ordered exact R1 artifact locators" + required: false + type: string + bridge: + description: "R2 only: designated same-cell bridge locator" + required: false + type: string + assembly: + description: "R3/R4 only: original exact R2 assembly locator" + required: false + type: string + acceptance: + description: "R4 only: exact completed R3 artifact locator" + required: false + type: string +permissions: + contents: read + actions: read +concurrency: + group: public-packed-${{ github.ref }}-${{ inputs.mode }}-${{ github.run_id }} + cancel-in-progress: false +env: + PUBLIC_MODE: ${{ inputs.mode }} + PUBLIC_SELECTED: ${{ inputs.selected }} + PUBLIC_INPUT: ${{ inputs.input }} + PUBLIC_STAGE: ${{ inputs.stage }} + PUBLIC_JOURNEYS: ${{ inputs.journeys }} + PUBLIC_BRIDGE: ${{ inputs.bridge }} + PUBLIC_ASSEMBLY: ${{ inputs.assembly }} + PUBLIC_ACCEPTANCE: ${{ inputs.acceptance }} +jobs: + public_inputs: + if: ${{ inputs.mode == 'produce' }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + outputs: + matrix: ${{ steps.evidence.outputs.matrix }} + steps: + - name: Bind canonical dispatch before checkout + shell: python -I -B {0} + run: | + import json, os, re + mode = os.environ['PUBLIC_MODE'] + names = {'selected': 'PUBLIC_SELECTED', 'input': 'PUBLIC_INPUT', 'stage': 'PUBLIC_STAGE', 'journeys': 'PUBLIC_JOURNEYS', 'bridge': 'PUBLIC_BRIDGE', 'assembly': 'PUBLIC_ASSEMBLY', 'acceptance': 'PUBLIC_ACCEPTANCE'} + required = {'selected', 'input', 'stage'} | {'produce': set(), 'assemble': {'journeys', 'bridge'}, 'attest': {'assembly'}, 'check': {'assembly', 'acceptance'}}[mode] + raw = {name: os.environ.get(env, '') for name, env in names.items()} + assert all(bool(body) == (name in required) for name, body in raw.items()) + def unique(pairs): + value = {} + for key, item in pairs: + assert key not in value + value[key] = item + return value + def value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + parsed = json.loads(raw[name], object_pairs_hook=unique, + parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token))) + assert raw[name] == json.dumps(parsed, ensure_ascii=False, indent=2, separators=(',', ': ')) + '\n' + return parsed + def locator(v): + assert set(v) == {'sha256', 'artifact'} and re.fullmatch('[0-9a-f]{64}', v['sha256']) + a = v['artifact']; assert set(a) == {'run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'} and re.fullmatch('[0-9a-f]{64}', a['artifact_sha256']) + assert all(type(a[k]) is int and a[k] > 0 for k in ('run_id', 'run_attempt', 'artifact_id')) and a['run_attempt'] <= 1000 + s = value('selected'); locator(value('input')); locator(value('stage')) + if mode == 'assemble': + journeys = value('journeys'); cells = [f'{target}/{"kit" if node == 18 else "pair"}-node{node}' for target in ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') for node in (18, 22, 24)] + assert type(journeys) is list and len(journeys) == 18 and [journey.get('cell') for journey in journeys] == cells + for journey in journeys: assert set(journey) == {'cell', 'sha256', 'artifact'}; locator({'sha256': journey['sha256'], 'artifact': journey['artifact']}) + locator(value('bridge')) + if mode in ('attest', 'check'): locator(value('assembly')) + if mode == 'check': locator(value('acceptance')) + assert set(s) == {'tag', 'ref', 'source', 'versions'} and type(s['versions']) is dict + assert set(s['versions']) == {'agentplugins', 'plugin-kit-ai'} + assert all(type(version) is str and len(version) <= 32 and re.fullmatch('(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)', version) for version in s['versions'].values()) + assert s['versions']['plugin-kit-ai'] == '2.0.0' and s['versions']['agentplugins'] != s['versions']['plugin-kit-ai'] + assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' + assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' + assert re.fullmatch('[0-9a-f]{40}', s['source']) and s['source'] != '0' * 40 + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + assert s['tag'] == 'agentplugins-v' + s['versions']['agentplugins'] + assert s['ref'] == 'refs/tags/' + s['tag'] == os.environ['GITHUB_REF'] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ fromJSON(inputs.selected).source }} + persist-credentials: false + - name: Independently bootstrap and admit inputs + id: evidence + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + run: | + import importlib.util, os + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + result = packed.public_workflow('inputs') + public_cell: + if: ${{ inputs.mode == 'produce' }} + needs: public_inputs + name: public_cell (${{ matrix.cell }}) + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.public_inputs.outputs.matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + env: + PUBLIC_CELL: ${{ matrix.cell }} + steps: + - name: Bind canonical dispatch before checkout + shell: python -I -B {0} + run: | + import json, os, re + mode = os.environ['PUBLIC_MODE'] + names = {'selected': 'PUBLIC_SELECTED', 'input': 'PUBLIC_INPUT', 'stage': 'PUBLIC_STAGE', 'journeys': 'PUBLIC_JOURNEYS', 'bridge': 'PUBLIC_BRIDGE', 'assembly': 'PUBLIC_ASSEMBLY', 'acceptance': 'PUBLIC_ACCEPTANCE'} + required = {'selected', 'input', 'stage'} | {'produce': set(), 'assemble': {'journeys', 'bridge'}, 'attest': {'assembly'}, 'check': {'assembly', 'acceptance'}}[mode] + raw = {name: os.environ.get(env, '') for name, env in names.items()} + assert all(bool(body) == (name in required) for name, body in raw.items()) + def unique(pairs): + value = {} + for key, item in pairs: + assert key not in value + value[key] = item + return value + def value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + parsed = json.loads(raw[name], object_pairs_hook=unique, + parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token))) + assert raw[name] == json.dumps(parsed, ensure_ascii=False, indent=2, separators=(',', ': ')) + '\n' + return parsed + def locator(v): + assert set(v) == {'sha256', 'artifact'} and re.fullmatch('[0-9a-f]{64}', v['sha256']) + a = v['artifact']; assert set(a) == {'run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'} and re.fullmatch('[0-9a-f]{64}', a['artifact_sha256']) + assert all(type(a[k]) is int and a[k] > 0 for k in ('run_id', 'run_attempt', 'artifact_id')) and a['run_attempt'] <= 1000 + s = value('selected'); locator(value('input')); locator(value('stage')) + if mode == 'assemble': + journeys = value('journeys'); cells = [f'{target}/{"kit" if node == 18 else "pair"}-node{node}' for target in ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') for node in (18, 22, 24)] + assert type(journeys) is list and len(journeys) == 18 and [journey.get('cell') for journey in journeys] == cells + for journey in journeys: assert set(journey) == {'cell', 'sha256', 'artifact'}; locator({'sha256': journey['sha256'], 'artifact': journey['artifact']}) + locator(value('bridge')) + if mode in ('attest', 'check'): locator(value('assembly')) + if mode == 'check': locator(value('acceptance')) + assert set(s) == {'tag', 'ref', 'source', 'versions'} and type(s['versions']) is dict + assert set(s['versions']) == {'agentplugins', 'plugin-kit-ai'} + assert all(type(version) is str and len(version) <= 32 and re.fullmatch('(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)', version) for version in s['versions'].values()) + assert s['versions']['plugin-kit-ai'] == '2.0.0' and s['versions']['agentplugins'] != s['versions']['plugin-kit-ai'] + assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' + assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' + assert re.fullmatch('[0-9a-f]{40}', s['source']) and s['source'] != '0' * 40 + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + assert s['tag'] == 'agentplugins-v' + s['versions']['agentplugins'] + assert s['ref'] == 'refs/tags/' + s['tag'] == os.environ['GITHUB_REF'] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ fromJSON(inputs.selected).source }} + persist-credentials: false + - name: Independently bootstrap and admit produce + id: evidence + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + run: | + import importlib.util, os + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + result = packed.public_workflow('produce', os.environ['PUBLIC_CELL']) + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: public-journey-${{ strategy.job-index }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.evidence.outputs.root }} + if-no-files-found: error + retention-days: 7 + public_producer_complete: + if: ${{ always() && inputs.mode == 'produce' }} + needs: [public_inputs, public_cell] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require all eighteen cells and the same-live-root bridge + shell: python -I -B {0} + env: + INPUTS_RESULT: ${{ needs.public_inputs.result }} + CELLS_RESULT: ${{ needs.public_cell.result }} + run: | + import os + assert os.environ['INPUTS_RESULT'] == os.environ['CELLS_RESULT'] == 'success' + public_assemble: + if: ${{ inputs.mode == 'assemble' }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Bind canonical dispatch before checkout + shell: python -I -B {0} + run: | + import json, os, re + mode = os.environ['PUBLIC_MODE'] + names = {'selected': 'PUBLIC_SELECTED', 'input': 'PUBLIC_INPUT', 'stage': 'PUBLIC_STAGE', 'journeys': 'PUBLIC_JOURNEYS', 'bridge': 'PUBLIC_BRIDGE', 'assembly': 'PUBLIC_ASSEMBLY', 'acceptance': 'PUBLIC_ACCEPTANCE'} + required = {'selected', 'input', 'stage'} | {'produce': set(), 'assemble': {'journeys', 'bridge'}, 'attest': {'assembly'}, 'check': {'assembly', 'acceptance'}}[mode] + raw = {name: os.environ.get(env, '') for name, env in names.items()} + assert all(bool(body) == (name in required) for name, body in raw.items()) + def unique(pairs): + value = {} + for key, item in pairs: + assert key not in value + value[key] = item + return value + def value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + parsed = json.loads(raw[name], object_pairs_hook=unique, + parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token))) + assert raw[name] == json.dumps(parsed, ensure_ascii=False, indent=2, separators=(',', ': ')) + '\n' + return parsed + def locator(v): + assert set(v) == {'sha256', 'artifact'} and re.fullmatch('[0-9a-f]{64}', v['sha256']) + a = v['artifact']; assert set(a) == {'run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'} and re.fullmatch('[0-9a-f]{64}', a['artifact_sha256']) + assert all(type(a[k]) is int and a[k] > 0 for k in ('run_id', 'run_attempt', 'artifact_id')) and a['run_attempt'] <= 1000 + s = value('selected'); locator(value('input')); locator(value('stage')) + if mode == 'assemble': + journeys = value('journeys'); cells = [f'{target}/{"kit" if node == 18 else "pair"}-node{node}' for target in ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') for node in (18, 22, 24)] + assert type(journeys) is list and len(journeys) == 18 and [journey.get('cell') for journey in journeys] == cells + for journey in journeys: assert set(journey) == {'cell', 'sha256', 'artifact'}; locator({'sha256': journey['sha256'], 'artifact': journey['artifact']}) + locator(value('bridge')) + if mode in ('attest', 'check'): locator(value('assembly')) + if mode == 'check': locator(value('acceptance')) + assert set(s) == {'tag', 'ref', 'source', 'versions'} and type(s['versions']) is dict + assert set(s['versions']) == {'agentplugins', 'plugin-kit-ai'} + assert all(type(version) is str and len(version) <= 32 and re.fullmatch('(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)', version) for version in s['versions'].values()) + assert s['versions']['plugin-kit-ai'] == '2.0.0' and s['versions']['agentplugins'] != s['versions']['plugin-kit-ai'] + assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' + assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' + assert re.fullmatch('[0-9a-f]{40}', s['source']) and s['source'] != '0' * 40 + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + assert s['tag'] == 'agentplugins-v' + s['versions']['agentplugins'] + assert s['ref'] == 'refs/tags/' + s['tag'] == os.environ['GITHUB_REF'] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ fromJSON(inputs.selected).source }} + persist-credentials: false + - name: Independently bootstrap and admit assemble + id: evidence + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + run: | + import importlib.util, os + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + result = packed.public_workflow('assemble') + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: public-assembly-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.evidence.outputs.root }} + if-no-files-found: error + retention-days: 7 + public_evidence_intake: + if: ${{ inputs.mode == 'attest' }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Bind canonical dispatch before checkout + shell: python -I -B {0} + run: | + import json, os, re + mode = os.environ['PUBLIC_MODE'] + names = {'selected': 'PUBLIC_SELECTED', 'input': 'PUBLIC_INPUT', 'stage': 'PUBLIC_STAGE', 'journeys': 'PUBLIC_JOURNEYS', 'bridge': 'PUBLIC_BRIDGE', 'assembly': 'PUBLIC_ASSEMBLY', 'acceptance': 'PUBLIC_ACCEPTANCE'} + required = {'selected', 'input', 'stage'} | {'produce': set(), 'assemble': {'journeys', 'bridge'}, 'attest': {'assembly'}, 'check': {'assembly', 'acceptance'}}[mode] + raw = {name: os.environ.get(env, '') for name, env in names.items()} + assert all(bool(body) == (name in required) for name, body in raw.items()) + def unique(pairs): + value = {} + for key, item in pairs: + assert key not in value + value[key] = item + return value + def value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + parsed = json.loads(raw[name], object_pairs_hook=unique, + parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token))) + assert raw[name] == json.dumps(parsed, ensure_ascii=False, indent=2, separators=(',', ': ')) + '\n' + return parsed + def locator(v): + assert set(v) == {'sha256', 'artifact'} and re.fullmatch('[0-9a-f]{64}', v['sha256']) + a = v['artifact']; assert set(a) == {'run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'} and re.fullmatch('[0-9a-f]{64}', a['artifact_sha256']) + assert all(type(a[k]) is int and a[k] > 0 for k in ('run_id', 'run_attempt', 'artifact_id')) and a['run_attempt'] <= 1000 + s = value('selected'); locator(value('input')); locator(value('stage')) + if mode == 'assemble': + journeys = value('journeys'); cells = [f'{target}/{"kit" if node == 18 else "pair"}-node{node}' for target in ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') for node in (18, 22, 24)] + assert type(journeys) is list and len(journeys) == 18 and [journey.get('cell') for journey in journeys] == cells + for journey in journeys: assert set(journey) == {'cell', 'sha256', 'artifact'}; locator({'sha256': journey['sha256'], 'artifact': journey['artifact']}) + locator(value('bridge')) + if mode in ('attest', 'check'): locator(value('assembly')) + if mode == 'check': locator(value('acceptance')) + assert set(s) == {'tag', 'ref', 'source', 'versions'} and type(s['versions']) is dict + assert set(s['versions']) == {'agentplugins', 'plugin-kit-ai'} + assert all(type(version) is str and len(version) <= 32 and re.fullmatch('(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)', version) for version in s['versions'].values()) + assert s['versions']['plugin-kit-ai'] == '2.0.0' and s['versions']['agentplugins'] != s['versions']['plugin-kit-ai'] + assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' + assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' + assert re.fullmatch('[0-9a-f]{40}', s['source']) and s['source'] != '0' * 40 + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + assert s['tag'] == 'agentplugins-v' + s['versions']['agentplugins'] + assert s['ref'] == 'refs/tags/' + s['tag'] == os.environ['GITHUB_REF'] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ fromJSON(inputs.selected).source }} + persist-credentials: false + - name: Independently bootstrap and admit attest + id: evidence + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + run: | + import importlib.util, os + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + result = packed.public_workflow('attest') + public_evidence_attestation: + if: ${{ inputs.mode == 'attest' }} + needs: public_evidence_intake + # Only the accepted hosted signer receives E2 signing permissions. + permissions: + contents: read + actions: read + id-token: write + attestations: write + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Bind canonical dispatch before checkout + shell: python -I -B {0} + run: | + import json, os, re + mode = os.environ['PUBLIC_MODE'] + names = {'selected': 'PUBLIC_SELECTED', 'input': 'PUBLIC_INPUT', 'stage': 'PUBLIC_STAGE', 'journeys': 'PUBLIC_JOURNEYS', 'bridge': 'PUBLIC_BRIDGE', 'assembly': 'PUBLIC_ASSEMBLY', 'acceptance': 'PUBLIC_ACCEPTANCE'} + required = {'selected', 'input', 'stage'} | {'produce': set(), 'assemble': {'journeys', 'bridge'}, 'attest': {'assembly'}, 'check': {'assembly', 'acceptance'}}[mode] + raw = {name: os.environ.get(env, '') for name, env in names.items()} + assert all(bool(body) == (name in required) for name, body in raw.items()) + def unique(pairs): + value = {} + for key, item in pairs: + assert key not in value + value[key] = item + return value + def value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + parsed = json.loads(raw[name], object_pairs_hook=unique, + parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token))) + assert raw[name] == json.dumps(parsed, ensure_ascii=False, indent=2, separators=(',', ': ')) + '\n' + return parsed + def locator(v): + assert set(v) == {'sha256', 'artifact'} and re.fullmatch('[0-9a-f]{64}', v['sha256']) + a = v['artifact']; assert set(a) == {'run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'} and re.fullmatch('[0-9a-f]{64}', a['artifact_sha256']) + assert all(type(a[k]) is int and a[k] > 0 for k in ('run_id', 'run_attempt', 'artifact_id')) and a['run_attempt'] <= 1000 + s = value('selected'); locator(value('input')); locator(value('stage')) + if mode == 'assemble': + journeys = value('journeys'); cells = [f'{target}/{"kit" if node == 18 else "pair"}-node{node}' for target in ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') for node in (18, 22, 24)] + assert type(journeys) is list and len(journeys) == 18 and [journey.get('cell') for journey in journeys] == cells + for journey in journeys: assert set(journey) == {'cell', 'sha256', 'artifact'}; locator({'sha256': journey['sha256'], 'artifact': journey['artifact']}) + locator(value('bridge')) + if mode in ('attest', 'check'): locator(value('assembly')) + if mode == 'check': locator(value('acceptance')) + assert set(s) == {'tag', 'ref', 'source', 'versions'} and type(s['versions']) is dict + assert set(s['versions']) == {'agentplugins', 'plugin-kit-ai'} + assert all(type(version) is str and len(version) <= 32 and re.fullmatch('(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)', version) for version in s['versions'].values()) + assert s['versions']['plugin-kit-ai'] == '2.0.0' and s['versions']['agentplugins'] != s['versions']['plugin-kit-ai'] + assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' + assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' + assert re.fullmatch('[0-9a-f]{40}', s['source']) and s['source'] != '0' * 40 + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + assert s['tag'] == 'agentplugins-v' + s['versions']['agentplugins'] + assert s['ref'] == 'refs/tags/' + s['tag'] == os.environ['GITHUB_REF'] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ fromJSON(inputs.selected).source }} + persist-credentials: false + - name: Independently bootstrap and admit attest + id: evidence + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + run: | + import importlib.util, os + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + result = packed.public_workflow('attest') + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: ${{ steps.evidence.outputs.subjects }} + - name: Re-admit original custody and compare every post-sign byte + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + PUBLIC_SIGNED_ROOT: ${{ steps.evidence.outputs.root }} + run: | + import importlib.util, os + from pathlib import Path + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + checked = packed.public_workflow('attest') + def closure(root): + root = Path(root) + result = {} + for p in sorted(root.rglob('*')): + assert not p.is_symlink() + if p.is_file(): + result[str(p.relative_to(root))] = packed.provision_bytes(p, 16 * 1024 * 1024) + else: + assert p.is_dir() + return result + assert closure(os.environ['PUBLIC_SIGNED_ROOT']) == closure(checked['root']) + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: public-evidence-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.evidence.outputs.root }} + if-no-files-found: error + retention-days: 7 + public_check: + if: ${{ inputs.mode == 'check' }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Bind canonical dispatch before checkout + shell: python -I -B {0} + run: | + import json, os, re + mode = os.environ['PUBLIC_MODE'] + names = {'selected': 'PUBLIC_SELECTED', 'input': 'PUBLIC_INPUT', 'stage': 'PUBLIC_STAGE', 'journeys': 'PUBLIC_JOURNEYS', 'bridge': 'PUBLIC_BRIDGE', 'assembly': 'PUBLIC_ASSEMBLY', 'acceptance': 'PUBLIC_ACCEPTANCE'} + required = {'selected', 'input', 'stage'} | {'produce': set(), 'assemble': {'journeys', 'bridge'}, 'attest': {'assembly'}, 'check': {'assembly', 'acceptance'}}[mode] + raw = {name: os.environ.get(env, '') for name, env in names.items()} + assert all(bool(body) == (name in required) for name, body in raw.items()) + def unique(pairs): + value = {} + for key, item in pairs: + assert key not in value + value[key] = item + return value + def value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + parsed = json.loads(raw[name], object_pairs_hook=unique, + parse_constant=lambda token: (_ for _ in ()).throw(ValueError(token))) + assert raw[name] == json.dumps(parsed, ensure_ascii=False, indent=2, separators=(',', ': ')) + '\n' + return parsed + def locator(v): + assert set(v) == {'sha256', 'artifact'} and re.fullmatch('[0-9a-f]{64}', v['sha256']) + a = v['artifact']; assert set(a) == {'run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'} and re.fullmatch('[0-9a-f]{64}', a['artifact_sha256']) + assert all(type(a[k]) is int and a[k] > 0 for k in ('run_id', 'run_attempt', 'artifact_id')) and a['run_attempt'] <= 1000 + s = value('selected'); locator(value('input')); locator(value('stage')) + if mode == 'assemble': + journeys = value('journeys'); cells = [f'{target}/{"kit" if node == 18 else "pair"}-node{node}' for target in ('linux-amd64', 'linux-arm64', 'darwin-amd64', 'darwin-arm64', 'windows-amd64', 'windows-arm64') for node in (18, 22, 24)] + assert type(journeys) is list and len(journeys) == 18 and [journey.get('cell') for journey in journeys] == cells + for journey in journeys: assert set(journey) == {'cell', 'sha256', 'artifact'}; locator({'sha256': journey['sha256'], 'artifact': journey['artifact']}) + locator(value('bridge')) + if mode in ('attest', 'check'): locator(value('assembly')) + if mode == 'check': locator(value('acceptance')) + assert set(s) == {'tag', 'ref', 'source', 'versions'} and type(s['versions']) is dict + assert set(s['versions']) == {'agentplugins', 'plugin-kit-ai'} + assert all(type(version) is str and len(version) <= 32 and re.fullmatch('(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)', version) for version in s['versions'].values()) + assert s['versions']['plugin-kit-ai'] == '2.0.0' and s['versions']['agentplugins'] != s['versions']['plugin-kit-ai'] + assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' + assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' + assert re.fullmatch('[0-9a-f]{40}', s['source']) and s['source'] != '0' * 40 + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + assert s['tag'] == 'agentplugins-v' + s['versions']['agentplugins'] + assert s['ref'] == 'refs/tags/' + s['tag'] == os.environ['GITHUB_REF'] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ fromJSON(inputs.selected).source }} + persist-credentials: false + - name: Independently bootstrap and admit read + id: evidence + shell: python -I -B {0} + env: + GH_TOKEN: ${{ github.token }} + run: | + import importlib.util, os + spec = importlib.util.spec_from_file_location('packed', 'scripts/check-packed-ci.py') + packed = importlib.util.module_from_spec(spec) + spec.loader.exec_module(packed) + result = packed.public_workflow('read') 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 fda5cdcd..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" { @@ -493,3 +530,543 @@ func TestReleasePairedPromotionShellSyntax(t *testing.T) { } } } + +func TestFrozenNativeReadOnlyWorkflowContract(t *testing.T) { + w := readProducerWorkflow(t, "authoring-frozen-native.yml") + 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' && (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" { + 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 != "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++ + 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@") { + 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|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.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) + } + } +} + +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") + } +} + +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/commands.go b/cli/plugin-kit-ai/internal/authoring/commands/commands.go index edbc4163..cd4f6c05 100644 --- a/cli/plugin-kit-ai/internal/authoring/commands/commands.go +++ b/cli/plugin-kit-ai/internal/authoring/commands/commands.go @@ -335,8 +335,8 @@ func (a App) init(ctx context.Context, req request) (report.Report, error) { } destination = cwd + string(os.PathSeparator) + destination } - result, e := scaffold.Apply(ctx, plan, scaffold.ApplyOptions{Destination: destination, Validate: func(ctx context.Context, stage string) error { - p, e := a.Projects.Read(ctx, stage) + result, e := scaffold.Apply(ctx, plan, scaffold.ApplyOptions{Destination: destination, Validate: func(ctx context.Context, stage string, dir *os.Root) error { + p, e := a.Projects.ReadGeneratedStaging(ctx, stage, dir) r = report.Build("init", a.Revision, p, false) r.Mode = "local_mutation" if req.disclose { 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/commands/vertical_fix_test.go b/cli/plugin-kit-ai/internal/authoring/commands/vertical_fix_test.go index 996a9dbf..83bdf33d 100644 --- a/cli/plugin-kit-ai/internal/authoring/commands/vertical_fix_test.go +++ b/cli/plugin-kit-ai/internal/authoring/commands/vertical_fix_test.go @@ -142,7 +142,7 @@ func TestReviewInitCleanupFailurePrecedence(t *testing.T) { if err != nil { t.Fatal(err) } - result, err := scaffold.Apply(fault, plan, scaffold.ApplyOptions{Destination: dest, Validate: func(ctx context.Context, stage string) error { + result, err := scaffold.Apply(fault, plan, scaffold.ApplyOptions{Destination: dest, Validate: func(ctx context.Context, stage string, _ *os.Root) error { p, err := (project.Service{Scratch: scratch}).Read(ctx, stage) if err != nil { return err diff --git a/cli/plugin-kit-ai/internal/authoring/project/project.go b/cli/plugin-kit-ai/internal/authoring/project/project.go index ec224bdc..f8a159a7 100644 --- a/cli/plugin-kit-ai/internal/authoring/project/project.go +++ b/cli/plugin-kit-ai/internal/authoring/project/project.go @@ -5,6 +5,7 @@ package project import ( "context" "errors" + "os" "strings" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packageview" @@ -23,12 +24,36 @@ type Service struct { Limits packageview.Limits } -func (s Service) Read(ctx context.Context, exactRoot string) (result Result, err error) { +func (s Service) Read(ctx context.Context, exactRoot string) (Result, error) { + return s.read(ctx, exactRoot, packageview.GeneratedStaging{}) +} + +// ReadGeneratedStaging validates a package this process just generated into +// its own private, exclusively owned staging directory, immediately before +// scaffold.Apply's atomic publish -- never external or user-selected content. +// +// dir must be the live handle scaffold's own Validate callback received for +// stagingRoot; it is used only to prove identity (see +// packageview.NewGeneratedStaging), never for a second read path. That proof +// authorizes relaxing only the Darwin profile's read-only-mount requirement +// for this one directory. Ordinary validate/inspect/test/doctor/compat +// requests call Read, which never receives this proof, and they never hold a +// pre-opened handle for a caller-supplied root, so they have no path to this +// method either. +func (s Service) ReadGeneratedStaging(ctx context.Context, exactRoot string, dir *os.Root) (result Result, err error) { + generated, err := packageview.NewGeneratedStaging(dir) + if err != nil { + return result, err + } + return s.read(ctx, exactRoot, generated) +} + +func (s Service) read(ctx context.Context, exactRoot string, generated packageview.GeneratedStaging) (result Result, err error) { exactRoot, err = readRoot(exactRoot) if err != nil { return result, err } - lease, err := (packageview.Reader{TempDir: s.Scratch, Limits: s.Limits}).Open(ctx, exactRoot) + lease, err := (packageview.Reader{TempDir: s.Scratch, Limits: s.Limits, Generated: generated}).Open(ctx, exactRoot) if err != nil { return result, err } diff --git a/cli/plugin-kit-ai/internal/authoring/project/project_test.go b/cli/plugin-kit-ai/internal/authoring/project/project_test.go index 6cddd5a0..01b317b2 100644 --- a/cli/plugin-kit-ai/internal/authoring/project/project_test.go +++ b/cli/plugin-kit-ai/internal/authoring/project/project_test.go @@ -189,3 +189,39 @@ func TestCapturedCommandContainment(t *testing.T) { empty(t, scratch) } } + +// ReadGeneratedStaging is the seam scaffold.Apply's own self-validation uses; +// it must behave exactly like Read for an ordinary caller-owned root (same +// facts, same cleanup) and must require a live directory handle, never a bare +// path. A proof built for a different directory is rejected only where the +// profile can act on it at all (Darwin; see source_darwin_test.go) -- Linux +// and Windows never required a read-only mount and ignore it, exactly as +// they ignore the zero value. +func TestReadGeneratedStagingMatchesReadAndRequiresLiveHandle(t *testing.T) { + writableNative(t) + root, scratch := t.TempDir(), t.TempDir() + put(t, root, "plugin.json", core) + s := Service{Scratch: scratch} + want, e := s.Read(context.Background(), root) + if e != nil { + t.Fatal(e) + } + empty(t, scratch) + dir, e := os.OpenRoot(root) + if e != nil { + t.Fatal(e) + } + defer dir.Close() + got, e := s.ReadGeneratedStaging(context.Background(), root, dir) + if e != nil { + t.Fatal(e) + } + if got.Input.Identity.Digest != want.Input.Identity.Digest || got.Facts.Package == nil { + t.Fatalf("ReadGeneratedStaging diverged from Read: %+v vs %+v", got, want) + } + empty(t, scratch) + if _, e := s.ReadGeneratedStaging(context.Background(), root, nil); e == nil { + t.Fatal("nil handle accepted") + } + empty(t, scratch) +} diff --git a/cli/plugin-kit-ai/internal/authoring/scaffold/apply.go b/cli/plugin-kit-ai/internal/authoring/scaffold/apply.go index 4e04607d..6a744590 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/apply.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/apply.go @@ -18,7 +18,14 @@ import ( // all leases before returning. A no-op success callback violates this contract. // This service deliberately contains no second package parser. A function's // semantics cannot be checked at runtime; composition tests must prove them. -type Validate func(ctx context.Context, stagingRoot string) error +// +// dir is a live handle to the exact stagingRoot directory, still held open by +// Apply. It exists only so the callback can prove -- by identity, not by a +// second path lookup -- that it is validating this operation's own freshly +// created, exclusively owned payload, never caller-selected content. The +// callback MUST NOT write through dir; it is passed only for that identity +// proof (see packageview.GeneratedStaging). +type Validate func(ctx context.Context, stagingRoot string, dir *os.Root) error type ApplyOptions struct { Destination string // clean absolute missing destination; parent must exist @@ -165,7 +172,7 @@ func apply(ctx context.Context, p Plan, o ApplyOptions, ops applyOps) (result Re if e = ctx.Err(); e != nil { return result, e } - if e = o.Validate(ctx, stagingPath); e != nil { + if e = o.Validate(ctx, stagingPath, root); e != nil { return result, fmt.Errorf("validate generated package: %w", e) } if e = ctx.Err(); e != nil { diff --git a/cli/plugin-kit-ai/internal/authoring/scaffold/apply_test.go b/cli/plugin-kit-ai/internal/authoring/scaffold/apply_test.go index 1c0f3c4f..d79dfd12 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/apply_test.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/apply_test.go @@ -39,8 +39,8 @@ func TestFailureCancellationAndCleanup(t *testing.T) { if kind == "commit-failure" { ops.rename = func(*os.File, string, *os.File, string) error { return sentinel } } - result, err := apply(ctx, planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string) error { - if err := validate(ctx, s); err != nil { + result, err := apply(ctx, planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, root *os.Root) error { + if err := validate(ctx, s, root); err != nil { return err } switch kind { @@ -169,8 +169,8 @@ func TestStagingReplacementRefusesForeignCleanup(t *testing.T) { fault := errors.New("abort after denied stage replacement") blocked := false var original, replaced string - result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string) error { - if err := validate(ctx, s); err != nil { + result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, root *os.Root) error { + if err := validate(ctx, s, root); err != nil { return err } original = filepath.Dir(s) @@ -197,7 +197,7 @@ func TestStagingReplacementRefusesForeignCleanup(t *testing.T) { if err != nil || !result.Committed || result.Destination != dest { t.Fatalf("commit after denied attack: %+v %v", result, err) } - if err := validate(context.Background(), dest); err != nil { + if err := validate(context.Background(), dest, nil); err != nil { t.Fatal(err) } assertOnly(t, parent, "out", "unowned") @@ -238,8 +238,8 @@ func TestParentReplacementRefusesCommitAndCleansOwnedStage(t *testing.T) { attempted, blocked := false, false dest := filepath.Join(parent, "out") p := planFor(t, "skill") - result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string) error { - if err := validate(ctx, s); err != nil { + result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, root *os.Root) error { + if err := validate(ctx, s, root); err != nil { return err } attempted = true @@ -265,7 +265,7 @@ func TestParentReplacementRefusesCommitAndCleansOwnedStage(t *testing.T) { if err != nil || !result.Committed || result.Destination != dest { t.Fatalf("commit after denied attack: %+v %v", result, err) } - if err := validate(context.Background(), dest); err != nil { + if err := validate(context.Background(), dest, nil); err != nil { t.Fatal(err) } assertOnly(t, parent, "out", "unowned") @@ -315,8 +315,8 @@ func TestPayloadReplacementDoesNotDeleteForeignTree(t *testing.T) { parent := tempRoot(t) validate := realValidation(t) var foreign, moved string - result, err := Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: filepath.Join(parent, "out"), Validate: func(ctx context.Context, s string) error { - if err := validate(ctx, s); err != nil { + result, err := Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: filepath.Join(parent, "out"), Validate: func(ctx context.Context, s string, root *os.Root) error { + if err := validate(ctx, s, root); err != nil { return err } foreign = s @@ -428,12 +428,12 @@ func TestPostCommitCleanupErrorRetainsCommittedResult(t *testing.T) { } return nil }} - result, err := apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string) error { container = filepath.Dir(s); return validate(ctx, s) }}, ops) + result, err := apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, root *os.Root) error { container = filepath.Dir(s); return validate(ctx, s, root) }}, ops) var cleanup *CleanupError if !errors.As(err, &cleanup) || !result.Committed || result.Destination != dest { t.Fatalf("lost committed result: %+v %v", result, err) } - if err := validate(context.Background(), dest); err != nil { + if err := validate(context.Background(), dest, nil); err != nil { t.Fatal(err) } b, readErr := os.ReadFile(filepath.Join(container, "retained")) diff --git a/cli/plugin-kit-ai/internal/authoring/scaffold/scaffold_test.go b/cli/plugin-kit-ai/internal/authoring/scaffold/scaffold_test.go index 4db7c4d1..1a027f58 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/scaffold_test.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/scaffold_test.go @@ -69,7 +69,7 @@ func realValidation(t *testing.T) Validate { if err != nil { t.Fatal(err) } - return func(ctx context.Context, root string) error { + return func(ctx context.Context, root string, _ *os.Root) error { envelope, err := (loader.Loader{Registry: registry}).Load(ctx, domain.LoadInput{SnapshotRoot: root}) if err != nil { return err @@ -117,7 +117,7 @@ func TestTemplateGoldenTreesAndCurrentStandardLoader(t *testing.T) { dest := filepath.Join(root, "result") calls := 0 validate := realValidation(t) - result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string) error { + result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, dir *os.Root) error { calls++ if filepath.Dir(filepath.Dir(s)) != root || s == dest { t.Fatal("not private sibling staging") @@ -128,7 +128,7 @@ func TestTemplateGoldenTreesAndCurrentStandardLoader(t *testing.T) { t.Fatalf("nonprivate stage: %v %v", i, e) } } - return validate(ctx, s) + return validate(ctx, s, dir) }}) if err != nil || !result.Committed || calls != 1 { t.Fatalf("apply: %+v %v calls=%d", result, err, calls) @@ -138,7 +138,7 @@ func TestTemplateGoldenTreesAndCurrentStandardLoader(t *testing.T) { t.Fatalf("output tree differs: %v", treeGolden(actual)) } assertOnly(t, root, "result") - if err = validate(context.Background(), dest); err != nil { + if err = validate(context.Background(), dest, nil); err != nil { t.Fatal(err) } // Schema validation is independent evidence of static conformance for both @@ -448,7 +448,7 @@ func TestAllExistingDestinationsPreserved(t *testing.T) { t.Fatal(err) } called := false - result, err := Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(context.Context, string) error { called = true; return errors.New("must not validate") }}) + result, err := Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(context.Context, string, *os.Root) error { called = true; return errors.New("must not validate") }}) after, e := os.Lstat(dest) if err == nil || e != nil || result.Committed || called || !os.SameFile(before, after) { t.Fatalf("existing destination changed: %v %v", result, err) @@ -526,8 +526,8 @@ func TestConcurrentApplyExactlyOneWinner(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - r, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string) error { - if err := validate(ctx, s); err != nil { + r, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, dir *os.Root) error { + if err := validate(ctx, s, dir); err != nil { return err } ready <- struct{}{} diff --git a/cli/plugin-kit-ai/internal/authoring/scaffold/stage_windows_test.go b/cli/plugin-kit-ai/internal/authoring/scaffold/stage_windows_test.go index a63a2c04..7ee97467 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/stage_windows_test.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/stage_windows_test.go @@ -22,7 +22,7 @@ func TestWindowsPrivateStageDACL(t *testing.T) { t.Fatal(err) } dest := filepath.Join(tempRoot(t), "output") - _, err = Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, root string) error { + _, err = Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, root string, dir *os.Root) error { descriptor, err := windows.GetNamedSecurityInfo(filepath.Dir(root), windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION) if err != nil { return err @@ -49,7 +49,7 @@ func TestWindowsPrivateStageDACL(t *testing.T) { if ace.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE || sid.String() != user.User.Sid.String() { t.Fatal("unexpected private stage trustee") } - return validate(ctx, root) + return validate(ctx, root, dir) }}) if err != nil { t.Fatal(err) @@ -128,9 +128,9 @@ func TestDeniedParent(t *testing.T) { t.Logf("installed parent DACL: %s", actual.String()) deniedParentProbeCreates(t, parent, "denied", true) validated := false - r, err := Apply(context.Background(), plan, ApplyOptions{Destination: filepath.Join(parent, "out"), Validate: func(ctx context.Context, root string) error { + r, err := Apply(context.Background(), plan, ApplyOptions{Destination: filepath.Join(parent, "out"), Validate: func(ctx context.Context, root string, dir *os.Root) error { validated = true - return validate(ctx, root) + return validate(ctx, root, dir) }}) // makePrivateStage returns raw NTSTATUS; an unrelated validation failure // must not satisfy this fixture (nor does a fabricated callback error). 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/docs/STANDARD_FIRST_AUTHORING_ENGINE_IMPLEMENTATION_PLAN.md b/docs/STANDARD_FIRST_AUTHORING_ENGINE_IMPLEMENTATION_PLAN.md index 551a503e..24ff8c5d 100644 --- a/docs/STANDARD_FIRST_AUTHORING_ENGINE_IMPLEMENTATION_PLAN.md +++ b/docs/STANDARD_FIRST_AUTHORING_ENGINE_IMPLEMENTATION_PLAN.md @@ -1,5 +1,185 @@ # Standard-First Authoring Engine Implementation Plan +## Owner decision: accelerated MVP scope (2026-09-10) + +The current delivery target is **Milestone A**, shipped as the smallest coherent +standard-first vertical slice. Phases 7-11 remain an approved roadmap, but they +do not block the current MVP PR, draft qualification, or merge. Re-enter each +later phase through its own bounded plan and PR after the MVP is stable. + +The accelerated MVP contains only: + +- one shared Go implementation behind `agentplugins author` and + `plugin-kit-ai`; +- `init`, `validate`, `inspect`, `compat`, `doctor`, and offline `test`; +- Skill and stdio/remote MCP templates that validate without running package + content; +- local installer planning through `agentplugins add --dry-run` in a + disposable client root; +- truthful public documentation and historical v1 guidance; +- draft artifacts and provenance sufficient to test the exact candidate. + +The accelerated MVP explicitly defers runtime execution, `dev`, `bootstrap`, +normalize/import/migration, client projection generation, export/bundle, +Directory submission, remote publishing, and legacy code isolation. Deferred +commands stay absent from public help. Useful YAML implementation, dependencies, +tests, templates, and design ideas remain preserved under the capability +preservation contract; narrower `plugin.json` support is never deletion +authorization. + +### Minimal evidence gate + +Run the following evidence once on the exact merge candidate: + +1. Focused unit and contract tests for changed boundaries, including forbidden + legacy reads, atomic filesystem behavior, entrypoint parity, and unchanged + installer safety. +2. One genuine clean-root E2E through **each** public entrypoint: + `init -> validate -> inspect -> test -> local add --dry-run`, using only new + sandbox projects and fixture client homes. Compare normalized JSON contracts + and resulting package tree digests. + Package acquisition is exact-candidate and local: the E2E must never resolve + the authored package from a registry or `latest` tag. The production + installer security boundary remains enabled and may perform credential-free, + read-only requests to its pinned public Directory, Discovery, Security Index, + and scanner-release endpoints. Record that distinction in evidence; do not + claim the complete installer journey is offline and do not introduce a + test-only bypass into the release binary. +3. Linux amd64 runs the complete E2E. Windows amd64 and macOS arm64 run packaged + launcher smoke for both entrypoints plus init/validate and cleanup. Other + released architectures require build/package checks, not duplicate full + product/runtime matrices. +4. Exact-head required repository CI and one independent code review. +5. Draft artifact identity, checksum, provenance, install, and readback. Public + release still requires separate owner approval for the concrete version. + +Do not require an eighteen-cell product/runtime matrix, four independent hosted +workflow invocations, duplicate evidence replays, every package-manager channel, +or phases 7-11 for this MVP. Existing tests that cheaply protect a real defect +may remain, but their repetition is not a release gate. Do not weaken security, +path containment, cancellation/cleanup, deterministic output, fail-closed +verification, or the ban on real user-project execution. + +### Delivery choice + +Chosen: thin Milestone A release slice. + +- 🎯 confidence 9/10 🛡️ reliability 9/10 🧠 complexity 4/10 +- Approximate remaining change: 1,500-3,500 total changed lines, including + tests, CI fixes, wrappers and docs; 450-1,200 logical production lines. + +Alternatives rejected for the current delivery: + +- Complete phases 0-11 before merge: 🎯 5/10 🛡️ 8/10 🧠 10/10; + approximately 12,000-30,000 changed lines remain and useful delivery waits + behind unrelated migration/publication work. +- Ship only wording and mocks: 🎯 4/10 🛡️ 2/10 🧠 2/10; + approximately 100-500 lines, but it does not satisfy the requested working + E2E and is not an acceptable substitute. + +Future hosted implementation and review workers use `gpt-5.6-sol`, reasoning +effort `low`, service tier `default` (no fast), unless the owner changes this +profile again. + +## Owner clarification: preserve legacy capabilities (2026-09-06) + +This clarification controls every phase, inventory, worker assignment and +acceptance check below. Standard-first command retirement is not permission to +delete the corresponding implementation. A capability missing from plugin.json +or Agent Plugins 1.0 must not be discarded for that reason. + +Preserve useful plugin.yaml implementation, necessary dependencies, tests and +design documentation outside the standard authoring dependency graph. Reuse or +adapt it through a narrow neutral interface when a planned consumer needs it; +do not build a second engine or move files merely to create an archive. + +Before any legacy deletion, record each capability's implementation, tests, +consumers, exact standard mapping if one exists, preservation destination and +support status. Classify it as reuse, adapt, preserve/defer, or explicitly +approved removal. Default unresolved cases to preserve/defer. Neither lack of a +standard field nor lack of a current standard-first caller proves dead code. +Deletion requires the owner's explicit acceptance of that capability's removal. + +Normal standard authoring still reads only plugin.json and its standard +components, with no YAML fallback or legacy model dependency. Keeping source +does not advertise a supported v2 command. A maintained separate YAML entrypoint +is not implicitly authorized and remains a separate product decision. + +Historical v1 binaries alone do not satisfy code preservation. See +[the preservation contract](./AUTHORING_CAPABILITY_PRESERVATION.md) and the +Phase 11 inventory gate. Current standard CLI/npm work may continue; blanket +legacy deletion is not authorized. + +## Owner decision: early public wording and site checkpoint (2026-09-08) + +The owner explicitly authorizes prioritizing a stable intermediate PR merge and +public site update before the full v2 CLI release and phases 7-11. This is a +partial delivery of this plan, not a smaller replacement for its full objective. +It supersedes preparation-document rules that require D5 merely to publish +truthful public wording; full executable release qualification still requires D5. + +Scope: replace obsolete primary positioning with clear Use plugins / Build +plugins journeys, consistent maintained locales, canonical links and explicit +availability labels. Describe unreleased standard authoring commands as +preparation, never as installed or currently executable features. Preserve +accurate instructions for currently available products and the historical v1 +reference, including redirects. Verify actual published versions before claiming +availability; do not relabel an unreleased candidate as the current version. + +Implement a minimal explicit public-documentation boundary. The existing +DOCS_PREPARATION_PREVIEW flag remains restricted to disposable non-published +previews: do not enable it in production, remove the check without a replacement, +or use noindex alone as proof of safe publication. Update conflicting preparation +docs and their tests in the same bounded PR so subsequent agents see this owner +decision. Do not create a second docs engine or a generic release platform. + +Checkpoint acceptance: + +- Every maintained locale distinguishes available installation from unreleased + authoring; no runnable future command is presented as current. +- All Use/Build/history links and redirects resolve in the actual built site; + canonical-English fallback is explicit where a translation is absent. +- Counter, geometry, catalog and affected navigation E2E pass with genuine + verified feeds, original assertions/timeouts and no masked retries. +- The production-mode docs/landing build succeeds without the preview flag; + actual Pages assembly and public-boundary checks pass on the merge candidate. +- Independent review and required CI cover the exact merge candidate. Merge a + dependency-safe, reversible checkpoint and verify the resulting deployed site. + +Do not hold this checkpoint for unrelated runtime, migration, export, publish or +legacy-isolation phases. Do not merge the current preparation stack unchanged: +main/master auto-deploys Pages, so establish and verify the boundary first. +CLI asset release, npm/PyPI tags, Homebrew and native qualification remain +separate gates; this checkpoint does not attest them or activate future commands. +Useful YAML capabilities and all preservation constraints above remain intact. + +The preliminary 100-500 changed-line estimate is a target, not a guarantee. +Re-estimate after bounded intake against the actual merge base; do not weaken +acceptance or expand scope just to satisfy that number. Hosted workers use the +current owner-selected profile recorded above: `gpt-5.6-sol`, reasoning `low`, +service tier `default` (no fast). + +Checkpoint delivery evidence (2026-09-08): [PR #190](https://github.com/777genius/universal-agent-plugins/pull/190) +merged as `dc28313ab6f567eb86eecac3ef903f79b584d4c3`. +Final source head `e93a8ee0ceaba62befe42aa125c10c7c1aff5f8b` passed +all applicable CI; the separate native Windows authoring rename-sharing test +still failed and is not claimed as qualified. Production-artifact browser proof +was 34 passing scenarios plus one tooltip failure; independent trace review +identified an offscreen-focus scroll race, a one-line test positioning fix kept +all assertions/timeouts, and the complete affected scenario passed without retry. +This is composite evidence, not a fresh 35-test pass. Pages run 34170377916 was +superseded by [34170488596](https://github.com/777genius/universal-agent-plugins/actions/runs/34170488596) +on descendant `758c1656e1d3e6f1783b96638839486416921453` after independently +merged PR #194. The newer deployment passed. Public HTTP readback on 2026-09-08 verified +HTTP 200, availability wording and all 62 historical fragment IDs across five +quickstarts, plus the create-plugin journey and history link. The owner lifted the storage pause for this task. Full deployed-site browser E2E +then completed: 30 passed, 5 failed, no skipped tests or retries (179091.334 ms). +All nine added locale/anchor/geometry cases passed. Five navigation/detail cases +remain unresolved and are under independent hosted trace review; publication +alone is not proof that this remaining acceptance is complete. PR #194 changes Windows init; its qualification is separate from +PR #190 evidence and must be checked before carrying it into the authoring stack. +This checkpoint does not complete phases 0-11 or release the new authoring CLI. + ## Status - Decision: approved for planning. @@ -50,13 +230,14 @@ The migration is a strangler refactor: 2. Move reusable command construction out of `package main`. 3. Convert commands by user job, starting with a complete `init -> validate -> inspect -> test` vertical slice. -4. Preserve useful command names, but do not preserve behavior whose only - purpose was generating client manifests from `plugin/plugin.yaml`. +4. Preserve useful command names and implementations. Keep YAML-specific + generation outside standard authoring; classify it for preservation or + adaptation instead of deleting it because the portable standard is narrower. 5. Provide one explicit, non-destructive legacy project importer. 6. Migrate first-party examples and documentation. -7. Remove the old manifest engine from normal authoring after the migration - acceptance gate, while retaining one isolated read-only importer for the - announced migration-support window. +7. Detach legacy wiring from normal standard authoring after the migration + gate. Retain the isolated importer and useful legacy implementations with + their tests; deletion follows the explicit capability-preservation gate. Expected implementation size: @@ -96,8 +277,10 @@ Go/no-go gates: through both entrypoints in disposable roots. 4. **Release gate:** native assets and wrappers bind the same authoring engine revision and current documentation teaches no legacy format. -5. **Retirement gate:** old production packages are deleted only after consumer - inventory, first-party migration, and released migration tooling. +5. **Preservation gate:** after consumer inventory, first-party migration and + released migration tooling, detach legacy standard-command wiring. Preserve + useful implementations and tests; each deletion needs an inventoried + capability decision explicitly accepted by the owner. ## Highest-risk assumptions @@ -124,9 +307,10 @@ Go/no-go gates: 4. Keep installation and authoring in one source repository and one domain model while preserving separate command responsibilities. 5. Keep both public entrypoints behaviorally identical for authoring commands. -6. Remove the current `plugin/plugin.yaml` authoring path from new documentation, - templates, source packages, and future releases after first-party migration; - historical release artifacts remain immutable. +6. Stop teaching `plugin/plugin.yaml` as the current standard authoring path + after first-party migration. Preserve historical design docs, useful templates + and implementation in explicitly labelled legacy boundaries; historical + release artifacts remain immutable. 7. Preserve installer security, lifecycle state, rollback, Directory trust, and client adapters without coupling them to authoring concerns. 8. Remain forward-compatible with later Agent Plugins specification versions @@ -382,7 +566,7 @@ The adaptation boundary is the project model. Reused services must accept a standard-first project or narrower capability interface, not `pluginmanifest.Manifest`. -### Must be replaced or retired +### Excluded from the standard domain; preserve implementation pending review - `plugin/plugin.yaml` as the authored root. - `targets` stored inside the authored manifest. @@ -1259,7 +1443,9 @@ Record the approved standard-first authoring decision before behavior changes. 3. Mark `PLUGIN_STANDARD_AND_PUBLISH_PLAN.md` and `PLUGIN_YAML_V1_SPEC.md` as historical, not current Agent Plugins guidance. 4. Record the forbidden dependency directions and classify every current - command as reuse, adapt, migrate, or retire. + command as reuse, adapt, migrate, or retire from the standard interface. + Track implementation disposition separately as reuse, adapt, preserve/defer, + or owner-approved removal; retiring a command never implies code deletion. 5. Capture current CLI `--help`, JSON outputs, release artifacts, and test baselines for intentional-diff review. @@ -1545,8 +1731,9 @@ Complete a useful offline authoring loop without executing package content. 2. Make default `test` compose conformance, authoring hygiene, static Skills, and MCP configuration checks without resolving or starting executables. 3. Reuse Skills init/validate logic under the package `skills/` root. -4. Remove the external npm Skills lifecycle wrappers from the authoring tree; - document their independent replacement when needed. +4. Exclude external npm Skills lifecycle wrappers from standard authoring + command wiring; preserve their implementation and tests under the capability + inventory contract. Document an independent replacement only when it exists. 5. Record the embedded Agent Skills profile identity in JSON results. ### Edge cases @@ -1583,6 +1770,10 @@ installer's ability to load otherwise usable package components. ## Phase 6 - Dual-entrypoint MVP release and documentation +Delivery order: first deliver the owner-approved early public wording/site +checkpoint above; then qualify and publish the full dual-entrypoint MVP. Public +wording acceptance does not satisfy executable-release acceptance below. + ### Summary Publish the proven core authoring slice through both command names before @@ -1629,10 +1820,12 @@ building migration, advanced packaging, and remote publication features. ### Tests - install both entrypoints in fresh isolated environments; -- run the same golden authoring flow through each; +- run the same golden authoring flow through each once on Linux amd64; - compare JSON output and resulting tree digests; -- upgrade/uninstall/reinstall smoke on supported OSes; -- npm provenance/checksum/cache tests; +- run packaged launcher/init/validate/cleanup smoke on Windows amd64 and macOS + arm64; use build/package checks for other released architectures; +- verify only the package channels included in this MVP candidate; +- verify draft provenance, checksum, install and readback; - docs build, link checker, and landing smoke; - v1-to-v2 invocation tests for every removed, renamed, or deferred command. @@ -1646,7 +1839,9 @@ package. ### Acceptance criteria - both entrypoints execute the same authoring engine revision; -- Milestone A is usable without a registry, account, OAuth, or client install; +- Milestone A authors and acquires the candidate package without a package + registry, account, OAuth, or preinstalled client. Local installer planning may + read the production public security feeds required by its fail-closed policy; - the main docs contain no current workflow requiring `plugin/plugin.yaml`; - a new user can distinguish install versus author in one screen; - deferred commands are absent; bounded v1 error shims never claim success or @@ -1865,12 +2060,12 @@ artifacts. - the CLI performs no direct merge or ownership claim; any conditional merge is attributable to registry-owned protected policy and remains auditable. -## Phase 11 - Legacy retirement +## Phase 11 - Legacy isolation and capability preservation ### Summary -Remove the old authoring model after standard-first parity and migration are -proven. +Detach the old model from standard authoring after parity and migration are +proven, while preserving useful legacy capabilities and their implementation. ### Preconditions @@ -1878,22 +2073,29 @@ proven. - standard-first CLI and docs released; - migration command released and tested; - npm/Homebrew/PyPI transition documented; -- no current CI job depends on old manifests; +- standard authoring/release CI does not require legacy manifests; isolated + tests for preserved implementations may still use explicit legacy fixtures; - repository-wide search classifies every remaining `plugin.yaml` reference as - a canonical legacy path, historical fixture, or migration test. + a preserved legacy implementation/design, canonical legacy path, historical + fixture, or migration test; +- every capability has a reviewed preservation disposition before removal. ### Detailed implementation steps -1. Remove old templates and normal command wiring. +1. Detach old templates and command wiring from standard authoring; preserve + useful template source and tests in their documented legacy boundary. 2. Remove old lifecycle aliases from the authoring binary. -3. Remove unused `pluginmanifest`, generation, and publication code in bounded - dependency-safe PRs. -4. Retain only the isolated read-only legacy importer, its minimal fixtures, and - migration documentation. It does not become a general legacy domain library. +3. Complete the capability inventory with source, tests, consumers, mapping, + preservation destination and support status. Preserve unresolved code. +4. Retain the isolated read-only importer plus useful legacy implementations, + required dependencies, tests and design documentation outside the standard + authoring graph. Remove only individually reviewed, owner-approved items in + bounded PRs. No automatic deletion based on unused standard-first imports. 5. Mark `plugin-kit-ai` v1 npm/PyPI versions deprecated without deleting historical artifacts. -6. Remove old runtime packages only after a consumer audit proves they are not - needed by maintained examples. +6. Audit old runtime consumers and preserve useful packages even when no + maintained example currently calls them. Removal needs a recorded capability + decision and explicit owner acceptance, not only an unused-import search. ### Edge cases @@ -1909,12 +2111,15 @@ proven. - repository-wide forbidden-import and forbidden-generated-file checks; - clean build/test/package from a fresh clone; - released standard-first smoke; -- legacy source accepted only by migration command; -- old releases remain downloadable. +- standard authoring accepts legacy source only through explicit migration; + preserved legacy tests remain isolated and do not create implicit fallback; +- old releases remain downloadable; +- retained implementations and their required tests remain in source; +- every deletion matches a reviewed inventory item explicitly accepted by the owner. ### Rollback / kill switch -Revert one bounded removal PR. Do not restore the old authoring path inside a +Revert one bounded isolation or owner-approved removal PR. Do not restore the old authoring path inside a new standard package. Historical binaries remain the fallback for an old project while it is migrated. @@ -1924,6 +2129,8 @@ project while it is migrated. `plugin/plugin.yaml`; - explicit migration remains available through a narrow isolated reader until a future major version removes it under a separately announced support policy; +- every legacy capability has an explicit preservation/adaptation destination + or owner-approved removal decision; unresolved cases remain preserved; - `plugin-kit-ai` means standard-first authoring; - `agentplugins` and `plugin-kit-ai` share one authoring implementation. @@ -2015,6 +2222,10 @@ project while it is migrated. ## Test strategy +For the accelerated MVP, the minimal evidence gate near the top of this plan +controls. The broader strategy below applies when the corresponding deferred +phase is entered; it is not cumulative pre-MVP work. + ### Unit tests - command factories and flag isolation; @@ -2066,17 +2277,23 @@ At minimum: ## CI gates Each bounded PR runs only the relevant focused suite plus required repository -checks. The final release candidate runs: +checks. The accelerated MVP merge candidate runs: 1. Go tests for standard domain, loader, authoring, installer, and command roots. 2. Conformance adapter suite. 3. Template golden and generated-package validation matrix. 4. Static forbidden-import/forbidden-manifest checks. -5. Cross-platform build and launcher smoke. -6. Deterministic archive comparison. -7. npm/Homebrew/PyPI package verification where affected. +5. Full Linux amd64 E2E for both entrypoints, packaged launcher/init/validate + smoke on Windows amd64 and macOS arm64, and build/package checks for other + released architectures. +6. Deterministic generated-tree comparison between the two entrypoints. +7. Verification for only the package channels changed by the candidate. 8. Documentation build and link check. -9. One full clean-clone E2E of each public authoring entrypoint. +9. One full clean-clone E2E of each public authoring entrypoint on Linux amd64. + +The full eighteen-cell runtime matrix, repeated cross-host evidence replay, and +four-invocation orchestration are not MVP gates. Add them later only when a +specific supported runtime, publisher, or custody boundary requires them. Do not block independent implementation work on long CI when focused gates are already available. Do not rerun a fully proven exact head unless code, @@ -2114,6 +2331,9 @@ may include the selected root only when explicitly requested. ## Proposed PR sequence and review budget +For the accelerated delivery, PRs 1-8 comprise the active roadmap through +Milestone A. Items 9-16 are deferred backlog and do not block the current MVP. + Approximate implementation budget: Phase 0 counts handwritten ADR/plan lines; Phases 1-10 estimate production code and exclude tests, generated fixtures, and generated documentation. @@ -2131,7 +2351,7 @@ generated documentation. | 8 | Normalize/import/migration | 800-1,400 lines | | 9 | Preview/export/bundle | 700-1,200 lines | | 10 | GitHub/Directory publication | 500-900 lines | -| 11 | Legacy retirement | Net deletion; migration shims only | +| 11 | Legacy isolation and preservation | Re-estimate from capability inventory; no assumed net deletion | Re-estimate after Phases 2 and 6 using actual changed production/test lines and reuse achieved. Do not protect an early estimate by hiding necessary policy @@ -2173,8 +2393,9 @@ identified generated docs, golden fixtures, and mechanical file moves. - deterministic archives and bundle inspection. 15. `feat(authoring): publish standard packages` - exact GitHub release and Directory submission plan. -16. `refactor(authoring): retire plugin yaml engine` - - one or more bounded deletion PRs after preconditions pass. +16. `refactor(authoring): isolate legacy capabilities` + - preserve useful implementation and tests outside standard authoring; + deletion PRs only for individually owner-approved inventory items. The exact PR count may shrink when adjacent changes remain below the review budget and share one invariant. Do not combine installer lifecycle changes, @@ -2206,11 +2427,16 @@ Exit criteria: shared MCP, and client-specific activation outcomes; - no legacy manifest emitted or read; - installer tests remain green; -- both native entrypoints and supported wrappers are released from the same - engine revision. +- both native entrypoints and included wrappers are qualified from the same + engine revision as draft artifacts; public release requires the separate + version-specific owner approval; +- the minimal evidence gate above passes on the exact merge candidate. ### Milestone B - Useful parity +Deferred after the accelerated MVP. It is not part of the current PR's +completion denominator. + Adds: - runtime test; @@ -2230,12 +2456,15 @@ Exit criteria: - standard archives are deterministic; - old authoring docs are no longer primary. -### Milestone C - Publish and retirement +### Milestone C - Publish and capability preservation + +Deferred after Milestone B. It is not part of the current PR's completion +denominator. Adds: - GitHub release and Directory submission; -- legacy code retirement. +- legacy isolation and reviewed capability preservation. Size depends on how much existing publication infrastructure can be cleanly adapted. Re-estimate after Milestone B rather than inventing a large platform @@ -2243,11 +2472,16 @@ up front. ## Final acceptance criteria +The checklist below remains the **full roadmap** acceptance. For the current +accelerated delivery, completion means Milestone A plus its minimal evidence +gate; unchecked Milestone B/C items are tracked backlog, not a reason to hold +the MVP merge. + The program is complete when all of the following are true: -- [ ] `plugin.json` is the only current authoring manifest. -- [ ] No normal command reads or creates `plugin/plugin.yaml`. -- [ ] Legacy input is accepted only by explicit non-destructive migration. +- [ ] `plugin.json` is the only current standard authoring manifest. +- [ ] No normal standard authoring command reads or creates `plugin/plugin.yaml`. +- [ ] Standard authoring accepts legacy input only by explicit non-destructive migration. - [ ] `plugin-kit-ai` and `agentplugins author` invoke one Go implementation. - [ ] Equivalent commands have equivalent JSON contracts, exit codes, and filesystem effects. @@ -2264,7 +2498,9 @@ The program is complete when all of the following are true: - [ ] Current docs and templates do not teach the old format. - [ ] First-party examples are migrated or explicitly classified as non-portable client extensions. -- [ ] Old current-release authoring engine is removed after migration gates. +- [ ] Standard command wiring is independent of the legacy model after migration gates. +- [ ] Useful legacy implementation, dependencies, tests and design documentation + are preserved; each deletion has an explicit owner-approved inventory decision. ## Decision summary diff --git a/install/integrationctl/agentplugins/adapters/packageview/bootstrap_directory_guard_windows_test.go b/install/integrationctl/agentplugins/adapters/packageview/bootstrap_directory_guard_windows_test.go index 99988796..3fa8cdb4 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/bootstrap_directory_guard_windows_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/bootstrap_directory_guard_windows_test.go @@ -46,7 +46,7 @@ func TestWindowsBootstrapDirectoryGuard(t *testing.T) { t.Fatal(err) } // Production must proceed through NTFS inventory and protected root pins. - s, err := openSource(root) + s, err := openSource(root, GeneratedStaging{}) if err != nil { t.Fatalf("production bootstrap: %v", err) } diff --git a/install/integrationctl/agentplugins/adapters/packageview/contract_test.go b/install/integrationctl/agentplugins/adapters/packageview/contract_test.go index 6d322007..dd8ed982 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/contract_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/contract_test.go @@ -3,6 +3,7 @@ package packageview import ( "context" "errors" + "os" "testing" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packagedigest" @@ -61,3 +62,57 @@ func TestLimitsAndSafeErrors(t *testing.T) { t.Fatal(e) } } + +// GeneratedStaging is the only seam that can relax Darwin's read-only-mount +// requirement. It must be impossible to build one without an already open, +// live directory handle: a caller holding only a path string (every ordinary +// validate/inspect/test request) can never obtain one, on any platform. +func TestGeneratedStagingRequiresLiveHandle(t *testing.T) { + if _, e := NewGeneratedStaging(nil); e == nil { + t.Fatal("nil handle accepted") + } + var zero GeneratedStaging + if zero.present() { + t.Fatal("zero value reports present") + } + if zero.matches(nil) { + t.Fatal("zero value matches nil") + } + dir, e := os.OpenRoot(t.TempDir()) + if e != nil { + t.Fatal(e) + } + g, e := NewGeneratedStaging(dir) + if e != nil { + t.Fatal(e) + } + if !g.present() { + t.Fatal("live handle did not produce a present proof") + } + info, e := dir.Stat(".") + if e != nil { + t.Fatal(e) + } + if !g.matches(info) { + t.Fatal("proof does not match the directory it was built from") + } + other, e := os.OpenRoot(t.TempDir()) + if e != nil { + t.Fatal(e) + } + defer other.Close() + otherInfo, e := other.Stat(".") + if e != nil { + t.Fatal(e) + } + if g.matches(otherInfo) { + t.Fatal("proof matched an unrelated directory") + } + if e := dir.Close(); e != nil { + t.Fatal(e) + } + // Close does not invalidate the already captured identity snapshot. + if !g.present() || !g.matches(info) { + t.Fatal("proof invalidated by closing the source handle") + } +} diff --git a/install/integrationctl/agentplugins/adapters/packageview/race_linux_test.go b/install/integrationctl/agentplugins/adapters/packageview/race_linux_test.go index d56c3554..562af272 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/race_linux_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/race_linux_test.go @@ -183,7 +183,7 @@ func TestMutationDuringReadAndBetweenStages(t *testing.T) { func TestVerifiedHandleReopensPinnedInode(t *testing.T) { root, r := fixture(t) - s, e := openSource(root) + s, e := openSource(root, GeneratedStaging{}) if e != nil { t.Fatal(e) } diff --git a/install/integrationctl/agentplugins/adapters/packageview/scratch_alias_stages_windows_test.go b/install/integrationctl/agentplugins/adapters/packageview/scratch_alias_stages_windows_test.go index 72e21caa..18640bad 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/scratch_alias_stages_windows_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/scratch_alias_stages_windows_test.go @@ -39,7 +39,7 @@ func TestWindowsScratchAliasResolutionStages(t *testing.T) { t.Logf("stdlib EvalSymlinks=%q err=%v", eval, evalErr) before := winRecordCount() if evalErr == nil { - old, oldErr := openSource(eval) + old, oldErr := openSource(eval, GeneratedStaging{}) t.Logf("stdlib result openSource err=%v", oldErr) if old != nil { if e := old.close(); e != nil { @@ -60,7 +60,7 @@ func TestWindowsScratchAliasResolutionStages(t *testing.T) { t.Fatal("protected acquisition stage:", e) } defer scratch.close() - source, e := openSource(target) + source, e := openSource(target, GeneratedStaging{}) if e != nil { t.Fatal("target acquisition stage:", e) } diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_darwin.go b/install/integrationctl/agentplugins/adapters/packageview/source_darwin.go index 49cb9419..3020d0ec 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_darwin.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_darwin.go @@ -14,17 +14,25 @@ import ( ) // Darwin has no O_PATH -> data-descriptor upgrade in the Go/xsys seam. -// This profile therefore requires a READ-ONLY LOCAL APFS volume. The held -// directory plus immutable directory entry pins an object before any data open. -// A privileged remount, hostile mount namespace or underlying block-device -// writer is outside the profile. Writable APFS is deliberately unavailable. -// In particular O_EVTONLY and /dev/fd are NOT used to upgrade file access. +// This profile therefore requires a READ-ONLY LOCAL APFS volume, UNLESS the +// caller supplied a matching GeneratedStaging proof (see openSource), in +// which case only this exact, caller-proven directory is exempt from the +// read-only requirement; it must still be local APFS. The held directory plus +// immutable directory entry pins an object before any data open. A privileged +// remount, hostile mount namespace or underlying block-device writer is +// outside the profile. Arbitrary writable APFS remains deliberately +// unavailable. In particular O_EVTONLY and /dev/fd are NOT used to upgrade +// file access. type source struct { root *os.Root anchor *os.File legacyInfo os.FileInfo dev int32 fsid unix.Fsid + // trustedWritable is set only when openSource verified a GeneratedStaging + // proof against this exact opened directory. It never widens to a + // separately opened or path-resolved object. + trustedWritable bool } type pinned struct { file *os.File // owned parent directory, NOT a data handle to the entry @@ -33,17 +41,24 @@ type pinned struct { source *source } -func darwinFS(fd int) (unix.Statfs_t, error) { +// darwinFS enforces the local-APFS profile. trustedWritable, which only +// openSource can set (and only after a live-handle identity match), is the +// sole thing that exempts a source from the MNT_RDONLY requirement; MNT_LOCAL +// and the apfs filesystem type are always required, trusted or not. +func darwinFS(fd int, trustedWritable bool) (unix.Statfs_t, error) { var fs unix.Statfs_t if e := unix.Fstatfs(fd, &fs); e != nil { return fs, e } - if unix.ByteSliceToString(fs.Fstypename[:]) != "apfs" || fs.Flags&(unix.MNT_RDONLY|unix.MNT_LOCAL) != unix.MNT_RDONLY|unix.MNT_LOCAL { + if unix.ByteSliceToString(fs.Fstypename[:]) != "apfs" || fs.Flags&unix.MNT_LOCAL == 0 { + return fs, fail("filesystem_unavailable") + } + if !trustedWritable && fs.Flags&unix.MNT_RDONLY == 0 { return fs, fail("filesystem_unavailable") } return fs, nil } -func openSource(name string) (_ *source, err error) { +func openSource(name string, generated GeneratedStaging) (_ *source, err error) { if n := strings.TrimRight(name, "/"); n != "" { name = n } @@ -73,7 +88,16 @@ func openSource(name string) (_ *source, err error) { if e != nil || !os.SameFile(before, opened) { return nil, fail("source_changed") } - fs, e := darwinFS(fd) + // A supplied proof must match the exact object just opened by path, or the + // call fails closed: a stale/mismatched proof is never silently downgraded + // to the ordinary strict profile, since that would mask a swapped root. + if generated.present() { + if !generated.matches(opened) { + return nil, fail("generated_staging_mismatch") + } + s.trustedWritable = true + } + fs, e := darwinFS(fd, s.trustedWritable) if e != nil { return nil, e } @@ -160,7 +184,7 @@ func (s *source) pin(rel string, nofollow bool) (*pinned, error) { if e != nil { return nil, e } - fs, e := darwinFS(int(dir.Fd())) + fs, e := darwinFS(int(dir.Fd()), s.trustedWritable) if e != nil || fs.Fsid != s.fsid { dir.Close() return nil, syscall.EXDEV @@ -235,7 +259,7 @@ func (p *pinned) reopen(directory bool) (*os.File, error) { if (directory && !p.info.IsDir()) || (!directory && !p.info.Mode().IsRegular()) { return nil, fail("wrong_kind") } - fs, e := darwinFS(int(p.file.Fd())) + fs, e := darwinFS(int(p.file.Fd()), p.source.trustedWritable) if e != nil || fs.Fsid != p.source.fsid { return nil, fail("filesystem_unavailable") } @@ -249,7 +273,7 @@ func (p *pinned) reopen(directory bool) (*os.File, error) { return nil, e } f := os.NewFile(uintptr(fd), "source-data") - openedFS, e := darwinFS(fd) + openedFS, e := darwinFS(fd, p.source.trustedWritable) if e != nil || openedFS.Fsid != p.source.fsid { f.Close() return nil, fail("filesystem_unavailable") diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_darwin_test.go b/install/integrationctl/agentplugins/adapters/packageview/source_darwin_test.go index 9f531487..5a26668b 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_darwin_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_darwin_test.go @@ -56,7 +56,7 @@ func nativeFixture(t *testing.T, build func(string)) string { return root } func TestDarwinWritableProfileRejected(t *testing.T) { - s, e := openSource(t.TempDir()) + s, e := openSource(t.TempDir(), GeneratedStaging{}) if e == nil { s.close() t.Fatal("writable filesystem accepted") @@ -165,7 +165,7 @@ func TestDarwinHandleCleanupAndTypeChecks(t *testing.T) { } before := count() for i := 0; i < 10; i++ { - s, e := openSource(root) + s, e := openSource(root, GeneratedStaging{}) if e != nil { t.Fatal(e) } @@ -245,4 +245,71 @@ func TestDarwinRootSelectionTraversalOrder(t *testing.T) { } } +// A matching GeneratedStaging proof is the only way an ordinary writable +// local APFS directory is ever accepted; TestDarwinWritableProfileRejected +// above proves the zero-value (untrusted) path still rejects it. +func TestDarwinGeneratedStagingProofAcceptsOwnWritableRoot(t *testing.T) { + root := t.TempDir() + nativeWrite(t, root, "plugin.json", "core") + dir, e := os.OpenRoot(root) + if e != nil { + t.Fatal(e) + } + defer dir.Close() + proof, e := NewGeneratedStaging(dir) + if e != nil { + t.Fatal(e) + } + s, e := openSource(root, proof) + if e != nil { + t.Fatal("trusted generated-staging proof rejected on its own writable root:", e) + } + defer s.close() + if !s.trustedWritable { + t.Fatal("trusted flag not set from a matching proof") + } + // The relaxation must still require local APFS and every other check: + // this exercises the same darwinFS/pin/reopen path the untrusted case uses. + p, e := s.pin("plugin.json", true) + if e != nil { + t.Fatal(e) + } + defer p.file.Close() + f, e := p.reopen(false) + if e != nil { + t.Fatal("trusted data reopen failed:", e) + } + defer f.Close() +} + +// A GeneratedStaging proof built from one directory must never authorize a +// different one: the caller could otherwise mint a proof against a trivially +// creatable writable directory and pass an unrelated path to openSource. +func TestDarwinGeneratedStagingProofMismatchFailsClosed(t *testing.T) { + a := filepath.Join(t.TempDir(), "a") + b := filepath.Join(t.TempDir(), "b") + for _, d := range []string{a, b} { + if e := os.Mkdir(d, 0700); e != nil { + t.Fatal(e) + } + } + dirA, e := os.OpenRoot(a) + if e != nil { + t.Fatal(e) + } + defer dirA.Close() + proof, e := NewGeneratedStaging(dirA) + if e != nil { + t.Fatal(e) + } + s, e := openSource(b, proof) + if e == nil { + s.close() + t.Fatal("mismatched generated-staging proof accepted a different directory") + } + var safe *Error + if !errors.As(e, &safe) || safe.Code != "generated_staging_mismatch" { + t.Fatal(e) + } +} func nativeLinkPrivilegeError(e error) bool { return os.IsPermission(e) } diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_linux.go b/install/integrationctl/agentplugins/adapters/packageview/source_linux.go index 8a9696ed..2328bd3c 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_linux.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_linux.go @@ -22,7 +22,10 @@ type pinned struct { info os.FileInfo } -func openSource(name string) (_ *source, err error) { +// Linux's filesystem allowlist already permits ordinary writable local +// filesystems (see the switch below), so a generated-staging proof adds +// nothing here and is intentionally ignored. +func openSource(name string, _ GeneratedStaging) (_ *source, err error) { // A trailing slash must not hide a final symlink from Lstat. if trimmed := strings.TrimRight(name, "/"); trimmed != "" { name = trimmed diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_native_test.go b/install/integrationctl/agentplugins/adapters/packageview/source_native_test.go index edff7b21..ac8b39e9 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_native_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_native_test.go @@ -32,7 +32,7 @@ func nativeLink(t *testing.T, root, target, name string) { } func nativeSource(t *testing.T, root string) *source { t.Helper() - s, e := openSource(root) + s, e := openSource(root, GeneratedStaging{}) if e != nil { t.Fatal(e) } @@ -211,7 +211,7 @@ func TestNativeRootLinkRejected(t *testing.T) { t.Fatal(e) } for _, p := range []string{link, link + string(filepath.Separator)} { - s, e := openSource(p) + s, e := openSource(p, GeneratedStaging{}) if e == nil { s.close() t.Fatal("accepted root link", p) diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_unsupported.go b/install/integrationctl/agentplugins/adapters/packageview/source_unsupported.go index 6561a4b7..042bb4df 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_unsupported.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_unsupported.go @@ -12,11 +12,13 @@ type pinned struct { info os.FileInfo } -func openSource(string) (*source, error) { return nil, fail("platform_unavailable") } -func (*source) close() error { return nil } -func (*source) pin(string, bool) (*pinned, error) { return nil, fail("platform_unavailable") } -func stateOf(error) State { return Blocked } -func same(a, b os.FileInfo) bool { return false } -func multipleLinks(os.FileInfo) bool { return true } -func (*pinned) link(int64) (string, error) { return "", fail("platform_unavailable") } -func (*pinned) reopen(bool) (*os.File, error) { return nil, fail("platform_unavailable") } +func openSource(string, GeneratedStaging) (*source, error) { return nil, fail("platform_unavailable") } +func (*source) close() error { return nil } +func (*source) pin(string, bool) (*pinned, error) { + return nil, fail("platform_unavailable") +} +func stateOf(error) State { return Blocked } +func same(a, b os.FileInfo) bool { return false } +func multipleLinks(os.FileInfo) bool { return true } +func (*pinned) link(int64) (string, error) { return "", fail("platform_unavailable") } +func (*pinned) reopen(bool) (*os.File, error) { return nil, fail("platform_unavailable") } diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_windows.go b/install/integrationctl/agentplugins/adapters/packageview/source_windows.go index 533337e4..97a113f4 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_windows.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_windows.go @@ -303,7 +303,13 @@ func (s *source) remember(f *os.File, outside ...bool) (*pinned, error) { winInfos.Unlock() return &pinned{dup, info}, nil } -func openSource(name string) (_ *source, err error) { return openSourceWithMetadataStage(name, nil) } + +// Windows' profile already accepts local fixed-drive NTFS without a read-only +// requirement, so a generated-staging proof adds nothing here and is +// intentionally ignored. +func openSource(name string, _ GeneratedStaging) (_ *source, err error) { + return openSourceWithMetadataStage(name, nil) +} func openSourceWithMetadataStage(name string, stage func(*os.File, string)) (_ *source, err error) { return openWindowsRoot(name, winCapturedSource, stage) } diff --git a/install/integrationctl/agentplugins/adapters/packageview/source_windows_test.go b/install/integrationctl/agentplugins/adapters/packageview/source_windows_test.go index 8a1d2730..60e79094 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/source_windows_test.go +++ b/install/integrationctl/agentplugins/adapters/packageview/source_windows_test.go @@ -195,7 +195,7 @@ func TestWindowsHandleLifetimeAndFailureCleanup(t *testing.T) { } } // Windows denies removal if a directory handle leaked without delete sharing. - s, e := openSource(root) + s, e := openSource(root, GeneratedStaging{}) if e != nil { t.Fatal(e) } @@ -368,7 +368,7 @@ func TestWindowsJunctionsAndNamespaceRoots(t *testing.T) { } } for _, name := range []string{filepath.Join(root, "contained-junction"), `\\.\pipe\packageview-disposable-nonexistent`, `\\?\GLOBALROOT\Device\NamedPipe`, `C:relative`, `\\server\share`} { - other, e := openSource(name) + other, e := openSource(name, GeneratedStaging{}) if e == nil { other.close() t.Fatal("namespace/reparse root accepted", name) @@ -586,7 +586,7 @@ func TestWindowsRootAndIntermediateReparseRejected(t *testing.T) { root := nativeFixture(t, func(root string) { nativeWrite(t, root, "real/plugin.json", "core") }) nativeLink(t, root, "real", "link") for _, path := range []string{root + `\link\..\real`, root + `\link\.`} { - s, e := openSource(path) + s, e := openSource(path, GeneratedStaging{}) if e == nil { s.close() t.Fatalf("root traversal followed reparse: %s", path) @@ -594,7 +594,7 @@ func TestWindowsRootAndIntermediateReparseRejected(t *testing.T) { } before := winRecordCount() for i := 0; i < 10; i++ { - s, e := openSource(root + `\missing\root`) + s, e := openSource(root + `\missing\root`, GeneratedStaging{}) if e == nil { s.close() t.Fatal("accepted missing root") diff --git a/install/integrationctl/agentplugins/adapters/packageview/view.go b/install/integrationctl/agentplugins/adapters/packageview/view.go index 2b054d9c..a7ba615e 100644 --- a/install/integrationctl/agentplugins/adapters/packageview/view.go +++ b/install/integrationctl/agentplugins/adapters/packageview/view.go @@ -140,8 +140,42 @@ func (v Limits) bounded() (Limits, error) { // trusted scratch parent outside the selected source. It is never cleanup // authority. Only the exact MkdirTemp child is owned by the returned lease. type Reader struct { - TempDir string - Limits Limits + TempDir string + Limits Limits + Generated GeneratedStaging +} + +// GeneratedStaging authorizes relaxing only the Darwin profile's read-only-mount +// source requirement, for exactly one caller-proven directory. Every other +// containment, symlink, type and mutation check stays exactly as strict; other +// platforms are unaffected (they never required a read-only mount). The zero +// value proves nothing and changes no behavior. +// +// It can be built only from a live *os.Root handle to that directory, never +// from a path string, so a caller that holds only a path -- every ordinary +// validate/inspect/test request -- can never construct one. Open still reopens +// by path and independently reverifies identity against this proof with +// os.SameFile before trusting it, so a stale or mismatched proof is rejected, +// not silently ignored. +type GeneratedStaging struct{ info os.FileInfo } + +// NewGeneratedStaging captures live identity for dir by Stat-ing the already +// open handle. Callers must pass the exact directory they exclusively created +// and still hold open; the caller-held handle -- not a reopened path -- is the +// source of truth. +func NewGeneratedStaging(dir *os.Root) (GeneratedStaging, error) { + if dir == nil { + return GeneratedStaging{}, fail("generated_staging_required") + } + info, err := dir.Stat(".") + if err != nil { + return GeneratedStaging{}, fail("generated_staging_required") + } + return GeneratedStaging{info: info}, nil +} +func (g GeneratedStaging) present() bool { return g.info != nil } +func (g GeneratedStaging) matches(fi os.FileInfo) bool { + return g.info != nil && fi != nil && os.SameFile(g.info, fi) } // Error is safe to print/serialize. Cancellation remains errors.Is-compatible. @@ -215,7 +249,7 @@ func (r Reader) open(ctx context.Context, exactRoot string, hooks *captureHooks) if exactRoot == "" || r.TempDir == "" { return nil, fail("explicit_roots_required") } - s, err := openSource(exactRoot) + s, err := openSource(exactRoot, r.Generated) if err != nil { return nil, err } 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 new file mode 100644 index 00000000..d15fda09 --- /dev/null +++ b/npm/agentplugins/scripts/authoring-native-qualification.js @@ -0,0 +1,1067 @@ +#!/usr/bin/env node +"use strict"; + +// 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"); +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"); +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"; // 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"]); +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", "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); }; +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, target = TARGET) { + hostContract(target); + return { product, 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, 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 => { + 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}`; 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 => stop(`subprocess failed: ${e.code}`)); + child.on("close", (status, sig) => { + 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"; + 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 finish(problem || `signal ${sig}`); + try { + const decoder = new TextDecoder("utf-8", { fatal: true }); + 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"); + 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, target = TARGET) { + 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") { 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); } + 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, 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"); + 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 === "." || 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(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(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) { + 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 } : {}) }; + const captured = capturedBytes(value.files, value.documents); + for (const [name, parsed] of Object.entries(documents)) { + 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(captured[name]), captured[name].length]); + } + 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, project) { + const data = r.data.security ? r.data : r.data.targets?.[0]?.output; + const security = data?.security; + 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"); + // 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, 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", "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, 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, 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) { + 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, 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++) { + 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, 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) { + assert.equal(before[file], undefined, "fresh scan must precede cache creation"); + 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.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]); + 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; +} +// 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))); + } + } +} +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. + // 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) { + // 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); + 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}`; + 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], + ["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]); + } + 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) { + 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", 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 === "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)) { + 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") { + // 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"); + assert.notEqual(result.activation.authentication_attested, true); + 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", 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); + 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, []); + } +} +// 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 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, 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"); + 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, target); + 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); + commandContinuity(rows, projects[p]); + } + 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, ["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"], 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, []); } + // 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"], 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`]); + 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"); + 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) { + 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", ...(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); + 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: 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"]]) { + 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", ...(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()], [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. + 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 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 = []; + 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: 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 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"), 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) }; + 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); } + } + 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); + 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"); + } + 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()) { + 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"); + } + 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)) })), + projects_before: projects, projects_after: afterProjects, author_homes_before: homesBefore, author_homes_after: homesAfter, + custody_before: custodyBefore, custody_after: inputCustody(root, after.subjects) }; + 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, + observation: "whole-descendant-authoring-no-process-network/1" }; + 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"); + 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, ...(options.target ? { target } : {}) }); + } 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..5691d154 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,16 @@ 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 publicContract = require('./public-authoring-acceptance'); + const unsupported = lanes.filter(x => x.lane === "public-packed-pair" ? + x.schema !== publicContract.ACCEPTANCE_SCHEMA || x.workflow !== publicContract.WORKFLOW : + 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 schema registration is source-only; native acceptance remains required. 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 +188,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 +514,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 +522,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 +540,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; } @@ -245,48 +553,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 })), @@ -337,7 +610,7 @@ function inspectPair(record, cwd) { } function options(v) { - c.keys(v, ["record", "root", "scratch", "workflow_sha", "preparation", "selected"], "promotion options"); + c.keys(v, ["record", "root", "scratch", "workflow_sha", "preparation", "selected", ...(Object.hasOwn(v, 'public') ? ['public'] : [])], "promotion options"); for (const name of ["root", "scratch"]) c.safeDirectory(v[name]); if (v.root === v.scratch || v.root.startsWith(v.scratch + path.sep) || v.scratch.startsWith(v.root + path.sep)) fail("scratch and inputs must be disjoint"); if (typeof v.record !== "string" || !path.isAbsolute(v.record) || path.basename(v.record) !== "authoring-promotion.json") fail("absolute authoring-promotion.json required"); @@ -346,16 +619,15 @@ 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); // Native acceptance still required; zero protected effects. + const publicEvidence = admitPublicEvidence(record, { request: o.public, preparation: o.preparation }); const subjects = frozenSubjects(o.root, record); subjects.push({ file: o.record, sha256: c.digest(encodeRecord(record)) }); - return { o, record, subjects }; + return { o, record, subjects, native, publicEvidence }; } function verifyAll(state) { for (const subject of state.subjects) verifySubject(subject.file, { @@ -452,5 +724,64 @@ 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 }; +// Fixed public wrappers reuse the existing provider and verifier policy. They +// introduce no accepted-native override and grant no publication authority. +function inspectPublicAttempt(pin, selected, mode, cwd) { + const publicEvidence = require('./public-authoring-acceptance'); + if (!Object.hasOwn(publicEvidence.PUBLIC_JOBS, mode)) fail('fixed public attempt mode required'); + const run = attemptAtRef(pin, publicEvidence.WORKFLOW, selected, cwd, 'completed'); + const jobs = attemptJobs(pin, selected, cwd), names = publicEvidence.PUBLIC_JOBS[mode]; + const selectedJobs = names.map(name => oneJob(jobs, name, 'completed')); + // Other invocation modes may exist only as skipped jobs in this workflow. + for (const job of jobs) if (!names.includes(job.name) && + (job.status !== 'completed' || job.conclusion !== 'skipped')) fail('unexpected active public job'); + const value = { producer: { workflow: publicEvidence.WORKFLOW, source: selected.source, ref: selected.ref, + run_id: pin.run_id, run_attempt: pin.run_attempt }, status: run.status, conclusion: run.conclusion, + jobs: selectedJobs.map(j => ({ id: j.id, name: j.name, run_id: j.run_id, run_attempt: j.run_attempt, + source: j.head_sha, ref: selected.ref, status: j.status, conclusion: j.conclusion })) }; + publicEvidence.completedAttempt(value, selected, mode); return value; +} +function inspectPublicCaller(selected, workflowSha, mode, cwd) { + const a = require('./public-authoring-acceptance'); + if (!Object.hasOwn(a.PUBLIC_JOBS, mode)) fail('fixed public caller mode'); + exact(workflowSha, selected.source, 'public workflow F'); + const jobs = mode === 'produce' ? ['public_inputs', 'public_cell', 'public_producer_complete'] : a.PUBLIC_JOBS[mode]; + const caller = currentCaller(selected, a.WORKFLOW, jobs); + attemptAtRef(caller, a.WORKFLOW, selected, cwd, 'in_progress'); + const name = process.env.GITHUB_JOB === 'public_cell' ? `public_cell (${process.env.PUBLIC_CELL})` : process.env.GITHUB_JOB; + if (!a.PUBLIC_JOBS[mode].includes(name)) fail('fixed public cell'); + oneJob(attemptJobs(caller, selected, cwd), name, 'in_progress'); + return { workflow: a.WORKFLOW, source: selected.source, ref: selected.ref, run_id: caller.run_id, run_attempt: caller.run_attempt }; +} +function verifyPublicSubject(file, expected, cwd) { + const a = require('./public-authoring-acceptance'), names = [a.ACCEPTANCE_FILE, a.INDEX_FILE]; + if (!Array.isArray(expected.subjects) || expected.subjects.length !== 2) fail('exact E2 subjects required'); + exact(expected.subjects.map(s => s.name).sort(), [...names].sort(), 'exact E2 subject names'); + if (!names.includes(expected.name)) fail('fixed E2 subject required'); + return verifyWorkflowSubject(file, expected, cwd, a.WORKFLOW); +} +function admitPublicEvidence(value, context) { + const record = recordShape(value), a = require('./public-authoring-acceptance'); + c.keys(context, ['request', 'preparation'], 'public promotion context'); locator(context.preparation); + const lane = record.qualification.lanes.find(row => row.lane === 'public-packed-pair'); + exact([lane.schema, lane.workflow], [a.ACCEPTANCE_SCHEMA, a.WORKFLOW], 'fixed public promotion contract'); + const request = context.request; + if (!request || request.schema !== 'authoring-public-read/v1') fail('completed public read request required'); + exact(request.selected, { tag: record.products.agentplugins.tag, ref: `refs/tags/${record.products.agentplugins.tag}`, + source: record.identity.commit, versions: record.identity.versions }, 'public Q selection'); + exact(request.workflow_sha, record.identity.commit, 'public Q workflow F'); + exact(request.acceptance, { sha256: lane.sha256, artifact: lane.artifact }, 'public Q exact completed R3 locator'); + const admitted = a.readAcceptance(request), e = admitted.record; + for (const key of ['identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256']) + exact(e[key], record[key], `public Q ${key}`); + exact(admitted.input.preparation.artifact, context.preparation, 'public Q original preparation'); + exact(admitted.input.products, record.products, 'public Q all twelve full native outer/inner pins'); + exact(e.native_inputs, admitted.stage.native_inputs, 'public Q authenticated I locator'); + exact(e.packs, admitted.stage.packs, 'public Q both complete original pack pins'); + exact(e.stage, request.stage, 'public Q original S locator'); + return admitted; +} +module.exports = { admitPublicEvidence, inspectPublicCaller, inspectPublicAttempt, verifyPublicSubject, 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/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/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js new file mode 100644 index 00000000..1dce4c1e --- /dev/null +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node +"use strict"; +const fs=require("node:fs"),path=require("node:path"),cp=require("node:child_process"),crypto=require("node:crypto"); +const producer=require("./stage-dual-authoring-candidate"),packer=require("./stage-dual-authoring-npm"); +const PRODUCTS=["agentplugins","plugin-kit-ai"],TARGETS={linux:["amd64"],windows:["amd64"],darwin:["arm64"]}; +const VERSIONS=Object.freeze({agentplugins:"0.1.91","plugin-kit-ai":"2.0.0"}); +const COMMANDS=["init","validate","inspect","test","local-add-dry-run"]; +const MAX_TRUSTED_TOOL_BYTES=128*1024*1024; +function fail(s){throw new Error(s)} function sha(b){return crypto.createHash("sha256").update(b).digest("hex")} +function absolute(n,v){if(!v||!path.isAbsolute(v)||path.resolve(v)!==v)fail(`${n} must be absolute`);return v} +function nativeAbsolute(v,platform=process.platform){const api=platform==="win32"?path.win32:path.posix;if(!v||!api.isAbsolute(v))fail("root must be absolute");return api.resolve(v)} +function canonicalTool(label,input){let resolved;try{resolved=fs.realpathSync(absolute(`${label} tool input`,input))}catch(error){fail(`${label} canonical tool resolution failed: ${error.message}`)}let stat;try{stat=fs.lstatSync(resolved)}catch(error){fail(`${label} canonical tool inspection failed: ${error.message}`)}if(!stat.isFile()||stat.size<=0||stat.size>MAX_TRUSTED_TOOL_BYTES||stat.nlink!==1)fail(`${label} canonical tool must be regular, nonempty, bounded and unaliased`);return resolved} +function resolveTrustedTools(go,npm,node=process.execPath){return{go:canonicalTool("go",go),node:canonicalTool("node",node),npm:canonicalTool("npm CLI",npm)}} +function resolveNpmCLI(node=process.execPath){const bin=path.dirname(canonicalTool("node",node)),candidates=process.platform==="win32"?[path.join(bin,"node_modules","npm","bin","npm-cli.js")]:[path.resolve(bin,"../lib/node_modules/npm/bin/npm-cli.js"),path.join(bin,"node_modules","npm","bin","npm-cli.js")];for(const candidate of candidates)if(fs.existsSync(candidate))return canonicalTool("npm CLI",candidate);fail(`npm CLI was not found beside trusted node: ${bin}`)} +function inside(root,v,label="path"){const r=path.relative(root,v);if(r===""||(!r.startsWith(`..${path.sep}`)&&r!==".."&&!path.isAbsolute(r)))return v;fail(`${label} escapes journey root: ${v}`)} +function mode700(file,label){if((fs.lstatSync(file).mode&0o777)!==0o700)fail(`${label} must have mode 0700`)} +function precreate(root,platform=process.platform){absolute("root",root);if(fs.existsSync(root))fail("Milestone A root must be new");const evidence=path.join(root,"evidence");fs.mkdirSync(evidence,{recursive:true,mode:0o700});if(platform!=="win32")for(const file of [root,evidence])fs.chmodSync(file,0o700);return evidence} +function fresh(root,precreated=false){absolute("root",root);if(precreated){if(!fs.existsSync(root))fail("precreated Milestone A root is missing");if(process.platform!=="win32")mode700(root,"Milestone A root");const entries=fs.readdirSync(root);if(entries.length!==1||entries[0]!=="evidence"||!fs.lstatSync(path.join(root,"evidence")).isDirectory())fail("precreated Milestone A root must contain only evidence");if(process.platform!=="win32")mode700(path.join(root,"evidence"),"Milestone A evidence root")}else precreate(root);fs.mkdirSync(path.join(root,"journeys"),{mode:0o700})} +function tree(root){const out=[];function visit(d,p=""){for(const e of fs.readdirSync(d,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){const r=p?`${p}/${e.name}`:e.name,f=path.join(d,e.name);if(e.isSymbolicLink())fail(`generated tree contains non-regular entry: ${r}`);if(e.isDirectory())visit(f,r);else if(e.isFile())out.push([r,sha(fs.readFileSync(f))]);else fail(`generated tree contains non-regular entry: ${r}`)}}visit(root);return out} +function snapshot(root,ex=[]){const omit=new Set(ex.map(x=>path.resolve(x))),out=[];function visit(d,p=""){for(const e of fs.readdirSync(d,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){const f=path.join(d,e.name);if(omit.has(path.resolve(f)))continue;const r=p?`${p}/${e.name}`:e.name;if(e.isSymbolicLink())fail(`sandbox contains link: ${r}`);if(e.isDirectory())visit(f,r);else if(e.isFile())out.push([r,sha(fs.readFileSync(f))]);else fail(`sandbox contains unexpected entry: ${r}`)}}visit(root);return out} +function run(exe,args,o={}){const r=cp.spawnSync(exe,args,{encoding:"utf8",timeout:120000,windowsHide:true,maxBuffer:16777216,...o});if(r.error)throw r.error;if(r.signal)fail(`${path.basename(exe)} terminated by ${r.signal}`);return r} +function commandOutput(r){return `stdout:\n${r.stdout||""}\nstderr:\n${r.stderr||""}`} +function ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}\n${commandOutput(r)}`);return r} +function result(r,l){ok(r,l);let v;try{v=JSON.parse(r.stdout)}catch{fail(`${l} did not return JSON`)}if(v.schema_version!==1||v.result!=="success")fail(`${l} returned an invalid result contract`);return v} +function makeJourney(root,p){const j=path.join(root,"journeys",p);fs.mkdirSync(j);const o={root:j};for(const n of ["home","tmp","config","cache","data","state","appdata","localappdata","npm-cache","npm-prefix","installation"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}o.project=path.join(j,"project");o.client=path.join(o.home,".codex");fs.mkdirSync(o.client);fs.writeFileSync(path.join(o.client,"config.toml"),"# isolated disposable Codex fixture\n");return o} +function envFor(j){const e={};for(const k of ["PATH","SystemRoot","ComSpec","PATHEXT","WINDIR","LANG","LC_ALL","TZ","HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy","SSL_CERT_FILE","SSL_CERT_DIR"])if(process.env[k]!==undefined)e[k]=process.env[k];return{...e,HOME:j.home,USERPROFILE:j.home,TMPDIR:j.tmp,TMP:j.tmp,TEMP:j.tmp,XDG_CONFIG_HOME:j.config,XDG_CACHE_HOME:j.cache,XDG_DATA_HOME:j.data,XDG_STATE_HOME:j.state,APPDATA:j.appdata,LOCALAPPDATA:j.localappdata,CODEX_HOME:j.client,npm_config_cache:j.npm_cache,npm_config_prefix:j.npm_prefix,npm_config_offline:"true",npm_config_audit:"false",npm_config_fund:"false",npm_config_update_notifier:"false",NODE_OPTIONS:"--max-old-space-size=384",GIT_TERMINAL_PROMPT:"0"}} +function acquisition(input){return{candidate_provenance:"exact-head locally packed candidate",exact_local_tarballs:PRODUCTS.map(p=>path.join(input,"packages",p==="agentplugins"?`universal-agent-plugins-${VERSIONS.agentplugins}.tgz`:`plugin-kit-ai-${VERSIONS["plugin-kit-ai"]}.tgz`)),package_registry_or_latest_acquisition:false,npm_tarball_install_offline:true}} +function securityNetwork(){return{production_security_checks_enabled:true,credential_free_read_only_public_network_allowed:true,allowed_pinned_endpoint_classes:["Directory","Discovery","Security Index","scanner releases"],test_bypass:false}} +function candidateIdentity(head){const value={repository:"777genius/universal-agent-plugins",commit:head,engine_revision:head,versions:{...VERSIONS}};require("./dual-authoring-candidate").identity(value);return value} +function failureReceipt(root,error){try{const evidence=path.join(root,"evidence");if(fs.existsSync(evidence))fs.writeFileSync(path.join(evidence,"prepare-failure.json"),JSON.stringify({schema:"milestone-a-e2e-prepare-failure/v1",message:String(error?.message||error).slice(0,4096),candidate_present:fs.existsSync(path.join(root,"candidate")),publication:false},null,2)+"\n",{flag:"wx",mode:0o600})}catch{}} +function prepare(c){const root=absolute("root",c.root),repo=absolute("repo",c.repo);try{fresh(root,c.precreatedRoot===true);const head=ok(run("git",["rev-parse","HEAD"],{cwd:repo}),"git head").stdout.trim();if(head!==c.expectedHead)fail("checkout is not the expected exact candidate");const identity=candidateIdentity(head),common={candidate:true,repo,identity,assetScope:"six-platform-pair",authoringMode:"release-cli-contract-v1",go:absolute("go",c.go),workParent:path.join(root,"journeys")},candidate=path.join(root,"candidate"),built=producer.stageCandidate({...common,output:candidate,modCache:absolute("modCache",c.modCache)}),packages=path.join(root,"packages"),packed=packer.stagePair({...common,root:candidate,manifestDigest:built.manifest_sha256,output:packages,node:absolute("node",c.node),npm:absolute("npm",c.npm)}),receipt={schema:"milestone-a-e2e-prepare/v1",identity,candidate_head:head,candidate_sha256:built.manifest_sha256,asset_scope:"six-platform-pair",packages:packed.packs,package_acquisition:acquisition(root),security_boundary:securityNetwork(),publication:false};fs.writeFileSync(path.join(root,"evidence","prepare.json"),JSON.stringify(receipt,null,2)+"\n");return receipt}catch(error){failureReceipt(root,error);throw error}} +function provenance(ep,head){const real=fs.realpathSync(ep);let d=path.dirname(real),m,p,field,revision;while(d!==path.dirname(d)){const x=path.join(d,"package.json"),release=path.join(d,"private-release.json");if(fs.existsSync(release)){m=x;if(!fs.existsSync(m))fail(`package provenance has no package manifest: ${ep}`);p=JSON.parse(fs.readFileSync(m));revision=JSON.parse(fs.readFileSync(release)).identity?.commit;field="private-release.json#identity.commit";break}if(fs.existsSync(x)){const candidate=JSON.parse(fs.readFileSync(x)),candidateField=["gitHead","commit","revision"].find(k=>candidate[k]!==undefined)||(candidate.build?.revision!==undefined?"build.revision":null);if(candidateField){m=x;p=candidate;field=candidateField;revision=field.startsWith("build")?p.build.revision:p[field];break}}d=path.dirname(d)}if(!m)fail(`entrypoint has no package provenance: ${ep}`);if(revision!==head)fail(`package provenance revision does not equal exact candidate: ${revision}`);return{entrypoint:real,package_manifest:fs.realpathSync(m),package_name:p.name,package_version:p.version,revision_field:field,revision}} +// Only declared product/help labels and paths inside this entrypoint's journey vary. +function normalize(v,project,platform=process.platform){const pathApi=platform==="win32"?path.win32:path.posix,journey=pathApi.resolve(pathApi.dirname(project));function string(s){if(!pathApi.isAbsolute(s))return s;const resolved=pathApi.resolve(s),relative=pathApi.relative(journey,resolved),outside=relative===".."||relative.startsWith(`..${pathApi.sep}`)||pathApi.isAbsolute(relative);if(outside)return s;return relative?`/${relative.split(pathApi.sep).join("/")}`:""}function walk(x){if(typeof x==="string")return string(x);if(Array.isArray(x))return x.map(walk);if(x&&typeof x==="object")return Object.fromEntries(Object.entries(x).map(([k,y])=>[k,walk(y)]));return x}const x=walk(v);if(x.data){delete x.data.product;delete x.data.product_version;if(x.data.help?.use)x.data.help.use=x.data.help.use.replace(/^agentplugins author|^plugin-kit-ai/,"")}return x} +function reported(v,root){function walk(x,k=""){if(Array.isArray(x))return x.forEach(y=>walk(y,k));if(x&&typeof x==="object")return Object.entries(x).forEach(([a,b])=>walk(b,a));if(typeof x==="string"&&path.isAbsolute(x)&&/(path|root|dir|home|prefix|locator|destination|project|target|file)$/i.test(k))inside(root,path.resolve(x),`reported ${k}`)}walk(v)} +function addProof(v,codexRoot){ + // The real CLI renders a single --target=codex dry-run as the batch/group + // envelope (addMultiResult), not a bare addResultData: exactly one entry in + // data.targets, with the per-target plan under targets[0].output.result.plan. + const data=v?.data,assessment=data?.security,digest=/^sha256:[0-9a-f]{64}$/; + if(v.command!=="add"||data?.dry_run!==true)fail("local add did not return the established dry-run result"); + if(!Array.isArray(data?.targets)||data.targets.length!==1)fail("local add did not resolve exactly one requested codex target"); + const target=data.targets[0],plan=target?.output?.result?.plan; + if(target?.target!=="codex"||plan?.client_id!=="codex")fail("local add did not resolve target exactly to codex"); + if(!assessment||assessment.schema_version!==1||!assessment.scanner||typeof assessment.scanner.id!=="string"||!assessment.scanner.id||typeof assessment.scanner.version!=="string"||!assessment.scanner.version||!assessment.policy||typeof assessment.policy.id!=="string"||!assessment.policy.id||!Number.isInteger(assessment.policy.version)||assessment.policy.version<1||!digest.test(assessment.policy.digest||"")||!digest.test(assessment.report_digest||"")||assessment.evidence_source!=="local_scan")fail("local add omitted authoritative production security evidence"); + const counts=assessment.counts; + if(!counts||![counts.blocking,counts.warnings,counts.total,assessment.scanned_files].every(Number.isInteger)||counts.blocking!==0||counts.total!==counts.blocking+counts.warnings||assessment.scanned_files<1||!["no_blocking_findings","warnings"].includes(assessment.outcome))fail("local add security assessment did not pass"); + if(assessment.subject?.tree_digest!==data.tree_digest||assessment.subject?.manifest_digest!==data.manifest_digest||!digest.test(data.tree_digest||"")||!digest.test(data.manifest_digest||""))fail("local add security evidence does not describe its package"); + function paths(x,k=""){if(Array.isArray(x))return x.forEach(y=>paths(y,k));if(x&&typeof x==="object")return Object.entries(x).forEach(([a,b])=>paths(b,a));if(typeof x==="string"&&path.isAbsolute(x)&&/(path|root|dir|home|prefix|locator|destination|write|target|file)$/i.test(k))inside(codexRoot,path.resolve(x),`planned ${k}`)}paths(data.targets); + return{fixture_target_id:plan.client_id,fixture_target_root:codexRoot,security:{schema_version:assessment.schema_version,scanner:assessment.scanner,policy:assessment.policy,outcome:assessment.outcome,counts:assessment.counts,scanned_files:assessment.scanned_files,evidence_source:assessment.evidence_source,report_digest:assessment.report_digest,subject:assessment.subject}}; +} +// Darwin's packageview profile requires a genuine read-only local APFS mount +// for any source opened in a fresh process; GeneratedStaging (view.go) only +// proves the exact in-process init handle and can never cover a later +// validate/inspect/test process. This builds and attaches that immutable view +// of the just-generated project and verifies it reproduces the exact +// generated bytes; consume's cleanup always detaches it, even on failure. +function darwinPackageView(root,projectDir){ + const image=path.join(root,"packageview.dmg"),mount=path.join(root,"packageview-mount"); + fs.mkdirSync(mount,{mode:0o700}); + ok(run("hdiutil",["create","-quiet","-srcfolder",projectDir,"-fs","APFS","-format","UDRO",image]),"darwin packageview image create"); + ok(run("hdiutil",["attach","-quiet","-readonly","-nobrowse","-mountpoint",mount,image]),"darwin packageview image attach"); + if(JSON.stringify(tree(mount))!==JSON.stringify(tree(projectDir)))fail("darwin packageview read-only copy diverged from the generated project"); + return mount; +} +function consume(c){const root=absolute("root",c.root),input=absolute("input",c.input),head=c.expectedHead;if(!/^[0-9a-f]{40}$/.test(head||""))fail("expectedHead must be an exact commit");fresh(root,c.precreatedRoot===true);if(!TARGETS[c.platform]?.includes(c.arch))fail("unsupported Milestone A platform lane");const outer=path.dirname(root),before=snapshot(outer,[root,...(c.snapshotExcludes||[])]),reports={},trees={},provenances={},roots={},views={},bases={},assertions=[];let primary,receipt; + try{const proofs={};for(const p of PRODUCTS){const j=makeJourney(root,p);roots[p]=j.root;const env={...envFor(j),UAP_PRIVATE_NPM_CANDIDATE:c.candidateRoot?absolute("candidateRoot",c.candidateRoot):path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{recursive:true,mode:0o700});if(c.platform!=="windows")fs.chmodSync(env.UAP_PRIVATE_NPM_CACHE,0o700);let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-0.1.91.tgz":"plugin-kit-ai-2.0.0.tgz",npmCLI=absolute("npm CLI",c.npm);ok(run(process.execPath,[npmCLI,"install","--ignore-scripts","--offline","--no-package-lock","--prefix",j.installation,path.join(input,"packages",tgz)],{env}),`install ${p}`);ep=path.join(j.installation,"node_modules",p==="agentplugins"?"universal-agent-plugins":p,"bin",`${p}.js`)}ep=absolute(`${p} entrypoint`,ep);provenances[p]=provenance(ep,head);const argv=a=>[ep,...(p==="agentplugins"?["author"]:[]),...a];reports[p]=[];reports[p].push(result(run(process.execPath,argv(["init",j.project,"--template=skill","--name=milestone-a-fixture","--description=Disposable Milestone A fixture.","--format=json"]),{env}),`${p} init`));bases[p]=[path.join(roots[p],"project")];const readProject=c.platform==="darwin"?(views[p]=darwinPackageView(j.root,j.project)):j.project;if(c.platform==="darwin")assertions.push(`${p}:packageview-readonly`);for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])]){reports[p].push(result(run(process.execPath,argv([cmd,readProject,"--format=json"]),{env}),`${p} ${cmd}`));bases[p].push(readProject)}if(c.platform==="linux"&&p==="agentplugins"){const add=result(run(process.execPath,[ep,"add",j.project,"--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);bases[p].push(j.project);proofs[p]=addProof(add,j.client)}for(const r of reports[p]){const rev=r.data?.revision??r.revision;if(rev!==undefined&&rev!==head)fail(`${p} reported revision does not equal exact candidate`);if(rev===undefined&&provenances[p].revision!==head)fail(`${p} result omitted revision without exact package provenance`);reported(r,j.root)}trees[p]=tree(j.project);assertions.push(`${p}:commands`,`${p}:revision`,`${p}:paths`,`${p}:fixture-codex`,...(c.platform==="linux"&&p==="agentplugins"?[`${p}:security-proof`]:[]))}const sharedLen=reports["plugin-kit-ai"].length,norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].slice(0,sharedLen).map((r,i)=>normalize(r,bases[p][i],c.platform==="windows"?"win32":c.platform))]));if(JSON.stringify(norm.agentplugins)!==JSON.stringify(norm["plugin-kit-ai"]))fail("entrypoint command JSON differs after allowed normalization");if(JSON.stringify(trees.agentplugins)!==JSON.stringify(trees["plugin-kit-ai"]))fail("entrypoints generated different trees");assertions.push("ordered-json-equality","generated-tree-equality");receipt={schema:"milestone-a-e2e-run/v1",platform:c.platform,arch:c.arch,exact_candidate:PRODUCTS.every(p=>assertions.includes(`${p}:revision`)),entrypoints:PRODUCTS,commands:c.platform==="linux"?COMMANDS:["init","validate"],fixture_target_id:c.platform==="linux"?proofs.agentplugins.fixture_target_id:null,fixture_roots:c.platform==="linux"?{agentplugins:proofs.agentplugins.fixture_target_root}:{},security_assessments:c.platform==="linux"?{agentplugins:proofs.agentplugins.security}:{},provenances,reports,trees,package_acquisition:acquisition(input),security_boundary:securityNetwork(),assertions,clean_root_separation:false,cleanup:"pending"};fs.writeFileSync(path.join(root,"evidence","run.json"),JSON.stringify(receipt,null,2)+"\n");return receipt}catch(e){primary=e;fs.writeFileSync(path.join(root,"evidence","failure.json"),JSON.stringify({schema:"milestone-a-e2e-failure/v1",platform:c.platform,arch:c.arch,message:e.message,assertions,cleanup:"pending"},null,2)+"\n");throw e}finally{const detachFailures=[];for(const p of PRODUCTS)if(views[p]){const d=cp.spawnSync("hdiutil",["detach","-quiet",views[p]],{encoding:"utf8",timeout:30000});if(d.status!==0)detachFailures.push(`darwin packageview detach failed for ${p}: ${commandOutput(d)}`)}for(const p of PRODUCTS)if(roots[p])fs.rmSync(roots[p],{recursive:true,force:true});if(fs.readdirSync(path.join(root,"journeys")).length)fail("journey cleanup incomplete");if(JSON.stringify(before)!==JSON.stringify(snapshot(outer,[root,...(c.snapshotExcludes||[])])))fail("unexpected change outside controlled root");if(detachFailures.length){const msg=detachFailures.join("; ");primary=primary?new Error(`${primary.message}; additionally, ${msg}`):new Error(msg)}const f=path.join(root,"evidence",primary?"failure.json":"run.json"),separation=new Set(Object.values(roots)).size===PRODUCTS.length;if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=separation;if(detachFailures.length)r.message=primary.message;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}else if(primary){fs.writeFileSync(f,JSON.stringify({schema:"milestone-a-e2e-failure/v1",platform:c.platform,arch:c.arch,message:primary.message,assertions,cleanup:"complete",clean_root_separation:separation},null,2)+"\n")}if(detachFailures.length)throw primary}} +function main(a){if(a.length!==2||!["prepare","run"].includes(a[0]))fail("usage: milestone-a-e2e.js ");const c=JSON.parse(fs.readFileSync(absolute("config",a[1])));return a[0]==="prepare"?prepare(c):consume(c)} +if(require.main===module)try{process.stdout.write(JSON.stringify(main(process.argv.slice(2)))+"\n")}catch(e){process.stderr.write(`Milestone A E2E: ${e.message}\n`);process.exitCode=1} +module.exports={prepare,consume,main,tree,snapshot,normalize,inside,precreate,nativeAbsolute,candidateIdentity,canonicalTool,resolveTrustedTools,resolveNpmCLI,commandOutput,envFor,COMMANDS,TARGETS,VERSIONS}; 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..5c25e0ff 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,361 @@ 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. + +### Completed public evidence source contracts (recovery work) + +The public acceptance module now separates recorded remote tool identities from +local controller admission. `historicalTools` compares remote paths, versions and +hashes to the source provisioning manifest without opening those paths. +`readCompletedJourney` consumes the owner custody facade's checked retained +`{root, artifact, attempt}` result and replays the existing semantic validators +only against retained evidence and the original recorded host namespace. +The `attempt` contains a producer, completed status, successful conclusion and +fixed job identities; each job binds its run, attempt, source and ref. The custody +owner still must supply the real facade and accepted upload/attempt binding. + +`encodeAcceptance`/`decodeAcceptance` retain the original S/I and both full pack +pins, all eighteen ordered R1 locators, the designated same-artifact bridge and +the separate R2 producer. `acceptanceGraph` checks the distinct completed R1/R2/R3 +attempts and unchanged E digest. The index codec rejects missing cells, duplicate +or unbounded members and self-referential E/index entries; its local closure +reader pins every retained member. These contracts are not proof of execution. + +The fixed public subject wrapper requires exactly the two E2 names and delegates +to the existing unchanged workflow verifier. Native admission remains closed as +before. No fixture, codec, local bridge receipt or source registration qualifies +public E or Q. Aggregate orchestration, attestation intake, the complete E reader, +the four-invocation workflow and P integration are still unfinished in this +recovery increment. No workflow dispatch or signing occurred. + +The authenticated Python source seal now includes the scaffold template directory +used by the generated-project validator. It remains closed on missing controller +or execution prerequisites before launching the bridge or producing outputs. + +### Public completion assembly and workflow continuation + +The source now wires `--assemble`, `--attest-inputs` and `--read` to the completed +journey/bridge closure reader. Assembly re-admits original I19/S3 and all eighteen +R1 artifacts twice, compares the complete retained closure, and writes E last. +Attestation intake re-admits completed R2 and R1 and prepares exactly the E/index +subjects. The completed reader binds R1, original R2 and completed R3 separately, +uses the unchanged public verifier API, and repeats custody and byte checks. + +`authoring-public-packed.yml` defines four manual invocations. Produce preflight +resolves the complete source-frozen matrix before any product job is scheduled; +all cells and the designated live bridge must succeed. Assemble consumes exact +R1 locators. Attest uses a separate hosted signer job with only E2 signing +permissions and compares the full closure after signing. Check consumes exact +R2/R3 locators. No running invocation waits for its own completion. Python/OS and +the protected exact checkout remain bootstrap authority; no setup action or +receipt authenticates the first Node executable. Immutable provision is still an +external prerequisite, not established by hash-then-exec checks. + +The fixed P adapter invokes the same completed reader and compares Q identity, +preparation, all twelve full native asset pins, original I/S and both full pack +pins. Source schema registration does not open `requireNativeContracts`, whose +global failure remains unconditional. P retains both admission results for its +existing repeated pre-effect admission path. No new publisher is introduced. + +These are source contracts, not genuine E acceptance. The real custody, +observation and installer facades and immutable eighteen-cell provision remain +missing; cross-host execution, actual verifier compatibility, protected workflow +settings, exact-head review and genuine E2E remain unverified. N2 remains NOT +ACCEPTED and its refused internals are unchanged. Fixtures cannot close these +dependencies or any of the remaining full-program phases. + +The fixed `public-evidence` R3 transport root now contains exactly +`assembly-locator.json` and `evidence/`. The locator file is the canonical original +R2 `{sha256,artifact}` locator; `evidence/` contains the unchanged E/index/retained +closure from R2. Only E and index are signing subjects. R4 compares the external +locator to its independently selected original R2 locator before signature +verification, after verification and at late artifact readback. Neither E nor +index contains its own digest or artifact identity. The custody owner must support +this fixed transport closure through `readPublicArtifact(kind=public-evidence)`; +no archive reader or verification policy is added by this source implementation. diff --git a/npm/agentplugins/scripts/packed-installer-bridge.test.js b/npm/agentplugins/scripts/packed-installer-bridge.test.js index 1f2f763a..19d928ca 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), /PUBLIC_PROVISIONING_REQUIRED/); +}); +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/private-npm/bootstrap.js b/npm/agentplugins/scripts/private-npm/bootstrap.js index 1e7321fa..b8c0dcc8 100644 --- a/npm/agentplugins/scripts/private-npm/bootstrap.js +++ b/npm/agentplugins/scripts/private-npm/bootstrap.js @@ -86,9 +86,25 @@ function freezeSelected(root, release) { return binary; } +// A single digest over the complete canonical identity tuple (namespace, mode, +// candidate digest, product, version, target, exact binary digest) keeps the +// resolved Windows path short and constant-width while still separating every +// distinguishing field; truncating directory names cannot bound this because +// the fixed suffix already approaches legacy MAX_PATH on its own. +function cacheIdentity(product, target, release) { + return c.digest(c.encode({ + namespace: "dual-authoring-npm-cache/v2", + authoring_mode: release.descriptor.authoring_mode, + candidate_sha256: release.descriptor.candidate_sha256, + product, + version: release.version, + target, + binary_sha256: release.asset.binary.sha256, + })); +} + function cachePath(root, product, target, release) { - return path.join(root, "dual-authoring-npm-v1", release.descriptor.authoring_mode, - release.descriptor.candidate_sha256, product, release.version, target, release.asset.binary.sha256, release.asset.binary.file); + return path.join(root, "v2", cacheIdentity(product, target, release), release.asset.binary.file); } // hooks are internal fault/observation seams for offline structural tests. They diff --git a/npm/agentplugins/scripts/public-authoring-acceptance.js b/npm/agentplugins/scripts/public-authoring-acceptance.js new file mode 100644 index 00000000..ad200ddb --- /dev/null +++ b/npm/agentplugins/scripts/public-authoring-acceptance.js @@ -0,0 +1,1738 @@ +"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 = 4; + 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 = 4; + }; + for (const row of value.rows) { + const bytes = c.encode(row); assert.ok(bytes.length <= LIMIT, '1MiB process record'); + // c.encode indents every line two more spaces inside an array. Count + // actual encoded bytes, including commas; path length must not make a + // nominally bounded shard exceed its byte limit at flush time. + let lines = 0; for (const byte of bytes) if (byte === 10) lines++; + const nested = bytes.length + 2 * lines; + if (size + nested + (shard.length ? 1 : 0) > TRANSCRIPT_LIMIT) flush(); + size += nested + (shard.length ? 1 : 0); shard.push(row); + } + 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; +} +// E syntax is deliberately separate from completed-attempt admission. Encoding +// these records cannot establish custody, recompute assertions, or qualify E. +const ACCEPTANCE_SCHEMA = 'authoring-public-packed/v1'; +const ACCEPTANCE_FILE = 'public-packed-completion.json'; +const INDEX_FILE = 'public-packed-evidence.json'; +const ATTESTATION_LINK = 'assembly-locator.json'; +const E_FIELDS = ['schema', 'lane', 'identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256', + 'native_inputs', 'stage', 'packs', 'producer', 'matrix', 'journeys', 'bridge', 'evidence', 'assertions']; +const BRIDGE_CELL = 'linux-amd64/pair-node22'; +function orderedLocator(value) { + locator(value); + return { sha256: value.sha256, artifact: Object.fromEntries( + ['run_id', 'run_attempt', 'artifact_id', 'artifact_sha256'].map(k => [k, value.artifact[k]])) }; +} +function acceptance(value, inputBytes, stageBytes) { + const input = contract.decodeInputs(inputBytes), stage = require('./stage-authoring-npm').decodeStage(stageBytes, inputBytes); + fields(value, E_FIELDS, 'C3 E'); fixed(value.schema, ACCEPTANCE_SCHEMA, 'E schema'); + fixed(value.lane, 'public-packed-pair', 'single public Q lane'); + const inherited = ['identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256', 'native_inputs', 'packs']; + for (const k of inherited) agree(value[k], stage[k], `E original S ${k}`); + const retainedStage = orderedLocator(value.stage); + agree(retainedStage.sha256, c.digest(stageBytes), 'E exact original S bytes'); + agree([retainedStage.artifact.run_id, retainedStage.artifact.run_attempt], + [stage.producer.run_id, stage.producer.run_attempt], 'E original S attempt'); + producer(value.producer, input); + const upstreamRuns = [input.preparation.artifact.run_id, input.producer.run_id, stage.producer.run_id]; + assert.ok(!upstreamRuns.includes(value.producer.run_id), 'R2 separate from frozen input invocations'); + agree(value.matrix, { schema: MATRIX_SCHEMA, cells: matrix.map(row => row.key) }, 'E exact eighteen-cell matrix'); + list(value.journeys, matrix.length, 'E eighteen ordered journeys'); + const ids = new Set([input.preparation.artifact.artifact_id, stage.native_inputs.artifact.artifact_id, retainedStage.artifact.artifact_id]); + assert.equal(ids.size, 3, 'distinct upstream artifact identities'); + const digests = new Set(), archives = new Set(); let attempt; + const journeys = value.journeys.map((row, i) => { + fields(row, ['cell', 'sha256', 'artifact'], 'E journey locator'); fixed(row.cell, matrix[i].key, 'E ordered cell'); + const located = orderedLocator({ sha256: row.sha256, artifact: row.artifact }); + const current = [row.artifact.run_id, row.artifact.run_attempt]; + if (attempt === undefined) attempt = current; + agree(current, attempt, 'all eighteen J from the same exact R1 attempt'); + assert.ok(!upstreamRuns.includes(current[0]) && current[0] !== value.producer.run_id, 'R1 and R2 are independent invocations'); + assert.ok(!ids.has(row.artifact.artifact_id), 'unique cell artifact ID'); ids.add(row.artifact.artifact_id); + assert.ok(!digests.has(row.sha256), 'unique cell J digest'); digests.add(row.sha256); + assert.ok(!archives.has(row.artifact.artifact_sha256), 'unique cell archive digest'); archives.add(row.artifact.artifact_sha256); + return { cell: row.cell, ...located }; + }); + const bridge = orderedLocator(value.bridge), designated = journeys.find(row => row.cell === BRIDGE_CELL); + agree(bridge.artifact, designated.artifact, 'bridge retained in designated same-cell R1 artifact'); + assert.ok(!digests.has(bridge.sha256), 'bridge record is separate from J'); + fields(value.evidence, ['path', 'size', 'sha256'], 'E index pin'); fixed(value.evidence.path, INDEX_FILE, 'E index filename'); + positive(value.evidence.size, LIMIT, 'index byte bound'); hash(value.evidence.sha256, 'index'); + fields(value.assertions, ASSERTIONS, 'E assertion syntax'); + for (const k of ASSERTIONS) fixed(value.assertions[k], true, 'E assertion syntax only'); + const normalized = { schema: ACCEPTANCE_SCHEMA, lane: 'public-packed-pair', + ...Object.fromEntries(inherited.map(k => [k, stage[k]])), stage: retainedStage, + producer: Object.fromEntries(['workflow', 'source', 'ref', 'run_id', 'run_attempt'].map(k => [k, value.producer[k]])), + matrix: { schema: MATRIX_SCHEMA, cells: matrix.map(row => row.key) }, journeys, bridge, + evidence: { path: INDEX_FILE, size: value.evidence.size, sha256: value.evidence.sha256 }, + assertions: Object.fromEntries(ASSERTIONS.map(k => [k, true])) }; + return Object.fromEntries(E_FIELDS.map(k => [k, normalized[k]])); +} +function encodeAcceptance(value, input, stage) { + const body = c.encode(acceptance(value, input, stage)); assert.ok(body.length <= LIMIT, 'E byte bound'); return body; +} +function decodeAcceptance(body, input, stage) { + const result = acceptance(bounded(body, LIMIT), input, stage); + agree(body, c.encode(result), 'fixed E field order'); return result; +} +// This table is shared by the completed reader and the fixed workflow wrappers. +// Distinct invocations are essential: an executing job cannot attest its own +// eventual success. Artifact names alone never identify an attempt. +const PUBLIC_JOBS = freeze({ + produce: ['public_inputs', ...matrix.map(row => `public_cell (${row.key})`), 'public_producer_complete'], + assemble: ['public_assemble'], + attest: ['public_evidence_intake', 'public_evidence_attestation'], + check: ['public_check'] +}); +const INDEX_SCHEMA = 'authoring-public-packed-evidence/v1'; +const BRIDGE_FILES = freeze(['authenticated-run.json', 'summary.json', 'bridge-config/request.json', 'bridge-config/sealed.json', + 'results/completion.json', ...['head', 'clean', 'seal', 'discovery', 'planner', 'post-verify', 'terminal-clean'].flatMap(n => + ['json', 'stdout', 'stderr'].map(ext => `logs/${n}.${ext}`))].sort()); +function closureBytes(file, maximum = TRANSCRIPT_LIMIT) { + // Empty bridge stdout/stderr is valid evidence. All nonempty members use the + // existing bounded reader; never interpret these retained bytes as an archive. + c.safeDirectory(path.dirname(file)); const st = fs.lstatSync(file); + if (st.size !== 0) return c.readFile(file, maximum); + assert.ok(st.isFile() && st.nlink === 1, 'empty retained regular file'); + const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)); + try { + for (const after of [fs.fstatSync(fd), fs.lstatSync(file)]) + assert.ok(['dev', 'ino', 'size', 'mode', 'nlink', 'mtimeMs', 'ctimeMs'].every(k => after[k] === st[k]), 'empty retained file changed'); + return Buffer.alloc(0); + } finally { fs.closeSync(fd); } +} +function acceptanceIndex(value, e) { + fields(value, ['schema', 'matrix', 'journeys', 'bridge', 'files'], 'E exhaustive evidence index'); + fixed(value.schema, INDEX_SCHEMA, 'E index schema'); agree(value.matrix, e.matrix, 'index matrix'); + agree(value.journeys, e.journeys, 'index original J locators'); agree(value.bridge, e.bridge, 'index original bridge locator'); + assert.ok(Array.isArray(value.files) && value.files.length > 0 && value.files.length <= 32768, 'bounded exhaustive file inventory'); + let previous = '', total = 0; const names = new Set(); + for (const row of value.files) { + fields(row, ['path', 'size', 'sha256'], 'index member'); hash(row.sha256, 'index member'); + assert.ok(typeof row.path === 'string' && row.path.length <= 4096 && + /^[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*$/.test(row.path) && + !row.path.split('/').some(p => p === '.' || p === '..') && row.path > previous, 'canonical sorted unique index path'); + previous = row.path; + assert.ok(!names.has(row.path.toLowerCase()), 'case-independent unique closure path'); names.add(row.path.toLowerCase()); + assert.ok(row.path.startsWith('journeys/') || row.path.startsWith('bridge/'), 'fixed closure namespace; no E/index digest cycle'); + nonnegative(row.size, TRANSCRIPT_LIMIT, 'bounded closure member'); total += row.size; + assert.ok(total <= AGGREGATE_LIMIT, '128MiB aggregate acceptance closure'); + if (row.path.startsWith('journeys/')) { + const parts = row.path.split('/'), key = parts.slice(1, 3).join('/'); cell(key); + const member = parts.slice(3).join('/'); + assert.ok(member === 'public-journey.json' || EVIDENCE.includes(member) || /^sidecars\/[a-zA-Z0-9_.-]+$/.test(member), 'fixed J closure member'); + } else assert.ok(BRIDGE_FILES.includes(row.path.slice(7)), 'fixed retained bridge closure'); + } + for (const j of e.journeys) { + const prefix = `journeys/${j.cell}/`, record = value.files.find(row => row.path === prefix + 'public-journey.json'); + assert.ok(record && record.sha256 === j.sha256 && record.size > 0 && record.size <= LIMIT, 'each original J in exhaustive closure'); + for (const name of EVIDENCE) assert.ok(names.has(prefix + name), 'all five evidence records for each J'); + } + assert.ok(value.files.some(row => row.path === 'bridge/summary.json' && row.sha256 === e.bridge.sha256 && row.size > 0), 'same-cell bridge summary pin'); + for (const name of BRIDGE_FILES) assert.ok(names.has('bridge/' + name), 'complete bridge closure'); + return { schema: INDEX_SCHEMA, matrix: e.matrix, journeys: e.journeys, bridge: e.bridge, + files: value.files.map(row => ({ path: row.path, size: row.size, sha256: row.sha256 })) }; +} +function encodeAcceptanceIndex(value, e) { + const bytes = c.encode(acceptanceIndex(value, e)); assert.ok(bytes.length <= LIMIT, 'bounded index'); return bytes; +} +function decodeAcceptanceIndex(bytes, e) { + const index = acceptanceIndex(bounded(bytes, LIMIT), e); agree(bytes, c.encode(index), 'fixed index field order'); return index; +} +function readAcceptanceClosure(root, e) { + c.safeDirectory(absolute(root)); + const bytes = pin(path.join(root, INDEX_FILE), e.evidence.sha256); agree(bytes.length, e.evidence.size, 'index pin size'); + const index = decodeAcceptanceIndex(bytes, e), found = [], directories = new Set(); let count = 0; + for (const row of index.files) { + const parts = row.path.split('/'); parts.pop(); + while (parts.length) { directories.add(parts.join('/')); parts.pop(); } + } + const visit = (directory, prefix) => { + c.safeDirectory(directory); + for (const name of fs.readdirSync(directory).sort()) { + assert.ok(++count <= 65536, 'bounded closure entries'); + const file = path.join(directory, name), st = fs.lstatSync(file), relative = prefix + name; + if (st.isDirectory()) { + assert.ok(directories.has(relative), 'no unindexed retained directory'); + visit(file, relative + '/'); + } + else { + assert.ok(st.isFile() && st.nlink === 1, 'regular unaliased retained closure'); + if (prefix === '' && [ACCEPTANCE_FILE, INDEX_FILE].includes(name)) continue; + const row = index.files.find(row => row.path === relative); assert.ok(row, 'no unindexed retained member'); + const body = closureBytes(file); agree(c.digest(body), row.sha256, 'retained closure digest'); agree(body.length, row.size, 'retained closure size'); found.push(relative); + } + } + }; + visit(root, ''); agree(found.sort(), index.files.map(row => row.path), 'exhaustive retained closure'); + agree(pin(path.join(root, INDEX_FILE), e.evidence.sha256), bytes, 'late index bytes'); return index; +} +function completedAttempt(value, selected, mode) { + fields(value, ['producer', 'status', 'conclusion', 'jobs'], 'completed public attempt'); + fields(selected, ['tag', 'ref', 'source', 'versions'], 'public selection'); + fields(value.producer, ['workflow', 'source', 'ref', 'run_id', 'run_attempt'], 'public attempt producer'); + fixed(value.producer.workflow, WORKFLOW, 'public attempt workflow'); + fixed(value.producer.source, selected.source, 'public attempt F'); + fixed(value.producer.ref, selected.ref, 'public attempt ref'); + fixed(selected.ref, `refs/tags/${selected.tag}`, 'canonical selected tag ref'); + positive(value.producer.run_id, Number.MAX_SAFE_INTEGER, 'public attempt run'); + positive(value.producer.run_attempt, 1000, 'public attempt number'); + fixed(value.status, 'completed', 'completed public invocation'); fixed(value.conclusion, 'success', 'successful public invocation'); + assert.ok(Object.hasOwn(PUBLIC_JOBS, mode), 'fixed public mode'); + list(value.jobs, PUBLIC_JOBS[mode].length, 'all fixed attempt jobs'); + const ids = new Set(), names = new Set(); + for (const job of value.jobs) { + fields(job, ['id', 'name', 'run_id', 'run_attempt', 'source', 'ref', 'status', 'conclusion'], 'public attempt job'); + positive(job.id, Number.MAX_SAFE_INTEGER, 'public job ID'); + assert.ok(!ids.has(job.id) && !names.has(job.name), 'unique public attempt job'); ids.add(job.id); names.add(job.name); + assert.ok(PUBLIC_JOBS[mode].includes(job.name), 'fixed public attempt job name'); + for (const k of ['run_id', 'run_attempt', 'source', 'ref']) agree(job[k], value.producer[k], `public job ${k}`); + fixed(job.status, 'completed', 'completed public job'); fixed(job.conclusion, 'success', 'no successful skips'); + } + return value.producer; +} +function acceptanceGraph(e, attempts, selected, assembly, attested) { + fields(attempts, ['produce', 'assemble', 'attest'], 'R1 R2 R3 graph'); + locator(assembly); locator(attested); + const producers = Object.fromEntries(['produce', 'assemble', 'attest'].map(mode => + [mode, completedAttempt(attempts[mode], selected, mode)])); + agree(producers.assemble, e.producer, 'original E producer is R2'); + assert.equal(new Set(Object.values(producers).map(p => p.run_id)).size, 3, 'R1 R2 R3 distinct invocations'); + const bind = (loc, p) => agree([loc.artifact.run_id, loc.artifact.run_attempt], [p.run_id, p.run_attempt], 'exact graph artifact attempt'); + e.journeys.forEach(j => bind(j, producers.produce)); bind(e.bridge, producers.produce); + bind(assembly, producers.assemble); bind(attested, producers.attest); + agree(assembly.sha256, attested.sha256, 'unchanged R2 E carried into R3'); + assert.ok(!e.journeys.some(j => [assembly.artifact.artifact_id, attested.artifact.artifact_id].includes(j.artifact.artifact_id)) && + assembly.artifact.artifact_id !== attested.artifact.artifact_id, 'separate assembly and attestation artifacts'); + return producers; +} +// Pure historical comparison: never stat or execute a recorded remote tool. +function historicalTools(j, manifest) { + const selected = cell(j.cell), provisioned = manifest.cells[j.cell], controller = manifest.controllers[selected.target]; + assert.ok(provisioned && controller, `PUBLIC_PROVISIONING_REQUIRED:${j.cell}:entry`); + for (const key of ['runner', 'image', 'observer', ...(selected.node === 18 ? [] : ['installer_policy']), 'npm_node', 'shim_node', 'npm']) + assert.ok(provisioned[key] != null, `PUBLIC_PROVISIONING_REQUIRED:${j.cell}:${key}`); + const fixedTools = { orchestrator_node: controller.node, npm_node: provisioned.npm_node, shim_node: provisioned.shim_node, + npm: provisioned.npm, go: j.cell === BRIDGE_CELL ? provisioned.go : null }; + for (const [key, expected] of Object.entries(fixedTools)) { + if (key === 'go' && j.cell !== BRIDGE_CELL) { agree(j.tools[key], null, 'remote Go absent outside bridge cell'); continue; } + assert.ok(expected, `PUBLIC_PROVISIONING_REQUIRED:${j.cell}:${key}`); + agree(j.tools[key], Object.fromEntries(['path', 'sha256', 'version'].map(k => [k, expected[k]])), 'historical source-frozen tool identity'); + } + return j.tools; +} +function retainedJourney(root, located, inputBytes, stageBytes, manifest, attempt, selected) { + c.safeDirectory(absolute(root)); locator(located); + const body = pin(path.join(root, 'public-journey.json'), located.sha256), j = decodeJourney(body, inputBytes, stageBytes); + agree(j.producer, completedAttempt(attempt, selected, 'produce'), 'J completed R1 producer'); + agree([j.producer.run_id, j.producer.run_attempt], [located.artifact.run_id, located.artifact.run_attempt], 'J artifact exact attempt'); + historicalTools(j, manifest); + const evidence = {}, budget = { size: j.evidence.reduce((sum, row) => sum + row.size, body.length) }; + for (const row of j.evidence) { + const maximum = row.path === 'commands.json' ? TRANSCRIPT_LIMIT : LIMIT; + const bytes = pin(path.join(root, row.path), row.sha256, maximum); agree(bytes.length, row.size, 'retained remote evidence size'); + evidence[row.path] = expandEvidence(row.path, bounded(bytes, maximum), root, budget); + } + // Only retained closure paths are opened. Original project paths remain the + // recorded host namespace consumed by the same semantic validators as R1. + const result = verifyJourney({ record: j, evidence }); verifySidecars(evidence, root); + agree(pin(path.join(root, 'public-journey.json'), located.sha256), body, 'late retained J bytes'); + return result; +} +function readCompletedJourney(value) { + fields(value, ['schema', 'selected', 'workflow_sha', 'input_file', 'stage', 'repo', 'work_parent', 'journey'], 'completed J request'); + fixed(value.schema, 'authoring-public-completed-journey/v1', 'completed J request schema'); locator(value.journey); locator(value.stage); + const provision = require('./public-authoring-tools'); + agree(provision.requireController('linux-amd64'), process.execPath, 'independently provisioned reader controller'); + const api = requireFacades(BRIDGE_CELL)['public-authoring-custody']; + assert.equal(typeof api.readPublicArtifact, 'function', 'PUBLIC_FACADE_REQUIRED:public-authoring-custody.js#readPublicArtifact'); + const manifest = provision.readProvisioning(), frozen = { ...manifest, key: BRIDGE_CELL }; + agree(value.repo, path.resolve(__dirname, '../../..'), 'executing completed reader source'); + const before = sourceSeal(value.repo, value.workflow_sha, frozen); + const retained = api.readPublicArtifact({ kind: 'public-journeys', locator: value.journey, selected: value.selected, + workflow_sha: value.workflow_sha, work_parent: value.work_parent }); + // Owner-supplied custody returns checked retained bytes and exact attempt + // identity; no caller callback, completion boolean or archive engine here. + fields(retained, ['root', 'artifact', 'attempt'], 'retained public artifact'); + agree(retained.artifact, value.journey.artifact, 'retained artifact provider identity'); + const p = completedAttempt(retained.attempt, value.selected, 'produce'); + const inputs = admitProducerInputs({ ...value, producer: p, tools: null }, api); + const result = retainedJourney(retained.root, value.journey, inputs.inputBytes, inputs.stageBytes, manifest, retained.attempt, value.selected); + const after = api.readPublicArtifact({ kind: 'public-journeys', locator: value.journey, selected: value.selected, + workflow_sha: value.workflow_sha, work_parent: value.work_parent }); + agree(after.artifact, retained.artifact, 'late J artifact identity'); agree(after.attempt, retained.attempt, 'late R1 attempt'); + const rechecked = retainedJourney(after.root, value.journey, inputs.inputBytes, inputs.stageBytes, manifest, after.attempt, value.selected); + agree(rechecked, result, 'late completed J closure'); + provision.requireController('linux-amd64'); agree(sourceSeal(value.repo, value.workflow_sha, frozen), before, 'late reader source closure'); + return result; +} +const READER_FIELDS = ['schema', 'selected', 'workflow_sha', 'input_file', 'stage', 'repo', 'work_parent']; +function readerContext(r, mode) { + const provision = require('./public-authoring-tools'); + agree(provision.requireController('linux-amd64'), process.execPath, 'independently provisioned E reader'); + const extra = { assemble: ['output', 'producer', 'journeys', 'bridge'], attest: ['assembly', 'output'], read: ['acceptance', 'assembly'] }; + fields(r, [...READER_FIELDS, ...extra[mode]], 'closed completed E request'); + fixed(r.schema, `authoring-public-${mode}/v1`, 'fixed completed E operation'); + fields(r.selected, ['tag', 'ref', 'source', 'versions'], 'selected E identity'); + agree(r.workflow_sha, r.selected.source, 'E reader F'); locator(r.stage); + for (const k of ['input_file', 'repo', 'work_parent']) absolute(r[k]); + agree(r.repo, path.resolve(__dirname, '../../..'), 'executing E source'); c.safeDirectory(r.work_parent); + disjoint([r.repo, r.work_parent, r.input_file]); + if (mode !== 'read') { absolute(r.output); c.safeDirectory(path.dirname(r.output)); assert.ok(!fs.existsSync(r.output), 'fresh E output'); disjoint([r.repo, r.work_parent, r.input_file, r.output]); } + const api = requireFacades(BRIDGE_CELL)['public-authoring-custody']; + assert.equal(typeof api.readPublicArtifact, 'function', 'PUBLIC_FACADE_REQUIRED:public-authoring-custody.js#readPublicArtifact'); + const manifest = provision.readProvisioning(), frozen = { ...manifest, key: BRIDGE_CELL }, before = sourceSeal(r.repo, r.workflow_sha, frozen); + const recheck = () => { + agree(provision.requireController('linux-amd64'), process.execPath, 'late E controller'); + agree(provision.readProvisioning(), manifest, 'late E manifest'); agree(sourceSeal(r.repo, r.workflow_sha, frozen), before, 'late E full source closure'); + }; + return { r, api, manifest, recheck }; +} +function completedArtifact(ctx, located, mode) { + locator(located); ctx.recheck(); + const { r, api } = ctx, kind = { produce: 'public-journeys', assemble: 'public-assembly', attest: 'public-evidence' }[mode]; + const result = api.readPublicArtifact({ kind, locator: located, selected: r.selected, workflow_sha: r.workflow_sha, work_parent: r.work_parent }); + fields(result, ['root', 'artifact', 'attempt'], 'owner retained public artifact'); + c.safeDirectory(absolute(result.root)); agree(result.artifact, located.artifact, 'exact public artifact custody'); + agree(result.attempt, require('./authoring-promotion').inspectPublicAttempt(located.artifact, r.selected, mode, r.work_parent), 'independent fixed public attempt'); + completedAttempt(result.attempt, r.selected, mode); ctx.recheck(); return result; +} +function bridgeReplay(ctx, root, j, jroot, located) { + agree(c.digest(c.readFile(path.join(root, 'summary.json'), LIMIT)), located.sha256, 'designated original bridge digest'); + const python = ctx.manifest.controllers['linux-amd64'].python; + assert.ok(python, 'PUBLIC_PROVISIONING_REQUIRED:linux-amd64:python'); ctx.recheck(); + const result = require('node:child_process').spawnSync(python.path, ['-B', path.join(ctx.r.repo, 'scripts/check-packed-ci.py'), + '--completed-bridge', root, ctx.r.workflow_sha, path.join(jroot, 'public-journey.json'), path.join(jroot, 'projects.json')], { + cwd: ctx.r.repo, env: { PATH: '/usr/local/bin:/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' }, timeout: 60000, maxBuffer: TRANSCRIPT_LIMIT + }); + ctx.recheck(); assert.ok(!result.error && result.status === 0 && result.signal === null && result.stderr.length === 0, 'completed bridge semantic replay'); + const inputs = bounded(result.stdout, TRANSCRIPT_LIMIT); + agree(inputs.public_inputs.journey_sha256, c.digest(c.readFile(path.join(jroot, 'public-journey.json'), LIMIT)), 'bridge exact original J pin'); + agree(inputs.identity, j.identity, 'bridge J identity'); agree(inputs.public_inputs.producer, j.producer, 'bridge same-live-root R1'); return inputs; +} +function collectJourneys(ctx, e, intake) { + const files = new Map(), journeys = [], attempts = []; let total = 0, designated; + const add = (name, file) => { + assert.ok(!files.has(name), 'unique aggregate member'); const bytes = closureBytes(file); total += bytes.length; + assert.ok(total <= AGGREGATE_LIMIT, '128MiB aggregate E'); files.set(name, bytes); + }; + for (const loc of e.journeys) { + const located = { sha256: loc.sha256, artifact: loc.artifact }, retained = completedArtifact(ctx, located, 'produce'); + const local = retainedJourney(retained.root, located, intake.inputBytes, intake.stageBytes, ctx.manifest, retained.attempt, ctx.r.selected); + agree(local.record.cell, loc.cell, 'ordered retained E cell'); agree(local.record.stage, ctx.r.stage, 'same original S across cells'); + if (attempts.length) agree(retained.attempt, attempts[0], 'one exact completed R1'); attempts.push(retained.attempt); journeys.push(local); + const names = ['public-journey.json', ...EVIDENCE, ...fs.readdirSync(path.join(retained.root, 'sidecars')).map(n => 'sidecars/' + n)]; + for (const name of names) add(`journeys/${loc.cell}/${name}`, path.join(retained.root, name)); + if (loc.cell === BRIDGE_CELL) designated = { retained, local }; + } + assert.ok(designated, 'designated bridge cell'); agree(e.bridge.artifact, designated.retained.artifact, 'same artifact bridge'); + const bridgeRoot = path.join(designated.retained.root, 'bridge'); + const bridge = bridgeReplay(ctx, bridgeRoot, designated.local.record, designated.retained.root, e.bridge); + for (const name of BRIDGE_FILES) add('bridge/' + name, path.join(bridgeRoot, name)); + return { files, journeys, bridge, attempt: attempts[0] }; +} +function compareFiles(a, b) { + agree([...a.keys()].sort(), [...b.keys()].sort(), 'same complete retained closure'); + for (const [name, body] of a) agree(body, b.get(name), 'late retained closure bytes'); +} +function intakeAgain(ctx, producer, first) { + const next = admitProducerInputs({ ...ctx.r, producer, tools: null }, ctx.api); + agree(next.inputBytes, first.inputBytes, 'late E authenticated I'); agree(next.stageBytes, first.stageBytes, 'late E authenticated S'); + agree(next.retained.map(({ kind, relative, sha256, mode }) => ({ kind, relative, sha256, mode })), + first.retained.map(({ kind, relative, sha256, mode }) => ({ kind, relative, sha256, mode })), 'late E full I19 S3 custody'); ctx.recheck(); +} +function writeBundle(ctx, eBytes, indexBytes, files) { + ctx.recheck(); fs.mkdirSync(ctx.r.output, { mode: 0o700 }); + // E is written last. Any interrupted/failed copy lacks its completion record. + for (const [name, bytes] of [...files, [INDEX_FILE, indexBytes]]) { + const file = path.join(ctx.r.output, name); fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); + fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 }); + } + ctx.recheck(); fs.writeFileSync(path.join(ctx.r.output, ACCEPTANCE_FILE), eBytes, { flag: 'wx', mode: 0o600 }); + return { root: ctx.r.output, sha256: c.digest(eBytes), subjects: [ACCEPTANCE_FILE, INDEX_FILE].map(name => ({ + file: path.join(ctx.r.output, name), sha256: c.digest(name === ACCEPTANCE_FILE ? eBytes : indexBytes) })) }; +} +function assembleAcceptance(r) { + const ctx = readerContext(r, 'assemble'), promotion = require('./authoring-promotion'); + agree(r.producer, promotion.inspectPublicCaller(r.selected, r.workflow_sha, 'assemble', r.work_parent), 'current R2 producer'); + const intake = admitProducerInputs({ ...r, tools: null }, ctx.api); + const e = { schema: ACCEPTANCE_SCHEMA, lane: 'public-packed-pair', + ...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, matrix: { schema: MATRIX_SCHEMA, cells: matrix.map(row => row.key) }, journeys: r.journeys, bridge: r.bridge, + evidence: { path: INDEX_FILE, size: 1, sha256: c.digest(Buffer.from('pending index')) }, assertions: Object.fromEntries(ASSERTIONS.map(k => [k, true])) }; + // Syntax admission precedes custody acquisition; assertions are recomputed by + // collectJourneys and never accepted merely because this table says true. + acceptance(e, intake.inputBytes, intake.stageBytes); + const first = collectJourneys(ctx, e, intake), second = collectJourneys(ctx, e, intake); compareFiles(first.files, second.files); + intakeAgain(ctx, r.producer, intake); + agree(r.producer, promotion.inspectPublicCaller(r.selected, r.workflow_sha, 'assemble', r.work_parent), 'late R2 caller'); + const index = { schema: INDEX_SCHEMA, matrix: e.matrix, journeys: e.journeys, bridge: e.bridge, + files: [...first.files].map(([path, bytes]) => ({ path, size: bytes.length, sha256: c.digest(bytes) })).sort((a, b) => a.path < b.path ? -1 : 1) }; + const indexBytes = encodeAcceptanceIndex(index, e); e.evidence = { path: INDEX_FILE, size: indexBytes.length, sha256: c.digest(indexBytes) }; + return writeBundle(ctx, encodeAcceptance(e, intake.inputBytes, intake.stageBytes), indexBytes, first.files); +} +function admittedAssembly(ctx, located) { + const retained = completedArtifact(ctx, located, 'assemble'), p = retained.attempt.producer; + const intake = admitProducerInputs({ ...ctx.r, producer: p, tools: null }, ctx.api); + const bytes = pin(path.join(retained.root, ACCEPTANCE_FILE), located.sha256), e = decodeAcceptance(bytes, intake.inputBytes, intake.stageBytes); + agree(e.producer, p, 'original completed R2 producer'); agree(e.stage, ctx.r.stage, 'requested original S'); + const index = readAcceptanceClosure(retained.root, e), collected = collectJourneys(ctx, e, intake); + const copied = new Map(index.files.map(row => [row.path, closureBytes(path.join(retained.root, row.path))])); compareFiles(copied, collected.files); + intakeAgain(ctx, p, intake); + const after = completedArtifact(ctx, located, 'assemble'); agree(after.attempt, retained.attempt, 'late R2 attempt'); + agree(pin(path.join(after.root, ACCEPTANCE_FILE), located.sha256), bytes, 'late R2 E'); readAcceptanceClosure(after.root, e); + return { retained, intake, bytes, e, index, collected }; +} +function attestAcceptanceInputs(r) { + const ctx = readerContext(r, 'attest'), promotion = require('./authoring-promotion'); locator(r.assembly); + const caller = promotion.inspectPublicCaller(r.selected, r.workflow_sha, 'attest', r.work_parent); + const first = admittedAssembly(ctx, r.assembly); + assert.ok(![first.e.producer.run_id, first.collected.attempt.producer.run_id].includes(caller.run_id), 'R3 separate from R1 R2'); + const second = admittedAssembly(ctx, r.assembly); agree(second.bytes, first.bytes, 'unchanged attestation E'); compareFiles(first.collected.files, second.collected.files); + agree(caller, promotion.inspectPublicCaller(r.selected, r.workflow_sha, 'attest', r.work_parent), 'late R3 caller'); + // R3 carries the original completed R2 locator outside the unchanged E/index + // closure. Neither signed subject contains its own artifact identity or digest. + ctx.recheck(); fs.mkdirSync(r.output, { mode: 0o700 }); + fs.writeFileSync(path.join(r.output, ATTESTATION_LINK), c.encode(orderedLocator(r.assembly)), { flag: 'wx', mode: 0o600 }); + const nested = { ...ctx, r: { ...r, output: path.join(r.output, 'evidence') } }; + const result = writeBundle(nested, first.bytes, encodeAcceptanceIndex(first.index, first.e), first.collected.files); + return { ...result, root: r.output, assembly: r.assembly }; +} +function attestedRoot(root, assembly) { + c.safeDirectory(absolute(root)); + agree(fs.readdirSync(root).sort(), [ATTESTATION_LINK, 'evidence'], 'fixed R3 transport closure'); + const bytes = c.readFile(path.join(root, ATTESTATION_LINK), LIMIT), value = orderedLocator(bounded(bytes, LIMIT)); + agree(bytes, c.encode(value), 'canonical original assembly locator'); agree(value, assembly, 'R3 original R2 locator'); + const evidence = path.join(root, 'evidence'); c.safeDirectory(evidence); return evidence; +} +function readAcceptance(r) { + const ctx = readerContext(r, 'read'); locator(r.acceptance); locator(r.assembly); + const first = admittedAssembly(ctx, r.assembly), signed = completedArtifact(ctx, r.acceptance, 'attest'); + acceptanceGraph(first.e, { produce: first.collected.attempt, assemble: first.retained.attempt, attest: signed.attempt }, r.selected, r.assembly, r.acceptance); + const signedRoot = attestedRoot(signed.root, r.assembly); + agree(pin(path.join(signedRoot, ACCEPTANCE_FILE), r.acceptance.sha256), first.bytes, 'R3 unchanged original E'); + agree(readAcceptanceClosure(signedRoot, first.e), first.index, 'R3 unchanged exhaustive index'); + const subjects = [ACCEPTANCE_FILE, INDEX_FILE].map(name => ({ name, digest: { sha256: name === ACCEPTANCE_FILE ? r.acceptance.sha256 : first.e.evidence.sha256 } })); + for (const subject of subjects) require('./authoring-promotion').verifyPublicSubject(path.join(signedRoot, subject.name), { + name: subject.name, sha256: subject.digest.sha256, source: r.selected.source, workflow_sha: r.workflow_sha, ref: r.selected.ref, + run_id: r.acceptance.artifact.run_id, run_attempt: r.acceptance.artifact.run_attempt, subjects + }, r.work_parent); + ctx.recheck(); readAcceptanceClosure(attestedRoot(signed.root, r.assembly), first.e); + agree(pin(path.join(signedRoot, ACCEPTANCE_FILE), r.acceptance.sha256), first.bytes, 'post-signature unchanged E'); + const last = admittedAssembly(ctx, r.assembly); compareFiles(last.collected.files, first.collected.files); agree(last.bytes, first.bytes, 'late original R2'); + const lateSigner = completedArtifact(ctx, r.acceptance, 'attest'); agree(lateSigner.attempt, signed.attempt, 'late completed R3'); + const lateRoot = attestedRoot(lateSigner.root, r.assembly); + readAcceptanceClosure(lateRoot, first.e); agree(pin(path.join(lateRoot, ACCEPTANCE_FILE), r.acceptance.sha256), first.bytes, 'late signer artifact bytes'); + ctx.recheck(); return { record: first.e, input: first.intake.input, stage: first.intake.stage, + subjects, assembly: r.assembly, acceptance: r.acceptance }; +} +const DISPATCH_INPUTS = freeze({ + produce: [], + assemble: ['journeys', 'bridge'], + attest: ['assembly'], + check: ['assembly', 'acceptance'] +}); +const DISPATCH_ENV = freeze({ selected: 'PUBLIC_SELECTED', input: 'PUBLIC_INPUT', stage: 'PUBLIC_STAGE', + journeys: 'PUBLIC_JOURNEYS', bridge: 'PUBLIC_BRIDGE', assembly: 'PUBLIC_ASSEMBLY', acceptance: 'PUBLIC_ACCEPTANCE' }); +function dispatchInputs(mode) { + const dispatchMode = mode === 'inputs' ? 'produce' : mode === 'read' ? 'check' : mode; + assert.ok(Object.hasOwn(DISPATCH_INPUTS, dispatchMode), 'fixed workflow operation'); + const required = new Set(['selected', 'input', 'stage', ...DISPATCH_INPUTS[dispatchMode]]), values = {}; + for (const [name, env] of Object.entries(DISPATCH_ENV)) { + const body = process.env[env] || ''; + if (required.has(name)) assert.ok(body.length > 0, `required ${dispatchMode} dispatch input: ${name}`); + else assert.equal(body, '', `foreign ${dispatchMode} dispatch input: ${name}`); + if (body) values[name] = bounded(Buffer.from(body), LIMIT); + } + // Reuse P's complete selected-source contract while dispatch is still pure: + // canonical bytes and every selection semantic precede provisioning/facades. + values.selected = require('./authoring-promotion').workflowSelection(values.selected, process.env.GITHUB_WORKFLOW_SHA); + locator(values.input); locator(values.stage); + if (dispatchMode === 'assemble') { + list(values.journeys, matrix.length, 'eighteen dispatch journey locators'); + values.journeys.forEach((row, i) => { fields(row, ['cell', 'sha256', 'artifact'], 'dispatch journey locator'); fixed(row.cell, matrix[i].key, 'ordered dispatch cell'); locator({ sha256: row.sha256, artifact: row.artifact }); }); + locator(values.bridge); + } + if (values.assembly) locator(values.assembly); + if (values.acceptance) locator(values.acceptance); + return values; +} +function workflowRequest(mode, key) { + assert.ok(['inputs', 'produce', 'assemble', 'attest', 'read'].includes(mode), 'fixed workflow operation'); + const dispatch = dispatchInputs(mode); + if (mode === 'produce') cell(key); + const provisioning = require('./public-authoring-tools'), manifest = provisioning.readProvisioning(); + const target = mode === 'produce' ? cell(key).target : 'linux-amd64'; + agree(provisioning.requireController(target), process.execPath, 'workflow independent controller'); + const api = requireFacades(mode === 'produce' ? key : BRIDGE_CELL)['public-authoring-custody']; + assert.equal(typeof api.readPublicArtifact, 'function', 'PUBLIC_FACADE_REQUIRED:public-authoring-custody.js#readPublicArtifact'); + const { selected, stage, input } = dispatch; + const repo = path.resolve(__dirname, '../../..'), parent = absolute(process.env.RUNNER_TEMP); + const source = sourceSeal(repo, selected.source, { ...manifest, key: mode === 'produce' ? key : BRIDGE_CELL }); + const promotion = require('./authoring-promotion'), graphMode = mode === 'inputs' ? 'produce' : mode === 'read' ? 'check' : mode; + const producer = promotion.inspectPublicCaller(selected, process.env.GITHUB_WORKFLOW_SHA, graphMode, parent); + if (mode === 'produce') agree(process.env.PUBLIC_CELL, key, 'fixed workflow cell'); + c.safeDirectory(parent); const invocation = fs.mkdtempSync(path.join(parent, 'public-packed-')); + const work = path.join(invocation, 'work'), custody = path.join(invocation, 'custody'); + fs.mkdirSync(work, { mode: 0o700 }); fs.mkdirSync(custody, { mode: 0o700 }); + // The fixed custody facade accepts the original input locator for workflow + // intake, returning the same checked I19/S3 roots as byte-based admission. + const admitted = api.readPublicInputs({ input, selected, workflow_sha: selected.source, stage, repo, work_parent: custody, tools: null }); + const inputFile = path.join(admitted.input.root, contract.INPUT_FILE); pin(inputFile, input.sha256); + const r = { schema: `authoring-public-${mode}/v1`, selected, workflow_sha: selected.source, input_file: inputFile, + stage, repo, work_parent: work }; + agree(sourceSeal(repo, selected.source, { ...manifest, key: mode === 'produce' ? key : BRIDGE_CELL }), source, 'workflow full source unchanged'); + const output = path.join(invocation, 'output'); + if (mode === 'inputs') { + const checked = admitProducerInputs({ ...r, producer, tools: null }, api); + const include = matrix.map(({ key }) => { + const row = manifest.cells[key]; + for (const name of ['runner', 'image', 'observer', 'npm_node', 'shim_node', 'npm', + ...(key.endsWith('kit-node18') ? [] : ['installer_policy']), ...(key === BRIDGE_CELL ? ['go', 'mod_cache'] : [])]) + assert.ok(row[name] !== null, `PUBLIC_PROVISIONING_REQUIRED:${key}:${name}`); + for (const name of ['node', 'python', 'git', 'gh', 'tar']) + assert.ok(manifest.controllers[row.controller][name] !== null, `PUBLIC_PROVISIONING_REQUIRED:${row.controller}:${name}`); + return { cell: key, runner: row.runner.id }; + }); + return { selected, input: { sha256: c.digest(checked.inputBytes), artifact: input.artifact }, stage, matrix: { include } }; + } + if (mode === 'produce') { + const row = provisioning.requireCellTools(key), tools = { orchestrator_node: manifest.controllers[target].node, + ...Object.fromEntries(['npm_node', 'shim_node', 'npm', 'go'].map(k => [k, row[k] === null ? null : + Object.fromEntries(['path', 'sha256', 'version'].map(n => [n, row[k][n]]))])), + host: { platform: process.platform, arch: process.arch } }; + return { ...r, schema: 'authoring-public-produce/v1', output, cell: key, tools, producer }; + } + if (mode === 'assemble') return { ...r, output, producer, journeys: dispatch.journeys, bridge: dispatch.bridge }; + if (mode === 'attest') return { ...r, output, assembly: dispatch.assembly }; + return { ...r, acceptance: dispatch.acceptance, assembly: dispatch.assembly }; +} +async function workflowOperation(mode, key) { + const r = workflowRequest(mode, key); + if (mode === 'inputs') return r; + if (mode === 'assemble') return assembleAcceptance(r); + if (mode === 'attest') return attestAcceptanceInputs(r); + if (mode === 'read') return readAcceptance(r); + const produced = await produceJourney(r), manifest = require('./public-authoring-tools').readProvisioning(); + let bridgeRoot; + if (key === BRIDGE_CELL) { + bridgeRoot = r.output + '-bridge'; const file = path.join(r.work_parent, 'bridge-options.json'); + fs.writeFileSync(file, c.encode({ request: produced.request, go: r.tools.go.path, node: r.tools.orchestrator_node.path, + modCache: manifest.cells[key].mod_cache.root }), { flag: 'wx', mode: 0o600 }); + const result = require('node:child_process').spawnSync(manifest.controllers['linux-amd64'].python.path, + ['-B', path.join(r.repo, 'scripts/run-packed-ci.py'), '--public-authenticated', bridgeRoot, r.workflow_sha, file], + { cwd: r.repo, env: { PATH: '/usr/local/bin:/usr/bin:/bin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' }, timeout: 2400000, maxBuffer: TRANSCRIPT_LIMIT }); + assert.ok(!result.error && result.status === 0 && result.signal === null, 'same-live-root bridge completed'); + } + readJourney(produced.request); + const retained = r.output + '-retained'; fs.mkdirSync(retained, { mode: 0o700 }); + const original = path.dirname(produced.request.journey), names = ['public-journey.json', ...EVIDENCE, + ...fs.readdirSync(path.join(original, 'sidecars')).map(n => 'sidecars/' + n)]; + for (const name of [...names, ...(bridgeRoot ? BRIDGE_FILES.map(n => 'bridge/' + n) : [])]) { + const bytes = closureBytes(name.startsWith('bridge/') ? path.join(bridgeRoot, name.slice(7)) : path.join(original, name)); + const file = path.join(retained, name); fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 }); + } + readJourney(produced.request); + return { root: retained, cell: key, sha256: produced.request.journeySha256, + bridge_sha256: bridgeRoot ? c.digest(c.readFile(path.join(bridgeRoot, 'summary.json'), LIMIT)) : null }; +} +function main(args) { + if ((args.length === 2 || args.length === 3) && args[0] === '--workflow') return workflowOperation(args[1], args[2]); + if (args.length === 2 && args[0] === "--produce-journey") return produceJourney(fileJSON(args[1])); + if (args.length === 2 && ['--assemble', '--attest-inputs', '--read'].includes(args[0])) + return ({ '--assemble': assembleAcceptance, '--attest-inputs': attestAcceptanceInputs, '--read': readAcceptance })[args[0]](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, + encodeAcceptance, decodeAcceptance, ACCEPTANCE_SCHEMA, ACCEPTANCE_FILE, INDEX_FILE, ATTESTATION_LINK, + PUBLIC_JOBS, completedAttempt, acceptanceGraph, historicalTools, retainedJourney, readCompletedJourney, + INDEX_SCHEMA, encodeAcceptanceIndex, decodeAcceptanceIndex, readAcceptanceClosure, + BRIDGE_FILES, assembleAcceptance, attestAcceptanceInputs, readAcceptance, dispatchInputs, workflowRequest, 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 new file mode 100644 index 00000000..9777fdc5 --- /dev/null +++ b/npm/agentplugins/test/authoring-native-qualification.test.js @@ -0,0 +1,1306 @@ +"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(); + 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 }])) }; + 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; +} +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) 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 = { 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)); + 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); +} +// 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(); + Object.assign(value, { receipt_reconciled: false, native_discovery_reconciled: false, native_identity_state: "indeterminate" }); + 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 = {}; }, + "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"; }, + "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. + 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; +${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) { + 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'); + 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 { + 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); + 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: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'}]:[]; + 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');} +// 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)},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; +} +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(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',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:{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}}}; +} + +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:'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={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!=='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; + 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.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'}; + 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.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)); + } + } + 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") { + 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, 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) => { + 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); + // 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); + 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); + } + 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 + // 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 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 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');"; + 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); + 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(JSON.parse(row.stdout), null, 2) + "\n\t"); + assert.deepEqual(row.before, row.after); + 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; + assert.throws(() => checkInfo(changed.data, reg, Object.values(reg.clients)[0]), /public info installation/); + } +}); +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/); + 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", + "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", "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 = { "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/, + "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"))); + }); +} +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(()=>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(fixtureReader(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(()=>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);} + }); + } +}); + +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(()=>fixtureReader(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); + } +}); + +// 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 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"; }, + "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)); + } + 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(fixtureReader(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)); +}); + +// 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); + }); +}); + + +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..b5ba40cc 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,543 @@ 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('C3 public adapter rejects non-E2 subject sets before verifier effects', t => { + let effects = 0; + t.mock.method(cp, 'spawnSync', () => { effects++; throw new Error('unexpected verifier effect'); }); + for (const subjects of [[], [{ name: 'public-packed-completion.json' }], + [{ name: 'public-packed-completion.json' }, { name: 'public-packed-completion.json' }], + [{ name: 'public-packed-completion.json' }, { name: 'package.tgz' }]]) + assert.throws(() => p.verifyPublicSubject('/not-opened/public-packed-completion.json', + { name: 'public-packed-completion.json', subjects }, '/not-opened'), /E2/); + assert.equal(effects, 0); +}); + +test('C3 public adapter binds completed reader to Q and retains native gate', t => { + const f = fixture(), a = require('../scripts/public-authoring-acceptance'); + const lane = f.record.qualification.lanes.at(-1); + lane.schema = a.ACCEPTANCE_SCHEMA; lane.workflow = a.WORKFLOW; + const request = { schema: 'authoring-public-read/v1', selected, workflow_sha: ID.commit, + acceptance: { sha256: lane.sha256, artifact: lane.artifact }, stage: { sha256: hash('S'), artifact: pin } }; + const context = { request, preparation: pin }; + const result = { record: Object.fromEntries(['identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256'] + .map(k => [k, structuredClone(f.record[k])])), input: { preparation: { artifact: pin }, products: structuredClone(f.record.products) }, + stage: { native_inputs: { sha256: hash('I'), artifact: pin }, packs: { agentplugins: { sha256: hash('pack1') }, 'plugin-kit-ai': { sha256: hash('pack2') } } } }; + Object.assign(result.record, { native_inputs: result.stage.native_inputs, packs: result.stage.packs, stage: request.stage }); + let reads = 0, effects = 0, returned = result; + t.mock.method(cp, 'spawnSync', () => { effects++; throw Error('protected effect'); }); + t.mock.method(a, 'readAcceptance', value => { reads++; assert.equal(value, request); return returned; }); + assert.equal(p.admitPublicEvidence(f.record, context), result); + for (const mutate of [ + r => { r.record.identity.commit = 'b'.repeat(40); }, + r => { r.input.preparation.artifact.run_attempt++; }, + r => { r.input.products['plugin-kit-ai'].assets['windows-arm64'].binary.sha256 = hash('other inner'); }, + r => { r.input.products.agentplugins.assets['linux-arm64'].sha256 = hash('other outer'); }, + r => { r.record.packs['plugin-kit-ai'].sha256 = hash('other pack'); }, + r => { r.record.native_inputs.sha256 = hash('other I'); } + ]) { + // JSON cloning intentionally separates repeated object references so a + // changed admitted E pin cannot also change the independent S expectation. + returned = JSON.parse(JSON.stringify(result)); mutate(returned); + assert.throws(() => p.admitPublicEvidence(f.record, context)); + } + const count = reads; + assert.throws(() => p.admitPublicEvidence(f.record, { ...context, request: { ...request, + acceptance: { ...request.acceptance, artifact: { ...pin, run_attempt: 99 } } } }), /exact completed R3 locator/); + assert.equal(reads, count); + assert.throws(() => p.requireNativeContracts(f.record.qualification.lanes), /NATIVE_EVIDENCE_INTEGRATION_REQUIRED/); + assert.equal(effects, 0); +}); + +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/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js new file mode 100644 index 00000000..c2050906 --- /dev/null +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -0,0 +1,95 @@ +"use strict"; +const test=require("node:test"),assert=require("node:assert/strict"),fs=require("node:fs"),os=require("node:os"),path=require("node:path"); +const harness=require("../scripts/milestone-a-e2e"),HEAD="9db754c93c713219c72206eab54d71ee39e88abf"; +const D="sha256:"+"a".repeat(64),R="sha256:"+"b".repeat(64),P="sha256:"+"c".repeat(64); +const repo=path.resolve(__dirname,"../../.."),workflow=fs.readFileSync(path.join(repo,".github/workflows/authoring-milestone-a-e2e.yml"),"utf8"); +test("prepare assembles the exact current stager identity schema",()=>{const identity=harness.candidateIdentity(HEAD);assert.deepEqual(identity,{repository:"777genius/universal-agent-plugins",commit:HEAD,engine_revision:HEAD,versions:{agentplugins:"0.1.91","plugin-kit-ai":"2.0.0"}});assert.notEqual(identity.versions.agentplugins,identity.versions["plugin-kit-ai"]);}); +test("negative metadata mutation is rejected by the real stager validator",()=>{const identity=harness.candidateIdentity(HEAD);delete identity.engine_revision;const c=require("../scripts/dual-authoring-candidate");assert.throws(()=>c.identity(identity),/identity: unexpected or missing fields/);}); +test("trusted tool config resolves setup-node-style npm and Go symlinks to invocable regular files",()=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-tools-"));try{const npmCli=path.join(root,"lib/node_modules/npm/bin/npm-cli.js"),goTarget=path.join(root,"toolchains/go/bin/go");fs.mkdirSync(path.dirname(npmCli),{recursive:true});fs.mkdirSync(path.dirname(goTarget),{recursive:true});fs.writeFileSync(npmCli,'if(process.argv[2]!=="--probe")process.exit(19);process.stdout.write("npm-cli-ok")');fs.writeFileSync(goTarget,"go-binary-placeholder");const npmLink=path.join(root,"bin/npm"),goLink=path.join(root,"bin/go");fs.mkdirSync(path.dirname(npmLink));fs.symlinkSync(path.relative(path.dirname(npmLink),npmCli),npmLink);fs.symlinkSync(path.relative(path.dirname(goLink),goTarget),goLink);const tools=harness.resolveTrustedTools(goLink,npmLink,process.execPath);assert.equal(tools.go,fs.realpathSync(goTarget));assert.equal(tools.npm,fs.realpathSync(npmCli));assert.equal(tools.node,fs.realpathSync(process.execPath));for(const value of Object.values(tools)){const stat=fs.lstatSync(value);assert.equal(stat.isFile(),true);assert.equal(stat.nlink,1)}assert.equal(require("node:child_process").execFileSync(tools.node,[tools.npm,"--probe"],{encoding:"utf8"}),"npm-cli-ok");}finally{fs.rmSync(root,{recursive:true,force:true})}}); +test("trusted tool config rejects a genuinely aliased canonical tool with its label",()=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-aliased-tool-"));try{const tool=path.join(root,"go"),alias=path.join(root,"go-alias");fs.writeFileSync(tool,"not-empty");fs.linkSync(tool,alias);assert.throws(()=>harness.canonicalTool("go",tool),/^Error: go canonical tool must be regular, nonempty, bounded and unaliased$/);}finally{fs.rmSync(root,{recursive:true,force:true})}}); +test("precreated failure root stays uploadable while candidate starts absent",()=>{const outer=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-prepare-failure-")),root=path.join(outer,"artifact"),evidence=path.join(root,"evidence");try{fs.mkdirSync(evidence,{recursive:true,mode:0o700});assert.throws(()=>harness.prepare({root,precreatedRoot:true,repo,expectedHead:"0".repeat(40)}),/expected exact candidate/);assert.equal(fs.statSync(root).mode&0o777,0o700);assert.equal(fs.statSync(evidence).mode&0o777,0o700);assert.equal(fs.existsSync(path.join(root,"candidate")),false);const receipt=JSON.parse(fs.readFileSync(path.join(evidence,"prepare-failure.json")));assert.equal(receipt.candidate_present,false);assert.equal(receipt.publication,false);}finally{fs.rmSync(outer,{recursive:true,force:true});}}); +function fixture(options={}){const outer=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-exec-")),input=path.join(outer,"input"),logs=path.join(outer,"declared-logs");fs.mkdirSync(input);fs.mkdirSync(logs);fs.mkdirSync(path.join(input,"candidate"));const entrypoints={}; + for(const product of ["agentplugins","plugin-kit-ai"]){const pkg=path.join(outer,`package-${product}`),bin=path.join(pkg,"bin");fs.mkdirSync(bin,{recursive:true});fs.writeFileSync(path.join(pkg,"package.json"),JSON.stringify({name:product,version:"2.0.0",...(options.stagedProvenance?{}:{gitHead:options.packageRevision||HEAD})}));if(options.stagedProvenance){fs.writeFileSync(path.join(bin,"package.json"),JSON.stringify({type:"commonjs"}));fs.writeFileSync(path.join(pkg,"private-release.json"),JSON.stringify({schema:"dual-authoring-npm/v1",identity:{commit:options.packageRevision||HEAD}}));}const ep=path.join(bin,`${product}.js`);fs.writeFileSync(ep,`#!/usr/bin/env node +const fs=require('node:fs'),p=require('node:path'); +const product=${JSON.stringify(product)},a=process.argv.slice(2),author=a[0]==='author',v=author?a[1]:a[0],project=v==='add'?process.cwd():(author?a[2]:a[1]); +const required=['HOME','USERPROFILE','TMPDIR','TMP','TEMP','XDG_CONFIG_HOME','XDG_CACHE_HOME','XDG_DATA_HOME','XDG_STATE_HOME','APPDATA','LOCALAPPDATA','npm_config_cache','npm_config_prefix','CODEX_HOME']; +if(!required.every(k=>process.env[k]&&process.env[k].startsWith(p.dirname(process.env.HOME))))process.exit(17); +if(${JSON.stringify(options.requireCandidateCache||false)}&&(!fs.statSync(process.env.UAP_PRIVATE_NPM_CACHE).isDirectory()||process.env.UAP_PRIVATE_NPM_CACHE!==p.join(p.dirname(process.env.HOME),'cache','candidate')))process.exit(18); +if(${JSON.stringify(options.requireDestinationAbsent||false)}&&v==='init'&&fs.existsSync(project))process.exit(19); +fs.appendFileSync(${JSON.stringify(path.join(logs,product+".jsonl"))},JSON.stringify({argv:a,env:Object.fromEntries(required.map(k=>[k,process.env[k]]))})+'\\n'); +if(${JSON.stringify(options.failAdd||false)}&&v==='add'){process.stdout.write('complete stdout diagnostic');process.stderr.write('complete stderr diagnostic');process.exit(23);} +if(${JSON.stringify(options.failValidate||false)}&&v==='validate'){process.stdout.write('validate stdout diagnostic');process.stderr.write('validate stderr diagnostic');process.exit(29);} +if(v==='init'){fs.mkdirSync(project);fs.writeFileSync(p.join(project,'fixture.txt'),'same');} +let data={revision:${JSON.stringify(options.resultRevision===null?null:(options.resultRevision||HEAD))},operation:v};if(data.revision===null)delete data.revision; +if(v==='add'){const tree=${JSON.stringify(D)},manifest=${JSON.stringify(R)};const plan={client_id:${JSON.stringify(options.wrongTarget?"claude":"codex")},scope:'user',status:'manual_activation_required',package_mode:'managed_projection',activation:'manual_activation_required',authentication:'not_checked',policy:'allowed',verification:'package_valid',physical_artifact_id:'fixture',components:[{kind:'skill',name:'milestone-a-fixture',support:'projected'}]};const output={revision:${JSON.stringify(options.resultRevision===null?null:(options.resultRevision||HEAD))},plugin:'milestone-a-fixture',source:project,tree_digest:tree,manifest_digest:manifest,dry_run:true,result:{installation_id:'',plan,requires_confirmation:true,mutated:false}};if(output.revision===null)delete output.revision;data={batch:true,status:'planned',succeeded:1,failed:0,revision:${JSON.stringify(options.resultRevision===null?null:(options.resultRevision||HEAD))},plugin:'milestone-a-fixture',source:project,tree_digest:tree,manifest_digest:manifest,dry_run:true,security:{schema_version:1,subject:{tree_digest:tree,manifest_digest:manifest},scanner:{id:'lintai',version:'0.1.3'},policy:{id:'agent-plugin-install',version:2,digest:${JSON.stringify(P)}},outcome:'no_blocking_findings',counts:{blocking:0,warnings:0,total:0},scanned_files:4,report_digest:${JSON.stringify(D)},evidence_source:'local_scan'},targets:[{target:${JSON.stringify(options.wrongRequestedTarget?"claude":"codex")},status:'manual_activation_required',output}]};if(${JSON.stringify(options.noSecurity||false)})delete data.security;if(${JSON.stringify(options.failedSecurity||false)}){data.security.outcome='blocking_findings';data.security.counts={blocking:1,warnings:0,total:1}}if(${JSON.stringify(options.destination||"")})plan.destination=${JSON.stringify(options.destination||"")}.replace('',process.env.CODEX_HOME).replace('',p.dirname(process.env.HOME));if(data.revision===null)delete data.revision;} +if(${JSON.stringify(options.outside||false)}&&v==='validate')data.output_root=p.join(p.dirname(p.dirname(process.env.HOME)),'sibling'); +if(${JSON.stringify(options.mismatch||false)}&&product==='plugin-kit-ai'&&v==='validate')data.changed=true; +process.stdout.write(JSON.stringify({schema_version:1,...(v==='add'?{command:'add'}:{}),result:'success',data})+'\\n');`);fs.chmodSync(ep,0o755);entrypoints[product]=ep} + return{outer,input,logs,entrypoints,root:path.join(outer,"controlled-run"),config:{root:path.join(outer,"controlled-run"),outerRoot:outer,input,platform:"linux",arch:"amd64",expectedHead:HEAD,entrypoints,snapshotExcludes:[input,logs,...Object.values(entrypoints).map(x=>path.dirname(path.dirname(x)))]}}} +function dispose(f){fs.rmSync(f.outer,{recursive:true,force:true})} +test("consume executes ordered entrypoints in independent complete roots, compares JSON, proves revision/target, and cleans",()=>{const f=fixture();try{harness.consume(f.config);const receipt=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","run.json")));assert.equal(receipt.exact_candidate,true);assert.equal(receipt.cleanup,"complete");assert.equal(receipt.clean_root_separation,true);assert.equal(receipt.fixture_target_id,"codex");assert.ok(path.isAbsolute(receipt.fixture_roots.agentplugins));assert.equal(receipt.fixture_roots["plugin-kit-ai"],undefined);assert.equal(receipt.security_assessments["plugin-kit-ai"],undefined);assert.deepEqual(receipt.commands,harness.COMMANDS);assert.equal(receipt.package_acquisition.package_registry_or_latest_acquisition,false);assert.equal(receipt.package_acquisition.npm_tarball_install_offline,true);assert.ok(receipt.package_acquisition.exact_local_tarballs.every(x=>path.isAbsolute(x)&&x.endsWith(".tgz")));assert.equal(receipt.security_boundary.production_security_checks_enabled,true);assert.equal(receipt.security_boundary.credential_free_read_only_public_network_allowed,true);assert.equal(receipt.security_boundary.test_bypass,false);assert.equal(receipt.security_assessments.agentplugins.evidence_source,"local_scan");assert.equal(receipt.security_assessments.agentplugins.scanner.id,"lintai"); + for(const p of Object.keys(f.entrypoints)){const rows=fs.readFileSync(path.join(f.logs,p+".jsonl"),"utf8").trim().split("\n").map(JSON.parse);assert.deepEqual(rows.map(x=>x.argv[0]==="author"?x.argv[1]:x.argv[0]),p==="agentplugins"?["init","validate","inspect","test","add"]:["init","validate","inspect","test"]);assert.ok(Object.values(rows[0].env).every(x=>x.startsWith(path.join(f.root,"journeys",p))));assert.notEqual(rows[0].env.HOME,rows[0].env.TMPDIR);assert.notEqual(rows[0].env.npm_config_cache,rows[0].env.npm_config_prefix);}assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);}finally{dispose(f)}}); +test("consume finds exact staged private-release provenance above the bin package scope",()=>{const f=fixture({stagedProvenance:true,resultRevision:null});try{harness.consume(f.config);const receipt=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","run.json")));for(const value of Object.values(receipt.provenances)){assert.equal(value.revision,HEAD);assert.equal(value.revision_field,"private-release.json#identity.commit");assert.equal(path.basename(value.package_manifest),"package.json");assert.equal(path.basename(path.dirname(value.package_manifest)).startsWith("package-"),true);}}finally{dispose(f)}}); +test("portable precreation keeps an uploadable evidence path and Unix private modes",()=>{const outer=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-portable-root-")),root=path.join(outer,"run");try{fs.rmSync(outer,{recursive:true,force:true});assert.equal(harness.precreate(root,"win32"),path.join(root,"evidence"));assert.equal(fs.existsSync(path.join(root,"evidence")),true);fs.rmSync(outer,{recursive:true,force:true});harness.precreate(root,"linux");assert.equal(fs.statSync(root).mode&0o777,0o700);assert.equal(fs.statSync(path.join(root,"evidence")).mode&0o777,0o700);}finally{fs.rmSync(outer,{recursive:true,force:true})}}); +test("init destination stays absent while the agentplugins launcher cache exists at journey/cache/candidate",()=>{const f=fixture({requireCandidateCache:true,requireDestinationAbsent:true});try{harness.consume(f.config);assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);}finally{dispose(f)}}); +test("both public product journeys require and receive their own isolated private npm cache root before the launcher runs",()=>{const f=fixture({requireCandidateCache:true});try{harness.consume(f.config);const rows=Object.fromEntries(Object.keys(f.entrypoints).map(p=>[p,fs.readFileSync(path.join(f.logs,p+".jsonl"),"utf8").trim().split("\n").map(JSON.parse)]));assert.ok(rows.agentplugins.length>0);assert.ok(rows["plugin-kit-ai"].length>0);}finally{dispose(f)}}); +test("local add dry-run reads the real single-target group/batch envelope, not a bare result, and is never sent to plugin-kit-ai",()=>{const f=fixture();try{const receipt=harness.consume(f.config);assert.equal(receipt.fixture_target_id,"codex");const agentpluginsRows=fs.readFileSync(path.join(f.logs,"agentplugins.jsonl"),"utf8").trim().split("\n").map(JSON.parse),pluginKitRows=fs.readFileSync(path.join(f.logs,"plugin-kit-ai.jsonl"),"utf8").trim().split("\n").map(JSON.parse);assert.ok(agentpluginsRows.map(x=>x.argv).some(a=>(a[0]==="author"?a[1]:a[0])==="add"));assert.ok(!pluginKitRows.map(x=>x.argv).some(a=>(a[0]==="author"?a[1]:a[0])==="add"));const security=receipt.security_assessments.agentplugins;assert.equal(security.scanner.id,"lintai");assert.equal(security.subject.tree_digest,D);assert.equal(security.subject.manifest_digest,R);assert.equal(receipt.security_assessments["plugin-kit-ai"],undefined);}finally{dispose(f)}}); +test("darwin platform reads validate through a bounded hdiutil-mounted immutable local view and always detaches it",()=>{const stubDir=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-hdiutil-")),log=path.join(stubDir,"hdiutil.log"),bin=path.join(stubDir,"hdiutil");fs.writeFileSync(bin,`#!/usr/bin/env node +const fs=require('node:fs'),path=require('node:path'),a=process.argv.slice(2); +fs.appendFileSync(${JSON.stringify(log)},a.join(' ')+'\\n'); +function copy(src,dst){fs.mkdirSync(dst,{recursive:true});for(const e of fs.readdirSync(src,{withFileTypes:true})){const s=path.join(src,e.name),d=path.join(dst,e.name);if(e.isDirectory())copy(s,d);else fs.copyFileSync(s,d)}} +if(a[0]==='create'){fs.writeFileSync(a[a.length-1],a[a.indexOf('-srcfolder')+1]);process.exit(0)} +if(a[0]==='attach'){copy(fs.readFileSync(a[a.length-1],'utf8'),a[a.indexOf('-mountpoint')+1]);process.exit(0)} +if(a[0]==='detach')process.exit(0); +process.exit(1); +`);fs.chmodSync(bin,0o755);const oldPath=process.env.PATH;process.env.PATH=`${stubDir}${path.delimiter}${oldPath}`;const f=fixture();f.config.platform="darwin";f.config.arch="arm64";try{const receipt=harness.consume(f.config);assert.equal(receipt.platform,"darwin");assert.deepEqual(receipt.commands,["init","validate"]);for(const p of Object.keys(f.entrypoints))assert.ok(receipt.assertions.includes(`${p}:packageview-readonly`));const lines=fs.readFileSync(log,"utf8").trim().split("\n");assert.equal(lines.filter(l=>l.startsWith("create ")).length,2);assert.equal(lines.filter(l=>l.startsWith("attach ")).length,2);assert.equal(lines.filter(l=>l.startsWith("detach ")).length,2);assert.equal(fs.readdirSync(path.join(f.root,"journeys")).length,0);}finally{process.env.PATH=oldPath;dispose(f);fs.rmSync(stubDir,{recursive:true,force:true})}}); +test("darwin packageview mount is detached and journeys are cleaned even when the mounted validate fails",()=>{const stubDir=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-hdiutil-fail-")),log=path.join(stubDir,"hdiutil.log"),bin=path.join(stubDir,"hdiutil");fs.writeFileSync(bin,`#!/usr/bin/env node +const fs=require('node:fs'),path=require('node:path'),a=process.argv.slice(2); +fs.appendFileSync(${JSON.stringify(log)},a.join(' ')+'\\n'); +function copy(src,dst){fs.mkdirSync(dst,{recursive:true});for(const e of fs.readdirSync(src,{withFileTypes:true})){const s=path.join(src,e.name),d=path.join(dst,e.name);if(e.isDirectory())copy(s,d);else fs.copyFileSync(s,d)}} +if(a[0]==='create'){fs.writeFileSync(a[a.length-1],a[a.indexOf('-srcfolder')+1]);process.exit(0)} +if(a[0]==='attach'){copy(fs.readFileSync(a[a.length-1],'utf8'),a[a.indexOf('-mountpoint')+1]);process.exit(0)} +if(a[0]==='detach')process.exit(0); +process.exit(1); +`);fs.chmodSync(bin,0o755);const oldPath=process.env.PATH;process.env.PATH=`${stubDir}${path.delimiter}${oldPath}`;const f=fixture({failValidate:true});f.config.platform="darwin";f.config.arch="arm64";try{assert.throws(()=>harness.consume(f.config),/exited 29/);const lines=fs.readFileSync(log,"utf8").trim().split("\n");assert.equal(lines.filter(l=>l.startsWith("attach ")).length,1);assert.equal(lines.filter(l=>l.startsWith("detach ")).length,1);assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);const failure=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","failure.json")));assert.equal(failure.cleanup,"complete");}finally{process.env.PATH=oldPath;dispose(f);fs.rmSync(stubDir,{recursive:true,force:true})}}); +test("a darwin detach failure combined with a pre-existing primary error is fully visible in the thrown error and in evidence",()=>{const stubDir=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-hdiutil-detach-combined-")),log=path.join(stubDir,"hdiutil.log"),bin=path.join(stubDir,"hdiutil");fs.writeFileSync(bin,`#!/usr/bin/env node +const fs=require('node:fs'),path=require('node:path'),a=process.argv.slice(2); +fs.appendFileSync(${JSON.stringify(log)},a.join(' ')+'\\n'); +function copy(src,dst){fs.mkdirSync(dst,{recursive:true});for(const e of fs.readdirSync(src,{withFileTypes:true})){const s=path.join(src,e.name),d=path.join(dst,e.name);if(e.isDirectory())copy(s,d);else fs.copyFileSync(s,d)}} +if(a[0]==='create'){fs.writeFileSync(a[a.length-1],a[a.indexOf('-srcfolder')+1]);process.exit(0)} +if(a[0]==='attach'){copy(fs.readFileSync(a[a.length-1],'utf8'),a[a.indexOf('-mountpoint')+1]);process.exit(0)} +if(a[0]==='detach')process.exit(1); +process.exit(1); +`);fs.chmodSync(bin,0o755);const oldPath=process.env.PATH;process.env.PATH=`${stubDir}${path.delimiter}${oldPath}`;const f=fixture({failValidate:true});f.config.platform="darwin";f.config.arch="arm64";try{assert.throws(()=>harness.consume(f.config),error=>/exited 29/.test(error.message)&&/darwin packageview detach failed for agentplugins/.test(error.message));assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);const failure=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","failure.json")));assert.match(failure.message,/exited 29/);assert.match(failure.message,/darwin packageview detach failed for agentplugins/);assert.equal(failure.cleanup,"complete");}finally{process.env.PATH=oldPath;dispose(f);fs.rmSync(stubDir,{recursive:true,force:true})}}); +test("a darwin detach failure for one product does not prevent detach attempts, root cleanup, or the journey/outer-mutation checks for the other product",()=>{const stubDir=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-hdiutil-detach-partial-")),log=path.join(stubDir,"hdiutil.log"),bin=path.join(stubDir,"hdiutil");fs.writeFileSync(bin,`#!/usr/bin/env node +const fs=require('node:fs'),path=require('node:path'),a=process.argv.slice(2); +fs.appendFileSync(${JSON.stringify(log)},a.join(' ')+'\\n'); +function copy(src,dst){fs.mkdirSync(dst,{recursive:true});for(const e of fs.readdirSync(src,{withFileTypes:true})){const s=path.join(src,e.name),d=path.join(dst,e.name);if(e.isDirectory())copy(s,d);else fs.copyFileSync(s,d)}} +if(a[0]==='create'){fs.writeFileSync(a[a.length-1],a[a.indexOf('-srcfolder')+1]);process.exit(0)} +if(a[0]==='attach'){copy(fs.readFileSync(a[a.length-1],'utf8'),a[a.indexOf('-mountpoint')+1]);process.exit(0)} +if(a[0]==='detach'){if(a[a.length-1].includes('plugin-kit-ai'))process.exit(1);process.exit(0)} +process.exit(1); +`);fs.chmodSync(bin,0o755);const oldPath=process.env.PATH;process.env.PATH=`${stubDir}${path.delimiter}${oldPath}`;const f=fixture();f.config.platform="darwin";f.config.arch="arm64";try{assert.throws(()=>harness.consume(f.config),/darwin packageview detach failed for plugin-kit-ai/);const lines=fs.readFileSync(log,"utf8").trim().split("\n");assert.equal(lines.filter(l=>l.startsWith("attach ")).length,2);assert.equal(lines.filter(l=>l.startsWith("detach ")).length,2);assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);for(const p of Object.keys(f.entrypoints)){const rows=fs.readFileSync(path.join(f.logs,p+".jsonl"),"utf8").trim().split("\n").map(JSON.parse);assert.deepEqual(rows.map(x=>x.argv[0]==="author"?x.argv[1]:x.argv[0]),["init","validate"]);}const failure=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","failure.json")));assert.match(failure.message,/darwin packageview detach failed for plugin-kit-ai/);assert.equal(failure.cleanup,"complete");assert.equal(failure.clean_root_separation,true);}finally{process.env.PATH=oldPath;dispose(f);fs.rmSync(stubDir,{recursive:true,force:true})}}); +test("failed real commands retain complete stdout and stderr in evidence",()=>{const f=fixture({failAdd:true});try{assert.throws(()=>harness.consume(f.config),error=>error.message.includes("exited 23")&&error.message.includes("stdout:\ncomplete stdout diagnostic")&&error.message.includes("stderr:\ncomplete stderr diagnostic"));const failure=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","failure.json")));assert.match(failure.message,/stdout:\ncomplete stdout diagnostic\nstderr:\ncomplete stderr diagnostic/);}finally{dispose(f)}}); +test("journey environment preserves public HTTPS transport without leaking unrelated host state",()=>{const f=fixture();const old={HTTPS_PROXY:process.env.HTTPS_PROXY,SSL_CERT_FILE:process.env.SSL_CERT_FILE,UNRELATED_MILESTONE_SECRET:process.env.UNRELATED_MILESTONE_SECRET};try{process.env.HTTPS_PROXY="http://proxy.invalid:8443";process.env.SSL_CERT_FILE="/etc/ssl/cert.pem";process.env.UNRELATED_MILESTONE_SECRET="must-not-pass";const j={home:path.join(f.outer,"home"),tmp:path.join(f.outer,"tmp"),config:path.join(f.outer,"config"),cache:path.join(f.outer,"cache"),data:path.join(f.outer,"data"),state:path.join(f.outer,"state"),appdata:path.join(f.outer,"appdata"),localappdata:path.join(f.outer,"localappdata"),client:path.join(f.outer,"client"),npm_cache:path.join(f.outer,"npm-cache"),npm_prefix:path.join(f.outer,"npm-prefix")},env=harness.envFor(j);assert.equal(env.HTTPS_PROXY,process.env.HTTPS_PROXY);assert.equal(env.SSL_CERT_FILE,process.env.SSL_CERT_FILE);assert.equal(env.UNRELATED_MILESTONE_SECRET,undefined);assert.equal(env.npm_config_offline,"true");}finally{for(const [key,value] of Object.entries(old))if(value===undefined)delete process.env[key];else process.env[key]=value;dispose(f)}}); +test("trusted npm CLI is a JavaScript entrypoint invoked by node with explicit arguments",()=>{const npmCLI=harness.resolveNpmCLI();assert.equal(path.extname(npmCLI),".js");const source=fs.readFileSync(path.join(repo,"npm/agentplugins/scripts/milestone-a-e2e.js"),"utf8");assert.match(source,/run\(process\.execPath,\[npmCLI,"install"/);assert.doesNotMatch(source,/spawnSync\([^\n]*(?:npm\.cmd|shell:\s*true)/);}); +test("local add dry-run source argv is an explicit absolute local path, not a bare dot",()=>{const source=fs.readFileSync(path.join(repo,"npm/agentplugins/scripts/milestone-a-e2e.js"),"utf8");assert.match(source,/\[ep,"add",j\.project,"--target=codex","--dry-run","--format=json"\]/);assert.doesNotMatch(source,/\[ep,"add","\."/);}); +test("Windows mixed-separator runner roots become canonical native absolute paths",()=>{assert.equal(harness.nativeAbsolute("D:\\a\\_temp/milestone-a/run","win32"),"D:\\a\\_temp\\milestone-a\\run");assert.equal(path.win32.isAbsolute(harness.nativeAbsolute("D:\\a\\_temp/milestone-a/run","win32")),true);assert.throws(()=>harness.nativeAbsolute("milestone-a/run","win32"),/root must be absolute/);}); +for(const [name,opts,pattern] of [ + ["normalized result mismatch",{mismatch:true},/command JSON differs/], + ["reported candidate revision mismatch",{resultRevision:"1111111111111111111111111111111111111111"},/reported revision/], + ["package provenance revision mismatch",{resultRevision:null,packageRevision:"2222222222222222222222222222222222222222"},/package provenance revision/], + ["reported outside-root path",{outside:true},/escapes journey root/] +])test(`negative executable: ${name} writes failure evidence and cleans`,()=>{const f=fixture(opts);try{assert.throws(()=>harness.consume(f.config),pattern);const failure=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","failure.json")));assert.match(failure.message,pattern);assert.equal(failure.cleanup,"complete");assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);}finally{dispose(f)}}); +for(const [name,opts,pattern] of [["absent security",{noSecurity:true},/omitted authoritative production security evidence/],["failed security",{failedSecurity:true},/security assessment did not pass/],["wrong target ID",{wrongTarget:true},/resolve target exactly to codex/],["wrong requested target",{wrongRequestedTarget:true},/resolve target exactly to codex/],["contained non-Codex destination",{destination:"/state/planned"},/planned destination escapes journey root/],["escaped destination",{destination:path.resolve(os.tmpdir(),"milestone-a-escape")},/planned destination escapes journey root/]])test(`negative add evidence: ${name}`,()=>{const f=fixture(opts);try{assert.throws(()=>harness.consume(f.config),pattern);const failure=JSON.parse(fs.readFileSync(path.join(f.root,"evidence","failure.json")));assert.match(failure.message,pattern);assert.equal(failure.cleanup,"complete");}finally{dispose(f)}}); +test("outside sibling mutation is rejected",()=>{const f=fixture();const marker=path.join(f.outer,"unexpected-sibling");fs.appendFileSync(f.entrypoints.agentplugins,`\nfs.writeFileSync(${JSON.stringify(marker)},'changed');\n`);try{assert.throws(()=>harness.consume(f.config),/unexpected change outside/);}finally{dispose(f)}}); +test("tree and snapshot reject links",()=>{const r=fs.mkdtempSync(path.join(os.tmpdir(),"milestone-a-link-"));try{fs.writeFileSync(path.join(r,"a"),"a");fs.symlinkSync(path.join(r,"a"),path.join(r,"link"));assert.throws(()=>harness.tree(r),/non-regular/);assert.throws(()=>harness.snapshot(r),/link/);}finally{fs.rmSync(r,{recursive:true,force:true});}}); +test("Windows normalization is root-specific, case-aware, and separator-aware",()=>{const a={data:{product:"agentplugins",product_version:"2",help:{use:"agentplugins author validate"},project:"C:\\RUNS\\Alpha\\project",nested:["C:/runs/alpha/project/plugin.json","C:\\runs\\unrelated\\project"]}},b={data:{product:"plugin-kit-ai",product_version:"2",help:{use:"plugin-kit-ai validate"},project:"D:\\work\\Beta\\project",nested:["D:/WORK/beta/project/plugin.json","C:\\runs\\unrelated\\project"]}};const na=harness.normalize(a,"C:\\runs\\alpha\\project","win32"),nb=harness.normalize(b,"D:\\work\\beta\\project","win32");assert.equal(na.data.project,"/project");assert.equal(na.data.nested[0],"/project/plugin.json");assert.deepEqual(na,nb);}); +test("negative Windows normalization preserves unrelated paths, similar roots, and escapes",()=>{const value={data:{outside:"C:\\elsewhere\\file",escape:"C:\\root\\one\\..\\secret",similar:"C:\\root\\one-other\\file"}};assert.deepEqual(harness.normalize(value,"C:\\root\\one\\project","win32"),value);}); +test("workflow uses real local packages and has no test entrypoint injection",()=>{assert.match(workflow,/download-artifact@[0-9a-f]{40}[\s\S]*milestone-a-input/);assert.match(workflow,/milestone-a-e2e\.js run/);assert.match(workflow,/production security checks retain credential-free read-only access/);assert.doesNotMatch(workflow,/JSON\.stringify\(\{[^}]*entrypoints|fixture|fake|test[_-]only|registry_fallback|complete installer journey is offline/i);const source=fs.readFileSync(path.join(repo,"npm/agentplugins/scripts/milestone-a-e2e.js"),"utf8");for(const token of ['[npmCLI,"install","--ignore-scripts","--offline"','path.join(input,"packages",tgz)','path.join(j.installation,"node_modules"'])assert.ok(source.includes(token),`missing real-package contract: ${token}`);}); +test("workflow is unfiltered, secretless, pinned, bounded and failure-preserving",()=>{assert.match(workflow,/pull_request:\s*\n\s*workflow_dispatch:/);assert.doesNotMatch(workflow,/pull_request:[\s\S]{0,200}paths:/);assert.match(workflow,/permissions:\n contents: read/);assert.doesNotMatch(workflow,/secrets\.|permissions:\s*write|publish|npm-token|id-token/);assert.match(workflow,/ubuntu-24\.04[\s\S]*windows-2022[\s\S]*macos-14/);assert.match(workflow,/if: always\(\)[\s\S]*actions\/upload-artifact@[0-9a-f]{40}/);assert.doesNotMatch(workflow,/uses:\s*[^\n]+@(?![0-9a-f]{40}(?:\s|$))/);for(const n of [...workflow.matchAll(/timeout-minutes:\s*(\d+)/g)].map(x=>+x[1]))assert.ok(n<=20);assert.match(workflow,/NODE_OPTIONS: --max-old-space-size=384/);}); +test("workflow creates a private prepare upload root before packing and installs a bounded failure receipt",()=>{assert.match(workflow,/test ! -e "\$root"\s+mkdir -m 700 "\$root"\s+mkdir -m 700 "\$root\/evidence"/);assert.match(workflow,/precreatedRoot:true/);assert.match(workflow,/milestone-a-e2e-workflow-failure\/v1/);assert.match(workflow,/\.slice\(0,4096\)|status:Number/);}); +test("workflow precreates portable run evidence before execution and uploads that stable path",()=>{const run=workflow.slice(workflow.indexOf("Run both packaged public entrypoints"));assert.match(run,/native_root=.*nativeAbsolute[\s\S]*MILESTONE_A_EVIDENCE=%s[\s\S]*\.precreate\(process\.argv\[1\]\)/);assert.match(run,/native_outer=.*nativeAbsolute[\s\S]*native_input=.*nativeAbsolute/);assert.match(run,/precreatedRoot:true/);assert.doesNotMatch(run,/mkdir -m 700|rmdir \"\$root\/evidence\"/);assert.match(run,/if: always\(\)[\s\S]*path: \$\{\{ env\.MILESTONE_A_EVIDENCE \}\}/);}); +test("workflow converts the MSYS run config path to native Win32 before writing and invoking",()=>{const run=workflow.slice(workflow.indexOf("Run both packaged public entrypoints"));assert.match(run,/native_config=.*nativeAbsolute\(process\.argv\[1\]\)[\s\S]*writeFileSync\(process\.argv\[1\][\s\S]*\"\$native_config\"[\s\S]*milestone-a-e2e\.js run \"\$native_config\"/);assert.doesNotMatch(run,/milestone-a-e2e\.js run \"\$config\"/);}); +test("workflow gives Darwin packageview a local read-only APFS candidate and always detaches it",()=>{assert.match(workflow,/runner\.os == 'macOS'[\s\S]*hdiutil create[^\n]*-fs APFS -format UDRO[\s\S]*hdiutil attach[^\n]*-readonly/);assert.match(workflow,/candidateRoot:process\.argv\[5\]/);assert.match(workflow,/if: always\(\) && runner\.os == 'macOS'[\s\S]*hdiutil detach/);}); +test("workflow canonicalizes trusted prepare tools before constructing config",()=>{assert.match(workflow,/resolveTrustedTools\(process\.argv\[3\],process\.argv\[5\]\)/);assert.doesNotMatch(workflow,/npm:process\.argv\[5\]/);}); 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/private-npm-bootstrap.test.js b/npm/agentplugins/test/private-npm-bootstrap.test.js index e773a740..47f070d4 100644 --- a/npm/agentplugins/test/private-npm-bootstrap.test.js +++ b/npm/agentplugins/test/private-npm-bootstrap.test.js @@ -75,6 +75,49 @@ const locks = f => fs.existsSync(path.join(f.cache, ".locks")) ? fs.readdirSync( const run = (f, product = "agentplugins", extra = {}, hooks = {}) => p.ensureBinary(product, f.options(product, extra), hooks); function changeJSON(file, change) { const value = JSON.parse(fs.readFileSync(file)); change(value); write(file, c.encode(value)); } +// Platform-independent: these check the digest/segment structure of cachePath +// itself, not a live filesystem, so they run everywhere (not gated to Linux). +nodeTest("cache path identity digest changes with every distinguishing tuple field", () => { + const base = { descriptor: { authoring_mode: "vertical-slice-v1", candidate_sha256: "a".repeat(64) }, + version: "0.1.23", asset: { binary: { sha256: "b".repeat(64), file: "agentplugins" } } }; + const baseline = p.cachePath("/cache", "agentplugins", "linux-amd64", base); + const variants = [ + () => p.cachePath("/cache", "plugin-kit-ai", "linux-amd64", { ...base, asset: { binary: { ...base.asset.binary, file: "plugin-kit-ai" } } }), + () => p.cachePath("/cache", "agentplugins", "windows-amd64", base), + () => p.cachePath("/cache", "agentplugins", "linux-amd64", { ...base, version: "0.1.24" }), + () => p.cachePath("/cache", "agentplugins", "linux-amd64", { ...base, descriptor: { ...base.descriptor, candidate_sha256: "c".repeat(64) } }), + () => p.cachePath("/cache", "agentplugins", "linux-amd64", { ...base, descriptor: { ...base.descriptor, authoring_mode: "release-cli-contract-v1" } }), + () => p.cachePath("/cache", "agentplugins", "linux-amd64", { ...base, asset: { binary: { ...base.asset.binary, sha256: "d".repeat(64) } } }), + ]; + const seen = new Set([baseline]); + for (const build of variants) { + const value = build(); + assert.equal(seen.has(value), false, `tuple change did not change cache path: ${value}`); + seen.add(value); + } + assert.equal(seen.size, variants.length + 1); +}); + +nodeTest("resolved Windows cache path stays well under legacy MAX_PATH", () => { + // cachePath's fixed suffix is "v2" + sep + 64 hex digest chars + sep + binary + // filename; the digest folds in mode/candidate/product/version/target/binary + // identity so the suffix width no longer grows with those fields' lengths. + const longest = { descriptor: { authoring_mode: "vertical-slice-v1", candidate_sha256: "f".repeat(64) }, + version: "999.999.999", asset: { binary: { sha256: "e".repeat(64), file: "plugin-kit-ai.exe" } } }; + const suffix = p.cachePath("", "plugin-kit-ai", "windows-amd64", longest); + assert.ok(suffix.length <= 90, `cache path suffix grew unexpectedly: ${suffix.length}`); + // Legacy Windows MAX_PATH is 260 characters. A realistic, deeply nested, + // space-containing resolved cache root (a long username under + // AppData\Local) is comfortably under 170 characters, which leaves >90 + // characters of headroom for the fixed suffix above -- unlike the prior + // nested-segment layout, whose fixed suffix alone (~265 chars) already + // exceeded MAX_PATH regardless of root length. + const realisticRoot = "C:\\Users\\Jane Alexandra Doe-Whitfield\\AppData\\Local\\uap\\private-npm-cache"; + assert.ok(realisticRoot.length < 170, `test root assumption no longer realistic: ${realisticRoot.length}`); + const resolved = path.win32.join(realisticRoot, "v2", "f".repeat(64), "plugin-kit-ai.exe"); + assert.ok(resolved.length < 260, `resolved windows path too long: ${resolved.length}`); +}); + for (const product of c.PRODUCTS) test(`STRUCTURAL ${product}: cold, source-free warm, and ordinary corruption repair`, async () => { const f = fixture(); const before = snapshot(f.source); const rootMode = fs.statSync(f.source).mode; let acquisitions = 0; const hooks = { afterFreeze() { acquisitions++; } }; @@ -105,8 +148,8 @@ test("STRUCTURAL explicit roots, complete identity and product isolation; legacy write(path.join(f.cache, ID.versions.agentplugins, "agentplugins"), "wrong engine"); const [a, b] = await Promise.all(c.PRODUCTS.map(product => run(f, product))); assert.notEqual(a.binaryPath, b.binaryPath); - assert.match(a.binaryPath, new RegExp(f.descriptors.agentplugins.candidate_sha256)); - assert.match(a.binaryPath, new RegExp(f.manifest.products.agentplugins.assets[TARGET].binary.sha256)); + assert.equal(a.binaryPath, p.cachePath(f.cache, "agentplugins", TARGET, f.release("agentplugins"))); + assert.equal(path.basename(a.binaryPath), f.manifest.products.agentplugins.assets[TARGET].binary.file); const newer = fixture("linux-amd64-pair", { ...ID, versions: { agentplugins: "0.1.24", "plugin-kit-ai": "2.0.1" } }); const unlockVersion = await v.acquireLock(a.binaryPath, { lockRoot: path.join(f.cache, ".locks") }); try { diff --git a/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js new file mode 100644 index 00000000..0ffcc470 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js @@ -0,0 +1,199 @@ +'use strict'; +// Source contracts only. These tests never dispatch, sign, or qualify E. +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 childProcess = require('node:child_process'); +const Module = require('node:module'); +const a = require('../scripts/public-authoring-acceptance'); +const text = fs.readFileSync(path.resolve(__dirname, '../../../.github/workflows/authoring-public-packed.yml'), 'utf8'); +const jobs = Object.fromEntries([...text.matchAll(/^ (public_[a-z_]+):\n([\s\S]*?)(?=^ public_[a-z_]+:|$(?![\s\S]))/gm)] + .map(m => [m[1], m[2]])); +const encode = value => JSON.stringify(value, null, 2) + '\n'; +const digest = value => crypto.createHash('sha256').update(value).digest('hex'); +const locator = n => ({ sha256: digest(`payload-${n}`), artifact: { + run_id: n + 1, run_attempt: 1, artifact_id: n + 101, artifact_sha256: digest(`artifact-${n}`) +} }); +const selected = () => ({ tag: 'agentplugins-v2.1.0', ref: 'refs/tags/agentplugins-v2.1.0', source: 'a'.repeat(40), + versions: { agentplugins: '2.1.0', 'plugin-kit-ai': '2.0.0' } }); +const dispatch = mode => { + const values = { + selected: selected(), + input: locator(1), stage: locator(2), journeys: a.matrix.map((row, i) => ({ cell: row.key, ...locator(i + 10) })), + bridge: locator(40), assembly: locator(41), acceptance: locator(42) + }; + const required = { inputs: [], produce: [], assemble: ['journeys', 'bridge'], attest: ['assembly'], read: ['assembly', 'acceptance'] }[mode]; + const env = Object.fromEntries(['selected', 'input', 'stage', 'journeys', 'bridge', 'assembly', 'acceptance'].map(name => [`PUBLIC_${name.toUpperCase()}`, ''])); + for (const name of ['selected', 'input', 'stage', ...required]) env[`PUBLIC_${name.toUpperCase()}`] = encode(values[name]); + return env; +}; + +test('C3 runtime rejects every open, missing, and malformed dispatch before effects', () => { + const owned = ['selected', 'input', 'stage', 'journeys', 'bridge', 'assembly', 'acceptance']; + const required = { inputs: ['selected', 'input', 'stage'], produce: ['selected', 'input', 'stage'], + assemble: ['selected', 'input', 'stage', 'journeys', 'bridge'], attest: ['selected', 'input', 'stage', 'assembly'], + read: ['selected', 'input', 'stage', 'assembly', 'acceptance'] }; + const savedEnv = { ...process.env }, load = Module._load; + const originalFs = Object.fromEntries(['mkdirSync', 'mkdtempSync', 'writeFileSync', 'rmSync'].map(name => [name, fs[name]])); + const originalChild = Object.fromEntries(['spawn', 'spawnSync', 'execFileSync'].map(name => [name, childProcess[name]])); + const effects = { controller: 0, facade: 0, filesystem: 0, subprocess: 0 }; + Module._load = function(request, parent, isMain) { + if (request.includes('public-authoring-tools')) effects.controller++; + if (request.includes('public-authoring-custody')) effects.facade++; + return load.call(this, request, parent, isMain); + }; + for (const name of Object.keys(originalFs)) fs[name] = (...args) => { effects.filesystem++; return originalFs[name](...args); }; + for (const name of Object.keys(originalChild)) childProcess[name] = (...args) => { effects.subprocess++; return originalChild[name](...args); }; + try { + for (const [mode, names] of Object.entries(required)) { + const cases = []; + process.env = { ...savedEnv, GITHUB_WORKFLOW_SHA: 'a'.repeat(40), ...dispatch(mode) }; + assert.doesNotThrow(() => a.dispatchInputs(mode), `${mode}: valid closed dispatch`); + for (const foreign of owned.filter(name => !names.includes(name))) cases.push([`foreign ${foreign}`, env => { env[`PUBLIC_${foreign.toUpperCase()}`] = encode(locator(90)); }]); + for (const missing of names) cases.push([`missing ${missing}`, env => { delete env[`PUBLIC_${missing.toUpperCase()}`]; }]); + for (const malformed of names) cases.push([`malformed ${malformed}`, env => { env[`PUBLIC_${malformed.toUpperCase()}`] = encode({}); }]); + const selectedCases = [ + ['versions null', s => { s.versions = null; }], + ['missing version', s => { delete s.versions.agentplugins; }], + ['extra version', s => { s.versions.extra = '1.0.0'; }], + ['non-string version', s => { s.versions.agentplugins = 210; }], + ['oversized version', s => { s.versions.agentplugins = '1'.repeat(33); }], + ['wrong kit version', s => { s.versions['plugin-kit-ai'] = '2.0.1'; }], + ['wrong tag', s => { s.tag = 'agentplugins-v9.9.9'; }], + ['wrong ref', s => { s.ref = 'refs/tags/agentplugins-v9.9.9'; }], + ['wrong workflow source', s => { s.source = 'b'.repeat(40); }] + ]; + for (const [label, mutate] of selectedCases) cases.push([label, env => { const s = selected(); mutate(s); env.PUBLIC_SELECTED = encode(s); }]); + cases.push(['duplicate selected key', env => { env.PUBLIC_SELECTED = env.PUBLIC_SELECTED.replace(' "tag":', ' "tag": "agentplugins-v2.1.0",\n "tag":'); }]); + cases.push(['non-canonical selected JSON', env => { env.PUBLIC_SELECTED = JSON.stringify(selected()); }]); + for (const [label, mutate] of cases) { + process.env = { ...savedEnv, GITHUB_WORKFLOW_SHA: 'a'.repeat(40), ...dispatch(mode) }; mutate(process.env); + assert.throws(() => a.workflowRequest(mode, mode === 'produce' ? a.matrix[0].key : undefined), `${mode}: ${label}`); + assert.deepEqual(effects, { controller: 0, facade: 0, filesystem: 0, subprocess: 0 }, `${mode}: ${label}`); + } + } + } finally { + process.env = savedEnv; Module._load = load; + for (const [name, fn] of Object.entries(originalFs)) fs[name] = fn; + for (const [name, fn] of Object.entries(originalChild)) childProcess[name] = fn; + } +}); + +test('C3 workflow four invocations retain all cells and exact completed jobs', () => { + assert.deepEqual(Object.keys(jobs), ['public_inputs', 'public_cell', 'public_producer_complete', 'public_assemble', + 'public_evidence_intake', 'public_evidence_attestation', 'public_check']); + assert.match(text, /options: \[produce, assemble, attest, check\]/); + assert.match(jobs.public_cell, /fail-fast: false/); + assert.match(jobs.public_cell, /needs: public_inputs/); + assert.match(jobs.public_cell, /name: public_cell \(\$\{\{ matrix.cell \}\}\)/); + assert.match(jobs.public_cell, /matrix: \$\{\{ fromJSON\(needs.public_inputs.outputs.matrix\) \}\}/); + assert.match(jobs.public_producer_complete, /needs: \[public_inputs, public_cell\]/); + assert.match(jobs.public_producer_complete, /== os.environ\['CELLS_RESULT'\] == 'success'/); + assert.equal(a.PUBLIC_JOBS.produce.length, 20); + assert.equal(new Set(a.matrix.map(r => r.key)).size, 18); + for (const [mode, names] of Object.entries(a.PUBLIC_JOBS)) { + const producer = { workflow: a.WORKFLOW, source: 'a'.repeat(40), ref: 'refs/tags/agentplugins-v2.0.0', run_id: 50, run_attempt: 2 }; + const selected = { tag: 'agentplugins-v2.0.0', ref: producer.ref, source: producer.source, versions: {} }; + const attempt = { producer, status: 'completed', conclusion: 'success', jobs: names.map((name, i) => ({ + id: i + 1, name, run_id: 50, run_attempt: 2, source: producer.source, ref: producer.ref, status: 'completed', conclusion: 'success' + })) }; + assert.deepEqual(a.completedAttempt(attempt, selected, mode), producer); + for (let i = 0; i < names.length; i++) for (const field of ['run_attempt', 'source', 'status', 'conclusion']) { + const bad = structuredClone(attempt); + bad.jobs[i][field] = { run_attempt: 1, source: 'b'.repeat(40), status: 'in_progress', conclusion: 'skipped' }[field]; + assert.throws(() => a.completedAttempt(bad, selected, mode), `${mode}/${names[i]}/${field}`); + } + } +}); + +test('C3 workflow isolates E2 signing and rechecks closure after signing', () => { + for (const [name, body] of Object.entries(jobs)) { + if (name === 'public_evidence_attestation') continue; + assert.doesNotMatch(body, /id-token: write|attestations: write|actions\/attest@/); + } + const signer = jobs.public_evidence_attestation; + assert.match(signer, /needs: public_evidence_intake/); + assert.match(signer, /runs-on: ubuntu-24.04/); + assert.match(signer, /id-token: write\n attestations: write/); + assert.match(signer, /subject-path: \$\{\{ steps.evidence.outputs.subjects \}\}/); + assert.ok(signer.indexOf("packed.public_workflow('attest')") < signer.indexOf('uses: actions/attest@')); + assert.ok(signer.indexOf('uses: actions/attest@') < signer.indexOf("checked = packed.public_workflow('attest')")); + assert.ok(signer.indexOf('assert closure(') < signer.indexOf('uses: actions/upload-artifact@')); + assert.match(signer, /assert closure\(os.environ\['PUBLIC_SIGNED_ROOT'\]\) == closure\(checked\['root'\]\)/); + assert.doesNotMatch(text, /contents: write|packages: write|gh release|npm publish|workflow_call:|push:/); +}); + +test('C3 workflow pins source actions and independent controller bootstrap', () => { + const uses = [...text.matchAll(/uses: ([^\s]+)/g)].map(m => m[1]); + assert.ok(uses.length > 0); + for (const use of uses) assert.match(use, /^actions\/(checkout|upload-artifact|attest)@[0-9a-f]{40}$/); + for (const [name, body] of Object.entries(jobs)) { + if (name === 'public_producer_complete') continue; + assert.match(body, /persist-credentials: false/); + assert.match(body, /s\['source'\] == os.environ\['GITHUB_SHA'\] == os.environ\['GITHUB_WORKFLOW_SHA'\]/); + assert.match(body, /shell: python -I -B \{0\}/); + assert.match(body, /packed.public_workflow\(/); + assert.doesNotMatch(body, /setup-node|node-version|node npm\//); + } + assert.doesNotMatch(jobs.public_assemble, /needs:/); + assert.doesNotMatch(jobs.public_evidence_intake, /needs:/); + assert.doesNotMatch(jobs.public_check, /needs:/); +}); + +test('C3 trusted bootstrap closes and validates dispatch inputs before checkout', () => { + const admitted = Object.entries(jobs).filter(([name]) => name !== 'public_producer_complete'); + assert.equal(admitted.length, 6); + for (const [name, body] of admitted) { + const checkout = body.indexOf('uses: actions/checkout@'); + assert.ok(checkout > 0, name); + const bootstrap = body.slice(0, checkout); + assert.match(bootstrap, /bool\(body\) == \(name in required\)/, name); + assert.match(bootstrap, /object_pairs_hook=unique/, name); + assert.match(bootstrap, /raw\[name\] == json\.dumps\(parsed, ensure_ascii=False, indent=2, separators=/, name); + assert.match(bootstrap, /def locator\(v\):/, name); + assert.match(bootstrap, /locator\(value\('input'\)\); locator\(value\('stage'\)\)/, name); + assert.match(bootstrap, /if mode == 'assemble':/, name); + assert.match(bootstrap, /if mode in \('attest', 'check'\): locator\(value\('assembly'\)\)/, name); + assert.match(bootstrap, /if mode == 'check': locator\(value\('acceptance'\)\)/, name); + assert.match(bootstrap, /set\(s\['versions'\]\) == \{'agentplugins', 'plugin-kit-ai'\}/, name); + assert.match(bootstrap, /s\['tag'\] == 'agentplugins-v' \+ s\['versions'\]\['agentplugins'\]/, name); + } +}); + +test('C3 trusted pre-checkout bootstraps reject the selected-source semantic bypass', () => { + const modes = { public_inputs: 'produce', public_cell: 'produce', public_assemble: 'assemble', + public_evidence_intake: 'attest', public_evidence_attestation: 'attest', public_check: 'check' }; + const cases = [ + ['versions null', s => { s.versions = null; }], + ['missing version', s => { delete s.versions.agentplugins; }], + ['extra version', s => { s.versions.extra = '1.0.0'; }], + ['non-string version', s => { s.versions.agentplugins = 210; }], + ['oversized version', s => { s.versions.agentplugins = '1'.repeat(33); }], + ['wrong kit value', s => { s.versions['plugin-kit-ai'] = '2.0.1'; }], + ['wrong tag', s => { s.tag = 'agentplugins-v9.9.9'; }], + ['wrong ref', s => { s.ref = 'refs/tags/agentplugins-v9.9.9'; }] + ]; + for (const [job, mode] of Object.entries(modes)) { + const bootstrap = jobs[job].slice(0, jobs[job].indexOf('uses: actions/checkout@')); + const script = bootstrap.match(/run: \|\n([\s\S]*?)(?=^ - )/m)[1].replace(/^ {10}/gm, ''); + const base = { ...process.env, ...dispatch(mode === 'check' ? 'read' : mode), PUBLIC_MODE: mode, + GITHUB_REPOSITORY: '777genius/universal-agent-plugins', GITHUB_EVENT_NAME: 'workflow_dispatch', + GITHUB_SHA: 'a'.repeat(40), GITHUB_WORKFLOW_SHA: 'a'.repeat(40), + GITHUB_REF: 'refs/tags/agentplugins-v2.1.0' }; + assert.equal(childProcess.spawnSync('python3', ['-I', '-B', '-'], { input: script, env: base }).status, 0, `${job}: valid`); + for (const [label, mutate] of cases) { + const s = selected(); mutate(s); + const result = childProcess.spawnSync('python3', ['-I', '-B', '-'], { input: script, env: { ...base, PUBLIC_SELECTED: encode(s) } }); + assert.notEqual(result.status, 0, `${job}: ${label}`); + } + for (const [label, raw] of [ + ['duplicate key', encode(selected()).replace(' "tag":', ' "tag": "agentplugins-v2.1.0",\n "tag":')], + ['non-canonical JSON', JSON.stringify(selected())] + ]) { + const result = childProcess.spawnSync('python3', ['-I', '-B', '-'], { input: script, env: { ...base, PUBLIC_SELECTED: raw } }); + assert.notEqual(result.status, 0, `${job}: ${label}`); + } + } +}); 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..015c86b2 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-acceptance.test.js @@ -0,0 +1,967 @@ +"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, key = 'linux-amd64/pair-node22') { + 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 selectedCell = a.matrix.find(row => row.key === key), windows = key.startsWith('windows-'); + const namespace = windows ? path.win32.join('Z:\\SYNTHETIC-retained', path.basename(root)) : root; + const recorded = file => windows ? path.win32.join(namespace, ...path.relative(root, file).split(path.sep)) : file; + for (const name of ['npm_node', 'shim_node']) tools[name].version = `v${selectedCell.node}.21.1`; + tools.host = { platform: windows ? 'win32' : selectedCell.target.split('-')[0], arch: selectedCell.target.endsWith('amd64') ? 'x64' : 'arm64' }; + if (key !== 'linux-amd64/pair-node22') tools.go = null; + if (windows) for (const [name, tool] of Object.entries(tools)) if (tool && name !== 'host') + tool.path = path.win32.join('Z:\\SYNTHETIC-tools', selectedCell.target, name + '.exe'); + 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: key, tools, command_contract_sha256: H("pending"), + subjects: Object.fromEntries(c.PRODUCTS.map(p => [p, input.products[p].assets])), + projects: Object.fromEntries(selectedCell.products.map(p => [p, recorded(projects[p])])), 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(selectedCell.products.map(p => { + const snapshot = bridge.snapshot(projects[p]); snapshot.root = recorded(snapshot.root); + if (windows) for (const entry of snapshot.entries) entry.mode = entry.kind === 'directory' ? 0o777 : 0o666; + snapshot.sha256 = c.digest(c.encode(snapshot.entries)); return [p, snapshot]; + })), + "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, namespace, 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.namespace, j.cell); + const hostPath = j.cell.startsWith('windows-') ? path.win32 : path.posix; + 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] : hostPath.join(hostPath.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: hostPath.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: hostPath.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; } +} + +// All eighteen semantic records are synthetic; only existing public owner APIs +// are substituted. The actual aggregate reader/encoder/filesystem code runs. +function aggregateFixture(t, run, phase = 'assemble') { + const artifacts = new Map(), modes = new Map(), journeys = [], manifest = { controllers: {}, cells: {} }; + let first, designated; + for (const { key, target } of a.matrix) { + const f = semanticFixture(fixture(t, key)), j = f.j; + if (!first) first = { j, root: f.root, admission: f.admission, inputBytes: f.inputBytes, stageBytes: f.stageBytes, + inputResult: f.inputResult, stageResult: f.stageResult }; + assert.deepEqual(f.inputBytes, first.inputBytes); assert.deepEqual(f.stageBytes, first.stageBytes); + const artifact = { run_id: 4, run_attempt: 2, artifact_id: 1000 + journeys.length, artifact_sha256: H('SYNTHETIC archive ' + key) }; + journeys.push({ cell: key, sha256: f.request.journeySha256, artifact }); + artifacts.set(artifact.artifact_id, { root: f.admission.journey_root, artifact }); + modes.set(artifact.artifact_id, 'produce'); + manifest.controllers[target] = { node: j.tools.orchestrator_node, git: { path: '/SYNTHETIC/git' }, python: { path: '/SYNTHETIC/python' } }; + manifest.cells[key] = { ...j.tools, runner: {}, image: {}, observer: {}, installer_policy: {} }; + if (key === 'linux-amd64/pair-node22') designated = { j, root: f.admission.journey_root, located: journeys.at(-1) }; + } + const attempt = (mode, run_id) => { + const producer = { ...first.j.producer, run_id }; + return { producer, status: 'completed', conclusion: 'success', jobs: a.PUBLIC_JOBS[mode].map((name, i) => ({ + id: run_id * 100 + i, name, run_id, run_attempt: producer.run_attempt, source: producer.source, + ref: producer.ref, status: 'completed', conclusion: 'success' })) }; + }; + for (const value of artifacts.values()) value.attempt = attempt('produce', 4); + for (const name of a.BRIDGE_FILES) { + const file = path.join(designated.root, 'bridge', name); fs.mkdirSync(path.dirname(file), { recursive: true }); + write(file, Buffer.from('SYNTHETIC bridge transport ' + name)); + } + const bridgeLocator = { sha256: hash(path.join(designated.root, 'bridge/summary.json')), artifact: designated.located.artifact }; + const inputSubjects = first.inputResult.subjects.slice(0, 7); + for (const product of c.PRODUCTS) for (const target of c.TARGETS) { + const file = path.join(first.inputResult.root, first.j.subjects[product][target].file); + write(file, Buffer.from((product === 'agentplugins' ? '' : 'outer') + product + target)); + inputSubjects.push({ file, sha256: hash(file) }); + } + first.inputResult.subjects = inputSubjects; + const request = { schema: 'authoring-public-assemble/v1', selected: first.admission.selected, workflow_sha: first.j.identity.commit, + input_file: first.admission.input_file, stage: first.j.stage, repo, work_parent: first.admission.work_parent, + output: path.join(first.root, 'SYNTHETIC-R2'), producer: attempt('assemble', 5).producer, journeys, bridge: bridgeLocator }; + const provision = require('../scripts/public-authoring-tools'), promotion = require('../scripts/authoring-promotion'), cp = require('node:child_process'); + t.mock.method(provision, 'requireController', () => process.execPath); + t.mock.method(provision, 'readProvisioning', () => manifest); + t.mock.method(cp, 'execFileSync', (file, args) => { + assert.equal(file, '/SYNTHETIC/git'); + if (args[0] === 'rev-parse') return Buffer.from(request.workflow_sha); + 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'); + }); + t.mock.method(cp, 'spawnSync', (file, args) => { + assert.equal(file, '/SYNTHETIC/python'); assert.deepEqual(args.slice(0, 3), ['-B', path.join(repo, 'scripts/check-packed-ci.py'), '--completed-bridge']); + assert.equal(args[3], path.join(designated.root, 'bridge')); assert.equal(args[4], request.workflow_sha); + return { status: 0, signal: null, stderr: Buffer.alloc(0), stdout: c.encode({ identity: designated.j.identity, + public_inputs: { journey_sha256: designated.located.sha256, producer: designated.j.producer } }) }; + }); + t.mock.method(promotion, 'inspectPublicCaller', (selected, sha, mode) => { + assert.deepEqual(selected, request.selected); assert.equal(sha, request.workflow_sha); assert.equal(mode, phase); + return attempt(mode, mode === 'attest' ? 6 : 5).producer; + }); + t.mock.method(promotion, 'inspectPublicAttempt', (artifact, selected, mode) => { + assert.equal(mode, modes.get(artifact.artifact_id)); assert.deepEqual(selected, request.selected); + assert.deepEqual(artifact, artifacts.get(artifact.artifact_id).artifact); return artifacts.get(artifact.artifact_id).attempt; + }); + const seedBundle = () => { + const root = path.join(first.root, 'SYNTHETIC-original-R2'), files = []; + for (const loc of journeys) { + const retained = artifacts.get(loc.artifact.artifact_id).root; + for (const name of ['public-journey.json', 'commands.json', 'projects.json', 'npm-lifecycle.json', 'cache-process.json', 'installer.json', + ...fs.readdirSync(path.join(retained, 'sidecars')).map(name => 'sidecars/' + name)]) + files.push({ name: `journeys/${loc.cell}/${name}`, original: path.join(retained, name) }); + } + files.push(...a.BRIDGE_FILES.map(name => ({ name: 'bridge/' + name, original: path.join(designated.root, 'bridge', name) }))); + const e = { schema: a.ACCEPTANCE_SCHEMA, lane: 'public-packed-pair', + ...Object.fromEntries(['identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256', 'native_inputs', 'stage', 'packs'].map(k => [k, first.j[k]])), + producer: request.producer, matrix: { schema: a.MATRIX_SCHEMA, cells: a.matrix.map(row => row.key) }, journeys, + bridge: bridgeLocator, evidence: { path: a.INDEX_FILE, size: 1, sha256: H('pending index') }, assertions: first.j.assertions }; + const index = { schema: a.INDEX_SCHEMA, matrix: e.matrix, journeys, bridge: bridgeLocator, + files: files.map(({ name, original }) => ({ path: name, size: fs.statSync(original).size, sha256: hash(original) })) + .sort((a, b) => a.path < b.path ? -1 : 1) }; + const indexBytes = a.encodeAcceptanceIndex(index, e); + e.evidence = { path: a.INDEX_FILE, size: indexBytes.length, sha256: c.digest(indexBytes) }; + const bytes = a.encodeAcceptance(e, first.inputBytes, first.stageBytes); + fs.mkdirSync(root); + for (const { name, original } of files) { + const file = path.join(root, name); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.copyFileSync(original, file); + } + write(path.join(root, a.INDEX_FILE), indexBytes); write(path.join(root, a.ACCEPTANCE_FILE), bytes); + const locators = {}; + for (const [mode, run_id, artifact_id] of [['assemble', 5, 2000], ['attest', 6, 2001]]) { + const artifact = { run_id, run_attempt: 2, artifact_id, artifact_sha256: H('SYNTHETIC bundle transport ' + mode) }; + const retained = mode === 'assemble' ? root : path.join(first.root, 'SYNTHETIC-original-R3'); + if (mode === 'attest') { + fs.mkdirSync(retained); write(path.join(retained, a.ATTESTATION_LINK), locators.assemble); + fs.cpSync(root, path.join(retained, 'evidence'), { recursive: true }); + } + artifacts.set(artifact_id, { root: retained, artifact, attempt: attempt(mode, run_id) }); modes.set(artifact_id, mode); + locators[mode] = { sha256: c.digest(bytes), artifact }; + } + return { e, index, root, bytes, assembly: locators.assemble, acceptance: locators.attest }; + }; + return withFacades(t, api => { + api['public-authoring-custody'].readPublicInputs = () => ({ input: first.inputResult, stage: first.stageResult }); + api['public-authoring-custody'].readPublicArtifact = ({ kind, locator }) => { + assert.equal(kind, { produce: 'public-journeys', assemble: 'public-assembly', attest: 'public-evidence' }[modes.get(locator.artifact.artifact_id)]); + return artifacts.get(locator.artifact.artifact_id); + }; + return run({ request, first, artifacts, manifest, api, seedBundle }); + }); +} +function substitutedAssemblyLink(t, late) { + return aggregateFixture(t, ({ request, seedBundle, artifacts }) => { + const original = seedBundle(), promotion = require('../scripts/authoring-promotion'); let signatures = 0; + const replace = () => { + const value = structuredClone(original.assembly); value.artifact.run_attempt++; + write(path.join(artifacts.get(original.acceptance.artifact.artifact_id).root, a.ATTESTATION_LINK), value); + }; + if (!late) replace(); + t.mock.method(promotion, 'verifyPublicSubject', () => { if (++signatures === 1 && late) replace(); }); + const r = { schema: 'authoring-public-read/v1', ...Object.fromEntries(['selected', 'workflow_sha', 'input_file', 'stage', 'repo', 'work_parent'].map(k => [k, request[k]])), + assembly: original.assembly, acceptance: original.acceptance }; + assert.throws(() => a.readAcceptance(r), /R3 original R2 locator/); + assert.equal(signatures, late ? 2 : 0); + }, 'read'); +} + +module.exports = { fixture, withReaders }; +if (require.main === module) { + test('C3 unit R3 original locator substitution rejects before signatures', t => substitutedAssemblyLink(t, false)); + test('C3 unit R3 late original locator change cannot return acceptance', t => substitutedAssemblyLink(t, true)); + test('C3 unit attestation inputs re-admit original aggregate without signing', t => aggregateFixture(t, ({ request, first, seedBundle }) => { + const original = seedBundle(); + const r = { schema: 'authoring-public-attest/v1', ...Object.fromEntries(['selected', 'workflow_sha', 'input_file', 'stage', 'repo', 'work_parent'].map(k => [k, request[k]])), + assembly: original.assembly, output: path.join(first.root, 'SYNTHETIC-R3-intake') }; + const result = a.attestAcceptanceInputs(r); + assert.deepEqual(result.assembly, original.assembly); assert.equal(result.sha256, original.assembly.sha256); + assert.deepEqual(fs.readFileSync(path.join(result.root, 'evidence', a.ACCEPTANCE_FILE)), original.bytes); + assert.deepEqual(a.readAcceptanceClosure(path.join(result.root, 'evidence'), original.e), original.index); + assert.deepEqual(JSON.parse(fs.readFileSync(path.join(result.root, a.ATTESTATION_LINK))), original.assembly); + assert.deepEqual(fs.readdirSync(result.root).sort(), [a.ATTESTATION_LINK, 'evidence']); + assert.deepEqual(result.subjects.map(row => path.basename(row.file)), [a.ACCEPTANCE_FILE, a.INDEX_FILE]); + }, 'attest')); + test('C3 unit completed aggregate reader binds original R2 and both R3 subjects', t => aggregateFixture(t, ({ request, seedBundle }) => { + const original = seedBundle(), promotion = require('../scripts/authoring-promotion'); let signatures = 0; + t.mock.method(promotion, 'verifyPublicSubject', (file, expected) => { + signatures++; assert.equal(hash(file), expected.sha256); + assert.equal(expected.run_id, 6); assert.equal(expected.run_attempt, 2); + assert.equal(expected.source, request.workflow_sha); assert.equal(expected.workflow_sha, request.workflow_sha); + assert.equal(expected.ref, request.selected.ref); + assert.deepEqual(expected.subjects, [{ name: a.ACCEPTANCE_FILE, digest: { sha256: original.assembly.sha256 } }, + { name: a.INDEX_FILE, digest: { sha256: original.e.evidence.sha256 } }]); + assert.equal(expected.name, path.basename(file)); + }); + const r = { schema: 'authoring-public-read/v1', ...Object.fromEntries(['selected', 'workflow_sha', 'input_file', 'stage', 'repo', 'work_parent'].map(k => [k, request[k]])), + assembly: original.assembly, acceptance: original.acceptance }; + const result = a.readAcceptance(r); + assert.equal(signatures, 2); assert.deepEqual(result.record, original.e); + assert.deepEqual(result.assembly, original.assembly); assert.deepEqual(result.acceptance, original.acceptance); + }, 'read')); + test('C3 unit assembly replays eighteen cells through fixed owner interfaces', t => aggregateFixture(t, ({ request, first, api }) => { + const assembled = a.assembleAcceptance(request); + assert.equal(assembled.root, request.output); + const bytes = fs.readFileSync(path.join(assembled.root, a.ACCEPTANCE_FILE)); + assert.equal(c.digest(bytes), assembled.sha256); + const e = a.decodeAcceptance(bytes, first.inputBytes, first.stageBytes); + assert.deepEqual(e.journeys, request.journeys); assert.deepEqual(e.bridge, request.bridge); + assert.deepEqual(e.producer, request.producer); assert.equal(a.readAcceptanceClosure(assembled.root, e).journeys.length, 18); + assert.deepEqual(assembled.subjects.map(row => path.basename(row.file)), [a.ACCEPTANCE_FILE, a.INDEX_FILE]); + const custody = api['public-authoring-custody'], originalArtifact = custody.readPublicArtifact; + custody.readPublicArtifact = value => { + const retained = originalArtifact(value); + return { ...retained, artifact: { ...retained.artifact, run_attempt: 99 } }; + }; + const denied = { ...request, output: path.join(first.root, 'SYNTHETIC-rejected-R2') }; + assert.throws(() => a.assembleAcceptance(denied), /exact public artifact custody/); + assert.equal(fs.existsSync(denied.output), false); + const badStage = structuredClone(first.stageResult.record); badStage.packs['plugin-kit-ai'].size++; + custody.readPublicInputs = () => ({ input: first.inputResult, stage: { ...first.stageResult, record: badStage } }); + let artifactEffects = 0; custody.readPublicArtifact = () => { artifactEffects++; throw Error('unexpected artifact acquisition'); }; + assert.throws(() => a.assembleAcceptance(denied), /authenticated S/); + assert.equal(artifactEffects, 0); assert.equal(fs.existsSync(denied.output), false); + })); + for (const { key } of a.matrix) test(`C3 unit completed cell ${key}`, t => { + const f = semanticFixture(fixture(t, key)), j = f.j; + const manifest = { cells: { [key]: { ...j.tools, runner: {}, image: {}, observer: {}, installer_policy: {} } }, + controllers: { [key.split('/')[0]]: { node: j.tools.orchestrator_node } } }; + const attempt = { producer: j.producer, status: 'completed', conclusion: 'success', + jobs: a.PUBLIC_JOBS.produce.map((name, i) => ({ id: i + 1, name, run_id: j.producer.run_id, + run_attempt: j.producer.run_attempt, source: j.producer.source, ref: j.producer.ref, status: 'completed', conclusion: 'success' })) }; + const located = { sha256: f.request.journeySha256, artifact: { run_id: j.producer.run_id, + run_attempt: j.producer.run_attempt, artifact_id: 500, artifact_sha256: H('SYNTHETIC transport') } }; + let remoteOpens = 0; + for (const name of ['lstatSync', 'statSync', 'realpathSync', 'readFileSync', 'openSync', 'readdirSync']) { + const original = fs[name]; + t.mock.method(fs, name, function(file, ...args) { + if (typeof file === 'string' && (file.startsWith('Z:\\') || Object.values(j.projects).some(root => file === root || file.startsWith(root + path.sep)))) { + remoteOpens++; throw Error('original producer project/tool namespace opened'); + } + return original.call(this, file, ...args); + }); + } + withFacades(t, () => assert.deepEqual(a.retainedJourney(f.admission.journey_root, located, f.inputBytes, + f.stageBytes, manifest, attempt, f.admission.selected).record, j)); + assert.equal(remoteOpens, 0); + }); + test('C3 unit completed J replays retained bytes without opening original roots', t => { + const f = semanticFixture(fixture(t)), j = f.j; + const retained = path.join(f.root, 'retained-copy'); + fs.cpSync(f.admission.journey_root, retained, { recursive: true }); + const row = { ...j.tools, runner: {}, image: {}, observer: {}, installer_policy: {} }; + const manifest = { cells: { [j.cell]: row }, controllers: { 'linux-amd64': { node: j.tools.orchestrator_node } } }; + const located = { sha256: f.request.journeySha256, artifact: { run_id: j.producer.run_id, + run_attempt: j.producer.run_attempt, artifact_id: 500, artifact_sha256: H('SYNTHETIC retained transport') } }; + const attempt = { producer: j.producer, status: 'completed', conclusion: 'success', + jobs: a.PUBLIC_JOBS.produce.map((name, i) => ({ id: i + 1, name, run_id: j.producer.run_id, + run_attempt: j.producer.run_attempt, source: j.producer.source, ref: j.producer.ref, status: 'completed', conclusion: 'success' })) }; + const check = () => a.retainedJourney(retained, located, f.inputBytes, f.stageBytes, manifest, attempt, f.admission.selected); + let forbidden = 0; + for (const name of ['lstatSync', 'statSync', 'realpathSync', 'readFileSync', 'openSync', 'readdirSync']) { + const original = fs[name]; + t.mock.method(fs, name, function(file, ...args) { + if (typeof file === 'string' && file.startsWith(f.root + path.sep) && file !== retained && !file.startsWith(retained + path.sep)) { + forbidden++; throw Error('original remote namespace opened'); + } + return original.call(this, file, ...args); + }); + } + withFacades(t, api => { + assert.deepEqual(check().record, j); + const original = attempt.jobs[0].run_attempt; + attempt.jobs[0].run_attempt++; + assert.throws(check, /public job run_attempt/); attempt.jobs[0].run_attempt = original; + const verify = api['public-process-observation'].verifyPublicObservation; + api['public-process-observation'].verifyPublicObservation = () => true; + assert.throws(check, /never boolean success/); + api['public-process-observation'].verifyPublicObservation = verify; + fs.appendFileSync(path.join(retained, 'sidecars/synthetic-observation.json'), 'changed'); + assert.throws(check, /pin|size/); + }); + assert.equal(forbidden, 0); + }); + test('C3 unit E syntax binds all cells and distinct assembly attempt', t => { + const f = fixture(t), j = f.j; + const journeys = a.matrix.map((row, i) => ({ cell: row.key, sha256: H(`J-${i}`), + artifact: { run_id: 4, run_attempt: 2, artifact_id: 200 + i, artifact_sha256: H(`archive-${i}`) } })); + const e = { schema: a.ACCEPTANCE_SCHEMA, lane: 'public-packed-pair', + ...Object.fromEntries(['identity', 'authoring_mode', 'asset_scope', 'candidate_sha256', 'pair_marker_sha256', 'native_inputs', 'stage', 'packs'].map(k => [k, j[k]])), + producer: { ...j.producer, run_id: 5 }, matrix: { schema: a.MATRIX_SCHEMA, cells: a.matrix.map(row => row.key) }, journeys, + bridge: { sha256: H('bridge'), artifact: journeys[1].artifact }, + evidence: { path: a.INDEX_FILE, size: 100, sha256: H('index') }, assertions: j.assertions }; + const encoded = a.encodeAcceptance(e, f.inputBytes, f.stageBytes); + assert.deepEqual(a.decodeAcceptance(encoded, f.inputBytes, f.stageBytes), e); + const reversed = Object.fromEntries(Object.entries(e).reverse()); + assert.deepEqual(a.encodeAcceptance(reversed, f.inputBytes, f.stageBytes), encoded); + assert.throws(() => a.decodeAcceptance(c.encode(reversed), f.inputBytes, f.stageBytes), /fixed E field order/); + const cases = [ + ['missing cell', x => x.journeys.pop()], + ['duplicate cell', x => { x.journeys[2] = x.journeys[1]; }], + ['wrong R1 attempt', x => { x.journeys[17].artifact.run_attempt++; }], + ['wrong R1 run', x => { x.journeys[17].artifact.run_id++; }], + ['R2 self-completion cycle', x => { x.producer.run_id = 4; }], + ['R2 input cycle', x => { x.producer.run_id = 2; }], + ['wrong workflow', x => { x.producer.workflow = '.github/workflows/agentplugins-release.yml'; }], + ['wrong F', x => { x.producer.source = 'b'.repeat(40); }], + ['wrong ref', x => { x.producer.ref = 'refs/heads/master'; }], + ['wrong stage attempt', x => { x.stage.artifact.run_attempt++; }], + ['wrong bridge cell', x => { x.bridge.artifact = x.journeys[2].artifact; }], + ['bridge substituted J', x => { x.bridge.sha256 = x.journeys[1].sha256; }], + ['duplicate J digest', x => { x.journeys[2].sha256 = x.journeys[1].sha256; }], + ['duplicate archive digest', x => { x.journeys[2].artifact.artifact_sha256 = x.journeys[1].artifact.artifact_sha256; }], + ['duplicate artifact ID', x => { x.journeys[2].artifact.artifact_id = x.journeys[1].artifact.artifact_id; }], + ['wrong second pack SRI', x => { x.packs['plugin-kit-ai'].integrity = 'sha512-' + 'A'.repeat(88); }], + ['wrong I digest', x => { x.native_inputs.sha256 = H('other I'); }], + ['wrong matrix', x => x.matrix.cells.reverse()], + ['unknown assertion', x => { x.assertions.fixture_accepted = true; }], + ['false assertion', x => { x.assertions.children_reaped = false; }], + ['self locator', x => { x.artifact = x.bridge.artifact; }], + ['wrong index filename', x => { x.evidence.path = a.ACCEPTANCE_FILE; }], + ['oversized index', x => { x.evidence.size = a.LIMIT + 1; }] + ]; + for (const [name, mutate] of cases) { + const bad = structuredClone(e); mutate(bad); + assert.throws(() => a.encodeAcceptance(bad, f.inputBytes, f.stageBytes), name); t.diagnostic(name); + } + 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.decodeAcceptance(body, f.inputBytes, f.stageBytes)); + // A canonical E fixture remains unable to cross completed custody admission. + assert.throws(() => a.readAcceptance(e), /PUBLIC_PROVISIONING_REQUIRED/); + const selected = f.admission.selected; + const attempt = (mode, run_id) => { + const producer = { ...j.producer, run_id }; + return { producer, status: 'completed', conclusion: 'success', jobs: a.PUBLIC_JOBS[mode].map((name, i) => ({ + id: run_id * 100 + i, name, run_id, run_attempt: producer.run_attempt, source: producer.source, + ref: producer.ref, status: 'completed', conclusion: 'success' })) }; + }; + const attempts = { produce: attempt('produce', 4), assemble: attempt('assemble', 5), attest: attempt('attest', 6) }; + const assembly = { sha256: c.digest(encoded), artifact: { run_id: 5, run_attempt: 2, artifact_id: 300, artifact_sha256: H('R2') } }; + const signed = { sha256: c.digest(encoded), artifact: { run_id: 6, run_attempt: 2, artifact_id: 301, artifact_sha256: H('R3') } }; + assert.deepEqual(a.acceptanceGraph(e, attempts, selected, assembly, signed), + Object.fromEntries(Object.entries(attempts).map(([k, v]) => [k, v.producer]))); + for (const [name, mutate] of [ + ['R1 incomplete', x => { x.produce.status = 'in_progress'; }], + ['R2 wrong attempt', x => { x.assemble.producer.run_attempt++; }], + ['R3 wrong attempt', x => { x.attest.producer.run_attempt++; }], + ['missing bridge cell', x => { x.produce.jobs.splice(2, 1); }], + ['duplicate job', x => { x.produce.jobs[3] = x.produce.jobs[2]; }], + ['skipped cell', x => { x.produce.jobs[3].conclusion = 'skipped'; }], + ['wrong job F', x => { x.attest.jobs[1].source = 'b'.repeat(40); }], + ['R3 self-completion', x => { x.attest.producer.run_id = 5; }] + ]) { + const bad = structuredClone(attempts); mutate(bad); + assert.throws(() => a.acceptanceGraph(e, bad, selected, assembly, signed), name); + } + assert.throws(() => a.acceptanceGraph(e, attempts, selected, assembly, { ...signed, sha256: H('changed E') }), /unchanged R2 E/); + const index = { schema: a.INDEX_SCHEMA, matrix: e.matrix, journeys: e.journeys, bridge: e.bridge, + files: e.journeys.flatMap(j => ['public-journey.json', 'commands.json', 'projects.json', 'npm-lifecycle.json', 'cache-process.json', 'installer.json'].map(name => ({ + path: `journeys/${j.cell}/${name}`, size: 1, sha256: name === 'public-journey.json' ? j.sha256 : H(j.cell + name) }))) }; + index.files.push(...a.BRIDGE_FILES.map(name => ({ path: 'bridge/' + name, size: 1, + sha256: name === 'summary.json' ? e.bridge.sha256 : H('bridge/' + name) }))); + index.files.sort((a, b) => a.path < b.path ? -1 : 1); + const indexBytes = a.encodeAcceptanceIndex(index, e); + assert.deepEqual(a.decodeAcceptanceIndex(indexBytes, e), index); + for (const [name, mutate] of [ + ['missing cell record', x => x.files.pop()], + ['missing bridge', x => x.files.shift()], + ['duplicate member', x => x.files.push(x.files[0])], + ['closure traversal', x => { x.files[0].path = 'bridge/../summary.json'; }], + ['index self cycle', x => { x.files[0].path = a.INDEX_FILE; }], + ['unknown cell', x => { x.files[1].path = 'journeys/linux-other/kit-node18/public-journey.json'; }], + ['wrong J digest', x => { x.files.find(r => r.path.endsWith('/public-journey.json')).sha256 = H('other J'); }], + ['wrong bridge digest', x => { x.files.find(r => r.path === 'bridge/summary.json').sha256 = H('other bridge'); }], + ['aggregate overflow', x => { x.files.forEach(r => { r.size = 16 * a.LIMIT; }); }] + ]) { + const bad = structuredClone(index); mutate(bad); assert.throws(() => a.encodeAcceptanceIndex(bad, e), name); + } + // Exercise the actual retained filesystem reader, including empty directory + // additions. These bytes are only transport fixtures, never completed E. + const closure = path.join(f.root, 'SYNTHETIC-closure'); fs.mkdirSync(closure); + for (const row of index.files) { + const i = e.journeys.findIndex(j => row.path === `journeys/${j.cell}/public-journey.json`); + const bytes = Buffer.from(i >= 0 ? `J-${i}` : row.path === 'bridge/summary.json' ? 'bridge' : row.path); + row.size = bytes.length; row.sha256 = c.digest(bytes); + const file = path.join(closure, row.path); fs.mkdirSync(path.dirname(file), { recursive: true }); write(file, bytes); + } + const retainedIndex = a.encodeAcceptanceIndex(index, e); + e.evidence = { path: a.INDEX_FILE, size: retainedIndex.length, sha256: c.digest(retainedIndex) }; + write(path.join(closure, a.INDEX_FILE), retainedIndex); + write(path.join(closure, a.ACCEPTANCE_FILE), a.encodeAcceptance(e, f.inputBytes, f.stageBytes)); + assert.deepEqual(a.readAcceptanceClosure(closure, e), index); + fs.mkdirSync(path.join(closure, 'extra-empty')); + assert.throws(() => a.readAcceptanceClosure(closure, e), /unindexed retained directory/); + fs.renameSync(path.join(closure, 'extra-empty'), path.join(f.root, 'retained-extra-empty')); + const member = path.join(closure, index.files[0].path), displaced = path.join(f.root, 'retained-missing-member'); + fs.renameSync(member, displaced); + assert.throws(() => a.readAcceptanceClosure(closure, e), /exhaustive retained closure/); + fs.renameSync(displaced, member); + assert.deepEqual(a.readAcceptanceClosure(closure, e), index); + fs.appendFileSync(member, 'changed'); + assert.throws(() => a.readAcceptanceClosure(closure, e), /closure digest/); + }); + test('C3 unit historical tools use remote namespace without local filesystem access', t => { + const f = fixture(t), j = structuredClone(f.j); j.cell = 'windows-arm64/pair-node24'; + j.tools.host = { platform: 'win32', arch: 'arm64' }; j.tools.go = null; + for (const key of ['orchestrator_node', 'npm_node', 'shim_node', 'npm']) { + j.tools[key].path = `Z:\\retained remote ü\\${key}.exe`; + if (key.endsWith('_node')) j.tools[key].version = key === 'orchestrator_node' ? 'v22.21.1' : 'v24.1.0'; + } + const row = { ...structuredClone(j.tools), runner: {}, image: {}, observer: {}, installer_policy: {} }; + const manifest = { cells: { [j.cell]: row }, controllers: { 'windows-arm64': { node: j.tools.orchestrator_node } } }; + t.mock.method(fs, 'lstatSync', () => { throw new Error('remote path opened'); }); + t.mock.method(fs, 'readFileSync', () => { throw new Error('remote path opened'); }); + assert.deepEqual(a.historicalTools(j, manifest), j.tools); + for (const key of ['npm_node', 'shim_node', 'npm']) { + const bad = structuredClone(manifest); bad.cells[j.cell][key].sha256 = H('substituted'); + assert.throws(() => a.historicalTools(j, bad), /historical source-frozen/); + } + row.observer = null; assert.throws(() => a.historicalTools(j, manifest), /PUBLIC_PROVISIONING_REQUIRED/); + }); + 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), /PUBLIC_PROVISIONING_REQUIRED/); + 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 })); + const before = fs.readdirSync(f.root); let effects = 0; + t.mock.method(require('node:child_process'), 'spawnSync', () => { effects++; throw Error('unexpected effect'); }); + for (const mode of ['--assemble', '--attest-inputs', '--read']) + assert.throws(() => a.main([mode, f.request.journey]), /PUBLIC_PROVISIONING_REQUIRED/); + assert.equal(effects, 0); assert.deepEqual(fs.readdirSync(f.root), before); + }); + 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..8590bf25 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,426 @@ 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(): + return require_public_controller('linux-amd64') + + +def require_public_controller(target): + require(target in PROVISION_TARGETS, 'fixed public controller target') + value = read_provisioning() + for name, tool in value['controllers'][target].items(): + message = 'PUBLIC_PROVISIONING_REQUIRED:' + target + ':' + 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'][target]['node']['path'] + + +def require_authenticated_execution(): + repo = Path(__file__).absolute().parent.parent + # Availability only. Actual exports, source/tool closure and complete local J + # semantics are independently checked by the fixed Node bridge before plans. + for name in ('public-authoring-custody', 'public-process-observation', 'public-installer-evidence'): + file = repo / 'npm/agentplugins/scripts' / (name + '.js') + require(file.exists(), 'C3b execution incomplete: PUBLIC_FACADE_REQUIRED:' + name + '.js') + provision_bytes(file, 1024 * 1024) + + +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', + 'cli/plugin-kit-ai/internal/authoring/scaffold/templates'): + 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 public_workflow(mode, cell=None): + """Fixed workflow bootstrap; trusted OS Python precedes repository Node. + + Provisioning must independently keep the checkout and tool destinations + immutable. This wrapper neither installs tools nor accepts receipt pins. + """ + import os + import subprocess + require(mode in ('inputs', 'produce', 'assemble', 'attest', 'read'), 'fixed public workflow operation') + require((mode == 'produce' and cell in PROVISION_CELLS) or (mode != 'produce' and cell is None), 'fixed workflow cell') + require(os.environ.get('GITHUB_REPOSITORY') == '777genius/universal-agent-plugins' and + os.environ.get('GITHUB_EVENT_NAME') == 'workflow_dispatch', 'canonical public dispatch') + selected = json.loads(os.environ.get('PUBLIC_SELECTED', '')) + require(set(selected) == {'tag', 'ref', 'source', 'versions'} and + re.fullmatch('[0-9a-f]{40}', selected['source']) and + selected['source'] == os.environ.get('GITHUB_SHA') == os.environ.get('GITHUB_WORKFLOW_SHA') and + selected['ref'] == 'refs/tags/' + selected['tag'] == os.environ.get('GITHUB_REF'), 'exact public tag F') + target = cell.split('/')[0] if cell else 'linux-amd64' + controller = require_public_controller(target) + require_authenticated_execution() + source = authenticated_source() + repo = Path(__file__).absolute().parent.parent + env = {k: v for k, v in os.environ.items() if k.startswith(('GITHUB_', 'PUBLIC_', 'RUNNER_')) or + k in ('GH_TOKEN', 'SYSTEMROOT', 'SystemRoot', 'TEMP', 'TMP', 'TMPDIR', 'HOME', 'USERPROFILE')} + env.update(PATH='/usr/local/bin:/usr/bin:/bin', LANG='C.UTF-8', LC_ALL='C.UTF-8') + argv = [controller, str(repo / 'npm/agentplugins/scripts/public-authoring-acceptance.js'), '--workflow', mode] + if cell: argv.append(cell) + require(require_public_controller(target) == controller and authenticated_source() == source, 'workflow bootstrap changed') + try: + result = subprocess.run(argv, cwd=repo, env=env, capture_output=True, timeout=3600) + finally: + require(require_public_controller(target) == controller and authenticated_source() == source, 'workflow bootstrap changed') + require(result.returncode == 0 and result.stderr == b'', 'public workflow incomplete: ' + result.stderr.decode(errors='replace')[:4096]) + require(len(result.stdout) <= 1024 * 1024, 'bounded public workflow result') + value = json.loads(result.stdout) + # GitHub output destinations originate in the runner, never dispatch inputs. + with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output: + if mode == 'inputs': output.write('matrix=' + json.dumps(value['matrix'], separators=(',', ':')) + '\n') + if mode in ('produce', 'assemble', 'attest'): + root = value['root']; require(isinstance(root, str) and not any(c in root for c in '\r\n'), 'single-line artifact root') + output.write('root=' + root + '\n') + if mode == 'attest': + subjects = value['subjects'] + require(len(subjects) == 2 and [Path(s['file']).name for s in subjects] == + ['public-packed-completion.json', 'public-packed-evidence.json'], 'exact E2 action subjects') + output.write('subjects<= 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("