@ember-sovereignty/provisioner
v0.1.1
Published
Ember vendor provisioner — image + index card + egress manifest in, SCA-governed contained CVM out. Compose render, governance ceremony, attestation evidence, read-only console, ember CLI.
Readme
Provisioner — vendor image + index card in, governed contained CVM out
Implements the vendor release pipeline (plans/VENDOR-RELEASE-PLAN.md,
issue #54 records the deferred dynamic-egress variant). Generalizes the
RAG demo's live-verified path (demo/anythingllm/phala/): a vendor
submits a digest-pinned image, an env index card, and an egress manifest;
the provisioner renders the contained compose (vey proxy as sole exit,
policy inlined and therefore measured; an empty manifest renders the
no-exit variant — see Intake rules), runs the governance ceremony
(DstackApp on Base, SCA addComposeHash), deploys the CVM with sealed
secrets, and captures silicon↔chain evidence.
This runs locally — it is not a hosted service
Stale as of 2026-08-03: it IS hosted. The provisioner runs on Railway at
https://provisioner-production-b5b7.up.railway.app, and/onboarding/tells vendors to pointEMBER_APIat it — that is how self-serve onboarding works and how the Onyx and ToolHive dry runs were done. The argument below still describes the real exposure (admin authority over CVM deploys and chain ceremonies), and it is worth re-reading before widening that surface; it just no longer describes how we operate. Note the mitigation that landed with hosting: the pipeline can never sign a governance approval (P9, #82), so a compromised admin token cannot approve a measurement. See../MVP.md.
The provisioner is an npm package you run on your own machine, not
something to deploy to Railway/Fly/etc. npm run dev (or the real-driver
boot) starts the API, the console, and the CLI's endpoint locally; the
operator runs it with the admin token and Phala/Base credentials in their
shell for the duration of a provisioning ceremony, then stops it. The
only thing it deploys to a cloud is the vendor's CVM (to Phala) —
the provisioner itself never needs to be reachable on the public
internet, and hosting it there would just expose admin authority over
CVM deployments and chain ceremonies for no benefit.
ember CLI / read-only console
│
provisioner API (api.ts — vendor bearer tokens, admin token)
│
ProvisionerService (provisioner.ts — validate → render → measure →
│ ceremony → sealed commit → evidence)
┌────┴─────┐
CvmDriver Governance (drivers.ts — the only seams to the world)
│ │
Phala Cloud Base SCA real: phala.ts / chain.ts (⚠️ see below)
FakeCvm FakeGov dev/test doubles, same code path + semanticsRun it
npm install # .npmrc pins legacy-peer-deps (permissionless/ox peer skew)
npm test # includes verify vs the REAL committed quote
./test/zero-egress/run-tests.sh # zero-egress containment proof (needs docker)
npm run dev # everything on the fakes; seeds a RAG-demo-shaped tenant
# console: http://localhost:4100 (token printed at boot)
# POST /dev/approve/:id = the owner SCA's wallet doublePublish to npm
./publish.sh does the whole flow: npm login (only if not already
authenticated), a clean npm ci, npm pack --dry-run to show what
ships, then npm publish. There is no separate compile step — the
package ships TypeScript source and the CLI runs it via tsx (a runtime
dependency); prepublishOnly re-runs typecheck + tests as the final
gate inside npm publish. Requires publish rights to the
@ember-sovereignty org. files in package.json keeps tests and
src/test-support/ out of the tarball. Note that a public npm package
makes this directory's source publicly readable.
CLI (bin/ember.mjs, no build step):
export EMBER_API=http://localhost:4100 EMBER_TOKEN=emv_…
ember init # scaffold ember.json (the index card)
ember deploy # secrets read from your shell env, sealed at deploy
ember status | list
ember egress add api.example.com # a governed ceremony, not a config edit
ember release name@sha256:… # new vendor release, same ceremony
ember verify # trustless of this service: quote + Base RPC directlySelf-custody release governance (#65, P9/#82)
The pipeline never signs. Every deployment names the SCA that owns
its DstackApp at create (ownerSca — the vendor org's SCA by default
with an ember login credential, explicit and required with an emv_
token; an institution's SCA for institution-governed apps). Fail-closed:
no owner, no deployment — there is no Ember-held default, and the
Governance driver contains no signing capability at all.
Every governed change is two-stage:
- Propose —
ember release/egress addmeasures the new compose; the deployment goespending-approval; nothing is signed. - The owner approves — the owning SCA's holders review the proposal
(the console's detail view renders the image/egress/compose diff and
the exact hash to sign) and run
addComposeHashout-of-band from their own wallet. - Pipeline commits — the approval watcher polls
allowedComposeHashes(hash)on Base (APPROVAL_POLL_MS, default 30s) and, the moment it flips true, upgrades the CVM and recaptures evidence. The chain is the approval bus — no trust in our database, and no code path commits without it.
A second watcher reconciles the record with the platform (#136): every
RECONCILE_POLL_MS (default 60s) it reads the CVM state of settled
deployments and flips running↔stopped when the platform disagrees
(e.g. a billing suspension stopping every CVM on the account), recording
the raw report as platformStatus/platformCheckedAt. ember status
additionally live-probes each endpoint (HEAD, 5s timeout) and prints
reachability next to the recorded lifecycle status.
ember release --wait polls until the proposal commits; the default is
async ("proposal recorded; awaiting the owner SCA's approval"). Timeout /
withdrawal / rejection semantics are decided (Chris, 2026-07-30; #65) —
explicit off-chain withdraw/reject + proposal TTL, removeComposeHash as
the on-chain revocation of a granted approval, pre-commit isAllowed
re-check, and allowlist pruning to the current hash after commit. Full
rationale: plans/SAML-SSO-ACTION-SIGNER-PLAN.md ("#65 proposal-lifecycle
mechanism"). Not yet implemented.
Dev/test doubles for the owner's wallet: npm run dev exposes
POST /dev/approve/:id; unit tests call approveOutOfBand on the fake;
scripts/live-dogfood.ts holds a funded test-vendor Kernel SCA in the
harness and signs the real userop — the pipeline under test never signs.
ember verify is the product's honest line, executable: it fetches the
quote (live with PHALA_CLOUD_API_KEY, else the stored one), hashes the
measured app_compose, and checks allowedComposeHashes + owner() on
Base (BASE_RPC_URL, default mainnet). The same function ran green
against the real contract 0x4697B0De…F428e with the committed
2026-07-28 quote.
Intake rules (v1)
- Image:
name@sha256:<digest>only; tags rejected. Must belinux/amd64and anonymously pullable (ghcr.io / Docker Hub public repos; not an expiring registry like ttl.sh — the pinned digest must stay pullable for the deployment's lifetime). - Egress: exact hostnames on 443 only — each becomes a docker alias
on the vey container + a measured SNI
exact_match. No wildcards, no suffixes (needs Mode A/two-CVM), no IP literals. Any change = new compose hash = SCA approval = upgrade ceremony.egress: []is first-class: the render carries no vey at all (nothing to allowlist means no exit exists) — the app stays on the internal-only network with a loopback DNS upstream, so it can initiate no outbound TCP and no outbound DNS while its declared ports keep serving through the ingress relays. The compose states it with a column-0# egress-policy: nonemarker (what verify/console/trust surfaces read as "in-measure zero", distinct from "couldn't parse"). The field stays required: an absent list is a 400 — only an explicit[]declares zero. A custom domain is refused on a zero-egress deployment (the domain-ingress sidecar performs its own ACME/DNS-API egress, which would falsify the claim — fail-closed until the sidecar is contained). Containment proof:test/zero-egress/run-tests.sh(docker; also a CI job) — inbound answers, outbound TCP/DNS die, and a positive control shows an uncontained container on the same host CAN get out. - Secrets: names declared in the spec; values read at deploy and sealed via Phala's encrypted env — never stored, never in the compose (placeholders only, verified in tests against the real quote).
- Resources: optional
spec.resources(cpus≤ 16,memoryMb≤ 32768,diskGb≤ 200) sizes the CVM at create — the driver maps it to the smallest TDX instance that fits (defaulttdx.medium, 40 GB). Never part of the measurement; not resizable after create.
What's real vs pending
- Orchestration, validation, render, ceremony sequencing, evidence
verification: tested here, incl. against the live CVM's committed
attestation (
src/verify.test.ts). - Whole lifecycle proven live (2026-07-28,
scripts/live-dogfood.ts): create — the RAG demo image to a running attested CVM in ~2 min, with the DstackApp owned by the governance SCA from block one (the factory'sinitialOwnerparameter; the gas-paying EOA never holds authority, no ownership transfer exists). Update — a live egress-add ceremony in 74s: new measured hash, SCAaddComposeHashuserop (tx0xded8c4c3…8794), CVM upgrade,ember verifygreen with both hosts decoded from the live quote. CVMs stopped after (cost discipline). Since P9 (#82) the signer lives in the dogfood harness (playing the vendor's wallet), not the product; the owner SCA must hold gas before its first userop — the harness funds it explicitly, never implicitly. - Read-only console (
public/): list + detail; the egress panel renders from the measured compose when evidence exists, not our records. - Store: memory/JSON-file; Postgres rides issue #3. Vendor auth: bearer tokens; passkey→SCA rides issue #2.
