@agent-native/dispatch 0.14.9 → 0.14.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/list-workspace-apps.js +1 -1
- package/dist/actions/list-workspace-apps.js.map +1 -1
- package/dist/actions/upsert-destination.d.ts +12 -12
- package/dist/server/lib/vault-boot-resync.d.ts +23 -0
- package/dist/server/lib/vault-boot-resync.d.ts.map +1 -0
- package/dist/server/lib/vault-boot-resync.js +44 -0
- package/dist/server/lib/vault-boot-resync.js.map +1 -0
- package/dist/server/lib/vault-store.d.ts +22 -0
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +49 -0
- package/dist/server/lib/vault-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/db.d.ts +8 -1
- package/dist/server/plugins/db.d.ts.map +1 -1
- package/dist/server/plugins/db.js +13 -1
- package/dist/server/plugins/db.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +2 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/index.spec.ts +12 -0
- package/src/actions/list-workspace-apps.ts +1 -1
- package/src/server/lib/vault-boot-resync.spec.ts +68 -0
- package/src/server/lib/vault-boot-resync.ts +50 -0
- package/src/server/lib/vault-store.spec.ts +132 -0
- package/src/server/lib/vault-store.ts +58 -0
- package/src/server/plugins/agent-chat.ts +2 -1
- package/src/server/plugins/db.ts +14 -1
- package/src/server/plugins/integrations.ts +2 -1
|
@@ -52,7 +52,8 @@ Use the standard workspace primitives:
|
|
|
52
52
|
- Use recurring jobs for scheduled behavior.
|
|
53
53
|
- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.
|
|
54
54
|
- You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.
|
|
55
|
-
-
|
|
55
|
+
- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
56
|
+
- When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
|
|
56
57
|
- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.
|
|
57
58
|
- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
|
|
58
59
|
- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,kBAAkB,GAAG;IACzB,aAAa;IACb,qBAAqB;IACrB,uBAAuB;IACvB,SAAS;IACT,UAAU;IACV,0BAA0B;IAC1B,2BAA2B;IAC3B,2BAA2B;IAC3B,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,8BAA8B;IAC9B,oCAAoC;IACpC,sBAAsB;IACtB,mBAAmB;IACnB,sBAAsB;IACtB,sBAAsB;IACtB,UAAU;CACX,CAAC;AAEF,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,gBAAgB,EAAE,kBAAkB;IACpC,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,OAAO,EAAE,eAAe;IACxB,aAAa,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;IAC1C,YAAY,EAAE
|
|
1
|
+
{"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,kBAAkB,GAAG;IACzB,aAAa;IACb,qBAAqB;IACrB,uBAAuB;IACvB,SAAS;IACT,UAAU;IACV,0BAA0B;IAC1B,2BAA2B;IAC3B,2BAA2B;IAC3B,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,8BAA8B;IAC9B,oCAAoC;IACpC,sBAAsB;IACtB,mBAAmB;IACnB,sBAAsB;IACtB,sBAAsB;IACtB,UAAU;CACX,CAAC;AAEF,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,gBAAgB,EAAE,kBAAkB;IACpC,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,OAAO,EAAE,eAAe;IACxB,aAAa,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;IAC1C,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;4EAwB4D;CAC3E,CAAC,CAAC","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport { createAgentChatPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\n\nconst INITIAL_TOOL_NAMES = [\n \"view-screen\",\n \"list-workspace-apps\",\n \"list-connected-agents\",\n \"ask_app\",\n \"open_app\",\n \"list-workspace-resources\",\n \"create-workspace-resource\",\n \"update-workspace-resource\",\n \"list-vault-secrets\",\n \"request-vault-secret\",\n \"create-vault-secret\",\n \"list-destinations\",\n \"upsert-destination\",\n \"list-dreams\",\n \"start-workspace-app-creation\",\n \"list-available-workspace-templates\",\n \"provider-api-catalog\",\n \"provider-api-docs\",\n \"provider-api-request\",\n \"query-staged-dataset\",\n \"navigate\",\n];\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\n initialToolNames: INITIAL_TOOL_NAMES,\n // Without this, AGENT_ORG_ID is never set on agent action calls and every\n // row written through the frontend (vault secrets, destinations, workspace\n // resources) lands with org_id=NULL — breaking data isolation across orgs.\n resolveOrgId: async (event) => {\n const ctx = await getOrgContext(event);\n return ctx.orgId;\n },\n // Read actions directly from the package's own action map rather than from\n // a build-time-generated `.generated/actions-registry.ts` (the latter is a\n // template-only construct that the Vite plugin emits next to actions/).\n actions: dispatchActions,\n codeExecution: { production: \"sandboxed\" },\n systemPrompt: `You are the central dispatch for this workspace.\n\nDefault posture:\n- Treat Slack and Telegram as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A when another app owns the job.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Prefer replying in the current external thread unless the user explicitly asks you to send to a saved destination.\n\nUse the standard workspace primitives:\n- Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.\n- Use recurring jobs for scheduled behavior.\n- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.\n- You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.\n- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.\n- When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.\n- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.\n\nWhen a user asks for something like a digest, reminder, routing rule, or saved behavior:\n- First decide whether it should be a resource, a recurring job, a destination, or a delegated task.\n- Keep responses concise and operational.\n- Avoid inventing integrations or destinations that are not configured yet.`,\n});\n"]}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
declare const _default: (nitroApp: any) => void | Promise<void>;
|
|
2
1
|
export default _default;
|
|
2
|
+
/**
|
|
3
|
+
* Run dispatch's own migrations first (this is what guarantees
|
|
4
|
+
* `vault_secrets` exists), then kick off the vault boot resync. The resync
|
|
5
|
+
* itself waits several more seconds before touching the DB — see
|
|
6
|
+
* vault-boot-resync.ts — so this ordering is a belt-and-suspenders
|
|
7
|
+
* guarantee, not a hard dependency.
|
|
8
|
+
*/
|
|
9
|
+
declare function _default(nitroApp: any): Promise<void>;
|
|
3
10
|
//# sourceMappingURL=db.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/db.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/db.ts"],"names":[],"mappings":";AASA;;;;;;GAMG;0BACmB,QAAQ,EAAE,GAAG"}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { runMigrations } from "@agent-native/core/db";
|
|
2
2
|
import { dispatchMigrations } from "../../db/migrations.js";
|
|
3
|
-
|
|
3
|
+
import { scheduleVaultBootResync } from "../lib/vault-boot-resync.js";
|
|
4
|
+
const runDispatchMigrations = runMigrations(dispatchMigrations, {
|
|
4
5
|
table: "dispatch_migrations",
|
|
5
6
|
});
|
|
7
|
+
/**
|
|
8
|
+
* Run dispatch's own migrations first (this is what guarantees
|
|
9
|
+
* `vault_secrets` exists), then kick off the vault boot resync. The resync
|
|
10
|
+
* itself waits several more seconds before touching the DB — see
|
|
11
|
+
* vault-boot-resync.ts — so this ordering is a belt-and-suspenders
|
|
12
|
+
* guarantee, not a hard dependency.
|
|
13
|
+
*/
|
|
14
|
+
export default async (nitroApp) => {
|
|
15
|
+
await runDispatchMigrations(nitroApp);
|
|
16
|
+
scheduleVaultBootResync();
|
|
17
|
+
};
|
|
6
18
|
//# sourceMappingURL=db.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/server/plugins/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/server/plugins/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,MAAM,qBAAqB,GAAG,aAAa,CAAC,kBAAkB,EAAE;IAC9D,KAAK,EAAE,qBAAqB;CAC7B,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,eAAe,KAAK,EAAE,QAAa,EAAE,EAAE;IACrC,MAAM,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACtC,uBAAuB,EAAE,CAAC;AAC5B,CAAC,CAAC","sourcesContent":["import { runMigrations } from \"@agent-native/core/db\";\n\nimport { dispatchMigrations } from \"../../db/migrations.js\";\nimport { scheduleVaultBootResync } from \"../lib/vault-boot-resync.js\";\n\nconst runDispatchMigrations = runMigrations(dispatchMigrations, {\n table: \"dispatch_migrations\",\n});\n\n/**\n * Run dispatch's own migrations first (this is what guarantees\n * `vault_secrets` exists), then kick off the vault boot resync. The resync\n * itself waits several more seconds before touching the DB — see\n * vault-boot-resync.ts — so this ordering is a belt-and-suspenders\n * guarantee, not a hard dependency.\n */\nexport default async (nitroApp: any) => {\n await runDispatchMigrations(nitroApp);\n scheduleVaultBootResync();\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,QAAA,MAAM,0BAA0B,aAAoB,GAAG,kBAuBtD,CAAC;eAEa,0BAA0B"}
|
|
@@ -18,7 +18,8 @@ Default posture:
|
|
|
18
18
|
- Treat Slack, Telegram, and email as shared entrypoints into the workspace.
|
|
19
19
|
- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).
|
|
20
20
|
- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.
|
|
21
|
-
-
|
|
21
|
+
- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
22
|
+
- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.
|
|
22
23
|
- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
23
24
|
- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
|
|
24
25
|
- Keep durable memory and operating instructions in resources rather than ephemeral chat.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AAEzC,MAAM,0BAA0B,GAAG;IACjC,GAAG,eAAe;IAClB,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,OAAO,EAAE;QACP,GAAG,eAAe,CAAC,OAAO;QAC1B,SAAS,EAAE,KAAK;KACjB;CACF,CAAC;AAEF,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;4FA2BiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,0BAA0B;QACnC,uBAAuB,EAAE,+BAA+B;QACxD,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport {\n beforeDispatchProcess,\n resolveDispatchExecutionContext,\n} from \"../lib/dispatch-integrations.js\";\n\nconst dispatchIntegrationActions = {\n ...dispatchActions,\n // Messaging integrations should use the core call-agent tool for cross-app\n // delegation because it queues A2A continuations when serverless budgets are\n // tight. The MCP-facing ask_app action is still available outside this path.\n ask_app: {\n ...dispatchActions.ask_app,\n agentTool: false,\n },\n};\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).\n- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.\n- When asked whether workspace apps expose agent cards or A2A endpoints, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- Synthetic uptime, health-check, and URL-availability monitors belong to Analytics, even when the monitored target is another app such as Clips. Delegate creation to Analytics and relay its exact monitor URL. Dispatch-native recurring jobs are for reminders, digests, and agent workflows, not HTTP uptime probes.\n- Route by the requested artifact type, not by organization-specific names stored in code. For structured records, databases, tables, queues, boards, and intake forms, resolve the owning app and canonical destination from loaded workspace instructions/resources plus discovered app capabilities; do not assume Content, a database ID, schema, owner, or required fields. Visual designs, mockups, wireframes, screens, and interfaces belong to Design. A trusted Required target agent hint in integration context is authoritative.\n- When delegating structured intake to the resolved owning app, preserve the exact Source thread URL and the workspace instruction context, inspect the destination's current required fields, ask only for missing values, submit once, verify the saved record, and return the exact link.\n- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode \"coming-soon\", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode \"builder-unavailable\", report the action's message exactly enough to preserve the missing identity/credential detail.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchIntegrationActions,\n resolveExecutionContext: resolveDispatchExecutionContext,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
|
|
1
|
+
{"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AAEzC,MAAM,0BAA0B,GAAG;IACjC,GAAG,eAAe;IAClB,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,OAAO,EAAE;QACP,GAAG,eAAe,CAAC,OAAO;QAC1B,SAAS,EAAE,KAAK;KACjB;CACF,CAAC;AAEF,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;4FA4BiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,0BAA0B;QACnC,uBAAuB,EAAE,+BAA+B;QACxD,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport {\n beforeDispatchProcess,\n resolveDispatchExecutionContext,\n} from \"../lib/dispatch-integrations.js\";\n\nconst dispatchIntegrationActions = {\n ...dispatchActions,\n // Messaging integrations should use the core call-agent tool for cross-app\n // delegation because it queues A2A continuations when serverless budgets are\n // tight. The MCP-facing ask_app action is still available outside this path.\n ask_app: {\n ...dispatchActions.ask_app,\n agentTool: false,\n },\n};\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).\n- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.\n- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.\n- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- Synthetic uptime, health-check, and URL-availability monitors belong to Analytics, even when the monitored target is another app such as Clips. Delegate creation to Analytics and relay its exact monitor URL. Dispatch-native recurring jobs are for reminders, digests, and agent workflows, not HTTP uptime probes.\n- Route by the requested artifact type, not by organization-specific names stored in code. For structured records, databases, tables, queues, boards, and intake forms, resolve the owning app and canonical destination from loaded workspace instructions/resources plus discovered app capabilities; do not assume Content, a database ID, schema, owner, or required fields. Visual designs, mockups, wireframes, screens, and interfaces belong to Design. A trusted Required target agent hint in integration context is authoritative.\n- When delegating structured intake to the resolved owning app, preserve the exact Source thread URL and the workspace instruction context, inspect the destination's current required fields, ask only for missing values, submit once, verify the saved record, and return the exact link.\n- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized fork/copy.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode \"coming-soon\", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode \"builder-unavailable\", report the action's message exactly enough to preserve the missing identity/credential detail.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchIntegrationActions,\n resolveExecutionContext: resolveDispatchExecutionContext,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.10",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
94
94
|
"vite": "8.1.0",
|
|
95
95
|
"vitest": "^4.1.5",
|
|
96
|
-
"@agent-native/core": "0.
|
|
96
|
+
"@agent-native/core": "0.101.3"
|
|
97
97
|
},
|
|
98
98
|
"peerDependencies": {
|
|
99
99
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -30,4 +30,16 @@ describe("dispatch action registry", () => {
|
|
|
30
30
|
),
|
|
31
31
|
).toEqual([]);
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it("distinguishes mounted workspace apps from connected A2A agents", () => {
|
|
35
|
+
expect(dispatchActions["list-workspace-apps"].tool.description).toContain(
|
|
36
|
+
"not the hosted/connected A2A agent registry",
|
|
37
|
+
);
|
|
38
|
+
expect(dispatchActions["list-workspace-apps"].tool.description).toContain(
|
|
39
|
+
"list-connected-agents",
|
|
40
|
+
);
|
|
41
|
+
expect(dispatchActions["list-connected-agents"].tool.description).toContain(
|
|
42
|
+
"A2A delegation",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
33
45
|
});
|
|
@@ -13,7 +13,7 @@ const httpBoolean = z.preprocess((value) => {
|
|
|
13
13
|
|
|
14
14
|
export default defineAction({
|
|
15
15
|
description:
|
|
16
|
-
"List apps
|
|
16
|
+
"List apps mounted inside this workspace deployment, including paths, absolute URLs, audience (internal/public), page route access overrides, and agent-card/A2A metadata for ready mounted apps by default. This is not the hosted/connected A2A agent registry; use list-connected-agents to discover agents such as Analytics or Content. UI polling callers can pass includeAgentCards=false to skip network probes.",
|
|
17
17
|
schema: z.object({
|
|
18
18
|
includeAgentCards: httpBoolean
|
|
19
19
|
.default(true)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
resyncAllVaultSecretsToCredentialStore: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("./vault-store.js", () => ({
|
|
8
|
+
resyncAllVaultSecretsToCredentialStore:
|
|
9
|
+
mocks.resyncAllVaultSecretsToCredentialStore,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
__resetVaultBootResyncGuardForTests,
|
|
14
|
+
scheduleVaultBootResync,
|
|
15
|
+
} from "./vault-boot-resync.js";
|
|
16
|
+
|
|
17
|
+
describe("scheduleVaultBootResync", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
__resetVaultBootResyncGuardForTests();
|
|
21
|
+
mocks.resyncAllVaultSecretsToCredentialStore.mockResolvedValue({
|
|
22
|
+
groups: 0,
|
|
23
|
+
failedGroups: 0,
|
|
24
|
+
syncedKeys: 0,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.useRealTimers();
|
|
30
|
+
mocks.resyncAllVaultSecretsToCredentialStore.mockReset();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not run the resync synchronously — it waits for the boot delay", () => {
|
|
34
|
+
scheduleVaultBootResync();
|
|
35
|
+
expect(mocks.resyncAllVaultSecretsToCredentialStore).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("runs the resync exactly once after the boot delay elapses", async () => {
|
|
39
|
+
scheduleVaultBootResync();
|
|
40
|
+
await vi.runAllTimersAsync();
|
|
41
|
+
expect(mocks.resyncAllVaultSecretsToCredentialStore).toHaveBeenCalledTimes(
|
|
42
|
+
1,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("only schedules one timer per process even if called repeatedly", async () => {
|
|
47
|
+
scheduleVaultBootResync();
|
|
48
|
+
scheduleVaultBootResync();
|
|
49
|
+
scheduleVaultBootResync();
|
|
50
|
+
await vi.runAllTimersAsync();
|
|
51
|
+
expect(mocks.resyncAllVaultSecretsToCredentialStore).toHaveBeenCalledTimes(
|
|
52
|
+
1,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("swallows a resync failure instead of throwing", async () => {
|
|
57
|
+
mocks.resyncAllVaultSecretsToCredentialStore.mockRejectedValue(
|
|
58
|
+
new Error("boom"),
|
|
59
|
+
);
|
|
60
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
61
|
+
|
|
62
|
+
scheduleVaultBootResync();
|
|
63
|
+
await vi.runAllTimersAsync();
|
|
64
|
+
|
|
65
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
66
|
+
warnSpy.mockRestore();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resyncAllVaultSecretsToCredentialStore } from "./vault-store.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Give the process a moment to finish booting (migrations, DB pool warmup,
|
|
5
|
+
* etc.) before touching every vault secret. This is a self-heal, not a
|
|
6
|
+
* hot-path dependency, so there is no urgency to run it immediately.
|
|
7
|
+
*/
|
|
8
|
+
const BOOT_RESYNC_DELAY_MS = 8000;
|
|
9
|
+
|
|
10
|
+
/** Guards against scheduling more than one resync per process — see
|
|
11
|
+
* `scheduleVaultBootResync` below. */
|
|
12
|
+
let scheduled = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Schedule a one-time, best-effort re-sync of every vault secret (across
|
|
16
|
+
* every tenant) into the shared credential store, a few seconds after boot.
|
|
17
|
+
*
|
|
18
|
+
* Why this exists: `syncSecretsToCredentialStore` only runs when a vault
|
|
19
|
+
* secret is created or updated, so it only re-encrypts rows a user happens
|
|
20
|
+
* to touch. When the shared `app_secrets` encryption format changes under
|
|
21
|
+
* it — e.g. the `shared_encrypted_value` dual-write, or hosted workspaces
|
|
22
|
+
* deriving shared key material from `A2A_SECRET` — existing rows are stuck
|
|
23
|
+
* on the old format until someone manually re-saves each vault secret,
|
|
24
|
+
* which breaks sibling apps reading them. Re-running the sync for every row
|
|
25
|
+
* at boot self-heals that without any manual step.
|
|
26
|
+
*
|
|
27
|
+
* Fires at most once per process (`scheduled` guard) so calling this again
|
|
28
|
+
* — e.g. if the owning plugin module re-runs under dev's `*-plugin.ts` HMR
|
|
29
|
+
* reload — doesn't stack duplicate timers. Runs fully non-blocking: it
|
|
30
|
+
* never delays startup, and any failure is caught and logged rather than
|
|
31
|
+
* thrown, since a stale-encryption row is a degraded state, not a crash.
|
|
32
|
+
*/
|
|
33
|
+
export function scheduleVaultBootResync(): void {
|
|
34
|
+
if (scheduled) return;
|
|
35
|
+
scheduled = true;
|
|
36
|
+
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
void resyncAllVaultSecretsToCredentialStore().catch((error) => {
|
|
39
|
+
console.warn(
|
|
40
|
+
"[dispatch] vault boot resync failed to run",
|
|
41
|
+
error instanceof Error ? error.message : error,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
}, BOOT_RESYNC_DELAY_MS);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Test-only: reset the once-per-process guard between spec runs. */
|
|
48
|
+
export function __resetVaultBootResyncGuardForTests(): void {
|
|
49
|
+
scheduled = false;
|
|
50
|
+
}
|
|
@@ -4,12 +4,14 @@ const mocks = vi.hoisted(() => ({
|
|
|
4
4
|
deleteAppSecret: vi.fn(),
|
|
5
5
|
getDb: vi.fn(),
|
|
6
6
|
listAppSecretsForScope: vi.fn(),
|
|
7
|
+
readAppSecret: vi.fn(),
|
|
7
8
|
writeAppSecret: vi.fn(),
|
|
8
9
|
}));
|
|
9
10
|
|
|
10
11
|
vi.mock("@agent-native/core/secrets", () => ({
|
|
11
12
|
deleteAppSecret: mocks.deleteAppSecret,
|
|
12
13
|
listAppSecretsForScope: mocks.listAppSecretsForScope,
|
|
14
|
+
readAppSecret: mocks.readAppSecret,
|
|
13
15
|
writeAppSecret: mocks.writeAppSecret,
|
|
14
16
|
}));
|
|
15
17
|
|
|
@@ -21,10 +23,13 @@ vi.mock("../../db/index.js", async (importOriginal) => {
|
|
|
21
23
|
};
|
|
22
24
|
});
|
|
23
25
|
|
|
26
|
+
import { readAppSecret } from "@agent-native/core/secrets";
|
|
27
|
+
|
|
24
28
|
import {
|
|
25
29
|
cleanupSyncedCredentialKeysIfUnused,
|
|
26
30
|
credentialStoreScopeForVaultCtx,
|
|
27
31
|
isTrustedEnvVarSyncAgentUrl,
|
|
32
|
+
resyncAllVaultSecretsToCredentialStore,
|
|
28
33
|
syncSecretsToCredentialStore,
|
|
29
34
|
} from "./vault-store.js";
|
|
30
35
|
|
|
@@ -187,3 +192,130 @@ describe("cleanupSyncedCredentialKeysIfUnused", () => {
|
|
|
187
192
|
});
|
|
188
193
|
});
|
|
189
194
|
});
|
|
195
|
+
|
|
196
|
+
describe("resyncAllVaultSecretsToCredentialStore", () => {
|
|
197
|
+
function mockVaultSecretsRows(rows: Array<Record<string, unknown>>) {
|
|
198
|
+
mocks.getDb.mockReturnValue({
|
|
199
|
+
select: () => ({
|
|
200
|
+
from: () => Promise.resolve(rows),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** In-memory stand-in for the shared credential store, keyed the same way
|
|
206
|
+
* the real app_secrets table is: scope + scopeId + key. */
|
|
207
|
+
function fakeCredentialStore() {
|
|
208
|
+
const store = new Map<string, string>();
|
|
209
|
+
mocks.writeAppSecret.mockImplementation(async (args: any) => {
|
|
210
|
+
store.set(`${args.scope}:${args.scopeId}:${args.key}`, args.value);
|
|
211
|
+
return "app-secret-id";
|
|
212
|
+
});
|
|
213
|
+
mocks.readAppSecret.mockImplementation(async (ref: any) => {
|
|
214
|
+
const value = store.get(`${ref.scope}:${ref.scopeId}:${ref.key}`);
|
|
215
|
+
return value === undefined ? null : { value, updatedAt: Date.now() };
|
|
216
|
+
});
|
|
217
|
+
return store;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
afterEach(() => {
|
|
221
|
+
mocks.writeAppSecret.mockReset();
|
|
222
|
+
mocks.readAppSecret.mockReset();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("syncs vault secrets from different tenants into their own credential-store scopes", async () => {
|
|
226
|
+
fakeCredentialStore();
|
|
227
|
+
mockVaultSecretsRows([
|
|
228
|
+
{
|
|
229
|
+
id: "secret_org",
|
|
230
|
+
ownerEmail: "admin@example.test",
|
|
231
|
+
orgId: "org_123",
|
|
232
|
+
name: "OpenAI API Key",
|
|
233
|
+
credentialKey: "OPENAI_API_KEY",
|
|
234
|
+
value: "sk-org-key",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "secret_solo",
|
|
238
|
+
ownerEmail: "owner@example.test",
|
|
239
|
+
orgId: null,
|
|
240
|
+
name: "Personal API Key",
|
|
241
|
+
credentialKey: "PERSONAL_API_KEY",
|
|
242
|
+
value: "sk-personal-key",
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const result = await resyncAllVaultSecretsToCredentialStore();
|
|
247
|
+
|
|
248
|
+
expect(result).toEqual({ groups: 2, failedGroups: 0, syncedKeys: 2 });
|
|
249
|
+
|
|
250
|
+
const orgScope = credentialStoreScopeForVaultCtx({
|
|
251
|
+
ownerEmail: "admin@example.test",
|
|
252
|
+
orgId: "org_123",
|
|
253
|
+
});
|
|
254
|
+
const soloScope = credentialStoreScopeForVaultCtx({
|
|
255
|
+
ownerEmail: "owner@example.test",
|
|
256
|
+
orgId: null,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await expect(
|
|
260
|
+
readAppSecret({ key: "OPENAI_API_KEY", ...orgScope }),
|
|
261
|
+
).resolves.toMatchObject({ value: "sk-org-key" });
|
|
262
|
+
await expect(
|
|
263
|
+
readAppSecret({ key: "PERSONAL_API_KEY", ...soloScope }),
|
|
264
|
+
).resolves.toMatchObject({ value: "sk-personal-key" });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("logs and skips a group that fails without blocking the other groups", async () => {
|
|
268
|
+
const store = fakeCredentialStore();
|
|
269
|
+
const writeImpl = mocks.writeAppSecret.getMockImplementation();
|
|
270
|
+
mocks.writeAppSecret.mockImplementation(async (args: any) => {
|
|
271
|
+
if (args.key === "BROKEN_KEY") {
|
|
272
|
+
throw new Error("simulated credential-store write failure");
|
|
273
|
+
}
|
|
274
|
+
return writeImpl!(args);
|
|
275
|
+
});
|
|
276
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
277
|
+
|
|
278
|
+
mockVaultSecretsRows([
|
|
279
|
+
{
|
|
280
|
+
id: "secret_broken",
|
|
281
|
+
ownerEmail: "admin@broken.test",
|
|
282
|
+
orgId: "org_broken",
|
|
283
|
+
name: "Broken Key",
|
|
284
|
+
credentialKey: "BROKEN_KEY",
|
|
285
|
+
value: "sk-broken-value",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "secret_solo",
|
|
289
|
+
ownerEmail: "owner@example.test",
|
|
290
|
+
orgId: null,
|
|
291
|
+
name: "Personal API Key",
|
|
292
|
+
credentialKey: "PERSONAL_API_KEY",
|
|
293
|
+
value: "sk-personal-key",
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
const result = await resyncAllVaultSecretsToCredentialStore();
|
|
298
|
+
|
|
299
|
+
expect(result).toEqual({ groups: 2, failedGroups: 1, syncedKeys: 1 });
|
|
300
|
+
|
|
301
|
+
// The failed org's key never landed in the credential store.
|
|
302
|
+
expect(store.get("org:org_broken:BROKEN_KEY")).toBeUndefined();
|
|
303
|
+
|
|
304
|
+
// The other tenant's group still synced successfully.
|
|
305
|
+
const soloScope = credentialStoreScopeForVaultCtx({
|
|
306
|
+
ownerEmail: "owner@example.test",
|
|
307
|
+
orgId: null,
|
|
308
|
+
});
|
|
309
|
+
await expect(
|
|
310
|
+
readAppSecret({ key: "PERSONAL_API_KEY", ...soloScope }),
|
|
311
|
+
).resolves.toMatchObject({ value: "sk-personal-key" });
|
|
312
|
+
|
|
313
|
+
// Exactly one warning, naming the key but never the plaintext value.
|
|
314
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
315
|
+
const [warnMessage] = warnSpy.mock.calls[0]!;
|
|
316
|
+
expect(String(warnMessage)).toContain("BROKEN_KEY");
|
|
317
|
+
expect(String(warnMessage)).not.toContain("sk-broken-value");
|
|
318
|
+
|
|
319
|
+
warnSpy.mockRestore();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -763,6 +763,64 @@ export async function syncSecretsToCredentialStore(
|
|
|
763
763
|
return { ...target, keys: syncedKeys };
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
/**
|
|
767
|
+
* Re-sync every vault secret across every tenant into the shared credential
|
|
768
|
+
* store, regardless of which request/ctx is currently active.
|
|
769
|
+
*
|
|
770
|
+
* `syncSecretsToCredentialStore` normally only runs on `createSecret` /
|
|
771
|
+
* `updateSecret`, so it only re-encrypts the rows a user happens to touch.
|
|
772
|
+
* When the shared `app_secrets` encryption format changes underneath it
|
|
773
|
+
* (e.g. a new dual-write format, or a change to how key material is
|
|
774
|
+
* derived), existing rows are stuck on the old format until someone
|
|
775
|
+
* manually re-saves each vault secret. This walks every `vault_secrets`
|
|
776
|
+
* row directly — bypassing the ctx-scoped `listSecrets()` — groups them by
|
|
777
|
+
* their (orgId, ownerEmail) tenant, and re-runs the sync per group so every
|
|
778
|
+
* row regains fresh ciphertext.
|
|
779
|
+
*
|
|
780
|
+
* A failure syncing one tenant's group is caught and logged (key NAMES
|
|
781
|
+
* only, never values) so it can't block the rest of the resync.
|
|
782
|
+
*/
|
|
783
|
+
export async function resyncAllVaultSecretsToCredentialStore(): Promise<{
|
|
784
|
+
groups: number;
|
|
785
|
+
failedGroups: number;
|
|
786
|
+
syncedKeys: number;
|
|
787
|
+
}> {
|
|
788
|
+
const db = getDb();
|
|
789
|
+
const rows = await db.select().from(schema.vaultSecrets);
|
|
790
|
+
|
|
791
|
+
const groups = new Map<string, { ctx: VaultCtx; rows: VaultSecretRow[] }>();
|
|
792
|
+
for (const row of rows) {
|
|
793
|
+
if (!row.credentialKey || !row.value) continue;
|
|
794
|
+
const ctx: VaultCtx = { ownerEmail: row.ownerEmail, orgId: row.orgId };
|
|
795
|
+
const groupKey = `${ctx.orgId ?? ""}${ctx.ownerEmail}`;
|
|
796
|
+
const group = groups.get(groupKey);
|
|
797
|
+
if (group) {
|
|
798
|
+
group.rows.push(row);
|
|
799
|
+
} else {
|
|
800
|
+
groups.set(groupKey, { ctx, rows: [row] });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
let failedGroups = 0;
|
|
805
|
+
let syncedKeys = 0;
|
|
806
|
+
|
|
807
|
+
for (const { ctx, rows: groupRows } of groups.values()) {
|
|
808
|
+
try {
|
|
809
|
+
const result = await syncSecretsToCredentialStore(groupRows, ctx);
|
|
810
|
+
syncedKeys += result.keys.length;
|
|
811
|
+
} catch (error) {
|
|
812
|
+
failedGroups++;
|
|
813
|
+
const keyNames = groupRows.map((row) => row.credentialKey).join(", ");
|
|
814
|
+
console.warn(
|
|
815
|
+
`[dispatch] vault boot resync failed for org=${ctx.orgId ?? "(solo)"} owner=${ctx.ownerEmail}; affected keys: ${keyNames}`,
|
|
816
|
+
error instanceof Error ? error.message : error,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return { groups: groups.size, failedGroups, syncedKeys };
|
|
822
|
+
}
|
|
823
|
+
|
|
766
824
|
export async function cleanupSyncedCredentialKeysIfUnused(
|
|
767
825
|
ctx: VaultCtx,
|
|
768
826
|
candidateKeys?: string[],
|
|
@@ -55,7 +55,8 @@ Use the standard workspace primitives:
|
|
|
55
55
|
- Use recurring jobs for scheduled behavior.
|
|
56
56
|
- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.
|
|
57
57
|
- You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.
|
|
58
|
-
-
|
|
58
|
+
- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
59
|
+
- When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
|
|
59
60
|
- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.
|
|
60
61
|
- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
|
|
61
62
|
- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
package/src/server/plugins/db.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { runMigrations } from "@agent-native/core/db";
|
|
2
2
|
|
|
3
3
|
import { dispatchMigrations } from "../../db/migrations.js";
|
|
4
|
+
import { scheduleVaultBootResync } from "../lib/vault-boot-resync.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const runDispatchMigrations = runMigrations(dispatchMigrations, {
|
|
6
7
|
table: "dispatch_migrations",
|
|
7
8
|
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run dispatch's own migrations first (this is what guarantees
|
|
12
|
+
* `vault_secrets` exists), then kick off the vault boot resync. The resync
|
|
13
|
+
* itself waits several more seconds before touching the DB — see
|
|
14
|
+
* vault-boot-resync.ts — so this ordering is a belt-and-suspenders
|
|
15
|
+
* guarantee, not a hard dependency.
|
|
16
|
+
*/
|
|
17
|
+
export default async (nitroApp: any) => {
|
|
18
|
+
await runDispatchMigrations(nitroApp);
|
|
19
|
+
scheduleVaultBootResync();
|
|
20
|
+
};
|
|
@@ -24,7 +24,8 @@ Default posture:
|
|
|
24
24
|
- Treat Slack, Telegram, and email as shared entrypoints into the workspace.
|
|
25
25
|
- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).
|
|
26
26
|
- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.
|
|
27
|
-
-
|
|
27
|
+
- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
28
|
+
- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.
|
|
28
29
|
- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
29
30
|
- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
|
|
30
31
|
- Keep durable memory and operating instructions in resources rather than ephemeral chat.
|