Skip to main content

Admin API

The operational surface for OWNER and ADMIN roles: workspace stats, user management, GDPR subject-access and erasure, memory administration, the Keeper security audit log, and a set of cross-cutting system and metrics endpoints. Most endpoints require the OWNER role; the GDPR and memory-admin endpoints require ADMIN or OWNER (manage permission). The Keeper Phase 2 evaluators and three system endpoints have their own auth model, noted inline.
Unless a section says otherwise, every endpoint on this page requires the OWNER role. The GDPR, memory-admin, and Keeper-requests endpoints accept ADMIN or OWNER (manage permission).

Endpoints

Stats & users

Read surfaces for workspace-level counts and membership. Both require the OWNER role.

GET /api/v1/admin/stats

Returns aggregate statistics for the current workspace. Required role: OWNER Request: no body. Response: JSON object shown below.

Response

Stats are scoped to the current workspace to prevent cross-workspace data leakage. The workspaces field always returns 1.

GET /api/v1/admin/users

Lists all users in the current workspace with their roles. Required role: OWNER Request: no body. Response: JSON array shown below.

Response

Instance administration

These routes require ADMIN or OWNER (manage permission). The mutation routes operate on instance or Docker state, although the caller must still have an authenticated workspace context.

GET /api/v1/admin/health

Returns process uptime, the current log-level state, encryption-key source, database liveness, and data-directory disk usage. The database and disk sections report their own errors while the endpoint remains a health snapshot. Auth: ADMIN or OWNER (manage permission). Request: no body.

Response

GET /api/v1/admin/log-level

Reads the live process level, configured baseline, and optional expiry of a temporary override. Auth: ADMIN or OWNER (manage permission). Request: no body.

Response

expires_at is omitted when no timed override is active.

PUT /api/v1/admin/log-level

Changes the live process log level. A positive TTL automatically restores the baseline; ttl_seconds: 0 leaves the override active until changed, and the server caps TTL at 24 hours. Auth: ADMIN or OWNER (manage permission).

Request

level must be a supported logging level (debug, info, warn, or error).

Response

200 OK returns the same {level, baseline, expires_at} object as the GET endpoint.

GET /api/v1/admin/legacy-resources

Read-only, instance-wide detection of orphaned pre-C1 slug-only Docker containers or volumes. It does not remove anything. Auth: ADMIN or OWNER (manage permission). Request: no body.

Response

Rate limits

Rate-limit settings are instance-wide even though the request still requires an authenticated workspace context. Reads and writes require ADMIN or OWNER (manage permission). Overrides apply immediately and persist until reset.

GET /api/v1/admin/rate-limits

Request: no body. Response: 200 OK with every tunable limiter.

PUT /api/v1/admin/rate-limits/

Overrides one registered limiter. Auth: ADMIN or OWNER (manage).

Request

Response

200 OK returns the updated limiter state in the same shape as the list response. Unknown keys are rejected; values must be within the limiter’s published bounds.

DELETE /api/v1/admin/rate-limits/

Resets the named limiter to its shipped default. Auth: ADMIN or OWNER (manage). Request: no body. Response: 200 OK with the reset limiter state.

POST /api/v1/admin/prune-legacy-resources

Removes orphaned pre-C1, slug-only Docker containers and volumes across the instance. Live id-scoped resources are protected. The request has no body and the response lists the resource names removed. Auth: ADMIN or OWNER (manage permission). Request: no body.

Response

200 OK is returned on success. A Docker-unavailable deployment returns 503; a mid-operation failure returns 500 with the partial removed list and count.

POST /api/v1/admin/reap-orphan-containers

Scans the current workspace’s running crew containers for a sidecar token fingerprint that definitively disagrees with the token derived from the current internal-token master. The default is a dry run. Add ?apply=true to stop and remove each detected orphan; the next dispatch recreates it. Auth: ADMIN or OWNER (manage permission). Request: no body; apply is an optional query parameter. Response: the scan object below.

Response

reaped is true only when apply=true and stop/remove succeeded. A running container with no advertised fingerprint is not classified or removed; inspected counts running containers reached and identified counts those that advertised a fingerprint. detector_inert is true when at least one container was inspected but none advertised a fingerprint. The endpoint returns 200 OK even when an individual reap fails; that entry remains reaped: false. Docker-unavailable deployments return 503.

POST /api/v1/admin/reencrypt

Re-encrypts every inventoried stored AES-256-GCM envelope to the current master-key version. The request has no body. It is instance-wide and idempotent: envelopes already at the current version are counted as skipped. Rows that cannot be decrypted are counted as failed and left untouched. Auth: ADMIN or OWNER (manage permission). Request: no body.

Response

Each columns entry has the same three integer counters as the top-level response. A valid run returns 200 OK. Invalid current-key configuration fails before any row is written with 500; an infrastructure failure during the walk also returns 500 with partial counts and columns.

