mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
8ade96e671bca7d84fd2d764ffca305e2fdc358d
237
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a4e1ea7140 |
MUL-6581: expose globally versioned Plugin Public API (#7446)
* feat(plugins): expose globally versioned public API Co-authored-by: multica-agent <[email protected]> * fix(plugins): proxy same-origin public API Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
8d2da6b8db |
MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step (#7325)
* MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step Local development had six entry points, thirty-four make targets and no way to see or delete what they created. `make up` / `status` / `list` / `down` / `destroy` / `gc` / `env-exec` replace that with one set of verbs, shared by humans and agents, with `C=api,web,daemon,desktop` selecting components. Three defects made the old flow untrustworthy rather than merely verbose. 1. The database the tooling created was not the one the application reached. `ensure-postgres.sh` creates through `docker exec`; when a native PostgreSQL owns 5432 the container never binds the host port, so the create lands in a server the backend never talks to. It printed "✓ PostgreSQL ready" and the next step died with `SQLSTATE 3D000`. On the machine this was written on the two servers had already diverged: 94 same-named databases, 69 more in the container with no counterpart. `up` now creates through DATABASE_URL and treats a successful `migrate up` — which pings that same string — as the only proof; failure diagnoses the port owner instead of reporting a missing database. 2. Identity was computed, not allocated. Ports, database names and CLI profiles all came from `cksum($PWD) % 1000`, and a collision was silent: the loser fails to bind while the winner keeps answering 200. Allocation now happens under a lock, probing from the same path hash so a checkout keeps its familiar numbers, and requires the slot to be both unregistered and free of a live listener. The manifest in ~/.multica/dev/envs/<name>/ is what every later command reads. 3. Creating an environment left no record of how to destroy it, so there was no destroy — only archaeology. `down` stops processes and keeps the data; `destroy` consumes the manifest: processes, database, profile, slot. `list` makes leaks visible and `gc` collects environments whose checkout is gone or whose TTL passed. /health now reports pid, commit and started_at. A 200 alone proves something is listening, which is exactly what let a failed restart keep serving the previous build; `up` compares started_at against its own launch and refuses an answer that predates it. Two constraints are enforced rather than documented: the daemon is started from a built server/bin/multica, never `go run` (the daemon re-execs its own path for every task, and `go run` deletes that binary when the launcher exits), and `C=daemon` under a .multica/daemon_task_context.json marker stops with that explanation up front instead of spending a login on a refusal. scripts/dev-env.test.sh covers the registry verbs against a throwaway MULTICA_DEV_HOME with no services running, and pins the regression that made `down` exit 1 after reporting success: on bash 3.2 a command substitution whose function ends in a failing command aborts the script under `set -e`, and "no process is listening" is that function's normal answer. `make dev` is unchanged and still runs in the foreground. Co-authored-by: multica-agent <[email protected]> * fix(dev): close local environment lifecycle gaps Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bedad9e222 |
MUL-6485: isolate hosted plugin surfaces (#7329)
* feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden surface validation and error reporting * fix(migrations): renumber plugin package migrations * fix(plugins): validate classic surfaces and reset failures * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * fix(plugins): make surface termination event-driven Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * fix(plugins): make surface termination event-driven Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5a80f802d3 |
MUL-6558 feat(server): make task queue expiry configurable via MULTICA_TASK_QUEUED_TTL (#7418)
Lets self-hosted deployments raise the queued-task expiry window above the built-in 2h default, so low-concurrency runtimes stop losing legitimately-pending work to queued_expired. Default behavior is unchanged. Covers .env.example, Compose, Helm values/ConfigMap, and the behavior docs in all four locales. Co-authored-by: Martin Montes <[email protected]> |
||
|
|
53e15b35c6 |
MUL-6502: fix startup recovery during PostgreSQL outages (#7372)
* fix(startup): retry transient database outages Co-authored-by: multica-agent <[email protected]> * fix(startup): address database retry review Co-authored-by: multica-agent <[email protected]> * test(startup): address retry review nits Co-authored-by: multica-agent <[email protected]> * fix(startup): preserve native pgx connect timeout Co-authored-by: multica-agent <[email protected]> * fix(startup): apply timeout fallback to services Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5e2d349ec4 |
MUL-6426: refactor(dingtalk): safely retire group routing and enhance discovery (#7241)
Retire mutable DingTalk group routing: inbound messages always target the installation's configured Agent, and observed groups become read-only inventory in dedicated tables with persisted activity counters. Migrations 382-389 clean stale cross-Agent bindings, create and backfill dingtalk_group_presence and dingtalk_bot_identity, and install a mirror trigger so writes from an older process during a rollback window are not lost. The legacy dingtalk_group_route table and its indexes are retained for expand/contract; a later release drops the trigger and table together. Closes #7239 |
||
|
|
8febf40970 |
MUL-6525: docs: document GitHub App Contents permission for PR snapshots (#7369)
Add Contents: Read-only to the self-hosting GitHub App permission table in all four locales, note that it cannot be skipped along with the CI permissions, and list it in the No CI status troubleshooting entry. Co-authored-by: Sebastian San Blas <[email protected]> |
||
|
|
a3bd38da6a |
fix(cli): redact autopilot webhook credentials (#7365)
* fix(cli): redact autopilot webhook credentials * fix(cli): preserve webhook token presence metadata |
||
|
|
814ee0cbf7 |
docs: remove six finished implementation records (#7258)
* docs: remove the two implementation docs under docs/ The database index audit and the Telegram channel review were one-off implementation records for MUL-6108 and the Telegram contribution; both are finished work and neither is referenced by the product docs. Drops the now-dangling review pointer from the Telegram handoff notes. docs/assets/ stays — the READMEs load the logos from it. Co-authored-by: multica-agent <[email protected]> * docs: remove the Telegram integration handoff notes HANDOFF-telegram.md was the implementation record for the Telegram channel contribution and is referenced by nothing. Operator-facing setup lives in the localized Telegram guides under apps/docs/content/docs/. Co-authored-by: multica-agent <[email protected]> * docs: remove three finished implementation leftovers - apps/mobile/docs/markdown-renderer-research.md: 2026-05 renderer selection research and incident log. Its conclusions already live in markdown-rendering-adr.md and in the MD_FONT/MD_LINE comments; the HIG calibration source is folded into markdown-style.ts so the numbers keep their rationale. - packages/views/locales/glossary.md: a redirect stub. The content moved to the docs-site conventions pages, which now say the file is gone instead of claiming a stub still points at them. - scripts/audit-redundant-indexes.sql: the MUL-6108 one-off audit query, orphaned once its report was deleted — no caller in Makefile, check.sh or CI. All pointers into the deleted files are updated; nothing dangles. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3268d01172 |
fix(daemon): move the in_progress start write into workflow step 3 (MUL-6417 follow-up 2) (#7295)
* fix(daemon): move the in_progress start write into workflow step 3; define ancillary by output (MUL-6417 follow-up 2) Field evidence from MUL-6460: a run that verifiably received the #7283 brief (its workdir CLAUDE.md carries the two-moment rule) walked steps 1→2→3 for over an hour and never issued a single status write. Two compounding causes, both in the prompt: - Placement. The start-moment rule lived in the status block BELOW the numbered steps. At the moment its condition triggers the model is executing the list, and a rule outside the list does not fire — the pre-MUL-6417 opening write was reliable precisely because it was an explicit step. The conditional write now opens step 3 itself, at the read→work boundary. - The research example. "A todo issue you were asked to research stays todo" was written for consult pull-ins, but the MUL-6460 turn's work WAS research toward its own issue's ask — the example pattern-matched the exact case it was never meant to cover. Ancillary is now defined by output (the turn produced none of the issue's own deliverable), and research/design explicitly count as work when they are the ask. Guard tests pin the step-3 placement and the research-counts-as-work clause, and ban the retired example. Skill, source maps, and user docs updated to match. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): state the status rule as continuous, anchored at the moments that fire (MUL-6417) Bohan's review of #7295: 'at the end of your work, write the state it reached' still read as restricting writes to fixed checkpoints. The lead now states the invariant — keep status true at every point in the turn, write whenever the work changes it, mid-turn included (a blocker is recorded when hit) — and the end of the turn becomes a verification ('do not exit with a stale value') instead of the write moment. The anchors stay. A free-floating 'write when necessary' with no attachment points is exactly the shape that produced zero writes in the MUL-6460 incident: permission without a trigger moment does not fire. Step 3 keeps the start write; the exit check keeps the guarantee that a turn never ends with a lying board. Co-authored-by: multica-agent <[email protected]> * fix(daemon): drop activity-word skip-lists; ancillary is decided by output alone (MUL-6417) Elon's #7295 review: 'review' still sat in step 3's skip clause and the no-write bullet's example list, replaying the research incident's exact failure class on an issue whose ask IS reviewing a PR. Activity words carry no deliverable information in either direction, so both lists are gone: step 3 names forms of work (code, research, a design, a plan, or a review when reviewing is the ask), the no-write case is stated purely by output (the turn produced none of THIS issue's deliverable), and the rule now says so explicitly — the kind of activity never decides, only whether the output is part of what the issue asks for. Guards pin the review-as-work clause and the never-decides sentence, and ban both retired skip-lists. Skill, source map, and docs synced. Measured brief delta vs main (claude, production-length UUID): ordinary 13,362 → 13,971; leader 14,467 → 15,076 (+609 each, cached prefix); per-turn messages unchanged. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): adopt J's review — one criterion, anchors in steps, category-scoped skip (MUL-6417) J's review of #7295, all four points taken: - The positive form-list that replaced the banned skip-list was the same failure shape read as exhaustive (a triage or reproduce-the-bug turn finds itself in no list). Both directions of activity-word lists are gone; the never-decides sentence moves INTO step 3 — the position that fires — instead of the status block, the position this PR itself proved dead. - The exit-side check is anchored inside step 5 ('Before exiting, ...'), making the PR's own placement argument self-consistent. - The step-3 skip is scoped to the in_progress CATEGORY, so a custom status like Planning already counts as recorded once MUL-6460 puts the status catalog in front of agents — no rewording needed then. - Tense: 'if any part of what this turn will produce ...' — step 3 is a prediction; mispredictions self-heal under the continuous rule. Plus J's meta-point on guards: presence-pins cannot tell where a sentence lives, and both field incidents were correct sentences in dead positions — the anchors are now pinned positionally (must appear inside the step-3 / step-5 lines), and the retired positive list joins the banned set. Kept the questions/discussion/acknowledgements clause (J's conservative option): acknowledgements are not covered by 'answered a question' and the sentence is the MUL-6300-era invariant other tests pin. Measured (claude, production-length UUID): ordinary brief 13,685, leader 14,790 — +243 vs main, down from +609 at the previous commit. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
cc21d19353 |
MUL-6463: confirm the promotion that starts an agent run (#7293)
* feat(issues): confirm the promotion that starts an agent run (MUL-6463) Assigning an agent to an issue opens a confirmation with a handoff note and "Don't start yet". Promoting a parked issue that ALREADY has an agent owner starts a run just as directly — the status change alone is what fires RunSourceStatus — but applied silently from a one-click picker, with no way to tell the agent anything before it woke up. The dialog now covers both writes. `RunConfirmModal` gains a `promote` mode that submits the status change (plus the same optional handoff note, or suppress_run for "Don't start yet") instead of an assignee change; the owner is already on the issue, so the promotion payload carries the status and nothing else. The gate is the run predicate, not a status allow-list: parked (backlog CATEGORY) + agent/squad owner + a target outside backlog/done/cancelled. So built-in `todo` and every custom Todo-category status behave identically, which is what the report asked for, and so do the other promotion targets that start a run the same way. Drag-and-drop and the batch toolbar keep applying directly — the existing split (MUL-4155), not a new one. Also fixes the assign gate next to it, which still compared the raw status KEY to "backlog": an issue parked in a CUSTOM backlog-category status popped a confirm promising a run the server would never start. Both gates now resolve the category the way the server does (issuestatus.Effective), preferring the payload's own status_category and falling back to the workspace catalog. Tests: promotion routing for built-in todo / custom todo / custom backlog origin / non-todo target, the five no-run status changes that stay direct, the custom-backlog assign regression, and the modal's promote payloads (note, suppress_run, no assignee fields) plus its status naming. Verified each fails without the change. Docs: the promotion dialog in assigning-issues (en/zh/ja/ko). Co-authored-by: multica-agent <[email protected]> * fix(issues): bold the status name in the promotion confirm "Move this issue to Rework." left the status looking like an ordinary word — worse for a custom status an admin named something like Later or Blocked-ish. The headline already had a fence-and-bold mechanism for the assignee name, so generalize it to bold every fenced span and fence the status too. CJK copy drops the 「」 it used for the same job: bold is now the one mechanism across all four locales. Co-authored-by: multica-agent <[email protected]> * fix(issues): close the three run-confirm gaps from review (MUL-6463) 1. Server and client disagreed about moving WITHIN the backlog category. `WillEnqueueRun`'s status branch only excluded `done`/`cancelled`, so `backlog` → a custom Backlog-category status (`Later`) changed the key, normalized both sides to `backlog`, and still enqueued a run — while the client, which reads categories, applied it silently. Before custom statuses the two conditions were the same; now leaving the category is its own requirement. Regression test covers both halves: no run inside the category, still exactly one on the way out. 2. The table's inline pickers bypassed the gate entirely — `meta.updateIssue` went straight to the surface action, so promoting an agent-owned backlog row from the table started a run with no confirmation. `meta.updateIssue` now takes the issue (the gate needs its category and owner) and routes through the same intent. 3. "Unresolvable category" was collapsing into a guess. `catalog.categoryOf` answers `todo` for a custom key it has not loaded, which made an unknown status read as not-parked and skipped the promote dialog. Category resolution is now explicitly three-state — carried, built-in, catalog, else null — and both writes that can start a run confirm on null. The gate itself moves to a pure `runConfirmIntent` next to a canonical matrix test, so the issue detail, the context menu and the table all route on one answer instead of three copies; the hook/table suites keep only their wiring. Docs nit from the same review: the new CJK paragraphs said issue / イシュー / 이슈 where the glossary requires 任务 / タスク / 태스크, and named statuses in capitalized prose instead of backticked keys. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a1d160ee0d |
MUL-6468: fix(docs): replace dead dimcode.ai link with official Dim CLI docs (#7294)
* fix(docs): replace dead dimcode.ai link with dimagent.cn/docs/cli The install-agent-runtime guide linked Dim to https://dimcode.ai which does not resolve. Replace with the official CLI docs at https://dimagent.cn/docs/cli across all 4 language versions (en/zh/ja/ko). * fix(docs): use English Dim CLI links for non-Chinese docs Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
49ed7f1151 |
MUL-6459: docs: document custom issue statuses (#7284)
* docs: document custom issue statuses (MUL-6459) The docs still described the seven statuses as a fixed set. Add a Custom statuses section to the Issues page in all four locales covering the model users actually need: the seven categories are fixed and each one IS its built-in status key, a custom status declares a category at creation and inherits its platform behavior in full, and that category is immutable afterwards. Also state the consequences that are easy to get wrong: board columns are categories so a new status never adds a column, built-in statuses are locked, and archiving retires a status for future assignment without touching the issues already on it. Two rules elsewhere were written as status rules but are category rules: the backlog gate on assignment and the auto-archive of run-failure inbox notifications. Both now say so. Co-authored-by: multica-agent <[email protected]> * docs: correct the run-start trigger, category scope, and key wording Review follow-up on the custom statuses section (MUL-6459). The run-start trigger was attributed to entering `todo`. The only status-change trigger is leaving the `backlog` category for anything other than `done` / `cancelled` (service/issue_trigger.go), so the rule now sits on the `backlog` row where it belongs. The claim that every status named in the docs means its category was too broad, and contradicted the system-changes callout on the same page: the failed-run rollback writes the literal `todo` key (service/task.go) and a merged PR writes the literal `done` key (handler/github.go). Scope it to statuses that TRIGGER behavior, and say the write direction is literal. Key derivation was described as always coming from the name. The API takes an explicit key and only derives one when it is omitted (handler/issue_status.go), and the key never changes on rename. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4ff37edf91 |
fix(daemon): record in_progress when work on the issue's ask starts, not only at turn end (MUL-6417 follow-up) (#7283)
Post-merge report from Bohan: a todo issue assigned to an agent sat in todo for the whole first work turn — half an hour of active work the board attributed to nobody. The end-of-turn-only rule conflated when the agent can JUDGE with when the FACT changes: a turn that advances the issue's own ask makes "being worked" true the moment it begins. The activity indicator shows the run, but columns, filters, and sorting read status — the indicator alone is not the surface people watch. The status rule now has two write moments, same fact anchor: - start: the moment the turn is known to advance the issue's own ask → in_progress (only if it differs — rework on an in_review issue flips it back the same way); - end: the state the work reached (in_review / in_progress-continuing / blocked). An ancillary turn (research, answers, review, discussion) still writes nothing at either moment, so the research-stays-todo case and the concurrent-convergence property from the original MUL-6417 change are unchanged. This is the pre-MUL-6417 timing with the fact anchor's conditionality — not a revert to the unconditional lifecycle flip. Docs, skill reference, and source maps updated to the two-moment rule; guard tests pin the start moment and ban the end-only phrasing. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e1be1625cb |
MUL-5959: feat(agent): add Dim (DimCode) ACP runtime (#6675)
* feat(agent): add Dim (DimCode) ACP runtime Add Dim (dimcode, the `dim` CLI) as a first-party agent runtime, driven over the ACP (Agent Client Protocol) transport via `dim acp`. Key integration points: - New dim backend (pkg/agent/dim.go): spawns `dim acp`, performs the ACP initialize/session/new handshake, and raises the runtime's hardcoded read-only permission preset to full-access (plus agent mode) via session/set_config_option before the first prompt — without it every file write is denied by a capability rule. Model override uses session/set_model; the model catalog is read from session/new. - Session resume is intentionally skipped: Dim binds sessions to the creating process, so a later process's session/load is rejected with "held by another process" even after a clean session/close. The backend always starts a fresh session and reports ResumeRejected so the daemon classifies the run correctly. A best-effort session/close keeps Dim's own session list free of orphaned entries. - Registration: SupportedTypes whitelist, New() factory, launch header, daemon probe (MULTICA_DIM_PATH / MULTICA_DIM_MODEL), default agent command name, protocol_family CHECK migration (255), model discovery, MCP config support, metrics label, and runtime display. - Docs/UI: provider logo, landing i18n (en/zh/ja/ko), providers and install-agent-runtime docs, README, CLI_AND_DAEMON, SELF_HOSTING. - Tests: dim backend unit tests (fresh session + resume behavior), shared ACP deliverable case, logo and MCP-support tests. * feat(views): use official DimCode app icon for Dim provider logo Replace the placeholder inline SVG with the official DimCode desktop client app icon (from /opt/DimAgent/resources/build/icon.png, resized to 128px), imported as a static asset the same way the Qwen Code mark is handled. * test(agent): add real dim ACP smoke tests (agentintegration) Two gated tests behind MULTICA_RUN_REAL_AGENT_SMOKE=1: - TestDimRealACPSmoke: full end-to-end against real `dim acp` — initialize, session/new, set_config_option (permission/mode), prompt, and assert completed output. - TestDimRealResumeRejected: pass a fake ResumeSessionID, assert the backend starts a fresh session (no session/load), and reports ResumeRejected=true so the daemon classifies the run correctly. Verified against dimcode 0.3.2 and 0.3.8. * fix: renumber dim migration 265 → 271 (upstream added 265-270) * fix: renumber dim migration to 272 (upstream added 271) * fix(agent): dim cross-run resume via session/load + review fixes Address review feedback on the Dim ACP runtime: - #1 Deliver runtime brief to Dim: add dim to the AGENTS.md provider list (verified dim 0.3.8+ reads AGENTS.md from the session cwd). - #2 Cross-run session continuity: dim 0.3.10+ releases its per-process session lock ~5s after the owning process exits, so resume now goes through the standard ACP session/load (same as traecli/kiro/grok) instead of always starting fresh. A loaded session retains its permission/mode, so set_config_option runs only on fresh sessions. Add a two-run regression (TestDimRealCrossRunResume) proving run B recalls context established only in run A. - #3 Process lifecycle races: the deferred cleanup now cancels the run context before cmd.Wait (a child ignoring stdin EOF no longer hangs Result delivery) with a bounded force-kill fallback; the success path waits for the final prompt notification with a grace window instead of a non-blocking read that could miss the last message. - #4 Isolate model discovery by executable path (discoveryCacheKey). - #6 Real smoke test now writes a sentinel file to prove full-access is effective; a best-effort session/close is sent before teardown. - #7 gofmt. * test(agent): add dim process-lifecycle regressions (review #3) - TestDimCleanupKillsHangingChild: a child that ignores stdin EOF and SIGTERM after an early failure is force-killed within dimProcessWaitTimeout, so Result still closes. - TestDimPromptMissingNotificationStillCompletes: when session/prompt returns without stopReason (onPromptDone never fires), the bounded final-notification wait falls through and Result completes instead of hanging. * fix(migrations): renumber dim runtime_profile migration 272 → 273 (upstream added 272) * fix(agent): review round 2 — process tree, quiescence, fail-closed, version check Address all remaining blockers from the second review: - #1 Require dim >= 0.3.10 (version check at initialize); bounded retry on session/load when the lock is not yet released instead of silently starting fresh; integration test no longer sleeps before resume. - #2 Process-tree cleanup: configureProcessGroup + signalProcessGroup + waitProcessGroupGone; cmd.Cancel returns nil so we own all signalling; regression asserts the descendant PID is gone. - #3 Notification quiescence: onActivity + waitForACPNotificationQuiescence (same as hermes); Dim equivalent of the late-final-notification test. - #4 Permission fail-closed: set_config_option runs on BOTH fresh and resumed sessions; on config failure, session/close is sent before returning so a partially configured session is not resumed; regression. - #5 Migration renumbered 273 → 274 (upstream added 273). - #6 AGENTS.md mapping regression, two-executable cache isolation test, session/close unit assertion, smoke test now executes a command. - #7 README.md + CLI_AND_DAEMON.md updated with 22 CLIs including Dim. * chore: remove temporary review actions doc (not for PR) * fix(migrations): renumber dim migration 274 → 310 (upstream added 274-309) * review: fix retry break scope, dedup isACPSessionNotFound comment, update stale notes * fix(agent): pass *exec.Cmd to signalProcessGroup/waitProcessGroupGone (upstream signature change) * fix(agent): close session on set_model failure + resume regression test Audit-found gaps from self-review: - set_model failure now sends session/close before returning (same as set_config_option failure) — reviewer #4 said 'permission, mode, or model'. - Add TestDimConfigFailThenResumeReestablishes: run A config fails → session/close sent → run B resumes and re-applies set_config_option (fail-closed), completing successfully. - Fix two stale comments that contradicted the code (config block now runs on both fresh and resumed sessions). * fix(agent): add WaitDelay, use labeled break in retry loop Self-audit improvements: - Add cmd.WaitDelay=10s for consistency with claude.go (hard backstop if a process somehow survives SIGKILL). - Use labeled break (break loadRetry) so runCtx cancellation during the retry delay exits the for loop directly, not just the select. - Update PR description: migration 273→310, set_config_option now re-applied on both fresh and resumed sessions. * fix(migrations): renumber dim migration 310 → 313 (upstream added 310-312) * fix(migrations): remove stale 313 dim migration (replaced by 314 after upstream added dsh at 313) * fix: DSH compatibility + thinking levels + reviewer round 3 Systematic fix of all 39 items from the compatibility gap analysis: Rebase + migration: - Rebase to latest upstream/main (resolves all DSH conflicts) - Migration 314_runtime_profile_add_dim (whitelist includes both dsh + dim) - Remove stale 313 dim migration DSH + Dim coexistence (provider lists, counts, docs): - SupportedTypes, config.go, metrics labels: both dsh + dim - i18n (4 files): count 23, lists include DSH + Dim + Oh-My-Pi - README.md/README.zh.md: count 23 - CLI_AND_DAEMON.md: 23-row table - environment-variables.mdx (4 langs): MULTICA_DIM_PATH/MODEL callout - display.ts, mcp-support.ts, types/agent.ts, provider-logo.tsx: both dsh+dim Thinking levels (was MISSING — dim supports thought_level): - dim.go: call applyACPEffortOption after set_model - thinking.go: add dim to acpCatalogThinkingProviders - dim.go: retain sessionResult for effort option Reviewer round 3: - Version check fail-closed: empty/malformed → reject (not allow) - Cache test through ListModels call site with fake executables - set_model failure regression test - Test companions: sidecar_manifest_test, runtime_config_test add dim Code quality: - agent.go: fix unreachable duplicate return (rebase artifact) - dim.go: labeled break in retry loop, WaitDelay, session/close on set_model fail * fix: deliverable test fake version + provider-logo rebase artifact * fix(migrations): renumber dim migration 314 → 315 (upstream added 314_workspace_mcp_config) * fix(migrations): renumber dim migration 315 → 319 (upstream added 315-318) * fix: Windows process tree, MinVersions, retry tests, migration 327 Address all 5 remaining blockers from review round 4: 1. Windows process-tree ownership: use startOwnedProcessTree instead of cmd.Start(), releaseProcessGroup in cleanup defer. This attaches the child to a Job Object on Windows (no-op on Unix), ensuring descendants are captured and terminated. 2. Migration renumbered 319 → 327 (upstream added 319-326). 3. Add dim: 0.3.10 to MinVersions in version.go so the daemon registers old Dim binaries as offline and refuses triggers (defense in depth on top of the ACP agentInfo.version check in dim.go). 4. Session-lock retry contract tests: success after bounded retries, exhaustion without falling through to session/new. Fake script supports DIM_LOAD_HELD_N (held for N calls then succeed) and DIM_LOAD_HELD_ALWAYS (always held). 5. README.md:206 20→23 runtimes. PR description updated. * test: add missing reviewer-requested regressions (round 4) - Windows Job Object regression: dim_windows_test.go proves the Dim backend captures and terminates descendants via startOwnedProcessTree (Windows-only build tag; companion to TestStartOwnedProcessTree). - MinVersions registration: add dim test cases to TestCheckMinVersion (0.3.10 ok, 0.3.9 rejected, invalid rejected). - Retry cancellation: TestDimSessionLoadRetryCancelled cancels the context during the retry delay, asserts no session/new fallback. * fix: sync upstream Command signature changes + restore dim additions Upstream changed ListModels/discoverXxxModels/detectCLIVersion to accept Command (struct with Path+Prefix) instead of string. The previous rebase kept our old-signature versions, causing widespread compile failures. Restored all affected files from upstream, then re-applied dim-specific additions: - agent.go: dim in SupportedTypes, New(), launchHeaders - hermes.go: isACPHeldByProcess + -32002 in isACPSessionNotFound - thinking.go: dim in acpCatalogThinkingProviders - models.go: dim case in ListModels + discoverDimModels - version.go: dim in MinVersions - All dim test files preserved (dim_test.go, dim_integration_test.go, dim_windows_test.go, models_test.go dim cache test) * fix: rebase to latest main, migration 327→341, resolve DetectVersion conflict * fix: remove unused os/exec import in dim_windows_test.go * fix(migrations): renumber dim migration 341 → 342 (upstream added 341) * fix: use Command.exec instead of exec.CommandContext (upstream GH #7046) Upstream's TestOnlyLaunchGoSpawnsRuntimeProcesses requires all backends to build processes through Command.exec (launch.go), so a custom runtime's fixed_args are carried into the subprocess. Dim was the only backend still using exec.CommandContext directly. * fix: launch prefix policy + remove empty test files + deterministic retry tests Address all 4 findings from review round 5: 1. Add "dim": dimBlockedArgs to launchPrefixBlockedArgs so protocol-breaking flags (--help/--auth-setup/--remote) are filtered from fixed_args. 2. Remove 77 zero-byte *_test.go files accidentally added at repo root. 3. Make retry tests deterministic: TestDimSessionLoadRetryCancelled waits for the first session/load request before cancelling (no fixed sleep); TestDimSessionLoadRetryExhausted asserts exactly 4 load attempts. 4. PR description will be updated separately. * fix: rebase to latest main, migration 342→343 (upstream added mcode at 342) * chore: trigger CI * chore: refresh PR * fix: restore mcode that was lost during merge conflict resolution Merge took 'ours' side which predated mcode. Restore mcode in: - SupportedTypes, New() factory, launchHeaders - agent_supported_types_test.go want map - metrics/labels.go - config.go defaultAgentCommandNames - migration 343 up/down whitelists * fix: restore mcode probe in agents_probe.go + agent-cli-command-names.txt * fix: restore all remaining mcode references lost during merge Files fixed: - README.md: mcode row in runtimes table - mcp-support.test.ts: mcode assertion - config.go: mcode in Agents comment + error message - runtime_config.go: mcode in AGENTS.md case - version.go: mcode in MinVersions - version_test.go: mcode test cases Verified: no file has fewer mcode references than upstream. * fix: restore mcode in environment-variables docs + CLI_AND_DAEMON ACP list - environment-variables.{mdx,ja,ko,zh}: restored 'MiniMax Code' in the QwenPaw model-variable sentence - CLI_AND_DAEMON.md: added MiniMax Code to ACP-family list + loadSession fallback description Verified: every changed file now has >= mcode references vs upstream. * fix(migrations): renumber dim migration 343 → 344 (upstream added 343) * fix(migrations): renumber dim migration 344 → 348 (upstream added 344-347 plugin migrations) * fix(migrations): update down migration comment 344 → 348 * chore: retrigger CI (flaky TestOpenclawDiscoveryCacheConcurrentPreparations — upstream test, passes locally x5) * fix(migrations): renumber dim migration 348 → 352 (upstream added 348-351) * fix: Dim launch-prefix regression test + version.go comment repair (review #6) - Add TestDimLaunchPrefixFiltersBlockedFlags: proves allowed prefix reaches command before acp, and --help/--auth-setup/--remote/-h are stripped from Dim's launch prefix. - Add dim to TestLaunchPrefixReachesACPFamilies family list. - Repair version.go: dim's cross-run session/load comment was appended to mcode entry during a rebase conflict; restore two independent accurate comments. * fix(migrations): update down migration comment 348 → 352 * chore: retrigger CI (backend-tests stuck 50+ min) * fix(migrations): renumber dim migration 352 → 362 (upstream added 352-361) * fix: use logAgentCommand for dim argv logging (upstream redaction requirement) Upstream's TestOnlyLaunchGoLogsAgentCommandArgs enforces that all runtime argv logging goes through Config.logAgentCommand so sensitive args are redacted. dim.go was still using Logger.Info directly. * fix(migrations): renumber dim migration to 370 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
658b0b7d9f |
MUL-6406: fix(auth): fail fast in production on insecure JWT secret defaults (#7212)
* fix(auth): fail fast in production on insecure JWT secret defaults * chore(deploy): require a strong JWT_SECRET in compose and docs * fix(ci): make selfhost config test pass with required JWT_SECRET The compose file's JWT_SECRET:? error message contained a colon-space sequence that YAML parsed as a mapping indicator, breaking docker compose config. Quote the openssl hint instead. .env.example now ships an empty JWT_SECRET, so the test's docker compose config calls fail interpolation; supply a throwaway secret from the environment, which Compose lets outrank the env file. * fix(ci): seed JWT_SECRET into the recipe .env in selfhost config test The Makefile includes .env and bare-exports every variable to the recipe environment, so the make-driven recipe cases clobbered the test's JWT_SECRET export with the empty value .env.example ships. The stub's `docker compose config` then failed interpolation with stderr discarded, produced empty JSON, and crashed the node parser. Seed the throwaway secret into the recipe .env, which both make and Compose load. * docs(env): use a CSPRNG for the Windows JWT_SECRET generation hint Get-Random is not cryptographically secure, so the Windows hint could leave self-hosters with a predictable signing key. Point at RandomNumberGenerator instead, per review. * docs(selfhost): fix stale JWT secret comments Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4f56785b02 |
refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417) (#7245)
* refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417)
The Reply/Ownership split dated from PR #205 and had already been merged
in substance by MUL-5377 (one brief section + mode router) and MUL-6300
(reply turns own the same status arc). What remained was a per-turn
marker plus two mode blocks restating information the per-turn message
already carries as data. This finishes the merge:
- Status is judged once, at the END of the turn, from what the work
actually changed about the issue — not from the trigger type, not from
the run lifecycle, and not gated on being the assignee. No opening
in_progress write: the board's in-flight signal is the issue activity
indicator, and a todo issue the agent was only asked to research now
correctly stays todo (the old unconditional arc got that wrong twice).
Lifecycle writes oscillate under concurrent runs; fact writes converge,
which is what the retired assignee gate existed to protect.
- Delivery routes on data: a triggering comment in the per-turn message
(reply in that thread with THIS turn's --parent, one reply per thread)
or none (post a new top-level comment).
- The blocked rule and the squad-leader dispatch case become end states
of the same judgment instead of mode-block special cases; the leader
keeps one bullet (dispatching members is not delivery). The reply-mode
mention pointer is gone — the discipline lives in ## Mentions.
- The Turn mode markers are removed from the per-turn prompts, deleting
the marker/router-drift failure class (
|
||
|
|
f7abcdded3 |
docs: publish the community-maintained areas and their maintainers (#7249)
Until now, "community-maintained" appeared in the docs without ever naming who maintains what, and the actual maintainers were recorded only in Go package comments and PR threads. Users filing an issue had no way to learn any of it. Add a Community-maintained areas page in all four locales listing each area, its code path, its maintainers, and the retirement rule, and make that page the single source of truth: channels, the DingTalk and Telegram guides, the README, and the three package doc comments now point at it instead of restating it. Those package comments no longer carry maintainer handles or a second copy of the policy, so ownership changes are recorded in exactly one place. The page is deliberately a credit-and-expectations page, not a support directory — it asks readers to file a GitHub issue rather than page a volunteer directly, since what we promised these maintainers is that we would route work to them, not that they are on the hook for it. It also avoids the term "code owner": we are not adding a GitHub CODEOWNERS file, and that term carries a specific automated meaning there. Both issue forms gain a required Area field so a report can be routed without asking the reporter to read source, with the community-maintained areas marked as such in the option list. Co-authored-by: multica-agent <[email protected]> |
||
|
|
67c6aa05c4 |
MUL-6166: feat(telegram): add Telegram channel integration (#6944)
* feat(telegram): add Telegram channel integration Co-authored-by: multica-agent <[email protected]> * chore(telegram): rebase onto main and renumber migrations to 366-367 349/350 collided with main's agent_task_queue_chat_terminal_resume_index and agent_task_queue_chat_retired_session_index. Renumber the Telegram origin pair to the next free prefixes, update the cross-references inside the SQL comments, and point telegram_origin_migration_test.go at the new filenames. Contributor-facing behavior is unchanged. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: Bohan-J <[email protected]> |
||
|
|
801c671973 |
chore(assets): convert bitmaps to WebP, dedupe, and gate image size (MUL-6352) (#7149)
* chore(assets): convert web and docs bitmaps to WebP The repo carried 21.7MB of PNG/JPG and zero modern formats. next/image already serves visitors AVIF/WebP, but the raw masters are still paid for on every clone, every Docker build context, and every deploy upload. Re-encode with cwebp and cap the source width at the largest size any layout can request: 1920px for the decorative landing backgrounds (they render behind content under object-cover), 2640px for the landing hero (2x its 1320px slot), 1600px for screenshots (2x the ~720px docs and use-case column). EXIF goes with it — the four feature backgrounds were 144dpi print exports. Encoder choice is per image rather than uniform. Most compress best as lossy; two of the docs screenshots are flat palettized PNGs that encode smaller with `-lossless -z 9`, so they use that instead. Screenshots are q80 to keep UI text crisp, decorative art q72. apps/web/public 10.70MB -> 1.57MB apps/docs/public 7.47MB -> 2.44MB Verified: 1:1 crops against losslessly-downscaled references show no visible difference; all 266 image references in the tree resolve; docs prerenders all 177 pages and the landing/use-case pages render with no broken images. Co-authored-by: multica-agent <[email protected]> * chore(assets): drop the duplicated README hero image docs/assets/hero-board.png was byte-for-byte identical to the docs site's workspace-overview screenshot — 633KB stored twice. Only the two READMEs referenced the copy, so point them at the canonical file instead. Co-authored-by: multica-agent <[email protected]> * chore(assets): losslessly recompress app icons oxipng -o max --strip safe over the app icon PNGs: -640KB with pixels untouched (verified by comparing lossless-WebP encodes of the before and after of every file — all 16 identical). Deliberately not switched to WebP and not regenerated: the prerendered sizes under apps/desktop/build/icons/ exist because electron-builder's auto-derivation silently shipped only the 1024x1024 source in the v0.2.31 .deb (#2515), and the PWA manifest and .icns/.ico containers want PNG. Filenames, dimensions, and the electron-builder config are unchanged, so packaging behaves exactly as before. Co-authored-by: multica-agent <[email protected]> * ci: gate pull requests on a 300KB image budget Keeps the previous three commits from being undone one screenshot at a time. A bitmap added — or grown — past 300KB fails the check until the PR description carries an `Oversized image exemption:` line saying why. Shrinking an oversized file always passes: the check compares against the size at the base ref, not just the absolute size. Path-filtered to PRs that touch a bitmap, so the full-history checkout it needs to diff against the base is only paid for when it is relevant. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3c3e77b00c |
MUL-6362: redact secrets from provider command logs (#7206)
* fix(agent): redact provider command values Co-authored-by: multica-agent <[email protected]> * fix(agent): preserve safe command diagnostics Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
24d8c521a6 |
MUL-6364: make the LLM client retry budget explicit, wired, and validated (#7201)
* feat(llm): make the LLM client retry budget explicit, wired, and validated Config.MaxRetries was a bare int that nothing outside tests ever set. The production wiring in handler.New never passed it and no MULTICA_LLM_* env var fed it, so the field was configurable in name only and every deployment ran on the SDK default. Its semantics were also inverted. Because the zero value and an explicit 0 are the same int, "0" fell into the unset branch and silently produced 2 retries, while a negative — clamped to 0 to dodge the panic in option.WithMaxRetries — was the only value that actually disabled retries. - MaxRetries becomes *int so unset, 0 (disabled) and N (exact) stay distinct. New always passes the budget to the SDK explicitly, including the default, so the enforced policy and the reported one cannot drift apart. - Add MULTICA_LLM_MAX_RETRIES as the single configuration source. Non-numeric, negative and above-ceiling values fail the boot instead of being corrected into something that looks configured. The ceiling of 5 is a latency budget: backoff reaches ~21s at 6 retries while the internal callers of this layer time out after 8s and 20s. - Report the effective policy at startup via Client.RetryBudget, whose fields are counts and a fixed enum so the line cannot carry a key or gateway URL. - Document the four states in .env.example and the environment-variable docs, and record what the budget does not cover: the GenerateJSON parameter negotiation and a stream that breaks after ChatStream returns. Tests pin every state by upstream request count, so the unset-versus-zero collapse cannot come back unnoticed. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> * fix(llm): reject invalid retry budgets at the client boundary and wire self-host Review found the retry budget still reachable in two invalid shapes, and never reachable at all on the main self-hosted deployment path. - docker-compose.selfhost.yml declares the backend environment as an explicit allowlist and mapped none of the MULTICA_LLM_* variables, so every value .env.example and the docs told a self-hoster to set was dropped before the container: the LLM layer read as unconfigured no matter what they wrote. Map all four, and assert propagation in scripts/selfhost-config.test.sh, plus a drift guard so a future MULTICA_LLM_* documented without a mapping fails the script instead of silently going nowhere. - Config.MaxRetries took a *int, so a negative still reached llm.New, which warned and used the default instead. Warning is not silence, but substituting a different budget is still the implicit correction #7154 asked to remove. It now takes *RetryOverride, whose field is unexported and whose only constructor rejects negatives — the invalid state is unrepresentable, so New has no correction branch left to take. - The budget is a ceiling, not a quota: only retryable failures consume it, and a success or the caller's deadline can end a call sooner. Say "at most N" rather than "exactly N" in the field docs, .env.example and all four environment-variable docs. The negative-value test now asserts rejection instead of pinning the fallback as contract. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bb8a04f3a1 |
docs(llm): disclose what the server-side LLM layer sends, and test the zero-request opt-out (MUL-6369) (#7199)
* docs(llm): spell out what the server-side LLM layer sends upstream (MUL-6369) The two MULTICA_LLM_* variables are the only switch governing whether chat content reaches an external model, but nothing said so: the .env.example comment named chat titles as an example consumer, the docs page said only that callers 'use local fallback logic', and neither said what is sent. An operator whose policy forbids chat content leaving the deployment had no way to confirm that leaving the variables empty is sufficient, which is what GitHub issue #7162 reports. Document the layer where it is configured: both consumers (chat auto-titling, follow-up questions) and exactly what each sends, plus the fact that leaving the key and base URL empty is a supported configuration that costs nothing but the LLM-written title and the suggestion buttons. The package doc carries the same inventory next to the code, so a new consumer is added in the file that documents them. Co-authored-by: multica-agent <[email protected]> * test(llm): pin the zero-upstream-request and single-entry-point contracts (MUL-6369) The documentation added in the previous commit makes two promises that were true only by accident. An unconfigured client returning ErrNotConfigured was never asserted to do so without dialing, and 'every server-side LLM call goes through pkg/llm' held only because no one had imported the SDK elsewhere yet. TestUnconfiguredClientMakesZeroUpstreamRequests drives every exported call path through a transport that counts and refuses, then proves the counter can move for a configured client so the zero-count assertions cannot pass vacuously if the HTTPClient seam is ever dropped. TestOutboundLLMTrafficGoesThroughThisPackage parses the server module and fails on an OpenAI SDK import outside this package, because the consumer inventory is only auditable while this package is the only door. Co-authored-by: multica-agent <[email protected]> * fix(llm): scope the privacy claim, and guard the consumer inventory (MUL-6369) Addresses both must-fixes from review. They land together because they share the same surfaces: the package doc is the inventory the operator copy is written from, and it names the tests that hold each half. 1. The disclosure claimed more than the code guarantees. The copy said these variables are the deployment's only switch for sending chat content to a model, and that emptying them means the server sends nothing to any provider. What is actually guaranteed is narrower: server/pkg/llm makes no request. Running an agent reaches a model by an entirely separate path — the daemon executes an AI coding tool under that tool's own credentials, with MULTICA_* stripped from its environment. For a privacy document that gap matters more than it would anywhere else: an admin who reads the old wording concludes emptying two variables stops all model traffic, which is the kind of wrong belief #7162 is about. Every surface now says "this layer sends nothing" and names the agent path explicitly — .env.example, the four docs locales, the package doc, the test comments. 2. The inventory had no guard. The import test proved the OpenAI SDK has one door; it said nothing about how many callers walk through it, so a third feature calling GenerateText would ship undisclosed with every test green while the docs kept naming two consumers. TestDocumentedConsumersAreTheOnlyCallers scans production call sites and requires the set to match the published list in both directions: an undocumented caller fails, and so does a documented one that stopped calling, since an inventory that overstates what is sent misleads too. The import test is renamed TestOpenAISDKIsImportedOnlyByThisPackage to claim only what it checks. Also drops the package doc's stale "drafting a quick-create issue" example, which named a consumer that does not exist and contradicted the inventory two paragraphs below it. Co-authored-by: multica-agent <[email protected]> * fix(llm): correct the MULTICA_* claim about agent environments (MUL-6369) Review caught a factual overreach in the previous fix. The copy said MULTICA_* variables are dropped from the agent's environment rather than passed to it, which is not what mergeEnv does: it drops the namespace the daemon INHERITED, then appends the task-scoped values daemon.go assembled — MULTICA_SERVER_URL and MULTICA_TOKEN reach the child by design, and claude_test.go asserts it. The guarantee an operator actually needs is narrower and still sufficient: the daemon does not forward these MULTICA_LLM_* settings to the AI coding tool, and agent runs use that tool's own credentials. Say that instead, and name the task-scoped injection so the sentence does not contradict the subprocess env contract. Applied to .env.example, the four docs locales and the package doc. Also splits methodNameCollisions out of documentedConsumers, per the same review's non-blocking note. A method-name collision quieted by adding a file to documentedConsumers would have corrupted a list operators read as 'what sends my chat content somewhere' in order to silence a test — a bad trade the guard should not invite. Empty today; the repository has no collisions. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
22db0b0a2c |
MUL-5651: preserve tasks through runtime network partitions (#7129)
* fix(runtime): preserve tasks through network partitions Co-authored-by: multica-agent <[email protected]> * fix(runtime): bound reconnect retry waiting Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f78c867168 |
docs(tasks): describe the execution log where it actually lives (#7120)
The 'Viewing run history' section read as a two-step navigation ('go to the
Execution log' then 'open a run record'), which does not match the product:
ExecutionLogSection renders inline in the issue's right sidebar, expanded by
default, with a one-click View transcript on each row.
An external report (#7107) asked for an inline collapsible execution view on
the issue page based on this wording, and separately asked whether issue
pages auto-refresh. Both already exist; the docs described neither.
- Say the log lives in the issue's right sidebar, not a separate page/tab
- Note the default-expanded state, pinned active runs, Show past runs
- Point at the sidebar panel button for when the section is not visible
- State that the log updates live over the realtime connection
- Mention the header live chip and the transcript's Newest first toggle
- Add the sidebar screenshot ahead of the existing transcript screenshot
Applied to en/zh/ja/ko.
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
de902a4aab |
MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime * fix(agent): terminate mcode process trees * fix(agent): address mcode review blockers * test(agent): keep mcode model contract API-stable * fix(migrations): resolve mcode prefix collision * fix(agent): launch mcode through runtime command Assisted-by: MiniMax Code * fix(migrations): renumber mcode migration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9a30c51dac |
MUL-6286: feat(properties): actor / multi_actor custom property types (members) (#7075)
* feat(properties): add actor / multi_actor custom property types (MUL-6286) Custom issue properties gain their first reference type: `actor` (single) and `multi_actor` (multiple), holding workspace members and agents. Value shape is a prefixed string — "member:<user_id>" / "agent:<agent_id>", multi being an array of them. Keeping it a plain string rather than an object means the existing @> containment filter, the jsonb_path_ops GIN index and the client value schema all keep working unchanged; an object-shaped value would have been silently dropped by the client preprocess that guards against unknown value shapes. Scope decisions: - Squads are out. The assignee pair accepts member/agent/squad, but a squad is a routing target rather than a person. The stored form carries the kind, so widening later needs no migration and no new type. - Referencing an agent is NOT an assignment: nothing is enqueued, routed or notified. The picker therefore does not gate agents on runtime binding or invoke permission the way the assignee picker does — it gates on visibility, matching the server. - Agent identity still needs protecting, so writes and facet reads both run through accessibleAgentIDs: a private agent never becomes referenceable or discoverable by id. - multi_actor dedupes but preserves insertion order instead of canonicalizing like multi_select. There is no config order to sort against, and sorting by id would reshuffle the avatar row on every edit. @> containment is order-insensitive, so filtering is unaffected. - Actor properties are filterable (including "= me", with the signed-in member sorted first) but not sortable or groupable — the same position multi_select already holds. Both types ship together because a property's type is immutable: shipping single-only would have forced anyone wanting multi to recreate the property and re-enter its values. Co-authored-by: multica-agent <[email protected]> * refactor(properties): narrow actor properties to members only (MUL-6286) Scope change after review: `actor` / `multi_actor` hold workspace members only. Agents are out — a passive reference had to answer agent-visibility questions (private / non-allow-listed agents must not become discoverable by id) and carry a "referencing is not assigning" caveat everywhere, all for a use case nobody has asked for. The original request was for members. The envelope is unchanged on purpose, so this stays a one-line widening later rather than a migration: the types are still named `actor` / `multi_actor`, the stored value is still the kind-prefixed "member:<user_id>", and the migration still allows both type names. Adding "agent" back means appending to `actorPropertyKinds` — existing definitions would gain the kind in place, with no new property type and no value rewrite. What this removes: - The agent branch of `resolveActorRefs`, including the accessibleAgentIDs visibility gate and the archived-agent check. - `filterAccessibleActorFacetValues` entirely. Facet keys are now all `member:<uuid>`, and workspace membership is already visible to every member, so there is nothing left to gate. - The agent section of the actor picker, and agents from the filter dropdown's candidate list. - The "reference, not an assignment" framing from the skill and product docs — that distinction only existed because agents were allowed, and keeping it would now confuse rather than clarify. `actorPropertyKinds` carries the note that any future kind which is not universally visible to every member must restore the visibility gate on both the write path and the facet read path. Tests updated to match: agent refs are now rejected at the kind check, and the private-agent visibility test is replaced by TestIssueActorPropertyFacets, which covers the facet path that backs the "= me" filter. Co-authored-by: multica-agent <[email protected]> * fix(properties): address actor-property review findings (MUL-6286) Four defects from review, plus one pre-existing bug found next to them. 1. Editing a multi_actor value silently deleted kinds the client did not recognise. The picker rebuilt the array from `actorRefsFromValue`, which filters unknown kinds — so once the backend widens actorPropertyKinds, an installed desktop build would drop every agent reference the moment the user toggled a member. The edit path now round-trips the RAW string list via the new `actorRefValuesFromValue`, and the toggle is extracted to `toggleActorRefValue` so the guarantee is directly testable. 2. Actor ids were stored exactly as received. `uuid.Parse` also accepts uppercase, braces and the urn: form, and every consumer (member directory lookup, "= me" filter, @> containment) compares reference strings exactly — so `member:<UPPERCASE-UUID>` passed validation and then rendered as Unknown and matched no filter. `parseActorRef` now stores the canonical form. 3. The CLI, docs and skill all promised member email as a way to address a value, but `resolveAssignee` only matched id and name. Email is now an exact-match alias ranked with id matches (it is as unambiguous as an id), so the documented behaviour is real. This also makes `--assignee` accept an email, which is a strict improvement. 4. An active actor-property filter rendered as a bare count. The chips bar loads each directory lazily and gated the member directory on assignee/creator filters only, then looked names up in `config.options`, which actor properties do not have. It now detects actor-typed filters, loads the directory, and renders names plus an avatar stack. Found while fixing 4: the chips bar keyed its actor-name map by the membership row id (`m.id`) while every actor filter value carries `user_id`, so member names never resolved in the assignee and creator chips either. One token, same expression, fixed here rather than left next to the new code. Also drops two comments still describing the member/agent scope. Co-authored-by: multica-agent <[email protected]> * fix(properties): stop a single actor value overwriting an unreadable ref (MUL-6286) Round-two review finding. The previous fix only protected `multi_actor`: its toggle round-trips raw strings, so a reference kind this build cannot parse survives an edit. Single `actor` had no such protection. Once the backend widens actorPropertyKinds, an older client reading `agent:<id>` in a single actor property would filter it out, render the field as empty, and then — when the user "filled in the empty field" — replace it outright. The user sees themselves setting a blank field; what actually happens is a value they were never shown gets deleted. The editor now degrades to read-only when a single actor value holds a reference it cannot parse, and the display says so instead of showing the empty-state label. Clear is still offered, so a genuinely stale value can be removed deliberately. `multi_actor` stays editable — its toggle is already non-destructive. The read-only decision moved into an exported `isCustomPropertyReadOnly`, so all three reasons it can fire (archived, unknown type, unreadable actor ref) are covered by tests rather than being inline in the component. Also closes the two non-blocking gaps from the same review: - The "email outranks a name substring match" CLI test asserted a ranking its fixture could not actually exercise — no candidate's name contained the email. Added an agent named "Mailer for [email protected]", so the test now fails if email stops being ranked with id matches. - Extracted `buildChipActorNames` and asserted a member resolves by `user_id` and NOT by the membership row id — the pre-existing bug fixed in the last commit now has a test that would catch its return. Co-authored-by: multica-agent <[email protected]> * chore(migrations): renumber actor-types migration to 341 (MUL-6286) 332 was taken by 332_issue_status on main while this PR was open; the migration lint test caught the collision in CI. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2014ae3613 |
feat(skills): bulk update from source (#6930)
Rescoped from the original freshness-awareness PR per maintainer review on #6930: the upstream freshness tracking (import-time version markers, the refresh-check endpoint and cache, and the detail-page up-to-date indicator) is dropped entirely; only the bulk update remains, simplified. The skills list batch toolbar gains an Update action: the confirm dialog shows how many selected skills will be re-downloaded from their hosted source and how many are skipped (manual/runtime/no permission), then reuses the existing single-skill POST /api/skills/:id/refresh sequentially across the updatable selection. No freshness probe first — re-fetching an already-current skill costs one download on an explicit click. Kept from the original design: per-item failure tolerance (one 409 must not strand the batch), a partial-failure toast, and the selection surviving a partial run so retry is one click. Frontend-only; no API or schema changes. Co-authored-by: Guillaume Sayer <[email protected]> |
||
|
|
d0bd6683f1 |
MUL-5733: keep Go security patches current (#6409)
* fix(release): keep Go security patches current Co-authored-by: multica-agent <[email protected]> * fix(release): address vulnerability review Co-authored-by: multica-agent <[email protected]> * fix(release): restrict vulnerability bypass Co-authored-by: multica-agent <[email protected]> * fix(release): require Go 1.26.6 Co-authored-by: multica-agent <[email protected]> * docs: align Go prerequisite with security floor Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: Sol-Boy <[email protected]> |
||
|
|
3b08423684 |
MUL-6260 fix(daemon): treat custom runtime fixed_args as the argv prefix (#7052)
* fix(daemon): treat custom runtime fixed_args as the argv prefix (MUL-6260) A custom runtime profile configured as `ccms start q36` failed every task with `unknown option '-p'`. The profile's fixed_args were merged into ExecOptions.ExtraArgs, which each backend appends *after* the protocol flags it hardcodes, so the wrapper saw `-p` before its own `start` subcommand had been selected and rejected it (GH #7046). Ordering was only half the defect. ExtraArgs is opt-in — six of the twenty-one backends read it — so on the other fifteen families, including every ACP one, fixed_args were silently discarded rather than misordered. Model discovery and `--version` probing carried the profile's path but not its arguments, so even a fixed launch would have left the model picker and the reported version describing the wrapper instead of the CLI behind it. fixed_args now travel as agent.Config.LaunchPrefix and are spliced in directly after the executable, ahead of everything a backend builds. The prefix is applied by a new Command boundary in launch.go through which every runtime process in the package is constructed — launches, version probes and discovery alike — so a backend cannot forget it. A guard test fails the build if any file outside launch.go calls os/exec directly; distributed opt-in is what let ExtraArgs rot, and repeating it would rot the same way. Flag-style commands (`agent --model composer-2.5`) are unaffected: a CLI takes its own flags in any position, which is why prefix-first can be the single order rather than a per-runtime setting. One deliberate behaviour change. The prefix used to sit last and win every last-wins parse, so a profile pinning `--model` silently overrode the model chosen on the agent — the UI showed one model and ran another. Prefix-first inverts that: the more specific per-agent choice is the later token and wins. Overlaps are logged. Protocol-critical flags are filtered out of the prefix once in New(), which knows the family; positional tokens always pass through. Codex additionally strips `-c` overrides aimed at daemon-managed config namespaces, since those beat config.toml from any argv position. The order was never documented, and every shipped fixed_args example is flag-style, so this fills in a contract rather than breaking one. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close the launch-prefix gaps found in review (MUL-6260) Four defects from the first round of review on the fixed_args-as-argv-prefix change. Hermes could escape its own task overlay. The profile selection was resolved from custom_args alone and stripped from custom_args alone, so a profile configured as `hermes -p research` kept that flag in the launch prefix and re-pointed HERMES_HOME past the overlay the daemon had just built. Resolution now parses the argv hermes actually sees — launch prefix then custom args, in process order — and the strip covers both regions. It also removes every selection rather than the first: taking out the first promotes the second, and two separately-configured argv regions make "two selections" an ordinary configuration rather than a user mistake. Positional tokens were not passing through as documented. filterLaunchPrefix delegated to filterCustomArgs, whose blocklists deliberately carry positional tokens — hermes and qwenpaw block `acp`, traecli blocks `acp`/`serve`, grok blocks `agent`/`stdio`/`serve` — because in custom_args those are someone re-issuing the backend's own subcommand. In a launch prefix the same token is the command's identity, so `hermes-wrapper acp tenant` was being rewritten to `tenant`. Only a leading `-` now makes a token eligible for the blocklist; a blocked flag still consumes its separate value so no stray value survives to be read as a subcommand. Codex fast mode lost to the prefix. `--disable fast_mode` beats `--enable fast_mode` regardless of argv order and `-c features.fast_mode=false` beats config.toml from any position, so prefix-first does not protect an explicitly selected priority tier. The removal half of enforceCodexFastMode is now a separate helper applied to the prefix too; the managed enable is still appended once, on the final args. Windows CI could not compile the daemon tests: the Command migration left an unused `path := runtimeCmd.Path` in a hook whose body never referenced it. Verified with GOOS=windows and GOOS=linux vet, which is what should have caught it the first time. Also softens the docs and the Command doc comment: flag-style prefixes usually parse the same in either position, but a CLI that separates global from subcommand flags can care, so the runtime docs now name the position change and tell operators with existing fixed_args to re-run one task after upgrading instead of promising the move is invisible. Co-authored-by: multica-agent <[email protected]> * fix(daemon): resolve and strip the Hermes profile from the real launch argv (MUL-6260) Second-round review found the Hermes profile chain still reasoning about a hand-assembled approximation of the command line instead of the command line itself. The backend launches `<prefix> acp <filtered custom args>`, but the daemon assembled `<prefix> <custom args>` — no `acp`, no filtering — and that token participates in Hermes' own argv scan. With fixed_args `--model` and custom_args `-p research`, the approximation reads `-p` as `--model`'s value and finds no selection, so the overlay is seeded from the default home; the real `--model acp -p research` skips `--model acp` and selects `research`. The selector was then stripped, so the task stayed inside the overlay — but an overlay built from the wrong profile, silently. Stripping had the matching defect, and this one was still an escape rather than a mismatch. A launch prefix ending in a bare `-p` captures the backend's `acp` as its profile value, so neither region holds a complete selection and per-region stripping removes nothing: `-p acp research --yolo` reaches Hermes intact and re-points HERMES_HOME straight out of the overlay. Both halves now go through one assembly. agent.HermesLaunchArgv builds exactly what hermesBackend.Execute builds — the backend calls the same helper, so they cannot drift — and the daemon resolves from it. StripHermesProfileSelectors strips against that same argv and hands surviving tokens back to the region they came from, iterating to a fixed point because removing one selection promotes the next. The `acp` token is never removed; the backend re-adds it, and dropping the flag that captured it is what breaks the selection. StripAllHermesProfileArgs, hermesLaunchArgs and hermesEffectiveArgs are gone: each was only ever correct on a complete argv, and keeping them would leave the same footgun for the next caller. Tests cover the case the review named (prefix ending in a value-taking flag, custom args opening with a selector), the straddling-selection escape, multiple occurrences across both regions, and an invalid value left untouched because Hermes discards it anyway. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
0a1357498a |
MUL-6247: rate limit workspace invitations (#7030)
* feat(workspaces): rate limit invitations (MUL-6247) Co-authored-by: multica-agent <[email protected]> * fix(workspaces): preserve invitation rate budgets Co-authored-by: multica-agent <[email protected]> * fix(workspaces): avoid partial limiter failures Co-authored-by: multica-agent <[email protected]> * fix(workspaces): relax recipient invitation limit Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9f85d72a06 |
MUL-6193 Support Slack message attachments in chat (#6975)
* feat(slack): ingest inbound message file attachments as chat media Slack messages carrying files now come through to the chat: the inbound translation captures each file's id, name, mimetype, size, and private download URL into the raw envelope, and a new engine.MediaResolver fetches the bytes with the installation's bot token, stores them through the shared intent-ledger pipeline, and binds them as chat attachments. The download client only talks to Slack-owned hosts so the bot token cannot leak, and an HTML login-page response (missing files:read scope) is rejected instead of stored as the file. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(slack): address attachment review findings * docs(slack): make media comments standalone * fix(slack): harden inbound attachment ingestion Address second-round review findings on the Slack media resolver. - Restore the bot token on permitted redirects. net/http forwards Authorization only to the same host or a subdomain, so a hop to a sibling such as files-origin.slack.com arrived unauthenticated, Slack answered with its HTML login page, and the file was silently dropped. The redirect policy has already proved the destination is Slack-owned. - Drop files the resolver could never fetch at translation time. Remote "external" files point url_private at Google Drive or Dropbox, so carrying them made HasMedia promise media the pipeline cannot bind: the message got a deferred run and an intent-ledger row per doomed download. - Share the Router's media deadline across the files still to fetch. Ten files at a flat 30s each ran far past the 45s budget, and the Router discards every ref once it expires, so one slow file cost the whole message its attachments. - Refuse an oversized file on its declared size, before it costs an intent row and a full transfer. Correct the limit wording to MiB. - Cover the redirect policy, the size boundary, the spent budget, and the off-Slack filter with regression tests, and drive the file_share translation test through slackevents.ParseEvent instead of a hand-built MessageEvent.Message. - Document sending files, the per-file and per-message limits, and the skip-silently behaviour in all four locales. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]> Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4d495056e8 |
[MUL-6139] Expand Plugin skills and hosted MCP connections (#6920)
* feat(plugins): add remote MCP runtime Co-authored-by: multica-agent <[email protected]> * chore(plugins): add private Exa example Co-authored-by: multica-agent <[email protected]> * docs(plugins): remove private plugin guide Co-authored-by: multica-agent <[email protected]> * fix(plugins): stabilize remote MCP tool digests Co-authored-by: multica-agent <[email protected]> * fix(plugins): preserve remote MCP notification status Co-authored-by: multica-agent <[email protected]> * fix(plugins): make Exa example runnable end to end Co-authored-by: multica-agent <[email protected]> * fix(codex): record MCP tool calls in agent log Co-authored-by: multica-agent <[email protected]> * chore(plugins): add community skills example Co-authored-by: multica-agent <[email protected]> * feat(plugins): support multi-file skill bundles Co-authored-by: multica-agent <[email protected]> * feat(plugins): add Mobbin MCP example Co-authored-by: multica-agent <[email protected]> * chore(plugins): bundle full Matt Pocock skill set Co-authored-by: multica-agent <[email protected]> * feat(plugins): add remote MCP OAuth connections Co-authored-by: multica-agent <[email protected]> * fix(plugins): gate enable on remote MCP readiness Co-authored-by: multica-agent <[email protected]> * fix(plugin): authenticate remote mcp credential broker Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden remote mcp oauth flow Co-authored-by: multica-agent <[email protected]> * refactor(plugins): use a single feature flag Co-authored-by: multica-agent <[email protected]> * fix(plugins): clean up remote MCP OAuth state Co-authored-by: multica-agent <[email protected]> * chore(plugins): remove Matt Pocock example Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2c0912b6ec |
MUL-5421: make workspace MCP servers an explicit per-agent assignment (#6976)
* feat: make workspace MCP servers an explicit per-agent assignment #6961 shipped workspace MCP as a shared document every agent inherited automatically. That is the wrong shape: a workspace MCP server should behave like a workspace skill — a library entry that reaches an agent only when someone gives it to that agent, with a per-agent on/off toggle. Creating one now gives it to nobody, so nothing propagates implicitly. Model (mirrors `skill` + `agent_skill`): - `workspace_mcp_server` — the library, keyed by id so renaming is safe - `agent_mcp_server` — the assignment, carrying its own `enabled` toggle - the agent's own `mcp_config` stays what it was: servers private to it At claim time the effective set is runtime servers < (assigned AND enabled workspace servers + the agent's own), with the agent's own entry winning a name collision. 318 drops `workspace.mcp_config`: leaving it would keep a second, silently inherited source alive. No data migration — converting each workspace document into library entries would ASSIGN them to every agent, which is the behaviour being removed, and the column shipped in the same unreleased cycle. The write-only secret contract, the legacy-container normalization, and the transport guards from the previous round are all preserved; the resolver is re-pointed at the assignment list rather than a workspace document. UI: workspace Settings → MCP is now a library (adding says so, and renaming is allowed because assignments key off the id); the agent's MCP tab replaces the auto "Inherited from workspace" list with the servers actually assigned to it, a picker to add more from the library, and a per-assignment toggle. Co-authored-by: multica-agent <[email protected]> * fix: fence the MCP writes, stop laundering unknown transports, gate on canEdit Three findings from review: 1. Neither new table carries a foreign key (repo rule), so nothing in the database catches a write landing after the row it depends on is gone. Create and assign now run in a transaction that joins the workspace teardown fence (#5219), and assign additionally holds the target server FOR SHARE while DeleteWorkspaceMcpServer holds it FOR UPDATE — so an assignment either lands before the delete and is swept, or waits and finds the server gone. Driven by two two-transaction race tests, verified to fail without the locks. 2. `mcpTransportOf` classified `{"type":"websocket","url":"wss://…"}` as http, because an unrecognised type fell through to the url check. That told the settings UI the guided form could edit the entry, and saving from it rewrites the protocol to `type: "http"` — the failure the client-side guard was added to prevent, reintroduced from the server. An explicit type is now returned verbatim; inference from command / url happens only when none was declared, which is lossless. 3. The agent MCP tab showed the picker, toggles, and remove button to members who cannot edit the agent, where every click 403s. It now takes `canEdit` like SkillsTab and hides the write affordances, keeping the inventory readable. 318 keeps dropping `workspace.mcp_config` with no data migration and no expand-contract window, now recorded as an explicit maintainer decision (Bohan, 2026-08-14): 314 merged in this same cycle and was never deployed, so there is no stored document to preserve and no old binary reading the column. The comment spells out what this file would have to become if that ever stops holding. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d96c936430 |
docs: link the Multica CLI skill from the README and CLI docs (#6978)
The skill that teaches Codex, Claude Code, and Cursor to operate Multica through the authenticated CLI lives at multica-ai/multica-cli, but nothing in this repository points at it — a search for the repo name returns only release-asset filenames. Users have filed feature requests for a skill that already exists (#6968, #4681), which is a discoverability problem, not a missing feature. Adds a row to the README documentation table and a section to the CLI docs page in all four languages, plus a Next steps entry. Docs-only change; no build or tests were run. |
||
|
|
736838af86 |
MUL-5421: workspace-level MCP configuration (resolution contract, write-only API, Settings UI, CLI) (#6961)
* feat(server): resolve a workspace-level MCP config under each agent's Adds workspace.mcp_config and layers it beneath the agent's own config on every claim, so a workspace's shared MCP servers no longer have to be re-entered on each agent (GH #6062). Resolution contract: - agent config absent (NULL/null) -> inherit the workspace document - agent declares >=1 server -> union by server name, agent wins - agent declares no server ({}) -> opt out entirely The last rule is what keeps shared credentials from silently reaching an agent that was deliberately given none: "managed but empty" already means "this agent runs with zero MCP" under the three-state contract from #3545, and it keeps meaning exactly that. The merged document keeps the AGENT's top-level keys, so a legacy `mcp` container survives untouched, and a shared server is skipped when the agent already declares that name under either spelling. On malformed input the agent's config is returned unchanged: a bad shared document must never take away servers an agent runs with today. Read on every claim like agent.mcp_config, so an admin's edit lands on each agent's next task with nothing to restart. Co-authored-by: multica-agent <[email protected]> * feat(server): add the workspace MCP configuration API GET /api/workspaces/{id}/mcp-config is member-visible so an agent's settings view can tell what it inherits, but the document itself follows the same visibility rules mcp_config already has on agents: agent actors never read it, the workspace always-redact setting wins over any role, and otherwise only the owner/admin who may write it can read it back. The coarse server_count stays visible either way — it carries no credential material. PUT is owner/admin and replaces the whole document (so removing a shared server is expressible), with an explicit null to clear the layer. It is shape-validated at the edge, unlike the agent column: one bad document here would otherwise fail-soft on every agent's claim for the whole workspace. Agent actors are refused even when their PAT satisfies the role gate. Co-authored-by: multica-agent <[email protected]> * feat(cli): add `multica workspace mcp get|set` Reuses resolveMcpConfig so the workspace surface agrees with `agent update` on what a valid document is, on the three mutually exclusive input channels, and on 'null' meaning clear. Table output prints server names and transports only — never entry bodies — so a summary stays safe to paste into a ticket; the JSON default is how an admin reads the document itself. Documents the new inheritance in the agent-creation built-in skill (an agent with a private server no longer has to re-list the shared ones, and `{}` is now the only way to give an agent no MCP at all) and adds the command to the CLI reference. Co-authored-by: multica-agent <[email protected]> * fix(server): keep a legacy agent's own MCP servers when the workspace shares some The resolver wrote shared servers into `mcpServers` and left an agent's legacy top-level `mcp` entries beside them. The daemon's runtime merge only falls back to `mcp` when `mcpServers` is ABSENT, so as soon as a workspace shared anything, an OpenCode agent that stored its servers under the legacy container ran with the shared set and silently lost every private server of its own. Fold both containers into the canonical `mcpServers` when merging, and drop the legacy key so the daemon can never read a second, stale copy. Nothing changes for an agent whose workspace shares no servers: that path still passes the agent document through byte-for-byte. The regression guard runs the real chain — ResolveWorkspaceMcpConfig into the daemon's mergeRuntimeAndAgentMcpConfig — because the two halves only agree if the resolver emits the container the merge reads, which is exactly what broke. Verified it fails against the previous resolver. Co-authored-by: multica-agent <[email protected]> * feat(server): make the shared MCP configuration write-only Upstream #6062 asks for shared secrets to be write-only and never echoed back; the previous read path handed the whole document to owner/admin, which also tied "audit what is shared" to "read the credentials". GET now returns only a non-secret inventory — name, transport, enabled — for every role. Not a redaction: there is no document field and no `_redacted` flag, because nothing secret is in the payload to gate. A url counts as secret here; a Composio-style session url is a bearer credential on its own. That removes the client's ability to read-modify-write, so editing one server gets its own endpoints: PUT/DELETE /mcp-config/servers/{name} merge server-side under a row lock (two admins editing different servers can no longer lose one another's write). Deleting the last server clears the column rather than storing an empty document, so agents fall back to their own config instead of inheriting a managed-empty one. The whole-document PUT stays for the declarative CLI path. Co-authored-by: multica-agent <[email protected]> * feat(cli): add `workspace mcp add|remove`, and stop printing the document `workspace mcp get` now renders the server inventory in both output formats — there is no document to print, and the previous JSON default would have written whatever the endpoint returned straight to a terminal. Since the document can no longer be read back, `set` (full replace) is not enough to change one server: `add <name>` and `remove <name>` map to the per-server endpoints. `add` takes the entry through the same three mutually exclusive secret-safe channels as `--mcp-config`, generalized here so both surfaces agree on what a valid payload is; `null` is rejected for a single entry, where it is far likelier to be a mistake than an intent to clear. Adds the CLI test coverage this surface was missing: endpoint and body per command, `null` clear, mutual exclusion, name escaping in the path, and a guard that a regressed server returning the document cannot get it printed. Co-authored-by: multica-agent <[email protected]> * docs: correct the workspace MCP contract in the built-in skill and CLI reference - "`{}` is the only way to opt out" was wrong: any config declaring no servers opts out, which is what the implementation and tests do. - The source map pointed the API row at workspace_mcp.go; the handlers live in workspace_mcp_api.go. - Document the write-only read path, the legacy-container normalization, and the new per-server commands. Co-authored-by: multica-agent <[email protected]> * feat(web): manage workspace MCP servers from Settings The user journey for this feature is a settings screen, not the CLI. Adds a workspace Settings → MCP tab that lists the shared servers and adds, replaces, and removes them one at a time through the per-server endpoints, reusing the agent tab's McpServerDialog for the form/JSON editor. Because the stored configuration is write-only, an edit opens an empty form and REPLACES the entry rather than prefilling it. The screen says so instead of implying the blank form is the saved state. Members see the inventory (nothing in it is secret) but no write affordances. The agent's MCP tab gains the matching "Inherited from workspace" section, with an Overridden badge on a shared server the agent shadows by name, and an explicit note when the agent's managed-but-empty config opts it out of the workspace layer entirely — otherwise the effective server set is impossible to reason about from the agent's own screen. Co-authored-by: multica-agent <[email protected]> * fix(client): stop the write-only boundary leaking through the clients The server never returns the stored document, but neither client enforced that on its own side, so a regressed (or hostile) backend could still push secrets into the frontend and onto a terminal: - the two workspace-MCP zod schemas were `.loose()`, so unknown fields — a top-level `mcp_config`, or `url` / `headers` / `env` inside a server entry — survived parsing into the app object and the query cache. These are now the only schemas in that file that strip: forward-compatibility is the right default everywhere else, but here keeping unknown keys is the hole. - the CLI decoded into `map[string]any` and re-encoded it, so the DEFAULT JSON output printed whatever arrived. It now decodes into a named struct, which is what makes leaking impossible rather than merely unlikely. The old "never prints secrets" test forced `--output table` and so missed the default path entirely. Replaced with a poisoned-response fixture (secrets at the top level AND inside a server entry) asserted across both output formats and on the write path; verified it fails against the previous renderer. Co-authored-by: multica-agent <[email protected]> * fix(web): stop a workspace MCP edit from silently becoming an add The shared dialog lets the name be edited and this API has no atomic rename, so renaming `linear` to `linear-v2` upserted a second server, left the original in place, and still toasted "updated". Editing now pins the name (the dialog gained a `lockName` prop) and the upsert targets the entry that was opened. Also seed the edit form's transport from the safe summary: the entry cannot be read back, and `formFromConfig({})` defaults to HTTP, so editing a known stdio server opened the wrong form. Co-authored-by: multica-agent <[email protected]> * fix(web): show the agent's real effective MCP set Two ways the agent tab was stating things it did not know: - With `mcp_config_redacted`, the viewer cannot tell an explicit empty document (opted out) from one that overrides some names — but the tab listed every workspace server as inherited anyway. It now says inheritance cannot be determined instead of guessing. - The runtime list marked a server "overridden" using the agent's names only. The real precedence is runtime < workspace < agent, so a shared server hides a same-named runtime one too; that was under-reported. Overrides are now computed from workspace + agent names, and from the agent's alone when it opted out. Co-authored-by: multica-agent <[email protected]> * docs: correct the opt-out claim and apply the zh terminology conventions An agent whose config declares no servers opts out of the WORKSPACE layer; it does not run with zero MCP. The daemon still merges the runtime's own local servers under whatever the claim carries — which the daemon test on this branch already asserts, so only the prose was wrong (skill doc, CLI help, UI copy). The new zh-Hans strings also used `agent` / `url` / `header` raw; per conventions.zh.mdx those are 智能体 / URL / 请求头. Co-authored-by: multica-agent <[email protected]> * fix(i18n): restore the zh-Hans quick-actions strings this branch clobbered My earlier terminology pass ran a `re.sub` for `description` / `delete_description` with count=1, which matched the FIRST such keys in the file — `quick_actions`, not `mcp` — so Quick Actions ended up describing shared MCP servers and its delete confirmation talked about sharing. A user-visible regression in an unrelated feature, and one no key/type check can catch because both values stayed valid strings. Restored both to their origin/main text. Audited all eight locale files the branch touches the same way; these two were the only unintended changes. Co-authored-by: multica-agent <[email protected]> * fix(web): never edit a non-stdio/http MCP server through the rewriting form The guided form only expresses stdio and http, and saving from it REWRITES the entry — `configFromForm` emits `type: "http"` for anything non-stdio. The server classifies and returns `sse`, so editing an SSE server opened the Streamable HTTP form and saving silently changed its protocol, which can break the server outright. Entries the form cannot express now take the JSON path, which round-trips what is typed, and the Form tab is disabled rather than merely unselected — switching to it would rewrite them just the same. The rule keys off the saved config when there is one and off the safe summary's transport when there is not, so it covers both the workspace tab (write-only, summary only) and the agent tab, where the same rewrite was already reachable for a saved SSE entry. Also fixes the garbled `multica workspace mcp get|list-only` in the built-in skill, which reads as a shell pipe. Co-authored-by: multica-agent <[email protected]> * fix(web): judge form-editability by the entry's explicit type, not its display bucket The previous guard asked `mcpTransport(config)` whether the form could express a saved entry, but that is a lossy DISPLAY classifier: it buckets anything with a `url` as http regardless of `type`. So `{"type":"websocket","url":"wss://…"}` still looked form-editable and `configFromForm` rewrote it to `type: "http"` on save — the very rewrite the guard exists to prevent. SSE happened to be caught only because the classifier has a dedicated branch for it. Saved entries are now judged on the explicit `type`, allowlisting exactly what `configFromForm` writes back unchanged (local / stdio / remote / http / streamable-http), with the no-type case still inferred from command / url. The summary path keeps using the transport string, which the server emits from a closed set. Tests cover both unknown-type spellings on the agent side and pin the common shapes to the form — this is a guard against rewriting entries, not a general retreat to the JSON editor. Verified the websocket case fails against the previous check while sse passed, which is why the earlier test missed it. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3025c38027 |
MUL-6019: fix(codex): add MULTICA_CODEX_FIRST_TURN_TIMEOUT to raise the first-turn ceiling (#6753)
* fix(codex): add MULTICA_CODEX_FIRST_TURN_TIMEOUT to raise the first-turn ceiling The first-turn no-progress watchdog caps at the 60s default that the configured semantic-inactivity timeout can only shrink, never raise. An app-server that is legitimately slow to its first event (heavy MCP boot, cold model catalog) is killed at 60s with no way to extend it. Add a dedicated MULTICA_CODEX_FIRST_TURN_TIMEOUT knob (default 0 = unset) threaded through ExecOptions to codexFirstTurnNoProgressTimeout, which now honors an explicit value upward and is otherwise byte-identical. Default behaviour and the clamp contract are preserved. Supersedes #5959. Refs #3262. * fix(codex): pin and document the first-turn/semantic timer interaction The MULTICA_CODEX_FIRST_TURN_TIMEOUT override only raises the first-turn watchdog, not the semantic-inactivity timer that the same first status:running arms concurrently. An override above the semantic timeout is therefore truncated to it, and the resulting failure is reclassified as semantic inactivity — which also skips the #3291 model-catalog startup retry. The prior docs, comment, and test wording implied the override "wins outright / unbounded above", hiding this real upper bound and the configuration-dependent loss of automatic recovery. - Warn at config load when MULTICA_CODEX_FIRST_TURN_TIMEOUT exceeds the semantic timeout, naming both the truncation and the disabled startup retry. - State the effective bound (semantic timeout + overall execution timeout) in all four environment-variables*.mdx pages, and that both must be raised together when the desired first-turn wait exceeds the semantic limit. - Narrow the "wins outright / unbounded above" resolver comment and test name to describe only the first-turn watchdog ceiling. - Add a runtime regression test that drives the real run loop with an override above the semantic timeout and asserts the semantic timer wins the race and the startup retry is skipped; add a LoadConfig empty/explicit/zero test (this knob is environment-only, no Overrides parity). Addresses review on #6753. Refs #3262 / #5959 / #3291. * fix(codex): warn at equal first-turn/semantic timeouts, require strictly greater multica-eve's re-review on #6753: the warning fired only when the first-turn timeout was strictly greater than the semantic timeout, and told operators to raise the semantic timeout "to match". But the semantic timer is armed first (codex.go resets it before creating the first-turn timer), so at equal durations the semantic deadline is reached first and Go's select does not deterministically favour the first-turn branch — the failure can still be classified as semantic inactivity and skip the #3291 startup retry. An operator following the old guidance to equal values got no warning and silently lost the retry. - Warn when first-turn >= semantic (was >), and tell operators to set the semantic timeout strictly above the first-turn timeout (with margin), not "to match". - Update all four environment-variables*.mdx locales accordingly. - Add TestLoadConfig_CodexFirstTurnTimeoutEqualToSemanticWarns pinning the equal-duration warning and the strictly-greater no-warning boundary. Addresses review on #6753. Refs #3262 / #3291. |
||
|
|
b5a48eca13 |
fix(agent tasks): prevent duplicate self-assignment work (MUL-6168) (#6956)
* fix(agent tasks): avoid duplicate self-assignment runs Co-authored-by: multica-agent <[email protected]> * fix(agent tasks): address duplicate-run review Co-authored-by: multica-agent <[email protected]> * fix(agent tasks): resolve sibling-run nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e3ec3f8b57 |
MUL-6162 docs: sync landing copy and docs for the DeepSeek Harness runtime (#6940)
Completes the user-facing surface for the DeepSeek Harness runtime merged in #6923: landing copy (21 -> 22 tools), README, CLI_AND_DAEMON, SELF_HOSTING, provider comparison and install docs across en/zh/ja/ko, plus the provider logo. Review follow-ups folded in: the mark is inlined so currentColor tracks the app theme (an <img>-loaded SVG pinned it to black in dark mode), the landing enumerations and environment-variable docs are complete in all four locales, and the new rows sit in their tables' existing order. |
||
|
|
698013cedb |
MUL-5771 feat(daemon): allow configuring workspaces root (#6830)
* feat(daemon): allow configuring workspaces root * fix(daemon): align disk usage workspace roots |
||
|
|
09efbe597d |
docs: explain the two local-directory execution modes (MUL-5707) (#6913)
The mode reference existed but three things had drifted from the product: - The docs only ever named `in_place` / `worktree`, while Desktop labels the same choice Direct / Parallel. A user reading either surface could not tell they were the same decision. Both names now appear on the mode headings and in the section intro. - The mode is now chosen while creating the project, and Desktop preselects Parallel for a git repository the runtime can isolate. The add-a-directory steps still described the old add-then-edit path only, so the preselection rule — and the fact that existing resources keep their saved mode — is written down where someone deciding will actually read it. - The gate was described as a daemon version floor. #6904 made it a declared capability, precisely because a dev build carries a version string that looks new enough while having none of the implementation. The prose said "too old" / "below the minimum" in all four locales. Also points `waiting_local_directory` in troubleshooting at Parallel mode — someone landing there because a task is queued should know the queue is a property of Direct mode, not of local directories — and documents `--execution-mode` in the CLI reference table. All four locales updated. MDX renders and every cross-page anchor resolves (checked against the running docs site). Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6db6b235ba |
MUL-6126: fix(runtime): make private runtimes owner-only in the API and CLI too (#6905)
* MUL-6126: fix(runtime): make private runtimes owner-only in the API and CLI too The web UI has always disabled another member's private runtime in every runtime picker, but canUseRuntimeForAgent let a workspace owner/admin bind an agent to it anyway, so the same action succeeded over the API and CLI and failed in the UI (#6898). Unify on the stricter rule the clients already show: a private runtime is usable only by its owner, a public one by anyone in the workspace, and workspace role plays no part. A private runtime is someone's own machine — running an agent there spends their credentials and their files, which is not an administrative decision. Admins keep canEditRuntime, so opening a runtime up stays possible by flipping it to public, an explicit and visible act instead of a silent bypass. Drops the role branch from canUseRuntimeForAgent, which every bind path already shares (create, update, builder create/switch, Mika, onboarding), and retires the "or a workspace admin" half of the 403s and the matching UI copy and docs in all four locales. On the client the inspector runtime picker now calls the shared isRuntimeUsableForUser instead of re-deriving the rule inline, so there is one predicate per side and they agree. Existing agents already bound to another member's private runtime keep running; only new binds and re-binds are refused. An ownerless private runtime is now refused up front rather than at task claim, where it already failed for want of an owner to mint the agent token (MUL-3292). Co-authored-by: multica-agent <[email protected]> * MUL-6126: test(server): own the integration fixture runtime The router integration fixture registered its runtime with no owner_id, so it relied on the workspace-owner override that this branch removes: every agent these tests create through POST /api/agents now 403s on a private, ownerless runtime. Give the fixture runtime to the fixture user, matching the handler test fixture and every runtime a real daemon registers with a member credential. The gate is what changed; ownerless was never the shape under test. Co-authored-by: multica-agent <[email protected]> * MUL-6126: fix(runtime): let only the owner share a runtime, and refuse ownerless binds Two holes in the owner-only rule this branch introduced, both found in review. canEditRuntime still let a workspace admin flip someone else's runtime to public, which is the old override with one extra request in front of it: the admin publishes the member's machine to the whole workspace, binds, and can flip it back, with no owner consent anywhere in the loop. Visibility now has its own owner-only gate (canSetRuntimeVisibility) while canEditRuntime keeps covering rename and delete, which hand out nobody's credentials. A no-op resubmit of the unchanged value is still tolerated so an admin renaming a runtime through a PATCH-as-PUT client doesn't start 403ing. The runtime detail page follows: the Private/Public editor is the owner's, everyone else gets the read-only chip, whose tooltip now speaks in the third person because admins read it too. The bind gate also let an ownerless runtime through whenever it was public. Nothing can run there — task claim needs an owner to mint the agent's task token and cancels the task without one (MUL-3292) — so binding produced agents that failed at their first task. Both predicates now check for an owner first, server and client alike. Docs, UI copy and the RuntimeVisibility type comment drop the last claims that an admin may bind or share a private runtime. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
31645cc510 |
[MUL-6125] Ship Private Skill Plugin developer loop (#6900)
* [MUL-6125] Ship private Skill Plugin developer loop Co-authored-by: multica-agent <[email protected]> * [MUL-6125] Address Private Plugin review findings Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c9727e6b2b |
feat(daemon): worktree mode for local_directory resources (MUL-5707) (#6759)
* feat(daemon): worktree mode for local_directory resources (MUL-5707) Local directory resources serialise every task on a per-path mutex, so two tasks on one folder queue behind each other. This adds an opt-in worktree mode: each task gets its own git worktree of the user's repo inside the daemon's env root, so tasks run concurrently and deliver their work as a branch instead of editing the user's working copy. Opt in per resource with execution_mode=worktree (default stays in_place, so existing resources are unchanged). Requires a git repo with at least one commit; a non-git directory fails with an actionable message rather than silently falling back to serial execution. Design points that make the mode safe to use: - The agent starts from what the user sees, not HEAD. Tracked edits are captured with `git stash create` (which writes a commit object without touching the index, working tree, or stash list) and replayed into the worktree; untracked non-ignored files are copied, bounded at 2000 files / 200MB with the skipped count logged at warn level. - That replayed state is committed as a baseline, so "did this task change anything?" has an exact answer and `git diff <baseline>..<branch>` is precisely the agent's work. - Nothing is silently discarded: leftovers are committed to the branch before the worktree is removed, including when the task fails partway. - A task that changes nothing leaves no branch behind. - Sidecars stay out of the branch by running the existing CleanupRuntimeConfig + CleanupSidecars pass over the worktree before finalizing. The .git/info/exclude approach repocache uses is not usable here: a linked worktree resolves info/exclude to the user's own common git dir, so it would change what `git status` hides in their checkout. - Worktree-mode env roots do not get the local_directory GC exemption, so they age out normally instead of accumulating one env root per task. - `git worktree prune` on create self-heals registrations orphaned by a crashed daemon. Adds --execution-mode to `project resource add/update`, and updates the docs and the projects-and-resources built-in skill. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close data-loss and isolation gaps in worktree mode (MUL-5707) Review follow-up on #6759. Six issues, all on failure or skew paths. 1. Preserve work when the final commit fails. Finalize logged a warning and then ran `git worktree remove --force`, destroying the agent's changes. commit.gpgSign with no key available to the daemon triggers this, and --no-verify does not disable signing. Finalize now returns an error and deliberately keeps the worktree — it stays registered, so `git worktree list` points at the work — and the daemon fails the task instead of reporting success with a branch that is missing the changes. 2. Fail closed on an unsupported execution_mode. Treating an unknown mode as in_place was wrong: execution_mode is how a user asks for ISOLATION, so the "safe" fallback would edit the working copy they asked us to stay out of. An unrecognised mode now fails the task naming the version skew. 3. Keep Codex conversations alive across turns. The per-issue session store was keyed on LocalWorkDir != "", so worktree tasks — which also never reuse an env root — got a fresh empty CODEX_HOME per turn and silently lost the thread. Covered by a two-turn regression test that fails without the fix. 4. Fail closed on an incomplete snapshot. A failed stash create/apply, a failed untracked copy, or hitting the copy bounds all degraded to "agent sees HEAD" with only a daemon log line. A half-replayed tree looks like a working checkout, so nothing downstream questions it while the agent reads different code than the user has. All of these now abort the prepare and tear the partial worktree back down. 5. Do not snapshot a directory mid-mutation. The same real path can be attached to another project as an in_place resource; snapshotting under a running in_place task captured a half-written tree plus that task's in-flight .agent_context/.multica sidecars, which then reached the branch and could feed the agent another issue's brief. Worktree prepare now takes the per-path mutex for the snapshot alone (released before the run, so tasks still overlap), and the untracked replay skips Multica's own sidecars. 6. Stop the test helper from masking regressions. gitRun skipped on any git failure, including post-finalize assertions, so a lost branch could report as a skip. Git availability is now checked once at repo setup; every later git failure is fatal. Also rebased onto main, keeping both MulticaConfigRoot and LocalWorktree in the Environment initialiser. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close sidecar-commit and wait-visibility gaps in worktree mode; gate worktree saves on daemon version (MUL-5707) - runTask: register the sidecar-cleanup defer immediately after the Finalize defer. Previously two early returns (temp-dir setup, StartTask failure) sat between them, and on those paths Finalize auto-committed the sidecars Prepare had just written — delivering a branch whose only content is Multica's own runtime files (in_place: leaving them in the user's tree). - runTask: give the worktree snapshot wait the same plumbing as the in-place acquire — waiting_local_directory status, resourceWaitTasks accounting, and a cancellation poller. The holder can be an hours-long in_place task; the wait was invisible and a user cancel went unnoticed until the 5-minute prepare timeout. - isMulticaSidecarPath: match sidecar dirs as whole path segments at any depth, not only at the repo root. An in_place resource pointed at a SUBDIRECTORY leaves its sidecars below that subdirectory; replaying them committed another issue's brief into the delivered branch. - handler: refuse saving execution_mode=worktree while the owning daemon's registered cli_version is below MinLocalWorktreeCLIVersion (422, daemon_version_unsupported). An old daemon json-skips the field and runs tasks IN PLACE — the daemon-side unknown-mode refusal cannot protect a daemon that predates the field, so the server has to. Residual (documented): a daemon downgraded after the save needs a claim-time gate. - copyUntrackedFiles: read the -z listing raw instead of TrimSpace-ing it, which corrupted filenames beginning/ending with whitespace into paths that silently vanished from the replay. - docs + skill: describe the save-time version gate; drop the stale claim that old runtimes fail the task themselves. Co-authored-by: multica-agent <[email protected]> * feat(views): UI switch for local_directory execution mode (MUL-5707) Worktree mode shipped CLI/API-only, which for its target user — someone running several tasks at once from Desktop — means it does not exist. This adds the switch to the resources panel. The choice is framed by what the user gets back, not by the mode identifiers: "Edit this folder directly" (one task at a time, changes land in your working copy) vs "Run in parallel, isolated" (tasks run at the same time and each hands back an agent/… branch). `in_place` / `worktree` stay visible as a secondary hint for anyone cross-referencing the CLI or docs. Two entry points, matching where the decision is actually made: - The add-a-folder flow now asks for the mode after the folder is picked and validated, instead of creating the resource and leaving the mode to be discovered later. - Every local_directory row has a mode control. It is deliberately NOT gated on being the owning machine — switching the mode only rewrites a field, so it works from the web app too, unlike rename or the folder picker. Both blocked states are handled where the user chooses, not after they commit: - Non-git folder: the desktop validator now reports whether the path is inside a git working tree (walking up for `.git`, so a subdirectory of a repo counts), and the option is disabled with that reason. Unknown — an older desktop build, or an existing row — stays permissive; the daemon re-checks authoritatively. - Daemon too old: the version floor is mirrored from the server as MIN_LOCAL_WORKTREE_CLI_VERSION and checked against the runtime for the resource's OWN daemon, not the machine the browser happens to be on. A server rejection is rendered inline in the dialog rather than as a toast, so the dialog stays actionable. A "Parallel" badge on the row is always visible (not hover-only): without it there is no way to tell whether tasks on a folder edit it directly or hand back a branch, which is the first thing anyone asks when a task queues. Also adds `execution_mode` to LocalDirectoryResourceRef — the field existed server-side but had no TypeScript counterpart — and brings the zh/ja/ko project-resources docs in line with the English ones, which were the only locale updated when the mode landed. Co-authored-by: multica-agent <[email protected]> * fix(daemon,handler): close worktree isolation and delivery gaps (MUL-5707) Second review round. Five must-fix items, all on paths where the mode's promises could still break. 1. A failed sidecar cleanup no longer ships Multica's own files as the user's branch. The cleanup errors were logged and Finalize then ran `git add -A`, so a permission/disk/lock failure could deliver a branch whose entire content was the runtime brief and issue context. Worktree mode now escalates a cleanup failure to an abort: nothing is committed, the worktree is kept for inspection, and the task fails with the preserved path. In place it stays a warning — no commit, no branch, and the leftovers are visible in the user's own tree. 2. The untracked replay no longer drops files silently. `Lstat` failures and symlinks were `continue`d without counting, so the caller read a partial snapshot as complete. Both now count as skipped, which the existing fail-closed check turns into a refused prepare. Non-content entries (sockets, FIFOs, devices) stay uncounted deliberately. 3. Prepare rolls back a worktree it created. Any later failure — context files, provider home, MCP config — returned without an Environment, so nothing downstream knew a worktree existed, leaving a registration and a branch no task ever ran in. A deferred Discard now runs unless Prepare reached its successful return. 4. The daemon-version gate moved to where it can actually hold. Save-time validation cannot see a machine downgraded afterwards, and an old daemon json-skips execution_mode entirely — it would run the task in place, editing the working copy the user asked to isolate. Claims are now checked against the runtime that is actually claiming, and a blocked task is cancelled with a reason rather than left to be redelivered to the same too-old runtime forever. 5. branch_name is delivered end to end. The daemon sent it, but TaskCompleteRequest had no field so it vanished at decode, and the failure path never sent it at all. It now has a column, travels on both terminal callbacks, and renders as a copyable row in Run details. The failure path matters most: worktree mode commits the agent's leftovers before tearing the worktree down, so a run that died partway still has work worth finding. Docs (all four locales) and the projects-and-resources skill are updated for the claim-time gate and the branch surface. Co-authored-by: multica-agent <[email protected]> * fix(handler,daemon): migration renumber, cancel reasons, branch on every terminal path (MUL-5707) Third review round. 1. Rebased onto main and renumbered the migration to 285 — main had taken 273 and is now at 284. sqlc regenerated on top of the rebased queries; the conflicts were confined to generated files. 2. The claim gate now persists why it cancelled. It called CancelTask, which writes only status='cancelled', and returned the reason as a 4xx that reaches the daemon's log and nothing else — batch claim drops the task from its response entirely. A run refused for a durable reason the user has to act on cannot present as a bare "cancelled", so this adds CancelAgentTaskWithReason and records error + failure_reason on the row. 3. branch_name now survives every terminal path, not just the two direct callbacks. Three were dropping it: - the context-exhausted complete→fail reroute did not copy it; - the daemon's complete→fail fallback (server rejected the complete, agent had succeeded) did not set it; - both cancellation paths discarded the result wholesale, even though the worktree is finalized — and its work committed — before the daemon learns of the cancel. The branch now rides the cancel-ack and is recorded by SetAgentTaskBranchName, which never overwrites a name a terminal callback already wrote. 4. Rollout/rollback compatibility. Isolation is enforced by two server-side gates, so a daemon downgraded at any point after the resource was saved is caught. The gap they cannot close is rolling the SERVER back past this change while sub-floor daemons are still connected: an old server has no gate and an old daemon ignores execution_mode. That constraint is now documented where an operator will meet it (the migration and the docs), and the version floor is pinned across server and frontend by test so the two gates cannot silently drift apart. Co-authored-by: multica-agent <[email protected]> * fix(server,views): route claim-gate cancels through TaskService, make cancel reasons visible, harden cancel-ack branch delivery (MUL-5707) Closes the fourth-round review findings on worktree mode's cancellation chain: - Claim gate: cancel through a new TaskService.CancelTaskWithReason instead of a raw CancelAgentTaskWithReason query, so a gate-cancelled task gets the full cancellation side effects — audit capture, chat settle, agent status reconcile, task:cancelled broadcast, NotifyTaskFinished. Added REAL claim integration tests: singular claim (422 + row cancelled with reason), batch claim (task omitted + row cancelled with reason), and a current-runtime control. - Cancel reasons are now user-visible: execution log rows, agent activity rows, and the transcript dialog label a SERVER-cancelled task (persisted error/failure_reason) like a failed one, with the actionable text on hover and a Reason row in Run details. User-initiated cancels stay a plain "Cancelled". New cancelReasonLabel helper + local_directory_error label; details_reason locale key in en/zh-Hans/ja/ko; widened AgentTask.failure_reason to the open string the wire actually carries. - Cancel-ack is now a terminal delivery: the daemon retries it on transient errors (same schedule as complete/fail), the server fails LOUD (5xx) when persisting the branch/error fails instead of warn-and-200, and re-broadcasts task:cancelled after a successful write so clients that already refetched a branchless row pick up the delivery. Replays are idempotent (never overwrite). - Cancel + Finalize-abort no longer swallows the preserved-worktree pointer: the finalize error is wrapped in worktreePreservedError (joined onto an existing run error, never displacing it), extracted on the cancel path, and delivered via the ack into agent_task_queue.error through the new SetAgentTaskErrorIfEmpty query — never overwriting a reason recorded by a fail callback or the claim gate. Other run errors on a cancelled run stay discarded as before: they are expected noise. - Docs: the self-hosted rollback constraint callout now exists in zh/ja/ko, not just English. Co-authored-by: multica-agent <[email protected]> * fix(handler): guard cancel-ack writes to cancelled rows; requeue when the claim gate's cancel fails (MUL-5707) Fifth-round review fixes: - SetAgentTaskBranchName / SetAgentTaskErrorIfEmpty now carry a status='cancelled' CAS. The daemon acks on EVERY terminal status it observes, so a late or replayed ack from a stale run could previously backfill its branch or preserved-path error into a completed/failed row whose own callback — the authoritative channel — recorded nothing, without any event invalidating the change. A CAS-refused write is a deliberate no-op (still 200: nothing to retry), and the rebroadcast stays guarded by the same status check. Regression tests: a late ack against a completed row with empty fields and against a failed row leaves both untouched. - When the worktree claim gate's CancelTaskWithReason fails, the claimed row no longer strands in dispatched (daemon refused, nothing to release it but stale reclaim). The gate requeues via RequeueTaskAfterClaimFailure — the next claim re-runs the gate and retries the cancel — and reports 5xx so the singular daemon reads a transient server problem, not a claim refusal; the batch path omits the task as before, now with the row requeued. Fault injection tests (row-scoped trigger raising on the cancel UPDATE while letting the requeue through) cover singular and batch. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8060aa7277 |
MUL-5947: feat(dingtalk): route groups to different agents (#6660)
Route each DingTalk group discovered by one Stream robot to its own agent, with a revision-fenced cutover so a reassigned group never inherits the previous agent's session, transcript, or pending outbound card. Co-authored-by: mxbao063-bao <[email protected]> |
||
|
|
e4ebd41def |
fix(daemon): make the daemon log path discoverable (#6884)
Debugging a daemon meant guessing which daemon.log was live. In #6038 an external reporter followed the docs to ~/.multica/daemon.log, which had been stale for 17 hours (a leftover default-profile daemon) but read perfectly — the worst kind of wrong file. He only found the real log by ignoring the instructions and digging through the directory. Two causes, both in this change: - The docs only described the default profile's path. The real layout is ProfileDir(profile)/daemon.log, so a named profile — what a Desktop install normally runs on — logs to ~/.multica/profiles/<name>/. Document both, along with daemon.pid and daemon.err.log which sit beside it. - `multica daemon logs` revealed the resolved path only on the file-not-found error path; the success path tailed silently. It now prints the absolute path and profile label it resolved. That goes to stderr, before the tail starts, so it also shows under -f and a pipe or redirect still carries log content only. Resolution is factored into daemonLogSourcePath, which fails loudly rather than returning a bare "daemon.log" that would resolve against the caller's cwd — a printed path has to be one you can actually open. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
147cd8d84f |
feat(skills): update imported skills from their source (#6847)
A skill imported from GitHub, skills.sh, or ClawHub can now be re-downloaded
in place via POST /api/skills/{id}/refresh, the Update button on the skill
page / list row menu, or 'multica skill refresh <id>'. The skill id,
created_by, created_at, labels, and agent_skill bindings are preserved, so
agents keep the skill without re-adding it; name (upstream renames are
adopted, 409 on collision), description, SKILL.md, files, and config.origin
are replaced. Permission follows canManageSkill (creator or workspace
owner/admin); non-refreshable origins (manual, archive, runtime_local)
return 422. Custom config keys are preserved by merging only the origin key.
Co-authored-by: Guillaume Sayer <[email protected]>
|
||
|
|
254a02a795 |
MUL-5799: feat(daemon): allow relocating per-task temp dirs via MULTICA_AGENT_TEMP_BASE (#6481)
* feat(daemon): allow relocating per-task temp dirs via MULTICA_AGENT_TEMP_BASE (#6475) On Linux/macOS the private per-task temp dir was always created under /tmp and the daemon's own TMPDIR was ignored, so a self-hosted operator could not move agent temp data to a larger disk. When MULTICA_AGENT_TEMP_BASE is set, it must be an absolute path to an existing directory and is used as the MkdirTemp base; invalid values fail task startup with a clear error instead of silently falling back. Unset keeps the previous platform default byte-for-byte. Random naming, 0700 permissions, TMPDIR/TMP/TEMP injection, the agent custom_env blocklist, and task-exit cleanup are unchanged. Windows is untouched. Co-authored-by: multica-agent <[email protected]> * feat(daemon): validate MULTICA_AGENT_TEMP_BASE is writable (#6475) An existing directory the daemon cannot write to still failed task startup, but as a bare mkdir error from deeper in the run. Probe writability while resolving the base so the failure names the variable the operator set. The probe runs only when the variable is set, leaving the default /tmp path byte-for-byte unchanged. Cover the contract at the level operators experience it: an unusable base fails runTask with a message naming the variable, and the agent never starts. Also restore the environment in the unset subtest, which previously called os.Unsetenv without a matching restore and could leak into later tests in the package. Co-authored-by: multica-agent <[email protected]> * chore: remove accidentally committed commitmsg.txt (#6475) A commit message draft was swept into the previous commit by git add -A. It is not part of the product surface and does not belong in the tree. Co-authored-by: multica-agent <[email protected]> * docs: scope the sun_path limit to Unix-socket platforms (#6475) The 108/104 byte sun_path ceiling only constrains platforms that use Unix domain sockets; the previous phrasing read as a cross-platform rule. Co-authored-by: multica-agent <[email protected]> * fix(daemon): address temp base review feedback (#6475) Keep MULTICA_AGENT_TEMP_BASE Unix-only, use the real task directory creation as the sole writability check, and wrap configured-base failures with the variable name. Remove the diagnostic probe and its possible residue path. Add the missing localized documentation and pin private task directory permissions at 0700 in tests. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
5e569fa38a |
fix(workspace): derive new issue prefixes from the slug, editable at create (#6815)
Every workspace named only in Chinese (or Japanese, Korean, emoji…) was
created with the issue prefix "WS": the server derived it from the
workspace NAME by stripping non-ASCII letters, and a CJK name strips to
nothing. Not a collision risk — a certainty, for every such team, with no
way to change it in the create flow.
New workspaces now derive their default prefix from the slug, which the
same form already forces the user to pick in ASCII, and the onboarding
field is editable instead of a read-only preview. The client preview and
the server derivation are now the same algorithm, so the create screen
stops promising an identifier the user won't get.
Existing workspaces are untouched by design: no backfill, and the
read-time fallback for rows with an empty prefix keeps the old
name-based derivation (now legacyIssuePrefixFromName, frozen and
test-guarded). Identifiers are computed at read time, so moving that
path would have rewritten every historical issue identifier in those
workspaces.
Also validates issue_prefix as ^[A-Z0-9]{1,10}$ on create and update.
Settings guarded this client-side, but the API took anything — CJK text
or a 100-character string went straight into the column.
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
3ef345b658 |
fix(cli): allow login with stale daemon port env (#6801)
* fix(cli): allow login with stale daemon port env Co-authored-by: multica-agent <[email protected]> * fix(cli): address daemon port review findings Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |