Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
719b9d4
Add tests for a Manus tool package
TheGreatAxios Aug 26, 2026
d4b5748
Add a Manus plugin covering API v2
TheGreatAxios Aug 26, 2026
366d1c6
Update docs: Manus plugin
TheGreatAxios Aug 26, 2026
8487d54
Drop required Manus credential binding from the assistant
TheGreatAxios Aug 27, 2026
65cfee3
Wait for Manus slide decks without pdf or stale status
TheGreatAxios Aug 27, 2026
989ed15
Add tests for folding pinned-package credentials at launch
TheGreatAxios Aug 27, 2026
5489911
Fold connected pinned-package credentials into launch
TheGreatAxios Aug 27, 2026
f71388c
Name Manus in probes, assistant pins, and credential-wiring docs
TheGreatAxios Aug 27, 2026
65a9010
Add factory tests for pinned-package connector credentials
TheGreatAxios Aug 27, 2026
63fe2a9
Resolve pinned-package credentials via connector ownership
TheGreatAxios Aug 27, 2026
92c2891
Bump @corbits/manus-tools after source changes
TheGreatAxios Aug 27, 2026
363027b
Stop pinning manus-tools version in its manifest test
TheGreatAxios Aug 27, 2026
51e6af1
Bump @corbits/manus-tools after the manifest test change
TheGreatAxios Aug 27, 2026
2e391c7
Add tests for Manus task skill and ContentPart fields
TheGreatAxios Aug 27, 2026
4e42c5d
Wire Manus task messages as ContentPart with optional skills
TheGreatAxios Aug 27, 2026
f08a9e3
Bump @corbits/manus-tools after skill field wiring
TheGreatAxios Aug 27, 2026
7d5ff8d
Add tests for Manus slide-deck lite agent profile
TheGreatAxios Aug 27, 2026
5e6308a
Default Manus slide-deck creates to the lite agent profile
TheGreatAxios Aug 27, 2026
35a4535
Bump @corbits/manus-tools after the lite profile default
TheGreatAxios Aug 27, 2026
4a1ac6f
Add tests for Manus createTask lite agent profile default
TheGreatAxios Aug 27, 2026
152e209
Default Manus createTask to the lite agent profile
TheGreatAxios Aug 27, 2026
44be9e1
Bump @corbits/manus-tools after the createTask lite default
TheGreatAxios Aug 27, 2026
a36d402
Add tests for Manus slide-deck listMessages not_found retry
TheGreatAxios Aug 27, 2026
e830690
Retry Manus slide-deck polls on listMessages not_found
TheGreatAxios Aug 27, 2026
4bd7237
Bump @corbits/manus-tools after listMessages not_found retry
TheGreatAxios Aug 27, 2026
ea02cdb
Add tests for Manus listMessages string timestamps
TheGreatAxios Aug 27, 2026
d5379ee
Accept string or number timestamps on Manus TaskEvent
TheGreatAxios Aug 27, 2026
f88ecc9
Bump @corbits/manus-tools after string timestamp parse
TheGreatAxios Aug 27, 2026
2ea3de2
Add tests for lowercase Manus listMessages not_found
TheGreatAxios Aug 27, 2026
119a77c
Match Manus listMessages not_found without Task casing
TheGreatAxios Aug 27, 2026
33f6f23
Bump @corbits/manus-tools after lowercase not_found match
TheGreatAxios Aug 27, 2026
daafc70
Validate Manus task detail responses
TheGreatAxios Aug 27, 2026
a4eda05
Gate mutating Manus tools and flag create_slides agent errors
TheGreatAxios Aug 27, 2026
3a2e893
Bump @corbits/manus-tools after admin-tool gates
TheGreatAxios Aug 27, 2026
8b7095c
Add tests for a Manus tool package
TheGreatAxios Aug 26, 2026
c6bf6c7
Add a Manus plugin covering API v2
TheGreatAxios Aug 26, 2026
bc4c76c
Update docs: Manus plugin
TheGreatAxios Aug 26, 2026
ae6c305
Drop required Manus credential binding from the assistant
TheGreatAxios Aug 27, 2026
d0fb6cf
Wait for Manus slide decks without pdf or stale status
TheGreatAxios Aug 27, 2026
b039bee
Add tests for folding pinned-package credentials at launch
TheGreatAxios Aug 27, 2026
e77f079
Fold connected pinned-package credentials into launch
TheGreatAxios Aug 27, 2026
a4b7424
Name Manus in probes, assistant pins, and credential-wiring docs
TheGreatAxios Aug 27, 2026
a4fd8f2
Add factory tests for pinned-package connector credentials
TheGreatAxios Aug 27, 2026
7286348
Resolve pinned-package credentials via connector ownership
TheGreatAxios Aug 27, 2026
018afec
Bump @corbits/manus-tools after source changes
TheGreatAxios Aug 27, 2026
b92d240
Stop pinning manus-tools version in its manifest test
TheGreatAxios Aug 27, 2026
69f41ff
Bump @corbits/manus-tools after the manifest test change
TheGreatAxios Aug 27, 2026
877d99a
Add tests for Manus task skill and ContentPart fields
TheGreatAxios Aug 27, 2026
cb96bbc
Wire Manus task messages as ContentPart with optional skills
TheGreatAxios Aug 27, 2026
6e88e22
Bump @corbits/manus-tools after skill field wiring
TheGreatAxios Aug 27, 2026
a68ee9b
Add tests for Manus slide-deck lite agent profile
TheGreatAxios Aug 27, 2026
5cecd58
Default Manus slide-deck creates to the lite agent profile
TheGreatAxios Aug 27, 2026
f0ebfeb
Bump @corbits/manus-tools after the lite profile default
TheGreatAxios Aug 27, 2026
0bb54fd
Add tests for Manus createTask lite agent profile default
TheGreatAxios Aug 27, 2026
5c1c589
Default Manus createTask to the lite agent profile
TheGreatAxios Aug 27, 2026
635203b
Bump @corbits/manus-tools after the createTask lite default
TheGreatAxios Aug 27, 2026
1ddddf2
Add tests for Manus slide-deck listMessages not_found retry
TheGreatAxios Aug 27, 2026
040adc8
Retry Manus slide-deck polls on listMessages not_found
TheGreatAxios Aug 27, 2026
006c24a
Bump @corbits/manus-tools after listMessages not_found retry
TheGreatAxios Aug 27, 2026
d159d8f
Add tests for Manus listMessages string timestamps
TheGreatAxios Aug 27, 2026
26de5b6
Accept string or number timestamps on Manus TaskEvent
TheGreatAxios Aug 27, 2026
2c7b8a0
Bump @corbits/manus-tools after string timestamp parse
TheGreatAxios Aug 27, 2026
b85a6b9
Add tests for lowercase Manus listMessages not_found
TheGreatAxios Aug 27, 2026
dae34fc
Match Manus listMessages not_found without Task casing
TheGreatAxios Aug 27, 2026
1fe58e6
Bump @corbits/manus-tools after lowercase not_found match
TheGreatAxios Aug 27, 2026
d9acaf0
Validate Manus task detail responses
TheGreatAxios Aug 27, 2026
439d803
Gate mutating Manus tools and flag create_slides agent errors
TheGreatAxios Aug 27, 2026
b2af571
Bump @corbits/manus-tools after admin-tool gates
TheGreatAxios Aug 27, 2026
671e9a7
Pass DATABASE_URL into walking-skeleton package suites
TheGreatAxios Aug 29, 2026
b436e82
Export .env before walking-skeleton package suites
TheGreatAxios Aug 29, 2026
8a5a925
Load root .env for walking-skeleton package suites
TheGreatAxios Aug 29, 2026
d96895c
Read DATABASE_URL from .env in e2eDatabaseUrl
TheGreatAxios Aug 29, 2026
68ce2a6
Merge remote-tracking branch 'origin/main' into cl-7087-add-a-manus-p…
TheGreatAxios Aug 29, 2026
ed9e4ca
Merge origin/cl-7087 to recover Manus history
TheGreatAxios Aug 29, 2026
1f92a69
Add tests for relaunching a live assistant when Manus connects
TheGreatAxios Aug 29, 2026
0ec1033
Relaunch live assistants when a pinned tool-package credential connects
TheGreatAxios Aug 29, 2026
12bb75f
Format launch.ts after Manus history merge
TheGreatAxios Aug 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/hub/src/connection-live-reconcile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// The hub connect hook must relaunch a live assistant the moment a
// `feedsTools` connector (Manus) is stored — not wait for a new invite,
// and not persist-only. `createHub` is not booted here; this is the
// hook body `apps/hub/src/index.ts` fires from `settleServiceConnection`.
import { describe, expect, test } from "bun:test";
import { CONNECTOR_REGISTRY } from "@workbench/connections/registry";
import { reconcilePinnedToolPackagesAfterConnect } from "./connection-live-reconcile";

describe("reconcilePinnedToolPackagesAfterConnect", () => {
test("manus connect relaunches live assistants that pin @corbits/manus-tools", async () => {
const calls: { tenantId: string; packages: readonly string[] }[] = [];
const result = await reconcilePinnedToolPackagesAfterConnect(
{
reconcilePinnedToolPackages: async (tenantId, packageNames) => {
calls.push({ tenantId, packages: [...packageNames] });
return { scanned: 1, relaunched: 1 };
},
},
{ tenantId: "ten_1", connectorId: "manus" },
);

expect(CONNECTOR_REGISTRY["manus"]?.feedsTools).toEqual([
"@corbits/manus-tools",
]);
expect(result).toEqual({ scanned: 1, relaunched: 1 });
expect(calls).toEqual([
{ tenantId: "ten_1", packages: ["@corbits/manus-tools"] },
]);
});

test("an inference-only connector does not relaunch for tool-package pins", async () => {
const calls: unknown[] = [];
const result = await reconcilePinnedToolPackagesAfterConnect(
{
reconcilePinnedToolPackages: async (...args) => {
calls.push(args);
return { scanned: 0, relaunched: 0 };
},
},
{ tenantId: "ten_1", connectorId: "anthropic" },
);

expect(result).toBeUndefined();
expect(calls).toEqual([]);
});
});
30 changes: 30 additions & 0 deletions apps/hub/src/connection-live-reconcile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// After a connector's credential lands, live assistants that already
// pin that connector's `feedsTools` packages still run the snapshot
// deployed at launch. Bindings for those packages
// (`pinnedPackageCredentialBindingsFor`) fold only at deploy, so a Myra
// launched at signup before Manus was pasted cannot `resolve("manus")`
// until this pass relaunches it. Persist-only (stamp the pin onto the
// launch row and leave the run) is the bug: the sidecar keeps the old
// snapshot.
import { CONNECTOR_REGISTRY } from "@workbench/connections/registry";

