Add a Manus plugin covering API v2 - #418
TheGreatAxios merged 77 commits into
Conversation
TheGreatAxios
commented
Aug 27, 2026
- /plugins Manus tile with API-key connect (x-manus-api-key)
- custom @corbits/manus-tools covering Manus API v2 (tasks, files, skills, projects, agents, webhooks, usage, connectors, browser, website)
- create_slides is the passing demo (pptx, waits for newest agent status, errors on timeout/waiting)
- unconnected degrades at tool time; assistant pins the package but does not require a Manus binding at launch
- Linear: https://linear.app/abklabs/issue/CL-7087/add-a-manus-plugin-with-a-custom-tool-package-covering-api-v2
TheGreatAxios
left a comment
There was a problem hiding this comment.
gaasbot — risk counsel on #418 (Manus API-key plugin)
Advice, not an approve. Recommend do not merge as-is. Credential handling is fine; the assistant-pinned tool dump is not.
Blocks merge
1. Assistant-pinned full v2 admin surface, including unconstrained webhooks.
workflows/assistant/src/index.ts:53 pins @corbits/manus-tools on every Myra deploy. packages/manus-tools/src/tool.ts:56-395 then exposes 32 RPC wrappers + create_slides (33 tools). Granola/Exa/Linear/GitHub are not assistant-pinned; they ride specific workflows. Manus is treated like a platform primitive.
Once a tenant pastes a key for “slide decks”, the agent can:
webhook_create(tool.ts:256-261) — arbitraryurl, no allowlist. Prompt injection → Manus POSTs task events (prompts, file URLs, share links) to an attacker. That is an exfil primitive, not a slides helper.webhook_list/webhook_delete/webhook_pubkey(tool.ts:264-286)browser_online(tool.ts:341-347) — lists live Manus browser clientswebsite_publish(tool.ts:360-370) plusshare_visibility: publicontask_create/task_update(tool.ts:75,tool.ts:112) — publish tenant work to the open webtask_delete/file_delete/agent_update/ team usage logs
Gallery copy does not match the grant:
packages/plugins-ui/src/plugin-meta.ts:73-74— “run Manus tasks and retrieve files — including slide decks”packages/connections/src/registry.ts:285-286— “run tasks and produce files — including slide decks”
Connecting for slides silently grants webhook/browser/publish/delete. Cut the assistant-pinned set to the demo (create_slides + task/file read) or stop pinning this package on wf_assistant until product explicitly wants Myra to administer Manus. Leave webhook/browser/website_publish off the default bundle.
2. Copy understates the grant (competitor-desktop names: none found).
origin/main...HEAD has no Cursor / Claude Desktop / ChatGPT / Copilot copy. The copy risk is the understatement above, not competitor naming.
Ships with a note (credential path is sound)
- No secrets in source. Tests use fakes (
manus_real_key,manus_key). Client asserts it does not attach auth (packages/manus-tools/src/client.test.ts:41-43). - Header injection is origin-pinned.
packages/credential-providers/src/http-x-manus-api-key-provider.ts:64-93pins origin, re-reads material per call,headers.set("x-manus-api-key", secret)(overwrites caller),redirect: "manual". Sidecar registers the plugin (apps/sidecar/src/workflow-substrate-factory/index.ts). Persist writesapiBaseUrl: "https://api.manus.ai"(packages/connections/src/persist-credential.ts:108-114) so the pin has an origin. - Unconnected degrade is correct.
tool.ts:401-418+ tests: missing capability orresolve("manus")throw →"Manus is not connected for this user."AssistantcredentialBindingsstay empty (workflows/assistant/test/definition.test.ts:153-158). Matches Granola. Keep that; just don’t pin 33 tools to get it.
Filed for later (not merge-blocking if the surface is cut)
persist-credential.ts:108special-casesid === "manus"instead of a descriptor origin field — will rot the first time the host moves.- 33 tool defs on every unconnected assistant turn (prompt cost / accidental calls).
create_slidespolls up to 5 minutes (DEFAULT_SLIDE_MAX_POLLS = 150× 2s) holding the tool call.file_uploadreturns a presignedupload_url(typically offapi.manus.ai); origin-pin will refuse a PUT through the mediated fetch.
Unraised miss
The team copied Granola’s degrade pattern and then did the opposite of Granola’s pinning pattern. Granola: 2 read tools, not on Myra, connected from Settings when a workflow needs notes. This PR: entire OpenAPI v2 dump, pinned on Myra, Settings copy talks about slides. The dangerous tools are not a future expansion — they ship on the first connect.
Verdict: do not ship until webhook/browser/website_publish (and ideally delete/usage-team) are off the assistant-pinned bundle. Credential plugin + probe + origin pin + unconnected degrade can land with a thin tool set.
TheGreatAxios
left a comment
There was a problem hiding this comment.
neckbeard
Neckbeard Review — PR 418 Manus plugin (hygiene only; not an approve)
Actually, I have reviewed this TypeScript and am ready to provide maximally annoying, pedantic nitpicks while completely missing whether the plugin works. Everything should be rewritten in Rust. Also, have you considered blockchain?
I did not implement. I did not --approve. I did not look at #417. LICENSE text is byte-identical to vendor/intx/LICENSE (good, stop celebrating). Pins for @intx/agent / @intx/types are the same 0.3.0 the sibling *-tools packages already use (boring). exactOptionalPropertyTypes construction in optionalCreateFields / extractOutputFiles / createSlideDeck actually omits keys instead of stuffing undefined (fine, I guess, if you insist on a garbage-collected language).
Peak Neckbeard Issues (4 found)
-
packages/manus-tools/README.md:3-7— Actually, this is not a "typed client covering tasks, files, skills, projects, agents, webhooks, usage, connectors, browser, and website endpoints."packages/manus-tools/src/client.tstypescreateTaskandlistTaskMessages. Everything else ismanusRequest→Record<string, unknown>. A README that lists ten domains and a client that types two is a docs lie. Real engineers would generate the entire OpenAPI surface into rustc-checked newtypes with zero-copy serde. Also have you considered protobuf. -
packages/connections/src/persist-credential.ts:108-113— Well technically, you special-casedargs.descriptor.id === "manus"and inlined"https://api.manus.ai"instead of putting the origin onConnectorDescriptor. The same literal also lives atpackages/manus-tools/src/client.ts:12,packages/connections/src/probes.ts:128, andpackages/credential-providers/src/http-x-manus-api-key-provider.test.ts:8. GitHub persist does not get this treatment (persist-credential.test.ts:76). In production at scale this is an anti-pattern. Should be a const generic in Rust with the origin in the type system. Also blockchain the origin so it is immutable. -
packages/credential-providers/src/http-x-manus-api-key-provider.ts:26-29— Actually, you copy-pastedFetchLikeinstead of importing it from./http-x-api-key-providerthe waymcp-streamable-http-provider.ts:24already does. ThreeFetchLiketypes in one package is a code smell. Real engineers would use a trait object. Have you considered Zig. -
packages/manus-tools/src/tool.ts:385website_ckpts— Well technically,@corbits/manus-tools/mn:website_list_checkpointsis still under the 64-char OpenAI cap the file header (tool.ts:671-673) cites as the reason to keep the local segment short.ckptsis an abbreviation of an abbreviation oflistCheckpoints. Meanwhilewebhook_pubkeyandtask_list_msgsandusage_team_stateach invent their own clipping algorithm. This would never happen in Haskell where the name is the type.
Unbearable Issues (5 found)
-
packages/connections/src/probes.ts:2— The file-header still says the live probes are "Granola, Exa, ScrapeCreators, Linear, and GitHub". You addedtestManusCredentialat line 124. The comment now lies. YAGNI, except you definitely needed to update the sentence you were standing in. -
workflows/assistant/src/index.ts:36— "the remaining four are the manager-tools bundles" whileASSISTANT_TOOL_PACKAGE_PINSis ten entries and the new one is Manus, which is not a manager-tools bundle. The test atworkflows/assistant/test/definition.test.ts:115repeats the same lie in its title. Google would never ship a comment that cannot count. -
docs/credential-wiring.md:60— "All four plugins mirror the same origin-pinning…" The sidecar registry atapps/sidecar/src/workflow-substrate-factory/index.ts:387-393registers five (builtin http, raw-authorization, x-api-key, x-manus-api-key, andcreateMcpStreamableHttpCredentialProvider). You updated two→four and still forgot MCP. According to a blog post I read, this is an off-by-one. -
packages/manus-tools/src/tool.ts:25JsonSchemaPropertyandpackages/manus-tools/src/client.ts:94jsonHeaders— AGENTS.md naming: acronyms are not words. That isJSONSchemaPropertyandJSONHeaders.HttpXManusApiKey/createHttpXManusApiKeyCredentialProvideralso writeApinotAPI. Matching the siblingHttpXApiKeyis not a defense; it is two wrongs. Real engineers would useHTTPXManusAPIKeyCredentialProviderFactoryFactory. -
IMPLEMENTATION.md:106has a whole "GitHub connect (shipped)" section. This PR ships a Manus plugin and touches none of PRODUCT.md / ARCHITECTURE.md / IMPLEMENTATION.md (zeromanushits). The map is not the territory, but the map also does not mention the new continent. Have you considered generating the docs from a Kubernetes CRD.
Maddening Nitpicks (4 found)
-
packages/manus-tools/package.json:6LGPL-2.1-or-later+LICENSEbyte-identical tovendor/intx/LICENSE— fine,check:licenseswill eat it. Well technically the rootLICENSE.mdis GPLv2-with-AI-Exception and this library is LGPL. I have 15 pages of thoughts on whether a slide-deck poller is a library. Also the SPDX says "or-later" and the file text is 2.1. Pick one. In Rust the license is a compile-time feature flag. -
packages/manus-tools/package.json:23-25pins@intx/agent/@intx/typesat0.3.0(exact) andarktypeatcatalog:(which is^2.2.0in the root catalog). Mixed exact vs caret in the same object. Premature optimization is the root of all evil, but also pin everything to a git SHA and a lock-free hash map. -
packages/manus-tools/tsconfig.json:6"include": ["src"]vs granola/linear"include": ["src", "test"]. Tests live undersrc/, so tsc sees them, but you still forked the include list from the sibling packages you copied the rest of the manifest from. Inconsistent tab—wait, you used spaces. I have opinions. -
packages/credential-providers/README.md:19-30register sample still only constructscreateHttpRawAuthorizationCredentialProvider. You added a third custom plugin and did not update the snippet that claims to show how to register "it". The newhttp-x-manus-api-keysection is prose-only. Docs that almost lie.
Insufferable Details (3 found)
-
packages/manus-tools/src/client.ts:16ManusClientConfig.baseUrl?vs persist'sapiBaseUrlvs probe's full URL. Three spellings of one origin, andasClientConfig(tool.ts:421-424) never threadsbaseUrlat all — it relies onDEFAULT_MANUS_BASE_URLmatching the provider row by social convention. UnderexactOptionalPropertyTypesthis is at least not{ baseUrl: undefined }. Under a borrow checker it would not compile. -
packages/manus-tools/src/tool.ts:676id: "@corbits/manus-tools/mn"—mnis not an acronym, not a word, and notmanus.granola-toolsusesgr,linear-toolsusesli, so you cargo-culted a two-letter clip. Have you consideredm. -
packages/plugins-ui/src/plugin-meta.ts:86manus: Stack— a generic stack glyph for a slide-deck product. Well actually, in my opinion, this needs a custom SDF icon with SIMD rasterization. Also Manus is not inFEATURED_CONNECTOR_IDS(plugin-meta.ts:95-102). I am not saying it should be. I am saying I noticed.
Key Suggestions (All Terrible)
- Rewrite
@corbits/manus-toolsin Rust with a lock-free poller, custom arena, and inline asm forqueryString. - Put
https://api.manus.aion a blockchain so persist/probe/client cannot drift. - Kubernetes operator for the 2s slide poll.
- GraphQL federation mesh instead of
manusRequest. - One
FetchLike. One origin constant. Comments that can count.
Recommendation
Do not --approve from this lane. Hygiene is not broken enough to demand a rewrite, which is disappointing. The LICENSE/pin/exactOptionalPropertyTypes omit-key pattern is copy-paste-correct. The docs-that-lie (README "typed client", probes header, assistant "remaining four", credential-wiring "four plugins", IMPLEMENTATION silence) and the id === "manus" origin special-case are the actual nits. Also, have you considered AI?
P.S. Real engineers would use Haskell anyway.
P.P.S. Actually, real real engineers would use Assembly.
P.P.P.S. Actually actually, real engineers would use Rust.
TheGreatAxios
left a comment
There was a problem hiding this comment.
greybeard · block
Adds a Manus connector, a fourth origin-pinned HTTP credential plugin, and @corbits/manus-tools pinned on every assistant deploy — 32 generic v2 RPC tools plus create_slides, with no credential binding on the workflow.
Pin without bind does not deliver a connected key
workflows/assistant/src/index.ts:53 pins @corbits/manus-tools. 8487d54a then dropped credentialBindings so Myra launch does not MissingCredentialError when Manus is absent (workflows/assistant/test/definition.test.ts:153-158). That instinct is right: Linear's required bind (workflows/morning-brief/src/index.ts:93-105) belongs on a specialist whose job is Linear, not on the always-on assistant.
Empty bindings are the wrong fix. docs/credential-wiring.md:9-34 is explicit: handle declaration → definition credentialBindings → buildCredentialDelivery at launch → env.credentials.resolve. With no binding, resolve throws even when Settings has a Manus key. Tool-time "Manus is not connected" (packages/manus-tools/src/tool.ts:409-418) then fires for the connected tenant too. The README (packages/manus-tools/README.md:20-23) claims Granola-style degrade; Granola degrades behind a present binding, not instead of one.
The owning layer already exists for this constraint: packages/folded-runs/src/launch.ts:385-403 folds tenant MCP bindings in at launch when the package is pinned, and does not fail the launch when none exist. Static interchange.credentials handles (packages/manus-tools/package.json:12-16) need that same optional delivery — restore Linear's bind on Myra and every tenant without Manus cannot launch her.
Do not ship a plugin whose connect path cannot reach the agent that pins it.
Generic endpoint map is not a tool package
packages/manus-tools/src/tool.ts:56-395 (MANUS_ENDPOINTS) plus tool.ts:675-709 turn OpenAPI v2 into ~32 tools on Myra. Sibling packages are first-class jobs: linear_list_recent_issues, web_search, github_activity. Capability growth is grants, not baking an admin console into the assistant (ARCHITECTURE.md:115-121).
The map already leaks first-class tools (task_create / task_send_msg / task_list_msgs special-case message.content at tool.ts:479-543). create_slides is the product. Webhooks, browser, website publish, deletes, team usage are not. Shipping them on wf_assistant is a backward-compat trap: removing tools later is a behavior change.
External side effects have no approval: "ask" on this bundle (zero approval hits under packages/manus-tools). The platform invariant is one human gate per side effect leaving the boundary. A generic RPC map has nowhere honest to hang that — stamp ask on everything, or stamp it on nothing. First-class tools own approval per job.
Cut the assistant-pinned surface to the product jobs (create_slides and the task/file reads it needs). Leave the rest off the bundle, or put them on a specialist workflow with a required bind.
http-x-manus-api-key is a fourth copy, not a new constraint
Cannot reuse http-x-api-key: Manus wants x-manus-api-key, not x-api-key. A distinct plugin key on the provider row is correct.
A distinct plugin module is not. packages/credential-providers/src/http-x-manus-api-key-provider.ts is http-x-api-key-provider.ts with the header string changed — origin pin, readCurrentMaterial, redirect: "manual", local resolveTargetUrl all copied. The constraint is "inject this header name on an origin-pinned handle." That belongs in one factory (createHttpHeaderCredentialProvider({ key, headerName })) registered under whatever key the row names. This is the fourth copy (Bearer, raw authorization, x-api-key, now vendor-named x-manus-api-key). Extract before the fifth.
Not a merge blocker by itself. Do not add the fourth copy as if it were the architecture.
Origin pin: persist must not special-case id === "manus"
packages/connections/src/persist-credential.ts:108-114 hardcodes apiBaseUrl: "https://api.manus.ai" when descriptor.id === "manus". Origin-pin is an invariant of delivery (vendor/intx/db/src/credential-resolution.ts:322-335 fails closed on no_origin). The connector descriptor owns the production origin; persist writes it for every origin-pinned connector. id === "manus" is symptom-chasing — the next custom-header connector repeats it.
The literal is already DEFAULT_MANUS_BASE_URL in packages/manus-tools/src/client.ts:12 and the probe URL in packages/connections/src/probes.ts:128. Put apiOrigin on ConnectorDescriptor and delete the branch.
This is load-bearing if a bind is restored: without origin, delivery fails no_origin. Fix the layer, keep the write.
Verdict
Block. Two merge blockers: (1) pin-without-bind cannot deliver a connected Manus key — optional delivery belongs at launch, MCP fold-in is the precedent; (2) MANUS_ENDPOINTS on wf_assistant is the wrong capability surface and has no approval owner. Provider copy and persist special-case are revise-with-the-bind-fix, not independent ships.
Linear required-bind on Myra is also wrong. Do not restore ASSISTANT_CREDENTIAL_BINDINGS without making unresolved optional. Either generalize launch-time fold-in for pinned packages' declared handles, or keep Manus off the assistant and give it a specialist workflow that can require the bind.
TheGreatAxios
left a comment
There was a problem hiding this comment.
critic
Pinning @corbits/manus-tools on the assistant without a credential binding leaves a connected Manus key inert at tool time (pin with no bind). Unconnected launch must stay fail-open; connected launch must fold the tenant's manus handle the way MCP bindings already fold. Prior slides_format=pdf / stale desc status / timeout-as-success items look fixed in the current branch.
A pin with no matching binding is inert at tool time. Launch already folds MCP bindings when the package is pinned; static handles such as Manus need the same coverage so a connected tenant does not see "not connected" after Settings.
A pin with no matching binding is inert at tool time. Required assistant binds would throw on first chat, so launch now folds CONNECTOR_REGISTRY-fed bindings for pins the tenant already connected, the same way it already folds MCP server bindings.
The probes header still listed four connectors, the assistant pin comment still counted four manager-tools bundles, and credential-wiring said four sidecar plugins while five are registered.
TheGreatAxios
left a comment
There was a problem hiding this comment.
critic — fold-in review only (not an approve). The pin-without-bind hole is still open.
Blocking
Production factory never sees a connected Manus credential.
createPinnedPackageCredentialBindingsFor (apps/hub/src/pinned-package-credential-bindings.ts:44-46) asks @corbits/chat's listConnectedProviders. That lister is the inference catalog:
packages/chat/src/inference-preferences.ts:42-49→listVisibleProviders→model_provider(vendor/intx/db/src/catalog-resolution.ts:74-94)model_provider.pluginis enumanthropic | openai | openai-compatible | google-genai(vendor/intx/db/src/schema/catalog.ts:46-48) —manuscannot even be a row there
Connecting Manus writes a provider + credential row and does not seed the catalog:
persistConnectorCredentialskipsseedCatalogunlessisInferenceProvider(packages/connections/src/persist-credential.ts:161)isInferenceProvider("manus")is false (persist-credential.test.ts:192)- Manus persist test asserts
seedsis empty (persist-credential.test.ts:92-112)
The codebase already documents this exact trap for GET /connections:
This is deliberately NOT
@corbits/chat'slistConnectedProviders… every non-inference connector … reads "not connected" there even with a live, verified credential. (packages/connections/src/workflow-connection-routes.ts:19-24)
Hub already uses the owning check for that route: resolveCredentialRequirement(db, tenantId, { providerName: connectorId, source: "tenant" }, null, null) (apps/hub/src/index.ts:2412-2419).
Trigger: tenant pastes a Manus key in Settings, Myra launches with @corbits/manus-tools pinned and empty credentialBindings (workflows/assistant/src/index.ts:56 + workflows/assistant/test/definition.test.ts:153-158). Factory returns []. deployAtHead concatenates nothing (packages/folded-runs/src/launch.ts:407-423). env.credentials.resolve("manus") still fails "not connected".
Unconnected launch still succeeds (empty extra bindings). The connected path is a no-op.
Unit tests hide this: bindingsForConnectedPins is fed ["manus"] by hand (apps/hub/src/pinned-package-credential-bindings.test.ts:8-16); launch.test.ts mocks the port itself. Nothing exercises createPinnedPackageCredentialBindingsFor against a live non-inference credential.
Should-fix / tests to keep
- Factory (or hub) test:
provider+credentialrow formanus, nomodel_providerrow → emit{ package: "@corbits/manus-tools", handle: "manus", provider: "manus", locator: "tenant" } - Inverse: catalog-connected
anthropiconly + manus-tools pin → no binding - Keep the existing
deployAtHeadtests that fold vs skip vs succeed-empty
Fine (once the lister is the credential-table one)
deployAtHeadconcat + handle skip (launch.ts:414-423) and runtimeConfig (launch.ts:503)- Hub wires the port on every
mcpCredentialBindingsForsite - Chat adapter forwards the optional port
- Handle/provider/locator match interchange (
manus-tools/package.json:13-15) andCONNECTOR_REGISTRY.manus(registry.ts:277-283)
Verdict: do not treat the fold-in as closed until the factory uses resolveCredentialRequirement (or an equivalent provider+credential listing), not listConnectedProviders.
The factory currently lists model catalog providers, so a connected Manus key never emits a binding. These cases pin the owning check: a live Manus connector plus a manus-tools pin binds, while a catalog-only Anthropic connection does not.
listConnectedProviders only sees model catalog rows, so a pasted Manus key never folded a tenant binding at launch. Ask isConnectorConnected instead — the same resolveCredentialRequirement check GET /connections already uses.
TheGreatAxios
left a comment
There was a problem hiding this comment.
critic
Re-review of 63fe2a9e (pinned-package credential fold-in: listConnectedProviders → isConnectorConnected / resolveCredentialRequirement). Prior blocker: factory listed catalog-only providers, so a pasted Manus key never folded.
Verdict: hole is closed. No remaining merge blockers on the lister fix.
Confirmations
-
Factory no longer imports
listConnectedProviders.apps/hub/src/pinned-package-credential-bindings.tstakesIsConnectorConnected(L18–21, L46–47) and asks that port perCONNECTOR_REGISTRYdescriptor thatfeedsToolsa pin (L52–56). The catalog lister is named only as the thing not to use (L10–12). -
Hub composition uses the owning check:
resolveCredentialRequirement(db, tenantId, { providerName: connectorId, source: "tenant" }, null, null)
atapps/hub/src/index.ts:718–725. That path walksprovider+ tenant credentials (vendor/intx/db/src/credential-resolution.ts), not the model catalog.listConnectedProvidersremains catalog-only (packages/chat/src/inference-preferences.ts:42–49) and is no longer on this launch path. -
Same lambda is passed to GET
/connections(index.ts:2417–2421). One binding, no drift. -
Unconnected still launches. Assistant pins
@corbits/manus-tools(workflows/assistant/src/index.ts:42–44, 56) with emptycredentialBindings(workflows/assistant/test/definition.test.ts:153–158). Factory emits a tenant bind only whenisConnectorConnectedis true; otherwisebindingsForConnectedPinsreturns[]. -
Tests would fail a factory-level revert to catalog-only:
createPinnedPackageCredentialBindingsFornow takes the port, notdb.pinned-package-credential-bindings.test.ts:47–66injectsisConnectorConnected— Manus true emits the bind; Anthropic-only (catalog-style) emits none.
File-for-later (not blocking this fix)
- The composition-root lambda body is not unit-tested. Swapping
index.ts:718–725back to wrappinglistConnectedProviderswould not fail the factory tests. listMyraUsableToolPackages(index.ts:2922) still uses catalog-onlylistConnectedProviders. Different surface (inventory listing), not launch fold-in.
Live list-messages bodies send ISO strings on timestamp. Pin that parse path, keep numeric timestamps, and still extract agent_status and attachments.
Live list-messages returns ISO strings. The plugin does not use timestamp for status or files, so both shapes must parse.
Live create_slides 404s with "task not found". Pin retry on that casing while keeping the capital-T envelope.
Live 404s say "task not found". Treat any Manus not_found as retryable during listMessages poll.
Webhook, website publish/update, and task/file delete now declare approval ask so an assistant pin cannot auto-allow those calls. create_slides maps agent_status error to a thrown failure so the tool result is isError.
b2af571 to
62e2bc2
Compare
62e2bc2 to
671e9a7
Compare
TheGreatAxios
left a comment
There was a problem hiding this comment.
critique · request-changes
Credential wiring and origin-pin for x-manus-api-key look sound, and webhook/delete/publish tools are now approval: "ask". The demo path still ships wrong: a live Myra launched before the Manus key is pasted never gets the binding.
- workflows/assistant/src/index.ts:56 — assistant pins @corbits/manus-tools with no required binding. Bindings are folded only inside deployAtHead.
- packages/chat/src/connect-pending.ts:219 — settleConnectedService only dispatchTurns the existing run. Sidecar resolve("manus") then throws; create_slides maps that to MANUS_NOT_CONNECTED.
- Trigger: signup launches Myra → request_connection /plugins?connect=manus → paste key → wake continues → create_slides says not connected until idle reap/relaunch.
Should-fix: create_slides poll budget equals CHAT_TURN_TIMEOUT_MS; stopped-with-no-attachments is still success; several mutating tools stay approval allow.
GitHub will not accept request-changes on the author's own PR; this comment is the review.
…lugin-with-a-custom-tool-package-covering-api # Conflicts: # apps/hub/src/index.ts # bun.lock # packages/folded-runs/src/launch.ts
# Conflicts: # packages/folded-runs/src/launch.ts
TheGreatAxios
left a comment
There was a problem hiding this comment.
PR 418 live-Myra Manus binding fix
Review of HEAD 0ec1033b on cl-7087-add-a-manus-plugin-with-a-custom-tool-package-covering-api.
Focus: the two commits just pushed, not the whole Manus plugin history.
1f92a69aAdd tests for relaunching a live assistant when Manus connects0ec1033bRelaunch live assistants when a pinned tool-package credential connects
Verdict: approve
Persist-only is gone. Hub feedsTools connect calls reconcilePinnedToolPackages. Tests fail if persist-only is restored in the platform or the extracted helper. No functional merge damage to launch.ts, bun.lock, or manus-tools. Authors are Sawyer Cutler <sawyer@dirtroad.dev>.
Persist-only vs relaunch
Persist-only (stamp a pin/binding onto workbench_launch and leave currentRunId) does not remain as the connect path.
The live-run pass mints a fresh run:
async function reconcilePinnedToolPackages(
tenantId: string,
packageNames: readonly string[],
): Promise<{ scanned: number; relaunched: number }> {
const wanted = new Set(packageNames);
const participants = await listLaunchesForTenant(
deps.db,
tenantId,
RECONCILE_SOURCES_LIMIT,
);
let relaunched = 0;
for (const live of participants) {
const pinsWanted = live.binding.foldedBody.toolPackagePins.some((pin) =>
wanted.has(pin.name),
);
if (!pinsWanted) continue;
try {
if (await isBeyondWake(deps.db, live.run)) continue;
if (live.run.address === null || !isRoutable(live.run.address)) {
continue;
}
// ...
await relaunchTerminalRun(live);
relaunched++;relaunchTerminalRun (packages/chat/src/platform-adapter.ts:371) calls launchFoldedRun → deployAtHead, which folds pinnedPackageCredentialBindingsFor (packages/folded-runs/src/launch.ts:467-495). repointBinding then swings currentRunId off the old run.
The pin list on the launch row is still persisted at mint time (empty credentialBindings). That is not persist-only: bindings still fold only at deploy. Connecting Manus no longer stops at settleConnectedService → dispatchTurn on the existing snapshot (packages/chat/src/connect-pending.ts:230).
Idle / not-routable runs are skipped here. That is not a hole for the stated bug: wakeFoldedRun also goes through deployAtHead, so a slept Myra picks up the binding on next wake. The bug is a resident sidecar still serving the pre-connect snapshot; those runs are routable and get relaunched.
Hub feedsTools hook
Yes. API-key connect in packages/connections/src/routes.ts:473 fires onConnected. Hub mounts that as settleServiceConnection (apps/hub/src/index.ts:2112, also oauth/mcp mounts).
Previously the hook returned early unless isInferenceProvider. That early return is gone. After settleConnectedService, Manus (not an inference provider) still reaches the new pass:
if (isInferenceProvider(info.connectorId)) {
void chatPlatform
.reconcileInferenceSources(info.tenantId)
// ...
}
void reconcilePinnedToolPackagesAfterConnect(chatPlatform, info)
.then((result) => {
if (result === undefined) return;
log.info`tool-package connector ${info.connectorId} changed on tenant ${info.tenantId}: re-checked ${String(result.scanned)} live agents, relaunched ${String(result.relaunched)}`;
})Helper (apps/hub/src/connection-live-reconcile.ts:23-30):
CONNECTOR_REGISTRY["manus"].feedsTools→["@corbits/manus-tools"]- calls
chatPlatform.reconcilePinnedToolPackages(tenantId, feedsTools) - inference-only connectors (
feedsTools: []) returnundefinedand do not relaunch
chatPlatform is createHubChatPlatform(...) (apps/hub/src/index.ts:1291) with pinnedPackageCredentialBindingsFor wired (:1300) and reconcilePinnedToolPackages exported (packages/chat/src/platform-adapter.ts:1327-1331).
Tests red-capable?
Yes for the actual persist-only bug.
packages/chat/test/platform-adapter.test.ts (describe at 3379):
ensureAwakeon an already-routable live run does not relaunch (currentRunIdstaysrun_live, no populated tree). Wake /dispatchTurnis not enough.reconcilePinnedToolPackages("ten_1", ["@corbits/manus-tools"])must:{ scanned: 1, relaunched: 1 }currentRunId !== "run_live"- deployed entry
credentialBindings === [MANUS_BINDING] buildCredentialDeliveryreceived that binding
Restoring persist-only (stamp the launch row, leave run_live) fails those assertions. A live run that does not pin the package is left alone (relaunched: 0).
apps/hub/src/connection-live-reconcile.test.ts:
connectorId: "manus"must callreconcilePinnedToolPackages("ten_1", ["@corbits/manus-tools"])and asserts the live registry mappingconnectorId: "anthropic"must not call it
Restoring persist-only inside the helper (return without calling the platform) fails the first test.
Gap (non-blocking): the hub test does not boot createHub. Deleting the void reconcilePinnedToolPackagesAfterConnect(chatPlatform, info) line in index.ts would not turn either new test red. The helper comment says that is intentional. Same posture as inference reconcile. Worth a later grep/wire test; not a reason to block.
Recommended permanent tests (already present; keep them):
- Live Myra pinning
@corbits/manus-toolsis relaunched on connect and the deployed snapshot contains themanusCredentialBinding - A live run that does not pin the package is not relaunched
- Manus
feedsToolsmaps to@corbits/manus-toolsand the hub helper forwards that list - Inference-only connect does not enter the tool-package relaunch
Merge damage (origin/418 recovery + origin/main)
Merges immediately under the fix:
68ce2a63Mergeorigin/main— conflicts:apps/hub/src/index.ts,bun.lock,packages/folded-runs/src/launch.tsed9e4ca9Merge origin/cl-7087 to recover Manus history — conflict:packages/folded-runs/src/launch.ts; net diff was a duplicate import inindex.ts
packages/folded-runs/src/launch.ts: no functional damage. Main's resolveLaunchSources extract is intact; Manus pinnedPackageCredentialBindingsFor folding is intact (:467-495). One extra blank line after resolveLaunchSources { (:285) is a merge artifact, not a behavior change. ed9e4ca9 vs 68ce2a63 has no launch.ts diff (kept the already-merged tree). No conflict markers.
bun.lock: workspace entry @corbits/manus-tools@workspace:packages/manus-tools is present. packages/manus-tools lock version is 0.0.1 while package.json is 0.0.11. That mismatch already existed on d96895cc / b2af5713 (pre-merge). Not introduced by recovering history. Granola-tools lock matches its package.json. File-for-later lock hygiene, not merge breakage.
packages/manus-tools: tree intact (client.ts, tool.ts, tests, LICENSE, interchange.credentials.handle: "manus"). Assistant still pins @corbits/manus-tools@0.0.11 without a required binding (workflows/assistant/src/index.ts:42-56). ed9e4ca9 did not touch this tree.
apps/hub/src/index.ts: ed9e4ca9 duplicated import { createPinnedPackageCredentialBindingsFor } .... 0ec1033b removed the duplicate while adding the reconcile import. Current HEAD has a single import (:362).
Authors
Both reviewed commits:
- Author: Sawyer Cutler
<sawyer@dirtroad.dev> - Committer: Sawyer Cutler
<sawyer@dirtroad.dev>
Non-blocking notes
- Hub
index.tswire-up is not itself red-capable (helper is unit-tested with a fake platform). relaunchTerminalRunstill logsrun ${run.id} is terminal (${run.status})while this pass relaunches arunningresident. Misleading log; same helper already used by inference drift.settleConnectedServicestilldispatchTurns the old run before the fire-and-forget relaunch. Settings connect (no pending room) is unaffected. In-room pending connect can one-shot the stale snapshot, then relaunch — same posture as CL-6687 inference reconcile.- bun.lock
packages/manus-toolsversion0.0.1vs package.json0.0.11(pre-existing).
Not covered
- Did not execute the new tests (worktree is
main, not0ec1033b). - Did not re-review the rest of the Manus plugin (client, tools, probes, docs).
- Did not merge or approve on GitHub.