GET /api/v1/admin/security-posture

Returns read-only, instance-level security state. It reports configuration presence and derived state only; it never returns a key, secret, or client secret. Request: no body. Response: JSON object shown below.

Response

rate_limit_disabled is configured intent; in prod, the limiter still runs and rate_limit_effectively_disabled is false. Warning keys are stable machine-readable identifiers and may include plaintext_secrets_allowed, encryption_key_missing, rate_limit_disabled, rate_limit_disabled_ignored_in_prod, signup_open, private_endpoints_ceiling_open, encryption_key_generated, privileged_credentials_enabled, private_endpoints_in_use, seed_account_default_password, and no_backup_recorded.

GDPR

Subject-access export and right-to-erasure across the cascadable tables. Both require ADMIN or OWNER (manage permission) and write a gdpr_actions audit row.
The DELETE endpoint cascade-purges every row referencing the user across the cascadable tables and cannot be undone. A reason is required for the audit trail.

GET /api/v1/admin/users/{userId}/data

GDPR Art. 15 (Right of Access) — return every row referencing the given user across the cascadable tables in the current workspace. Writes a gdpr_actions audit row with action='export'. Required role: ADMIN or OWNER (manage permission). MANAGER is intentionally not a SAR actor — auditor framing is Compliance/Founder separation of duties.

Response

lessons.md content is not scanned for user mentions — a known gap (see GDPR guide). Operators must manually review lessons after a SAR if any lesson body could carry user-attributable text.

DELETE /api/v1/admin/users/{userId}/data

GDPR Art. 17 (Right to Erasure) — cascade purge every row referencing the user across the cascadable tables. Writes a gdpr_actions audit row with action='delete'. reason is required for the audit trail. Required role: ADMIN or OWNER (manage permission).

Request

Response

202 Accepted on full success; 207 Multi-Status on partial success (some tables purged, others returned errors — the gdpr_actions row carries the full per-table summary).
On partial failure the same shape is returned with an additional "error": "<first-error>" field and HTTP 207.
Memory payload_ref content-addressed blobs on disk are not deleted by this endpoint — blobs are deduplicated across workspaces and require a separate sweep job (planned). The audit/index rows ARE purged so the SAR is honoured at the DB-visibility layer. See the GDPR guide for the operator workflow.

Workspaces

Workspace detail read, scoped to the current workspace. Requires the OWNER role.

GET /api/v1/admin/workspaces

Lists workspace details with member, agent, and crew counts. Required role: OWNER Request: no body. Response: JSON array shown below.

Response

This endpoint is scoped to the current workspace only. It does not list other workspaces in the system.

GET /api/v1/admin/journal/verify

Walks the current workspace’s journal hash-chain and reports whether it is intact — the tamper-evidence check for the append-only journal (internal/api/admin_journal_integrity.go). Detects content mutation, in-place reorder, and mid-chain deletion; see Audit Log → Tamper-Evidence for the guarantee. Required role: ADMIN or OWNER (canRole(role, "manage")) The endpoint takes no parameters; the workspace comes from request context (X-Workspace-ID). It stays 200 OK even when the chain is broken — inspect ok.

Response

A non-empty repairable also produces a priority break, so ok is false — see Audit Log → Recovered is not resolved. CLI parity: crewship journal verify (exits non-zero on a broken chain or when any entry has an unresolved priority).

Audit log

The workspace audit trail — every audit_logs row for the current workspace, newest first. Requires the manage permission (ADMIN or OWNER).

GET /api/v1/audit

Returns a paginated, filterable list of audit-log entries for the current workspace, joined to the acting user for email/name display (internal/api/audit.go:44). Required role: ADMIN or OWNER (canRole(role, "manage")internal/api/audit.go:48)

Query Parameters

Response

Memory admin

Inspect and tune the memory subsystem: aggregate stats, row-level version drill-down, raw blob retrieval, and the per-workspace retention config. All require ADMIN or OWNER (manage permission).

GET /api/v1/admin/memory/stats

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) Request: no body. Response: the totals and rollups object below.

Response

Example

Tiers with zero rows are omitted from by_tier. Tiers with rows the operator has never written to are NOT included as zero entries.

GET /api/v1/admin/memory/versions

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)

Query Parameters

All parameters are optional and AND-composed.

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.

Example

GET /api/v1/admin/memory/versions/{id}/content

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)

Response

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:

Example

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.

GET /api/v1/admin/memory/config

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) Request: no body. Response: the configuration object below.

Response

Example

PATCH /api/v1/admin/memory/config

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)

Request Body

Unknown top-level keys are passed through to the stored document for forward compatibility (e.g. future fields like compaction_hour_override).

Response

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.

Example

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.

Keeper