export type PinnedToolPackageReconcile = {
reconcilePinnedToolPackages(
tenantId: string,
packageNames: readonly string[],
): Promise<{ scanned: number; relaunched: number }>;
};

/**
* Relaunches live participants in `tenantId` whose pins include this
* connector's `feedsTools`. Returns `undefined` when the connector
* feeds no tool packages (inference-only, webhook, …).
*/
export async function reconcilePinnedToolPackagesAfterConnect(
platform: PinnedToolPackageReconcile,
info: { readonly tenantId: string; readonly connectorId: string },
): Promise<{ scanned: number; relaunched: number } | undefined> {
const feedsTools = CONNECTOR_REGISTRY[info.connectorId]?.feedsTools ?? [];
if (feedsTools.length === 0) return undefined;
return platform.reconcilePinnedToolPackages(info.tenantId, feedsTools);
}
69 changes: 48 additions & 21 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ import { createBootAssetWiring, REGISTRIES } from "./asset-service-factory";
import { createRoutineScheduler } from "./routine-scheduler";
import { createToolGrantsForPins } from "./tool-grants";
import { createMcpCredentialBindingsFor } from "./mcp-credential-bindings";
import { reconcilePinnedToolPackagesAfterConnect } from "./connection-live-reconcile";
import { createPinnedPackageCredentialBindingsFor } from "./pinned-package-credential-bindings";
import { shutdownHub } from "./shutdown";

