From 04863164d4ba272426d93968e0fc5b1edbbda689 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 03:38:22 +0000 Subject: [PATCH 01/18] feat(authoring): assemble completed public acceptance attempts Refs #216 Refs #208 --- .github/workflows/authoring-public-packed.yml | 314 +++++++++++ .../scripts/authoring-promotion.js | 72 ++- .../scripts/packed-installer-bridge.md | 72 +++ .../scripts/packed-installer-bridge.test.js | 2 +- .../scripts/public-authoring-acceptance.js | 517 +++++++++++++++++- .../test/authoring-promotion.test.js | 47 ++ ...blic-authoring-acceptance-workflow.test.js | 71 +++ .../test/public-authoring-acceptance.test.js | 415 +++++++++++++- scripts/check-packed-ci.py | 170 +++++- scripts/test_packed_ci.py | 38 +- 10 files changed, 1687 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/authoring-public-packed.yml create mode 100644 npm/agentplugins/test/public-authoring-acceptance-workflow.test.js diff --git a/.github/workflows/authoring-public-packed.yml b/.github/workflows/authoring-public-packed.yml new file mode 100644 index 00000000..e4c2995d --- /dev/null +++ b/.github/workflows/authoring-public-packed.yml @@ -0,0 +1,314 @@ +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_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 + s = json.loads(os.environ['PUBLIC_SELECTED']) + assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + 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 + s = json.loads(os.environ['PUBLIC_SELECTED']) + assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + 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 + s = json.loads(os.environ['PUBLIC_SELECTED']) + assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + 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 + s = json.loads(os.environ['PUBLIC_SELECTED']) + assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + 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 + s = json.loads(os.environ['PUBLIC_SELECTED']) + assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + 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 + s = json.loads(os.environ['PUBLIC_SELECTED']) + assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + assert s['source'] == os.environ['GITHUB_SHA'] == os.environ['GITHUB_WORKFLOW_SHA'] + 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/npm/agentplugins/scripts/authoring-promotion.js b/npm/agentplugins/scripts/authoring-promotion.js index 6c271247..5691d154 100644 --- a/npm/agentplugins/scripts/authoring-promotion.js +++ b/npm/agentplugins/scripts/authoring-promotion.js @@ -126,11 +126,13 @@ function requireNativeContracts(lanes) { // This is only the registered-contract inventory, never evidence admission. // Public packed has no accepted producer. Even twelve admitted native lanes // cannot authorize the thirteen-lane global gate or a protected effect. - const unsupported = lanes.filter(x => x.lane === "public-packed-pair" || + 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(", ")}]. ` + - "Public-packed producer/schema remains unsupported. dual-authoring-public-native/v1 is Linux fixture evidence with false release claims; private-packed or SLSA build success cannot qualify this pair. Signing and promotion are disabled."); + "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); @@ -608,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"); @@ -621,10 +623,11 @@ function admittedInputs(input) { exact(o.workflow_sha, record.identity.commit, "integrated workflow source"); const native = admitNativeEvidence(record, o.preparation, o.scratch); require("./authoring-native-qualification").readPreparation(o.root, projectedPins(record), native.preparation); - requireNativeContracts(record.qualification.lanes); // Public adapter still unavailable; zero effects. + 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, { @@ -721,7 +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 = { inspectStageCaller, workflowSelection, inspectInputCaller, checkPreparationRef, acquireCurrentStage, inspectCurrentStage, checkStageEvidence, SCHEMA, WORKFLOW, GH_VERSION, LANES, encodeRecord, decodeRecord, validateSelection, admitRecord, requireNativeContracts, +// 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/packed-installer-bridge.md b/npm/agentplugins/scripts/packed-installer-bridge.md index cfbfd2a9..5c25e0ff 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.md +++ b/npm/agentplugins/scripts/packed-installer-bridge.md @@ -519,3 +519,75 @@ 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 eaed11da..19d928ca 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.test.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.test.js @@ -236,7 +236,7 @@ test('C3 bridge authenticated intake preserves legacy dispatch', t => { } for (const extra of [{authenticated:true}, {nativeTap:'fixture.tap'}, {disposableEvidence:true}]) assert.throws(() => bridge.seal({...f.request,...extra})); }); - assert.throws(() => a.readAcceptance(f.request), /completed remote E/); + assert.throws(() => a.readAcceptance(f.request), /PUBLIC_PROVISIONING_REQUIRED/); }); test('C3 bridge seal binds original ten projects', t => { const a = require('./public-authoring-acceptance'); diff --git a/npm/agentplugins/scripts/public-authoring-acceptance.js b/npm/agentplugins/scripts/public-authoring-acceptance.js index bf642979..c23f4e64 100644 --- a/npm/agentplugins/scripts/public-authoring-acceptance.js +++ b/npm/agentplugins/scripts/public-authoring-acceptance.js @@ -766,17 +766,22 @@ function normalizeResult(v) { function evidenceFiles(name, value) { const files = {}; if (['npm-lifecycle.json', 'cache-process.json', 'installer.json'].includes(name) && Array.isArray(value.rows)) { - const refs = []; let shard = [], size = 3; + const 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 = 3; + 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'); - if (size + bytes.length + 1 > TRANSCRIPT_LIMIT) flush(); - shard.push(row); size += bytes.length + 1; + // 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 } }; } @@ -1184,9 +1189,506 @@ function readJourney(value) { const local = readJourneyInputs(value); verifyJourney(local); verifySidecars(local.evidence, path.dirname(value.journey)); provision.requireCellTools(selected.key); agree(sourceSeal(admission.repo, r.expectedCommit, frozen), before, 'late source closure'); return local; } -function readAcceptance() { throw new Error("C3b required: completed remote E reader is closed; local J and fixture success are not E"); } +// 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 }; +} +function workflowRequest(mode, key) { + assert.ok(['inputs', 'produce', 'assemble', 'attest', 'read'].includes(mode), 'fixed workflow operation'); + 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 parse = name => bounded(Buffer.from(process.env[name] || ''), LIMIT); + const selected = parse('PUBLIC_SELECTED'), stage = parse('PUBLIC_STAGE'), input = parse('PUBLIC_INPUT'); locator(stage); locator(input); + 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: parse('PUBLIC_JOURNEYS'), bridge: parse('PUBLIC_BRIDGE') }; + if (mode === 'attest') return { ...r, output, assembly: parse('PUBLIC_ASSEMBLY') }; + return { ...r, acceptance: parse('PUBLIC_ACCEPTANCE'), assembly: parse('PUBLIC_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, @@ -1195,7 +1697,10 @@ function main(args) { // No producer or completed-E CLI can return a success-shaped placeholder. module.exports = { generatedFiles, generatedTreeIdentity, evidenceFiles, expandEvidence, scenarioContract, plannedInvocation, rootsFor, requireFacades, verifyNpmLifecycle, verifyCacheProcess, verifyResults, produceJourney, expectedCachePath, PROFILES, SURFACE, clientFacts, componentFacts, LITERAL_DESCRIPTION, outputJSON, matrix, commandContract, encodeJourney, decodeJourney, readJourneyInputs, verifyJourney, readJourney, - readAcceptance, request, fileJSON, disjoint, main, LIMIT, SCHEMA, MATRIX_SCHEMA, INTAKE, WORKFLOW, MISSING }; + 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, 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/test/authoring-promotion.test.js b/npm/agentplugins/test/authoring-promotion.test.js index 658e2326..b5ba40cc 100644 --- a/npm/agentplugins/test/authoring-promotion.test.js +++ b/npm/agentplugins/test/authoring-promotion.test.js @@ -874,6 +874,53 @@ test("C1 provenance fixed tag adapter rejects a moved second product tag", t => // 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 => { 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..c3e5aff6 --- /dev/null +++ b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js @@ -0,0 +1,71 @@ +'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 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]])); + +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:/); +}); diff --git a/npm/agentplugins/test/public-authoring-acceptance.test.js b/npm/agentplugins/test/public-authoring-acceptance.test.js index 967c6376..015c86b2 100644 --- a/npm/agentplugins/test/public-authoring-acceptance.test.js +++ b/npm/agentplugins/test/public-authoring-acceptance.test.js @@ -8,7 +8,7 @@ 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) { +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"); @@ -68,11 +68,20 @@ function fixture(t) { } } } + 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: "linux-amd64/pair-node22", tools, command_contract_sha256: H("pending"), - subjects: Object.fromEntries(c.PRODUCTS.map(p => [p, input.products[p].assets])), projects, evidence: [], + 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)); @@ -86,7 +95,11 @@ function fixture(t) { cwd: row.scenario === "projects" ? projects[row.product] : path.join(fixtureRoot, `${row.product} malformed-skill ü`), status: row.status, signal: null, stdout: row.id === "product-help" ? "SYNTHETIC help ".repeat(10) : JSON.stringify(result), stderr: "" }; }); - const evidence = { "commands.json": rows, "projects.json": Object.fromEntries(c.PRODUCTS.map(p => [p, bridge.snapshot(projects[p])])), + 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) }; }); @@ -104,7 +117,7 @@ function fixture(t) { const inputSubjects = [{ file: path.join(inputRoot, ic.INPUT_FILE), sha256: c.digest(inputBytes) }]; for (let i = 0; i < 18; i++) { const file = path.join(inputRoot, `synthetic-subject-${i}`); write(file, Buffer.from(`SYNTHETIC ${i}`)); inputSubjects.push({ file, sha256: hash(file) }); } const inputResult = { root: inputRoot, input, subjects: inputSubjects }; - return { root, j, inputBytes, stageBytes, evidence, request, admission, stageResult, inputResult, save, + 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) { @@ -116,7 +129,8 @@ function withReaders(t, f, run) { // Every value below is explicitly SYNTHETIC. No pack, native process, scanner, // custody or observer implementation is executed by these semantic fixtures. function semanticFixture(f) { - const j = f.j, commands = a.commandContract(j.cell), roots = a.rootsFor(f.root, j.cell); + const 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 => { @@ -159,7 +173,7 @@ function semanticFixture(f) { clients: a.clientFacts(), commands: a.SURFACE, evidence_limits: ['static_only', 'no_path_lookup', 'no_executable_version_probe', 'no_runtime_or_oauth_evidence', 'native_files_metadata_only'] }; if (w.id === 'engine-version' || w.product === 'plugin-kit-ai' && w.id === 'product-version') Object.assign(data, { product: w.product, product_version: j.identity.versions[w.product] }); const result = { schema_version: 1, command: productVersion ? 'version' : operation, result: w.status ? 'failure' : 'success', data: productVersion ? { version: j.identity.versions.agentplugins } : data }; - return { product: w.product, id: w.id, argv: w.argv, cwd: w.scenario === 'projects' ? j.projects[w.product] : path.join(path.dirname(j.projects[w.product]), `${w.product} malformed-skill ü`), + 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; @@ -167,7 +181,7 @@ function semanticFixture(f) { const empty = () => Object.fromEntries(selected.products.map(p => [p, null])); const asset = (p, name) => ({ path: a.expectedCachePath(j, roots, { cache: name, prefix: 'unused', product: p, kind: 'cold' }, p), ...Object.fromEntries(['sha256', 'size'].map(k => [k, j.subjects[p][selected.target].binary[k]])), mode: j.cell.startsWith('windows-') ? 0o666 : 0o755 }); const pkg = (p, prefix) => ({ tree: H(p + 'tree'), shims: (j.cell.startsWith('windows-') ? ['posix', 'cmd', 'powershell'] : ['posix']).map(kind => ({ kind, - path: path.join(roots.npm, prefix, 'prefix', ...(j.cell.startsWith('windows-') ? [] : ['bin']), p + ({ posix: '', cmd: '.cmd', powershell: '.ps1' }[kind])), + 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(); @@ -197,7 +211,7 @@ function semanticFixture(f) { stdout: { size: Buffer.byteLength(stdout), sha256: H(stdout) }, stderr: { size: 0, sha256: H('') }, status: core ? core.status : s.kind.startsWith('invalid-') ? 1 : 0, signal: null, before, after, observation: ref(), events: !npm ? ['shim', ...(launches ? ['native'] : [])] : [], acquisitions, commits, downloads: 0, native_launches: launches, postinstall: null, interval: [next, next + 10], - literal: s.kind === 'literal-argv' ? { root: path.join(planned.cwd, 'literal project ü'), description: a.LITERAL_DESCRIPTION, manifest_sha256: H('synthetic literal manifest') } : null }; + 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'); @@ -235,8 +249,383 @@ function withFacades(t, run) { 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); @@ -383,10 +772,14 @@ if (require.main === module) { }); }); test("C3 unit same invocation bridge cannot authenticate remote E", t => { - const f = fixture(t); assert.throws(() => a.readAcceptance(f.request), /completed remote E reader is closed/); + 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 })); - assert.throws(() => a.main(["--assemble", f.request.journey]), /only --read-local-inputs/); + 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); diff --git a/scripts/check-packed-ci.py b/scripts/check-packed-ci.py index 4d4a356f..8590bf25 100644 --- a/scripts/check-packed-ci.py +++ b/scripts/check-packed-ci.py @@ -481,18 +481,29 @@ def tool(v, target, npm=False): 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']['linux-amd64'].items(): - message = 'PUBLIC_PROVISIONING_REQUIRED:linux-amd64:' + name + 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']['linux-amd64']['node']['path'] + return value['controllers'][target]['node']['path'] def require_authenticated_execution(): - raise ValueError('C3b execution incomplete: result/installer/observer validators and independent invocation authority required') + 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(): @@ -500,7 +511,8 @@ def authenticated_source(): # 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'): + 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()): @@ -531,6 +543,55 @@ def authenticated_verify(node, argv): 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< Date: Thu, 10 Sep 2026 04:23:01 +0000 Subject: [PATCH 02/18] fix(authoring): reject cross-mode dispatch inputs Refs #216 Refs #208 --- .github/workflows/authoring-public-packed.yml | 127 +++++++++++++++++- .../scripts/public-authoring-acceptance.js | 43 +++++- ...blic-authoring-acceptance-workflow.test.js | 72 ++++++++++ 3 files changed, 230 insertions(+), 12 deletions(-) diff --git a/.github/workflows/authoring-public-packed.yml b/.github/workflows/authoring-public-packed.yml index e4c2995d..8fd5ce9d 100644 --- a/.github/workflows/authoring-public-packed.yml +++ b/.github/workflows/authoring-public-packed.yml @@ -44,6 +44,7 @@ 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 }} @@ -63,7 +64,26 @@ jobs: shell: python -I -B {0} run: | import json, os, re - s = json.loads(os.environ['PUBLIC_SELECTED']) + 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 value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + return json.loads(raw[name]) + 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'} assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' @@ -101,7 +121,26 @@ jobs: shell: python -I -B {0} run: | import json, os, re - s = json.loads(os.environ['PUBLIC_SELECTED']) + 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 value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + return json.loads(raw[name]) + 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'} assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' @@ -152,7 +191,26 @@ jobs: shell: python -I -B {0} run: | import json, os, re - s = json.loads(os.environ['PUBLIC_SELECTED']) + 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 value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + return json.loads(raw[name]) + 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'} assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' @@ -189,7 +247,26 @@ jobs: shell: python -I -B {0} run: | import json, os, re - s = json.loads(os.environ['PUBLIC_SELECTED']) + 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 value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + return json.loads(raw[name]) + 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'} assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' @@ -227,7 +304,26 @@ jobs: shell: python -I -B {0} run: | import json, os, re - s = json.loads(os.environ['PUBLIC_SELECTED']) + 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 value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + return json.loads(raw[name]) + 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'} assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' @@ -290,7 +386,26 @@ jobs: shell: python -I -B {0} run: | import json, os, re - s = json.loads(os.environ['PUBLIC_SELECTED']) + 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 value(name): + assert 0 < len(raw[name].encode()) <= 1024 * 1024 + return json.loads(raw[name]) + 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'} assert os.environ['GITHUB_REPOSITORY'] == '777genius/universal-agent-plugins' assert os.environ['GITHUB_EVENT_NAME'] == 'workflow_dispatch' diff --git a/npm/agentplugins/scripts/public-authoring-acceptance.js b/npm/agentplugins/scripts/public-authoring-acceptance.js index c23f4e64..ada603fc 100644 --- a/npm/agentplugins/scripts/public-authoring-acceptance.js +++ b/npm/agentplugins/scripts/public-authoring-acceptance.js @@ -1606,15 +1606,46 @@ function readAcceptance(r) { 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); + } + fields(values.selected, ['tag', 'ref', 'source', 'versions'], 'selected source'); + assert.ok(typeof values.selected.source === 'string' && /^[0-9a-f]{40}$/.test(values.selected.source), 'selected source commit'); + 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 parse = name => bounded(Buffer.from(process.env[name] || ''), LIMIT); - const selected = parse('PUBLIC_SELECTED'), stage = parse('PUBLIC_STAGE'), input = parse('PUBLIC_INPUT'); locator(stage); locator(input); + 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; @@ -1651,9 +1682,9 @@ function workflowRequest(mode, key) { 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: parse('PUBLIC_JOURNEYS'), bridge: parse('PUBLIC_BRIDGE') }; - if (mode === 'attest') return { ...r, output, assembly: parse('PUBLIC_ASSEMBLY') }; - return { ...r, acceptance: parse('PUBLIC_ACCEPTANCE'), assembly: parse('PUBLIC_ASSEMBLY') }; + 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); @@ -1700,7 +1731,7 @@ module.exports = { generatedFiles, generatedTreeIdentity, evidenceFiles, expandE 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, request, fileJSON, disjoint, main, LIMIT, SCHEMA, MATRIX_SCHEMA, INTAKE, WORKFLOW, MISSING }; + 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/test/public-authoring-acceptance-workflow.test.js b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js index c3e5aff6..e8480061 100644 --- a/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js +++ b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js @@ -4,10 +4,66 @@ 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 dispatch = mode => { + const values = { + selected: { tag: 'agentplugins-v2.0.0', ref: 'refs/tags/agentplugins-v2.0.0', source: 'a'.repeat(40), versions: {} }, + 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, ...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({}); }]); + for (const [label, mutate] of cases) { + process.env = { ...savedEnv, ...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', @@ -69,3 +125,19 @@ test('C3 workflow pins source actions and independent controller bootstrap', () 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, /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); + } +}); From a1ee3ebc29494e803ba353a3e9dcb97ed4116159 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 04:44:30 +0000 Subject: [PATCH 03/18] fix(authoring): validate selected source before effects Refs #216 Refs #208 --- .github/workflows/authoring-public-packed.yml | 114 +++++++++++++++--- .../scripts/public-authoring-acceptance.js | 5 +- ...blic-authoring-acceptance-workflow.test.js | 62 +++++++++- 3 files changed, 158 insertions(+), 23 deletions(-) diff --git a/.github/workflows/authoring-public-packed.yml b/.github/workflows/authoring-public-packed.yml index 8fd5ce9d..b8fb9e7f 100644 --- a/.github/workflows/authoring-public-packed.yml +++ b/.github/workflows/authoring-public-packed.yml @@ -69,9 +69,18 @@ jobs: 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 - return json.loads(raw[name]) + 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']) @@ -84,11 +93,15 @@ jobs: locator(value('bridge')) if mode in ('attest', 'check'): locator(value('assembly')) if mode == 'check': locator(value('acceptance')) - assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + 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: @@ -126,9 +139,18 @@ jobs: 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 - return json.loads(raw[name]) + 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']) @@ -141,11 +163,15 @@ jobs: locator(value('bridge')) if mode in ('attest', 'check'): locator(value('assembly')) if mode == 'check': locator(value('acceptance')) - assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + 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: @@ -196,9 +222,18 @@ jobs: 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 - return json.loads(raw[name]) + 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']) @@ -211,11 +246,15 @@ jobs: locator(value('bridge')) if mode in ('attest', 'check'): locator(value('assembly')) if mode == 'check': locator(value('acceptance')) - assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + 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: @@ -252,9 +291,18 @@ jobs: 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 - return json.loads(raw[name]) + 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']) @@ -267,11 +315,15 @@ jobs: locator(value('bridge')) if mode in ('attest', 'check'): locator(value('assembly')) if mode == 'check': locator(value('acceptance')) - assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + 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: @@ -309,9 +361,18 @@ jobs: 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 - return json.loads(raw[name]) + 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']) @@ -324,11 +385,15 @@ jobs: locator(value('bridge')) if mode in ('attest', 'check'): locator(value('assembly')) if mode == 'check': locator(value('acceptance')) - assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + 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: @@ -391,9 +456,18 @@ jobs: 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 - return json.loads(raw[name]) + 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']) @@ -406,11 +480,15 @@ jobs: locator(value('bridge')) if mode in ('attest', 'check'): locator(value('assembly')) if mode == 'check': locator(value('acceptance')) - assert set(s) == {'tag', 'ref', 'source', 'versions'} + 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']) + 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: diff --git a/npm/agentplugins/scripts/public-authoring-acceptance.js b/npm/agentplugins/scripts/public-authoring-acceptance.js index ada603fc..ad200ddb 100644 --- a/npm/agentplugins/scripts/public-authoring-acceptance.js +++ b/npm/agentplugins/scripts/public-authoring-acceptance.js @@ -1624,8 +1624,9 @@ function dispatchInputs(mode) { else assert.equal(body, '', `foreign ${dispatchMode} dispatch input: ${name}`); if (body) values[name] = bounded(Buffer.from(body), LIMIT); } - fields(values.selected, ['tag', 'ref', 'source', 'versions'], 'selected source'); - assert.ok(typeof values.selected.source === 'string' && /^[0-9a-f]{40}$/.test(values.selected.source), 'selected source commit'); + // 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'); diff --git a/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js index e8480061..0ffcc470 100644 --- a/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js +++ b/npm/agentplugins/test/public-authoring-acceptance-workflow.test.js @@ -16,9 +16,11 @@ 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: { tag: 'agentplugins-v2.0.0', ref: 'refs/tags/agentplugins-v2.0.0', source: 'a'.repeat(40), versions: {} }, + 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) }; @@ -47,13 +49,27 @@ test('C3 runtime rejects every open, missing, and malformed dispatch before effe try { for (const [mode, names] of Object.entries(required)) { const cases = []; - process.env = { ...savedEnv, ...dispatch(mode) }; + 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, ...dispatch(mode) }; mutate(process.env); + 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}`); } @@ -134,10 +150,50 @@ test('C3 trusted bootstrap closes and validates dispatch inputs before 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}`); + } } }); From 4ca01accbde00840fb92e6500c319288128ed16f Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 04:41:12 +0000 Subject: [PATCH 04/18] test(authoring): add milestone A end-to-end gate Refs #216 Refs #208 --- .../workflows/authoring-milestone-a-e2e.yml | 121 ++++++++++++++ npm/agentplugins/scripts/milestone-a-e2e.js | 153 ++++++++++++++++++ npm/agentplugins/test/milestone-a-e2e.test.js | 49 ++++++ 3 files changed, 323 insertions(+) create mode 100644 .github/workflows/authoring-milestone-a-e2e.yml create mode 100644 npm/agentplugins/scripts/milestone-a-e2e.js create mode 100644 npm/agentplugins/test/milestone-a-e2e.test.js diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml new file mode 100644 index 00000000..2407c965 --- /dev/null +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -0,0 +1,121 @@ +name: Authoring Milestone A E2E + +on: + pull_request: + paths: + - '.github/workflows/authoring-milestone-a-e2e.yml' + - 'npm/agentplugins/scripts/milestone-a-e2e.js' + - 'npm/agentplugins/test/milestone-a-e2e.test.js' + - 'cli/plugin-kit-ai/**' + - 'install/**' + 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" + printf 'MILESTONE_A_BUNDLE=%s\n' "$root" >> "$GITHUB_ENV" + 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" + # Tool acquisition precedes the offline candidate builder. Runtime + # journeys never receive network access or this module cache. + GOMODCACHE="$modcache" go mod download + node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],repo:process.env.GITHUB_WORKSPACE,expectedHead:process.env.EXPECTED_HEAD,go:process.argv[3],modCache:process.argv[4],node:process.execPath,npm:process.argv[5]}))' \ + "$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 + 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: Run both packaged public entrypoints in fresh roots + shell: bash + run: | + set -euo pipefail + root="$RUNNER_TEMP/milestone-a-run-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + mkdir -p "$root/evidence" + printf 'MILESTONE_A_EVIDENCE=%s/evidence\n' "$root" >> "$GITHUB_ENV" + rmdir "$root/evidence" "$root" + config="$RUNNER_TEMP/milestone-a-run.json" + node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],input:process.argv[3],platform:process.argv[4],arch:process.argv[5]}))' \ + "$config" "$root" "$RUNNER_TEMP/milestone-a-input" '${{ matrix.platform }}' '${{ matrix.arch }}' + node npm/agentplugins/scripts/milestone-a-e2e.js run "$config" + test -z "$(git status --porcelain)" + - 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/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js new file mode 100644 index 00000000..cfd13cc9 --- /dev/null +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node +"use strict"; + +// Milestone A's bounded, offline exact-candidate producer and consumer. This +// intentionally composes the existing sealed native/npm assembly rather than +// introducing another package format. +const fs = require("node:fs"); +const path = require("node:path"); +const cp = require("node:child_process"); +const crypto = require("node:crypto"); +const producer = require("./stage-dual-authoring-candidate"); +const packer = require("./stage-dual-authoring-npm"); + +const PRODUCTS = ["agentplugins", "plugin-kit-ai"]; +const TARGETS = { linux: ["amd64"], windows: ["amd64"], darwin: ["arm64"] }; +const COMMANDS = ["init", "validate", "inspect", "test", "local-add-dry-run"]; + +function fail(message) { throw new Error(message); } +function sha(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); } +function absolute(name, value) { + if (!value || !path.isAbsolute(value) || path.resolve(value) !== value) fail(`${name} must be absolute`); + return value; +} +function cleanRoot(root) { + absolute("root", root); + if (fs.existsSync(root)) fail("Milestone A root must be new"); + fs.mkdirSync(root, { recursive: false, mode: 0o700 }); + for (const name of ["evidence", "work", "home", "tmp", "cache", "config"]) + fs.mkdirSync(path.join(root, name), { mode: 0o700 }); +} +function baseEnv(root) { + return { ...process.env, HOME: path.join(root, "home"), USERPROFILE: path.join(root, "home"), + TMPDIR: path.join(root, "tmp"), TMP: path.join(root, "tmp"), TEMP: path.join(root, "tmp"), + XDG_CONFIG_HOME: path.join(root, "config"), XDG_CACHE_HOME: path.join(root, "cache"), + npm_config_cache: path.join(root, "cache", "npm"), 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 run(exe, args, options = {}) { + const result = cp.spawnSync(exe, args, { encoding: "utf8", timeout: 120000, + windowsHide: true, maxBuffer: 16 * 1024 * 1024, ...options }); + if (result.error) throw result.error; + if (result.signal) fail(`${path.basename(exe)} terminated by ${result.signal}`); + return result; +} +function requireSuccess(result, label) { + if (result.status !== 0) fail(`${label} exited ${result.status}: ${result.stderr || result.stdout}`); + return result; +} +function jsonContract(result, label) { + requireSuccess(result, label); + let value; try { value = JSON.parse(result.stdout); } catch { fail(`${label} did not return JSON`); } + if (value.schema_version !== 1 || value.result !== "success" || typeof value.command !== "string") + fail(`${label} returned an invalid result contract`); + return value; +} +function tree(root) { + const rows = []; + function visit(dir, prefix = "") { + for (const ent of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const rel = prefix ? `${prefix}/${ent.name}` : ent.name, file = path.join(dir, ent.name); + if (ent.isDirectory()) visit(file, rel); + else if (ent.isFile()) rows.push([rel, sha(fs.readFileSync(file))]); + else fail(`generated tree contains non-regular entry: ${rel}`); + } + } + visit(root); return rows; +} +function prepare(config) { + const root = absolute("root", config.root), repo = absolute("repo", config.repo); + cleanRoot(root); + const head = requireSuccess(run("git", ["rev-parse", "HEAD"], { cwd: repo }), "git head").stdout.trim(); + if (head !== config.expectedHead) fail("checkout is not the expected exact candidate"); + const identity = { repository: "777genius/universal-agent-plugins", commit: head, + versions: { agentplugins: "2.0.0", "plugin-kit-ai": "2.0.0" } }; + const common = { candidate: true, repo, identity, assetScope: "six-platform-pair", + authoringMode: "release-cli-contract-v1", go: absolute("go", config.go), workParent: path.join(root, "work") }; + const candidate = path.join(root, "candidate"); + const built = producer.stageCandidate({ ...common, output: candidate, modCache: absolute("modCache", config.modCache) }); + const packages = path.join(root, "packages"); + const packed = packer.stagePair({ ...common, root: candidate, manifestDigest: built.manifest_sha256, + output: packages, node: absolute("node", config.node), npm: absolute("npm", config.npm) }); + const receipt = { schema: "milestone-a-e2e-prepare/v1", candidate_head: head, + candidate_sha256: built.manifest_sha256, asset_scope: "six-platform-pair", + packages: packed.packs, registry_fallback: false, publication: false }; + fs.writeFileSync(path.join(root, "evidence", "prepare.json"), JSON.stringify(receipt, null, 2) + "\n"); + return receipt; +} +function consume(config) { + const root = absolute("root", config.root), input = absolute("input", config.input); + cleanRoot(root); + const platform = config.platform, arch = config.arch; + if (!TARGETS[platform]?.includes(arch)) fail("unsupported Milestone A platform lane"); + const env = baseEnv(root), node = process.execPath, npm = process.platform === "win32" ? "npm.cmd" : "npm"; + const installed = {}, reports = {}, trees = {}; + let primary; + try { + for (const product of PRODUCTS) { + const prefix = path.join(root, "work", `install-${product}`); + fs.mkdirSync(prefix); + const packageName = product === "agentplugins" ? "universal-agent-plugins-2.0.0.tgz" : "plugin-kit-ai-2.0.0.tgz"; + requireSuccess(run(npm, ["install", "--ignore-scripts", "--offline", "--no-package-lock", "--prefix", prefix, + path.join(input, "packages", packageName)], { env }), `install ${product}`); + const packageDir = path.join(prefix, "node_modules", product === "agentplugins" ? "universal-agent-plugins" : product); + installed[product] = path.join(packageDir, "bin", `${product}.js`); + } + for (const product of PRODUCTS) { + const project = path.join(root, "work", `project-${product}`), client = path.join(root, "work", `client-${product}`); + fs.mkdirSync(client); + const argv = (args) => [installed[product], ...(product === "agentplugins" ? ["author"] : []), ...args]; + const commandEnv = { ...env, UAP_PRIVATE_NPM_CANDIDATE: path.join(input, "candidate"), + UAP_PRIVATE_NPM_CACHE: path.join(root, "cache", product) }; + reports[product] = []; + reports[product].push(jsonContract(run(node, argv(["init", project, "--template=skill", "--name=milestone-a-fixture", + "--description=Disposable Milestone A fixture.", "--format=json"]), { env: commandEnv }), `${product} init`)); + for (const command of ["validate", ...(platform === "linux" ? ["inspect", "test"] : [])]) + reports[product].push(jsonContract(run(node, argv([command, project, "--format=json"]), { env: commandEnv }), `${product} ${command}`)); + if (platform === "linux") { + const add = run(node, [installed.agentplugins, "add", ".", "--target=codex", "--dry-run", "--format=json"], + { cwd: project, env: { ...commandEnv, HOME: client, USERPROFILE: client } }); + reports[product].push(jsonContract(add, `${product} local add --dry-run`)); + } + trees[product] = tree(project); + } + if (platform === "linux" && JSON.stringify(trees.agentplugins) !== JSON.stringify(trees["plugin-kit-ai"])) + fail("entrypoints generated different trees; no provenance exception was required or applied"); + const receipt = { schema: "milestone-a-e2e-run/v1", platform, arch, exact_candidate: true, + entrypoints: PRODUCTS, clean_root_separation: true, commands: platform === "linux" ? COMMANDS : ["launcher-smoke", "init", "validate"], + fixture_target: "codex", registry_fallback: false, reports, trees, cleanup: "pending" }; + fs.writeFileSync(path.join(root, "evidence", "run.json"), JSON.stringify(receipt, null, 2) + "\n"); + return receipt; + } catch (error) { + primary = error; + fs.writeFileSync(path.join(root, "evidence", "failure.json"), JSON.stringify({ + schema: "milestone-a-e2e-failure/v1", platform, arch, message: String(error.message), cleanup: "pending" + }, null, 2) + "\n"); + throw error; + } finally { + for (const name of ["work", "home", "tmp", "cache", "config"]) + fs.rmSync(path.join(root, name), { recursive: true, force: true }); + const file = path.join(root, "evidence", primary ? "failure.json" : "run.json"); + if (fs.existsSync(file)) { const r = JSON.parse(fs.readFileSync(file)); r.cleanup = "complete"; + fs.writeFileSync(file, JSON.stringify(r, null, 2) + "\n"); } + } +} +function main(argv) { + if (argv.length !== 2 || !["prepare", "run"].includes(argv[0])) fail("usage: milestone-a-e2e.js "); + const config = JSON.parse(fs.readFileSync(absolute("config", argv[1]))); + return argv[0] === "prepare" ? prepare(config) : consume(config); +} +if (require.main === module) try { process.stdout.write(JSON.stringify(main(process.argv.slice(2))) + "\n"); } +catch (error) { process.stderr.write(`Milestone A E2E: ${error.message}\n`); process.exitCode = 1; } +module.exports = { prepare, consume, main, tree, COMMANDS, TARGETS }; 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..27335209 --- /dev/null +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -0,0 +1,49 @@ +"use strict"; +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const harness = require("../scripts/milestone-a-e2e"); + +const repo = path.resolve(__dirname, "../../.."); +const source = fs.readFileSync(path.join(repo, "npm/agentplugins/scripts/milestone-a-e2e.js"), "utf8"); +const workflow = fs.readFileSync(path.join(repo, ".github/workflows/authoring-milestone-a-e2e.yml"), "utf8"); + +test("harness contract fixes exact candidate, entrypoints, order, isolation, and cleanup", () => { + assert.deepEqual(harness.COMMANDS, ["init", "validate", "inspect", "test", "local-add-dry-run"]); + assert.deepEqual(harness.TARGETS, { linux: ["amd64"], windows: ["amd64"], darwin: ["arm64"] }); + for (const token of ["checkout is not the expected exact candidate", "six-platform-pair", + 'PRODUCTS = ["agentplugins", "plugin-kit-ai"]', '`project-${product}`', '`client-${product}`', + '"add", ".", "--target=codex", "--dry-run", "--format=json"', + 'registry_fallback: false', 'for (const name of ["work", "home", "tmp", "cache", "config"])']) assert.match(source, new RegExp(escape(token))); + assert.doesNotMatch(source, /npm (view|install).*latest|https?:\/\/registry|npx|npm_config_registry/); + assert.match(source, /timeout: 120000/); +}); + +test("workflow is secretless, pinned, bounded, PR/manual, and failure-preserving", () => { + assert.match(workflow, /pull_request:/); assert.match(workflow, /workflow_dispatch:/); + 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]*linux, arch: amd64/); + assert.match(workflow, /windows-2022[\s\S]*windows, arch: amd64/); + assert.match(workflow, /macos-14[\s\S]*darwin, arch: arm64/); + 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 value of [...workflow.matchAll(/timeout-minutes:\s*(\d+)/g)].map(m => Number(m[1]))) assert.ok(value <= 20); + assert.match(workflow, /NODE_OPTIONS: --max-old-space-size=384/); +}); + +test("tree digest is deterministic and rejects links", () => { + fs.mkdirSync(os.tmpdir(), { recursive: true }); + const root = fs.mkdtempSync(path.join(os.tmpdir(), "milestone-a-tree-")); + try { + fs.mkdirSync(path.join(root, "b")); fs.writeFileSync(path.join(root, "b", "z"), "z"); + fs.writeFileSync(path.join(root, "a"), "a"); + assert.deepEqual(harness.tree(root).map(row => row[0]), ["a", "b/z"]); + fs.symlinkSync(path.join(root, "a"), path.join(root, "link")); + assert.throws(() => harness.tree(root), /non-regular/); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); + +function escape(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } From 2f2c0287e1d3020412862c985e00b689a0a844c6 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 05:06:14 +0000 Subject: [PATCH 05/18] fix(authoring): prove milestone A isolation Refs #216 Refs #208 --- .../workflows/authoring-milestone-a-e2e.yml | 16 +- npm/agentplugins/scripts/milestone-a-e2e.js | 176 +++--------------- npm/agentplugins/test/milestone-a-e2e.test.js | 77 +++----- 3 files changed, 61 insertions(+), 208 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index 2407c965..b2ffe925 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -2,12 +2,6 @@ name: Authoring Milestone A E2E on: pull_request: - paths: - - '.github/workflows/authoring-milestone-a-e2e.yml' - - 'npm/agentplugins/scripts/milestone-a-e2e.js' - - 'npm/agentplugins/test/milestone-a-e2e.test.js' - - 'cli/plugin-kit-ai/**' - - 'install/**' workflow_dispatch: permissions: @@ -81,6 +75,7 @@ jobs: 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: @@ -102,13 +97,16 @@ jobs: shell: bash run: | set -euo pipefail - root="$RUNNER_TEMP/milestone-a-run-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + outer="$RUNNER_TEMP/milestone-a-outer-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + test ! -e "$outer" + mkdir -m 700 "$outer" + root="$outer/run" mkdir -p "$root/evidence" printf 'MILESTONE_A_EVIDENCE=%s/evidence\n' "$root" >> "$GITHUB_ENV" rmdir "$root/evidence" "$root" config="$RUNNER_TEMP/milestone-a-run.json" - node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],input:process.argv[3],platform:process.argv[4],arch:process.argv[5]}))' \ - "$config" "$root" "$RUNNER_TEMP/milestone-a-input" '${{ matrix.platform }}' '${{ matrix.arch }}' + node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],outerRoot:process.argv[3],input:process.argv[4],platform:process.argv[5],arch:process.argv[6],expectedHead:process.env.EXPECTED_HEAD}))' \ + "$config" "$root" "$outer" "$RUNNER_TEMP/milestone-a-input" '${{ matrix.platform }}' '${{ matrix.arch }}' node npm/agentplugins/scripts/milestone-a-e2e.js run "$config" test -z "$(git status --porcelain)" - name: Upload run evidence, including failures diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index cfd13cc9..d54cb9ae 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -1,153 +1,27 @@ #!/usr/bin/env node "use strict"; - -// Milestone A's bounded, offline exact-candidate producer and consumer. This -// intentionally composes the existing sealed native/npm assembly rather than -// introducing another package format. -const fs = require("node:fs"); -const path = require("node:path"); -const cp = require("node:child_process"); -const crypto = require("node:crypto"); -const producer = require("./stage-dual-authoring-candidate"); -const packer = require("./stage-dual-authoring-npm"); - -const PRODUCTS = ["agentplugins", "plugin-kit-ai"]; -const TARGETS = { linux: ["amd64"], windows: ["amd64"], darwin: ["arm64"] }; -const COMMANDS = ["init", "validate", "inspect", "test", "local-add-dry-run"]; - -function fail(message) { throw new Error(message); } -function sha(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); } -function absolute(name, value) { - if (!value || !path.isAbsolute(value) || path.resolve(value) !== value) fail(`${name} must be absolute`); - return value; -} -function cleanRoot(root) { - absolute("root", root); - if (fs.existsSync(root)) fail("Milestone A root must be new"); - fs.mkdirSync(root, { recursive: false, mode: 0o700 }); - for (const name of ["evidence", "work", "home", "tmp", "cache", "config"]) - fs.mkdirSync(path.join(root, name), { mode: 0o700 }); -} -function baseEnv(root) { - return { ...process.env, HOME: path.join(root, "home"), USERPROFILE: path.join(root, "home"), - TMPDIR: path.join(root, "tmp"), TMP: path.join(root, "tmp"), TEMP: path.join(root, "tmp"), - XDG_CONFIG_HOME: path.join(root, "config"), XDG_CACHE_HOME: path.join(root, "cache"), - npm_config_cache: path.join(root, "cache", "npm"), 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 run(exe, args, options = {}) { - const result = cp.spawnSync(exe, args, { encoding: "utf8", timeout: 120000, - windowsHide: true, maxBuffer: 16 * 1024 * 1024, ...options }); - if (result.error) throw result.error; - if (result.signal) fail(`${path.basename(exe)} terminated by ${result.signal}`); - return result; -} -function requireSuccess(result, label) { - if (result.status !== 0) fail(`${label} exited ${result.status}: ${result.stderr || result.stdout}`); - return result; -} -function jsonContract(result, label) { - requireSuccess(result, label); - let value; try { value = JSON.parse(result.stdout); } catch { fail(`${label} did not return JSON`); } - if (value.schema_version !== 1 || value.result !== "success" || typeof value.command !== "string") - fail(`${label} returned an invalid result contract`); - return value; -} -function tree(root) { - const rows = []; - function visit(dir, prefix = "") { - for (const ent of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { - const rel = prefix ? `${prefix}/${ent.name}` : ent.name, file = path.join(dir, ent.name); - if (ent.isDirectory()) visit(file, rel); - else if (ent.isFile()) rows.push([rel, sha(fs.readFileSync(file))]); - else fail(`generated tree contains non-regular entry: ${rel}`); - } - } - visit(root); return rows; -} -function prepare(config) { - const root = absolute("root", config.root), repo = absolute("repo", config.repo); - cleanRoot(root); - const head = requireSuccess(run("git", ["rev-parse", "HEAD"], { cwd: repo }), "git head").stdout.trim(); - if (head !== config.expectedHead) fail("checkout is not the expected exact candidate"); - const identity = { repository: "777genius/universal-agent-plugins", commit: head, - versions: { agentplugins: "2.0.0", "plugin-kit-ai": "2.0.0" } }; - const common = { candidate: true, repo, identity, assetScope: "six-platform-pair", - authoringMode: "release-cli-contract-v1", go: absolute("go", config.go), workParent: path.join(root, "work") }; - const candidate = path.join(root, "candidate"); - const built = producer.stageCandidate({ ...common, output: candidate, modCache: absolute("modCache", config.modCache) }); - const packages = path.join(root, "packages"); - const packed = packer.stagePair({ ...common, root: candidate, manifestDigest: built.manifest_sha256, - output: packages, node: absolute("node", config.node), npm: absolute("npm", config.npm) }); - const receipt = { schema: "milestone-a-e2e-prepare/v1", candidate_head: head, - candidate_sha256: built.manifest_sha256, asset_scope: "six-platform-pair", - packages: packed.packs, registry_fallback: false, publication: false }; - fs.writeFileSync(path.join(root, "evidence", "prepare.json"), JSON.stringify(receipt, null, 2) + "\n"); - return receipt; -} -function consume(config) { - const root = absolute("root", config.root), input = absolute("input", config.input); - cleanRoot(root); - const platform = config.platform, arch = config.arch; - if (!TARGETS[platform]?.includes(arch)) fail("unsupported Milestone A platform lane"); - const env = baseEnv(root), node = process.execPath, npm = process.platform === "win32" ? "npm.cmd" : "npm"; - const installed = {}, reports = {}, trees = {}; - let primary; - try { - for (const product of PRODUCTS) { - const prefix = path.join(root, "work", `install-${product}`); - fs.mkdirSync(prefix); - const packageName = product === "agentplugins" ? "universal-agent-plugins-2.0.0.tgz" : "plugin-kit-ai-2.0.0.tgz"; - requireSuccess(run(npm, ["install", "--ignore-scripts", "--offline", "--no-package-lock", "--prefix", prefix, - path.join(input, "packages", packageName)], { env }), `install ${product}`); - const packageDir = path.join(prefix, "node_modules", product === "agentplugins" ? "universal-agent-plugins" : product); - installed[product] = path.join(packageDir, "bin", `${product}.js`); - } - for (const product of PRODUCTS) { - const project = path.join(root, "work", `project-${product}`), client = path.join(root, "work", `client-${product}`); - fs.mkdirSync(client); - const argv = (args) => [installed[product], ...(product === "agentplugins" ? ["author"] : []), ...args]; - const commandEnv = { ...env, UAP_PRIVATE_NPM_CANDIDATE: path.join(input, "candidate"), - UAP_PRIVATE_NPM_CACHE: path.join(root, "cache", product) }; - reports[product] = []; - reports[product].push(jsonContract(run(node, argv(["init", project, "--template=skill", "--name=milestone-a-fixture", - "--description=Disposable Milestone A fixture.", "--format=json"]), { env: commandEnv }), `${product} init`)); - for (const command of ["validate", ...(platform === "linux" ? ["inspect", "test"] : [])]) - reports[product].push(jsonContract(run(node, argv([command, project, "--format=json"]), { env: commandEnv }), `${product} ${command}`)); - if (platform === "linux") { - const add = run(node, [installed.agentplugins, "add", ".", "--target=codex", "--dry-run", "--format=json"], - { cwd: project, env: { ...commandEnv, HOME: client, USERPROFILE: client } }); - reports[product].push(jsonContract(add, `${product} local add --dry-run`)); - } - trees[product] = tree(project); - } - if (platform === "linux" && JSON.stringify(trees.agentplugins) !== JSON.stringify(trees["plugin-kit-ai"])) - fail("entrypoints generated different trees; no provenance exception was required or applied"); - const receipt = { schema: "milestone-a-e2e-run/v1", platform, arch, exact_candidate: true, - entrypoints: PRODUCTS, clean_root_separation: true, commands: platform === "linux" ? COMMANDS : ["launcher-smoke", "init", "validate"], - fixture_target: "codex", registry_fallback: false, reports, trees, cleanup: "pending" }; - fs.writeFileSync(path.join(root, "evidence", "run.json"), JSON.stringify(receipt, null, 2) + "\n"); - return receipt; - } catch (error) { - primary = error; - fs.writeFileSync(path.join(root, "evidence", "failure.json"), JSON.stringify({ - schema: "milestone-a-e2e-failure/v1", platform, arch, message: String(error.message), cleanup: "pending" - }, null, 2) + "\n"); - throw error; - } finally { - for (const name of ["work", "home", "tmp", "cache", "config"]) - fs.rmSync(path.join(root, name), { recursive: true, force: true }); - const file = path.join(root, "evidence", primary ? "failure.json" : "run.json"); - if (fs.existsSync(file)) { const r = JSON.parse(fs.readFileSync(file)); r.cleanup = "complete"; - fs.writeFileSync(file, JSON.stringify(r, null, 2) + "\n"); } - } -} -function main(argv) { - if (argv.length !== 2 || !["prepare", "run"].includes(argv[0])) fail("usage: milestone-a-e2e.js "); - const config = JSON.parse(fs.readFileSync(absolute("config", argv[1]))); - return argv[0] === "prepare" ? prepare(config) : consume(config); -} -if (require.main === module) try { process.stdout.write(JSON.stringify(main(process.argv.slice(2))) + "\n"); } -catch (error) { process.stderr.write(`Milestone A E2E: ${error.message}\n`); process.exitCode = 1; } -module.exports = { prepare, consume, main, tree, COMMANDS, TARGETS }; +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 COMMANDS=["init","validate","inspect","test","local-add-dry-run"]; +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 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 fresh(root){absolute("root",root);if(fs.existsSync(root))fail("Milestone A root must be new");fs.mkdirSync(root,{mode:0o700});for(const n of ["evidence","journeys"])fs.mkdirSync(path.join(root,n),{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 ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}: ${r.stderr||r.stdout}`);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","project","client"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}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"])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 prepare(c){const root=absolute("root",c.root),repo=absolute("repo",c.repo);fresh(root);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={repository:"777genius/universal-agent-plugins",commit:head,versions:{agentplugins:"2.0.0","plugin-kit-ai":"2.0.0"}},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",candidate_head:head,candidate_sha256:built.manifest_sha256,asset_scope:"six-platform-pair",packages:packed.packs,registry_fallback:false,publication:false};fs.writeFileSync(path.join(root,"evidence","prepare.json"),JSON.stringify(receipt,null,2)+"\n");return receipt} +function provenance(ep,head){const real=fs.realpathSync(ep);let d=path.dirname(real),m;while(d!==path.dirname(d)){const x=path.join(d,"package.json");if(fs.existsSync(x)){m=x;break}d=path.dirname(d)}if(!m)fail(`entrypoint has no package provenance: ${ep}`);const p=JSON.parse(fs.readFileSync(m)),release=path.join(path.dirname(m),"private-release.json");let field,revision;if(fs.existsSync(release)){revision=JSON.parse(fs.readFileSync(release)).identity?.commit;field="private-release.json#identity.commit"}else{field=["gitHead","commit","revision"].find(k=>p[k]!==undefined)||(p.build?.revision!==undefined?"build.revision":null);revision=field?.startsWith("build")?p.build.revision:p[field]}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 product labels and the per-entrypoint journey-root provenance vary. +function normalize(v,project){const journey=path.dirname(project),x=JSON.parse(JSON.stringify(v).split(journey).join(""));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 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);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={},assertions=[];let primary,receipt; + try{for(const p of PRODUCTS){const j=makeJourney(root,p);roots[p]=j.root;const env={...envFor(j),UAP_PRIVATE_NPM_CANDIDATE:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux")reports[p].push(result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`));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`)}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project")))]));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:"codex",fixture_roots:Object.fromEntries(PRODUCTS.map(p=>[p,path.join(roots[p],"client")])),provenances,reports,trees,registry_fallback:false,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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} +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,COMMANDS,TARGETS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index 27335209..50a0dcf2 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -1,49 +1,30 @@ "use strict"; -const test = require("node:test"); -const assert = require("node:assert/strict"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); -const harness = require("../scripts/milestone-a-e2e"); - -const repo = path.resolve(__dirname, "../../.."); -const source = fs.readFileSync(path.join(repo, "npm/agentplugins/scripts/milestone-a-e2e.js"), "utf8"); -const workflow = fs.readFileSync(path.join(repo, ".github/workflows/authoring-milestone-a-e2e.yml"), "utf8"); - -test("harness contract fixes exact candidate, entrypoints, order, isolation, and cleanup", () => { - assert.deepEqual(harness.COMMANDS, ["init", "validate", "inspect", "test", "local-add-dry-run"]); - assert.deepEqual(harness.TARGETS, { linux: ["amd64"], windows: ["amd64"], darwin: ["arm64"] }); - for (const token of ["checkout is not the expected exact candidate", "six-platform-pair", - 'PRODUCTS = ["agentplugins", "plugin-kit-ai"]', '`project-${product}`', '`client-${product}`', - '"add", ".", "--target=codex", "--dry-run", "--format=json"', - 'registry_fallback: false', 'for (const name of ["work", "home", "tmp", "cache", "config"])']) assert.match(source, new RegExp(escape(token))); - assert.doesNotMatch(source, /npm (view|install).*latest|https?:\/\/registry|npx|npm_config_registry/); - assert.match(source, /timeout: 120000/); -}); - -test("workflow is secretless, pinned, bounded, PR/manual, and failure-preserving", () => { - assert.match(workflow, /pull_request:/); assert.match(workflow, /workflow_dispatch:/); - 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]*linux, arch: amd64/); - assert.match(workflow, /windows-2022[\s\S]*windows, arch: amd64/); - assert.match(workflow, /macos-14[\s\S]*darwin, arch: arm64/); - 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 value of [...workflow.matchAll(/timeout-minutes:\s*(\d+)/g)].map(m => Number(m[1]))) assert.ok(value <= 20); - assert.match(workflow, /NODE_OPTIONS: --max-old-space-size=384/); -}); - -test("tree digest is deterministic and rejects links", () => { - fs.mkdirSync(os.tmpdir(), { recursive: true }); - const root = fs.mkdtempSync(path.join(os.tmpdir(), "milestone-a-tree-")); - try { - fs.mkdirSync(path.join(root, "b")); fs.writeFileSync(path.join(root, "b", "z"), "z"); - fs.writeFileSync(path.join(root, "a"), "a"); - assert.deepEqual(harness.tree(root).map(row => row[0]), ["a", "b/z"]); - fs.symlinkSync(path.join(root, "a"), path.join(root, "link")); - assert.throws(() => harness.tree(root), /non-regular/); - } finally { fs.rmSync(root, { recursive: true, force: true }); } -}); - -function escape(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +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 repo=path.resolve(__dirname,"../../.."),workflow=fs.readFileSync(path.join(repo,".github/workflows/authoring-milestone-a-e2e.yml"),"utf8"); +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",gitHead: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); +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(v==='init')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'){data={revision:${JSON.stringify(options.resultRevision===null?null:(options.resultRevision||HEAD))},operation:'add',target_id:'codex',target_root:p.join(process.env.CODEX_HOME,'plugins','planned')};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,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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);assert.deepEqual(receipt.commands,harness.COMMANDS); + 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","inspect","test","add"]);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)}}); +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)}}); +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("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/);}); From cb7e7cc8fbf7292a3082a48dec2bc44be3e56848 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 05:22:31 +0000 Subject: [PATCH 06/18] fix(authoring): distinguish security feed access Refs #216 Refs #208 --- .github/workflows/authoring-milestone-a-e2e.yml | 6 ++++-- npm/agentplugins/scripts/milestone-a-e2e.js | 10 ++++++---- npm/agentplugins/test/milestone-a-e2e.test.js | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index b2ffe925..1b234700 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -45,8 +45,10 @@ jobs: modcache="$RUNNER_TEMP/milestone-a-modcache-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" test ! -e "$modcache" mkdir -m 700 "$modcache" - # Tool acquisition precedes the offline candidate builder. Runtime - # journeys never receive network access or this module cache. + # 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 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],repo:process.env.GITHUB_WORKSPACE,expectedHead:process.env.EXPECTED_HEAD,go:process.argv[3],modCache:process.argv[4],node:process.execPath,npm:process.argv[5]}))' \ "$config" "$root" "$(command -v go)" "$modcache" "$(command -v npm)" diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index d54cb9ae..f95810da 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -15,13 +15,15 @@ function ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}: ${r.stderr||r.st 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","project","client"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}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"])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 prepare(c){const root=absolute("root",c.root),repo=absolute("repo",c.repo);fresh(root);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={repository:"777genius/universal-agent-plugins",commit:head,versions:{agentplugins:"2.0.0","plugin-kit-ai":"2.0.0"}},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",candidate_head:head,candidate_sha256:built.manifest_sha256,asset_scope:"six-platform-pair",packages:packed.packs,registry_fallback:false,publication:false};fs.writeFileSync(path.join(root,"evidence","prepare.json"),JSON.stringify(receipt,null,2)+"\n");return receipt} +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-2.0.0.tgz":"plugin-kit-ai-2.0.0.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 prepare(c){const root=absolute("root",c.root),repo=absolute("repo",c.repo);fresh(root);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={repository:"777genius/universal-agent-plugins",commit:head,versions:{agentplugins:"2.0.0","plugin-kit-ai":"2.0.0"}},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",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} function provenance(ep,head){const real=fs.realpathSync(ep);let d=path.dirname(real),m;while(d!==path.dirname(d)){const x=path.join(d,"package.json");if(fs.existsSync(x)){m=x;break}d=path.dirname(d)}if(!m)fail(`entrypoint has no package provenance: ${ep}`);const p=JSON.parse(fs.readFileSync(m)),release=path.join(path.dirname(m),"private-release.json");let field,revision;if(fs.existsSync(release)){revision=JSON.parse(fs.readFileSync(release)).identity?.commit;field="private-release.json#identity.commit"}else{field=["gitHead","commit","revision"].find(k=>p[k]!==undefined)||(p.build?.revision!==undefined?"build.revision":null);revision=field?.startsWith("build")?p.build.revision:p[field]}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 product labels and the per-entrypoint journey-root provenance vary. -function normalize(v,project){const journey=path.dirname(project),x=JSON.parse(JSON.stringify(v).split(journey).join(""));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} +// 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 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);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={},assertions=[];let primary,receipt; - try{for(const p of PRODUCTS){const j=makeJourney(root,p);roots[p]=j.root;const env={...envFor(j),UAP_PRIVATE_NPM_CANDIDATE:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux")reports[p].push(result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`));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`)}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project")))]));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:"codex",fixture_roots:Object.fromEntries(PRODUCTS.map(p=>[p,path.join(roots[p],"client")])),provenances,reports,trees,registry_fallback:false,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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} + try{for(const p of PRODUCTS){const j=makeJourney(root,p);roots[p]=j.root;const env={...envFor(j),UAP_PRIVATE_NPM_CANDIDATE:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux")reports[p].push(result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`));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`)}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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:"codex",fixture_roots:Object.fromEntries(PRODUCTS.map(p=>[p,path.join(roots[p],"client")])),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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,COMMANDS,TARGETS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index 50a0dcf2..f08575ad 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -17,7 +17,7 @@ if(${JSON.stringify(options.mismatch||false)}&&product==='plugin-kit-ai'&&v==='v process.stdout.write(JSON.stringify({schema_version:1,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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);assert.deepEqual(receipt.commands,harness.COMMANDS); +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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);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); 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","inspect","test","add"]);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)}}); for(const [name,opts,pattern] of [ ["normalized result mismatch",{mismatch:true},/command JSON differs/], @@ -27,4 +27,7 @@ for(const [name,opts,pattern] of [ ])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)}}); 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 ['["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/);}); From c326159f6863ab012f1118aeb2810404a5ad8af8 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 05:39:00 +0000 Subject: [PATCH 07/18] fix(authoring): validate installer E2E evidence Refs #216 Refs #208 --- npm/agentplugins/scripts/milestone-a-e2e.js | 15 +++++++++++++-- npm/agentplugins/test/milestone-a-e2e.test.js | 8 +++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index f95810da..ba26d746 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -13,7 +13,7 @@ function snapshot(root,ex=[]){const omit=new Set(ex.map(x=>path.resolve(x))),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 ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}: ${r.stderr||r.stdout}`);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","project","client"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}fs.writeFileSync(path.join(o.client,"config.toml"),"# isolated disposable Codex fixture\n");return o} +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","project"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}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"])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-2.0.0.tgz":"plugin-kit-ai-2.0.0.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}} @@ -22,8 +22,19 @@ function provenance(ep,head){const real=fs.realpathSync(ep);let d=path.dirname(r // 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){ + const data=v?.data,assessment=data?.security,plan=data?.result?.plan,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(!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"); + if(plan?.client_id!=="codex")fail("local add did not resolve target exactly to codex"); + 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(plan); + 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}}; +} 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);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={},assertions=[];let primary,receipt; - try{for(const p of PRODUCTS){const j=makeJourney(root,p);roots[p]=j.root;const env={...envFor(j),UAP_PRIVATE_NPM_CANDIDATE:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux")reports[p].push(result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`));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`)}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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:"codex",fixture_roots:Object.fromEntries(PRODUCTS.map(p=>[p,path.join(roots[p],"client")])),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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} + 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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,COMMANDS,TARGETS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index f08575ad..008ff447 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -1,6 +1,7 @@ "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"); 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",gitHead:options.packageRevision||HEAD}));const ep=path.join(bin,`${product}.js`);fs.writeFileSync(ep,`#!/usr/bin/env node @@ -11,13 +12,13 @@ if(!required.every(k=>process.env[k]&&process.env[k].startsWith(p.dirname(proces 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(v==='init')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'){data={revision:${JSON.stringify(options.resultRevision===null?null:(options.resultRevision||HEAD))},operation:'add',target_id:'codex',target_root:p.join(process.env.CODEX_HOME,'plugins','planned')};if(data.revision===null)delete data.revision;} +if(v==='add'){const tree=${JSON.stringify(D)},manifest=${JSON.stringify(R)};data={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'},result:{installation_id:'',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'}]},requires_confirmation:true,mutated:false}};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||"")})data.result.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,result:'success',data})+'\\n');`);fs.chmodSync(ep,0o755);entrypoints[product]=ep} +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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);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); +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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);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]),["init","validate","inspect","test","add"]);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)}}); for(const [name,opts,pattern] of [ ["normalized result mismatch",{mismatch:true},/command JSON differs/], @@ -25,6 +26,7 @@ for(const [name,opts,pattern] of [ ["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/],["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);}); From 4899dde9286cd9608c62700d19a5a360481da75e Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 08:49:50 +0300 Subject: [PATCH 08/18] docs(authoring): define accelerated milestone A gate Refs #216 Refs #208 --- ...ST_AUTHORING_ENGINE_IMPLEMENTATION_PLAN.md | 334 +++++++++++++++--- 1 file changed, 285 insertions(+), 49 deletions(-) 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 From 9ab6d3d93d9c27a0c903b309536e6047e51fb21c Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 06:02:14 +0000 Subject: [PATCH 09/18] fix(authoring): align exact candidate identity Refs #240 Refs #216 Refs #208 --- .github/workflows/authoring-milestone-a-e2e.yml | 6 +++++- npm/agentplugins/scripts/milestone-a-e2e.js | 14 +++++++++----- npm/agentplugins/test/milestone-a-e2e.test.js | 4 ++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index 1b234700..0d21a0cb 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -40,7 +40,11 @@ jobs: 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" @@ -50,7 +54,7 @@ jobs: # 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 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],repo:process.env.GITHUB_WORKSPACE,expectedHead:process.env.EXPECTED_HEAD,go:process.argv[3],modCache:process.argv[4],node:process.execPath,npm:process.argv[5]}))' \ + node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],precreatedRoot:true,repo:process.env.GITHUB_WORKSPACE,expectedHead:process.env.EXPECTED_HEAD,go:process.argv[3],modCache:process.argv[4],node:process.execPath,npm:process.argv[5]}))' \ "$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 diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index ba26d746..0a87f002 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -3,11 +3,13 @@ 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"]; 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 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 fresh(root){absolute("root",root);if(fs.existsSync(root))fail("Milestone A root must be new");fs.mkdirSync(root,{mode:0o700});for(const n of ["evidence","journeys"])fs.mkdirSync(path.join(root,n),{mode:0o700})} +function mode700(file,label){if((fs.lstatSync(file).mode&0o777)!==0o700)fail(`${label} must have mode 0700`)} +function fresh(root,precreated=false){absolute("root",root);if(precreated){if(!fs.existsSync(root))fail("precreated Milestone A root is missing");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");mode700(path.join(root,"evidence"),"Milestone A evidence root")}else{if(fs.existsSync(root))fail("Milestone A root must be new");fs.mkdirSync(root,{mode:0o700});fs.mkdirSync(path.join(root,"evidence"),{mode:0o700})}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} @@ -15,9 +17,11 @@ function ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}: ${r.stderr||r.st 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","project"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}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"])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-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz")),package_registry_or_latest_acquisition:false,npm_tarball_install_offline:true}} +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 prepare(c){const root=absolute("root",c.root),repo=absolute("repo",c.repo);fresh(root);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={repository:"777genius/universal-agent-plugins",commit:head,versions:{agentplugins:"2.0.0","plugin-kit-ai":"2.0.0"}},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",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} +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;while(d!==path.dirname(d)){const x=path.join(d,"package.json");if(fs.existsSync(x)){m=x;break}d=path.dirname(d)}if(!m)fail(`entrypoint has no package provenance: ${ep}`);const p=JSON.parse(fs.readFileSync(m)),release=path.join(path.dirname(m),"private-release.json");let field,revision;if(fs.existsSync(release)){revision=JSON.parse(fs.readFileSync(release)).identity?.commit;field="private-release.json#identity.commit"}else{field=["gitHead","commit","revision"].find(k=>p[k]!==undefined)||(p.build?.revision!==undefined?"build.revision":null);revision=field?.startsWith("build")?p.build.revision:p[field]}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} @@ -34,7 +38,7 @@ function addProof(v,codexRoot){ 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}}; } 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);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={},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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};let ep=c.entrypoints?.[p];if(!ep){const tgz=p==="agentplugins"?"universal-agent-plugins-2.0.0.tgz":"plugin-kit-ai-2.0.0.tgz";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} + 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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};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";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,COMMANDS,TARGETS}; +module.exports={prepare,consume,main,tree,snapshot,normalize,inside,candidateIdentity,COMMANDS,TARGETS,VERSIONS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index 008ff447..f5fa8f1f 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -3,6 +3,9 @@ const test=require("node:test"),assert=require("node:assert/strict"),fs=require( 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("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",gitHead: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'); @@ -33,3 +36,4 @@ test("Windows normalization is root-specific, case-aware, and separator-aware",( 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 ['["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/);}); From 6214f700e10604361b9f456dae7b988c2ce97adb Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 06:19:56 +0000 Subject: [PATCH 10/18] fix(authoring): canonicalize candidate tool paths Refs #240 Refs #216 Refs #208 --- .github/workflows/authoring-milestone-a-e2e.yml | 2 +- npm/agentplugins/scripts/milestone-a-e2e.js | 5 ++++- npm/agentplugins/test/milestone-a-e2e.test.js | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index 0d21a0cb..10aafe85 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -54,7 +54,7 @@ jobs: # 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 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],precreatedRoot:true,repo:process.env.GITHUB_WORKSPACE,expectedHead:process.env.EXPECTED_HEAD,go:process.argv[3],modCache:process.argv[4],node:process.execPath,npm:process.argv[5]}))' \ + 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 diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index 0a87f002..0203e914 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -5,8 +5,11 @@ const producer=require("./stage-dual-authoring-candidate"),packer=require("./sta 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 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 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 fresh(root,precreated=false){absolute("root",root);if(precreated){if(!fs.existsSync(root))fail("precreated Milestone A root is missing");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");mode700(path.join(root,"evidence"),"Milestone A evidence root")}else{if(fs.existsSync(root))fail("Milestone A root must be new");fs.mkdirSync(root,{mode:0o700});fs.mkdirSync(path.join(root,"evidence"),{mode:0o700})}fs.mkdirSync(path.join(root,"journeys"),{mode:0o700})} @@ -41,4 +44,4 @@ function consume(c){const root=absolute("root",c.root),input=absolute("input",c. 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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};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";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,candidateIdentity,COMMANDS,TARGETS,VERSIONS}; +module.exports={prepare,consume,main,tree,snapshot,normalize,inside,candidateIdentity,canonicalTool,resolveTrustedTools,COMMANDS,TARGETS,VERSIONS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index f5fa8f1f..dd8d4e60 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -5,6 +5,8 @@ const D="sha256:"+"a".repeat(64),R="sha256:"+"b".repeat(64),P="sha256:"+"c".repe 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",gitHead:options.packageRevision||HEAD}));const ep=path.join(bin,`${product}.js`);fs.writeFileSync(ep,`#!/usr/bin/env node @@ -37,3 +39,4 @@ test("negative Windows normalization preserves unrelated paths, similar roots, a 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 ['["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 canonicalizes trusted prepare tools before constructing config",()=>{assert.match(workflow,/resolveTrustedTools\(process\.argv\[3\],process\.argv\[5\]\)/);assert.doesNotMatch(workflow,/npm:process\.argv\[5\]/);}); From 69dee07007c98010541877fe58cbab1f1d4f1e22 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 06:36:38 +0000 Subject: [PATCH 11/18] fix(agentplugins): stabilize milestone A CI evidence Resolve staged private package provenance from the release descriptor and create portable, persistent evidence roots across runner platforms. Refs #216 Refs #208 --- .github/workflows/authoring-milestone-a-e2e.yml | 6 ++---- npm/agentplugins/scripts/milestone-a-e2e.js | 9 +++++---- npm/agentplugins/test/milestone-a-e2e.test.js | 5 ++++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index 10aafe85..5ec79973 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -105,13 +105,11 @@ jobs: set -euo pipefail outer="$RUNNER_TEMP/milestone-a-outer-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" test ! -e "$outer" - mkdir -m 700 "$outer" root="$outer/run" - mkdir -p "$root/evidence" printf 'MILESTONE_A_EVIDENCE=%s/evidence\n' "$root" >> "$GITHUB_ENV" - rmdir "$root/evidence" "$root" + node -e 'require("./npm/agentplugins/scripts/milestone-a-e2e").precreate(process.argv[1])' "$root" config="$RUNNER_TEMP/milestone-a-run.json" - node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],outerRoot:process.argv[3],input:process.argv[4],platform:process.argv[5],arch:process.argv[6],expectedHead:process.env.EXPECTED_HEAD}))' \ + node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],precreatedRoot:true,outerRoot:process.argv[3],input:process.argv[4],platform:process.argv[5],arch:process.argv[6],expectedHead:process.env.EXPECTED_HEAD}))' \ "$config" "$root" "$outer" "$RUNNER_TEMP/milestone-a-input" '${{ matrix.platform }}' '${{ matrix.arch }}' node npm/agentplugins/scripts/milestone-a-e2e.js run "$config" test -z "$(git status --porcelain)" diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index 0203e914..a86a5a68 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -12,7 +12,8 @@ function canonicalTool(label,input){let resolved;try{resolved=fs.realpathSync(ab function resolveTrustedTools(go,npm,node=process.execPath){return{go:canonicalTool("go",go),node:canonicalTool("node",node),npm:canonicalTool("npm CLI",npm)}} 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 fresh(root,precreated=false){absolute("root",root);if(precreated){if(!fs.existsSync(root))fail("precreated Milestone A root is missing");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");mode700(path.join(root,"evidence"),"Milestone A evidence root")}else{if(fs.existsSync(root))fail("Milestone A root must be new");fs.mkdirSync(root,{mode:0o700});fs.mkdirSync(path.join(root,"evidence"),{mode:0o700})}fs.mkdirSync(path.join(root,"journeys"),{mode:0o700})} +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} @@ -25,7 +26,7 @@ function securityNetwork(){return{production_security_checks_enabled:true,creden 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;while(d!==path.dirname(d)){const x=path.join(d,"package.json");if(fs.existsSync(x)){m=x;break}d=path.dirname(d)}if(!m)fail(`entrypoint has no package provenance: ${ep}`);const p=JSON.parse(fs.readFileSync(m)),release=path.join(path.dirname(m),"private-release.json");let field,revision;if(fs.existsSync(release)){revision=JSON.parse(fs.readFileSync(release)).identity?.commit;field="private-release.json#identity.commit"}else{field=["gitHead","commit","revision"].find(k=>p[k]!==undefined)||(p.build?.revision!==undefined?"build.revision":null);revision=field?.startsWith("build")?p.build.revision:p[field]}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}} +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)} @@ -40,8 +41,8 @@ function addProof(v,codexRoot){ 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(plan); 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}}; } -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);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={},assertions=[];let primary,receipt; +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={},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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};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";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,candidateIdentity,canonicalTool,resolveTrustedTools,COMMANDS,TARGETS,VERSIONS}; +module.exports={prepare,consume,main,tree,snapshot,normalize,inside,precreate,candidateIdentity,canonicalTool,resolveTrustedTools,COMMANDS,TARGETS,VERSIONS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index dd8d4e60..f084da33 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -9,7 +9,7 @@ test("trusted tool config resolves setup-node-style npm and Go symlinks to invoc 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",gitHead:options.packageRevision||HEAD}));const ep=path.join(bin,`${product}.js`);fs.writeFileSync(ep,`#!/usr/bin/env node + 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']; @@ -25,6 +25,8 @@ process.stdout.write(JSON.stringify({schema_version:1,...(v==='add'?{command:'ad 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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);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]),["init","validate","inspect","test","add"]);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})}}); for(const [name,opts,pattern] of [ ["normalized result mismatch",{mismatch:true},/command JSON differs/], ["reported candidate revision mismatch",{resultRevision:"1111111111111111111111111111111111111111"},/reported revision/], @@ -39,4 +41,5 @@ test("negative Windows normalization preserves unrelated paths, similar roots, a 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 ['["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,/MILESTONE_A_EVIDENCE=%s\/evidence[\s\S]*\.precreate\(process\.argv\[1\]\)/);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 canonicalizes trusted prepare tools before constructing config",()=>{assert.match(workflow,/resolveTrustedTools\(process\.argv\[3\],process\.argv\[5\]\)/);assert.doesNotMatch(workflow,/npm:process\.argv\[5\]/);}); From 25efeeef327846930ada2eec26810f76ca2919b4 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 06:49:14 +0000 Subject: [PATCH 12/18] fix(agentplugins): harden milestone A journey roots Refs #216 Refs #208 --- .github/workflows/authoring-milestone-a-e2e.yml | 9 ++++++--- npm/agentplugins/scripts/milestone-a-e2e.js | 5 +++-- npm/agentplugins/test/milestone-a-e2e.test.js | 5 ++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index 5ec79973..a4f3decd 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -106,11 +106,14 @@ jobs: outer="$RUNNER_TEMP/milestone-a-outer-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" test ! -e "$outer" root="$outer/run" - printf 'MILESTONE_A_EVIDENCE=%s/evidence\n' "$root" >> "$GITHUB_ENV" - node -e 'require("./npm/agentplugins/scripts/milestone-a-e2e").precreate(process.argv[1])' "$root" + 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")" + 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" node -e 'require("fs").writeFileSync(process.argv[1], JSON.stringify({root:process.argv[2],precreatedRoot:true,outerRoot:process.argv[3],input:process.argv[4],platform:process.argv[5],arch:process.argv[6],expectedHead:process.env.EXPECTED_HEAD}))' \ - "$config" "$root" "$outer" "$RUNNER_TEMP/milestone-a-input" '${{ matrix.platform }}' '${{ matrix.arch }}' + "$config" "$native_root" "$native_outer" "$native_input" '${{ matrix.platform }}' '${{ matrix.arch }}' node npm/agentplugins/scripts/milestone-a-e2e.js run "$config" test -z "$(git status --porcelain)" - name: Upload run evidence, including failures diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index a86a5a68..a43570dc 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -8,6 +8,7 @@ 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 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}`)} @@ -42,7 +43,7 @@ function addProof(v,codexRoot){ 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}}; } 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={},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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};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";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} + 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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};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";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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]=[];if(p==="agentplugins")fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{mode:0o700});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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,candidateIdentity,canonicalTool,resolveTrustedTools,COMMANDS,TARGETS,VERSIONS}; +module.exports={prepare,consume,main,tree,snapshot,normalize,inside,precreate,nativeAbsolute,candidateIdentity,canonicalTool,resolveTrustedTools,COMMANDS,TARGETS,VERSIONS}; diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index f084da33..d94368b8 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -14,6 +14,7 @@ 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)}&&product==='agentplugins'&&!fs.statSync(process.env.UAP_PRIVATE_NPM_CACHE).isDirectory())process.exit(18); 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(v==='init')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; @@ -27,6 +28,8 @@ test("consume executes ordered entrypoints in independent complete roots, compar 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","inspect","test","add"]);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("agentplugins private launcher receives an existing journey-local candidate cache",()=>{const f=fixture({requireCandidateCache:true});try{harness.consume(f.config);assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);}finally{dispose(f)}}); +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/], @@ -41,5 +44,5 @@ test("negative Windows normalization preserves unrelated paths, similar roots, a 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 ['["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,/MILESTONE_A_EVIDENCE=%s\/evidence[\s\S]*\.precreate\(process\.argv\[1\]\)/);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 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 canonicalizes trusted prepare tools before constructing config",()=>{assert.match(workflow,/resolveTrustedTools\(process\.argv\[3\],process\.argv\[5\]\)/);assert.doesNotMatch(workflow,/npm:process\.argv\[5\]/);}); From c5f57c2277397a1f5a9a2fb05690940eaae9ac85 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 06:59:32 +0000 Subject: [PATCH 13/18] fix(authoring): preserve absent init destination Refs #216 Refs #208 --- .github/workflows/authoring-milestone-a-e2e.yml | 5 +++-- npm/agentplugins/scripts/milestone-a-e2e.js | 2 +- npm/agentplugins/test/milestone-a-e2e.test.js | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index a4f3decd..621548ce 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -112,9 +112,10 @@ jobs: 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],platform:process.argv[5],arch:process.argv[6],expectedHead:process.env.EXPECTED_HEAD}))' \ - "$config" "$native_root" "$native_outer" "$native_input" '${{ matrix.platform }}' '${{ matrix.arch }}' - node npm/agentplugins/scripts/milestone-a-e2e.js run "$config" + "$native_config" "$native_root" "$native_outer" "$native_input" '${{ matrix.platform }}' '${{ matrix.arch }}' + node npm/agentplugins/scripts/milestone-a-e2e.js run "$native_config" test -z "$(git status --porcelain)" - name: Upload run evidence, including failures if: always() diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index a43570dc..32603956 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -20,7 +20,7 @@ function snapshot(root,ex=[]){const omit=new Set(ex.map(x=>path.resolve(x))),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 ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}: ${r.stderr||r.stdout}`);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","project"]){const k=n.replace("-","_");o[k]=path.join(j,n);fs.mkdirSync(o[k])}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 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"])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}} diff --git a/npm/agentplugins/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index d94368b8..7e37a9f8 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -14,9 +14,10 @@ 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)}&&product==='agentplugins'&&!fs.statSync(process.env.UAP_PRIVATE_NPM_CACHE).isDirectory())process.exit(18); +if(${JSON.stringify(options.requireCandidateCache||false)}&&product==='agentplugins'&&(!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(v==='init')fs.writeFileSync(p.join(project,'fixture.txt'),'same'); +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)};data={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'},result:{installation_id:'',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'}]},requires_confirmation:true,mutated:false}};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||"")})data.result.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'); @@ -28,7 +29,7 @@ test("consume executes ordered entrypoints in independent complete roots, compar 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","inspect","test","add"]);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("agentplugins private launcher receives an existing journey-local candidate cache",()=>{const f=fixture({requireCandidateCache:true});try{harness.consume(f.config);assert.deepEqual(fs.readdirSync(path.join(f.root,"journeys")),[]);}finally{dispose(f)}}); +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("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/], @@ -45,4 +46,5 @@ test("workflow uses real local packages and has no test entrypoint injection",() 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 canonicalizes trusted prepare tools before constructing config",()=>{assert.match(workflow,/resolveTrustedTools\(process\.argv\[3\],process\.argv\[5\]\)/);assert.doesNotMatch(workflow,/npm:process\.argv\[5\]/);}); From 4b3c5ece0a0df46662ab91e3ece121abfbfca3d6 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 07:14:34 +0000 Subject: [PATCH 14/18] fix(authoring): harden milestone A platform journeys Refs #216 Refs #208 --- .../workflows/authoring-milestone-a-e2e.yml | 26 +++++++++++++++++-- npm/agentplugins/scripts/milestone-a-e2e.js | 10 ++++--- npm/agentplugins/test/milestone-a-e2e.test.js | 7 ++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.github/workflows/authoring-milestone-a-e2e.yml b/.github/workflows/authoring-milestone-a-e2e.yml index 621548ce..63ec96e8 100644 --- a/.github/workflows/authoring-milestone-a-e2e.yml +++ b/.github/workflows/authoring-milestone-a-e2e.yml @@ -99,6 +99,19 @@ jobs: 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: | @@ -109,14 +122,23 @@ jobs: 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],platform:process.argv[5],arch:process.argv[6],expectedHead:process.env.EXPECTED_HEAD}))' \ - "$native_config" "$native_root" "$native_outer" "$native_input" '${{ matrix.platform }}' '${{ matrix.arch }}' + 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 diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index 32603956..11c18c0d 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -11,6 +11,7 @@ function absolute(n,v){if(!v||!path.isAbsolute(v)||path.resolve(v)!==v)fail(`${n 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} @@ -18,10 +19,11 @@ function fresh(root,precreated=false){absolute("root",root);if(precreated){if(!f 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 ok(r,l){if(r.status!==0)fail(`${l} exited ${r.status}: ${r.stderr||r.stdout}`);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"])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 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} @@ -43,7 +45,7 @@ function addProof(v,codexRoot){ 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}}; } 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={},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:path.join(input,"candidate"),UAP_PRIVATE_NPM_CACHE:path.join(j.cache,"candidate")};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";ok(run(process.platform==="win32"?"npm.cmd":"npm",["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]=[];if(p==="agentplugins")fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{mode:0o700});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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} + 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")};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]=[];if(p==="agentplugins")fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{mode:0o700});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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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,COMMANDS,TARGETS,VERSIONS}; +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/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index 7e37a9f8..c7fb21ca 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -17,6 +17,7 @@ if(!required.every(k=>process.env[k]&&process.env[k].startsWith(p.dirname(proces if(${JSON.stringify(options.requireCandidateCache||false)}&&product==='agentplugins'&&(!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(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)};data={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'},result:{installation_id:'',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'}]},requires_confirmation:true,mutated:false}};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||"")})data.result.plan.destination=${JSON.stringify(options.destination||"")}.replace('',process.env.CODEX_HOME).replace('',p.dirname(process.env.HOME));if(data.revision===null)delete data.revision;} @@ -30,6 +31,9 @@ test("consume executes ordered entrypoints in independent complete roots, compar 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("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("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/], @@ -42,9 +46,10 @@ test("outside sibling mutation is rejected",()=>{const f=fixture();const marker= 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 ['["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 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\]/);}); From 4e948787c62971ac42f3b1149819bbc7b703b021 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 11:56:59 +0000 Subject: [PATCH 15/18] fix(authoring): close cross-platform milestone E2E gaps Refs #216 Refs #208 --- .../internal/authoring/commands/commands.go | 4 +- .../authoring/commands/vertical_fix_test.go | 2 +- .../internal/authoring/project/project.go | 29 +++++++- .../authoring/project/project_test.go | 36 ++++++++++ .../internal/authoring/scaffold/apply.go | 11 ++- .../internal/authoring/scaffold/apply_test.go | 10 +-- .../authoring/scaffold/scaffold_test.go | 8 +-- .../authoring/scaffold/stage_windows_test.go | 4 +- .../bootstrap_directory_guard_windows_test.go | 2 +- .../adapters/packageview/contract_test.go | 55 ++++++++++++++ .../adapters/packageview/race_linux_test.go | 2 +- .../scratch_alias_stages_windows_test.go | 4 +- .../adapters/packageview/source_darwin.go | 48 +++++++++---- .../packageview/source_darwin_test.go | 71 ++++++++++++++++++- .../adapters/packageview/source_linux.go | 5 +- .../packageview/source_native_test.go | 4 +- .../packageview/source_unsupported.go | 18 ++--- .../adapters/packageview/source_windows.go | 8 ++- .../packageview/source_windows_test.go | 8 +-- .../agentplugins/adapters/packageview/view.go | 40 ++++++++++- npm/agentplugins/scripts/milestone-a-e2e.js | 2 +- .../scripts/private-npm/bootstrap.js | 20 +++++- npm/agentplugins/test/milestone-a-e2e.test.js | 1 + .../test/private-npm-bootstrap.test.js | 47 +++++++++++- 24 files changed, 379 insertions(+), 60 deletions(-) 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/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..566c7755 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/apply_test.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/apply_test.go @@ -39,7 +39,7 @@ 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 { + result, err := apply(ctx, planFor(t, "skill"), ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, _ *os.Root) error { if err := validate(ctx, s); err != nil { return err } @@ -169,7 +169,7 @@ 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 { + result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, _ *os.Root) error { if err := validate(ctx, s); err != nil { return err } @@ -238,7 +238,7 @@ 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 { + result, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, _ *os.Root) error { if err := validate(ctx, s); err != nil { return err } @@ -315,7 +315,7 @@ 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 { + result, err := Apply(context.Background(), planFor(t, "skill"), ApplyOptions{Destination: filepath.Join(parent, "out"), Validate: func(ctx context.Context, s string, _ *os.Root) error { if err := validate(ctx, s); err != nil { return err } @@ -428,7 +428,7 @@ 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, _ *os.Root) error { container = filepath.Dir(s); return validate(ctx, s) }}, ops) var cleanup *CleanupError if !errors.As(err, &cleanup) || !result.Committed || result.Destination != dest { t.Fatalf("lost committed result: %+v %v", result, err) 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..de9c30b1 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, _ *os.Root) error { calls++ if filepath.Dir(filepath.Dir(s)) != root || s == dest { t.Fatal("not private sibling staging") @@ -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,7 +526,7 @@ 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 { + r, err := Apply(context.Background(), p, ApplyOptions{Destination: dest, Validate: func(ctx context.Context, s string, _ *os.Root) error { if err := validate(ctx, s); err != nil { return err } 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..1d19b5b0 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, _ *os.Root) error { descriptor, err := windows.GetNamedSecurityInfo(filepath.Dir(root), windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION) if err != nil { return err @@ -128,7 +128,7 @@ 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, _ *os.Root) error { validated = true return validate(ctx, root) }}) 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/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index 11c18c0d..606bc0db 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -45,7 +45,7 @@ function addProof(v,codexRoot){ 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}}; } 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={},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")};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]=[];if(p==="agentplugins")fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{mode:0o700});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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){const add=result(run(process.execPath,[ep,"add",".","--target=codex","--dry-run","--format=json"],{cwd:j.project,env}),`${p} local add --dry-run`);reports[p].push(add);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} + 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")};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]=[];if(p==="agentplugins")fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{mode:0o700});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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){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);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} 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/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/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index c7fb21ca..f02fe36a 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -34,6 +34,7 @@ test("init destination stays absent while the agentplugins launcher cache exists 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/], 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 { From b32b51171eed9a2644b80072cb09b8992a9f4d55 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 10 Sep 2026 12:03:35 +0000 Subject: [PATCH 16/18] test(authoring): forward generated staging handles Refs #216 Refs #208 --- .../internal/authoring/scaffold/apply_test.go | 24 +++++++++---------- .../authoring/scaffold/scaffold_test.go | 10 ++++---- .../authoring/scaffold/stage_windows_test.go | 8 +++---- 3 files changed, 21 insertions(+), 21 deletions(-) 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 566c7755..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, _ *os.Root) 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, _ *os.Root) 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, _ *os.Root) 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, _ *os.Root) 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, _ *os.Root) 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 de9c30b1..1a027f58 100644 --- a/cli/plugin-kit-ai/internal/authoring/scaffold/scaffold_test.go +++ b/cli/plugin-kit-ai/internal/authoring/scaffold/scaffold_test.go @@ -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, _ *os.Root) 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 @@ -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, _ *os.Root) 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 1d19b5b0..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, _ *os.Root) 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, _ *os.Root) 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). From 63bc23005ccb887d2e0b6e1159a8eaf43330dfb2 Mon Sep 17 00:00:00 2001 From: iliya Date: Fri, 11 Sep 2026 06:18:12 +0000 Subject: [PATCH 17/18] fix(authoring): repair Milestone A E2E harness add schema, npm cache roots, and Darwin packageview lifecycle - create the isolated private npm cache root for both public entrypoints (agentplugins and plugin-kit-ai), not only agentplugins, fixing an ENOENT in the plugin-kit-ai private npm launcher - validate the real single-target `add --dry-run` batch/group envelope (data.targets[0].output.result.plan.client_id) instead of the nonexistent data.result.plan shape, keeping full path containment and security-evidence checks over the batch structure - read Darwin `validate` through a harness-owned read-only APFS view (hdiutil UDRO image plus read-only mount) built from the just-generated project, since the packageview profile requires a genuine read-only local source and GeneratedStaging only covers the in-process init handle - ensure a Darwin hdiutil detach failure is never silently lost: it is combined with any pre-existing primary error and surfaces in both the thrown error and the evidence JSON, and root/mount/image cleanup plus journey and outer-mutation checks still run for every product even when one products detach fails Refs #216 Refs #208 --- npm/agentplugins/scripts/milestone-a-e2e.js | 29 +++++++++--- npm/agentplugins/test/milestone-a-e2e.test.js | 45 +++++++++++++++++-- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index 606bc0db..06d42c0c 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -34,18 +34,37 @@ function provenance(ep,head){const real=fs.realpathSync(ep);let d=path.dirname(r 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){ - const data=v?.data,assessment=data?.security,plan=data?.result?.plan,digest=/^sha256:[0-9a-f]{64}$/; + // 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"); - if(plan?.client_id!=="codex")fail("local add did not resolve target exactly to codex"); - 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(plan); + 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}}; } -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={},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")};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]=[];if(p==="agentplugins")fs.mkdirSync(env.UAP_PRIVATE_NPM_CACHE,{mode:0o700});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`));for(const cmd of ["validate",...(c.platform==="linux"?["inspect","test"]:[])])reports[p].push(result(run(process.execPath,argv([cmd,j.project,"--format=json"]),{env}),`${p} ${cmd}`));if(c.platform==="linux"){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);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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].map(r=>normalize(r,path.join(roots[p],"project"),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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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{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");const f=path.join(root,"evidence",primary?"failure.json":"run.json");if(fs.existsSync(f)){const r=JSON.parse(fs.readFileSync(f));r.cleanup="complete";r.clean_root_separation=new Set(Object.values(roots)).size===PRODUCTS.length;fs.writeFileSync(f,JSON.stringify(r,null,2)+"\n")}}} +// 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"){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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index f02fe36a..9a8a2952 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -14,13 +14,14 @@ 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)}&&product==='agentplugins'&&(!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.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)};data={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'},result:{installation_id:'',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'}]},requires_confirmation:true,mutated:false}};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||"")})data.result.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(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} @@ -31,6 +32,44 @@ test("consume executes ordered entrypoints in independent complete roots, compar 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",()=>{const f=fixture();try{const receipt=harness.consume(f.config);assert.equal(receipt.fixture_target_id,"codex");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),addArgv=rows.map(x=>x.argv).find(a=>(a[0]==="author"?a[1]:a[0])==="add");assert.ok(addArgv);const security=receipt.security_assessments[p];assert.equal(security.scanner.id,"lintai");assert.equal(security.subject.tree_digest,D);assert.equal(security.subject.manifest_digest,R);}}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)/);}); @@ -42,7 +81,7 @@ for(const [name,opts,pattern] of [ ["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/],["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)}}); +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);}); From ddee2e8407f9d20f8941f392c9ba3acd8bcee63c Mon Sep 17 00:00:00 2001 From: iliya Date: Fri, 11 Sep 2026 06:42:53 +0000 Subject: [PATCH 18/18] fix(authoring): scope Milestone A local add dry-run to agentplugins only plugin-kit-ai has no add operation in v2 (it is authoring-only); the harness previously sent the same add --dry-run call to both public entrypoints inside the shared per-product loop. This was masked by an earlier addProof schema bug that threw before the loop ever reached the plugin-kit-ai iteration; once that schema bug was fixed, the real exact-head linux-amd64 CI lane exposed the actual v1_operation_unavailable failure for plugin-kit-ai add. - gate the add --dry-run invocation, addProof call, and the per-product security-proof assertion to agentplugins only on linux - keep full, unweakened cross-product JSON parity checking for every command both entrypoints genuinely share (init, validate, inspect, test) by comparing only the shared-length report prefix - stop fabricating or defaulting fixture_roots/security_assessments for plugin-kit-ai in the run receipt; that evidence is now genuinely absent for that product instead of crashing or being copied Refs #216 Refs #208 --- npm/agentplugins/scripts/milestone-a-e2e.js | 2 +- npm/agentplugins/test/milestone-a-e2e.test.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/npm/agentplugins/scripts/milestone-a-e2e.js b/npm/agentplugins/scripts/milestone-a-e2e.js index 06d42c0c..1dce4c1e 100644 --- a/npm/agentplugins/scripts/milestone-a-e2e.js +++ b/npm/agentplugins/scripts/milestone-a-e2e.js @@ -64,7 +64,7 @@ function darwinPackageView(root,projectDir){ 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"){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}:security-proof`]:[]))}const norm=Object.fromEntries(PRODUCTS.map(p=>[p,reports[p].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"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].fixture_target_root])):{},security_assessments:c.platform==="linux"?Object.fromEntries(PRODUCTS.map(p=>[p,proofs[p].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}} + 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/test/milestone-a-e2e.test.js b/npm/agentplugins/test/milestone-a-e2e.test.js index 9a8a2952..c2050906 100644 --- a/npm/agentplugins/test/milestone-a-e2e.test.js +++ b/npm/agentplugins/test/milestone-a-e2e.test.js @@ -27,13 +27,13 @@ if(${JSON.stringify(options.mismatch||false)}&&product==='plugin-kit-ai'&&v==='v 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.notEqual(receipt.fixture_roots.agentplugins,receipt.fixture_roots["plugin-kit-ai"]);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]),["init","validate","inspect","test","add"]);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 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",()=>{const f=fixture();try{const receipt=harness.consume(f.config);assert.equal(receipt.fixture_target_id,"codex");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),addArgv=rows.map(x=>x.argv).find(a=>(a[0]==="author"?a[1]:a[0])==="add");assert.ok(addArgv);const security=receipt.security_assessments[p];assert.equal(security.scanner.id,"lintai");assert.equal(security.subject.tree_digest,D);assert.equal(security.subject.manifest_digest,R);}}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');