-
Notifications
You must be signed in to change notification settings - Fork 1
REST API Reference
Complete endpoint catalog as of v0.50.0. All routes are defined as
constants in shared/.../ApiPath.kt; copy them into your client to stay
wire-compatible.
Wire changes since the original v0.10.0 baseline
- v0.13.0 —
claudeConsoleCancel(projectId)+ Prompt Template GET- v0.16.0 — Conversation history GET (per-project + scratch)
- v0.18.0 —
gitCommit(projectId)+templateIdfield on register- v0.19.0 — Emulator diagnostics (page-only; JSON API on roadmap)
- v0.20.0 —
PROMPT_TEMPLATESconstant +PromptTemplateDto/PromptTemplateListResponseDtopromoted toshared/- v0.21.0 —
ClaudeStatusDtogainsusagePercent: Int?andresetAt: String?(additive; default null)- v0.22.0 / v0.23.0 — Play Console + TestFlight upload (SSR only)
- v0.24.0 — Emulator AVD lifecycle (SSR POSTs; no REST wire)
- v0.26.0 —
LoginRequestDto.totpCode: String?(additive). 2FA- enabled users get401 totp_required; bad code returnsinvalid_totp.- v0.28.0 — APK signer inspection (SSR-only)
- v0.29.0 —
GET /projects/{id}/zipsource download (SSR streaming)- v0.30.0 —
/historycross-project search (SSR), build history chart (SSR), keyboard shortcuts (static JS); no REST wire- v0.31.0 —
GET /api/projects/{id}/claude/prompt-suggestions?prefix=…added (additive — server-only, no Dto inshared/).GET /projects/{id}/history/export+POST .../history/import(SSR)./agentsCRUD (SSR only).- v0.32.0 —
/projects/{id}/deps,/projects/{id}/env-files,/logs(all SSR; no REST wire)- v0.33.0 —
POST /api/webhooks/build/{projectId}external trigger (admin-auth-free, multi-secret);/projects/{id}/automationSSR- v0.34.0 —
/backupSSR +GET /backup/download(application/gzip)- v0.35.0 —
/projects/{id}/wrapper+/projects/{id}/stats+/code-search(all SSR; no REST wire)- v0.36.0 —
GET /api/agents(Bearer JSON — list.agents/*.md)./multi-console?projects=…(SSR-only)- v0.37.0 —
/usersSSR (admin-only) — create / role / delete.admin_users.rolecolumn added (db schema; no REST wire impact for existing endpoints).- v0.38.0 — Docker base rebase only; no API change.
- v0.39.0 —
/static/manifest.json+/static/sw.js(PWA static assets; no API change).- v0.40.0 —
admin_users.roleacceptsviewer. Destructive SSRPOSTendpoints gainrequireWriteAccessOrRedirect; viewers get a redirect to dashboard./audit,/settings,/backupadoptrequireAdminOrRedirect. No REST wire change.- v0.41.0 — Console page
@ Agent dispatchdropdown (uses existingGET /api/agentsfrom v0.36.0). No REST wire change.- v0.42.0 —
/emulator/vnc/*HTTP + WebSocket reverse proxy added (admin-only). Backend islocalhost:6080(websockify). Client-facing WS iswss://<host>/emulator/vnc/websockifywith subprotocolbinary.- v0.43.0 — Server side unchanged (only
vscode-extension/reaches v0.2.0).- v0.44.0 — Real multi-agent process pool. New routes:
POST /api/projects/{id}/agents/{agent}/console/prompt,POST /api/projects/{id}/agents/{agent}/console/cancel,GET /api/projects/{id}/agents/active,WS /ws/projects/{id}/agents/{agent}/console/logs. Existing endpoints unchanged.- v0.45.0 — JSON API + WebSocket role guards on existing endpoints. Bearer tokens whose user is
viewernow get403 viewer_readonlyon mutating REST calls and aviewer_readonlyerror frame on WSUserPrompt/ActionInvoke. Server-level setup endpoints (env-setup, Claude auth, MCP, Git integrations) requireadmin— non-admins get403 admin_only. Existing endpoint paths and DTOs unchanged.- v0.46.0 — Web Push endpoints added:
GET /api/push/vapid-public-key,POST /api/push/subscribe,DELETE /api/push/subscriptions/{id}. Service-worker push handler in/static/sw.js. Browser-only — Android client untouched.- v0.47.0 —
/settings/*SSR admin-only sweep (/settings/email,/settings/webhook,/settings/cors,/settings/git-integrations,/settings/cache). Added/usageSSR (raw Claude/statusviewer). No REST wire change.- v0.48.0 — WebAuthn (passkey) 2FA. New routes:
POST /api/webauthn/register/{options,verify},POST /api/webauthn/assert/{options,verify}. Theassert/verifypath is unauthenticated — it's the login itself and mints a freshvibe_sessioncookie + Bearer token. Newwebauthn_credentialstable. Existing endpoints unchanged.- v0.49.0 — Project ACL.
GET /api/projectsis now filtered by the caller'sDevicePrincipal.userRole+ ACL rows (admin sees everything; non-admin with 0 ACL rows sees everything; non-admin with 1+ ACL rows sees only those).GET /api/projects/{id}returns403 project_forbiddenon ACL violation. Response DTOs unchanged.conversation_turns.agent_namecolumn added (nullable) —GET /api/projects/{id}/historyautomatically returns sub-agent turns alongside main-console turns.- v0.50.0 — Web Push payload encryption (RFC 8291
aes128gcm). No route shape change, butPOSTto subscription endpoints now carriesContent-Encoding: aes128gcmwhen the subscription hasp256dh+auth. Service worker (/static/sw.js) reads decryptedevent.data.json()for title / body / url. ExistingPOST /api/push/subscribealready accepted these fields (since v0.46.0) — no client changes needed.
All /api/* routes (except /api/auth/setup, /api/auth/setup/status,
/api/auth/login, and /health) require Bearer authentication:
Authorization: Bearer <token>
Obtain a token with POST /api/auth/login. The same token works with both
the header and the vibe_session cookie path.
http://<host>:17880
For LAN deployments. HTTPS is the operator's responsibility (reverse proxy recommended for non-LAN exposure).
Check whether an admin user exists. Used by clients to decide between login and setup screen.
curl http://localhost:17880/api/auth/setup/status
# → {"adminExists": true}Create the first admin user (only when no admin exists). Returns
409 Conflict if one already exists.
curl -X POST http://localhost:17880/api/auth/setup \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"ChangeMe123","deviceName":"my-laptop"}'
# → {"token":"...","deviceId":"...","serverName":"...","username":"admin"}curl -X POST http://localhost:17880/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"ChangeMe123","deviceName":"my-android"}'
# → {"token":"...","deviceId":"...","serverName":"...","username":"admin"}10 consecutive failures lock the account for 15 minutes (timing-safe).
If the user has TOTP enabled (see Two-Factor Auth)
the first call without totpCode returns:
HTTP/1.1 401 Unauthorized
{"code":"totp_required","message":"2단계 인증 코드가 필요합니다."}Client should prompt the user for the 6-digit code from their
Authenticator app and resubmit with totpCode added:
curl -X POST http://localhost:17880/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"ChangeMe123","deviceName":"my-android","totpCode":"123456"}'Wrong code returns 401 invalid_totp (counts against the brute-force
limits). totp_required does not (1단계 통과 = legitimate progress).
The bearer token / vibe_session cookie auto-expires after
security.sessionIdleTimeoutMinutes (default 30) of inactivity. SSR
redirects to /login?err=session_timeout; JSON API returns plain 401.
Change password. Requires Bearer auth.
curl -X POST http://localhost:17880/api/auth/password \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json' \
-d '{"currentPassword":"old","newPassword":"new"}'
# → 204 No ContentHigh-level summary.
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/api/server/status{
"serverName": "Vibe Coder Server",
"serverVersion": "0.10.0",
"osName": "Linux 6.x",
"javaVersion": "17.0.19",
"workspaceRoot": "/workspace",
"projectCount": 3,
"runningTaskCount": 0,
"claudeAvailable": true,
"androidSdkAvailable": true,
"gitAvailable": true,
"freeDiskSpaceBytes": 102400000000
}Detailed per-component diagnostics.
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/api/projects
# → [{"id":"my-app","name":"My App","packageName":"...", ...}]Empty project:
curl -X POST http://localhost:17880/api/projects/register \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"projectId": "my-app",
"appName": "My App",
"packageName": "com.siamakerlab.myapp"
}'Clone from git:
curl -X POST http://localhost:17880/api/projects/register \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"projectId": "my-app",
"appName": "My App",
"packageName": "com.siamakerlab.myapp",
"sourceType": "clone",
"cloneUrl": "https://github.com/owner/repo.git",
"cloneBranch": "main"
}'Private SSH URL — first register a PAT or generate an SSH key via the Git integrations endpoints below.
When neither sourceType=clone nor an uploadedZipId is provided, the
server can scaffold the project from a built-in template instead of leaving
an empty directory.
curl -X POST http://localhost:17880/api/projects/register \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"projectId": "my-app",
"appName": "My App",
"packageName": "com.siamakerlab.myapp",
"templateId": "compose-mvvm-hilt"
}'Template IDs (resolved by ProjectTemplates.byId(...)):
templateId |
Description |
|---|---|
empty |
Bare CLAUDE.md only (default) |
compose-basic |
Single-activity Compose Material3 |
compose-mvvm-hilt |
ViewModel + Hilt skeleton |
compose-mvvm-room |
Hilt + Room database wired |
wear-os |
Wear OS Compose entry point |
android-tv |
Leanback / TV-launcher entry |
Each template seeds a starterPrompt that the project's first console
turn consumes — so the next user prompt feels like a follow-up to a real
conversation. The user can override or ignore it.
RegisterProjectRequestDto.keystore is accepted by the server but is not
exposed in the admin SSR project-create form — it is reachable only via
this JSON endpoint (curl / mobile client / direct API).
curl -X POST http://localhost:17880/api/projects/register \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"projectId": "my-app",
"appName": "My App",
"packageName": "com.siamakerlab.myapp",
"keystore": {
"alias": "myapp",
"password": "at-least-6-chars",
"dname": "CN=Jangwook Lee, OU=Mobile, O=Sia Makerlab, L=Jecheon, ST=Chungbuk, C=KR",
"validityDays": 36500
}
}'KeystoreRequestDto fields:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
alias |
String | ✅ | — | Non-blank. Becomes keyAlias in the generated .properties. |
password |
String | ✅ | — | Used for both storePassword and keyPassword. Minimum 6 chars. |
dname |
String? | — | Sia Makerlab default DName |
keytool -dname value. |
validityDays |
Int | — |
36500 (~100 yr) |
keytool -validity. |
Output files (always outside the project folder, in the workspace sidecar):
<workspace>/.vibecoder/keystores/<projectId>/
<projectId>.keystore ← keytool -genkeypair output
<projectId>-keystore.properties ← Gradle-readable signing config
.properties contents:
storeFile=<workspace>/.vibecoder/keystores/<projectId>/<projectId>.keystore
storePassword=<plain-password>
keyAlias=<alias>
keyPassword=<plain-password>Security caveat (current implementation).
KeystoreGeneratorinvokeskeytool -storepass <plain> -keypass <plain>— the password is therefore briefly visible inps -ef//proc/<pid>/cmdlineon the host while the command runs (~1 s). Acceptable under vibe-coder's single-operator LAN threat model, but if you share the host with anyone, generate the keystore manually outside the container and only drop the.keystorefile in. A future release will migrate to-storepass:env. Tracked in roadmap.
If keystore.password policy is stricter than the 6-char minimum, generate
the keystore separately and place the files under
<workspace>/.vibecoder/keystores/<projectId>/ yourself.
To skip keystore generation entirely, omit the field or send "keystore": null
(current default).
Queue a debug build. Returns immediately with a BuildDto; stream live
output via /ws/projects/{id}/builds/{buildId}/logs.
List recent builds.
Send SIGTERM (then SIGKILL after 5 s) to the build subprocess.
Stream the produced APK.
curl -X POST http://localhost:17880/api/projects/my-app/claude/console/prompt \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"text":"Add a settings screen with a dark mode toggle"}'
# → 202 Accepted (response streamed over WebSocket)Tear down the current session and start fresh (--resume <id> is dropped).
Send SIGTERM to the running Claude child process and preserve the saved
session-id. The next prompt resumes the same conversation via --resume.
Use to abort a turn that's heading in the wrong direction.
curl -X POST http://localhost:17880/api/projects/my-app/claude/console/cancel \
-H "Authorization: Bearer $TOKEN"
# → 202 Accepted (no body)Model, plan, quota remaining (cached 60 s).
{
"sessionId": "01J...",
"processAlive": true,
"model": "claude-opus-4-7-1m",
"plan": "Max",
"quotaRemaining": "20% remaining, resets at 2026-05-25T00:00Z",
"usagePercent": 80,
"resetAt": "resets at 2026-05-25T00:00Z",
"updatedAt": "2026-05-24T15:00:00Z"
}usagePercent and resetAt were added in v0.21.0 and are best-effort
(parsed from the CLI's /status plain-text output). Older CLI versions
or unfamiliar output formats yield null, in which case the dashboard
card gracefully shows "couldn't parse" rather than misleading numbers.
The same value powers the Email Notifications +
Webhook Notifications threshold alerts, fired
on transition past warnThresholdPercent (default 80) or
criticalThresholdPercent (default 95).
Returns the full user-defined prompt library. The browser console page uses this to populate the ▼ dropdown. Android client may use the same to ship a "templates" picker.
curl http://localhost:17880/api/prompt-templates \
-H "Authorization: Bearer $TOKEN"{
"templates": [
{
"id": "t-7a3...",
"title": "Add a Compose Settings screen",
"category": "Android",
"body": "Add a Settings screen using Compose Material3 with a dark-mode Switch...",
"createdAt": "2026-05-23T12:00:00Z",
"updatedAt": "2026-05-24T08:11:30Z"
}
]
}CRUD lives only behind the SSR /prompts page in v0.15.0 — REST CRUD is
on the roadmap. The store is a single JSON file at
<workspace>/.vibecoder/prompt-templates.json (RW lock).
/audit is SSR. A GET /api/audit JSON endpoint is on Roadmap §9.B.○.
For direct querying see the Audit Log page's psql snippet.
Every assistant message + user prompt + tool invocation is persisted to
the conversation_turns table for both per-project sessions and the
General Chat scratch project.
curl -H "Authorization: Bearer $TOKEN" \
'http://localhost:17880/api/projects/my-app/history?limit=50&sessionId=...'{
"turns": [
{
"id": 1234,
"sessionId": "01J...",
"turnIdx": 0,
"ts": "2026-05-24T10:00:00Z",
"role": "user",
"content": "Add a settings screen with dark-mode toggle.",
"toolName": null,
"toolUseId": null,
"tokensIn": null,
"tokensOut": null
},
{
"id": 1235,
"sessionId": "01J...",
"turnIdx": 1,
"ts": "2026-05-24T10:00:08Z",
"role": "assistant",
"content": "I'll add a Settings composable with a Switch wired to DataStore.",
"toolName": null,
"toolUseId": null,
"tokensIn": 712,
"tokensOut": 134
}
],
"nextCursor": null
}Query parameters: limit (default 100, max 500), sessionId (filter to
a single Claude session), before (cursor for pagination — pass the
turns[0].id from the previous page).
Same shape as above for the General Chat scratch project
(__scratch__). No projectId path segment.
See Conversation History for the page UI and retention strategy.
Read-only status / diff / log endpoints from v0.10.0 are joined by
a single write endpoint: commit + push.
curl -X POST http://localhost:17880/api/projects/my-app/git/commit \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"message": "feat: settings screen with dark mode",
"push": true,
"onlyTracked": false
}'{
"committed": true,
"pushed": true,
"sha": "a1b2c3d4...",
"branch": "main",
"errorMessage": null
}Behavior (sequential, fail-fast):
-
git config user.name vibe-coder/user.email vibe-coder@localhost(only if unset locally). -
git add -A(orgit add -uwhenonlyTracked=true). -
git diff --cached --quiet— if nothing staged, returnscommitted=false, pushed=false. -
git commit -m <message>withGIT_AUTHOR_*env. - If
push=true,git push <upstream-branch>withGIT_TERMINAL_PROMPT=0andGIT_SSH_COMMAND='ssh -o BatchMode=yes'— no interactive prompts ever appear. - Push failure does not roll back the commit — returns
committed=true, pushed=false, errorMessage=.... Retry with the same call is safe.
Auth is the project's existing PAT (~/.git-credentials) or SSH key
(~/.ssh/id_ed25519) — register via the Git integrations endpoints in
the previous section.
See Git Workflow for the SSR form and roadmap.
Stream the project's source tree as a application/zip. The response uses
Content-Disposition: attachment; filename="<projectId>-source-<yyyyMMdd-HHmm>.zip"
so browsers immediately download.
curl -L -OJ -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/projects/my-app/zipExcludes (hard-coded for deterministic backups):
-
.git/,.idea/ -
build/,.gradle/,node_modules/(anywhere in the tree) -
*.apk,*.aab
This is a SSR route, not under /api/* — same session-cookie / Bearer
auth applies. Streamed directly (no temp file), so large source trees are
memory-safe.
See Disk Monitor §"directory-level size" for inverse operation (measuring how much you'd be downloading).
Build detail page has CSRF-protected forms that fire a structured prompt into the project's Claude session — actual upload runs through the respective MCP. No REST-level wire today.
| Form | Endpoint | MCP it delegates to |
|---|---|---|
| Play Console upload | POST /projects/{id}/builds/{buildId}/play-upload |
google-play-publisher |
| TestFlight upload | POST /projects/{id}/builds/{buildId}/testflight-upload |
app-store-connect |
See Publishing to Stores for the full flow, prechecks, and tested-tester-group caveats.
Build detail page has these CSRF-protected forms — no REST-level wire:
| Action | Endpoint |
|---|---|
Create default AVD (vibe-default) |
POST /emulator/avd/create-default |
| Headless launch |
POST /emulator/avd/launch (form field name) |
| Stop a running emulator |
POST /emulator/avd/stop (form field serial) |
See Android Emulator for full lifecycle docs and :full
image setup.
GET /2fa — show enable / disable page.
| Form | Endpoint |
|---|---|
| Enable |
POST /2fa/enable (form field code) |
| Disable |
POST /2fa/disable (form field code) |
The pending secret lives in server memory until the user verifies a code. See Two-Factor Auth for the threat model and recovery.
LIKE-prefix match against this project's user turns. Used by the
browser console (and the Android client) for autocomplete.
curl -H "Authorization: Bearer $TOKEN" \
'http://localhost:17880/api/projects/my-app/claude/prompt-suggestions?prefix=Add%20a&limit=8'{ "suggestions": [
"Add a settings screen with dark mode toggle",
"Add a unit test for SignInViewModel",
"Add a release-notes section to CHANGELOG"
] }| Query param | Default | Notes |
|---|---|---|
prefix |
(required, ≥ 2 chars) | LIKE-escaped; sub-2-char ignored to avoid empty-prefix dumps |
limit |
8 | Clamped to [1, 20]
|
60-second in-memory cache per (projectId, prefix) to absorb keystroke
bursts. No persisted DTO in shared/ yet — the JSON shape is the simple
{"suggestions": [...]} map above.
Streams a JSON envelope of every persisted turn in the project.
Content-Disposition: attachment; filename="<id>-conversation-<yyyyMMdd-HHmm>.json".
Envelope (schemaVersion 1):
{
"schemaVersion": 1,
"projectId": "my-app",
"exportedAt": "2026-05-24T15:00:00Z",
"turnCount": 1234,
"turns": [
{ "sessionId": "...", "turnIdx": 0, "ts": "...", "role": "user", "content": "...", ... }
]
}Multipart upload. Query string carries ?_csrf=<session-csrf> because
the form is enctype=multipart/form-data and Ktor requireCsrf() reads
form fields by default.
| Query param | Default | Notes |
|---|---|---|
_csrf |
(required) | Session CSRF token (sess.csrf) |
dryRun |
true |
Counts what would be inserted without writing |
5 MB body cap. Idempotency is session-id level — sessions already present in the target are skipped wholesale.
See Conversation Search & Archive for the full flow including auto-archive (v0.33.0).
| Form | Endpoint |
|---|---|
| Add schedule |
POST /projects/{id}/automation/schedules (form: cronExpr, description) |
| Toggle schedule |
POST /projects/{id}/automation/schedules/{scheduleId}/toggle (form: enabled) |
| Delete schedule | POST /projects/{id}/automation/schedules/{scheduleId}/delete |
| Create webhook secret |
POST /projects/{id}/automation/secrets (form: name) |
| Delete webhook secret | POST /projects/{id}/automation/secrets/{secretId}/delete |
Admin-auth-free endpoint that lets external systems (GitHub Actions, GitLab CI, monitoring) fire a debug build.
SECRET='<paste>' # one-time shown in /automation
SECRET_ID='<from-secret-table>'
BODY='{"ci":"github-actions","sha":"deadbeef"}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST "http://<host>:17880/api/webhooks/build/my-app" \
-H "X-Vibe-Secret-Id: $SECRET_ID" \
-H "X-Vibe-Secret: $SECRET" \
-H "X-Vibe-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
--data "$BODY"| Header | Required | Purpose |
|---|---|---|
X-Vibe-Secret-Id |
✅ | Row lookup in build_webhook_secrets
|
X-Vibe-Secret |
✅ | Plaintext secret; server compares SHA-256(secret) to stored hash (TLS expected) |
X-Vibe-Signature |
optional |
HMAC-SHA256(secret, body) hex; if present and body non-empty, also verified |
Response: 202 Accepted {"projectId":"my-app","triggered":true}.
See Build Automation for the full security discussion and roadmap (rate limiting, HMAC-only mode).
GET /backup — page with subdirectory sizes and the download button.
GET /backup/download — streams application/gzip (Content-Disposition
vibe-workspace-<yyyyMMdd-HHmm>.tar.gz). Excludes
postgres/, dev-tools/gradle/caches+daemon, npm-cache,
playwright, .vibecoder/__scratch__/, .vibecoder/*/logs/.
PostgreSQL is not in the tar — use pg_dump separately (the page
renders the command). See Backup & Restore.
-
/history— global conversation search (LIKE + role filter, 200-hit cap, ±100 char excerpt with<mark>highlight). -
/logs— build-log grep across.vibecoder/<projectId>/logs/*.log(last 2 MB per file scanned, 200-match cap, optional project filter). -
/projects/{id}/deps— Gradle dependency tree (90 s timeout, raw output 200 KB cap) +group:name:versioncoordinate extraction. -
/projects/{id}/env-files— whitelist editor for 7 env / build property files (256 KB cap each, atomic write). -
/projects/{id}/wrapper— Gradle wrapper version + 1-click upgrade (atomicdistributionUrlrewrite). v0.35.0 -
/projects/{id}/stats— file / LoC / size per language (35+ languages classified). v0.35.0 -
/code-search— workspace-wide grep with<mark>highlight (200 match cap, 5 MB / file cap). v0.35.0 -
/multi-console?projects=id1,id2,…— N-pane iframe grid (≤ 6 projects). v0.36.0 -
/users— admin-only multi-user / role management. v0.37.0
All SSR-only (no REST wire).
Bearer-authenticated JSON of the registered Claude sub-agents (from
~/.claude/agents/*.md). Used by the browser console UI's dispatch
dropdown; Android / VS Code clients can consume the same.
curl -H "Authorization: Bearer $TOKEN" http://localhost:17880/api/agents{
"agents": [
{
"name": "code-reviewer",
"sizeBytes": 1234,
"preview": "You are a security-focused code reviewer..."
}
]
}Up to 200 chars of preview per agent. CRUD on the agent files themselves is SSR-only — see Custom Agents.
| Form | Endpoint |
|---|---|
| Create user |
POST /users (form: username, password, role) |
| Change role |
POST /users/{id}/role (form: role) |
| Delete user | POST /users/{id}/delete |
All require session.role == "admin". Member sessions get redirected
to dashboard with an err= query param.
The browser cookie carries the same Bearer token internally — there's
no separate "admin Bearer". v0.45.0 added JSON API + WebSocket role
guards (requireApiWrite / requireApiAdmin / WS viewer_readonly)
so a viewer / member Bearer token gets the same enforcement as the SSR
session. See Security Model
for the list of guarded endpoints.
See Users & Roles for the lockout protections and
the remaining admin-page sweep (/audit, /settings, /backup plus
the v0.47.0 /settings/* sweep).
Forwards every HTTP request to http://127.0.0.1:6080/{path} inside the
container. Used by the noVNC HTML / JS / CSS / image fetches. 15-second
backend timeout; Content-Type preserved from the upstream response;
falls back to application/octet-stream.
Admin-only via requireAdminOrRedirect. viewer / member get redirected.
Subprotocol binary. The server opens a backend WebSocket to
ws://127.0.0.1:6080/websockify and forwards binary/text frames in
both directions. Authentication is the vibe_session cookie passed
through the WS handshake; the server verifies device.userId → user.isAdmin before forwarding any frame.
This proxy is what lets /emulator embed the noVNC client in an
iframe (autoconnect=true&resize=remote) without exposing port 6080
on the host. See Android Emulator for the full setup.
JDK 11+ java.net.http.HttpClient + WebSocket. Zero external deps.
Real multi-agent. Each (projectId, agentName) gets its own Claude child.
See Multi-Agent Sub-Agent Pool for the model.
All routes here require requireApiWrite() — viewer Bearer tokens get
403 viewer_readonly.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Audit the auth module for OWASP A01-A03 issues."}' \
http://localhost:17880/api/projects/myapp/agents/reviewer/console/prompt{"ok": true}First prompt to a fresh agent session is auto-prefixed with
Use the <agent> sub-agent to on the server side.
SIGTERMs the alive child. Session-id is preserved so the next prompt resumes the same conversation.
Lists agent names with a live child for this project.
{"projectId": "myapp", "agents": ["reviewer", "frontend"]}Same protocol as /ws/projects/{id}/console/logs — same auth (cookie or
Bearer first-frame), same frame types (ConsoleSessionStarted,
ConsoleAssistant, ConsoleToolUse, ConsoleToolResult, ConsoleError,
ConsoleDone, ConsoleSystem, ConsoleReplayBegin/End), same since=
query for replay-from-seq.
Unlike the main console WS, client→server frames are ignored here. Send prompts via the REST endpoint above.
VAPID-authenticated browser notifications. See Web Push for the full lifecycle and limitations (payload-less).
curl -H "Authorization: Bearer $TOKEN" http://localhost:17880/api/push/vapid-public-key{"publicKey": "BL2...65-byte-uncompressed-base64url"}The browser uses this as applicationServerKey for PushManager.subscribe.
Upserts a subscription by endpoint (unique). Requires requireApiWrite.
{
"endpoint": "https://fcm.googleapis.com/fcm/send/cXl…",
"p256dh": "BAR…",
"auth": "uF1…",
"userAgent": "Mozilla/5.0 (…)"
}{"id": "5d6c8b2e-…", "ok": true}p256dh / auth are kept in the row for forward compatibility with
RFC 8291 payload encryption (not yet implemented).
Removes one row. Browsers should call this on unsubscribe(). The server
also auto-removes rows when the push service returns 404 / 410.
See WebAuthn (Passkey) for the full lifecycle.
Both endpoints require an active session (cookie or Bearer).
# 1) Server-side challenge
curl -X POST -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/api/webauthn/register/options{
"challenge": "base64url-32-bytes",
"rpId": "vibe.local",
"rpName": "Vibe Coder",
"userId": "base64url(userId)",
"username": "alice",
"excludeCredentialIds": ["base64url-credential-id", ...]
}# 2) Submit the AuthenticatorAttestationResponse the browser produced
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientDataJSON":"base64url",
"attestationObject":"base64url",
"transports":["usb","nfc"],
"name":"MacBook Touch ID"
}' \
http://localhost:17880/api/webauthn/register/verify{ "id": "<row-id>", "name": "MacBook Touch ID", "ok": true }These endpoints are unauthenticated — they ARE the login flow.
# 1) Lookup which credential IDs are registered for the username
curl -X POST -H "Content-Type: application/json" \
-d '{"username":"alice"}' \
http://localhost:17880/api/webauthn/assert/options{
"challenge": "base64url-32-bytes",
"rpId": "vibe.local",
"allowCredentialIds": ["base64url-id", ...]
}# 2) Submit the AuthenticatorAssertionResponse
curl -X POST -H "Content-Type: application/json" \
-d '{
"credentialId":"base64url",
"authenticatorData":"base64url",
"clientDataJSON":"base64url",
"signature":"base64url",
"userHandle":"base64url-or-null"
}' \
http://localhost:17880/api/webauthn/assert/verifyOn success the response sets the vibe_session cookie + returns:
{ "token": "<bearer>", "deviceId": "<uuid>", "username": "alice" }401 Unauthorized on any validation failure (challenge expired,
credential unknown, signature invalid, sign-count regression, etc.).
-
GET /static/manifest.json— Web App Manifest (standalone display, 512x512 icon, theme color#0b0d12). -
GET /static/sw.js— service worker. Cache-first for/static/*, network-only for/api/*//ws/*//admin/*.
Registered automatically by AdminTemplates.shell via inline
<script> (navigator.serviceWorker.register('/static/sw.js')).
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/api/env-setup/components{
"components": [
{"id":"java","displayName":"JDK 17","status":"INSTALLED","installable":false, ...},
{"id":"android-sdk","displayName":"Android SDK","status":"MISSING","installable":true, ...},
{"id":"claude-auth","displayName":"Claude 로그인","status":"INSTALLED","installable":false, ...}
]
}Trigger sequential install of every installable component. Returns
{"taskId":"..."}. Subscribe to /ws/env-setup/{taskId}/logs for live
output.
Install a specific component (e.g. android-sdk).
The ComponentStateDto returned by GET /api/env-setup/components has
no installingTaskId (or equivalent) field. Its full shape is:
id, displayName, description, sizeHint, status, message, installable
That means the wire does not tell you which component is currently installing. The contract is:
- Client calls
POST .../install(orinstall-all). - Server returns
EnvSetupTaskDto{ taskId }once. - Client stores the
(componentId → taskId)mapping itself and subscribes to/ws/env-setup/{taskId}/logs. - When
WsFrame.Donearrives, the client drops the mapping and may refreshGET /componentsto confirm the newstatus.
The server has a lastTask[component] cache internally (used by the SSR
template to render "View live progress →" links), but it is intentionally
not exposed in any JSON response — different clients can be installing
different components concurrently from different sessions, so per-client
ownership of the taskId is the right model.
If your UI shows a per-component "installing…" spinner, do it from local
state seeded by the install response, not by re-polling /components.
Upload .credentials.json obtained on another machine.
curl -X POST http://localhost:17880/api/env-setup/claude-auth/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@$HOME/.claude/.credentials.json"
# → {"targetPath":"/home/vibe/.claude/.credentials.json", ...}curl -X POST http://localhost:17880/api/env-setup/claude-auth/api-key \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"apiKey":"sk-ant-..."}'
# → 204 No ContentRemove the API key (also accepts POST for clients that lack DELETE).
Spawn claude auth login wrapped in script -q. Returns the initial
state.
curl -X POST http://localhost:17880/api/env-setup/claude-login/start \
-H "Authorization: Bearer $TOKEN"{
"id":"task-...",
"state":"STARTING",
"url":null,
"startedAt":"2026-05-23T12:00:00Z",
"updatedAt":"2026-05-23T12:00:00Z",
"errorMessage":null,
"lastLines":[]
}Poll /api/env-setup/claude-login/status every 1 s until state becomes
AWAITING_CODE (and url is set).
Same shape as above. Returns 204 if no session active.
curl -X POST http://localhost:17880/api/env-setup/claude-login/submit \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"code":"abc...#xyz"}'State → VERIFYING, then DONE or FAILED.
Destroy the child process. State → CANCELED.
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/api/env-setup/mcp{
"entries": [
{
"id":"github",
"displayName":"GitHub",
"pkg":"@modelcontextprotocol/server-github",
"description":"...",
"category":"Git 호스팅 (GitHub / GitLab / Gitea / Bitbucket)",
"trust":"VERIFIED",
"recommended":true,
"homepage":null,
"configFields":[
{"key":"GITHUB_PERSONAL_ACCESS_TOKEN","label":"GitHub PAT","placeholder":"ghp_...","isSecret":true,"required":true,"help":"..."}
],
"status":"NOT_INSTALLED",
"configValues":{},
"comingSoon":false
},
...
]
}The configFields[] entries have a fixed shape with these 8 keys
(no type, no default — those concepts don't exist in the catalog):
| Key | Type | Required | Default | Purpose |
|---|---|---|---|---|
key |
String | ✅ | — | Stored verbatim as the env var name in .mcp.json (e.g. GITHUB_PERSONAL_ACCESS_TOKEN). |
label |
String | ✅ | — | Human-readable text for the form label. |
placeholder |
String? | — | null |
HTML placeholder attribute, e.g. "ghp_...". |
isSecret |
Boolean | — | false |
Render as <input type="password"> / masked field. |
required |
Boolean | — | true |
Server rejects install with 400 missing_config if a required field is empty. |
help |
String? | — | null |
Tooltip / sub-label describing the field. |
isFile |
Boolean | — | false |
v0.11.0+. true → render a file picker and call the multipart upload endpoint below. |
acceptMime |
String? | — | null |
When isFile=true, value for HTML <input accept="...">, e.g. ".json,application/json". |
Notes:
- A field's value type is always String at the wire level. Files go through the multipart upload endpoint and the server returns an absolute path which the client then sends back as the String value.
- There is no
defaultfield in the wire. If a catalog entry needs a sensible default (e.g.--workspace-root /workspaceforfilesystem), it is baked intoargsTemplateinMcpCatalog.kt, not exposed to the client.
curl -X POST http://localhost:17880/api/env-setup/mcp/install \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"selections": {
"filesystem": {},
"github": {"GITHUB_PERSONAL_ACCESS_TOKEN":"ghp_..."},
"context7": {}
}
}'
# → {"taskId":"..."} (subscribe to /ws/env-setup/{taskId}/logs)Removes entries from .mcp.json. The npm package itself stays on disk.
curl -X POST http://localhost:17880/api/env-setup/mcp/unregister \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"ids":["github","slack"]}'
# → 204 No ContentUpload a secret file (Service Account JSON, Apple .p8, OAuth client.json)
for an MCP entry whose configFields[].isFile is true.
curl -X POST http://localhost:17880/api/env-setup/mcp/google-play-publisher/file/GOOGLE_PLAY_SERVICE_ACCOUNT_JSON \
-H "Authorization: Bearer $TOKEN" \
-F "file=@./play-sa.json"
# → {"path":"/home/vibe/.config/mcp-secrets/google-play-publisher-GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.json"}Then pass the returned path as the value for that config key in the
subsequent POST /api/env-setup/mcp/install:
curl -X POST http://localhost:17880/api/env-setup/mcp/install \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"selections": {
"google-play-publisher": {
"GOOGLE_PLAY_SERVICE_ACCOUNT_JSON": "/home/vibe/.config/mcp-secrets/google-play-publisher-GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.json",
"GOOGLE_PLAY_PACKAGE_NAME": "com.siamakerlab.myapp"
}
}
}'- File saved with 0600 permissions in
${CLAUDE_CONFIG_DIR}/mcp-secrets/. - 128 KB size cap (normal Service Account /
.p8is a few KB). - Filename sanitized — only
[A-Za-z0-9._-]in the stored name. - Re-upload to replace (atomic move).
List registered PATs (masked) + SSH public key.
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:17880/api/settings/git-integrations{
"tokens": [
{"provider":"github","host":"github.com","username":"x-access-token","tokenMasked":"••••••••wxyz","createdAt":"2026-05-23T12:00:00Z","note":null}
],
"sshPublicKey": "ssh-ed25519 AAAA... vibe-coder"
}curl -X POST http://localhost:17880/api/settings/git-integrations \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"provider":"github",
"host":"github.com",
"username":"x-access-token",
"token":"ghp_...",
"note":"vibe-coder PAT, expires 2026-12"
}'
# → 204 No Contentcurl -X POST http://localhost:17880/api/settings/git-integrations/delete \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"host":"github.com"}'Generate ed25519 key pair if missing. Returns the full state including the public key for the user to copy.
{
"code": "missing_clone_url",
"message": "sourceType=clone 일 때 cloneUrl 이 필수입니다.",
"detail": null
}HTTP status codes: 400 bad input, 401 no/invalid token, 403 insufficient
permission, 404 not found, 409 conflict, 413 payload too large, 502
upstream failed (git clone, npm install), 504 timeout.