Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions cmd/pilot-app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
package main

import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -212,6 +216,7 @@ func cmdVerifySubmission(args []string) {
fatalf("%v", err)
}
defer os.RemoveAll(tmp)
hasArtifacts := len(sub.Artifacts) > 0
allOK := true
for _, p := range b.Platforms {
path := filepath.Join(tmp, p.TarballName)
Expand All @@ -231,6 +236,19 @@ func cmdVerifySubmission(args []string) {
}
fmt.Printf(" %s %-34s %s\n", mark, c.Name, c.Msg)
}
// Close the native-delivery gap: a submission that declares `artifacts`
// MUST yield a bundle that actually carries install.json AND a manifest
// wired for staging (the fs.write $APP grant the asset-aware adapter
// needs). Absent either, the published app would have no binary to run —
// exactly the silent breakage that slipped through before this check.
if hasArtifacts {
if msg, ok := checkStaging(p.Tarball); ok {
fmt.Printf(" ✓ %-34s %s\n", "native-delivery (install.json)", msg)
} else {
fmt.Printf(" ✗ %-34s %s\n", "native-delivery (install.json)", msg)
allOK = false
}
}
}
if !allOK {
fmt.Fprintln(os.Stderr, "\nVERIFY FAILED — fix the ✗ items before submitting.")
Expand All @@ -239,6 +257,104 @@ func cmdVerifySubmission(args []string) {
fmt.Printf("\nVERIFY OK — built + verified %d platform(s) from the submission spec.\n", len(b.Platforms))
}

// checkStaging inspects one built platform tarball and confirms the native
// asset-delivery wiring is present: install.json (the registry staging spec the
// adapter reads at startup) AND a manifest whose grants include fs.write $APP
// (the capability the staging runtime needs to write the fetched binary). It
// returns a human-readable message and whether the bundle is staging-ready.
//
// This is what closes the false-pass: before this, a submission that declared
// artifacts but whose build dropped them (no install.json, no staging grant)
// still passed verify-submission, because the catalogue gate only checks the
// binary sha/signature and is blind to install.json.
func checkStaging(tarball []byte) (string, bool) {
files, err := tarballFiles(tarball)
if err != nil {
return fmt.Sprintf("read bundle: %v", err), false
}
spec, hasInstall := files["./install.json"]
if !hasInstall {
if _, alt := files["install.json"]; alt {
spec, hasInstall = files["install.json"], true
}
}
if !hasInstall {
return "submission declares artifacts but the built bundle has NO install.json — the adapter would have no binary to stage", false
}
// install.json must name a command and at least one asset.
var is struct {
Command string `json:"command"`
Assets []struct {
ExecPath string `json:"exec_path"`
} `json:"assets"`
}
if err := json.Unmarshal(spec, &is); err != nil {
return fmt.Sprintf("install.json present but unparseable: %v", err), false
}
if len(is.Assets) == 0 {
return "install.json present but lists no assets", false
}
// The manifest must grant fs.write $APP so the staging runtime can write the
// fetched binary under $APP — proof the adapter is wired for delivery, not
// just that the spec file rode along.
mfRaw, hasMf := files["./manifest.json"]
if !hasMf {
mfRaw, hasMf = files["manifest.json"]
}
if !hasMf {
return "install.json present but manifest.json missing from bundle", false
}
var mf struct {
Grants []struct {
Cap string `json:"cap"`
Target string `json:"target"`
} `json:"grants"`
}
if err := json.Unmarshal(mfRaw, &mf); err != nil {
return fmt.Sprintf("manifest.json unparseable: %v", err), false
}
stagingGrant := false
for _, g := range mf.Grants {
if g.Cap == "fs.write" && g.Target == "$APP" {
stagingGrant = true
break
}
}
if !stagingGrant {
return "install.json present but manifest lacks the fs.write $APP grant the staging adapter needs", false
}
return fmt.Sprintf("install.json + staging grant present (%d asset(s))", len(is.Assets)), true
}

// tarballFiles reads a gzipped tar bundle into a map of header-name → contents.
func tarballFiles(tarball []byte) (map[string][]byte, error) {
gz, err := gzip.NewReader(bytes.NewReader(tarball))
if err != nil {
return nil, err
}
defer gz.Close()
out := map[string][]byte{}
tr := tar.NewReader(gz)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hdr.Typeflag != tar.TypeReg {
continue
}
b, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
out[hdr.Name] = b
}
return out, nil
}

func cmdSubmit(args []string) {
fs := flag.NewFlagSet("submit", flag.ExitOnError)
dir := fs.String("C", ".", "project dir (holds manifest.json + the built tarball)")
Expand Down
153 changes: 153 additions & 0 deletions cmd/pilot-app/verify_staging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"io"
"testing"

"github.com/pilot-protocol/app-template/internal/publish"
)

