Publishing
You publish from a public GitHub repository you own. The registry indexes your workflow at a pinned commit — your repo stays the source of truth.
Package shape
A publishable package is a directory in your repository containing:
my-workflow/
├── workflow.json # the manifest — declared metadata
├── README.md # rendered on your workflow's page
└── .sandcastle/ # the payload — what gets installed
├── main.ts # entrypoint calling Sandcastle's run()
├── prompts/*.md # prompt files
├── Dockerfile
└── .env.exampleThe manifest declares identity, agents, env vars, disclosures, topology, and the exhaustive file list. It is declared, not authoritative: your orchestration code is the truth, and validation cross-checks the two. See the manifest reference.
The publishing flow
runcastle init— scaffold a package: manifest skeleton, payload layout, README stub.runcastle validate— run the full rule set locally. The server re-runs the exact same checks at publish time with identical error codes, so a clean local validate means a clean publish.runcastle publish— authenticate with GitHub, point at your repo (and directory, if the package is nested). The registry verifies you own or admin the repository, fetches the files at the current commit, computes checksums, and caches everything.
Pinned SHA and immutable versions
Publishing pins your package to the exact commit SHA it was published from. That SHA is the tamper-evidence anchor: installers fetch files at this commit and verify them against publish-time checksums. Even if the repository is later force-pushed, existing versions keep resolving to the cached, checksummed files.
Versions are immutable. Republishing an existing version number is rejected with a 409 Conflict — bump the semver instead. There is no way to edit a published version; publish a new one.
Validation rules
Validation exists so that what a reader sees on the site is what runs on their machine. Blocking errors include:
Static analyzability
- Restricted imports — the entrypoint may only import from an allowlist (Sandcastle and a small set of safe modules), so its behavior can be read from the source.
- Literal hooks — every lifecycle hook (
hooks.host.*,hooks.sandbox.*) must have a string-literalcommand. Dynamically-built commands cannot be reviewed, so they cannot be published.
Disclosure parity
Anything with side-effect potential must be declared in the manifest and detected in the code, and the two must match: host hooks, sandbox hooks, and !`command` shell-expansion blocks in prompt files (which require disclosures.usesShellExpansion). An undeclared hook is a blocking error, not a warning.
Hygiene checks
- Secret scan — anything that looks like a real API key or credential in package files blocks publish.
- Env parity— env vars referenced by the payload must be declared in the manifest's
envlist (and vice versa). - Exhaustive files list —
filesmust list every file the package installs; undeclared files in the payload or missing declared files block publish.
Warnings vs errors
Some findings are warnings rather than blocks — e.g. a missing topology, or promptFilepaths that won't survive a namespaced install. Fix them anyway; they affect how usable your workflow is.
Unlisting
You (or a registry maintainer) can unlista workflow at any time. Unlisting is a soft-delete: the workflow disappears from browse and search, but existing pinned installs keep working via the registry's cached files — consumers who depend on it are not broken. Unlist is not deletion, and it is weaker than a maintainer block, which cuts off file downloads for new installs entirely.