Documentation _
Everything you need to use, publish, and deploy agents on PHOSPHOR. This page is self-contained — every section below has the full reference. Repo source-of-truth paths are cited at the end of each section in case you want to read the implementation directly.
Architecture
How PHOSPHOR separates marketplace, sandbox runtime, policy engine, and audit. Service topology on Zeabur with internal DNS.
read >User Guide
Sign in, browse the catalog, acknowledge permissions, run agents inline, deploy to a sealed runtime.
read >Publisher Guide
Write a PHOSPHOR manifest, package with phosphor-pkg, sign with ed25519, submit to the registry.
read >Safety Model
Default-deny sandbox, permission manifests, host allowlists, rate limits, CSRF, secret scrubbing, audit trail.
read >API Reference
Every /api route on phosphor-web — auth, CSRF, rate-limit, request/response shape.
read >CLIs (phosphor-pkg + phosphor)
phosphor-pkg builds + signs bundles; phosphor connects your terminal to deployed agent instances.
read >Deployment
Deploy on Zeabur with templates, environment variables, volumes, and internal DNS wiring.
read >Get a working agent run in under 60 seconds:
- 1.Go to /login and sign in — use email, Google, GitHub, or Apple via Privy.
- 2.Open Hermes or Openclaw Dench.
- 3.Tick the permissions checkbox in the RAP SHEET section (required — nothing runs until you ack).
- 4.Leave the API key blank for mock mode and click JACK IN.
- 5.Structured output appears in ~200 ms. Swap in a real
sk-ant-…key for live Claude; overrideBASE_URL/MODELto point at any OpenAI-compatible provider (e.g.https://api.openai.com/v1).
Architecture
PHOSPHOR runs on Zeabur as a small set of long-lived services plus per-tenant agent runtimes that are provisioned on demand. Every mutating call flows through three gates (auth → CSRF → entitlement) before any tool is invoked, and every tool call emits an audit event to an append-only NDJSON log.
> Service topology
phosphor-web— the marketplace UI and API. Next.js 15 (app router) running on0.0.0.0:$PORTinside a Zeabur container. Hosts/api/run,/api/deploy,/api/checkout, the magic-link auth routes, and the Stripe webhook.audit-store— append-only NDJSON store on a Zeabur volume at/var/lib/audit. Reached only from other services via internal DNSaudit-store.zeabur.internal:8080. Never exposed to the public internet. See the retention policy atservices/audit-store/RETENTION.md.phosphor-agent-runtime— a published Zeabur template (codeLW76NP) that instantiates a per-tenant agent runtime on/api/deploy. The template takes tenant-specific tokens as variables and runs the agent image declared in the catalog (e.g.ghcr.io/nousresearch/hermes-agent:latest).
> Data flow — "run an agent"
browser phosphor-web audit-store │ POST /api/run ────► session gate │ csrf gate │ rate-limit gate │ ├─► load manifest │ ├─► load runner │ ├─► Promise.race( │ │ runner(ctx), │ │ timeout(limits.maxRuntimeSec)) │ ├─► auditAppend(event) ─────────► /ingest │ ◄──── 200 result ───┤ │ └─► deny path also emits event ─► /ingest
> Data flow — "deploy an agent"
browser phosphor-web Zeabur control plane │ POST /api/deploy ─► session + csrf + rate-limit │ validateManifest() │ entitlement check (plan → agent tier) │ zeabur-template install ────────────► create service │ wire env vars │ attach volume │ ◄──── serviceId + domain │ persist instance record │ ◄──── instance ──────┤ audit-store ◄──ingest │ │ │ │ └──► internal DNS ──► runtime on *.zeabur.internal
> Source
apps/web/app/api/run/route.ts— inline run pathapps/web/app/api/deploy/route.ts— template install pathservices/audit-store/— append-only ingest servicepackages/sandbox/src/runner.ts— per-run context & policy guard
User Guide
Five steps take you from a cold browser tab to a running agent with an auditable trail.
> 1. Sign in
Hit /login and enter any email address. PHOSPHOR sends a magic link via ZSend (or Resend) if ZSEND_API_KEY / RESEND_API_KEY is configured. When no provider is configured (dev mode) the link is returned inline on the response so you can click through immediately. GitHub OAuth is also supported when GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET are set.
> 2. Browse the catalog
Open /agents for the full catalog or / for FRESH DROPS. Every card shows a risk chip (low / med / high), a compact permissions summary (net hosts, fs mode, secret count), and a tier badge. Clicking a card loads the detail page.
> 3. Acknowledge permissions
Every agent detail page has a permissions rap sheet listing the full manifest — tools, fs read/write paths, net allow-hosts, env keys, and resource limits. Neither the TRY IT NOW nor DEPLOY button will work until you tick the checkbox. If you try, the rap sheet flashes and scrolls into view.
> 4. Run it inline (free)
For inline-runnable agents (e.g. Hermes, Openclaw Dench) the TRY IT NOW panel runs the agent synchronously in phosphor-web. Leave the API key blank for deterministic mock mode — useful for demos — or paste an sk-ant-… key for live Claude. You can override the base URL and model to point at any OpenAI-compatible provider (default placeholder: https://api.openai.com/v1).
> 5. Deploy to a sealed runtime
The deploy panel provisions a per-tenant Zeabur service from the phosphor-agent-runtime template (code LW76NP), wiring your configuration as template variables. Plan tier gating applies: a pro-tier agent (like Hermes) cannot be deployed on astarter plan — the response includes a requiredTier field so the UI can prompt the upgrade.
> Source
apps/web/app/login/page.tsx— magic link UIapps/web/components/AgentDetailFlow.tsx— rap-sheet ack, run panel, deploy panel
Publisher Guide
Publishing an agent is three files and one CLI: a manifest, a runner, and a bundle signed with an ed25519 keypair.
> 1. Manifest — default-deny
A PermissionManifest v1 lists everything the agent is permitted to do. The validator (packages/policy/validate.ts) rejects allowAll: true, wildcard paths/hosts (*, **, /, ~), path traversal, relative fs paths, unknown tools, and non-semver versions. Missing fields are errors, not defaults.
{
"id": "pm-hermes-agent-v1",
"version": "1.0.0",
"displayName": "Hermes Agent",
"description": "Multi-channel triage + function-calling.",
"tools": ["Read", "Write", "HttpRequest"],
"fs": {
"allowReadPaths": ["/workspace/in"],
"allowWritePaths": ["/workspace/out"]
},
"net": {
"allowHosts": ["api.anthropic.com"],
"allowAll": false
},
"env": {
"allowedEnvKeys": ["ANTHROPIC_API_KEY", "GMAIL_TOKEN"]
},
"limits": {
"cpu": 1,
"memMb": 1024,
"maxRuntimeSec": 900,
"maxFilesizeMb": 25
}
}> 2. Runner — reads the sealed context
A runner is a single async function that accepts a RunnerContext and returns output. The context exposes only what the manifest declared: a sandboxedFetch that rejects disallowed hosts, an env map filtered to the declared keys, and a logger that writes into the audit trail. Raw process.env, raw fetch, and filesystem ops are not part of the context.
> 3. Bundle with phosphor-pkg
# one-time: generate an ed25519 keypair phosphor-pkg keygen dev # build a bundle from the source dir phosphor-pkg build ./my-agent # → packages/agents/dist/my-agent-1.0.0.phosphor # verify locally (signature + checksum + manifest) phosphor-pkg verify packages/agents/dist/my-agent-1.0.0.phosphor # list local bundles phosphor-pkg ls
The bundle is a tarball containing the manifest, the runner, a SHA-256 checksum, and an ed25519 signature over the checksum. The public key is embedded so verifiers can check the signature without a key lookup.
> 4. Submit
For the hackathon build the "registry" is the static catalog in apps/web/lib/agents.ts plus the manifests in apps/web/lib/permissions.ts. Open a PR against the repo adding your agent row + manifest + runner. A future self-serve publisher dashboard will accept signed bundles directly.
> Source
packages/agent-package/— phosphor-pkg CLIpackages/policy/src/— manifest validatorapps/web/lib/runners/— runner interface + examples
Safety Model
PHOSPHOR's safety story is not "trust the agent" — it is "default-deny everything, make the agent declare what it needs, then hold it to that declaration at every layer."
> Layer 1 — permission manifest (declaration)
- Every tool, fs path, net host, env key, and resource limit must appear in the manifest. Unknown fields are rejected.
- Wildcards (
*,**,/,~) are banned.net.allowAll: trueis banned. - Manifests are immutable — changes require a version bump and re-publish.
- A compact summary is shown on every card (risk chip + net/fs label) so users see scope before the deploy page.
> Layer 2 — runtime gating (enforcement)
sandboxedFetch(manifest)wraps outbound HTTP and rejects any host not innet.allowHosts.RunnerContext.envis a frozen object containing only keys fromenv.allowedEnvKeys. Rawprocess.envis not passed through.limits.maxRuntimeSecis enforced withPromise.race+AbortController. A stuck runner is aborted and the denial is audited.
> Layer 3 — request gates (before the runner even loads)
- Auth. HMAC-signed session cookie
phosphor_sessionset by/api/auth/callback. Missing or invalid → 401. - CSRF. Double-submit: middleware sets
phosphor_csrfcookie, client echoes it asX-Phosphor-CSRF, server compares with constant-time equality. Missing / mismatch → 403. - Rate limit. In-memory sliding window, 10 requests per minute per user per action (
run:<sub>,deploy:<sub>). Exceeded → 429 withRetry-After. - Entitlement.
assertCan(plan, action, ctx)compares plan rank to the agent's required tier. MissingagentTieronrun_agentfails closed.
> Layer 4 — append-only audit
- Every tool call — success AND denial — emits a single
AuditEventtoaudit-store.zeabur.internal:8080. Missing events are a bug; escalate rather than paper over. - Secrets are redacted at write time via
scrubError— API keys, tokens, Authorization headers, anything matching common secret patterns. - The log is append-only; deletion is never permitted. Rotation is rename+gzip, retention is a 7-day hot tier, 90-day warm tier, cold tier kept for the lifetime of the tenant + 1 year.
> What is NOT covered (honest limits)
- Inline runners live in the phosphor-web process. They are not in a separate VM. The LLM SDKs (Anthropic, OpenAI) instantiate their own HTTP clients that bypass
sandboxedFetch— a documented gap. Real sealed runtimes (via/api/deploy) run in a Zeabur container and do not share the web process. - Per-run filesystem isolation is advisory for the inline path. The sealed-runtime path uses a per-service volume.
> Source
apps/web/middleware.ts— CSRF cookie issuanceapps/web/lib/csrf.ts— double-submit verificationapps/web/lib/rate-limit.ts— sliding-window bucketapps/web/lib/sandbox.ts— sandboxed fetch + scrubapps/web/lib/billing.ts— canUseAgent / assertCan
API Reference
Every route listed here lives under apps/web/app/api/. All mutating routes require auth + CSRF unless noted; run and deploy are additionally rate-limited. All responses are JSON. Errors use { error: string }.
> POST /api/run
Execute an inline-runnable agent synchronously in phosphor-web.
- auth
- required (phosphor_session)
- csrf
- required (X-Phosphor-CSRF)
- rate-limit
- 10/min per user, key run:<sub>
- body
{ agentId, input, config?, tenantId? }- 200
{ runId, output, latencyMs, audit: {...} }- 401
- missing / invalid session
- 403
- csrf check failed OR agent not inline-runnable
- 429
- rate limited — Retry-After header set
curl -X POST https://phosphor.zeabur.app/api/run \
-H 'Content-Type: application/json' \
-H 'Cookie: phosphor_session=<session>' \
-H 'X-Phosphor-CSRF: <csrf>' \
-d '{
"agentId": "hermes-agent",
"input": {
"messages": [{
"channel": "gmail",
"from": "cfo@acme.test",
"subject": "Q2 forecast",
"body": "Review by EOD."
}]
},
"config": { "ANTHROPIC_API_KEY": "sk-mock" }
}'> POST /api/deploy
Provision a per-tenant Zeabur service from the phosphor-agent-runtime template.
- auth
- required
- csrf
- required
- rate-limit
- 10/min per user, key deploy:<sub>
- body
{ agentId, tenantId, plan, acknowledgedPermissions, config? }- 200
{ instanceId, serviceId, domain }- 402
- entitlement denied — requiredTier in body
- 403
- permissions not acknowledged
> GET /api/instances
List the current user's deployed agent instances.
- auth
- required
- csrf
- not required (GET)
- 200
{ instances: Instance[] }
> POST /api/checkout
Create a Stripe Checkout Session for a paid agent upgrade. Returns a redirect URL that resolves against the public origin (publicOrigin(req)) so success / cancel URLs are never the internal bind address.
- auth
- required
- body
{ agentId, plan }- 200
{ url, sessionId }
> POST /api/checkout/mock-confirm
Deterministic mock for the checkout flow (for demos without a live Stripe account). Classifies card numbers into paid / declined / 3ds / invalid based on digit rules.
> POST /api/stripe/webhook
Stripe webhook receiver. Verifies the signature with STRIPE_WEBHOOK_SECRET and grants entitlement on checkout.session.completed.
> POST /api/auth/login
Accepts { email } or { provider: "github" }. For magic link: issues a one-time token and sends it via ZSend/Resend (or returns it inline when no provider is configured, for dev). For GitHub: returns an OAuth redirect URL.
> GET /api/auth/callback
Consumes a magic-link token or a GitHub OAuth code, upserts the user, signs a session cookie, and redirects to /agents on the public origin.
> CSRF token bootstrap
On every navigation, apps/web/middleware.ts ensures a phosphor_csrf cookie exists (24-byte hex, 7-day max-age, SameSite=Lax). Client code reads it with document.cookie and echoes it in the X-Phosphor-CSRF header on mutating calls.verifyDoubleSubmit compares with constant-time equality.
CLIs
PHOSPHOR ships two CLIs. phosphor-pkg is for authors — build and sign agent bundles. phosphor is for operators — connect a terminal to your deployed instances.
> phosphor-pkg (authors)
- install
npm i -g @phosphor/agent-package- package
packages/agent-package/
phosphor-pkg keygen [id] generate/read ed25519 keypair (default: dev) phosphor-pkg build <sourceDir> build bundle → packages/agents/dist/ phosphor-pkg verify <bundle> check checksum + signature; print summary phosphor-pkg ls list local bundles
Keys live in packages/agent-package/keys/<id>.{pub,priv}. The build step produces a tarball containing manifest.json, the runner, a SHA-256 checksum, and an ed25519 signature.
> phosphor (operators)
- install
npm i -g phosphor-cli- package
packages/cli/
phosphor login sign in via magic link phosphor logout clear stored credentials phosphor list list your agent instances phosphor connect <agent-id> interactive WebSocket session phosphor run <agent-id> [json] one-shot run (pipe-friendly) phosphor status show connection status
Config lives at ~/.config/phosphor/config.json. The connect command opens a WebSocket against the instance's internal-DNS URL (tunneled through phosphor-web) and streams stdout/stderr back to your terminal — useful for long-running agents where a one-shot request/response doesn't fit.
> Example: pipe a run into a shell pipeline
echo '{"messages":[...]}' | phosphor run hermes-agent - | jq '.output'Deployment (Zeabur)
PHOSPHOR is deployed on Zeabur using the zeabur/agent-skills plugin — every deploy, env var, domain, log fetch, and restart goes through a typed skill rather than raw CLI invocations. The marketplace itself (phosphor-web) is deployed with zeabur:zeabur-deploy; per-tenant runtimes are provisioned from the published template.
> Services in prod
phosphor-web— public athttps://phosphor.zeabur.app. Built fromapps/web/via the Dockerfile; listens on$PORT(default 8080) bound to0.0.0.0.audit-store— private, internal-DNS only. Volume mounted at/var/lib/audit.- Per-tenant
phosphor-agent-runtimeinstances — created on demand by/api/deployvia template codeLW76NP.
> Required environment variables
- PHOSPHOR_BASE_URL
- https://phosphor.zeabur.app
- PHOSPHOR_AUTH_SECRET
- HMAC key for session cookies
- ZEABUR_PROJECT_ID
- project id — used to scope template installs
- ZEABUR_TOKEN
- API token for the Zeabur control plane
- AUDIT_LOG_PATH
/var/lib/audit/events.ndjson- ANTHROPIC_API_KEY
- optional — if set, inline runners call Claude
- STRIPE_SECRET_KEY
- optional — real checkout
- STRIPE_WEBHOOK_SECRET
- optional — webhook verification
- ZSEND_API_KEY
- optional — magic-link email via ZSend
- RESEND_API_KEY
- optional — alternate magic-link provider
- GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
- optional — GitHub OAuth
> Internal DNS wiring
phosphor-web reaches audit-store via Zeabur's internal DNS <service>.zeabur.internal. Specifically audit-store.zeabur.internal:8080. No service is exposed to the internet except phosphor-web.
> Volumes
audit-store mounts a single 5 GiB volume at /var/lib/audit. The server only ever appends to events.ndjson; rotated archives land in archive/*.ndjson.gz. Rotation is operator-triggered today and will move to a Zeabur cron job.
> Deploy commands (actual skills used)
# Deploy phosphor-web (initial) zeabur:zeabur-deploy --path apps/web --name phosphor-web # Subsequent redeploys — reuse the service id to avoid duplicates zeabur:zeabur-deploy --path apps/web --service-id <saved> # Set an env var on phosphor-web zeabur:zeabur-variables --service phosphor-web PHOSPHOR_BASE_URL=https://phosphor.zeabur.app # Tail runtime + build logs zeabur:zeabur-deployment-logs --service phosphor-web # Publish or update the agent runtime template zeabur:zeabur-template-publish ./deploy/phosphor-agent-runtime.yaml # Attach a generated subdomain zeabur:zeabur-domain-url --service phosphor-web --subdomain phosphor
Every real invocation is logged in deploy/DEPLOY-LEDGER.md (append-only) and in build-log/ as one file per step. If a skill ever misbehaves, auto-repair turns the error into a draft PR against github.com/zeabur/agent-skills.
CLAUDE.md— project brief, rubric, current self-evaluation, and the running tally of skills / features used.DEMO.md— 8-step runbook for the face-cam walkthrough.build-log/— one file per completed step, including every bug fix and its root cause.deploy/DEPLOY-LEDGER.md— append-only log of every Zeabur action (service create, deploy, env var, domain, restart).services/audit-store/RETENTION.md— retention / rotation / backup contract for the audit store.