The operator-facing Keeper audit log, plus the Phase 2 F4 evaluator routes. The audit-log read requires ADMIN or OWNER (manage permission); the Phase 2 evaluator routes are internal-auth (see below).

POST /api/v1/admin/prune-crew-runtimes

Prunes crew runtime containers and volumes that are no longer needed. The operation is restricted to callers with manage permission (ADMIN or OWNER). Request: no body. Response: 200 OK with the number of pruned runtimes. The operation is safe to repeat; an unavailable runtime provider returns 503 and a pruning failure returns 500. Statuses: 200 OK; 401 without authentication; 403 without manage permission; 503 when the runtime provider is unavailable; 500 for a prune failure.

GET /api/v1/admin/keeper/requests

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)

Query Parameters

Response

Phase 2 request types (skill_review, behavior, memory_health, negative_learning) populate the same audit log surface as Phase 1 (credential, execute) with the same shape. The intent column carries the F4 evaluator’s structured summary instead of a free-form access reason; the ollama_prompt + ollama_raw_response capture the LLM evaluation round-trip. Filter by request_type to slice the log into per-evaluator views.
This endpoint returns the current state of each request. decision is written PENDING and updated in place, so it cannot tell you how the request got there. For that, use the transition history below.

GET /api/v1/admin/keeper/requests//events

