* feat(cli): add central error translation layer (PR1)
Introduce server/internal/cli/errors.go, a single user-facing error
translation layer that collapses raw transport errors, HTTP status
errors, and internal verb-wrapped chains into clear, localized messages.
- ErrorKind classification (network timeout/DNS/refused/TLS/offline,
401/403/404/409/400+422/429/5xx, unknown)
- NetworkError wraps transport errors and strips the raw URL from the
user-facing message; classifyNetworkError categorizes via errors.As/Is
with string fallbacks
- HTTPError.Kind() maps status codes onto ErrorKind
- FormatError: bilingual output (English default, auto-switch to Chinese
on a zh LC_ALL/LC_MESSAGES/LANG locale), validation errors surface the
server message; --debug / MULTICA_DEBUG appends the full raw chain
- ExitCodeFor: tiered exit codes (network=2, auth=3, 404=4, validation=5,
other=1)
- client.go: default HTTP timeout 15s -> 30s, overridable via
MULTICA_HTTP_TIMEOUT; wrap every transport Do() error as *NetworkError
- main.go: route errors through FormatError + ExitCodeFor, add persistent
--debug flag
Unit tests cover every ErrorKind, classification, language detection,
exit codes, server-message extraction, and timeout parsing.
Refs MUL-3104. PR1 of 3; PR2/PR3 (status-code copy refinement and
per-command customization) follow separately.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): address review — unify command timeouts and classify all helper errors
Must-fix 1: command-level contexts no longer truncate MULTICA_HTTP_TIMEOUT.
Added cli.APITimeout/AtLeastAPITimeout/APIContext (budget = transport timeout
+ small grace, honoring MULTICA_HTTP_TIMEOUT) and replaced the hardcoded 15s
context.WithTimeout in every API command (14 files, 92 sites) with
cli.APIContext. The issue-create/comment path now uses APITimeout() with a
60s floor for attachment uploads.
Must-fix 2: all API helpers now return *HTTPError on status >= 400. Added a
shared newHTTPError(method, path, resp) and routed GetJSON, GetJSONWithHeaders,
PostJSON, PutJSON, PatchJSON, DeleteJSON, DeleteJSONWithBody, UploadFile,
UploadFileWithURL, DownloadFile (and HealthCheck) through it, so issue
update/status/metadata (PUT), comment list (GetJSONWithHeaders), project/label/
comment delete (DELETE) and agent/workspace/autopilot update (PUT/PATCH) all
get HTTPError.Kind() classification, friendly copy, and the tiered exit code
instead of the raw string + exit 1.
Tests: new errors_integration_test.go drives the real helpers against a fake
server and asserts FormatError copy + ExitCodeFor for 401/403/404/422/500
across all 10 helpers, plus a slow-server test proving the command context
does not cancel before the transport timeout. Updated the UploadFileWithURL
assertion to check for *HTTPError.
Refs MUL-3104, PR #3892.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): make remaining fixed-timeout API commands honor MULTICA_HTTP_TIMEOUT
Closes out the timeout work: the last API command paths still used a
hardcoded context deadline that capped MULTICA_HTTP_TIMEOUT. Converted them
to cli.AtLeastAPITimeout(<original floor>) so the env override scales them up
while preserving each original lower bound:
- cmd_autopilot.go autopilot trigger 30s -> AtLeastAPITimeout(30s)
- cmd_attachment.go attachment download 60s -> AtLeastAPITimeout(60s)
- cmd_agent.go avatar upload 60s -> AtLeastAPITimeout(60s)
- cmd_skill.go skill import / search 60s -> AtLeastAPITimeout(60s)
- cmd_runtime.go runtime update 150s -> AtLeastAPITimeout(150s)
- cmd_login.go workspace-creation poll 10s -> AtLeastAPITimeout(10s)
The login poll keeps a short 10s floor to stay responsive within its 5-minute
loop, but it is NOT a silent exception: AtLeastAPITimeout means it still scales
with MULTICA_HTTP_TIMEOUT. Documented in code and covered by a new subtest in
TestAPITimeoutRespectsEnv.
Refs MUL-3104, PR #3892.
Co-authored-by: multica-agent <github@multica.ai>
* style(cli): gofmt cmd_attachment.go to unblock backend CI
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Self-hosted backends without the per-issue metadata route (older builds,
unapplied 105_issue_metadata migration, or proxy/ingress misroutes) reply
404 to GET /api/issues/:id/metadata. The agent runtime bootstrap calls
'multica issue metadata list <issue> --output json' best-effort, but a
non-zero exit was being escalated by Hermes into a failed agent run even
when the rest of the work succeeded.
This makes only the 'list' verb best-effort: a 404 from /metadata now
prints {} (or an empty table) and exits 0. Other status codes (401, 500,
etc.) keep real error semantics, and 'metadata get / set / delete' are
unaffected — those represent explicit caller intent.
To support the status-code check without changing the user-facing error
string, GetJSON now returns *cli.HTTPError on HTTP failures (the format
'GET <path> returned <code>: <body>' is preserved by HTTPError.Error()).
Refs GitHub issue #3711.
Co-authored-by: multica-agent <github@multica.ai>
Fix attachment download for self-hosted deployments using private S3-compatible buckets without CloudFront. Closes#3721.
**Server**
- New unified `GET /api/attachments/{id}/download` endpoint that picks CloudFront / S3 presign / server proxy at request time.
- `ATTACHMENT_DOWNLOAD_MODE=auto|cloudfront|presign|proxy` and `ATTACHMENT_DOWNLOAD_URL_TTL` env knobs; `auto` routes Docker hostnames / localhost / private IPs through the proxy and public S3 endpoints through presign.
- `Storage.PresignGet` capability; S3 implementation generates presigned GET URLs.
- `attachmentToResponse` returns the unified relative endpoint instead of leaking raw unsigned S3 URLs when CloudFront is not configured. Proxy path streams via `io.Copy` with `Content-Disposition` / `Content-Length` / `Cache-Control: no-store` / `X-Content-Type-Options: nosniff`.
**Clients**
- CLI / Desktop / Mobile resolve relative `download_url` values against the configured API base. Desktop covers the Electron native download bridge and the media preview modal; Mobile covers `Linking.openURL`, the markdown image RN loader, and the composer's completed non-image file chip.
- Mobile gains a minimal Node-environment vitest lane wired into `mobile-verify.yml`.
**Docs**
- `.env.example`, `docker-compose.selfhost.yml`, `SELF_HOSTING_ADVANCED.md`, and the `environment-variables` doc set updated with the new env keys and the `ATTACHMENT_DOWNLOAD_MODE=proxy` recommendation for Docker / VPC-internal object stores.
**Tests**
- `internal/storage`, `internal/cli`, `internal/handler` (download endpoint, mode selection, proxy header, `/content` non-regression), `cmd/server` (trusted proxy parser).
- `packages/views/editor/use-download-attachment.test.tsx` and `attachment-preview-modal.test.tsx` exercise relative URL resolution + absolute pass-through.
- `apps/mobile/lib/attachment-url.test.ts` covers every helper branch plus the composer non-image chip case.
* feat(cli): add UploadFileWithURL and AttachmentResponse to APIClient
* feat(cli): add agent avatar command and show avatar_url in agent get output
* fix(server): include id and url in no-workspace file upload response
* fix(cli): remove dead HTTPClient timeout swap, extend ctx to 60s for avatar upload
The 30s context deadline was tighter than the 60s HTTPClient timeout
swap, so the swap was dead code and did nothing for slow connections.
Both Neo and Omni Mentor flagged this in review.
Fix: extend the command context to 60s and remove the HTTPClient
mutation. This is simpler, thread-safe, and actually works for slow
uploads.
* fix: align fallback upload response shape and honor context deadline
- file.go: fallback returns {id, url, filename} instead of {filename, link},
matching the no-workspace path response shape.
- client.go UploadFileWithURL: tolerate empty attachment ID (S3 succeeded
but DB record failed — the file is still usable via its URL).
- client.go UploadFileWithURL: use a context-deadline-aware HTTP client so
that the 60s upload timeout set by the avatar command actually takes
effect instead of being shadowed by the default 15s client timeout.
- client_test.go: update 'missing id' test to verify empty-id success
(fallback tolerance).
* fix(cli): shallow-copy HTTP client to preserve Transport on upload timeout
When the context deadline exceeds the default 15s HTTP client timeout,
UploadFileWithURL was creating a bare &http.Client{Timeout: remaining},
silently dropping any custom Transport, Jar, or CheckRedirect configured
on the original client. This causes obscure connection failures when the
CLI uses an authenticated proxy, custom TLS, or mock transport in tests.
Fix: perform a shallow copy of the original client struct and only
mutate the Timeout field on the copy.
* feat: identify clients via X-Client-Platform/Version/OS
Adds client identification headers (and matching WS query params) across
all first-party clients so the server can split logs/metrics/gating by
caller without parsing User-Agent.
- HTTP: X-Client-Platform, X-Client-Version, X-Client-OS
- WS: client_platform, client_version, client_os query params
- Platform ∈ {web, desktop, cli, daemon}; OS ∈ {macos, windows, linux}
Wired through the shared TS ApiClient/WSClient via a new identity option
on CoreProvider. Web reads its version from package.json/env; Desktop
captures version + OS synchronously in preload via sendSync IPC. Go CLI
and daemon clients populate the same headers using runtime.GOOS
(normalized darwin → macos).
Server-side adds a ClientMetadata middleware that stashes the headers in
request context; the request logger and logger.RequestAttrs surface them
on every access log and handler-level log. Realtime hub logs the same
fields on websocket connect.
CORS allowlist extended for the new headers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: address client-identity PR nits
- Memoize the CoreProvider identity object on Web and Desktop, and key
WSProvider's effect on identity primitives instead of the object
reference, so unrelated parent re-renders no longer tear down and
reconnect the WebSocket.
- Add direct header-injection tests for the CLI and daemon Go HTTP
clients (X-Client-Platform/Version/OS) and a normalizeGOOS unit test
on both packages.
- Add a TS test for WSClient that asserts client_platform/client_version/
client_os land on the upgrade URL and never leak the auth token.
- Add a hub test that dials the WS endpoint with client_* query params
and asserts the "websocket connected" log entry surfaces them as
structured attributes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(cli): add autopilot commands
Expose the existing autopilot REST API through the multica CLI so
users and agents can list, get, create, update, delete, trigger, and
inspect autopilots, plus manage their triggers (schedule/webhook/api).
Also surface the read + core write commands in the agent meta skill
prompt so agents discover them without needing --help.
- new cmd_autopilot.go (+ test) wiring /api/autopilots endpoints
- add APIClient.PatchJSON (autopilot update uses PATCH)
- expose autopilot in CORE COMMANDS group
- extend runtime_config.go meta skill with autopilot entries
- document autopilot command group in CLI_AND_DAEMON.md
* fix(autopilot): address code review — restrict run_only, validate workspace on update
Code review caught two issues with the initial CLI PR:
1. run_only mode is broken end-to-end. The daemon-side
resolveTaskWorkspaceID() in internal/handler/daemon.go only resolves
workspace from issue/chat, so run_only tasks (which have neither)
return 404 from /start. BuildPrompt() would also emit an empty issue
ID. The service-level resolver in internal/service/task.go already
handles AutopilotRunID, but the daemon endpoint uses the handler
copy. Fixing that path is out of scope for the CLI PR; drop
run_only from the CLI and docs so we don't recommend a mode that
cannot complete. Server continues to accept it for the existing UI.
2. UpdateAutopilot did not verify that a new assignee_id belongs to
the workspace, unlike CreateAutopilot. This let a PATCH swap in an
agent from a different workspace. Mirror the same
GetAgentInWorkspace check.
1. Update CLAUDE.md template to document --limit, --offset, --since
params and guide agents to use pagination when comments are large
2. Add GetJSONWithHeaders to API client; CLI now prints "Showing X of Y
comments" to stderr when paginating
3. Cap --since without --limit at 500 server-side to prevent unbounded
result sets
* fix(agent): instruct agents to use download_url for attachments
Agents were not aware of the signed vs unsigned URL distinction in
attachments, causing failures when trying to read images. Added an
Attachments section to the generated CLAUDE.md/AGENTS.md template that
tells agents to always use `download_url`. Also increased signed URL
expiry from 5 to 30 minutes to better accommodate agent processing time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): add `multica attachment download` command
Adds a dedicated CLI command for downloading attachments by ID. The
command fetches attachment metadata from the API (which returns a fresh
signed URL), downloads the file, and saves it locally. This eliminates
the need for agents to understand signed vs unsigned URLs.
Changes:
- New `multica attachment download <id>` CLI command
- New `GET /api/attachments/{id}` backend endpoint
- `DownloadFile` helper on APIClient
- Updated CLAUDE.md template to document the command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): sanitize filename and add download size limit
- Use filepath.Base on attachment filename to prevent path traversal
- Add 100MB size limit to DownloadFile (matches upload limit)
- Include response body in download error messages for debugging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add file attachment support to `multica issue comment add`. The CLI
uploads files via multipart form to /api/upload-file, collects the
returned attachment IDs, and passes them when creating the comment.
Usage: multica issue comment add <issue-id> --content "..." --attachment file1.png --attachment file2.pdf
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(server): distinguish agent vs human CLI actions via X-Agent-ID/X-Task-ID headers
Extract resolveActor helper in handler to centralize agent identity resolution
from X-Agent-ID header with X-Task-ID cross-validation. Fix DeleteComment,
DeleteIssue, and UpdateComment handlers that previously hardcoded "member" as
actor type. Forward MULTICA_TASK_ID as X-Task-ID header from CLI client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(server): add debug logging and test coverage for resolveActor
Add slog.Debug on agent/task validation failures for easier debugging.
Add TestResolveActor with 5 cases covering member fallback, valid agent,
non-existent agent, valid task, and mismatched task.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When agents use the multica CLI during task execution, their comments,
issue updates, and issue creations were attributed to the daemon's user
(via JWT) instead of the agent. Pass MULTICA_AGENT_ID env var from the
daemon, send X-Agent-ID header from the CLI client, and use it in
handlers to set the correct author/actor identity.
- Add drag-to-resize sidebar with localStorage persistence
- Rewrite issue detail page with Tiptap rich text editor, due date picker, acceptance criteria
- Redesign create-issue modal with pill-based property toolbar and expand/collapse
- Consolidate @multica/sdk and @multica/types into apps/web/shared/
- Simplify auth: remove verification codes, PATs, email service (dev-only login)
- Add 401 unauthorized handler to redirect expired sessions to login
- Fix due date format to send full RFC3339 timestamps
- Increase description editor debounce to 1500ms
- Remove arbitrary Tailwind values in create-issue modal
- Renumber migrations (inbox_actor 012→009), remove unused migrations
- UI polish across agents, settings, inbox, knowledge-base pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`multica auth login` now opens the browser for email verification,
receives the JWT via localhost callback, and exchanges it for a PAT.
The legacy PAT-paste flow is preserved via `--token` flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add email verification login flow with 401 auto-redirect
Replace the old OAuth-based login with email verification codes:
- Backend: send-code / verify-code endpoints, verification_codes table (migration 009), rate limiting, Resend email service
- Frontend: two-step login UI (email → 6-digit OTP), auth store with sendCode/verifyCode
- SDK: ApiClient gains onUnauthorized callback; 401 responses auto-clear token and redirect to /login
- Fix login button staying disabled due to global isLoading state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): add brute-force protection, redirect loop guard, and expired code cleanup
- VerifyCode: increment attempts on wrong code, reject after 5 failed tries (migration 010)
- onUnauthorized: skip redirect if already on /login to prevent infinite loops
- SendCode: best-effort cleanup of expired verification codes older than 1 hour
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add master verification code for non-production environments
Allow code "888888" to bypass email verification in non-production
environments to simplify development and testing workflows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add personal access tokens for CLI and API authentication
Add full-stack PAT support: users create tokens in Settings, CLI authenticates
via `multica auth login`. Server stores SHA-256 hashes only. Auth middleware
extended to accept both JWTs and PATs (distinguished by `mul_` prefix).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Add Client.SendHeartbeat/Register methods — no more direct postJSON calls
2. Use url.Values for query params to prevent URL injection
3. Unexport helpers (envOrDefault, durationFromEnv, sleepWithContext)
4. CLI resolveWorkspaceID falls back to daemon.json
5. Implement agent stop (PUT /api/agents/{id} with status=offline)
6. Add --output flag to agent get for consistent UX
7. Add server/multica to .gitignore for stray builds
8. Inject version/commit via -ldflags in Makefile build target
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract daemon logic from cmd/daemon/ into internal/daemon/ package and
create a new unified CLI entry point at cmd/multica/ using cobra. The CLI
supports `daemon` as a long-running subcommand plus ctrl subcommands for
agent/runtime management, config, status, and version.
Server, migrate, and seed binaries remain unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>