Returns aggregate statistics for the memory subsystem within the current workspace — totals, per-tier rollups, and per-agent rollups derived from memory_versions.Required role: ADMIN or OWNER (manage permission)
Row-level drill-down into memory_versions. Pairs with the stats endpoint above: stats answers “how much memory does this workspace have?”, versions answers “which rows specifically?”. Results are ordered newest-first by written_at DESC, id DESC and paginated via an opaque keyset cursor.Required role: ADMIN or OWNER (manage permission)
Opaque cursor for the next page; null on last page
limit
integer
The resolved page size
filters_applied
object
Echo of the normalised filters that produced this response
The cursor is a base64url-encoded v1:<rfc3339nano>|<id> tuple pinning (written_at, id). Offset pagination would duplicate or skip rows because the audit watcher writes continuously; keyset pagination pins the boundary so concurrent inserts above the cursor land on the next refresh naturally.
Returns the raw blob bytes for a single memory_versions row. Used by the dashboard’s row-detail view and by compliance auditors who need the literal content (not just the metadata) — for example, to confirm a PII scrubber fired on the offending payload.Required role: ADMIN or OWNER (manage permission)
The body is the raw blob bytes (NOT JSON-wrapped). Content-Type is text/markdown; charset=utf-8 for paths ending in .md, otherwise application/octet-stream so the client cannot auto-render untrusted bytes as HTML.Audit metadata travels alongside the body via response headers:
Header
Description
X-Memory-Sha256
sha256 from the memory_versions row
X-Memory-Bytes
Length from the row (matches the row’s recorded size)
X-Memory-Tier
Tier (agent / crew / workspace / pins / learned)
X-Memory-Path
Canonical memory path
X-Memory-Written-At
RFC3339 timestamp
X-Memory-Written-By
Writer identifier (omitted when NULL)
Cache-Control
private, max-age=31536000, immutable — blobs are content-addressed
Unknown id OR cross-workspace probe (no existence leak)
410
Row exists but the blob file is missing on disk (retention sweep, restore-from-backup race)
413
Blob exceeds the 10 MB cap (DB-claimed size OR on-disk size)
500
sha mismatch (blob tampered after recording) OR payload_ref resolves outside the configured blob root
503
Memory versioning is not configured (lite-mode deployment without blob root)
The handler refuses to follow symlinks under payload_ref. The on-disk layout is fixed at blobRoot/<sha[:2]>/<sha>; filepath.EvalSymlinks is used to verify the resolved path stays inside the blob root, defending against path-traversal vectors in a corrupted or malicious payload_ref.
Returns the per-workspace memory configuration. Drives the retention sweep (versions_retention_days) and is the operator’s read surface for inspecting drift between “what’s stored on the row” and “what’s effective”.Required role: ADMIN or OWNER (manage permission)
Partial-merge update of the per-workspace memory configuration. Merges the request body’s keys into the existing JSON document; unspecified keys are preserved.Required role: ADMIN or OWNER (manage permission)
Returns the post-merge config in the same shape as the GET response above. A PATCH that produces no diff (e.g. resetting to the same value) returns 200 with the current shape and emits NO journal entry — the audit trail tracks actual change, not request count.
Missing workspace context, malformed JSON, trailing garbage after the JSON value, empty body, or versions_retention_days outside [1, 3650] / not a positive integer
403
MEMBER role
413
Request body exceeds 16 KB
500
UPDATE / commit failure
The read-merge-write runs inside a SQLite BEGIN IMMEDIATE (serializable) transaction so concurrent PATCHes touching different keys serialise rather than last-write-wins. Each real diff emits a memory.config_updated journal entry (Notice severity, ActorUser) with payload {workspace_id, changes: {field: {from, to}}}. If the stored JSON is corrupt, PATCH still succeeds (treats the existing document as empty) so operators can fix the row without resorting to manual SQL.
Returns the Keeper access request audit log — every credential access and command execution request evaluated by the Keeper.Required role: ADMIN or OWNER (manage permission)
Small cross-cutting endpoints that don’t belong to any single domain handler. They surface install state, telemetry consent, the running binary’s version, and dashboard time-series metrics. Three are intentionally unauthenticated because the login page itself needs to read them before any session exists.
First-run gate. Returns whether the install needs to be bootstrapped (empty users table) and whether public signup is enabled. The login page calls this on every page paint — when needs_bootstrap is true, the browser routes to /bootstrap instead of /login.Auth: none — the answer is what tells the browser which page to render.
Read-only consent gate for the frontend’s Sentry client. The Next.js sentry.client.config.ts fetches this before calling Sentry.init and bails out if enabled=false. Consent is flipped via the CLI (crewship telemetry on/off), never over HTTP — making this endpoint mutating would create a CSRF vector that flips the bit on every cross-site navigation.Auth: none — the login page must boot crash reporting before any session exists.
Reports the running binary’s version and (cache-permitting) the latest release from GitHub. The web UI uses this to render an “update available” banner.Auth: required (any authenticated user, no workspace role needed).
The running binary’s version (SetVersion-injected from cmd_start).
latest
string?
Latest release tag from GitHub. null on a cold cache + GitHub timeout.
newer
boolean
true when latest > current by semver.
url
string?
Release page URL. null paired with nulllatest.
The handler imposes a 4 s upper bound on top of the update package’s 5 s internal HTTP timeout — a cold cache + slow network still returns “no info” rather than blocking the UI render.
Bucketed time-series metrics for the dashboard charts. Returns zero-filled bucket sequences so the client never has to patch visual gaps. Reads workspace_id from the request context, never from a query param.Auth: required + workspace context.
Bucket-start in UTC, aligned to wall-clock boundaries (15m → :00/:15/:30/:45, 1h → top of hour, 1d → UTC midnight).
buckets[].series
object
Map of series key to numeric value. Always floats on the wire so cost and counts share the JSON shape.
series_labels
object
Map of series key to display label (e.g. crew name, model name, status). For group_by=none always contains {"total": "Total"}.
Status
Condition
400
Unknown metric / window / bucket / group_by; bucket larger than window; combination produces >200 buckets; group_by=model for a metric other than cost_usd; group_by=crew for a metric other than issues_closed/runs_count; group_by=status for a metric other than issues_closed/active_missions.
Licenses are verified using Ed25519 digital signatures:
1
Signed format
A license file contains a JSON object with payload (the claims as a JSON string) and signature (base64-encoded Ed25519 signature).
2
Public key embedding
The Ed25519 public key is embedded into the binary at build time via ldflags. This prevents license tampering by tying verification to the specific build.
3
Signature verification
On startup, Crewship decodes the public key and signature from base64, then verifies the payload using ed25519.Verify().
4
Expiration check
If the license has an expires_at timestamp and it is in the past, the license is rejected and community defaults apply.
5
Fallback
If no license file exists, verification fails, or the license is expired, Crewship runs with community edition defaults.
The public key variable is set at build time. Without a valid public key embedded in the binary, license loading will fail with “no public key embedded in binary” and community defaults will apply.