Returns the append-only transition history for one Keeper request, oldest first — the record that the in-place decision update would otherwise destroy (issue #1369). Required role: ADMIN or OWNER (manage permission) Request: no body. Statuses: 200 OK; 401 without authentication; 403 without manage permission; 404 when the request does not exist; 500 for a database error.

Path Parameters

Response

keeper_request_events is append-only at the database level — a BEFORE UPDATE trigger aborts any rewrite of a recorded transition. A decision can only be superseded by a further transition, which appears here as an extra row. See Audit → Keeper Decision Auditing.
Requests raised before the ledger migration have their current state backfilled (a PENDING at created_at plus the decision at decided_at) but no intermediate history.
CLI parity: crewship keeper history <request-id>.

Keeper Phase 2 — F4 evaluators

The four evaluator endpoints are internal-auth (X-Internal-Token) — they’re invoked by the platform itself (scheduler routines + the post-tool-call hook), not by operators directly. The admin-facing surface is POST /api/v1/admin/keeper/review/{slot}/run (below), the /api/v1/admin/keeper/requests log above, and the per-type filters in the admin UI’s “Keeper P2 reviews” panel (PR-F2).
The expected production path is automated: routines fire on cron, the behavior hook fires on tool-call sampling. To trigger an evaluator by hand, use the admin route below rather than the internal token.

POST /api/v1/admin/keeper/review/{slot}/run

Run one evaluator now. OWNER/ADMIN, workspace-scoped like every /api/v1/admin route (?workspace_id= is required by RequireWorkspace before the handler runs). slot is one of skill-review, behavior, memory-health, negative-learning; the aux-config spellings (curator, memory_health, negative) are accepted as aliases. Anything else is a 400 that names the valid slots. It calls the same handler the internal routes below do — same policy resolution, same keeper_requests row, same inbox escalation — and returns that handler’s response, so the shapes documented per-endpoint below apply unchanged. Request body (all fields optional; what you omit, the server derives from the workspace):
With an empty body: skill-review reviews the stalest skill assigned to one of the workspace’s agents; behavior judges a tool call named keeper.manual_probe; memory-health scores the crew’s memory via consolidate.ComputeHealth; negative-learning learns from the workspace’s most recent run.failed journal entry (400 when there is none — the trigger set is closed and inventing a failure would put fiction in the audit trail). workspace_id in the body is refused when it disagrees with the session’s, and a crew_id / agent_id from another workspace is a 403: the internal routes get that binding from a workspace-bound sidecar token, and an admin session has none. CLI: crewship keeper review run <slot> — see Keeper → Running a review on demand.

POST /api/v1/internal/keeper/skill-review

F4.1 — periodic skill audit. Cron-fires daily 03:00 UTC per Scheduler.RegisterPlatformRoutine. The evaluator reads each skills row + skill_invocations history, asks the F3 Curator aux model whether the skill should stay active, transition to stale (no recent invocations), or be archived (failures dominating). DENY decisions write a blocking inbox row; ALLOW updates skills.lifecycle_state in place. Request body:
Response:
DENY routes a blocking inbox_items row to the assigned agents’ workspace (per assigned_agents fan-out). ESCALATE routes a MANAGER-targeted blocking row.

POST /api/v1/internal/keeper/behavior

F4.2 — post-tool-call behavior monitor. Fires from behaviorhook.MaybeEvaluate (orchestrator EventPostToolCall event), sampled at the per-crew rate (default 1-in-5). The evaluator reads the (tool_name, tool_args_snippet, current crew behavior_mode) triple and returns ALLOW / DENY / ESCALATE. behavior_mode=warn (default): DENY → non-blocking inbox; agent’s NEXT tool call proceeds. behavior_mode=block: DENY → blocking inbox + ShouldBlock=true in the response so the orchestrator interrupts the agent’s next call. Forbidden combination (autonomy=full + behavior_mode=block) rejected at API + DB layer. Request body:
Response:
policy_decision is the resolved per-crew policy verdict (e.g. inbox_approve, auto_log_inbox, block_inbox) that drives whether an inbox row is written and whether it blocks.

POST /api/v1/internal/keeper/memory-health

F4.3 — periodic memory consolidation review. Cron-fires daily 03:30 UTC. The evaluator consumes a consolidate.HealthSnapshot (the 5-metric health score: Freshness / Coverage / Coherence / Efficiency / Reachability, each in [0, 100], plus the weighted Overall) and decides whether to auto-trigger a consolidation routine. Staleness and contradiction counts travel as separate top-level body fields, not inside snapshot. Request body:
snapshot is the consolidate.HealthSnapshot Go struct serialised without JSON tags, so its wire keys are PascalCase (Freshness, Reachability, Overall, …) and each metric is on a 0–100 scale — not the reachability_pct / 0–1 shape an external caller might guess. stalest_entry_days and contradiction_count are top-level fields alongside snapshot, not nested inside it.
Response:
auto_consolidate=true triggers consolidator.Run for the workspace; ESCALATE writes a blocking inbox row instead.

POST /api/v1/internal/keeper/negative-learning

F4.4 — failure-event lesson capture. Fires after a guardrail trip, run failure, or explicit operator “log this lesson” action. The evaluator decides whether the failure is worth a kind=negative lesson in the agent’s lessons.md. ALLOW writes through consolidate.WriteLesson (PR-Z Z.7) which enforces YAML schema + idempotency by ID + flock + atomic-rename. Self-learning gate: ALLOW auto-applies only when the agent has self_learning_enabled = 1 (migration v106). With self_learning_enabled = 0 (default), ALLOW queues a blocking inbox row with the full lesson proposal in payload_json and the marker "self_learning_gate": "off" so the UI can distinguish the gate-demoted path. See Autonomy + self-learning. Trigger kinds: run_failed, guardrail_warn, guardrail_error, keeper_execute_deny. Request body:
Response:
When self_learning_enabled = 0 the response still says write_lesson: true (the evaluator’s intent), but no lesson lands on disk — the operator must approve via inbox. Check gdpr_actions-style audit via GET /api/v1/admin/keeper/requests?request_type=negative_learning.

Cross-tenant guard

All four endpoints assert body.workspace_id == ctx.workspace_id via assertBodyWorkspaceMatchesCtx before any evaluator runs. Asymmetric forgery (query=A, body=B) returns 400 Bad Request. Empty ctx workspace also returns 400 — the gate refuses to operate without the middleware that’s supposed to set it. Symmetric forgery (caller picks one workspace consistently) requires PR-F24 token-to-workspace binding to close fully. See Internal IPC — Tenant isolation.

System

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.
setup-status and telemetry are intentionally unauthenticated because the login page needs to read them before any session exists; version and metrics/timeseries require an authenticated user (metrics/timeseries also needs workspace context).

GET /api/v1/system/setup-status

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. Request: no body. Response: {needs_bootstrap, allow_signup}.

Response

GET /api/v1/system/telemetry

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. Request: no body. Response: {enabled, install_id}.

Response

Errors fall back to {enabled: false, install_id: ""} rather than 5xx — a transient DB blip defaults to the privacy-preserving outcome.

GET /api/v1/system/version

Reports the running binary’s build identity and (cache-permitting) the latest release from GitHub. The web UI uses current/latest/newer/url to render an “update available” banner; the build fields answer the separate question of which build this server actually is. This is the only way to ask a running server what it is. crewship version --remote is the CLI for it. Auth: required (any authenticated user, no workspace role needed). Request: no body. Response: build identity and release metadata below.

Response

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. The build fields are computed in-process and are unaffected by it.
current alone cannot identify a build. Every binary an -ldflags-less go build has ever produced calls itself dev — which is how a dev slot once sat a full day behind main with nothing able to say so. Compare commit.

GET /api/v1/metrics/timeseries

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.

Query parameters

Response

License System

Roadmap (v0.2). The license/edition system below is on the v0.2 roadmap. v0.1 ships as fully open-source Apache-2.0 with no edition gating.
Crewship will use a three-tier licensing model that controls workspace limits and feature availability.

Editions

License Claims

Each license contains signed claims:

License Verification

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.

What’s Next

RBAC

Role-based access control and permission levels.

Keeper Guide

Configure the AI-powered security gatekeeper.