// Host policy constants, not configuration.
Expand Down Expand Up @@ -717,6 +719,20 @@ export async function createHub(config: HubConfig) {
);
// See `./mcp-credential-bindings.ts`'s own doc.
const mcpCredentialBindingsFor = createMcpCredentialBindingsFor(db);
// Same owning check GET /connections uses — see
// `@workbench/connections`' `workflow-connection-routes.ts` and the
// `createWorkflowConnectionRoutes` wiring below. Not
// `listConnectedProviders` (catalog-only).
const isConnectorConnected = async (tenantId: string, connectorId: string) =>
(await resolveCredentialRequirement(
db,
tenantId,
{ providerName: connectorId, source: "tenant" },
null,
null,
)) !== null;
const pinnedPackageCredentialBindingsFor =
createPinnedPackageCredentialBindingsFor(isConnectorConnected);
const sidecarRouter = createSidecarRouter({
hubPublicKey,
authenticateSidecar: createSidecarTokenAuthenticator({ db }),
Expand Down Expand Up @@ -1281,6 +1297,7 @@ export async function createHub(config: HubConfig) {
credentialCipher,
toolGrantsForPins,
mcpCredentialBindingsFor,
pinnedPackageCredentialBindingsFor,
// Chat residents are undeployed on idle again (see the comment above
// this function): `chatIdleReapMs` (env-overridable via
// `WORKBENCH_CHAT_IDLE_REAP_MS`, default 30 minutes) is
Expand Down Expand Up @@ -1987,6 +2004,7 @@ export async function createHub(config: HubConfig) {
eventCollectors,
toolGrantsForPins,
mcpCredentialBindingsFor,
pinnedPackageCredentialBindingsFor,
cryptoProviderCache: foldedRunCryptoProviders,
launchMode: AGENT_SECTION_MODE,
persistLaunch: workbenchLaunchPersistExtra,
Expand All @@ -2008,9 +2026,13 @@ export async function createHub(config: HubConfig) {
// An inference provider's credential landing also re-checks every
// live participant's deployed inference chain (CL-6687): a rotated
// key only ever reaches an agent at deploy time, so the relaunch has
// to be kicked here, not left for the next message. Not awaited — a
// relaunch is a sidecar deploy round-trip, and the connect response
// must not wait on it.
// to be kicked here, not left for the next message. A tool-package
// connector (`feedsTools`, e.g. Manus) is the same shape for a
// different payload: `pinnedPackageCredentialBindingsFor` only folds
// at deploy, so a live Myra launched at signup before the key was
// pasted stays on a snapshot that cannot `resolve("manus")` until
// this pass relaunches it. Not awaited — a relaunch is a sidecar
// deploy round-trip, and the connect response must not wait on it.
const settleServiceConnection: ServiceConnectedHook = async (info) => {
await settleConnectedService(
{
Expand All @@ -2027,15 +2049,27 @@ export async function createHub(config: HubConfig) {
displayName: info.displayName,
},
);
if (!isInferenceProvider(info.connectorId)) return;
void chatPlatform
.reconcileInferenceSources(info.tenantId)
.then(({ scanned, relaunched }) => {
log.info`inference credential ${info.connectorId} changed on tenant ${info.tenantId}: re-checked ${String(scanned)} live agents, relaunched ${String(relaunched)}`;
if (isInferenceProvider(info.connectorId)) {
void chatPlatform
.reconcileInferenceSources(info.tenantId)
.then(({ scanned, relaunched }) => {
log.info`inference credential ${info.connectorId} changed on tenant ${info.tenantId}: re-checked ${String(scanned)} live agents, relaunched ${String(relaunched)}`;
})
.catch((cause: unknown) => {
reportError(cause, {
operation: "connections.reconcile-inference-sources",
tenantId: 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)}`;
})
.catch((cause: unknown) => {
reportError(cause, {
operation: "connections.reconcile-inference-sources",
operation: "connections.reconcile-pinned-tool-packages",
tenantId: info.tenantId,
});
});
Expand Down Expand Up @@ -2458,18 +2492,9 @@ export async function createHub(config: HubConfig) {
"/api/workflow-connections",
createWorkflowConnectionRoutes({
authenticator: createWorkflowRunAuthenticator({ db }),
// The same resolution `buildCredentialDelivery` uses at agent-launch
// time to decide whether a tool actually gets a credential — so
// `list_connections` reports exactly what an agent could really use,
// for an inference provider and a tool connector alike (CL-6492).
isConnectorConnected: async (tenantId, connectorId) =>
(await resolveCredentialRequirement(
db,
tenantId,
{ providerName: connectorId, source: "tenant" },
null,
null,
)) !== null,
// Same `isConnectorConnected` the pinned-package factory is wired
// with above (CL-6492).
isConnectorConnected,
listMcpServers: (tenantId) => listMcpServerConnections(db, tenantId),
}),
);
Expand Down Expand Up @@ -2537,6 +2562,7 @@ export async function createHub(config: HubConfig) {
hubPublicKey,
toolGrantsForPins,
mcpCredentialBindingsFor,
pinnedPackageCredentialBindingsFor,
};

// Every genuine top-level deployment run, folded runs (workbench hosts,
Expand Down Expand Up @@ -2727,6 +2753,7 @@ export async function createHub(config: HubConfig) {
credentialCipher,
toolGrantsForPins,
mcpCredentialBindingsFor,
pinnedPackageCredentialBindingsFor,
cryptoProviderCache: foldedRunCryptoProviders,
joinDeliveryWorkbench: (input) =>
joinRunParticipant({ store: chatStore }, input),
Expand Down
67 changes: 67 additions & 0 deletions apps/hub/src/pinned-package-credential-bindings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, test } from "bun:test";
import {
bindingsForConnectedPins,
createPinnedPackageCredentialBindingsFor,
} from "./pinned-package-credential-bindings";

const MANUS_PIN = { name: "@corbits/manus-tools", version: "*" };
const GRANOLA_PIN = { name: "@corbits/granola-tools", version: "*" };

describe("bindingsForConnectedPins", () => {
test("emits a manus tenant binding when manus-tools is pinned and Manus is connected", () => {
expect(bindingsForConnectedPins([MANUS_PIN], ["manus"])).toEqual([
{
package: "@corbits/manus-tools",
handle: "manus",
provider: "manus",
locator: "tenant",
},
]);
});

test("returns none when manus-tools is pinned but Manus is not connected", () => {
expect(bindingsForConnectedPins([MANUS_PIN], ["granola"])).toEqual([]);
});

test("returns none when Manus is connected but manus-tools is not pinned", () => {
expect(bindingsForConnectedPins([GRANOLA_PIN], ["manus"])).toEqual([]);
});

test("emits a granola binding when granola-tools is pinned and Granola is connected", () => {
expect(bindingsForConnectedPins([GRANOLA_PIN], ["granola"])).toEqual([
{
package: "@corbits/granola-tools",
handle: "granola",
provider: "granola",
locator: "tenant",
},
]);
});

test("returns none for empty pins even when connectors are connected", () => {
expect(bindingsForConnectedPins([], ["manus", "granola"])).toEqual([]);
});
});

describe("createPinnedPackageCredentialBindingsFor", () => {
test("emits a manus tenant binding when isConnectorConnected is true for manus", async () => {
const bindingsFor = createPinnedPackageCredentialBindingsFor(
async (_tenantId, connectorId) => connectorId === "manus",
);
expect(await bindingsFor("tenant-1", [MANUS_PIN])).toEqual([
{
package: "@corbits/manus-tools",
handle: "manus",
provider: "manus",
locator: "tenant",
},
]);
});

test("emits none when only a catalog-style inference connector is connected", async () => {
const bindingsFor = createPinnedPackageCredentialBindingsFor(
async (_tenantId, connectorId) => connectorId === "anthropic",
);
expect(await bindingsFor("tenant-1", [MANUS_PIN])).toEqual([]);
});
});
61 changes: 61 additions & 0 deletions apps/hub/src/pinned-package-credential-bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// The `PinnedPackageCredentialBindingsFor` port every `FoldedRunsDeps`
// below is wired with — see `@corbits/folded-runs`' `types.ts` for why
// this has to be supplied by the composition root rather than declared as
// a required assistant `credentialBindings` entry. Static-handle packages
// (`@corbits/manus-tools`, granola-tools, …) declare
// `interchange.credentials`, but requiring those binds on the assistant
// would throw `MissingCredentialError` on signup / first chat. This
// factory emits a tenant binding only when a pin names a package a
// `CONNECTOR_REGISTRY` entry `feedsTools` AND the tenant already has a
// connected credential for that connector (`isConnectorConnected`, the
// same owning check `createWorkflowConnectionRoutes` uses — not
// `@corbits/chat`'s catalog-only `listConnectedProviders`).
import type { CredentialBinding } from "@intx/types";
import type { ToolPackagePin } from "@intx/types/tool-packages";
import type { PinnedPackageCredentialBindingsFor } from "@corbits/folded-runs";
import { CONNECTOR_REGISTRY } from "@workbench/connections/registry";

export type IsConnectorConnected = (
tenantId: string,
connectorId: string,
) => Promise<boolean>;

export function bindingsForConnectedPins(
pins: readonly ToolPackagePin[],
connectedConnectorIds: readonly string[],
): readonly CredentialBinding[] {
const pinNames = new Set(pins.map((pin) => pin.name));
if (pinNames.size === 0) return [];
const connected = new Set(connectedConnectorIds);
const bindings: CredentialBinding[] = [];
for (const descriptor of Object.values(CONNECTOR_REGISTRY)) {
if (!connected.has(descriptor.id)) continue;
for (const toolPackageName of descriptor.feedsTools) {
if (!pinNames.has(toolPackageName)) continue;
bindings.push({
package: toolPackageName,
handle: descriptor.id,
provider: descriptor.id,
locator: "tenant",
});
}
}
return bindings;
}

export function createPinnedPackageCredentialBindingsFor(
isConnectorConnected: IsConnectorConnected,
): PinnedPackageCredentialBindingsFor {
return async (tenantId, pins) => {
const pinNames = new Set(pins.map((pin) => pin.name));
if (pinNames.size === 0) return [];
const connectedConnectorIds: string[] = [];
for (const descriptor of Object.values(CONNECTOR_REGISTRY)) {
if (!descriptor.feedsTools.some((name) => pinNames.has(name))) continue;
if (await isConnectorConnected(tenantId, descriptor.id)) {
connectedConnectorIds.push(descriptor.id);
}
}
return bindingsForConnectedPins(pins, connectedConnectorIds);
};
}
19 changes: 19 additions & 0 deletions apps/hub/src/tool-grants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// `@corbits/folded-runs`' `deployAtHead`, which mints these into
// `config.grants`.
import { describe, expect, test } from "bun:test";
import { describeCorbitsToolPackages } from "@corbits/tool-registry-publish";
import { createToolGrantsForPins } from "./tool-grants";

const DESCRIPTIONS = [
Expand Down Expand Up @@ -85,4 +86,22 @@ describe("createToolGrantsForPins", () => {
const toolGrantsForPins = createToolGrantsForPins(DESCRIPTIONS);
expect(toolGrantsForPins([])).toEqual([]);
});

test("assistant pin of webhook_create is ask, not allow", async () => {
const toolGrantsForPins = createToolGrantsForPins(
await describeCorbitsToolPackages(),
);
const grants = toolGrantsForPins([
{ name: "@corbits/manus-tools", version: "*" },
]);
expect(
grants.find((g) => g.resource.endsWith(":webhook_create"))?.effect,
).toBe("ask");
expect(
grants.find((g) => g.resource.endsWith(":create_slides"))?.effect,
).toBe("allow");
expect(grants.find((g) => g.resource.endsWith(":task_list"))?.effect).toBe(
"allow",
);
});
});
Loading
Loading