feat(inbox): operator task dashboard, agent assign API, rank/estimate, timer, PWA #1
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/dev_pm_agent#1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The operator product is a phone-narrow Cursor chat relay. It is not an inbox for human work that other agents assign, rank, time, and complete.
Bundle (do not split):
Chat (
/chat) stays for Cursor CLI threads. After login, the default surface is the inbox.Related: keep
PLAN.mdsingle-admin design. Do not implement this in the Forgejo control-plane repo (that product is HMAC/gates, not a UI).Current codebase
Relayer is SQLite + command relay, not a task store
crates/relayeris Axum: auth, devices,POST/GET /api/commands, executor-onlyPATCH, repos, file proxy,/ws. Persistence isrusqlite(crates/relayer/Cargo.toml,crates/relayer/src/db/mod.rs). Migrations live inmigrations/and run fromcrates/relayer/src/db/migrations.rs.Config::from_envmapsDATABASE_PATHorDATABASE_URLto a filesystem path (trim_start_matches("sqlite:"), default./data/relayer.db). There is no Postgres driver, no sqlx, no task table.Schema today: single
adminrow,devices(executor|controller),commands(pending/running/done/failed + Cursor models +cursor_chat_id),repos, bootstrap/registration tables. Status machine is Cursor job status, not human work.Agents cannot assign human work
POST /api/commandsrequires a controller JWT. That enqueues a Cursor CLI run on the desktop executor and broadcastscommand_new(crates/relayer/src/api/routes.rs,docs/WEBSOCKET_PROTOCOL.md).EXECUTOR_API_KEYcanPATCHcommand output, sync repos/models, and answer file read/search. There is no third principal that may only create operator tasks. Bots that reused the executor key would inherit file and status write. Bots that reused the human JWT would be the operator.Input cap is 4KB. No idempotency key. No task kinds, priority, estimate, or timer columns.
UI is a capped chat column
apps/web/src/App.tsxroutes/→/chatwhen a JWT exists.Chat.tsxsubtitle is Compact mobile control plane..mobile-frameismax-width: 470px(930px fromsm). Login, setup, and add-device use the same frame.No inbox route, no Start control, no live elapsed clock, no Web App Manifest, no service worker (
apps/web/index.htmlis a Vite shell +vite.svg).package.jsonhas no Vitest/Playwright.Hardcoded translator/workload defaults in Chat are
composer-1.5. Ranking in this issue must not add another pin; useGET /api/modelslike Chat already does when the list is non-empty.Auth stays single-human MFA
Setup: bootstrap device key → username/password → TOTP (
PLAN.md§10,security_reviews/SECURITY_REVIEW_2025-02.md). JWT + optional refresh grace. Auth routes are rate-limited; other APIs are not. Device key + JWT live inlocalStorage(apps/web/src/stores/auth.ts).Existing tests that must stay green:
commands_update_rejects_controller_jwt,commands_update_accepts_executor_api_key, relayer db/auth unit tests, shared serde roundtrips.Why the work is needed
This is inbox + assign API + rank fields + timer + PWA + Postgres. It is not a secrets vault, not a deploy orchestrator, not a chain RPC runner, and not a new host/SKU/model choice.
Constraints / guardrails
adminrow. No multi-tenant, no extra human roles.GET /api/modelslist (same pattern as Chat). If the list is empty, fail the rank job closed; do not hardcode a model id. Do not add cloud instance types or container images in this ticket.RELAYER_DATABASE_URL) that must bepostgres:///postgresql://. Stop treating genericDATABASE_URLas a sqlite file path. Do not connect to another product’s database. Tests may use a local Postgres via sqlx; production placement is out of scope.dpa_prefix or equivalent) —POSTtasks +GETown/created tasks only. Never start/complete, neverPATCH /api/commands, never file read/search, never repo sync, never WS as executor.PATCHrank fields only on tasks inqueued/rankedwhen the rank command id matches. Cannot start the human timer.commandsrows as human work. Newtasks(andtask_time_spansif needed). A rank pass may create a command that reads task summaries and writes priority/estimate; it does not mark the human taskdone.POST .../startsetsstarted_at(timestamptz).POST .../completesetscompleted_atandduration_msfrom server timestamps. Ignore client-supplied elapsed. At most onein_progresstask. Second start →409. Reload resumes fromstarted_at. Completing a non-running task →409.secretsmeans “human must configure a secret elsewhere.” Reject create/update bodies that look like PEM, seed phrases,BEGIN OPENSSH,AKIA,ghp_,xox,postgres://user:pass@, orBearer eyJ. Never persist raw secret material. Never log assigner tokens after mint. Hash tokens (bcrypt/argon2, same family as device keys).deploy,config,chain_script,accountare checklists for the human. This API does not SSH, apply infra, sign txs, or run chain RPCs.Idempotency-Key(UUID or 8–128 char token) on assignerPOST /api/tasks. Same key + same principal → same task id. Different body →409.secrets | account | deploy | config | chain_script | other. Priority 1–9 (1 = do first) after rank; unranked =null. Estimate is integer minutes1..=480or null.dangerouslySetInnerHTMLfor task body; markdown optional only if sanitized (reuse Chat markdown path or plain text). WS payload viaserdestructs, not string concat.display: standalone,start_urlthe inbox (authenticated app still hits/loginif no JWT). Service worker: static shell only. Never cache/apior/ws. Never put JWT/device key in Cache Storage.theme-colorfrom existing CSS variables.lg(or equivalent): inbox uses the full viewport (no.mobile-framemax-width). Queue + now/detail panes. Touch targets ≥ 44px on coarse pointers. Auth pages may stay narrow.localStorage. Mint shows the token once. Device-key-in-localStorage is a known medium; do not make it worse by persisting assigner keys in the SPA.Relevant files
crates/relayer/src/config.rsDATABASE_URLcrates/relayer/src/db/mod.rs+migrations.rscrates/relayer/src/api/routes.rscrates/relayer/src/auth/mod.rscrates/relayer/src/relay/mod.rstask_new/task_updateWS typescrates/relayer/Cargo.tomlcrates/shared/src/models.rsmigrations/*.sqldocs/MIGRATION_STRATEGY.mddocs/WEBSOCKET_PROTOCOL.md/wsas executorPLAN.mdcrates/executor/src/*apps/web/index.html+ newmanifest.webmanifest+ SWapps/web/src/App.tsx/inbox; keep/chatapps/web/src/index.cssapps/web/src/pages/Chat.tsxapps/web/src/api/*apps/web/vite.config.ts/api/ws; PWA assetsapps/web/package.jsonsecurity_reviews/SECURITY_REVIEW_2025-02.mdRecommended direction
sqlx + Postgres for the relayer. One store for admin, devices, commands, tasks, assigner tokens. Numbered SQL migrations. Startup migrate. Optional one-shot importer: if a legacy sqlite file is configured, copy admin/devices/commands/repos then refuse dual-write. Relayer tests use sqlx’s Postgres test harness. Keep Axum surface.
Schema (illustrative):
tasks: id, admin_id, assigner_id nullable, kind, title, body, status (queued|ranked|in_progress|done|cancelled), priority nullable, estimate_minutes nullable, started_at, completed_at, duration_ms, rank_command_id nullable, idempotency_key, created_at, updated_at.assigner_tokens: id, admin_id, name, token_hash, created_at, revoked_at, last_used_at.in_progressper admin.HTTP (controller JWT unless noted):
POST /api/assigners→{ id, name, token }once.DELETE /api/assigners/{id}revoke.GET /api/tasksfilter status/kind; default open (not done/cancelled), order priority ASC NULLS LAST, created_at.POST /api/taskshuman-created (same validator as assigner).POST /api/tasks/{id}/start|complete|cancelPOST /api/tasks/rank→ create a command whose prompt is “rank these open tasks”; executor PATCHes priority + estimate only.Authorization: Bearer <token>onPOST /api/v1/agent/tasks(and GET created). Same JSON body. Rate-limited. OpenAPI snippet indocs/.Rank pass. Executor already runs
agent -pfor commands. Add a template the human/rank endpoint uses. Output JSON{ "id": "uuid", "priority": 1-9, "estimate_minutes": n }[]. Relayer validates IDs belong to this admin and are notdone. Do not execute task bodies. Model = request/workload selection from/api/models, not a constant.UI.
/inboxthree zones on desktop: Now (in_progress + running timer fromstarted_at), Queue (ranked then unranked), Done (recent). Detail pane: body, kind, estimate vs actual, Start / Complete. Mobile: list + detail full screen, sticky Start. Nav: Inbox | Chat | Devices. Login subtitle can stay compact; inbox must not.PWA. Manifest name/short_name,
standalone, icons (maskable + any; SVG or PNG inapps/web/public). Document Android: browser menu → Add to Home Screen. No native store.Acceptance criteria
DATABASE_URLpointing at a file path is not silently used.POSTcreates aqueuedtask visible on/inboxover WS or refresh. Title/kind/body persisted. DuplicateIdempotency-Keyreturns the same id.PATCH /api/commands, cannot start/complete, cannot file read. Executor key cannot mint assigners or start the timer. Controller JWT still cannotPATCHcommands (403).in_progress+started_at. Second start (any task) →409. Complete →done+duration_ms≈ server elapsed (tolerance 2s in tests). Clientduration_msin the body is ignored.400) for all principals. Kindsecretsallowed only as instructions without secret material.lg+: inbox uses full viewport (no 470px column). Mobile: usable list + Start, 44px targets./chatstill works./apior/ws. Logged-out install still reaches login./goes to/inbox, not/chat.Test plan (functional paths)
POST /api/taskskindconfigqueued; appears in GET409task_newto controllerduration_msin 500–3000409; A stillin_progressstarted_at409cancelled; no duration required; slot freedone/cancelledunless?status=//inbox/chatsend commandstandalone, start_url inbox, icons401400Vitest: kind/secret-scanner, duration math, sort order. Relayer: table-driven HTTP (assigner vs JWT vs executor). Playwright (5 workers): login stub or test admin → inbox start/complete; desktop vs mobile viewports; chat still lists threads.
PLAYWRIGHT_SKIP_EXECUTOR=1OK for UI.Test plan (attack, hack, and abuse)
PATCH /api/commands/{id}forged done401/403; row unchangedPOST .../start403POST /api/assigners401/403403postgres://x:y@400; no row<img onerror>{duration_ms: 1}after 60s wallin_progress(DB constraint)Idempotency-Key: ../../etc/passwd400; no path IOdeployauto-runs a script/api/files/read401/403401GET /api/tasksbodyURLrel=noopener; noNavigateto itVerification criteria
cargo test --workspacegreen including new task/auth tests and both existing command PATCH tests.verify-issue-*)./chatsurviving; installability checks (manifestlink,displaystandalone).curlPOST task → inbox shows it → Start (clock) → Complete (elapsed stored) → rank (if executor up) reorders. Paste a fake PEM → rejected. Android: Add to Home Screen opens standalone login/inbox..mobile-frameon inbox atlg; nodangerouslySetInnerHTMLon task body.Out of scope
cursor_chat_id.Migrated to
PlasticDigits/cl8y-pm#1.code/dev_pm_agentis archival only; live PM/workflow assignment work continues inPlasticDigits/cl8y-pm.