// cliArtifactsSubmission is the miren-shaped fixture: a cli backend (id
// io.pilot.<x>) whose binary is DELIVERED from the R2 artifact registry — the
// artifacts[] step with per-platform os/arch/url/sha256/unpack/exec_path. This
// is exactly the shape whose absence on main produced broken published bundles.
func cliArtifactsSubmission() publish.Submission {
return publish.Submission{
ID: "io.pilot.miren",
Version: "0.1.0",
Description: "Delivers and fronts the miren CLI from the registry.",
Email: "ops@pilotprotocol.network",
Backend: publish.SubBackend{
Type: "cli",
Command: []string{"miren"},
},
Methods: []publish.SubMethod{
{Name: "miren.version", Description: "Print the miren version.", Latency: "fast",
CLI: publish.SubCLIRoute{Args: []string{"version"}}},
{Name: "miren.exec", Description: "Run any miren subcommand.", Latency: "med",
Params: []publish.SubParam{{Name: "args", Type: "array"}},
CLI: publish.SubCLIRoute{Passthrough: true}},
},
Listing: publish.SubListing{DisplayName: "Miren", License: "MIT", Categories: []string{"dev"}, AppDescription: "Miren on Pilot."},
Vendor: publish.SubVendor{Name: "Pilot", AgentUsage: "agents drive miren", Capabilities: "microvm"},
Artifacts: []publish.SubArtifact{
{OS: "darwin", Arch: "arm64", URL: "https://pub-x.r2.dev/io.pilot.miren/0.1.0/darwin-arm64/miren.tar.gz",
SHA256: "1111111111111111111111111111111111111111111111111111111111111111",
Unpack: "tar.gz", ExecPath: "miren-0.1.0-darwin-arm64/miren", Order: 1},
{OS: "linux", Arch: "amd64", URL: "https://pub-x.r2.dev/io.pilot.miren/0.1.0/linux-amd64/miren",
SHA256: "2222222222222222222222222222222222222222222222222222222222222222",
ExecPath: "bin/miren", Order: 1},
},
}
}

// TestVerifyStagingGate_BuildsWiredBundle proves the positive path: a cli
// submission WITH artifacts builds a bundle that actually contains install.json
// and the StageAssets-wired adapter (manifest fs.write $APP grant), so
// checkStaging passes on every platform.
func TestVerifyStagingGate_BuildsWiredBundle(t *testing.T) {
if testing.Short() {
t.Skip("cross-compiles the adapter for all platforms; skipped under -short")
}
sub := cliArtifactsSubmission()
if errs := sub.Validate(); len(errs) != 0 {
t.Fatalf("fixture must validate, got: %v", errs)
}
_, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
b, err := publish.BuildBundle(sub.ToConfig(), priv)
if err != nil {
t.Fatalf("BuildBundle: %v", err)
}
if len(b.Platforms) == 0 {
t.Fatal("no platforms built")
}
for _, p := range b.Platforms {
msg, ok := checkStaging(p.Tarball)
if !ok {
t.Errorf("platform %s: staging check must pass for an artifact-delivering app, got: %s", p.Platform, msg)
}
}
}

// TestVerifyStagingGate_FailsWhenStagingStripped is the regression guard: if a
// build produced platform bundles WITHOUT install.json (the exact silent
// breakage that let broken bundles publish), checkStaging — and therefore
// verify-submission — must FAIL. We simulate a stripped bundle by rebuilding the
// tarball without install.json and without the fs.write $APP grant.
func TestVerifyStagingGate_FailsWhenStagingStripped(t *testing.T) {
if testing.Short() {
t.Skip("cross-compiles the adapter; skipped under -short")
}
sub := cliArtifactsSubmission()
_, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
b, err := publish.BuildBundle(sub.ToConfig(), priv)
if err != nil {
t.Fatalf("BuildBundle: %v", err)
}
stripped := stripStaging(t, b.Primary().Tarball)
if msg, ok := checkStaging(stripped); ok {
t.Fatalf("staging check MUST fail for a bundle missing install.json, but it passed: %s", msg)
}
}

// stripStaging rewrites a bundle tarball dropping install.json/install.sh and
// the manifest's fs.write $APP grant — modelling a build that declared artifacts
// but silently failed to wire native delivery.
func stripStaging(t *testing.T, tarball []byte) []byte {
t.Helper()
gz, err := gzip.NewReader(bytes.NewReader(tarball))
if err != nil {
t.Fatal(err)
}
defer gz.Close()
var buf bytes.Buffer
outGz := gzip.NewWriter(&buf)
tw := tar.NewWriter(outGz)
tr := tar.NewReader(gz)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
name := hdr.Name
if name == "./install.json" || name == "install.json" ||
name == "./install.sh" || name == "install.sh" {
continue // drop the staging spec
}
body, err := io.ReadAll(tr)
if err != nil {
t.Fatal(err)
}
if name == "./manifest.json" || name == "manifest.json" {
body = bytes.ReplaceAll(body,
[]byte(`{"cap": "fs.write", "target": "$APP"},`), nil)
}
hdr.Size = int64(len(body))
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(body); err != nil {
t.Fatal(err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
if err := outGz.Close(); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
12 changes: 11 additions & 1 deletion docs/NATIVE-APPS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Native (binary-delivery) apps — design

> Status: DESIGN + TODO. Native/CLI apps are **Coming soon** — blocked at the
> **SUPERSEDED (2026-06-22) for the delivery model.** This doc proposed delivering
> native binaries *by reference* (customer-hosted URL, "we never store the bytes").
> The shipped implementation instead **hosts the bytes in a Pilot-run Cloudflare
> R2 artifact registry**: the publisher uploads per-OS/arch binaries in the
> publish form's Artifacts step, and the generated cli adapter fetches + verifies
> + stages + execs them at install (with install order + optional args). See
> **`docs/R2-ARTIFACT-REGISTRY.md`** for the canonical, implemented design. The
> `assets[]` schema and the daemon-side staging notes below remain useful
> background, but where they disagree with R2-ARTIFACT-REGISTRY.md, that doc wins.

> Status (original): DESIGN + TODO. Native/CLI apps are **Coming soon** — blocked at the
> wizard's type step; only HTTP (translation-only) apps ship today. Decision
> (2026-06-17): native apps deliver the real binary via a **customer-hosted URL +
> per-OS/arch sha256**, pinned in the signed manifest and **fetched + verified +
Expand Down
Loading
Loading