mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
admin-workspace-create2
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8355c7aeca |
fix(cli): name the TLS handshake timeout and stop masking it on login (MUL-7572, GH #8654) (#8744)
A Windows user whose network dropped the two-packet TLS ClientHello that Go 1.24+ sends by default (post-quantum key share, ~1.5 KB) saw only "Sign-in did not complete: the server could not issue an access token" and, on the --token path, "make sure it is valid and not expired". Neither mentioned the network, and the generic timeout copy pointed at MULTICA_HTTP_TIMEOUT, which does not govern the handshake. - classify net/http's "TLS handshake timeout" as its own kind, with copy that names the GODEBUG=tlsmlkem=0 check in both languages - let `multica login` keep the transport copy for transport failures; its sign-in copy now only overrides HTTP refusals - troubleshooting entry (en/zh/ja/ko) Co-authored-by: multica-agent <[email protected]> |
||
|
|
60408ef252 |
MUL-7090 fix(autopilot): let an agent trigger a manual run for the human it acts for (#8099)
* fix(autopilot): let an agent trigger a manual run for the human it acts for MUL-6951 replaced canCreatorInvokeAgent, which judged automatic dispatch by the autopilot CREATOR and needed no trigger, with ResolveAutopilotTriggerPrincipal, which resolves the FIRING TRIGGER's creator. The manual entry point passes no trigger by construction, so the resolver returned nothing on its first line and every manual trigger from a non-member actor was refused. Scheduled runs kept working — they have a trigger row — which is what made this read as a configuration problem rather than a regression. Reported as #8078. The refusal was also self-contradicting: requireAutopilotWrite had already resolved a real human from the request (for a task token, the runtime owner the middleware binds) and confirmed they may trigger this autopilot. Three lines later memberActorUserID discarded that human for not being a "member" actor, and the empty identity fell into the automation branch. TriggerAutopilot now resolves the human the caller acts FOR: - a member acts for themselves, judged exactly as before; - an agent acts for its run's ORIGINATOR, which is how every other invoke surface judges an agent actor (MUL-3963 / canInvokeAgent), including this file's own private-leader gate on save. Reading the runtime owner instead would have been a smaller diff and the wrong grant: it makes every agent a standing proxy for its machine's owner, so anyone who can talk to the agent inherits that owner's autopilot rights. The originator must ALSO hold write access, so an agent can only press a "Run now" that the human ordering it could press themselves. No resolvable human is refused outright (403). The alternative was the workspace-broad exception invokeAgentDecision grants unattributed agent/system principals on a public_to-workspace agent, which would have admitted an originator-less chain here too. Rejected per Bohan: a manual trigger is by definition somebody's decision, so a chain arriving with no human at its top means something upstream dropped it, and that should surface. Two things this does NOT change, both deliberate: - requireAutopilotWrite still judges the request's authenticated user. That it resolves the runtime owner for an agent is pre-existing and shared by all eight autopilot write endpoints; narrowing it is a separate change with its own blast radius, filed separately. - The run's attribution stays direct_human to the resolved human rather than delegation. attributionForIssueTask treats a valid actor as direct_human by contract, shared with assign/promote/rerun, so labelling this one path delegation means threading a new parameter through TaskService's enqueue chain. The human on the hook is correct either way; only the label is coarser. Filed as follow-up rather than half-applied to one execution mode. Two independent defects the same report exposed: - The skip reason blamed "the trigger records no owner" on a path that has no trigger, sending #8078's reporter hunting a broken trigger owner for a day. It now branches on whether a trigger exists. - `multica autopilot trigger` printed "Autopilot triggered" and exited 0 for a skipped run, so the operator — and any agent running it for them — read success. It now mirrors the web client's runNowToastKind whitelist: only issue_created and running are success, everything else prints failure_reason and exits non-zero. --output json still writes the run to stdout first. This is the half that made the bug silent, and it covers every skip reason, not just this one. The MUL-4525 admission test asserted that (no actor, no trigger) must be refused and called it "automation dispatch" — but that is exactly the argument pair a manual trigger produced, so the test pinned the regression and would have blocked the fix. Rewritten to assert the no-principal phrasing and, explicitly, that the reason does not blame a trigger row that does not exist. Verification (real PostgreSQL 17): new handler suite covers agent-with- originator (dispatches), agent-without-originator (403, no run row), originator lacking write (403, no run row) and the untouched member path. The acceptance test was confirmed to fail against the pre-fix handler with exactly the reported symptom — status skipped, invocation_not_allowed. internal/service and internal/handler pass; the one internal/handler failure (TestPluginRoutes- RequireWorkspaceAdmin, a duplicate user email) and the cmd/multica failures reproduce identically on a clean tree. Refs #8078 Co-authored-by: multica-agent <[email protected]> * fix(autopilot): judge a manual trigger by the ordering human alone Addresses Elon's review of #8099. Two must-fixes, both confirmed against the code before changing it. Must-fix 1 — the endpoint still required TWO humans to hold the grant. requireAutopilotWrite ran in front of the originator check, and under a task token it judges the RUNTIME OWNER. So the effective rule was "runtime owner AND originator", not "the human who ordered it" — the opposite failure from the one #8078 reported, and it broke a case a member is plainly entitled to: U created the autopilot and may invoke an agent running on O's machine, O holds no grant, so U could press Run now themselves but could not ask the agent to. It also contradicted the docs this PR shipped, which already promised originator-based authorization. requireAutopilotTriggerInvoker is now this endpoint's only write gate. requireAutopilotWrite is not called alongside it. Workspace tenancy for the authenticated caller is unaffected: /api/autopilots sits inside the router's RequireWorkspaceMember group, so dropping the second check loses no isolation — it only stops a third party's machine ownership from vetoing the ordering human's own rights. The other seven autopilot write endpoints are untouched and stay with MUL-7108. Authorization also moved ahead of the status check, so an unauthorized caller cannot tell an active autopilot from a paused one. The positive test could not have caught this: it let testUserID be runtime owner, autopilot creator and originator at once. TestTriggerAutopilot_ RuntimeOwnerNeedsNoGrant now separates all three — the ordering human creates the autopilot, the runtime owner is a plain member with no grant — and was verified to fail against the previous commit with exactly the 403 Elon described. Must-fix 2 — the actionable refusal never reached the person running the command. The handler named the cause, but FormatError collapses every 403 into generic "no access" copy, so `calling run records no originator` died in the response body. That is the same silence #8078 was about, one layer up. Both refusals now carry a stable code (autopilot_trigger_no_originator, autopilot_trigger_forbidden) via writeErrorCode, and cli.ServerErrorCode reads it. The trigger command opts those two out of the generic copy and prints guidance. The branch is on the code, never on the English sentence: copy edits and translation must not silently disable it. The opt-out is per-code, so every 403 this command has not named keeps the deliberately vague copy that stops a refusal confirming a resource exists — pinned by a test. The two 403s deliberately share one sentence and one code whether the ordering human is a non-member or merely ungranted, so the endpoint cannot be used to probe workspace membership. Verification (real PostgreSQL 17): - TestTriggerAutopilot_RuntimeOwnerNeedsNoGrant fails against the previous commit's handler and passes now; the other four trigger tests still pass. - Both 403 tests assert the code, not just the status — the CLI's output depends on it. - New cmd/multica test runs the real cli.FormatError over both refusals, since the defect being guarded lives in FormatError's collapse, and asserts an un-opted-in 403 still gets the generic copy. - New internal/cli TestServerErrorCode covers missing code, non-JSON, malformed JSON, prose in `code`, and a non-HTTP error. - internal/service passes. Pre-existing unrelated failures reproduce identically on a clean tree: TestPluginRoutesRequireWorkspaceAdmin and TestCommentSourceContextLifecycle in internal/handler, four TestCLIConfig_* in internal/cli. - gofmt, go vet, git diff --check clean. Docs corrected at both sites to state the contract explicitly: the machine owner's grants are not consulted, only workspace membership. Refs #8078 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
54027ba763 |
MUL-6639: fix(skills): metadata-only skill listings and stall-based timeouts (#7574)
* MUL-6639: feat(skills): list skill files as metadata, not content `multica skill get` and `multica skill files list` both downloaded every file body to render output that shows none of them — `skill get`'s table view prints four columns, and the file listing prints paths and timestamps. Past a few hundred KB that stopped being waste and became a hard failure: a 599KB skill could not be fetched at all, and the listing that would have shown which file was oversized required downloading all of them first, so the diagnostic died with the thing it was meant to diagnose (GH #7498). Both endpoints now take `?include=content|metadata`. The metadata shape carries path, byte size and content hash per file, plus the size and hash of the SKILL.md body — computed in Postgres, so the bodies never leave the database. Response size now tracks file count, not content size. `GET /api/skills/{id}/files` defaults to metadata: it is a list endpoint, the CLI is its only caller, and `GET /api/skills` already dropped content for this exact reason (GH #2174). `GET /api/skills/{id}` keeps returning content by default — installed web and desktop builds call it for the skill editor and cannot be retrofitted with a query parameter. The CLI asks for metadata on both and takes `--with-content` when the bodies are actually wanted. The file listing gains a SIZE column, which is what makes an oversized skill diagnosable without downloading it. Co-authored-by: multica-agent <[email protected]> * MUL-6639: fix(cli): fail skill transfers on stall, not elapsed time `http.Client.Timeout` is a wall clock on the whole request, body included, so it punishes exactly the transfer that is working: a 599KB skill arriving steadily over a slow link was cut off at 30s mid-body, while a genuinely dead connection was held open for the same 30s (GH #7498). Raising the constant moves the cliff without removing it — the honest test is not "has this taken too long?" but "is it still making progress?". internal/cli/stall.go adds a client that asks the second question: bounded connect, TLS and response-header phases, and a response body that fails after 15s with no new bytes. The wall clock stays as a loose 10-minute backstop — demoted, not deleted, because dropping it to 0 would have left a stalled request upload with no deadline at all. Failures now say which thing went wrong. A new KindNetworkStalled reports "no data received for 15s after N bytes" rather than a timeout, because only one of those is fixed by waiting longer. Body-read errors are classified at all now: wrapTransport only ever saw errors from http.Client.Do, which returns once the headers arrive, so the failure #7498 actually reports reached users as a raw "context deadline exceeded ... while reading body" out of the JSON decoder. MULTICA_HTTP_STALL_TIMEOUT sets the no-progress budget. An explicitly set MULTICA_HTTP_TIMEOUT still applies on this path, keeping its plain meaning — the longest you will wait for a server — instead of silently reverting to a total-elapsed limit. Adoption starts with the skill commands, where the failure was reported; the mechanism itself is not skill-specific. Graduation is one edit: point NewAPIClient at NewStallAwareHTTPClient, fold StallAwareContext into APIContext, drop newSkillAPIClient. A permanent second client would leave the CLI with two timeout personalities, which is worse than either alone. Co-authored-by: multica-agent <[email protected]> * MUL-6639: fix(skills): hash raw UTF-8, keep the no-include default at content Review found two problems with the previous two commits. **The file hash was not over the file's bytes.** `sha256(content::bytea)` runs the bytea *input* parser over the text rather than taking its bytes, so it reads backslash escapes: a file containing `\x41` hashed as the single byte `A`, disagreeing silently with the Go-side hash of the SKILL.md body. Worse than the mismatch, and not caught earlier because the first check used accented text with no backslash in it: `content::bytea` *errors* on a bare backslash. `C:\Users\...`, a regex `\d+`, a LaTeX snippet — ordinary contents of a skill's supporting files — fail with "invalid input syntax for type bytea", making the listing 500 on the skills most likely to need it. `convert_to(content, 'UTF8')` takes the bytes, and matches Go exactly. **`/api/skills/{id}/files` must not change what a bare request returns.** The previous commit flipped its default to metadata on the grounds that the CLI is its only caller. That was the wrong reading of "only caller": the CLI is installed software too, and an older `skill files list --output json` reads `content`. It is the same argument that kept the detail endpoint's default — applied inconsistently to a client that upgrades separately from the server. Both endpoints now return content when `?include=` is absent. The shrink travels with the caller instead: the CLI sends `include=metadata` itself, so GH #7498 is fixed with no synchronized release, and the default can flip once clients that send `include=content` have aged in. The default is now uniform, so resolveSkillInclude no longer takes a per-endpoint parameter. Tests: TestSkillFileHashCoversRawUTF8Bytes covers `\x41`, invalid hex, bare backslashes, Unicode and empty bodies against the Go hash — verified to fail on the previous SQL. TestSkillEndpointsWithoutIncludeStillReturnContent pins the compatibility guarantee on both endpoints. Not addressed, and noted as follow-up: the metadata path still loads the full SKILL.md into Go, because loadSkillForUser needs the row for its tenant check. The HTTP response no longer grows with content; the database read still does. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
617d416029 |
MUL-6658 fix(cli): tell a rejected task token to stop, not to sign in again (#7522) (#7532)
* fix(cli): tell a revoked task token the task is over, not to sign in again The CLI's generic 401 copy reads "Your session has expired or you are not signed in. Run `multica login` to sign in again. On a self-hosted or non-OAuth setup, ask your administrator for valid credentials." That is correct advice for a person and actively harmful for an agent. A task's mat_ token is revoked the moment the task reaches a terminal state, so every call an orphaned agent makes after cancellation gets that message — and it reads as an instruction to go find a working credential. In GH #7522 one did exactly that: it read the daemon owner's profile PAT out of ~/.multica and kept working under the member's identity. A 401 on a task-scoped token now says the token was revoked because the task ended, and not to retry with another credential. The status code alone cannot distinguish the two cases, so HTTPError records which kind of credential the request carried; nothing else about the client is read, and exit classification is unchanged (still ExitAuth). This is defence in depth, not the fix for #7522 — cancellation not terminating the process tree is (PR #7531). It removes the motive. Co-authored-by: multica-agent <[email protected]> * fix(cli): base the task-token claim on the request, not the client Review found three problems with the first pass. TaskScoped read the client's own Token field, which answers "what does this client hold", not "what did the failing request send". DownloadFile deliberately sends no Authorization header for an absolute signed URL, so a 401 from object storage would have been reported to an agent as a finished task purely because the client happened to hold a mat_ token. It now reads resp.Request, which is the request that actually went out — also correct across a redirect that strips the header, which the caller's own request object would not be. newHTTPError goes back to a plain function. A test drives a real client through both paths instead of hand-building the error, and fails if the claim moves back to client state. The copy asserted a cause the CLI cannot verify: 401 also covers a malformed token, one sent to the wrong server, and one dropped by an unrelated cleanup. It now says the token was rejected and to stop, without naming a reason. "Stop, and do not fall back to a profile or member credential" is true in every one of those cases; "the task finished" is not. The kind is renamed to KindTaskTokenRejected to match. The Chinese copy used 任务 for an agent execution. Per the glossary, 任务 is the product entity a user files (an issue) and an agent execution stays lowercase `task`; the old wording said the user's issue was rejected. A test now fails if 任务 reappears in this message. Co-authored-by: multica-agent <[email protected]> * docs(cli): stop asserting the cause in the comments too The runtime copy no longer claims why a task token was rejected, but three comments still did: TaskTokenPrefix called a 401 "a normal end-of-task signal", TaskScoped described the token as "finished", and the wiring test quoted the old wording. A reader following the comments would have reintroduced exactly the claim this PR removed. They now say what the CLI can actually see — a task token was rejected, there is no sign-in that brings it back, and the reason is not visible from here. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d38da27ed4 |
MUL-5619: fix(cli): surface the server's 409 message instead of the generic conflict template (#6267)
* fix(cli): show the server's conflict message instead of the generic 409 template
Every 409 this API returns is a deterministic refusal that names its own fix
("a skill with this name already exists", "set parent_id (--parent) to <id>").
The CLI replaced all of them with a template that says the opposite — that the
state changed underneath you and you should re-fetch and retry. Agents took the
retry hint literally: GH #6264 reports 15+ identical retries over 10 minutes
followed by hours spent chasing an optimistic-concurrency theory that never
existed, and GH #5948 is a second user misdiagnosing the same way. MUL-4417 had
already written the useful message server-side; it just never reached anyone.
Route 409 through the same server-message extraction 400/422 already uses, so
roughly forty hand-written conflict messages across skills, agents, runtimes,
labels, projects and comments become visible by default. A body we cannot
recognize still falls back to the template, so this never dumps a raw response.
extractServerMessage now prefers prose over a bare identifier, because a few
endpoints put a stable code in "error" and the sentence in "message".
MUL-5619
Co-authored-by: multica-agent <[email protected]>
* fix(comments): stop telling a wrong --parent that it posted a top-level comment
The reply guard returns one message for two different mistakes. A resumed
session that carries a previous turn's --parent forward (GH #6264) did not ask
for a top-level comment, but is told it did — which sends it looking for a
new-thread opt-in (GH #5383) instead of correcting the parent it already passed.
Split the copy: name the rejected parent when one was supplied, and keep the
existing top-level wording for the parentless case. Both still point at the
trigger comment to use.
MUL-5619
Co-authored-by: multica-agent <[email protected]>
* fix(runtime): return 500, not a 409 echo, when the update store fails
InitiateUpdate answered every UpdateStore.Create failure with a 409 carrying
err.Error(). The in-memory store only ever returns errUpdateInProgress, so this
looked safe — but the Redis store also wraps infrastructure failures as
"reserve active update: <dial error>" and "persist update request: <error>".
Surfacing 409 bodies in the CLI turns that into a user-visible leak of internal
addresses, and labels an outage as a conflict the caller could fix by retrying.
Classify instead: errUpdateInProgress keeps its 409 and its actionable message,
everything else is logged and answered with a 500 and fixed copy.
Also pins the prose-over-machine-code preference for validation bodies, which
the shared extractor applies to 400/422 as well as 409. Only the issue-table
endpoints are shaped that way and none is reachable from the CLI today, but the
change is intentional and should fail loudly if reverted.
MUL-5619
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
9ff801f926 |
docs(cli): error-message conventions + sign-in copy (PR3, MUL-3104) (#3900)
* docs(cli): add Error Messages conventions + refine sign-in copy (PR3) Final pass of the CLI error-message work (MUL-3104). - CLI_AND_DAEMON.md: new "Error Messages" section documenting the user-facing contract — friendly single-line messages, server validation passthrough, English default with automatic Chinese on a zh locale, the tiered exit codes (0/1/2/3/4/5), --debug / MULTICA_DEBUG for the full chain, and MULTICA_HTTP_TIMEOUT. - cmd_auth.go: clarify three high-frequency sign-in errors so the message states what failed and the next step — local login-callback server start (hints at port/firewall), access-token creation, and token verification (suggests retrying `multica login` and checking the token is valid/not expired). All keep %w so exit-code tiering and --debug detail are preserved. cmd_id_resolver.go is left as-is — its not-found / ambiguous-prefix messages already point at `list --full-id` and need no change. The user-facing FormatError layer is unchanged, so its existing PR1/PR2 test coverage still applies; no test asserted the old verb strings. Refs MUL-3104. PR3 of 3 (final). Co-authored-by: multica-agent <[email protected]> * fix(cli): make login failure guidance visible via typed user-message wrapper Addresses 张大彪's PR3 review: the refined sign-in copy was wrapped with %w, so FormatError returned the centralized *HTTPError/*NetworkError copy and the new guidance only appeared under --debug. - Add cli.UserMessageError + cli.WithUserMessage: a typed wrapper carrying a user-facing message that FormatError surfaces by default, recognized before the network/http branches. Unwrap() is preserved, so ExitCodeFor still classifies by the underlying typed error and --debug still prints the full original chain. - cmd_auth.go: wrap the OAuth access-token-creation and PAT-verification failures with WithUserMessage (OAuth copy no longer mentions a passed token, since that flow has none), and move the token-specific 'valid / not expired' hint to the real Enter your personal access token: verification site (was the generic 'invalid token: %w'). - Focused tests: under a wrapped *HTTPError(401) the default FormatError shows the login hint, ExitCodeFor returns ExitAuth, and --debug retains the raw chain; a wrapped *NetworkError still classifies as ExitNetwork. - CLI_AND_DAEMON.md: narrow 'every error' to command errors returned to the top-level handler, noting commands like setup's fast /health probe bypass it. Refs MUL-3104, PR #3900. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
b83b41ff44 |
feat(cli): per-status error copy with actionable hints (PR2, MUL-3104) (#3897)
* feat(cli): refine per-status error copy with actionable hints (PR2) Builds on PR1's translation layer. Each HTTP-status message now carries an actionable next step, in both English and Chinese: - 401: run `multica login`; plus a self-hosted / non-OAuth fallback telling the user to ask their administrator for valid credentials - 403: check the workspace / ask an admin to grant access - 404: check the ID or run the matching `list` command - 409: re-fetch the latest state and retry - 422: check values / run with --help - 429: wait and retry; reduce call frequency if it persists - 5xx: retry, contact support, and re-run with --debug for the raw response Also adds ErrorKind.String() (stable snake_case identifiers) and uses it in --debug output instead of the raw int, and clears the pre-existing gofmt dirt Eve flagged in cmd_config.go, cmd_version.go, and help.go. Tests: TestErrorKindString (all kinds + uniqueness + out-of-range fallback) and TestFormatErrorActionableHints (locks the per-status hints in EN and ZH). Refs MUL-3104. PR2 of 3. Co-authored-by: multica-agent <[email protected]> * test(cli): cover validation (400/422) actionable hint TestFormatErrorActionableHints omitted KindValidation, so deleting the 400/422 hint would have gone unnoticed. Add 400 and 422 cases (no server message, so the generic validation copy is used) asserting EN contains --help / expected format and ZH contains --help / 格式 / 参数. Refs MUL-3104, PR #3897. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
28de8b8bde |
feat(cli): central error translation layer (PR1, MUL-3104) (#3892)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * style(cli): gofmt cmd_attachment.go to unblock backend CI Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |