diff --git a/.github/workflows/authoring-native.yml b/.github/workflows/authoring-native.yml index 4b7ccbd5..0d8ca77b 100644 --- a/.github/workflows/authoring-native.yml +++ b/.github/workflows/authoring-native.yml @@ -179,3 +179,78 @@ jobs: path: ${{ env.NATIVE_ROOT }}/evidence if-no-files-found: error retention-days: 14 + + packed: + name: Authoring / packed Linux amd64 + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + EXPECTED_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + PYTHONDONTWRITEBYTECODE: '1' + steps: + - name: Resolve disposable packed evidence path + shell: bash + run: | + set -euo pipefail + # Publish the path before checkout/setup so always() consumers retain it. + # Leave creation (0700) and stale-root rejection to run-packed-ci.py. + printf 'PACKED_ROOT=%s/authoring-packed-%s-%s\n' "$RUNNER_TEMP" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" >> "$GITHUB_ENV" + - 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' + architecture: x64 + cache: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '22.23.2' + # Use bundled npm 10.9.8; no separate package-manager migration. + check-latest: false + - name: Build, pack, execute, seal and plan at the exact checkout SHA + shell: bash + run: python3 -B scripts/run-packed-ci.py "$PACKED_ROOT" "$EXPECTED_HEAD" + - name: Require terminal packed evidence + if: always() + shell: bash + run: python3 -B scripts/check-packed-ci.py "$PACKED_ROOT" "$EXPECTED_HEAD" + - name: Preserve packed evidence including failure logs and sealed inputs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: authoring-packed-${{ github.run_id }}-${{ github.run_attempt }} + path: | + ${{ env.PACKED_ROOT }}/** + !${{ env.PACKED_ROOT }}/modules/** + !${{ env.PACKED_ROOT }}/orchestrator/** + !${{ env.PACKED_ROOT }}/planner/** + include-hidden-files: true + if-no-files-found: error + retention-days: 14 + + acceptance: + name: Authoring / required native and packed + needs: [native, packed] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + env: + REQUIRED_RESULTS: ${{ toJSON(needs) }} + PYTHONDONTWRITEBYTECODE: '1' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Check mandatory graph and focused negative controls + shell: bash + run: | + set -euo pipefail + python3 -B scripts/check-packed-workflow.py + python3 -B -m unittest discover -s scripts -p 'test_packed_ci.py' + - name: Require every native lane and packed job to succeed + if: always() + shell: bash + run: python3 -B scripts/check-packed-workflow.py --results "$REQUIRED_RESULTS" diff --git a/cli/plugin-kit-ai/internal/authoring/commands/packed_acceptance_test.go b/cli/plugin-kit-ai/internal/authoring/commands/packed_acceptance_test.go new file mode 100644 index 00000000..5c9df840 --- /dev/null +++ b/cli/plugin-kit-ai/internal/authoring/commands/packed_acceptance_test.go @@ -0,0 +1,136 @@ +//go:build packedci + +package commands_test + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +type packedInputs struct { + Repo string + Projects []packedProject + Snapshots []struct{ Root string } +} + +func TestPackedGeneratedPackagesReachExistingInstallerPlanner(t *testing.T) { + config := os.Getenv("UAP_PACKED_INSTALLER_CONFIG") + if config == "" { + for _, key := range []string{"UAP_PACKED_INSTALLER_CONFIG_SHA256", "UAP_PACKED_INSTALLER_COMMIT", "UAP_PACKED_INSTALLER_NODE", "UAP_PACKED_INSTALLER_OUTPUT"} { + if os.Getenv(key) != "" { + t.Fatalf("partial opt-in: %s without config", key) + } + } + t.Fatal("packedci requires terminal packed-native input") + } + if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { + t.Fatal("packed bridge requires Linux amd64") + } + _, file, _, _ := runtime.Caller(0) + repo := filepath.Clean(filepath.Join(filepath.Dir(file), "../../../../..")) + script := filepath.Join(repo, "npm/agentplugins/scripts/packed-installer-bridge.js") + node, digest, commit, output := os.Getenv("UAP_PACKED_INSTALLER_NODE"), os.Getenv("UAP_PACKED_INSTALLER_CONFIG_SHA256"), os.Getenv("UAP_PACKED_INSTALLER_COMMIT"), os.Getenv("UAP_PACKED_INSTALLER_OUTPUT") + if !filepath.IsAbs(node) || !filepath.IsAbs(config) || !filepath.IsAbs(output) || filepath.Clean(output) != output || len(digest) != 64 || len(commit) != 40 { + t.Fatal("complete absolute opt-in and identity pins required") + } + git := exec.Command("/usr/bin/git", "rev-parse", "HEAD") + git.Dir = repo + head, err := git.Output() + if err != nil || strings.TrimSpace(string(head)) != commit { + t.Fatalf("planner checkout is not intended commit: %v", err) + } + git = exec.Command("/usr/bin/git", "status", "--porcelain=v1", "--untracked-files=all") + git.Dir = repo + status, err := git.Output() + if err != nil || len(status) != 0 { + t.Fatalf("packed acceptance requires clean integrated checkout: %v\n%s", err, status) + } + verify := func() []byte { + t.Helper() + cmd := exec.Command(node, script, "verify", config, digest, commit) + var stderr bytes.Buffer + cmd.Stderr = &stderr + b, err := cmd.Output() + if err != nil { + t.Fatalf("sealed native intake: %v\n%s", err, stderr.String()) + } + return b + } + before := verify() + var inputs packedInputs + if err := json.Unmarshal(before, &inputs); err != nil { + t.Fatal(err) + } + if inputs.Repo != repo || len(inputs.Projects) != 10 { + t.Fatal("wrong source checkout or incomplete project matrix") + } + wantProjects := map[string]bool{} + for _, product := range []string{"agentplugins", "plugin-kit-ai"} { + for _, lane := range []string{"skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"} { + wantProjects[product+"/"+lane] = true + } + } + seenSources := map[string]bool{} + for _, p := range inputs.Projects { + key := p.Product + "/" + p.Lane + if !wantProjects[key] || seenSources[p.Source] { + t.Fatal("unexpected or duplicate packed project") + } + delete(wantProjects, key) + seenSources[p.Source] = true + } + for _, s := range inputs.Snapshots { + if within(s.Root, output) || within(output, s.Root) { + t.Fatal("bridge output overlaps input") + } + } + if within(repo, output) || within(filepath.Dir(config), output) { + t.Fatal("keep bridge output outside checkout and config directory") + } + var plans []map[string]any + for _, p := range inputs.Projects { + t.Run(p.Product+"/"+p.Lane, func(t *testing.T) { plans = append(plans, packedPlanner(t, p)...) }) + } + if !bytes.Equal(before, verify()) { + t.Fatal("native inputs changed during planning") + } + if t.Failed() { + return + } + if len(plans) != 30 { + t.Fatal("exactly thirty packed plans required") + } + tuples := map[string]bool{} + for _, plan := range plans { + key := fmt.Sprintf("%s/%s/%s", plan["product"], plan["lane"], plan["target"]) + if tuples[key] { + t.Fatal("duplicate packed plan tuple") + } + tuples[key] = true + } + record := map[string]any{"kind": "packed-generated-existing-injected-installer-planner", "commit": commit, "config_sha256": digest, "inputs": json.RawMessage(before), "plans": plans, "release_eligible": false, "platform_acceptance": false, "attested": false} + b, err := json.MarshalIndent(record, "", " ") + if err != nil { + t.Fatal(err) + } + // Exclusive terminal evidence only after all 30 plans and preservation checks. + if resolved, err := filepath.EvalSymlinks(filepath.Dir(output)); err != nil || resolved != filepath.Dir(output) { + t.Fatal("unsafe output parent") + } + f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + t.Fatal(err) + } + _, err = f.Write(append(b, '\n')) + closeErr := f.Close() + if err != nil || closeErr != nil { + t.Fatalf("write evidence: %v %v", err, closeErr) + } +} diff --git a/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go b/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go index 16109919..84d809f6 100644 --- a/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go +++ b/cli/plugin-kit-ai/internal/authoring/commands/packed_installer_test.go @@ -1,7 +1,6 @@ package commands_test -// Packed acceptance is opt-in and consumes ROOT's completed, pinned npm run. -// The separate source harness below tests this seam, never packed acceptance. +// Shared planner fixtures remain in ordinary native coverage. import ( "bytes" "context" @@ -11,7 +10,6 @@ import ( "fmt" "io/fs" "os" - "os/exec" "path/filepath" "reflect" "runtime" @@ -32,100 +30,6 @@ import ( ) type packedProject struct{ Product, Lane, Source string } -type packedInputs struct { - Repo string - Projects []packedProject - Snapshots []struct{ Root string } -} - -func TestPackedGeneratedPackagesReachExistingInstallerPlanner(t *testing.T) { - config := os.Getenv("UAP_PACKED_INSTALLER_CONFIG") - if config == "" { - for _, key := range []string{"UAP_PACKED_INSTALLER_CONFIG_SHA256", "UAP_PACKED_INSTALLER_COMMIT", "UAP_PACKED_INSTALLER_NODE", "UAP_PACKED_INSTALLER_OUTPUT"} { - if os.Getenv(key) != "" { - t.Fatalf("partial opt-in: %s without config", key) - } - } - t.Skip("ROOT-owned terminal packed-native input required") - } - if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { - t.Fatal("packed bridge requires Linux amd64") - } - _, file, _, _ := runtime.Caller(0) - repo := filepath.Clean(filepath.Join(filepath.Dir(file), "../../../../..")) - script := filepath.Join(repo, "npm/agentplugins/scripts/packed-installer-bridge.js") - node, digest, commit, output := os.Getenv("UAP_PACKED_INSTALLER_NODE"), os.Getenv("UAP_PACKED_INSTALLER_CONFIG_SHA256"), os.Getenv("UAP_PACKED_INSTALLER_COMMIT"), os.Getenv("UAP_PACKED_INSTALLER_OUTPUT") - if !filepath.IsAbs(node) || !filepath.IsAbs(config) || !filepath.IsAbs(output) || filepath.Clean(output) != output || len(digest) != 64 || len(commit) != 40 { - t.Fatal("complete absolute opt-in and identity pins required") - } - git := exec.Command("/usr/bin/git", "rev-parse", "HEAD") - git.Dir = repo - head, err := git.Output() - if err != nil || strings.TrimSpace(string(head)) != commit { - t.Fatalf("planner checkout is not intended commit: %v", err) - } - git = exec.Command("/usr/bin/git", "status", "--porcelain=v1", "--untracked-files=all") - git.Dir = repo - status, err := git.Output() - if err != nil || len(status) != 0 { - t.Fatalf("packed acceptance requires clean integrated checkout: %v\n%s", err, status) - } - verify := func() []byte { - t.Helper() - cmd := exec.Command(node, script, "verify", config, digest, commit) - var stderr bytes.Buffer - cmd.Stderr = &stderr - b, err := cmd.Output() - if err != nil { - t.Fatalf("sealed native intake: %v\n%s", err, stderr.String()) - } - return b - } - before := verify() - var inputs packedInputs - if err := json.Unmarshal(before, &inputs); err != nil { - t.Fatal(err) - } - if inputs.Repo != repo || len(inputs.Projects) != 10 { - t.Fatal("wrong source checkout or incomplete project matrix") - } - for _, s := range inputs.Snapshots { - if within(s.Root, output) || within(output, s.Root) { - t.Fatal("bridge output overlaps input") - } - } - if within(repo, output) || within(filepath.Dir(config), output) { - t.Fatal("keep bridge output outside checkout and config directory") - } - var plans []map[string]any - for _, p := range inputs.Projects { - t.Run(p.Product+"/"+p.Lane, func(t *testing.T) { plans = append(plans, packedPlanner(t, p)...) }) - } - if !bytes.Equal(before, verify()) { - t.Fatal("native inputs changed during planning") - } - if t.Failed() { - return - } - record := map[string]any{"kind": "packed-generated-existing-injected-installer-planner", "commit": commit, "config_sha256": digest, "inputs": json.RawMessage(before), "plans": plans, "release_eligible": false, "platform_acceptance": false, "attested": false} - b, err := json.MarshalIndent(record, "", " ") - if err != nil { - t.Fatal(err) - } - // Exclusive terminal evidence only after all 30 plans and preservation checks. - if resolved, err := filepath.EvalSymlinks(filepath.Dir(output)); err != nil || resolved != filepath.Dir(output) { - t.Fatal("unsafe output parent") - } - f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) - if err != nil { - t.Fatal(err) - } - _, err = f.Write(append(b, '\n')) - closeErr := f.Close() - if err != nil || closeErr != nil { - t.Fatalf("write evidence: %v %v", err, closeErr) - } -} func within(root, p string) bool { return p == root || strings.HasPrefix(p, root+string(filepath.Separator)) @@ -316,8 +220,8 @@ func packedPlanner(t *testing.T, p packedProject) []map[string]any { } func TestPackedInstallerSourceHarness(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("disposable Linux source harness only") + if (runtime.GOOS != "linux" && runtime.GOOS != "windows") || (runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64") { + t.Skip("source harness requires supported Linux/Windows amd64/arm64 host") } author := commands.App{Projects: project.Service{Scratch: t.TempDir()}, Revision: publicRevision, PublicContract: true} for _, lane := range []string{"skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"} { diff --git a/npm/agentplugins/scripts/packed-installer-bridge.js b/npm/agentplugins/scripts/packed-installer-bridge.js index 8bc313b0..d87ee840 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.js @@ -67,7 +67,7 @@ function snapshot(root, allowContainedLinks = false) { function falseClaims(record) { for (const key of ["release_eligible", "platform_acceptance", "attested"]) assert.equal(record[key], false, key); } -function intake(request) { +function privateIntake(request) { c.keys(request, REQUEST_KEYS, "bridge request"); assert.match(request.expectedCommit, /^[0-9a-f]{40}$/); assert.equal(request.disposableEvidence, true); @@ -149,6 +149,156 @@ function intake(request) { } return { identity: o.identity, repo: o.repo, candidate_sha256: o.manifestDigest, packs: native.packs, projects, snapshots }; } +// Public evidence is deliberately a separate schema and opt-in. Never translate +// its synthetic acquisition into private completion or authenticated promotion. +function publicInit(lane) { + assert.ok(LANES.includes(lane)); + const template = lane.startsWith("hybrid-") ? "hybrid" : lane; + return ["init", lane, `--template=${template}`, + ...(lane.endsWith("remote") ? ["--url=https://docs.example.com/mcp"] : lane.endsWith("stdio") ? ["--runtime=node"] : []), + ...(template === "hybrid" ? ["--mcp-template=mcp-" + lane.split("-")[1]] : [])]; +} +function publicEvidence(cfg, native, configPath) { + c.keys(cfg, ["prepare", "completionDigest", "evidenceOutput"], "public config"); + const o = cfg.prepare, v = o.candidate; + c.keys(o, ["candidate", "repo", "node", "npm", "output", "projectionPins", "pairMarkerDigest"], "public preparation"); + c.keys(v, ["candidate", "root", "identity", "manifestDigest", "go", "workParent", "assetScope", "authoringMode", "outputs", "pairMarker"], "public candidate"); + c.identity(v.identity); assert.equal(v.candidate, true); + assert.equal(v.assetScope, "six-platform-pair"); assert.equal(v.authoringMode, MODE); + c.keys(native, ["schema", "status", "identity", "candidate_sha256", "completion_sha256", "config_sha256", + "pair_marker_sha256", "projection_pins", "fixtureRoot", "target", "packs", "binaries", "installations", "tools", + "invocations", "invocations_sha256", "downloads_sha256", "result_sha256", "projects", "trees", + "fixture_acquisition_execution", "qualification", "signed_promotion", "public_eligible", "release_eligible", + "platform_acceptance", "attested", "runtime_evidence"], "public terminal"); + assert.equal(native.schema, "dual-authoring-public-native/v1"); assert.equal(native.status, "completed"); + falseClaims(native); assert.equal(native.signed_promotion, false); assert.equal(native.public_eligible, false); + assert.equal(native.qualification, null); assert.equal(native.fixture_acquisition_execution, true); + assert.equal(native.runtime_evidence, "not_evaluated"); assert.equal(native.target, "linux-amd64"); + assert.deepEqual(native.identity, v.identity); assert.equal(native.candidate_sha256, v.manifestDigest); + pin(configPath, native.config_sha256); + assert.equal(native.completion_sha256, cfg.completionDigest); + pin(path.join(o.output, "completion.json"), cfg.completionDigest); + const prep = json(path.join(o.output, "completion.json")); + c.keys(prep, ["schema", "identity", "candidate_sha256", "projection_pins", "pair_marker_sha256", "wrapper_blobs", + "generated", "packs", "tools", "qualification", "release_eligible", "platform_acceptance", "attested"], "public preparation completion"); + assert.equal(prep.schema, "dual-authoring-public-preparation/v1"); falseClaims(prep); assert.equal(prep.qualification, null); + assert.deepEqual(prep.identity, v.identity); assert.equal(prep.candidate_sha256, v.manifestDigest); + assert.equal(native.pair_marker_sha256, o.pairMarkerDigest); assert.equal(prep.pair_marker_sha256, o.pairMarkerDigest); + pin(v.pairMarker, o.pairMarkerDigest); + assert.deepEqual(native.projection_pins, o.projectionPins); assert.deepEqual(prep.projection_pins, o.projectionPins); + const frozen = c.frozenCandidate(v.root, v.identity, v.manifestDigest, v.assetScope, MODE); + const pair = json(v.pairMarker); + assert.deepEqual(pair, { schema: "authoring-release-pair/v1", status: "CANDIDATE", identity: v.identity, + candidate_sha256: v.manifestDigest, authoring_mode: MODE, asset_scope: v.assetScope, products: o.projectionPins, + release_eligible: false, platform_acceptance: false, attested: false }); + const roots = [o.repo, v.root, o.output, cfg.evidenceOutput, ...Object.values(v.outputs)]; + for (const root of [...roots, v.workParent, native.fixtureRoot]) c.safeDirectory(absolute(root)); + assert.equal(path.dirname(native.fixtureRoot), v.workParent); + assert.match(path.basename(native.fixtureRoot), /^dual-authoring-[A-Za-z0-9]+$/); + const disjoint = [...roots, native.fixtureRoot]; + for (let i = 0; i < disjoint.length; i++) for (const other of disjoint.slice(i + 1)) { + const a = disjoint[i].toLowerCase(), b = other.toLowerCase(); + assert.ok(a !== b && !a.startsWith(b + "/") && !b.startsWith(a + "/"), "overlapping public roots"); + } + c.keys(native.tools, ["node", "npm", "go", "producer_node"], "public tools"); + for (const key of ["node", "npm", "go", "producer_node"]) { + const tool = native.tools[key]; + c.keys(tool, key === "producer_node" ? ["path", "sha256", "version"] : ["path", "sha256"], "tool pin"); + assert.equal(tool.path, key === "go" ? v.go : key === "producer_node" ? o.node : o[key]); pin(tool.path, tool.sha256); + if (key === "node" || key === "npm") assert.deepEqual(tool, prep.tools[key]); + } + assert.match(native.tools.producer_node.version, /^v22\./); + assert.equal(native.tools.go.sha256, frozen.manifest.build.go_sha256); + // Source closure hashes are checked against the planner checkout as well as + // sealed preparation. The Go gate independently requires its exact clean SHA. + assert.ok(Object.keys(prep.wrapper_blobs).length > 0); + for (const [name, record] of Object.entries(prep.wrapper_blobs)) { + const file = path.join(o.repo, name); inside(o.repo, file); pin(file, record.sha256); + } + const invocationPath = path.join(cfg.evidenceOutput, "invocations.json"); pin(invocationPath, native.invocations_sha256); + pin(path.join(cfg.evidenceOutput, "downloads.log"), native.downloads_sha256); + pin(path.join(cfg.evidenceOutput, "result.json"), native.result_sha256); + assert.deepEqual(json(path.join(cfg.evidenceOutput, "result.json")), { source: v.identity.commit, + fixture_acquisition_execution: true, signed_promotion: false, public_eligible: false, runtime_evidence: "not_evaluated" }); + const invocations = json(invocationPath), expected = [], projects = []; + function command(product, argv, status = 0, author = false) { + expected.push({ product, argv: [...(author && product === "agentplugins" ? ["author"] : []), ...argv], status, author }); + } + for (const product of PRODUCTS) { + for (const map of [native.packs, native.binaries, native.projects, native.trees, prep.packs, v.outputs, o.projectionPins]) c.keys(map, PRODUCTS, "public product map"); + const projection = v.outputs[product], manifestPath = path.join(projection, "release-manifest.json"); + pin(manifestPath, o.projectionPins[product].manifest_sha256); + pin(path.join(projection, "checksums.txt"), o.projectionPins[product].checksums_sha256); + const manifest = json(manifestPath); + assert.deepEqual(manifest, { schema_version: 3, status: "CANDIDATE", product, repository: v.identity.repository, + tag: product === "agentplugins" ? `agentplugins-v${v.identity.versions[product]}` : `v${v.identity.versions[product]}`, + version: v.identity.versions[product], commit: v.identity.commit, engine_revision: v.identity.engine_revision, + versions: v.identity.versions, candidate_sha256: v.manifestDigest, authoring_mode: MODE, asset_scope: v.assetScope, + assets: frozen.manifest.products[product].assets, release_eligible: false, platform_acceptance: false, attested: false }); + assert.equal(c.readFile(path.join(projection, "checksums.txt")).toString(), + [...Object.values(manifest.assets).map(a => `${a.sha256} ${a.file}`), `${hash(manifestPath)} release-manifest.json`].join("\n") + "\n"); + const asset = manifest.assets[native.target]; + pin(path.join(projection, asset.file), asset.sha256); + const binary = native.binaries[product]; c.keys(binary, ["path", "sha256", "size"], "executed binary"); + inside(native.fixtureRoot, binary.path); pin(binary.path, asset.binary.sha256); + assert.equal(binary.sha256, asset.binary.sha256); assert.equal(binary.size, asset.binary.size); + const pack = native.packs[product]; c.keys(pack, ["file", "sha256", "size", "integrity"], "executed pack"); + const file = `${product === "agentplugins" ? "universal-agent-plugins" : product}-${v.identity.versions[product]}.tgz`; + assert.equal(pack.file, path.join(cfg.evidenceOutput, product, file)); + pin(pack.file, pack.sha256); const bytes = c.readFile(pack.file); + assert.equal(bytes.length, pack.size); assert.equal(pack.integrity, "sha512-" + crypto.createHash("sha512").update(bytes).digest("base64")); + assert.notEqual(pack.sha256, prep.packs[product].sha256, "preparation pack substituted for executed fixture"); + pin(path.join(o.output, file), prep.packs[product].sha256); + const parent = native.projects[product]; assert.equal(parent, path.join(native.fixtureRoot, `${product} projects ü`)); + assert.deepEqual(fs.readdirSync(parent).sort(), [...LANES].sort()); + assert.deepEqual(native.trees[product], snapshot(parent)); + command(product, ["version", "--format=json"]); command(product, ["--help"]); + command(product, ["version", "--format=json"], 0, true); command(product, ["--help", "--format=json"], 0, true); + for (const lane of LANES) { + const source = path.join(parent, lane); c.readFile(path.join(source, "plugin.json")); + c.readFile(path.join(source, "skills/extra-skill/SKILL.md")); projects.push({ product, lane, source }); + for (const args of [publicInit(lane), ["skills", "init", "extra-skill", source, "--description=Disposable fixture."], + ["skills", "validate", source], ...["validate", "inspect", "test"].map(n => [n, source])]) command(product, [...args, "--format=json"], 0, true); + } + } + command("plugin-kit-ai", ["update", "--all", "--format=json"], 2); + command("agentplugins", ["add", path.join(native.projects.agentplugins, "skill"), "--target=codex", "--dry-run", "--format=json"]); + assert.equal(native.invocations, expected.length); assert.equal(invocations.length, expected.length); + invocations.forEach((row, i) => { + c.keys(row, ["product", "argv", "status", "signal", "stdout", "stderr"], "public invocation"); + const want = expected[i]; assert.equal(row.product, want.product); assert.deepEqual(row.argv, want.argv); + assert.equal(row.status, want.status); assert.equal(row.signal, null); assert.equal(row.stderr, ""); assert.equal(typeof row.stdout, "string"); + if (want.author) { + const result = JSON.parse(row.stdout); assert.equal(result.result, "success"); assert.equal(result.schema_version, 1); + assert.equal(result.data.revision, v.identity.commit); assert.equal(result.data.engine, "standard-first-slice/1"); + if (row.argv.includes("init")) assert.equal(result.data.committed, true); + } + }); + const installs = [...PRODUCTS, ...PRODUCTS, "agentplugins"]; // independent/shared/reinstall + assert.equal(native.installations.length, installs.length); + native.installations.forEach((row, i) => { + c.keys(row, ["product", "argv", "status", "signal", "pack_sha256"], "public installation"); + const p = installs[i], prefix = path.join(native.fixtureRoot, i < 2 ? `${p} independent prefix` : "shared prefix ü"); + assert.equal(row.product, p); assert.equal(row.status, 0); assert.equal(row.signal, null); + assert.equal(row.pack_sha256, native.packs[p].sha256); + assert.deepEqual(row.argv, [o.npm, "install", "--global", "--prefix", prefix, "--offline", "--ignore-scripts", "--no-audit", "--no-fund", native.packs[p].file]); + }); + return { identity: v.identity, repo: o.repo, candidate_sha256: v.manifestDigest, packs: native.packs, projects, + snapshots: [...roots.slice(1).map(root => snapshot(root)), snapshot(native.fixtureRoot, true)], + public_evidence: { schema: native.schema, signed_promotion: false, public_eligible: false, qualification: null, + pair_marker_sha256: native.pair_marker_sha256, tools: native.tools, binaries: native.binaries } }; +} +function intake(request) { + if (!Object.hasOwn(request, "intake")) return privateIntake(request); + c.keys(request, [...REQUEST_KEYS, "intake"], "public bridge request"); + assert.equal(request.intake, "public-fixture/v1"); assert.equal(request.disposableEvidence, true); + pin(request.nativeConfig, request.nativeConfigSha256); + const cfg = json(request.nativeConfig); + const terminal = path.join(cfg.evidenceOutput, "public-native-completion.json"); pin(terminal, request.nativeCompletionSha256); + const native = json(terminal); + assert.equal(native.identity.commit, request.expectedCommit); assert.equal(native.fixtureRoot, request.fixtureRoot); + return publicEvidence(cfg, native, request.nativeConfig); +} function seal(request) { const inputs = intake(request); return { schema: "packed-installer-bridge/v1", request, verifier_sha256: hash(__filename), helper_sha256: hash(require.resolve("./dual-authoring-candidate")), inputs, @@ -163,21 +313,24 @@ function verify(configFile, configDigest, expectedCommit) { assert.deepEqual(cfg, seal(cfg.request), "sealed input changed"); return cfg.inputs; } +function publishSeal(request, output) { + const result = seal(request); absolute(output); + for (const root of [result.inputs.repo, request.fixtureRoot, ...result.inputs.snapshots.map(s => s.root)]) { + const a = output.toLowerCase(), b = root.toLowerCase(); + assert.ok(a !== b && !a.startsWith(b + "/") && !b.startsWith(a + "/"), "output overlaps evidence/source"); + } + c.safeDirectory(path.dirname(output)); + fs.writeFileSync(output, c.encode(result), { flag: "wx", mode: 0o600 }); + return hash(output); +} if (require.main === module) { try { const [command, file, digest, commit] = process.argv.slice(2); if (command === "seal" && process.argv.length === 5) { - const request = json(file), result = seal(request), output = absolute(digest); - // Keep config/evidence outside every observed root; exclusive publication. - for (const root of [request.fixtureRoot, ...result.inputs.snapshots.map(s => s.root)]) { - assert.ok(output !== root && !output.startsWith(root + "/"), "output overlaps evidence"); - } - c.safeDirectory(path.dirname(output)); - fs.writeFileSync(output, c.encode(result), { flag: "wx", mode: 0o600 }); - process.stdout.write(hash(output) + "\n"); + process.stdout.write(publishSeal(json(file), digest) + "\n"); } else if (command === "verify" && process.argv.length === 6) { process.stdout.write(c.encode(verify(file, digest, commit))); } else throw new Error("usage: node packed-installer-bridge.js seal REQUEST OUTPUT | verify CONFIG SHA256 COMMIT"); } catch (error) { process.stderr.write(`packed installer bridge: ${error.message}\n`); process.exitCode = 1; } } -module.exports = { LANES, PRODUCTS, intake, seal, verify, snapshot }; +module.exports = { LANES, PRODUCTS, intake, seal, verify, snapshot, publicInit, publicEvidence, publishSeal }; diff --git a/npm/agentplugins/scripts/packed-installer-bridge.md b/npm/agentplugins/scripts/packed-installer-bridge.md index 285f2716..ca52892f 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.md +++ b/npm/agentplugins/scripts/packed-installer-bridge.md @@ -1,4 +1,4 @@ -# ROOT's packed-generated project → injected installer bridge +# Packed-generated project → injected installer bridge This is a separate, opt-in **test boundary**, following a successful terminal `private-npm-native.test.js` run at the final integrated commit. It never builds @@ -8,11 +8,9 @@ The existing `add --dry-run` command constructs its real planner; only detection security assessment and lifecycle/state interfaces are injected. Fake Cursor, Codex and Claude directories exist only in Go test temporary directories. -The accepted input is ROOT's actual `native-completion.json`, independently pinned -along with its native config. Failed64b2 has no completion and is not input. -Partial invocations, synthetic test records and source-generated harness results -are never packed-native acceptance. Integrate the native oracle fix and these -four new files before the final native run. This worker did not rebuild a candidate. +The accepted input is the same-run actual `native-completion.json`, independently +pinned along with its native config. Partial invocations, synthetic test records +and source-generated harness results are never packed-native acceptance. ## Run after the final native gate @@ -82,28 +80,29 @@ namespace contract, not protection against a hostile concurrent same-UID writer. external directory from config and all observed inputs. Example from checkout: ```sh - env -i PATH=/var/data/uap-authoring-test-20260906/toolchain/go/bin:/usr/bin:/bin \ + env -i PATH="$(dirname "$GO")":/usr/bin:/bin \ HOME="$PRIVATE_HOME" USERPROFILE="$PRIVATE_HOME" APPDATA="$PRIVATE_HOME" \ LOCALAPPDATA="$PRIVATE_HOME" XDG_CONFIG_HOME="$PRIVATE_HOME" \ XDG_DATA_HOME="$PRIVATE_HOME" XDG_STATE_HOME="$PRIVATE_HOME" \ XDG_CACHE_HOME="$PRIVATE_CACHE" TMPDIR="$PRIVATE_TMP" TMP="$PRIVATE_TMP" TEMP="$PRIVATE_TMP" \ GOCACHE="$PRIVATE_CACHE/go-build" \ - GOMODCACHE=/tmp/uap-authoring-native-integration-old-20260906-artifacts/windows-modules \ + GOMODCACHE="$PRIVATE_MODULE_CACHE" \ GOPROXY=off GOSUMDB=off GOENV=off GOTOOLCHAIN=local GOMAXPROCS=2 \ UAP_PACKED_INSTALLER_NODE="$NODE" \ UAP_PACKED_INSTALLER_CONFIG=/absolute/bridge-config/sealed.json \ UAP_PACKED_INSTALLER_CONFIG_SHA256="$BRIDGE_SHA256" \ UAP_PACKED_INSTALLER_COMMIT="$FINAL_COMMIT" \ UAP_PACKED_INSTALLER_OUTPUT=/absolute/bridge-results/completion.json \ - go test -p=2 ./cli/plugin-kit-ai/internal/authoring/commands \ + "$GO" test -p=2 -tags=packedci ./cli/plugin-kit-ai/internal/authoring/commands \ -run '^TestPackedGeneratedPackagesReachExistingInstallerPlanner$' -count=1 -v ``` Create the private directories and result parent first. `NODE` is an absolute trusted installed Node 22/24 path. Go checks HEAD and clean tracked/untracked state; don't use `-trimpath`, `-overlay`, alternate build flags or source copies. - Do not set private directories inside the native fixture. No opt-in config - skips the test; a partial opt-in fails. A skip is not acceptance. + Do not set private directories inside the native fixture. The `packedci` tag + exposes acceptance only; absent or partial configuration fails. Ordinary + coverage retains the source harness on Linux/Windows amd64/arm64. Require exit 0, **ten passing project subtests**, and the exclusive result JSON with **30 reports** (two products × five templates × three fake targets). Each @@ -135,3 +134,102 @@ Fixtures are retained under private TMPDIR for diagnosis. fixtures using the existing public authoring test helper, including extra-skill, and exercises the same planner seam for 15 plans. It writes no packed result. Use the same offline/private environment above, without any packed opt-in vars. + +## Mandatory bounded CI + +`authoring-native.yml` adds a separate Linux amd64 packed job, with its own clean +exact-SHA checkout and 45-minute ceiling. It runs `scripts/run-packed-ci.py +"$PACKED_ROOT" "$EXPECTED_HEAD"` after setup. `PACKED_ROOT` must be absent, +absolute and outside checkout. The runner resolves installed Go, Node and npm's +JavaScript CLI; no root-host tool or cache paths are embedded. Go is 1.25.13, +Node is **22.23.2**, and npm is its bundled **10.9.8**, with no npm upgrade. +ROOT selected this pair from the fresh official Node release index (2026-07-28 +security release). Versions and tool SHA256 hashes, including stager Node, are +recorded. The private candidate uses the accepted release-contract fixture +versions agentplugins 0.1.91 and plugin-kit-ai 2.0.0; these are not publication. + +Relevant Go dependencies warm in a job-owned module cache. Subsequent commands +use allowlisted environments, private profiles, GOPROXY/GOSUMDB off and local +Go tooling. This is an offline command contract, not an OS network sandbox. +Both real npm packs/installations and both named native tests must pass with +741 invocations (739 distinct records; two repeated journeys are intentional). +The canonical seal pins those exact ten projects. Tagged Go discovery must name +`TestPackedGeneratedPackagesReachExistingInstallerPlanner` exactly once before +uncached execution. Ten project leaves and thirty unique product/lane/target +plans must pass; optional product grouping events do not count as leaves. + +The read-only terminal checker rejects missing phase exits, skipped named tests, +truncated transcripts, identity/pin/count/tuple mismatches and missing or true +claim fields where declared by each schema. It checks post-planner verification +of the unchanged seal. Failure artifacts retain logs and sealed inputs; the +artifact digest index preserves relationships. Downloaded archives are audit +material, not a fresh verification of relocated live trees. + +The always-running aggregate requires **native and packed success**. Its focused +structural controls reject missing/conditional jobs, dependencies, matrix lanes +and explicit tags. All four native lanes and all five source-harness lanes +remain mandatory. Existing Windows concurrent-init failure remains a blocker; +no retry or restricted diagnostic substitutes for that gate. PR167/169 and the +writable macOS owner gate remain upstream requirements. Required-check settings +are external: source controls do not prove GitHub enforcement. ROOT must run the +new integrated SHA and independently inspect its jobs/artifacts and required +check settings before CI acceptance. Release eligibility, platform acceptance +and attestation remain false. Local synthetic/source tests are not this run. + +### Explicit public fixture intake + +Public preparation is **not** private native completion. The public producer +`test/public-authoring-native.test.js` now emits an exclusive +`public-native-completion.json` (`dual-authoring-public-native/v1`) only after +both products finish five lanes, `extra-skill`, static parity, isolation, cache +recovery, and lifecycle checks. Its 70 authoring/installer invocation records +are ordered and complete. Five successful npm installations separately bind +actual fixture tarball paths and SHA256 values (independent prefixes, shared +prefix, and reinstall). Preparation tarballs remain separate, unqualified +inputs; substituting their hashes for executed packs is rejected. + +The terminal binds the configuration, preparation completion, source/engine +identity, candidate, pair marker, projections/checksums, tools, cached native +binaries, invocation/download/result logs, all ten project locators, and full +generated-tree inventories including modes and empty directories. Native binary +and tarball contents are pinned; supplied executables are never launched by the +bridge. The public producer's preceding native verification and the final +clean-SHA Go gate remain required execution/source proof. The bridge checks +read-only byte consistency; it does not authenticate an arbitrary supplied log. + +Use the existing six request fields (`expectedCommit`, `nativeConfig`, +`nativeConfigSha256`, `nativeCompletionSha256`, `fixtureRoot`, +`disposableEvidence`) with the additional exact field +`"intake": "public-fixture/v1"`. The completion digest must independently pin +`public-native-completion.json`. Omitting `intake` still selects the unchanged +strict private schema. Unknown intake values and public/private substitution +fail closed. Seal with the same `packed-installer-bridge.js seal REQUEST OUTPUT` +command and pass its digest to the existing tagged planner. Source/evidence +output overlap and existing seal/terminal destinations fail. + +For owner-scheduled final-source acceptance, the imported runner has an explicit +public consumption mode. It does not build or repeat the public native run: + +```text +python3 -B scripts/run-packed-ci.py --public OUTPUT FINAL_SHA OPTIONS +python3 -B scripts/check-packed-ci.py --public OUTPUT FINAL_SHA +``` + +`OPTIONS` is JSON with exactly `request` (the request object above), `nativeTap` +(absolute owner-terminal TAP path), `nativeTapSha256` (independent SHA256), `go`, +`node` (canonical absolute tools matching native completion), and `modCache` +(the existing offline module cache). `OUTPUT` must be new, external, and disjoint +from source and evidence. Both checker modes require exact named discovery, +ten projects, thirty unique Cursor/Codex/Claude plans, complete non-skipped +transcripts, and post-planner seal verification. Public planning is offline; +there is no public preparation/build/warmup fallback. The default private CI +runner and mandatory native/packed workflow graph remain intact. + +All public qualification, release, platform acceptance, attestation, signed +promotion and public eligibility claims remain false/null. Synthetic acquisition +proves fixture execution only. Historical f68 evidence cannot satisfy this +new schema or a later SHA and must not be enriched, repinned, or rerun here. +The final integrated source (including the independently owned preparation +initialization fix) still needs owner-scheduled public native execution and +packed planner acceptance. These focused synthetic tests do not satisfy that +E2E or the external release/required-check prerequisites. diff --git a/npm/agentplugins/scripts/packed-installer-bridge.test.js b/npm/agentplugins/scripts/packed-installer-bridge.test.js index 98db9215..1f2f763a 100644 --- a/npm/agentplugins/scripts/packed-installer-bridge.test.js +++ b/npm/agentplugins/scripts/packed-installer-bridge.test.js @@ -97,3 +97,73 @@ test("snapshot rejects links and captures empty files/directories and modes", po assert.equal(bridge.snapshot(root,true).entries.length,4); fs.unlinkSync(path.join(root,"link")); fs.symlinkSync("/etc/passwd",path.join(root,"link")); assert.throws(()=>bridge.snapshot(root,true)); }); + +function publicFixture(t) { + const f=fixture(t), cfg=JSON.parse(fs.readFileSync(f.nativeConfig)), old=cfg.stage; + const dir=name=>{const p=path.join(f.root,name);fs.mkdirSync(p);return p;}; + const outputs=Object.fromEntries(bridge.PRODUCTS.map(p=>[p,dir('projection-'+p)])), projectionPins={}, assets={}, binaries={}, packs={}; + const native=JSON.parse(fs.readFileSync(f.nativePath)), evidence=cfg.evidenceOutput, invocations=[]; + for(const p of bridge.PRODUCTS) { + const bytes=Buffer.from('synthetic binary '+p), asset={file:p+'.bin',sha256:c.digest(bytes),size:bytes.length,binary:{sha256:c.digest(bytes),size:bytes.length}}; + assets[p]={assets:{'linux-amd64':asset}}; + fs.writeFileSync(path.join(outputs[p],asset.file),bytes); + write(path.join(outputs[p],'release-manifest.json'),{schema_version:3,status:'CANDIDATE',product:p,repository:old.identity.repository,tag:p==='agentplugins'?`agentplugins-v${old.identity.versions[p]}`:`v${old.identity.versions[p]}`,version:old.identity.versions[p],versions:old.identity.versions,authoring_mode:old.authoringMode,asset_scope:'six-platform-pair',assets:assets[p].assets,commit:old.identity.commit,engine_revision:old.identity.commit,candidate_sha256:old.manifestDigest,release_eligible:false,platform_acceptance:false,attested:false}); + fs.writeFileSync(path.join(outputs[p],'checksums.txt'),`${asset.sha256} ${asset.file}\n${hash(path.join(outputs[p],'release-manifest.json'))} release-manifest.json\n`); + projectionPins[p]={manifest_sha256:hash(path.join(outputs[p],'release-manifest.json')),checksums_sha256:hash(path.join(outputs[p],'checksums.txt'))}; + const binary=path.join(f.fixtureRoot,p+'.bin');fs.writeFileSync(binary,bytes);binaries[p]={path:binary,sha256:asset.sha256,size:asset.size}; + const packdir=path.join(evidence,p);fs.mkdirSync(packdir);const pack=path.join(packdir,native.packs[p].file), body=Buffer.from('executed synthetic '+p);fs.writeFileSync(pack,body); + packs[p]={file:pack,sha256:c.digest(body),size:body.length,integrity:'sha512-'+crypto.createHash('sha512').update(body).digest('base64')}; + const parent=path.join(f.fixtureRoot,`${p} projects ü`);fs.renameSync(f.projects[p],parent);f.projects[p]=parent; + const add=(argv,author=false,status=0)=>invocations.push({product:p,argv:[...(author&&p==='agentplugins'?['author']:[]),...argv],status,signal:null,stderr:'',stdout:JSON.stringify({schema_version:1,result:'success',data:{engine:'standard-first-slice/1',revision:old.identity.commit,committed:true}})}); + add(['version','--format=json']);add(['--help']);add(['version','--format=json'],true);add(['--help','--format=json'],true); + for(const lane of bridge.LANES){ + const source=path.join(parent,lane);fs.mkdirSync(path.join(source,'skills'));fs.mkdirSync(path.join(source,'skills/extra-skill'));fs.writeFileSync(path.join(source,'skills/extra-skill/SKILL.md'),'synthetic'); + for(const args of [bridge.publicInit(lane),['skills','init','extra-skill',source,'--description=Disposable fixture.'],['skills','validate',source],...['validate','inspect','test'].map(n=>[n,source])]) add([...args,'--format=json'],true); + } + } + invocations.push({product:'plugin-kit-ai',argv:['update','--all','--format=json'],status:2,signal:null,stdout:'{}',stderr:''}, + {product:'agentplugins',argv:['add',path.join(f.projects.agentplugins,'skill'),'--target=codex','--dry-run','--format=json'],status:0,signal:null,stdout:'{}',stderr:''}); + const tool=path.join(f.root,'tool');fs.writeFileSync(tool,'synthetic tool');const toolpin={path:tool,sha256:hash(tool)}; + const candidate={candidate:true,root:f.candidate,identity:old.identity,manifestDigest:old.manifestDigest,go:tool,workParent:old.workParent,assetScope:'six-platform-pair',authoringMode:old.authoringMode,outputs,pairMarker:path.join(f.root,'pair.json')}; + write(candidate.pairMarker,{schema:'authoring-release-pair/v1',status:'CANDIDATE',identity:old.identity,candidate_sha256:old.manifestDigest,authoring_mode:old.authoringMode,asset_scope:candidate.assetScope,products:projectionPins,release_eligible:false,platform_acceptance:false,attested:false}); + const pairMarkerDigest=hash(candidate.pairMarker), prep={schema:'dual-authoring-public-preparation/v1',identity:old.identity,candidate_sha256:old.manifestDigest,projection_pins:projectionPins,pair_marker_sha256:pairMarkerDigest,wrapper_blobs:{'source.js':{sha256:c.digest(Buffer.from('synthetic source'))}},generated:{},packs:native.packs,tools:{node:toolpin,npm:toolpin},qualification:null,release_eligible:false,platform_acceptance:false,attested:false}; + fs.writeFileSync(path.join(old.repo,'source.js'),'synthetic source');write(f.completionPath,prep); + const publicCfg={prepare:{candidate,repo:old.repo,node:tool,npm:tool,output:old.output,projectionPins,pairMarkerDigest},completionDigest:hash(f.completionPath),evidenceOutput:evidence};write(f.nativeConfig,publicCfg); + write(path.join(evidence,'invocations.json'),invocations);fs.writeFileSync(path.join(evidence,'downloads.log'),'synthetic download'); + write(path.join(evidence,'result.json'),{source:old.identity.commit,fixture_acquisition_execution:true,signed_promotion:false,public_eligible:false,runtime_evidence:'not_evaluated'}); + const installations=[...bridge.PRODUCTS,...bridge.PRODUCTS,'agentplugins'].map((p,i)=>({product:p,argv:[tool,'install','--global','--prefix',path.join(f.fixtureRoot,i<2?`${p} independent prefix`:'shared prefix ü'),'--offline','--ignore-scripts','--no-audit','--no-fund',packs[p].file],status:0,signal:null,pack_sha256:packs[p].sha256})); + const terminal={schema:'dual-authoring-public-native/v1',status:'completed',identity:old.identity,candidate_sha256:old.manifestDigest,completion_sha256:hash(f.completionPath),config_sha256:hash(f.nativeConfig),pair_marker_sha256:pairMarkerDigest,projection_pins:projectionPins,fixtureRoot:f.fixtureRoot,target:'linux-amd64',packs,binaries,installations,tools:{node:toolpin,npm:toolpin,go:toolpin,producer_node:{...toolpin,version:'v22.21.1'}},invocations:invocations.length,invocations_sha256:hash(path.join(evidence,'invocations.json')),downloads_sha256:hash(path.join(evidence,'downloads.log')),result_sha256:hash(path.join(evidence,'result.json')),projects:f.projects,trees:Object.fromEntries(bridge.PRODUCTS.map(p=>[p,bridge.snapshot(f.projects[p])])),fixture_acquisition_execution:true,qualification:null,signed_promotion:false,public_eligible:false,release_eligible:false,platform_acceptance:false,attested:false,runtime_evidence:'not_evaluated'}; + f.nativePath=path.join(evidence,'public-native-completion.json');write(f.nativePath,terminal); + return {...f,terminal,assets,tool,request:{...f.request,intake:'public-fixture/v1',nativeConfigSha256:hash(f.nativeConfig),nativeCompletionSha256:hash(f.nativePath)}}; +} +test('SYNTHETIC public intake: exact lanes, executed packs, pins and schema separation',posixFixture,t=>{ + const original=c.frozenCandidate, f=publicFixture(t); + c.frozenCandidate=()=>({manifest:{products:f.assets,build:{go_sha256:hash(f.tool)}}}); + try { + const sealed=path.join(f.root,'sealed.json');const digest=bridge.publishSeal(f.request,sealed); + assert.throws(()=>bridge.publishSeal(f.request,sealed)); + assert.throws(()=>bridge.publishSeal(f.request,path.join(f.candidate,'overlap.json'))); + assert.throws(()=>bridge.publishSeal(f.request,path.join(f.root,'repo/overlap.json'))); + const verify=()=>bridge.verify(sealed,digest,f.request.expectedCommit);assert.equal(verify().projects.length,10); + for(const field of ['release_eligible','platform_acceptance','attested','signed_promotion','public_eligible']) for(const value of [true,undefined]) { + const bad={...f.terminal,[field]:value};write(f.nativePath,bad); + assert.throws(()=>bridge.seal({...f.request,nativeCompletionSha256:hash(f.nativePath)}),field); + } + for(const change of [{schema:'dual-authoring-npm-completion/v1'},{status:'running'},{qualification:{}},{invocations:69},{fixtureRoot:f.root},{identity:{...f.terminal.identity,engine_revision:'0'.repeat(40)}},{packs:{...f.terminal.packs,agentplugins:{...f.terminal.packs.agentplugins,sha256:JSON.parse(fs.readFileSync(f.completionPath)).packs.agentplugins.sha256}}},{projects:{...f.projects,agentplugins:f.projects['plugin-kit-ai']}},{installations:f.terminal.installations.slice(1)}]){ + write(f.nativePath,{...f.terminal,...change});assert.throws(()=>bridge.seal({...f.request,nativeCompletionSha256:hash(f.nativePath)})); + } + write(f.nativePath,f.terminal); + assert.throws(()=>bridge.seal({...f.request,expectedCommit:'0'.repeat(40)})); + assert.throws(()=>bridge.seal({...f.request,nativeConfigSha256:'0'.repeat(64)})); + const {intake,...privateRequest}=f.request;assert.throws(()=>bridge.seal(privateRequest));assert.throws(()=>bridge.seal({...f.request,intake:'private'})); + for(const file of [f.nativeConfig,f.completionPath,path.join(f.root,'pair.json'),path.join(f.root,'projection-agentplugins/checksums.txt'),path.join(f.root,'projection-agentplugins/release-manifest.json'),f.tool,f.terminal.packs.agentplugins.file,f.terminal.binaries.agentplugins.path,path.join(path.dirname(f.nativePath),'invocations.json'),path.join(f.projects.agentplugins,'skill/plugin.json')]){ + const before=fs.readFileSync(file);fs.appendFileSync(file,'changed');assert.throws(verify);fs.writeFileSync(file,before); + } + const inv=path.join(path.dirname(f.nativePath),'invocations.json'), before=fs.readFileSync(inv), rows=JSON.parse(before); + for(const bad of [rows.slice(1),[rows[0],...rows.slice(0,-1)],rows.map((r,i)=>i===5?{...r,status:1}:r)]){ + write(inv,bad);write(f.nativePath,{...f.terminal,invocations:bad.length,invocations_sha256:hash(inv)});assert.throws(()=>bridge.seal({...f.request,nativeCompletionSha256:hash(f.nativePath)})); + } + fs.writeFileSync(inv,before);write(f.nativePath,f.terminal);assert.equal(verify().projects.length,10); + } finally {c.frozenCandidate=original;} + assert.throws(()=>bridge.seal(f.request),'unstubbed candidate rejects synthetic bytes'); +}); diff --git a/npm/agentplugins/test/public-authoring-native.test.js b/npm/agentplugins/test/public-authoring-native.test.js index 3acaf071..4a6dccd9 100644 --- a/npm/agentplugins/test/public-authoring-native.test.js +++ b/npm/agentplugins/test/public-authoring-native.test.js @@ -10,6 +10,7 @@ const path = require("node:path"); const crypto = require("node:crypto"); const test = require("node:test"); const c = require("../scripts/dual-authoring-candidate"); +const bridge = require("../scripts/packed-installer-bridge"); const adapter = require("../scripts/authoring-release"); const packing = require("../scripts/stage-dual-authoring-npm"); const stager = require("../scripts/stage-authoring-npm"); @@ -28,7 +29,7 @@ function tree(root) { walk(root); return result; } -test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and lifecycle", () => { +test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and lifecycle", t => { const config = process.env.UAP_PUBLIC_AUTHORING_NATIVE_CONFIG; assert.ok(config && path.isAbsolute(config), "required UAP_PUBLIC_AUTHORING_NATIVE_CONFIG: exact integrated assets remain a coordinator gate"); const cfg = JSON.parse(c.readFile(config, 1024 * 1024)); @@ -36,6 +37,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and const o = cfg.prepare, candidate = o.candidate; const platform = require("../lib/platform").detectPlatform(); const target = platform.key; + assert.equal(target, "linux-amd64", "public packed intake is the bounded Linux lane"); c.outputPlacement(cfg.evidenceOutput, [o.repo, o.output, candidate.root, candidate.workParent]); const context = packing.npmContext(candidate.workParent); const env = { ...context.env, PATH: `${path.dirname(o.node)}:/usr/local/bin:/usr/bin:/bin` }; @@ -53,7 +55,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and for (const claim of ["release_eligible", "platform_acceptance", "attested"]) assert.equal(record[claim], false); assert.deepEqual(record.wrapper_blobs, Object.fromEntries(Object.entries(source).map(([n, { bytes, ...pin }]) => [n, pin]))); fs.mkdirSync(cfg.evidenceOutput, { mode: 0o700 }); - const bodies = {}, tarballs = {}, invocations = [], reports = {}, trees = {}; + const bodies = {}, tarballs = {}, packs = {}, binaries = {}, installations = [], invocations = [], reports = {}, trees = {}; const integrity = bytes => "sha512-" + crypto.createHash("sha512").update(bytes).digest("base64"); for (const p of c.PRODUCTS) { const manifestBytes = c.readFile(path.join(candidate.outputs[p], "release-manifest.json")); @@ -81,6 +83,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and const packed = packing.packPackage(p, files, root, { ...o, output, identity: candidate.identity }, { env, root: fs.mkdtempSync(path.join(context.root, "synthetic-pack-")) }); tarballs[p] = path.join(output, packed.file); + packs[p] = { ...packed, file: tarballs[p] }; const m = JSON.parse(manifestBytes), a = m.assets[target]; const body = c.readFile(path.join(candidate.outputs[p], a.file)); assert.equal(c.digest(body), a.sha256); @@ -88,11 +91,17 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and } const loader = path.join(context.root, "transport.cjs"), log = path.join(cfg.evidenceOutput, "downloads.log"); preload(loader, bodies, log); + function install(p, prefix, env) { + const argv = [o.npm, "install", "--global", "--prefix", prefix, "--offline", "--ignore-scripts", "--no-audit", "--no-fund", tarballs[p]]; + const result = run(o.node, argv, env, context.root); + installations.push({ product: p, argv, status: result.status, signal: result.signal, pack_sha256: c.digest(c.readFile(tarballs[p])) }); + ok(result); + } // Separate prefixes and homes prove neither native product needs its peer. for (const p of c.PRODUCTS) { const prefix = path.join(context.root, `${p} independent prefix`); const independent = environment({ ...context, env }, prefix, path.join(context.root, `${p} independent home`)); - ok(run(o.node, [o.npm, "install", "--global", "--prefix", prefix, "--offline", "--ignore-scripts", "--no-audit", "--no-fund", tarballs[p]], independent, context.root)); + install(p, prefix, independent); const packageRoot = path.join(prefix, "lib/node_modules", p === "agentplugins" ? "universal-agent-plugins" : p); const controlled = { ...independent, NODE_OPTIONS: `--require=${JSON.stringify(loader)}`, PATH: "/absent-peer-binary-path" }; if (p === "plugin-kit-ai") assert.equal(ok(run(o.node, [path.join(packageRoot, "lib/install.js")], controlled, context.root)), ""); @@ -102,7 +111,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and } const prefix = path.join(context.root, "shared prefix ü"), home = path.join(context.root, "consumer home ü"); const consumer = environment({ ...context, env }, prefix, home); - for (const p of c.PRODUCTS) ok(run(o.node, [o.npm, "install", "--global", "--prefix", prefix, "--offline", "--ignore-scripts", "--no-audit", "--no-fund", tarballs[p]], consumer, context.root)); + for (const p of c.PRODUCTS) install(p, prefix, consumer); const client = path.join(home, ".codex"), installer = path.join(context.root, "installer-state"); fs.mkdirSync(client); fs.mkdirSync(installer); fs.writeFileSync(path.join(client, "config.toml"), "# isolated synthetic target\n"); @@ -141,10 +150,11 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and for (const name of ["dev", "bootstrap", "normalize", "import", "export", "publish"]) { assert.ok(!JSON.stringify(help).includes(`"${name}"`), `deferred ${name} absent`); } - for (const lane of ["skill", "mcp-remote", "mcp-stdio"]) { - const extra = lane === "mcp-remote" ? ["--url=https://docs.example.com/mcp"] : lane === "mcp-stdio" ? ["--runtime=node"] : []; - assert.equal(author(p, ["init", lane, `--template=${lane}`, ...extra]).committed, true); + for (const lane of bridge.LANES) { + assert.equal(author(p, bridge.publicInit(lane)).committed, true); const project = path.join(projects[p], lane); + assert.equal(author(p, ["skills", "init", "extra-skill", project, "--description=Disposable fixture."]).committed, true); + author(p, ["skills", "validate", project]); for (const command of ["validate", "inspect", "test"]) { const d = author(p, [command, project]); assert.equal(d.runtime_evidence.status, "not_evaluated"); assert.ok(d.identity.tree_digest); @@ -171,6 +181,7 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and fs.writeFileSync(cached, "corrupt fixture cache"); ok(run(o.node, [bin(p), "version", "--format=json"], transportEnv, projects[p])); assert.equal(c.digest(c.readFile(cached)), release.asset.binary.sha256); + binaries[p] = { path: cached, sha256: release.asset.binary.sha256, size: c.readFile(cached).length }; } const offline = path.join(context.root, "offline.cjs"); preload(offline, {}, log, true); const offlineEnv = { ...transportEnv, NODE_OPTIONS: `--require=${JSON.stringify(offline)}` }; @@ -179,10 +190,28 @@ test("PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and ok(run(o.node, [o.npm, "uninstall", "--global", "--prefix", prefix, "--offline", "--ignore-scripts", "universal-agent-plugins"], consumer, context.root)); assert.deepEqual(fs.readFileSync(bin("plugin-kit-ai")), peerBytes); ok(run(o.node, [bin("plugin-kit-ai"), "version"], offlineEnv, projects["plugin-kit-ai"])); - ok(run(o.node, [o.npm, "install", "--global", "--prefix", prefix, "--offline", "--ignore-scripts", tarballs.agentplugins], consumer, context.root)); + install("agentplugins", prefix, consumer); ok(run(o.node, [bin("agentplugins"), "version"], offlineEnv, projects.agentplugins)); assert.deepEqual(tree(projects["plugin-kit-ai"]), before); assert.equal(fs.readFileSync(log, "utf8").trim().split("\n").length, 6, "independent/shared cold and corruption recovery downloads per product; warm is offline"); fs.writeFileSync(path.join(cfg.evidenceOutput, "result.json"), c.encode({ source: candidate.identity.commit, - fixture_acquisition_execution: true, signed_promotion: false, public_eligible: false, runtime_evidence: "not_evaluated" })); + fixture_acquisition_execution: true, signed_promotion: false, public_eligible: false, runtime_evidence: "not_evaluated" }), { flag: "wx" }); + for (const p of c.PRODUCTS) assert.deepEqual(bridge.LANES.map(lane => tree(path.join(projects[p], lane))), trees[p], "all generated projects preserved through lifecycle"); + const terminal = { schema: "dual-authoring-public-native/v1", status: "completed", + identity: candidate.identity, candidate_sha256: candidate.manifestDigest, completion_sha256: cfg.completionDigest, + config_sha256: c.digest(c.readFile(config)), pair_marker_sha256: o.pairMarkerDigest, projection_pins: o.projectionPins, + fixtureRoot: context.root, target, packs, binaries, installations, + tools: { ...record.tools, go: { path: candidate.go, sha256: c.digest(c.readFile(candidate.go)) }, + producer_node: { path: process.execPath, sha256: c.digest(c.readFile(process.execPath)), version: process.version } }, + invocations: invocations.length, invocations_sha256: c.digest(c.readFile(path.join(cfg.evidenceOutput, "invocations.json"))), + downloads_sha256: c.digest(c.readFile(log)), result_sha256: c.digest(c.readFile(path.join(cfg.evidenceOutput, "result.json"))), + projects, trees: Object.fromEntries(c.PRODUCTS.map(p => [p, bridge.snapshot(projects[p])])), + fixture_acquisition_execution: true, qualification: null, signed_promotion: false, public_eligible: false, + release_eligible: false, platform_acceptance: false, attested: false, runtime_evidence: "not_evaluated" }; + // Validate the full contract before exclusive terminal publication. No success + // marker exists if a lane, pin, or lifecycle assertion above failed. + bridge.publicEvidence(cfg, terminal, config); + const terminalPath = path.join(cfg.evidenceOutput, "public-native-completion.json"); + fs.writeFileSync(terminalPath, c.encode(terminal), { flag: "wx", mode: 0o600 }); + t.diagnostic("public native completion: " + JSON.stringify({ file: terminalPath, sha256: c.digest(c.readFile(terminalPath)), source: candidate.identity.commit })); }); diff --git a/scripts/check-authoring-native-results.py b/scripts/check-authoring-native-results.py index 7cf706bf..7cf418bf 100644 --- a/scripts/check-authoring-native-results.py +++ b/scripts/check-authoring-native-results.py @@ -249,6 +249,13 @@ def digest(path): scaffold_package = "github.com/777genius/plugin-kit-ai/cli/internal/authoring/scaffold" require((scaffold_package, "TestDeniedParent") in passed, "missing mandatory parent permission denial") command_package = "github.com/777genius/plugin-kit-ai/cli/internal/authoring/commands" + harness = "TestPackedInstallerSourceHarness" + require((command_package, harness) in discovered, "missing source harness discovery") + require(not any(t == "TestPackedGeneratedPackagesReachExistingInstallerPlanner" for _, t in discovered), + "packed acceptance must require explicit packedci tag") + for name in (harness, *(harness + "/" + lane for lane in + ("skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio"))): + require((command_package, name) in passed, "missing mandatory source harness: " + name) for contract in ("generated-skill", "skills-init", "static-validation", "duplicate-unchanged", "readiness", "strict-flags", "malformed-skill", "sdk-static-only"): test = "TestNativeBinaryLifecycle/" + contract diff --git a/scripts/check-packed-ci.py b/scripts/check-packed-ci.py new file mode 100644 index 00000000..f498381b --- /dev/null +++ b/scripts/check-packed-ci.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +"""Read-only terminal checks; successful same-SHA suites supply execution proof.""" +import base64 +from collections import Counter +import hashlib +import json +from pathlib import Path +import re +import stat +import sys + +VERSIONS = dict(go='go1.25.13', node='v22.23.2', npm='10.9.8') +MODE = 'release-cli-contract-v1' +PRODUCTS = ('agentplugins', 'plugin-kit-ai') +LANES = ('skill', 'mcp-remote', 'mcp-stdio', 'hybrid-remote', 'hybrid-stdio') +TARGETS = ('cursor', 'codex', 'claude') +NAME = 'TestPackedGeneratedPackagesReachExistingInstallerPlanner' +REGEX = '^' + NAME + '$' +PACKAGE_PATH = './cli/plugin-kit-ai/internal/authoring/commands' +PACKAGE = 'github.com/777genius/plugin-kit-ai/cli/internal/authoring/commands' +CANDIDATE_TEST = 'actual controlled Linux pair: offline verification, engine reports, frozen journeys and negative proof' +NATIVE_TESTS = ['native retirement oracle preserves original format tails and rejects output mismatches', + 'NATIVE opt-in: exact two Linux tarballs, five accepted template lanes and complete release journeys'] +PUBLIC_TEST = 'PUBLIC NATIVE: exact integrated packs, both actual bins, static parity and lifecycle' +CLAIMS = ('release_eligible', 'platform_acceptance', 'attested') + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def data(path): + path = Path(path) + require(path.resolve() == path.absolute(), 'symlink path: ' + str(path)) + st = path.lstat() + require(stat.S_ISREG(st.st_mode) and st.st_nlink == 1, 'nonregular/hardlinked evidence: ' + str(path)) + return path.read_bytes() + + +def digest(path): + return hashlib.sha256(data(path)).hexdigest() + + +def read(path): + def unique(pairs): + result = {} + for key, value in pairs: + require(key not in result, 'duplicate JSON key') + result[key] = value + return result + return json.loads(data(path), object_pairs_hook=unique) + + +def false_claims(record, fields=CLAIMS): + require(all(record.get(k) is False for k in fields), 'missing/true claim') + + +def tap(text, names): + require(text.startswith('TAP version 13\n') and text.endswith('\n'), 'truncated TAP') + require(not re.search(r'(?im)^\s*not ok\b|^ok .*#\s*(?:SKIP|TODO)\b|^Bail out!', text), 'failed/skipped TAP') + require(re.findall(r'^ok \d+ - (.+)$', text, re.M) == names, 'missing/extra named native test') + require(re.findall(r'^# Subtest: (.+)$', text, re.M) == names, 'missing native starts') + for key, number in dict(tests=len(names), suites=0, pass_=len(names), fail=0, cancelled=0, skipped=0, todo=0).items(): + require(re.findall(r'^# ' + key.rstrip('_') + r' (\d+)$', text, re.M) == [str(number)], 'TAP terminal ' + key) + require(re.search(r'^# duration_ms [0-9.]+\n\Z', text, re.M), 'truncated TAP terminal') + require(re.findall(r'^1\.\.(\d+)$', text, re.M) == [str(len(names))], 'missing TAP plan') + + +def events(text): + require(text.endswith('\n'), 'truncated Go JSON') + rows = [json.loads(line) for line in text.splitlines()] + require(rows and all(e.get('Package') == PACKAGE and e.get('Action') not in ('fail', 'skip') for e in rows), 'wrong package/failed/skipped Go event') + require([e['Action'] for e in rows if not e.get('Test') and e['Action'] in ('pass', 'fail', 'skip')] == ['pass'], 'missing Go package success') + return rows + + +def go_discovery(text): + rows = events(text) + require([e.get('Output') for e in rows if e.get('Output', '').startswith('Test')] == [NAME + '\n'], 'exact named discovery required') + require(not any(e.get('Test') for e in rows), 'execution substituted for discovery') + + +def go_results(text): + rows = events(text) + leaves = {NAME, *(NAME + '/' + p + '/' + l for p in PRODUCTS for l in LANES)} + groups = {NAME + '/' + p for p in PRODUCTS} + ran = Counter(e['Test'] for e in rows if e.get('Test') and e['Action'] == 'run') + passed = Counter(e['Test'] for e in rows if e.get('Test') and e['Action'] == 'pass') + require(ran == passed and leaves <= set(ran) <= leaves | groups and all(v == 1 for v in ran.values()), + 'exact root/ten leaves must run/pass; optional product groups are not leaves') + require(all(not e.get('Test') or e['Test'] in ran for e in rows), 'unexpected subtest') + + +def plans(record): + rows = record['plans'] + require(len(rows) == 30 and Counter((r['product'], r['lane'], r['target']) for r in rows) == + Counter({(p, l, t): 1 for p in PRODUCTS for l in LANES for t in TARGETS}), 'exact 30 unique plan tuples required') + for row in rows: + report = row['report'] + require(report['result'] == 'success' and report['data']['dry_run'] is True, 'successful dry run required') + found = [] + def walk(value): + if isinstance(value, dict): + if 'client_id' in value and 'components' in value and 'status' in value: + found.append(value) + for child in value.values(): walk(child) + elif isinstance(value, list): + for child in value: walk(child) + walk(report) + require(len(found) == 1, 'one target plan required') + plan = found[0] + require(plan['client_id'] == row['target'] and plan['scope'] == 'user' and + plan['status'] == ('ready' if row['target'] == 'claude' else 'manual_activation_required'), 'wrong plan target/status') + components = plan['components'] + expected = {('skill', 'extra-skill')} + if row['lane'] == 'skill' or row['lane'].startswith('hybrid-'): expected.add(('skill', row['lane'])) + if row['lane'] != 'skill': expected.add(('mcp_server', row['lane'])) + require({(c['kind'], c['name']) for c in components} == expected, 'exact lane component names') + kinds = Counter(c['kind'] for c in components) + require(kinds == Counter(skill=2 if row['lane'] == 'skill' or row['lane'].startswith('hybrid-') else 1, + **({} if row['lane'] == 'skill' else {'mcp_server': 1})), 'lane components') + require(len({(c['kind'], c['name']) for c in components}) == len(components) and + any(c['kind'] == 'skill' and c['name'] == 'extra-skill' for c in components) and + all(c.get('support') and c['support'] != 'unsupported' for c in components), 'missing/duplicate/unsupported components') + + +def invocation_rows(invocations, count): + require(count == len(invocations) == 741, 'exactly 741 invocations') + require(len({json.dumps(r, sort_keys=True) for r in invocations}) == 739, 'repeated invocation evidence') + counts = Counter((row['product'], tuple(row['argv']), row['status']) for row in invocations) + for command in ('publish', 'publication', 'publication doctor'): + for tail in (('--format=credential-fixture',), ('--format', 'credential-fixture')): + original = ('--format=json', *command.split(), *tail) + for argv in (original, (*original, '--format=json')): + require(counts[('plugin-kit-ai', argv, 2)] == 1, 'original format tail/JSON companion missing') + for row in invocations: + require(row['product'] in PRODUCTS and type(row['status']) is int and row['status'] in (0, 1, 2) and row['signal'] is None and row['stderr'] == '', 'invalid invocation') + +def check(root, sha): + run = read(root / 'run.json'); false_claims(run) + require(run['head'] == sha and re.fullmatch('[0-9a-f]{40}', sha) and run['versions'] == VERSIONS, 'run identity/tools') + require(str(run['run_id']).isdigit() and str(run['attempt']).isdigit(), 'run identity missing') + identity = run['identity'] + require(identity == dict(repository='777genius/universal-agent-plugins', commit=sha, engine_revision=sha, + versions={'agentplugins': '0.1.91', 'plugin-kit-ai': '2.0.0'}), 'candidate identity') + for tool in ('go', 'node', 'npm'): + require(digest(Path(run['tools'][tool]['path'])) == run['tools'][tool]['sha256'], 'tool changed') + phases = ('head', 'clean', 'go-version', 'node-version', 'npm-version', 'go-host', 'warmup', 'stage', 'verify', + 'candidate-native', 'npm-stage', 'npm-native', 'seal', 'discovery', 'planner', 'post-verify', 'terminal-clean') + for name in phases: + phase = read(root / 'logs' / (name + '.json')) + require(type(phase['exit']) is int and phase['exit'] == 0, 'phase failed: ' + name) + data(root / 'logs' / (name + '.stdout')); data(root / 'logs' / (name + '.stderr')) + require('AGENTPLUGINS_STAGED_TEST_CHILD' not in phase['env'] and 'GOFLAGS' not in phase['env'] and 'NODE_OPTIONS' not in phase['env'], 'forbidden inherited control') + if name in ('discovery', 'planner'): + flag = '-list' if name == 'discovery' else '-run' + require(phase['argv'] == [run['tools']['go']['path'], 'test', '-p=2', '-tags=packedci', '-json', + *([] if name == 'discovery' else ['-count=1', '-timeout=20m']), flag, REGEX, PACKAGE_PATH], 'tag/selection command mismatch') + repo = Path(__file__).resolve().parent.parent + scripts = repo / 'npm/agentplugins/scripts' + node = run['tools']['node']['path'] + expected_commands = { + 'candidate-native': [node, '--test', '--test-reporter=tap', 'npm/agentplugins/test/dual-authoring-candidate-native.test.js'], + 'npm-native': [node, '--test', '--test-reporter=tap', 'npm/agentplugins/test/private-npm-native.test.js'], + 'post-verify': [node, str(scripts / 'packed-installer-bridge.js'), 'verify', str(root / 'bridge-config/sealed.json'), + digest(root / 'bridge-config/sealed.json'), sha]} + for name, script, args in ( + ('stage', 'stage-dual-authoring-candidate.js', ['stage', '--candidate', str(root / 'stage.json')]), + ('verify', 'stage-dual-authoring-candidate.js', ['verify', '--candidate', str(root / 'verify.json')]), + ('npm-stage', 'stage-dual-authoring-npm.js', ['--candidate', str(root / 'npm-stage.json')]), + ('seal', 'packed-installer-bridge.js', ['seal', str(root / 'bridge-config/request.json'), str(root / 'bridge-config/sealed.json')])): + expected_commands[name] = [node, str(scripts / script), *args] + for name, argv in expected_commands.items(): + phase = read(root / 'logs' / (name + '.json')) + require(all(phase['env'].get(k) == v for k, v in dict(GOPROXY='off', GOSUMDB='off', GOENV='off', GOTOOLCHAIN='local', GOVCS='*:off').items()), 'offline phase environment') + require(phase['argv'] == argv and phase['cwd'] == str(repo), 'wrong execution command: ' + name) + for name, key, value in [('candidate-native', 'UAP_CANDIDATE_NATIVE_CONFIG', root / 'verify.json'), + ('candidate-native', 'UAP_CANDIDATE_NATIVE_SOURCE_REPO', repo), + ('npm-native', 'UAP_PRIVATE_NPM_NATIVE_CONFIG', root / 'npm-native.json')]: + require(read(root / 'logs' / (name + '.json'))['env'].get(key) == str(value), 'missing native opt-in') + log = lambda name: data(root / 'logs' / (name + '.stdout')).decode() + tap(log('candidate-native'), [CANDIDATE_TEST]); tap(log('npm-native'), NATIVE_TESTS) + go_discovery(log('discovery')); go_results(log('planner')) + candidate = read(root / 'candidate/candidate.json') + pack = read(root / 'npm-pair/completion.json') + native = read(root / 'native/native-completion.json') + cfg = read(root / 'npm-native.json') + sealed = read(root / 'bridge-config/sealed.json') + result = read(root / 'results/completion.json') + require(candidate['schema'] == 'dual-authoring-candidate/v1' and pack['schema'] == 'dual-authoring-npm-completion/v1' and + native['kind'] == 'actual-linux-private-npm-pair' and sealed['schema'] == 'packed-installer-bridge/v1' and + result['kind'] == 'packed-generated-existing-injected-installer-planner', 'wrong terminal schema/kind') + false_claims(candidate, ('release_eligible',)) + for record in (pack, native, sealed, result): false_claims(record) + for record in (candidate, pack, native): require(record['identity'] == identity, 'terminal identity mismatch') + candidate_pin = digest(root / 'candidate/candidate.json') + require(json.loads(log('stage'))['manifest_sha256'] == candidate_pin, 'stage pin') + false_claims(json.loads(log('stage'))) + verified = json.loads(log('verify')); false_claims(verified) + require(verified['manifest_sha256'] == candidate_pin and verified['consistency_verified'] is True, 'build verification') + require(candidate['asset_scope'] == 'linux-amd64-pair', 'candidate scope') + require(pack['status'] == candidate['status'] == 'CANDIDATE' and pack['authoring_mode'] == MODE and pack['asset_scope'] == 'linux-amd64-pair', 'candidate mode/scope') + require(pack['candidate_sha256'] == native['candidate_sha256'] == candidate_pin, 'candidate chain') + require(native['completion_sha256'] == cfg['completionDigest'] == digest(root / 'npm-pair/completion.json'), 'pack completion chain') + require(native['packs'] == pack['packs'] and set(pack['packs']) == set(PRODUCTS), 'pack identity') + for p in pack['packs'].values(): + body = data(root / 'npm-pair' / p['file']) + require(hashlib.sha256(body).hexdigest() == p['sha256'] and len(body) == p['size'] and + 'sha512-' + base64.b64encode(hashlib.sha512(body).digest()).decode() == p['integrity'], 'tarball pin') + require(native['tools'] == pack['tools'], 'native tool identity') + for name in ('node', 'npm', 'go', 'stager_node'): + tool = pack['tools'][name]; key = 'node' if name == 'stager_node' else name + require(tool['path'] == run['tools'][key]['path'] and tool['sha256'] == run['tools'][key]['sha256'], 'stager tool hash') + require(tool['version'] == ('go version go1.25.13 linux/amd64' if key == 'go' else VERSIONS[key]), 'stager tool version') + require(candidate['build']['go_version'] == VERSIONS['go'] and candidate['build']['authoring_mode'] == MODE and + candidate['build']['go_sha256'] == run['tools']['go']['sha256'], 'candidate build identity') + invocations = read(root / 'native/invocations.json') + invocation_rows(invocations, native['invocations']) + require(cfg['stage']['repo'] == str(repo), 'wrong terminal checkout') + require(native['inventory_sha256'] == digest(repo / 'cli/plugin-kit-ai/cmd/plugin-kit-ai/release_compat.go'), 'same-SHA inventory') + request = sealed['request'] + require(request == read(root / 'bridge-config/request.json') and request['expectedCommit'] == sha and + request['nativeConfigSha256'] == digest(root / 'npm-native.json') and + request['nativeCompletionSha256'] == digest(root / 'native/native-completion.json'), 'native seal pins') + require(sealed['verifier_sha256'] == digest(repo / 'npm/agentplugins/scripts/packed-installer-bridge.js') and + sealed['helper_sha256'] == digest(repo / 'npm/agentplugins/scripts/dual-authoring-candidate.js'), 'verifier pins') + require(log('seal').strip() == result['config_sha256'] == digest(root / 'bridge-config/sealed.json') and result['commit'] == sha, 'planner pin') + require(result['inputs'] == sealed['inputs'] == json.loads(log('post-verify')), 'post-planner seal verification') + require(Counter((p['product'], p['lane']) for p in result['inputs']['projects']) == Counter({(p, l): 1 for p in PRODUCTS for l in LANES}), 'ten project identities') + require(len({p['source'] for p in result['inputs']['projects']}) == 10, 'duplicate project') + journeys = re.findall(r'^# native journey evidence: (.+)$', log('candidate-native'), re.M) + require(len(journeys) == 1, 'candidate terminal journey missing') + journey = read(Path(journeys[0])); false_claims(journey, ('platform_acceptance', 'attested')) + require(journey['identity'] == identity and journey['manifest_sha256'] == candidate_pin and len(journey['trees']) == 2 and all(len(t) == 5 for t in journey['trees']), 'candidate journey identity/inventory') + plans(result) + require(log('head').strip() == sha and log('clean') == log('terminal-clean') == '', 'clean checkout proof') + + +def public_tap(text, request): + tap(text, [PUBLIC_TEST]) + markers = re.findall(r'^# public native completion: (.+)$', text, re.M) + require(len(markers) == 1, 'one public terminal TAP binding required') + cfg = read(request['nativeConfig']) + require(json.loads(markers[0]) == dict(file=str(Path(cfg['evidenceOutput']) / 'public-native-completion.json'), + sha256=request['nativeCompletionSha256'], source=request['expectedCommit']), 'public TAP terminal identity') + + +def unchanged_snapshots(inputs): + require(inputs['snapshots'], 'missing sealed inventories') + for snapshot in inputs['snapshots']: + root = Path(snapshot['root']); expected = snapshot['entries'] + encoded = (json.dumps(expected, indent=2, ensure_ascii=False) + '\n').encode() + require(hashlib.sha256(encoded).hexdigest() == snapshot['sha256'], 'inventory digest') + actual = [] + def walk(file, relative): + st = file.lstat(); require(not st.st_mode & 0o7000, 'special inventory mode') + row = dict(path=relative, mode=stat.S_IMODE(st.st_mode), kind='directory' if stat.S_ISDIR(st.st_mode) else 'file') + if stat.S_ISLNK(st.st_mode): + require(file.resolve(strict=True).is_relative_to(root) and file.resolve() != root, 'inventory link escape') + row.update(kind='symlink', target=str(file.readlink())) + elif not stat.S_ISDIR(st.st_mode): + body = data(file); row.update(size=len(body), sha256=hashlib.sha256(body).hexdigest()) + actual.append(row) + if stat.S_ISDIR(st.st_mode): + for child in sorted(file.iterdir()): walk(child, child.name if relative == '.' else relative + '/' + child.name) + require(root.resolve() == root, 'aliased inventory root') + walk(root, '.') + require(actual == expected, 'sealed tree changed') + + +def check_public(root, sha): + run = read(root / 'public-run.json'); false_claims(run) + require(run['schema'] == 'public-packed-run/v1' and run['head'] == sha and re.fullmatch('[0-9a-f]{40}', sha), 'public run identity') + options = run['options']; request = options['request'] + require(request['intake'] == 'public-fixture/v1' and request['expectedCommit'] == sha, 'public request identity') + require(digest(options['nativeTap']) == options['nativeTapSha256'], 'changed public TAP') + public_tap(data(options['nativeTap']).decode(), request) + require(digest(request['nativeConfig']) == request['nativeConfigSha256'], 'changed public config') + cfg = read(request['nativeConfig']) + terminal_path = Path(cfg['evidenceOutput']) / 'public-native-completion.json' + require(digest(terminal_path) == request['nativeCompletionSha256'], 'changed public terminal') + native = read(terminal_path); false_claims(native, (*CLAIMS, 'signed_promotion', 'public_eligible')) + require(native['schema'] == 'dual-authoring-public-native/v1' and native['status'] == 'completed' and + native['qualification'] is None and native['identity']['commit'] == native['identity']['engine_revision'] == sha, 'public terminal contract') + require(digest(Path(cfg['evidenceOutput']) / 'invocations.json') == native['invocations_sha256'], 'public invocation pin') + sealed_path = root / 'bridge-config/sealed.json'; sealed = read(sealed_path); false_claims(sealed) + require(sealed['schema'] == 'packed-installer-bridge/v1' and sealed['request'] == request == read(root / 'bridge-config/request.json'), 'public sealed request') + repo = Path(__file__).resolve().parent.parent; bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' + require(sealed['verifier_sha256'] == digest(bridge) and sealed['helper_sha256'] == digest(bridge.with_name('dual-authoring-candidate.js')), 'public verifier changed') + for key in ('go', 'node'): + tool = run['tools'][key] + require(tool['path'] == options[key] and tool['sha256'] == digest(tool['path']) == native['tools'][key]['sha256'], 'public tool changed') + node, go = [run['tools'][k]['path'] for k in ('node', 'go')] + commands = {'head': ['/usr/bin/git', 'rev-parse', 'HEAD'], + 'clean': ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'], + 'terminal-clean': ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'], + 'seal': [node, str(bridge), 'seal', str(root / 'bridge-config/request.json'), str(sealed_path)], + 'post-verify': [node, str(bridge), 'verify', str(sealed_path), digest(sealed_path), sha]} + for name, flag in [('discovery', '-list'), ('planner', '-run')]: + commands[name] = [go, 'test', '-p=2', '-tags=packedci', '-json', + *([] if name == 'discovery' else ['-count=1', '-timeout=20m']), flag, REGEX, PACKAGE_PATH] + for name, command in commands.items(): + phase = read(root / 'logs' / (name + '.json')) + require(type(phase['exit']) is int and phase['exit'] == 0 and phase['argv'] == command and phase['cwd'] == str(repo), 'public phase: ' + name) + require(all(phase['env'].get(k) == v for k, v in dict(GOPROXY='off', GOSUMDB='off', GOVCS='*:off', GOENV='off', GOTOOLCHAIN='local').items()), 'public offline environment') + require(not any(k in phase['env'] for k in ('GOFLAGS', 'NODE_OPTIONS', 'AGENTPLUGINS_STAGED_TEST_CHILD')), 'inherited public control') + data(root / 'logs' / (name + '.stdout')); data(root / 'logs' / (name + '.stderr')) + if name in ('discovery', 'planner'): + require(all(phase['env'].get(k) == v for k, v in dict(UAP_PACKED_INSTALLER_NODE=node, + UAP_PACKED_INSTALLER_CONFIG=str(sealed_path), UAP_PACKED_INSTALLER_CONFIG_SHA256=digest(sealed_path), + UAP_PACKED_INSTALLER_COMMIT=sha, UAP_PACKED_INSTALLER_OUTPUT=str(root / 'results/completion.json')).items()), 'public planner opt-in') + log = lambda name: data(root / 'logs' / (name + '.stdout')).decode() + go_discovery(log('discovery')); go_results(log('planner')) + result = read(root / 'results/completion.json'); false_claims(result) + require(result['kind'] == 'packed-generated-existing-injected-installer-planner' and result['commit'] == sha, 'public planner completion') + require(result['config_sha256'] == digest(sealed_path) == log('seal').strip(), 'public planner seal pin') + require(result['inputs'] == sealed['inputs'] == json.loads(log('post-verify')), 'public preservation') + unchanged_snapshots(result['inputs']) + projects = result['inputs']['projects'] + require(Counter((p['product'], p['lane']) for p in projects) == Counter({(p, l): 1 for p in PRODUCTS for l in LANES}) and + len({p['source'] for p in projects}) == 10, 'public ten distinct projects') + plans(result) + require(log('head').strip() == sha and log('clean') == log('terminal-clean') == '', 'public exact clean checkout') + + +if __name__ == '__main__': + if len(sys.argv) == 4 and sys.argv[1] == '--public': + check_public(Path(sys.argv[2]), sys.argv[3]) + elif len(sys.argv) == 3: + check(Path(sys.argv[1]), sys.argv[2]) + else: + raise SystemExit('usage: check-packed-ci.py ROOT SHA | --public ROOT SHA') diff --git a/scripts/check-packed-workflow.py b/scripts/check-packed-workflow.py new file mode 100644 index 00000000..1603c87a --- /dev/null +++ b/scripts/check-packed-workflow.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Structural guard for this one workflow, not hosted required-check enforcement.""" +import json +from pathlib import Path +import re +import sys + + +def require(ok, message): + if not ok: raise ValueError(message) + + +def results(value): + require(set(value) == {'native', 'packed'} and all(v.get('result') == 'success' for v in value.values()), + 'native and packed must both exist and succeed') + + +def check(text, runner): + # Fixed indentation is intentional: reject unsupported rewrites for review. + require('\non:\n pull_request:\n workflow_dispatch:\n' in text, 'unfiltered PR and dispatch required') + require(not re.search(r'^\s*(?:paths(?:-ignore)?|branches(?:-ignore)?|continue-on-error):', text, re.M), 'success override/filter') + jobs = dict(re.findall(r'^ (\w+):\n(.*?)(?=^ \w+:\n|\Z)', text.split('\njobs:\n', 1)[1], re.M | re.S)) + require(set(jobs) == {'native', 'packed', 'acceptance'}, 'required jobs missing/renamed') + native, packed, aggregate = (jobs[n] for n in ('native', 'packed', 'acceptance')) + # github/docs contexts.md: jobs..env excludes runner (steps.env allows it). + for body in jobs.values(): + job_env = re.search(r'^ env:\n((?:[ \t]+.*\n|\n)*?)(?=^ \S|\Z)', body, re.M) + if job_env: + require(not re.search(r'\$\{\{[^}]*\brunner\s*(?:\.|\[)', job_env[1]), + 'runner context is unavailable in job env') + initialization = """ - name: Resolve disposable packed evidence path + shell: bash + run: | + set -euo pipefail + # Publish the path before checkout/setup so always() consumers retain it. + # Leave creation (0700) and stale-root rejection to run-packed-ci.py. + printf 'PACKED_ROOT=%s/authoring-packed-%s-%s\\n' "$RUNNER_TEMP" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" >> "$GITHUB_ENV" +""" + require(' steps:\n' + initialization in packed, 'publish packed path first without creating root') + require(' PACKED_ROOT:' not in packed, 'packed path must come from runtime environment') + require(' - name: Require terminal packed evidence\n if: always()\n' in packed and + ' - name: Preserve packed evidence including failure logs and sealed inputs\n if: always()\n' in packed and + ' ${{ env.PACKED_ROOT }}/**\n' in packed and + ' if-no-files-found: error\n' in packed, 'always checker/upload required') + for body in (native, packed): + require(not re.search(r'^ (?:if|continue-on-error):', body, re.M), 'required job cannot be conditional') + matrix = re.findall(r'- \{runner: ([\w.-]+), os: (\w+), arch: (\w+)\}', native) + require(set(matrix) == {('ubuntu-24.04', 'linux', 'amd64'), ('ubuntu-24.04-arm', 'linux', 'arm64'), + ('windows-2022', 'windows', 'amd64'), ('windows-11-arm', 'windows', 'arm64')} and len(matrix) == 4, 'four native lanes required') + require(' needs: [native, packed]\n if: always()\n' in aggregate, 'aggregate must always need native+packed') + require(' REQUIRED_RESULTS: ${{ toJSON(needs) }}\n' in aggregate, 'actual needs results required') + for body in (packed, aggregate): + require(' ref: ${{ github.event.pull_request.head.sha || github.sha }}\n' in body and + ' persist-credentials: false\n' in body, 'exact clean checkout') + require(not re.search(r'\|\|\s*(?:true|:)|exit 0|set \+e', body), 'masked failure') + require(all(line.strip() == 'if: always()' for line in body.splitlines() if re.match(r'\s*if:', line)), 'skipped step') + require(' go-version: \'1.25.13\'' in packed and " node-version: '22.23.2'" in packed, 'tool pins') + require(' run: python3 -B scripts/run-packed-ci.py "$PACKED_ROOT" "$EXPECTED_HEAD"\n' in packed and + ' run: python3 -B scripts/check-packed-ci.py "$PACKED_ROOT" "$EXPECTED_HEAD"\n' in packed, 'runner/checker required') + require(' python3 -B scripts/check-packed-workflow.py\n' in aggregate and + " python3 -B -m unittest discover -s scripts -p 'test_packed_ci.py'\n" in aggregate and + ' run: python3 -B scripts/check-packed-workflow.py --results "$REQUIRED_RESULTS"\n' in aggregate, 'aggregate controls/results required') + for phase, flag in (('discovery', '-list'), ('planner', '-run')): + pattern = rf"run\('{phase}', \[go, 'test', '-p=2', '-tags=packedci', '-json'.*?'{flag}', proof.REGEX, proof.PACKAGE_PATH\]" + require(re.search(pattern, runner), 'explicit tagged exact ' + phase) + require('proof.check(root, sha)' in runner, 'terminal checker invocation') + require('go test -p 2 -json -count=1 -timeout=12m "${selected[@]}"' in native and not re.search(r'\s-run(?:[ =]|$)', native), 'ordinary native selection must remain unfiltered') + require('-tags=packedci' not in native and 'GOFLAGS' not in packed, 'tag must remain opt-in') + + +if __name__ == '__main__': + if len(sys.argv) == 3 and sys.argv[1] == '--results': + results(json.loads(sys.argv[2])) + else: + repo = Path(__file__).resolve().parent.parent + check((repo / '.github/workflows/authoring-native.yml').read_text(), + (repo / 'scripts/run-packed-ci.py').read_text()) diff --git a/scripts/run-packed-ci.py b/scripts/run-packed-ci.py new file mode 100644 index 00000000..d0cf5a16 --- /dev/null +++ b/scripts/run-packed-ci.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Bounded Linux packed proof. Setup/warmup precedes the offline command contract.""" +import importlib.util +import json +import os +from pathlib import Path +import platform +import re +import shutil +import subprocess +import sys +import time + +SPEC = importlib.util.spec_from_file_location('packed', Path(__file__).with_name('check-packed-ci.py')) +proof = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(proof) + + +def write(path, value): + with path.open('x', encoding='utf-8') as stream: + stream.write(json.dumps(value, indent=2, ensure_ascii=False) + '\n') + + +def private_env(root, go, node, repo, modules): + for name in ('home', 'tmp', 'config', 'cache', 'data', 'state', 'appdata', 'localappdata', 'hooks'): + (root / name).mkdir(parents=True, exist_ok=True) + env = {'PATH': os.pathsep.join([str(go.parent), str(node.parent), '/usr/bin', '/bin']), + 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC', 'GOENV': 'off', + 'GOTOOLCHAIN': 'local', 'GOMAXPROCS': '2', 'GOWORK': str(repo / 'go.work'), + 'GOCACHE': str(root / 'cache/go-build'), 'GOMODCACHE': str(modules), + 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': str(root / 'config/gitconfig'), + 'GIT_CONFIG_COUNT': '1', 'GIT_CONFIG_KEY_0': 'core.hooksPath', + 'GIT_CONFIG_VALUE_0': str(root / 'hooks'), 'GIT_TERMINAL_PROMPT': '0'} + for key, directory in {'HOME': 'home', 'USERPROFILE': 'home', 'TMPDIR': 'tmp', 'TMP': 'tmp', + 'TEMP': 'tmp', 'XDG_CONFIG_HOME': 'config', 'XDG_CACHE_HOME': 'cache', + 'XDG_DATA_HOME': 'data', 'XDG_STATE_HOME': 'state', 'APPDATA': 'appdata', + 'LOCALAPPDATA': 'localappdata'}.items(): + env[key] = str(root / directory) + return env + + +def planner(root, sha, go, node, sealed, printed, run): + bridge = Path(__file__).resolve().parent.parent / "npm/agentplugins/scripts/packed-installer-bridge.js" + results = root / 'results'; results.mkdir() + extra = dict(UAP_PACKED_INSTALLER_NODE=str(node), UAP_PACKED_INSTALLER_CONFIG=str(sealed), + UAP_PACKED_INSTALLER_CONFIG_SHA256=printed, UAP_PACKED_INSTALLER_COMMIT=sha, + UAP_PACKED_INSTALLER_OUTPUT=str(results / 'completion.json')) + discovery = run('discovery', [go, 'test', '-p=2', '-tags=packedci', '-json', '-list', proof.REGEX, proof.PACKAGE_PATH], extra) + proof.go_discovery(discovery) + output = run('planner', [go, 'test', '-p=2', '-tags=packedci', '-json', '-count=1', '-timeout=20m', '-run', proof.REGEX, proof.PACKAGE_PATH], extra) + proof.go_results(output) + run('post-verify', [node, bridge, 'verify', sealed, printed, sha]) + + +def main(root, sha): + repo = Path(__file__).resolve().parent.parent + proof.require(re.fullmatch('[0-9a-f]{40}', sha), 'exact SHA required') + proof.require(root.is_absolute() and root.resolve() == root and not root.is_relative_to(repo), 'external canonical output required') + root.mkdir(mode=0o700) # stale output is fatal + (root / 'logs').mkdir() + proof.require(platform.system() == 'Linux' and platform.machine() == 'x86_64', 'native Linux amd64 required') + go, node, npm = [Path(shutil.which(tool) or '/missing-tool').resolve(strict=True) for tool in ('go', 'node', 'npm')] + proof.require(npm.name == 'npm-cli.js', 'resolve npm JavaScript CLI, not shell shim') + modules = root / 'modules'; modules.mkdir() + env = private_env(root / 'orchestrator', go, node, repo, modules) + phases = {} + + def run(name, argv, extra=None, timeout=1200): + argv = list(map(str, argv)); started = time.monotonic() + record = {'argv': argv, 'cwd': str(repo), 'env': dict(env, **(extra or {})), 'exit': None} + try: + with (root / 'logs' / (name + '.stdout')).open('x') as out, (root / 'logs' / (name + '.stderr')).open('x') as err: + result = subprocess.run(argv, cwd=repo, env=record['env'], stdout=out, stderr=err, timeout=timeout) + record['exit'] = result.returncode + finally: + record['seconds'] = round(time.monotonic() - started, 3) + phases[name] = record + write(root / 'logs' / (name + '.json'), record) + proof.require(record['exit'] == 0, name + ' failed') + return (root / 'logs' / (name + '.stdout')).read_text() + + proof.require(run('head', ['/usr/bin/git', 'rev-parse', 'HEAD']).strip() == sha, 'wrong checkout') + proof.require(run('clean', ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all']) == '', 'dirty checkout') + versions = {'go': run('go-version', [go, 'env', 'GOVERSION']).strip(), + 'node': run('node-version', [node, '--version']).strip(), + 'npm': run('npm-version', [node, npm, '--version']).strip()} + proof.require(versions == proof.VERSIONS, 'CI requires exact bundled Node/npm and Go pins') + machine = json.loads(run('go-host', [go, 'env', '-json', 'GOOS', 'GOARCH', 'GOHOSTOS', 'GOHOSTARCH'])) + proof.require(machine == dict(GOOS='linux', GOARCH='amd64', GOHOSTOS='linux', GOHOSTARCH='amd64'), 'Go host mismatch') + # Private candidate versions are the accepted release-contract fixture identity, + # not npm publication versions (package.json deliberately says development). + identity = {'repository': '777genius/universal-agent-plugins', 'commit': sha, 'engine_revision': sha, + 'versions': {'agentplugins': '0.1.91', 'plugin-kit-ai': '2.0.0'}} + write(root / 'run.json', {'head': sha, 'run_id': os.environ['GITHUB_RUN_ID'], + 'attempt': os.environ['GITHUB_RUN_ATTEMPT'], 'versions': versions, + 'tools': {k: {'path': str(p), 'sha256': proof.digest(p)} for k, p in dict(go=go, node=node, npm=npm).items()}, + 'identity': identity, 'release_eligible': False, 'platform_acceptance': False, 'attested': False}) + run('warmup', [go, 'list', '-p=2', '-deps', '-test', proof.PACKAGE_PATH, + './cli/plugin-kit-ai/cmd/agentplugins', './cli/plugin-kit-ai/cmd/plugin-kit-ai']) + env.update(GOPROXY='off', GOSUMDB='off', GOVCS='*:off') + work = root / 'work'; work.mkdir() + stage = dict(candidate=True, repo=str(repo), output=str(root / 'candidate'), workParent=str(work), + go=str(go), modCache=str(modules), identity=identity, assetScope='linux-amd64-pair', authoringMode=proof.MODE) + write(root / 'stage.json', stage) + scripts = repo / 'npm/agentplugins/scripts' + producer = scripts / 'stage-dual-authoring-candidate.js' + staged = json.loads(run('stage', [node, producer, 'stage', '--candidate', root / 'stage.json'])) + pin = proof.digest(root / 'candidate/candidate.json') + proof.require(staged['manifest_sha256'] == pin, 'stage manifest pin') + verify = {k: stage[k] for k in ('candidate', 'workParent', 'go', 'identity', 'assetScope', 'authoringMode')} + verify.update(root=str(root / 'candidate'), manifestDigest=pin) + write(root / 'verify.json', verify) + run('verify', [node, producer, 'verify', '--candidate', root / 'verify.json']) + tap = run('candidate-native', [node, '--test', '--test-reporter=tap', 'npm/agentplugins/test/dual-authoring-candidate-native.test.js'], + {'UAP_CANDIDATE_NATIVE_CONFIG': str(root / 'verify.json'), 'UAP_CANDIDATE_NATIVE_SOURCE_REPO': str(repo)}) + proof.tap(tap, [proof.CANDIDATE_TEST]) + npm_stage = dict(verify, repo=str(repo), output=str(root / 'npm-pair'), node=str(node), npm=str(npm)) + write(root / 'npm-stage.json', npm_stage) + run('npm-stage', [node, scripts / 'stage-dual-authoring-npm.js', '--candidate', root / 'npm-stage.json']) + native = dict(stage=npm_stage, completionDigest=proof.digest(root / 'npm-pair/completion.json'), evidenceOutput=str(root / 'native')) + write(root / 'npm-native.json', native) + tap = run('npm-native', [node, '--test', '--test-reporter=tap', 'npm/agentplugins/test/private-npm-native.test.js'], + {'UAP_PRIVATE_NPM_NATIVE_CONFIG': str(root / 'npm-native.json')}) + proof.tap(tap, proof.NATIVE_TESTS) + completion = proof.read(root / 'native/native-completion.json') + proof.require(completion['invocations'] == len(proof.read(root / 'native/invocations.json')) == 741, 'exactly 741 invocations required') + parents = {str(Path(p).parent) for p in completion['projects'].values()} + proof.require(set(completion['projects']) == set(proof.PRODUCTS) and len(parents) == 1, 'two project parents required') + config = root / 'bridge-config'; config.mkdir() + write(config / 'request.json', dict(expectedCommit=sha, nativeConfig=str(root / 'npm-native.json'), + nativeConfigSha256=proof.digest(root / 'npm-native.json'), + nativeCompletionSha256=proof.digest(root / 'native/native-completion.json'), + fixtureRoot=parents.pop(), disposableEvidence=True)) + bridge = scripts / 'packed-installer-bridge.js' + sealed = config / 'sealed.json' + printed = run('seal', [node, bridge, 'seal', config / 'request.json', sealed]).strip() + proof.require(printed == proof.digest(sealed), 'seal pin mismatch') + env = private_env(root / 'planner', go, node, repo, modules) + env.update(GOPROXY='off', GOSUMDB='off', GOVCS='*:off') + planner(root, sha, go, node, sealed, printed, run) + proof.require(run('terminal-clean', ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all']) == '', 'checkout changed') + proof.check(root, sha) + write(root / 'artifact-index.json', {str(p.relative_to(root)): proof.digest(p) for p in sorted(root.rglob('*')) + if p.is_file() and not p.is_symlink() and not p.is_relative_to(modules) and not p.is_relative_to(root / 'orchestrator') and not p.is_relative_to(root / 'planner')}) + write(root / 'summary.json', dict(status='passed', head=sha, invocations=741, projects=10, plans=30, + release_eligible=False, platform_acceptance=False, attested=False)) + + +def public_main(root, sha, options_path): + """Consume an owner-terminal public run; never rebuild or enrich old proof.""" + repo = Path(__file__).resolve().parent.parent + options = proof.read(options_path) + proof.require(set(options) == {'request', 'nativeTap', 'nativeTapSha256', 'go', 'node', 'modCache'}, 'public runner options') + request = options['request'] + proof.require(request.get('intake') == 'public-fixture/v1' and request['expectedCommit'] == sha, 'explicit public intake SHA') + proof.require(proof.digest(options['nativeTap']) == options['nativeTapSha256'], 'public transcript pin') + proof.public_tap(proof.data(options['nativeTap']).decode(), request) + proof.require(re.fullmatch('[0-9a-f]{40}', sha), 'exact SHA required') + proof.require(root.is_absolute() and root.resolve() == root and not root.is_relative_to(repo), 'external canonical output required') + # Output must be disjoint before creating logs or planner homes. + cfg = proof.read(request['nativeConfig']); candidate = cfg['prepare']['candidate'] + protected = [repo, Path(options_path), Path(options['nativeTap']), Path(request['nativeConfig']), + Path(request['fixtureRoot']), Path(cfg['evidenceOutput']), Path(cfg['prepare']['output']), + Path(candidate['root']), Path(candidate['pairMarker']), Path(candidate['workParent']), + *[Path(options[k]) for k in ('go', 'node', 'modCache')], *map(Path, candidate['outputs'].values())] + for other in protected: + proof.require(not root.is_relative_to(other) and not other.is_relative_to(root), 'public output overlaps input') + go, node, modules = [Path(options[k]).resolve(strict=True) for k in ('go', 'node', 'modCache')] + proof.require(str(go) == candidate['go'] and str(node) == cfg['prepare']['node'], 'public tool paths differ') + proof.require(platform.system() == 'Linux' and platform.machine() == 'x86_64', 'native Linux amd64 required') + root.mkdir(mode=0o700); (root / 'logs').mkdir(); (root / 'bridge-config').mkdir() + env = private_env(root / 'planner', go, node, repo, modules) + env.update(GOPROXY='off', GOSUMDB='off', GOVCS='*:off') + def run(name, argv, extra=None): + argv = list(map(str, argv)); record = dict(argv=argv, cwd=str(repo), env=dict(env, **(extra or {})), exit=None) + started = time.monotonic() + try: + with (root / 'logs' / (name + '.stdout')).open('x') as out, (root / 'logs' / (name + '.stderr')).open('x') as err: + record['exit'] = subprocess.run(argv, cwd=repo, env=record['env'], stdout=out, stderr=err, timeout=1200).returncode + finally: + record['seconds'] = round(time.monotonic() - started, 3); write(root / 'logs' / (name + '.json'), record) + proof.require(record['exit'] == 0, name + ' failed') + return (root / 'logs' / (name + '.stdout')).read_text() + proof.require(run('head', ['/usr/bin/git', 'rev-parse', 'HEAD']).strip() == sha, 'wrong checkout') + proof.require(run('clean', ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all']) == '', 'dirty checkout') + write(root / 'public-run.json', dict(schema='public-packed-run/v1', head=sha, options=options, + tools={k: dict(path=str(p), sha256=proof.digest(p)) for k, p in dict(go=go, node=node).items()}, + release_eligible=False, platform_acceptance=False, attested=False)) + request_path = root / 'bridge-config/request.json'; write(request_path, request) + sealed = root / 'bridge-config/sealed.json' + bridge = repo / 'npm/agentplugins/scripts/packed-installer-bridge.js' + printed = run('seal', [node, bridge, 'seal', request_path, sealed]).strip() + proof.require(printed == proof.digest(sealed), 'public seal pin') + planner(root, sha, go, node, sealed, printed, run) + proof.require(run('terminal-clean', ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all']) == '', 'checkout changed') + proof.check_public(root, sha) + write(root / 'summary.json', dict(status='passed', intake='public-fixture/v1', head=sha, projects=10, plans=30, + release_eligible=False, platform_acceptance=False, attested=False, signed_promotion=False, public_eligible=False)) + + +if __name__ == '__main__': + if len(sys.argv) == 5 and sys.argv[1] == '--public': + public_main(Path(sys.argv[2]), sys.argv[3], Path(sys.argv[4])) + elif len(sys.argv) == 3: + main(Path(sys.argv[1]), sys.argv[2]) + else: + raise SystemExit('usage: run-packed-ci.py ROOT SHA | --public ROOT SHA OPTIONS') diff --git a/scripts/test_check_authoring_native_results.py b/scripts/test_check_authoring_native_results.py index c1a34d47..8ae4a877 100644 --- a/scripts/test_check_authoring_native_results.py +++ b/scripts/test_check_authoring_native_results.py @@ -206,6 +206,9 @@ def fixture(self, system, arch): if system == "windows": paths += ["namespace-rejection", "same-volume-overlapping-alias", "two-physical-ntfs-volumes"] tests.update((self.commands, "TestNativePathFixBothBinaries/" + t) for t in paths) + tests.add((self.commands, "TestPackedInstallerSourceHarness")) + tests.update((self.commands, "TestPackedInstallerSourceHarness/" + lane) for lane in + ("skill", "mcp-remote", "mcp-stdio", "hybrid-remote", "hybrid-stdio")) self.events = [dict(Action="pass", Package=p, Test=t) for p, t in sorted(tests)] self.events += [dict(Action="pass", Package=p) for p in packages] self.discovery = [dict(Action="output", Package=p, Output=t + "\n") @@ -254,6 +257,27 @@ def test_four_lane_controls(self): self.fixture(system, arch) self.assertEqual(self.check()["errors"], []) + def test_source_harness_required_on_every_host(self): + for system in ("linux", "windows"): + for arch in ("amd64", "arm64"): + for lane in ("", "/skill", "/mcp-remote", "/mcp-stdio", "/hybrid-remote", "/hybrid-stdio"): + for action in ("skip", "omit"): + with self.subTest(system=system, arch=arch, lane=lane, action=action): + self.fixture(system, arch) + name = "TestPackedInstallerSourceHarness" + lane + if action == "omit": + self.events = [e for e in self.events if e.get("Test") != name] + self.discovery = [e for e in self.discovery if e.get("Output") != name + "\n"] + else: + next(e for e in self.events if e.get("Test") == name)["Action"] = action + self.rejected("source harness") + + def test_packed_acceptance_cannot_leak_into_native_discovery(self): + self.fixture("linux", "amd64") + self.discovery.append(dict(Action="output", Package=self.commands, + Output="TestPackedGeneratedPackagesReachExistingInstallerPlanner\n")) + self.rejected("explicit packedci tag") + def test_architecture_mismatch(self): for system in ("linux", "windows"): for field in ("GOARCH", "GOHOSTARCH"): diff --git a/scripts/test_packed_ci.py b/scripts/test_packed_ci.py new file mode 100644 index 00000000..70dc189f --- /dev/null +++ b/scripts/test_packed_ci.py @@ -0,0 +1,342 @@ +"""Synthetic controls only; no product execution and no packed acceptance claim.""" +import copy +import importlib.util +import json +import os +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parent.parent + +def module(name): + spec = importlib.util.spec_from_file_location(name, ROOT / 'scripts' / (name + '.py')) + result = importlib.util.module_from_spec(spec); spec.loader.exec_module(result) + return result + +p = module('check-packed-ci') +w = module('check-packed-workflow') +r = module('run-packed-ci') + + +def tap(names): + return 'TAP version 13\n' + ''.join(f'# Subtest: {n}\nok {i} - {n}\n' for i, n in enumerate(names, 1)) + \ + f'1..{len(names)}\n# tests {len(names)}\n# suites 0\n# pass {len(names)}\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 1\n' + + +def go(rows): + return ''.join(json.dumps(dict(Package=p.PACKAGE, **row)) + '\n' for row in rows) + + +def plan_fixture(): + rows = [] + for product in p.PRODUCTS: + for lane in p.LANES: + components = [dict(kind='skill', name='extra-skill', support='native')] + if lane == 'skill' or lane.startswith('hybrid-'): + components.append(dict(kind='skill', name=lane, support='native')) + if lane != 'skill': components.append(dict(kind='mcp_server', name=lane, support='native')) + for target in p.TARGETS: + plan = dict(client_id=target, scope='user', status='ready' if target == 'claude' else 'manual_activation_required', components=copy.deepcopy(components)) + rows.append(dict(product=product, lane=lane, target=target, + report=dict(result='success', data=dict(dry_run=True, plan=plan)))) + return dict(plans=rows) + + +def invocation_fixture(): + rows = [dict(product='plugin-kit-ai', argv=[str(i)], status=i % 3, signal=None, stdout='', stderr='') for i in range(739)] + index = 0 + for command in ('publish', 'publication', 'publication doctor'): + for tail in (['--format=credential-fixture'], ['--format', 'credential-fixture']): + argv = ['--format=json', *command.split(), *tail] + for args in (argv, argv + ['--format=json']): + rows[index].update(argv=args, status=2); index += 1 + return rows + copy.deepcopy(rows[-2:]) + + +class EvidenceControls(unittest.TestCase): + def test_tap_named_and_terminal(self): + for names in ([p.CANDIDATE_TEST], p.NATIVE_TESTS): + valid = tap(names); p.tap(valid, names) + bad = [valid[:-1], valid.replace('ok 1 -', 'not ok 1 -'), valid.replace(names[0], 'renamed'), + valid.replace('# skipped 0', '# skipped 1'), valid.replace('# cancelled 0', '# cancelled 1'), + valid.replace('1..' + str(len(names)), ''), valid.replace('# pass ' + str(len(names)), ''), + valid.replace('ok 1 - ' + names[0], 'ok 1 - ' + names[0] + ' # SKIP'), tap(names[:0])] + for text in bad: + with self.subTest(text=text), self.assertRaises(ValueError): p.tap(text, names) + + def test_exact_go_discovery(self): + rows = [dict(Action='output', Output=p.NAME + '\n'), dict(Action='pass')] + p.go_discovery(go(rows)) + for bad in (rows[1:], rows + rows[:1], [dict(Action='output', Output='TestRenamed\n'), rows[1]], + [rows[0], dict(Action='skip')]): + with self.assertRaises(ValueError): p.go_discovery(go(bad)) + with self.assertRaises(ValueError): p.go_discovery(go(rows).replace(p.PACKAGE, 'wrong/package')) + with self.assertRaises(ValueError): p.go_discovery(go(rows)[:-1]) + + def test_exact_ten_leaves_with_group_events(self): + names = [p.NAME] + [p.NAME + '/' + x for x in p.PRODUCTS] + [p.NAME + '/' + x + '/' + l for x in p.PRODUCTS for l in p.LANES] + rows = [dict(Action=a, Test=n) for n in names for a in ('run', 'pass')] + [dict(Action='pass')] + p.go_results(go(rows)) + p.go_results(go([e for e in rows if e.get("Test") not in {p.NAME + "/" + x for x in p.PRODUCTS}])) + for bad in (rows[-1:], rows[1:], rows + rows[:2], [e for e in rows if e.get('Test') != names[-1]], + rows + [dict(Action='skip', Test=names[-1])], rows + [dict(Action='fail')]): + with self.assertRaises(ValueError): p.go_results(go(bad)) + + def test_exact_plan_tuple_and_components(self): + valid = plan_fixture(); p.plans(valid) + for mutate in (lambda x: x['plans'].pop(), lambda x: x['plans'].append(x['plans'][0]), + lambda x: x['plans'].__setitem__(1, x['plans'][0]), + lambda x: x['plans'][0].update(target='wrong'), + lambda x: x['plans'][0]['report']['data'].update(dry_run=False), + lambda x: x['plans'][0]['report']['data']['plan'].update(status='ready'), + lambda x: x['plans'][0]['report']['data']['plan']['components'].pop(0), + lambda x: x['plans'][0]['report']['data']['plan']['components'][0].update(support='unsupported')): + bad = copy.deepcopy(valid); mutate(bad) + with self.assertRaises(ValueError): p.plans(bad) + + def test_invocation_counts_duplicates_signals_and_negative_statuses(self): + rows = invocation_fixture() + p.invocation_rows(rows, 741) + for bad, count in ((rows[:-1], 740), (rows + rows[:1], 742), (rows, 740), ([rows[0]] * 741, 741)): + with self.assertRaises(ValueError): p.invocation_rows(bad, count) + for key, value in (('argv', ['removed-original-format']), ('signal', 'SIGTERM'), ('status', -9), ('status', True), ('stderr', 'failure')): + bad = copy.deepcopy(rows); bad[0][key] = value + with self.assertRaises(ValueError): p.invocation_rows(bad, 741) + + def test_claims_missing_or_true(self): + good = dict.fromkeys(p.CLAIMS, False); p.false_claims(good) + for key in p.CLAIMS: + for value in (True, None, 0, ''): + with self.assertRaises(ValueError): p.false_claims(dict(good, **{key: value})) + bad = dict(good); del bad[key] + with self.assertRaises(ValueError): p.false_claims(bad) + + def test_missing_malformed_linked_and_stale_files(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp); file = root / 'record.json' + with self.assertRaises(OSError): p.read(file) + file.write_text('{"x":1,"x":2}') + with self.assertRaises(ValueError): p.read(file) + file.write_text('{}'); p.read(file) + with self.assertRaises(FileExistsError): r.write(file, {}) + os.symlink(file, root / 'symbolic') + with self.assertRaises(ValueError): p.read(root / 'symbolic') + os.link(file, root / 'hard') + with self.assertRaises(ValueError): p.read(file) + with self.assertRaises((ValueError, OSError)): p.check(root, 'a' * 40) + + def test_private_environment_allowlist(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + env = r.private_env(root, Path('/tools/go'), Path('/tools/node'), ROOT, root / 'modules') + for key in ('GOFLAGS', 'NODE_OPTIONS', 'NODE_PATH', 'AGENTPLUGINS_STAGED_TEST_CHILD', 'GITHUB_TOKEN', 'NPM_TOKEN'): + self.assertNotIn(key, env) + for key in ('HOME', 'USERPROFILE', 'APPDATA', 'TMPDIR', 'XDG_CONFIG_HOME'): + self.assertTrue(Path(env[key]).is_relative_to(root)) + + +class TerminalControls(unittest.TestCase): + def fixture(self): + temp = tempfile.TemporaryDirectory(); self.addCleanup(temp.cleanup) + root = Path(temp.name); sha = 'a' * 40 + def put(name, value): + path = root / name; path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value) if not isinstance(value, str) else value) + def get(name): return json.loads((root / name).read_text()) + claims = dict.fromkeys(p.CLAIMS, False) + identity = dict(repository='777genius/universal-agent-plugins', commit=sha, engine_revision=sha, + versions={'agentplugins': '0.1.91', 'plugin-kit-ai': '2.0.0'}) + tools = {} + for key in p.VERSIONS: + put('tools/' + key, 'synthetic inert tool ' + key) + tools[key] = dict(path=str(root / 'tools' / key), sha256=p.digest(root / 'tools' / key)) + put('run.json', dict(head=sha, identity=identity, versions=p.VERSIONS, tools=tools, run_id='1', attempt='1', **claims)) + candidate = dict(schema='dual-authoring-candidate/v1', identity=identity, status='CANDIDATE', asset_scope='linux-amd64-pair', release_eligible=False, + build=dict(go_version=p.VERSIONS['go'], authoring_mode=p.MODE, go_sha256=tools['go']['sha256'])) + put('candidate/candidate.json', candidate); candidate_pin = p.digest(root / 'candidate/candidate.json') + packs = {} + for product in p.PRODUCTS: + file = product + '.tgz'; put('npm-pair/' + file, 'synthetic inert pack') + body = (root / 'npm-pair' / file).read_bytes() + packs[product] = dict(file=file, size=len(body), sha256=p.digest(root / 'npm-pair' / file), + integrity='sha512-' + p.base64.b64encode(p.hashlib.sha512(body).digest()).decode()) + pack_tools = {key: dict(value, version='go version go1.25.13 linux/amd64' if key == 'go' else p.VERSIONS[key]) for key, value in tools.items()} + pack_tools['stager_node'] = pack_tools['node'] + put('npm-pair/completion.json', dict(schema='dual-authoring-npm-completion/v1', identity=identity, status='CANDIDATE', authoring_mode=p.MODE, + asset_scope='linux-amd64-pair', candidate_sha256=candidate_pin, packs=packs, tools=pack_tools, **claims)) + completion_pin = p.digest(root / 'npm-pair/completion.json') + put('npm-native.json', dict(stage=dict(repo=str(ROOT)), completionDigest=completion_pin)) + native = dict(kind='actual-linux-private-npm-pair', identity=identity, candidate_sha256=candidate_pin, completion_sha256=completion_pin, + packs=packs, tools=pack_tools, invocations=741, + inventory_sha256=p.digest(ROOT / 'cli/plugin-kit-ai/cmd/plugin-kit-ai/release_compat.go'), **claims) + put('native/native-completion.json', native) + put('native/invocations.json', invocation_fixture()) + request = dict(expectedCommit=sha, nativeConfigSha256=p.digest(root / 'npm-native.json'), + nativeCompletionSha256=p.digest(root / 'native/native-completion.json')) + inputs = dict(projects=[dict(product=x, lane=l, source=str(root / x / l)) for x in p.PRODUCTS for l in p.LANES]) + scripts = ROOT / 'npm/agentplugins/scripts' + sealed = dict(schema='packed-installer-bridge/v1', request=request, inputs=inputs, verifier_sha256=p.digest(scripts / 'packed-installer-bridge.js'), + helper_sha256=p.digest(scripts / 'dual-authoring-candidate.js'), **claims) + put('bridge-config/request.json', request); put('bridge-config/sealed.json', sealed) + seal_pin = p.digest(root / 'bridge-config/sealed.json') + put('results/completion.json', dict(plan_fixture(), kind='packed-generated-existing-injected-installer-planner', inputs=inputs, config_sha256=seal_pin, commit=sha, **claims)) + put('journey.json', dict(identity=identity, manifest_sha256=candidate_pin, trees=[[{}]*5]*2, platform_acceptance=False, attested=False)) + names = [p.NAME] + [p.NAME + '/' + x + '/' + l for x in p.PRODUCTS for l in p.LANES] + transcripts = {'head': sha + '\n', 'clean': '', 'terminal-clean': '', + 'candidate-native': tap([p.CANDIDATE_TEST]).replace('TAP version 13\n', 'TAP version 13\n# native journey evidence: ' + str(root / 'journey.json') + '\n'), + 'npm-native': tap(p.NATIVE_TESTS), 'seal': seal_pin + '\n', 'post-verify': json.dumps(inputs), + 'stage': json.dumps(dict(manifest_sha256=candidate_pin, **claims)), + 'verify': json.dumps(dict(manifest_sha256=candidate_pin, consistency_verified=True, **claims)), + 'discovery': go([dict(Action='output', Output=p.NAME+'\n'), dict(Action='pass')]), + 'planner': go([dict(Action=a, Test=n) for n in names for a in ('run','pass')] + [dict(Action='pass')])} + for name in ('head', 'clean', 'go-version', 'node-version', 'npm-version', 'go-host', 'warmup', 'stage', 'verify', + 'candidate-native', 'npm-stage', 'npm-native', 'seal', 'discovery', 'planner', 'post-verify', 'terminal-clean'): + argv = []; env = dict(GOPROXY='off', GOSUMDB='off', GOENV='off', GOTOOLCHAIN='local', GOVCS='*:off') + if name in ('discovery', 'planner'): + argv = [tools['go']['path'], 'test', '-p=2', '-tags=packedci', '-json', + *([] if name == 'discovery' else ['-count=1','-timeout=20m']), '-list' if name == 'discovery' else '-run', p.REGEX, p.PACKAGE_PATH] + if name in ('candidate-native', 'npm-native'): + argv = [tools['node']['path'], '--test', '--test-reporter=tap', 'npm/agentplugins/test/' + ('dual-authoring-candidate-native.test.js' if name == 'candidate-native' else 'private-npm-native.test.js')] + env.update(dict(UAP_CANDIDATE_NATIVE_CONFIG=str(root / 'verify.json'), UAP_CANDIDATE_NATIVE_SOURCE_REPO=str(ROOT)) if name == 'candidate-native' else dict(UAP_PRIVATE_NPM_NATIVE_CONFIG=str(root / 'npm-native.json'))) + commands = { + 'stage': ('stage-dual-authoring-candidate.js', ['stage', '--candidate', str(root / 'stage.json')]), + 'verify': ('stage-dual-authoring-candidate.js', ['verify', '--candidate', str(root / 'verify.json')]), + 'npm-stage': ('stage-dual-authoring-npm.js', ['--candidate', str(root / 'npm-stage.json')]), + 'seal': ('packed-installer-bridge.js', ['seal', str(root / 'bridge-config/request.json'), str(root / 'bridge-config/sealed.json')])} + if name in commands: + script, args = commands[name]; argv = [tools['node']['path'], str(scripts / script), *args] + if name == 'post-verify': argv = [tools['node']['path'], str(scripts / 'packed-installer-bridge.js'), 'verify', str(root / 'bridge-config/sealed.json'), seal_pin, sha] + put('logs/' + name + '.json', dict(exit=0, argv=argv, env=env, cwd=str(ROOT))) + put('logs/' + name + '.stdout', transcripts.get(name, '')) + put('logs/' + name + '.stderr', '') + return root, sha, put, get + + def test_terminal_chain_and_negative_controls(self): + root, sha, put, get = self.fixture(); p.check(root, sha) + changes = [('results/completion.json', 'kind', 'candidate-only'), ('run.json', 'head', 'b'*40), ('run.json', 'versions', dict(p.VERSIONS, npm='12.0.2')), + ('logs/npm-native.json', 'exit', 1), ('logs/npm-native.json', 'env', {}), + ('logs/planner.json', 'argv', []), ('native/native-completion.json', 'inventory_sha256', '0'*64), + ('native/native-completion.json', 'invocations', 740), ('results/completion.json', 'config_sha256', '0'*64), + ('bridge-config/sealed.json', 'helper_sha256', '0'*64), ('candidate/candidate.json', 'identity', {}), + ('npm-pair/completion.json', 'candidate_sha256', '0'*64), ('results/completion.json', 'attested', True)] + for name, key, value in changes: + with self.subTest(name=name, key=key): + original = get(name); bad = dict(original); bad[key] = value; put(name, bad) + with self.assertRaises((ValueError, KeyError)): p.check(root, sha) + put(name, original) + (root / 'logs/npm-native.stdout').unlink() + with self.assertRaises(OSError): p.check(root, sha) + + +class PublicControls(unittest.TestCase): + def test_public_terminal_and_matrix_controls(self): + root, sha, put, get = TerminalControls.fixture(self) + # Reuse only the synthetic planner transcripts; no native claim. + private = get('run.json'); tools = private['tools']; claims = {k: False for k in p.CLAIMS} + request = dict(intake='public-fixture/v1', expectedCommit=sha, nativeConfig=str(root / 'public-config.json')) + put('public-config.json', dict(evidenceOutput=str(root / 'public-native'))) + put('public-native/invocations.json', []) + native = dict(schema='dual-authoring-public-native/v1', status='completed', identity=private['identity'], + qualification=None, tools=tools, invocations_sha256=p.digest(root / 'public-native/invocations.json'), + signed_promotion=False, public_eligible=False, **claims) + put('public-native/public-native-completion.json', native) + request.update(nativeConfigSha256=p.digest(request['nativeConfig']), + nativeCompletionSha256=p.digest(root / 'public-native/public-native-completion.json')) + marker = json.dumps(dict(file=str(root / 'public-native/public-native-completion.json'), sha256=request['nativeCompletionSha256'], source=sha)) + put('public.tap', tap([p.PUBLIC_TEST]).replace('TAP version 13\n', 'TAP version 13\n# public native completion: ' + marker + '\n')) + options = dict(request=request, nativeTap=str(root / 'public.tap'), nativeTapSha256=p.digest(root / 'public.tap'), + go=tools['go']['path'], node=tools['node']['path'], modCache=str(root / 'unused-modules')) + put('public-run.json', dict(schema='public-packed-run/v1', head=sha, options=options, tools=tools, **claims)) + sealed = get('bridge-config/sealed.json'); sealed['request'] = request + inventory_root = root / 'sealed-tree'; inventory_root.mkdir() + entries = [dict(path='.', mode=inventory_root.stat().st_mode & 0o777, kind='directory')] + sealed['inputs']['snapshots'] = [dict(root=str(inventory_root), entries=entries, + sha256=p.hashlib.sha256((json.dumps(entries, indent=2) + '\n').encode()).hexdigest())] + put('bridge-config/request.json', request); put('bridge-config/sealed.json', sealed) + pin = p.digest(root / 'bridge-config/sealed.json') + result = get('results/completion.json'); result.update(config_sha256=pin, inputs=sealed['inputs']); put('results/completion.json', result) + put('logs/post-verify.stdout', json.dumps(sealed['inputs'])) + put('logs/seal.stdout', pin + '\n') + for name in ('head', 'clean', 'terminal-clean', 'post-verify', 'discovery', 'planner'): + phase = get('logs/' + name + '.json') + if name == 'head': phase['argv'] = ['/usr/bin/git', 'rev-parse', 'HEAD'] + if name in ('clean', 'terminal-clean'): phase['argv'] = ['/usr/bin/git', 'status', '--porcelain=v1', '--untracked-files=all'] + if name == 'post-verify': phase['argv'][-2] = pin + if name in ('discovery', 'planner'): + phase['env'].update(UAP_PACKED_INSTALLER_NODE=tools['node']['path'], + UAP_PACKED_INSTALLER_CONFIG=str(root / 'bridge-config/sealed.json'), UAP_PACKED_INSTALLER_CONFIG_SHA256=pin, + UAP_PACKED_INSTALLER_COMMIT=sha, UAP_PACKED_INSTALLER_OUTPUT=str(root / 'results/completion.json')) + put('logs/' + name + '.json', phase) + p.check_public(root, sha) + valid_tap = (root / 'public.tap').read_text() + for bad in [tap([p.PUBLIC_TEST]), valid_tap.replace(sha, 'b'*40), valid_tap + '# public native completion: ' + marker + '\n']: + with self.assertRaises(ValueError): p.public_tap(bad, request) + for file, key, value in [('public-run.json', 'head', 'b'*40), ('logs/planner.json', 'argv', []), + ('logs/discovery.json', 'env', {}), ('logs/post-verify.json', 'exit', 1), + ('results/completion.json', 'plans', result['plans'][:-1]), + ('results/completion.json', 'plans', [result['plans'][0]] + result['plans'][:-1]), + ('results/completion.json', 'attested', True), ('bridge-config/sealed.json', 'helper_sha256', '0'*64)]: + original = get(file); put(file, dict(original, **{key: value})) + with self.subTest(file=file, key=key), self.assertRaises((ValueError, KeyError)): p.check_public(root, sha) + put(file, original) + (inventory_root / 'unexpected').mkdir() + with self.assertRaisesRegex(ValueError, 'sealed tree changed'): p.check_public(root, sha) + (inventory_root / 'unexpected').rmdir() + for text in [tap([p.PUBLIC_TEST])[:-1], tap([p.PUBLIC_TEST]).replace('# skipped 0', '# skipped 1'), tap(p.NATIVE_TESTS)]: + put('public.tap', text) + with self.assertRaises(ValueError): p.check_public(root, sha) + + def test_public_runner_rejects_missing_or_wrong_contract_before_output(self): + root = Path(tempfile.mkdtemp(prefix='public-packed-runner-SYNTHETIC-')) + options = root / 'options.json'; output = root / 'must-not-exist' + for value in ({}, dict(request={'intake': 'private'}, nativeTap='/unused', nativeTapSha256='0'*64, go='/unused', node='/unused', modCache='/unused')): + options.write_text(json.dumps(value)) + with self.assertRaises(ValueError): r.public_main(output, 'a'*40, options) + self.assertFalse(output.exists()) + + +class WorkflowControls(unittest.TestCase): + def test_runner_context_rejected_only_at_job_env_scope(self): + text = (ROOT / '.github/workflows/authoring-native.yml').read_text() + runner = (ROOT / 'scripts/run-packed-ci.py').read_text() + for expression in ('${{ runner.temp }}', "${{ runner['temp'] }}"): + bad = text.replace(" PYTHONDONTWRITEBYTECODE: '1'", + ' PACKED_ROOT: ' + expression + '/authoring-packed-${{ github.run_id }}-${{ github.run_attempt }}\n' + + " PYTHONDONTWRITEBYTECODE: '1'", 1) + with self.subTest(expression=expression), self.assertRaisesRegex(ValueError, 'runner context is unavailable in job env'): + w.check(bad, runner) + # The same context is supported at step scope. + good = text.replace(' - name: Build, pack, execute, seal and plan at the exact checkout SHA\n', + ' - name: Build, pack, execute, seal and plan at the exact checkout SHA\n' + ' env:\n STEP_TEMP: ${{ runner.temp }}\n') + w.check(good, runner) + + def test_committed_graph_and_mutations(self): + text = (ROOT / '.github/workflows/authoring-native.yml').read_text() + runner = (ROOT / 'scripts/run-packed-ci.py').read_text() + w.check(text, runner) + mutations = [(' packed:\n', ' omitted:\n'), ('needs: [native, packed]', 'needs: [native]'), + (' if: always()\n', ' if: success()\n'), (' packed:\n', ' packed:\n if: false\n'), + (' native:\n', ' native:\n if: false\n'), ('arch: arm64}', 'arch: amd64}'), + (' pull_request:\n', ' pull_request:\n paths: [scripts/**]\n'), + (' timeout-minutes: 45', ' continue-on-error: true\n timeout-minutes: 45'), + ('scripts/check-packed-ci.py "$PACKED_ROOT"', 'scripts/missing.py "$PACKED_ROOT"'), + ('scripts/check-packed-workflow.py --results', 'scripts/omitted.py --results'), + (' run: python3 -B scripts/run-packed-ci.py', ' if: false\n run: python3 -B scripts/run-packed-ci.py')] + for before, after in mutations: + with self.subTest(before=before), self.assertRaises(ValueError): w.check(text.replace(before, after), runner) + for before, after in [("'-tags=packedci', ", ''), ('proof.REGEX', "'^TestOther$'"), ('proof.PACKAGE_PATH', "'./wrong'"), ('proof.check(root, sha)', '')]: + with self.assertRaises(ValueError): w.check(text, runner.replace(before, after)) + + def test_dependencies_must_all_succeed(self): + good = dict(native=dict(result='success'), packed=dict(result='success')); w.results(good) + for job in good: + for status in ('failure', 'skipped', 'cancelled', None): + bad = copy.deepcopy(good); bad[job]['result'] = status + with self.assertRaises(ValueError): w.results(bad) + bad = copy.deepcopy(good); del bad[job] + with self.assertRaises(ValueError): w.results(bad) + + +if __name__ == '__main__': unittest.main()