// docs · reference

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.

quick start // run your first agent

Get a working agent run in under 60 seconds:

  1. 1.Go to /login and sign in — use email, Google, GitHub, or Apple via Privy.
  2. 2.Open Hermes or Openclaw Dench.
  3. 3.Tick the permissions checkbox in the RAP SHEET section (required — nothing runs until you ack).
  4. 4.Leave the API key blank for mock mode and click JACK IN.
  5. 5.Structured output appears in ~200 ms. Swap in a real sk-ant-… key for live Claude; override BASE_URL / MODEL to point at any OpenAI-compatible provider (e.g. https://api.openai.com/v1).
section 01 // architecture

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 on 0.0.0.0:$PORT inside 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 DNS audit-store.zeabur.internal:8080. Never exposed to the public internet. See the retention policy at services/audit-store/RETENTION.md.
  • phosphor-agent-runtime — a published Zeabur template (code LW76NP) 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 path
  • apps/web/app/api/deploy/route.ts — template install path
  • services/audit-store/ — append-only ingest service
  • packages/sandbox/src/runner.ts — per-run context & policy guard
section 02 // user guide

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 UI
  • apps/web/components/AgentDetailFlow.tsx — rap-sheet ack, run panel, deploy panel
section 03 // publisher guide

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 CLI
  • packages/policy/src/ — manifest validator
  • apps/web/lib/runners/ — runner interface + examples
section 04 // safety model

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: true is 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 in net.allowHosts.
  • RunnerContext.env is a frozen object containing only keys from env.allowedEnvKeys. Raw process.env is not passed through.
  • limits.maxRuntimeSec is enforced with Promise.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_session set by /api/auth/callback. Missing or invalid → 401.
  • CSRF. Double-submit: middleware sets phosphor_csrf cookie, client echoes it as X-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 with Retry-After.
  • Entitlement. assertCan(plan, action, ctx) compares plan rank to the agent's required tier. Missing agentTier on run_agent fails closed.

> Layer 4 — append-only audit

  • Every tool call — success AND denial — emits a singleAuditEvent to audit-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 issuance
  • apps/web/lib/csrf.ts — double-submit verification
  • apps/web/lib/rate-limit.ts — sliding-window bucket
  • apps/web/lib/sandbox.ts — sandboxed fetch + scrub
  • apps/web/lib/billing.ts — canUseAgent / assertCan
section 05 // api reference

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.

section 06 // clis

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'
section 07 // deployment

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 at https://phosphor.zeabur.app. Built from apps/web/ via the Dockerfile; listens on $PORT (default 8080) bound to 0.0.0.0.
  • audit-store — private, internal-DNS only. Volume mounted at /var/lib/audit.
  • Per-tenant phosphor-agent-runtime instances — created on demand by /api/deploy via template code LW76NP.

> 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.

see also // deeper references
  • 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.