@agent-native/dispatch 0.10.2 → 0.10.4

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.
Files changed (49) hide show
  1. package/README.md +5 -5
  2. package/dist/actions/delete-staged-dataset.d.ts +8 -0
  3. package/dist/actions/delete-staged-dataset.d.ts.map +1 -0
  4. package/dist/actions/delete-staged-dataset.js +31 -0
  5. package/dist/actions/delete-staged-dataset.js.map +1 -0
  6. package/dist/actions/index.d.ts.map +1 -1
  7. package/dist/actions/index.js +6 -0
  8. package/dist/actions/index.js.map +1 -1
  9. package/dist/actions/list-staged-datasets.d.ts +13 -0
  10. package/dist/actions/list-staged-datasets.d.ts.map +1 -0
  11. package/dist/actions/list-staged-datasets.js +33 -0
  12. package/dist/actions/list-staged-datasets.js.map +1 -0
  13. package/dist/actions/provider-api-catalog.d.ts +1 -1
  14. package/dist/actions/provider-api-catalog.js +1 -1
  15. package/dist/actions/provider-api-catalog.js.map +1 -1
  16. package/dist/actions/provider-api-request.d.ts +14 -1
  17. package/dist/actions/provider-api-request.d.ts.map +1 -1
  18. package/dist/actions/provider-api-request.js +88 -5
  19. package/dist/actions/provider-api-request.js.map +1 -1
  20. package/dist/actions/query-staged-dataset.d.ts +28 -0
  21. package/dist/actions/query-staged-dataset.d.ts.map +1 -0
  22. package/dist/actions/query-staged-dataset.js +105 -0
  23. package/dist/actions/query-staged-dataset.js.map +1 -0
  24. package/dist/actions/scaffold-workspace-app.d.ts.map +1 -1
  25. package/dist/actions/scaffold-workspace-app.js +17 -4
  26. package/dist/actions/scaffold-workspace-app.js.map +1 -1
  27. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  28. package/dist/server/lib/app-creation-store.js +2 -0
  29. package/dist/server/lib/app-creation-store.js.map +1 -1
  30. package/dist/server/lib/provider-api.d.ts +2 -1
  31. package/dist/server/lib/provider-api.d.ts.map +1 -1
  32. package/dist/server/lib/provider-api.js +2 -1
  33. package/dist/server/lib/provider-api.js.map +1 -1
  34. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  35. package/dist/server/plugins/agent-chat.js +2 -0
  36. package/dist/server/plugins/agent-chat.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/actions/delete-staged-dataset.ts +39 -0
  39. package/src/actions/index.ts +6 -0
  40. package/src/actions/list-staged-datasets.ts +36 -0
  41. package/src/actions/provider-api-catalog.ts +1 -1
  42. package/src/actions/provider-api-request.ts +118 -5
  43. package/src/actions/query-staged-dataset.ts +125 -0
  44. package/src/actions/scaffold-workspace-app.spec.ts +33 -0
  45. package/src/actions/scaffold-workspace-app.ts +20 -5
  46. package/src/server/lib/app-creation-store.spec.ts +13 -0
  47. package/src/server/lib/app-creation-store.ts +2 -0
  48. package/src/server/lib/provider-api.ts +2 -1
  49. package/src/server/plugins/agent-chat.ts +2 -0
@@ -1,5 +1,6 @@
1
1
  import { PROVIDER_API_IDS, createProviderApiRuntime, listCustomProviders, } from "@agent-native/core/provider-api";
2
2
  import { getCredentialContext } from "@agent-native/core/server";
3
+ export const DISPATCH_APP_ID = "dispatch";
3
4
  export const DISPATCH_PROVIDER_API_IDS = PROVIDER_API_IDS;
4
5
  function requireCtx(action) {
5
6
  const ctx = getCredentialContext();
@@ -9,7 +10,7 @@ function requireCtx(action) {
9
10
  return ctx;
10
11
  }
11
12
  const runtime = createProviderApiRuntime({
12
- appId: "dispatch",
13
+ appId: DISPATCH_APP_ID,
13
14
  localCredentialSource: "dispatch_local",
14
15
  getCredentialContext: () => requireCtx("requests"),
15
16
  getCustomProviders: async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"provider-api.js","sourceRoot":"","sources":["../../../src/server/lib/provider-api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,mBAAmB,GAIpB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEjE,MAAM,CAAC,MAAM,yBAAyB,GAAG,gBAAgB,CAAC;AAI1D,SAAS,UAAU,CAAC,MAAc;IAChC,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,yBAAyB,MAAM,6CAA6C,CAC7E,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,OAAO,GAAG,wBAAwB,CAAC;IACvC,KAAK,EAAE,UAAU;IACjB,qBAAqB,EAAE,gBAAgB;IACvC,oBAAoB,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;IAClD,kBAAkB,EAAE,KAAK,IAAI,EAAE;QAC7B,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,CAAC;QACpB,2DAA2D;QAC3D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,mBAAmB,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC;YAC1C,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;SACxE,CAAC,CAAC;QACH,MAAM,aAAa,GACjB,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,MAAM,YAAY,GAChB,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,2EAA2E;QAC3E,iDAAiD;QACjD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,gBAAuC,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,aAAa,EAAE,GAAG,YAAY,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,UAAU,sBAAsB,CAAC,QAAiB;IACtD,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAIpC;IACC,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAA4B;IACpE,OAAO,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC","sourcesContent":["import {\n PROVIDER_API_IDS,\n createProviderApiRuntime,\n listCustomProviders,\n type ProviderApiId,\n type ProviderApiMethod,\n type ProviderApiRequestArgs,\n} from \"@agent-native/core/provider-api\";\nimport { getCredentialContext } from \"@agent-native/core/server\";\n\nexport const DISPATCH_PROVIDER_API_IDS = PROVIDER_API_IDS;\nexport type DispatchProviderApiId = ProviderApiId;\nexport type { ProviderApiMethod, ProviderApiRequestArgs };\n\nfunction requireCtx(action: string) {\n const ctx = getCredentialContext();\n if (!ctx) {\n throw new Error(\n `Dispatch provider API ${action} requires an authenticated request context.`,\n );\n }\n return ctx;\n}\n\nconst runtime = createProviderApiRuntime({\n appId: \"dispatch\",\n localCredentialSource: \"dispatch_local\",\n getCredentialContext: () => requireCtx(\"requests\"),\n getCustomProviders: async () => {\n const ctx = getCredentialContext();\n if (!ctx) return [];\n // Load custom providers for both user scope and org scope.\n const results = await Promise.allSettled([\n listCustomProviders(\"user\", ctx.userEmail),\n ctx.orgId ? listCustomProviders(\"org\", ctx.orgId) : Promise.resolve([]),\n ]);\n const userProviders =\n results[0].status === \"fulfilled\" ? results[0].value : [];\n const orgProviders =\n results[1].status === \"fulfilled\" ? results[1].value : [];\n // Merge: user-scope providers take precedence over org-scope ones with the\n // same id, and neither can shadow a built-in id.\n const seen = new Set<string>(PROVIDER_API_IDS as unknown as string[]);\n const merged = [];\n for (const p of [...userProviders, ...orgProviders]) {\n if (!seen.has(p.id)) {\n seen.add(p.id);\n merged.push(p);\n }\n }\n return merged;\n },\n});\n\nexport function listProviderApiCatalog(provider?: string) {\n return runtime.listCatalog(provider);\n}\n\nexport function fetchProviderApiDocs(options: {\n provider: string;\n url?: string;\n maxBytes?: number;\n}) {\n return runtime.fetchDocs(options);\n}\n\nexport function executeProviderApiRequest(args: ProviderApiRequestArgs) {\n return runtime.executeRequest(args);\n}\n"]}
1
+ {"version":3,"file":"provider-api.js","sourceRoot":"","sources":["../../../src/server/lib/provider-api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,mBAAmB,GAIpB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEjE,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAC1C,MAAM,CAAC,MAAM,yBAAyB,GAAG,gBAAgB,CAAC;AAI1D,SAAS,UAAU,CAAC,MAAc;IAChC,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,yBAAyB,MAAM,6CAA6C,CAC7E,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,OAAO,GAAG,wBAAwB,CAAC;IACvC,KAAK,EAAE,eAAe;IACtB,qBAAqB,EAAE,gBAAgB;IACvC,oBAAoB,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;IAClD,kBAAkB,EAAE,KAAK,IAAI,EAAE;QAC7B,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,CAAC;QACpB,2DAA2D;QAC3D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,mBAAmB,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC;YAC1C,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;SACxE,CAAC,CAAC;QACH,MAAM,aAAa,GACjB,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,MAAM,YAAY,GAChB,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,2EAA2E;QAC3E,iDAAiD;QACjD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,gBAAuC,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,aAAa,EAAE,GAAG,YAAY,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,UAAU,sBAAsB,CAAC,QAAiB;IACtD,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAIpC;IACC,OAAO,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAA4B;IACpE,OAAO,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC","sourcesContent":["import {\n PROVIDER_API_IDS,\n createProviderApiRuntime,\n listCustomProviders,\n type ProviderApiId,\n type ProviderApiMethod,\n type ProviderApiRequestArgs,\n} from \"@agent-native/core/provider-api\";\nimport { getCredentialContext } from \"@agent-native/core/server\";\n\nexport const DISPATCH_APP_ID = \"dispatch\";\nexport const DISPATCH_PROVIDER_API_IDS = PROVIDER_API_IDS;\nexport type DispatchProviderApiId = ProviderApiId;\nexport type { ProviderApiMethod, ProviderApiRequestArgs };\n\nfunction requireCtx(action: string) {\n const ctx = getCredentialContext();\n if (!ctx) {\n throw new Error(\n `Dispatch provider API ${action} requires an authenticated request context.`,\n );\n }\n return ctx;\n}\n\nconst runtime = createProviderApiRuntime({\n appId: DISPATCH_APP_ID,\n localCredentialSource: \"dispatch_local\",\n getCredentialContext: () => requireCtx(\"requests\"),\n getCustomProviders: async () => {\n const ctx = getCredentialContext();\n if (!ctx) return [];\n // Load custom providers for both user scope and org scope.\n const results = await Promise.allSettled([\n listCustomProviders(\"user\", ctx.userEmail),\n ctx.orgId ? listCustomProviders(\"org\", ctx.orgId) : Promise.resolve([]),\n ]);\n const userProviders =\n results[0].status === \"fulfilled\" ? results[0].value : [];\n const orgProviders =\n results[1].status === \"fulfilled\" ? results[1].value : [];\n // Merge: user-scope providers take precedence over org-scope ones with the\n // same id, and neither can shadow a built-in id.\n const seen = new Set<string>(PROVIDER_API_IDS as unknown as string[]);\n const merged = [];\n for (const p of [...userProviders, ...orgProviders]) {\n if (!seen.has(p.id)) {\n seen.add(p.id);\n merged.push(p);\n }\n }\n return merged;\n },\n});\n\nexport function listProviderApiCatalog(provider?: string) {\n return runtime.listCatalog(provider);\n}\n\nexport function fetchProviderApiDocs(options: {\n provider: string;\n url?: string;\n maxBytes?: number;\n}) {\n return runtime.fetchDocs(options);\n}\n\nexport function executeProviderApiRequest(args: ProviderApiRequestArgs) {\n return runtime.executeRequest(args);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"agent-chat.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":";AAIA,wBAoCG"}
1
+ {"version":3,"file":"agent-chat.d.ts","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":";AAIA,wBAsCG"}
@@ -14,6 +14,7 @@ export default createAgentChatPlugin({
14
14
  // a build-time-generated `.generated/actions-registry.ts` (the latter is a
15
15
  // template-only construct that the Vite plugin emits next to actions/).
16
16
  actions: dispatchActions,
17
+ codeExecution: { production: "sandboxed" },
17
18
  systemPrompt: `You are the central dispatch for this workspace.
18
19
 
19
20
  Default posture:
@@ -32,6 +33,7 @@ Use the standard workspace primitives:
32
33
  - If the starter 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 "Starter", "Blank app", or "New app" UI behind.
33
34
  - 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.
34
35
  - 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.
36
+ - 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.
35
37
 
36
38
  When a user asks for something like a digest, reminder, routing rule, or saved behavior:
37
39
  - First decide whether it should be a resource, a recurring job, a destination, or a delegated task.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,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,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;4EAsB4D;CAC3E,CAAC,CAAC","sourcesContent":["import { createAgentChatPlugin } from \"@agent-native/core/server\";\nimport { getOrgContext } from \"@agent-native/core/org\";\nimport { dispatchActions } from \"../../actions/index.js\";\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\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 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- When answering whether workspace apps expose agent cards or A2A endpoints, 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 starter 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 \"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\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
+ {"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,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;;;;;;;;;;;;;;;;;;;;;;;4EAuB4D;CAC3E,CAAC,CAAC","sourcesContent":["import { createAgentChatPlugin } from \"@agent-native/core/server\";\nimport { getOrgContext } from \"@agent-native/core/org\";\nimport { dispatchActions } from \"../../actions/index.js\";\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\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- When answering whether workspace apps expose agent cards or A2A endpoints, 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 starter 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 \"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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "type": "module",
5
5
  "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.",
6
6
  "license": "MIT",
@@ -0,0 +1,39 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { deleteStagedDataset } from "@agent-native/core/provider-api/staged-datasets-store";
3
+ import { getCredentialContext } from "@agent-native/core/server/request-context";
4
+ import { z } from "zod";
5
+ import { DISPATCH_APP_ID } from "../server/lib/provider-api.js";
6
+
7
+ export default defineAction({
8
+ description:
9
+ "Delete a staged dataset by id, freeing its scratch storage. Use after analysis is complete or before re-staging under the same name. Only the owner who staged the dataset can delete it.",
10
+ schema: z.object({
11
+ datasetId: z
12
+ .string()
13
+ .min(1)
14
+ .describe(
15
+ "Dataset id to delete (from list-staged-datasets or provider-api-request stageAs result).",
16
+ ),
17
+ }),
18
+ http: false,
19
+ run: async (args) => {
20
+ const ctx = getCredentialContext();
21
+ if (!ctx) {
22
+ throw new Error("No authenticated context for delete-staged-dataset.");
23
+ }
24
+
25
+ const deleted = await deleteStagedDataset({
26
+ id: args.datasetId,
27
+ appId: DISPATCH_APP_ID,
28
+ ownerEmail: ctx.userEmail,
29
+ });
30
+
31
+ if (!deleted) {
32
+ throw new Error(
33
+ `Dataset ${args.datasetId} not found (or belongs to a different owner/app).`,
34
+ );
35
+ }
36
+
37
+ return { deleted: true, datasetId: args.datasetId };
38
+ },
39
+ });
@@ -11,6 +11,7 @@ import createWorkspaceResourceGrant from "./create-workspace-resource-grant.js";
11
11
  import createWorkspaceResource from "./create-workspace-resource.js";
12
12
  import createDreamReport from "./create-dream-report.js";
13
13
  import createEmbedSession from "./create_embed_session.js";
14
+ import deleteStagedDataset from "./delete-staged-dataset.js";
14
15
  import deleteDestination from "./delete-destination.js";
15
16
  import deleteVaultSecret from "./delete-vault-secret.js";
16
17
  import deleteWorkspaceResource from "./delete-workspace-resource.js";
@@ -40,6 +41,7 @@ import listIntegrationsCatalog from "./list-integrations-catalog.js";
40
41
  import listLinkedIdentities from "./list-linked-identities.js";
41
42
  import listMcpAppAccess from "./list-mcp-app-access.js";
42
43
  import listApps from "./list_apps.js";
44
+ import listStagedDatasets from "./list-staged-datasets.js";
43
45
  import listVaultAudit from "./list-vault-audit.js";
44
46
  import listVaultGrants from "./list-vault-grants.js";
45
47
  import listVaultRequests from "./list-vault-requests.js";
@@ -59,6 +61,7 @@ import providerApiCatalog from "./provider-api-catalog.js";
59
61
  import providerApiDocs from "./provider-api-docs.js";
60
62
  import providerApiRegister from "./provider-api-register.js";
61
63
  import providerApiRequest from "./provider-api-request.js";
64
+ import queryStagedDataset from "./query-staged-dataset.js";
62
65
  import rejectDispatchChange from "./reject-dispatch-change.js";
63
66
  import rejectDreamProposal from "./reject-dream-proposal.js";
64
67
  import removePendingWorkspaceApp from "./remove-pending-workspace-app.js";
@@ -103,6 +106,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
103
106
  "create-workspace-resource": createWorkspaceResource,
104
107
  "create-dream-report": createDreamReport,
105
108
  create_embed_session: createEmbedSession,
109
+ "delete-staged-dataset": deleteStagedDataset,
106
110
  "delete-destination": deleteDestination,
107
111
  "delete-vault-secret": deleteVaultSecret,
108
112
  "delete-workspace-resource": deleteWorkspaceResource,
@@ -133,6 +137,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
133
137
  "list-linked-identities": listLinkedIdentities,
134
138
  "list-mcp-app-access": listMcpAppAccess,
135
139
  list_apps: listApps,
140
+ "list-staged-datasets": listStagedDatasets,
136
141
  "list-vault-audit": listVaultAudit,
137
142
  "list-vault-grants": listVaultGrants,
138
143
  "list-vault-requests": listVaultRequests,
@@ -152,6 +157,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
152
157
  "provider-api-docs": providerApiDocs,
153
158
  "provider-api-register": providerApiRegister,
154
159
  "provider-api-request": providerApiRequest,
160
+ "query-staged-dataset": queryStagedDataset,
155
161
  "reject-dispatch-change": rejectDispatchChange,
156
162
  "reject-dream-proposal": rejectDreamProposal,
157
163
  "remove-pending-workspace-app": removePendingWorkspaceApp,
@@ -0,0 +1,36 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { listStagedDatasets } from "@agent-native/core/provider-api/staged-datasets-store";
3
+ import { getCredentialContext } from "@agent-native/core/server/request-context";
4
+ import { z } from "zod";
5
+ import { DISPATCH_APP_ID } from "../server/lib/provider-api.js";
6
+
7
+ export default defineAction({
8
+ description:
9
+ "List staged datasets stored by provider-api-request (stageAs) for the current user. Returns dataset ids, names, row counts, columns, and sizes. Use dataset ids with query-staged-dataset to aggregate, or with delete-staged-dataset to free scratch storage.",
10
+ schema: z.object({}),
11
+ http: { method: "GET" },
12
+ readOnly: true,
13
+ run: async () => {
14
+ const ctx = getCredentialContext();
15
+ if (!ctx) {
16
+ throw new Error("No authenticated context for list-staged-datasets.");
17
+ }
18
+
19
+ const datasets = await listStagedDatasets({
20
+ appId: DISPATCH_APP_ID,
21
+ ownerEmail: ctx.userEmail,
22
+ });
23
+
24
+ return {
25
+ datasets: datasets.map((d) => ({
26
+ id: d.id,
27
+ name: d.name,
28
+ rowCount: d.rowCount,
29
+ columns: d.columns,
30
+ byteSize: d.byteSize,
31
+ updatedAt: new Date(d.updatedAt).toISOString(),
32
+ })),
33
+ total: datasets.length,
34
+ };
35
+ },
36
+ });
@@ -20,7 +20,7 @@ export default defineAction({
20
20
  return {
21
21
  providers,
22
22
  guidance:
23
- "Workspace integrations and grants are not capability limits. When a provider can answer a question through its HTTP API, inspect docs/spec URLs here and call provider-api-request with the exact provider API method/path/query/body instead of adding a rigid one-off action. Custom providers registered via provider-api-register also appear here.",
23
+ "Workspace integrations and grants are not capability limits. When a provider can answer a question through its HTTP API, inspect docs/spec URLs here and call provider-api-request with the exact provider API method/path/query/body instead of adding a rigid one-off action. For broad searches, joins, classification, or absence claims, stage or save the full bounded corpus and reduce it with query-staged-dataset or run-code. Custom providers registered via provider-api-register also appear here.",
24
24
  };
25
25
  },
26
26
  });
@@ -1,9 +1,69 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import { stagingExecuteRequest } from "@agent-native/core/provider-api/staging";
3
+ import { getCredentialContext } from "@agent-native/core/server/request-context";
2
4
  import { z } from "zod";
3
- import { executeProviderApiRequest } from "../server/lib/provider-api.js";
5
+ import {
6
+ DISPATCH_APP_ID,
7
+ executeProviderApiRequest,
8
+ } from "../server/lib/provider-api.js";
4
9
 
5
10
  const MethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
6
11
 
12
+ const PaginationSchema = z
13
+ .object({
14
+ nextCursorPath: z
15
+ .string()
16
+ .optional()
17
+ .describe(
18
+ "Dot-path in the response JSON where the next cursor/token lives, e.g. 'next_cursor', 'meta.next', or 'nextPageToken'.",
19
+ ),
20
+ cursorParam: z
21
+ .string()
22
+ .optional()
23
+ .describe(
24
+ "Query parameter name to inject the cursor into the next request. Use cursorBodyPath for APIs that page through POST bodies.",
25
+ ),
26
+ cursorBodyPath: z
27
+ .string()
28
+ .optional()
29
+ .describe(
30
+ "Dot-path in the JSON request body to set to the next cursor. Use this for POST-body pagination.",
31
+ ),
32
+ pageParam: z
33
+ .string()
34
+ .optional()
35
+ .describe(
36
+ "Use page-number mode: this query param is incremented on each page.",
37
+ ),
38
+ startPage: z.coerce
39
+ .number()
40
+ .int()
41
+ .optional()
42
+ .describe("Starting page number for pageParam mode (default 1)."),
43
+ offsetParam: z
44
+ .string()
45
+ .optional()
46
+ .describe(
47
+ "Use offset mode: this query param is incremented by pageSize on each request.",
48
+ ),
49
+ pageSize: z.coerce
50
+ .number()
51
+ .int()
52
+ .min(1)
53
+ .optional()
54
+ .describe(
55
+ "Expected page size for offset increments. Defaults to the actual item count of the first page.",
56
+ ),
57
+ maxPages: z.coerce
58
+ .number()
59
+ .int()
60
+ .min(1)
61
+ .max(200)
62
+ .optional()
63
+ .describe("Maximum pages to fetch server-side (default 50, max 200)."),
64
+ })
65
+ .optional();
66
+
7
67
  export default defineAction({
8
68
  description:
9
69
  "Make an arbitrary authenticated HTTP request to a shared workspace integration, configured provider API, or custom provider registered via provider-api-register. Use this as the flexible escape hatch when Dispatch needs a provider endpoint, filter, pagination mode, payload, or API version that no canned action models. The request is constrained to the provider host, uses configured credentials automatically, blocks private/internal URLs, and redacts secrets from responses.",
@@ -77,8 +137,24 @@ export default defineAction({
77
137
  .string()
78
138
  .optional()
79
139
  .describe(
80
- "Workspace file path to save the full response body to instead of returning it in context (e.g. 'analysis/hubspot-deals.json'). When set, returns only a compact summary {savedTo, status, bytes, preview} and allows up to 20MB response. Useful for large datasets that would overflow context.",
140
+ "Workspace file path to save the full response body to instead of returning it in context (e.g. 'analysis/provider-response.json'). When set, returns only a compact summary {savedTo, status, bytes, preview} and allows up to 20MB response. Useful for large datasets that would overflow context.",
141
+ ),
142
+ stageAs: z
143
+ .string()
144
+ .min(1)
145
+ .optional()
146
+ .describe(
147
+ "When set, parse the response as an array of records and write them into a staged dataset with this name. Returns a compact summary instead of the raw body. Re-staging the same name replaces the previous dataset.",
148
+ ),
149
+ itemsPath: z
150
+ .string()
151
+ .optional()
152
+ .describe(
153
+ "Dot-path to the items array in the response JSON, e.g. 'items', 'results', or 'data'. Omit for auto-detection.",
81
154
  ),
155
+ pagination: PaginationSchema.describe(
156
+ "Pagination config for server-side fetchAll when stageAs is set. Supports cursor (nextCursorPath + cursorParam or cursorBodyPath), page, and offset modes.",
157
+ ),
82
158
  fetchAllPages: z
83
159
  .object({
84
160
  cursorPath: z
@@ -88,8 +164,15 @@ export default defineAction({
88
164
  ),
89
165
  cursorParam: z
90
166
  .string()
167
+ .optional()
168
+ .describe(
169
+ "Query parameter name to pass the cursor on subsequent pages, e.g. 'cursor' or 'page_token'. Use cursorBodyPath instead for APIs that put cursors in POST bodies.",
170
+ ),
171
+ cursorBodyPath: z
172
+ .string()
173
+ .optional()
91
174
  .describe(
92
- "Query parameter name to pass the cursor on subsequent pages, e.g. 'cursor' or 'page_token'.",
175
+ "Dot-path in the JSON request body to set to the next cursor. Use for POST-body pagination.",
93
176
  ),
94
177
  itemsPath: z
95
178
  .string()
@@ -109,9 +192,39 @@ export default defineAction({
109
192
  })
110
193
  .optional()
111
194
  .describe(
112
- "Enable cursor-based pagination. After each response, reads cursorPath from the JSON body and re-issues the request with cursorParam set, accumulating items from itemsPath (or whole bodies) until cursor is empty or maxPages is reached. Combine with saveToFile to write the full dataset to a workspace file.",
195
+ "Enable cursor-based pagination. After each response, reads cursorPath from the JSON body and re-issues the request with cursorParam or cursorBodyPath set, accumulating items from itemsPath (or whole bodies) until cursor is empty or maxPages is reached. Combine with saveToFile to write the full dataset to a workspace file.",
113
196
  ),
114
197
  }),
115
198
  http: false,
116
- run: async (args) => executeProviderApiRequest(args),
199
+ run: async (args) => {
200
+ if (args.stageAs) {
201
+ const ctx = getCredentialContext();
202
+ if (!ctx) {
203
+ throw new Error("No authenticated context for provider API staging.");
204
+ }
205
+ return stagingExecuteRequest(
206
+ {
207
+ provider: args.provider,
208
+ method: args.method,
209
+ path: args.path,
210
+ query: args.query,
211
+ headers: args.headers,
212
+ body: args.body,
213
+ auth: args.auth,
214
+ connectionId: args.connectionId,
215
+ accountId: args.accountId,
216
+ timeoutMs: args.timeoutMs,
217
+ maxBytes: args.maxBytes,
218
+ stageAs: args.stageAs,
219
+ itemsPath: args.itemsPath,
220
+ pagination: args.pagination,
221
+ },
222
+ (reqArgs) => executeProviderApiRequest(reqArgs),
223
+ { appId: DISPATCH_APP_ID, ownerEmail: ctx.userEmail },
224
+ );
225
+ }
226
+ return executeProviderApiRequest(
227
+ args as unknown as Parameters<typeof executeProviderApiRequest>[0],
228
+ );
229
+ },
117
230
  });
@@ -0,0 +1,125 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { runAggregateQuery } from "@agent-native/core/provider-api/staged-datasets-aggregate";
3
+ import {
4
+ getStagedDatasetMeta,
5
+ getStagedDatasetRows,
6
+ } from "@agent-native/core/provider-api/staged-datasets-store";
7
+ import { getCredentialContext } from "@agent-native/core/server/request-context";
8
+ import { z } from "zod";
9
+ import { DISPATCH_APP_ID } from "../server/lib/provider-api.js";
10
+
11
+ const WhereSchema = z.object({
12
+ column: z.string().min(1),
13
+ op: z.enum([
14
+ "equals",
15
+ "not_equals",
16
+ "contains",
17
+ "not_contains",
18
+ "gt",
19
+ "gte",
20
+ "lt",
21
+ "lte",
22
+ "exists",
23
+ "not_exists",
24
+ ]),
25
+ value: z.unknown().optional(),
26
+ });
27
+
28
+ const AggregateFieldSchema = z.object({
29
+ column: z.string().min(1).describe("Column to aggregate."),
30
+ op: z
31
+ .enum(["sum", "avg", "count", "min", "max", "count_distinct"])
32
+ .describe("Aggregation function."),
33
+ as: z
34
+ .string()
35
+ .optional()
36
+ .describe("Output column name. Default: {op}_{column}."),
37
+ });
38
+
39
+ export default defineAction({
40
+ description:
41
+ "Run a filter/aggregate/project query over a staged dataset previously written by provider-api-request (stageAs). Use after staging provider records, messages, documents, issues, events, or search results to count, group, filter, or project rows without re-fetching provider APIs.",
42
+ schema: z.object({
43
+ datasetId: z
44
+ .string()
45
+ .min(1)
46
+ .describe(
47
+ "Dataset id from provider-api-request stageAs result, or from list-staged-datasets.",
48
+ ),
49
+ where: z
50
+ .array(WhereSchema)
51
+ .optional()
52
+ .describe(
53
+ "Row-level filters (AND). Ops: equals, not_equals, contains, not_contains, gt, gte, lt, lte, exists, not_exists.",
54
+ ),
55
+ groupBy: z
56
+ .array(z.string().min(1))
57
+ .optional()
58
+ .describe(
59
+ "Column(s) to group by. Omit for a single aggregate over all rows.",
60
+ ),
61
+ aggregate: z
62
+ .array(AggregateFieldSchema)
63
+ .optional()
64
+ .describe(
65
+ "Aggregation operations. When set, non-group columns are aggregated. Omit to return raw rows.",
66
+ ),
67
+ select: z
68
+ .array(z.string().min(1))
69
+ .optional()
70
+ .describe("Column projection when aggregate is empty."),
71
+ orderBy: z.string().optional().describe("Sort output by this column."),
72
+ orderDir: z
73
+ .enum(["asc", "desc"])
74
+ .optional()
75
+ .describe("Sort direction (default asc)."),
76
+ limit: z.coerce
77
+ .number()
78
+ .int()
79
+ .min(1)
80
+ .max(10_000)
81
+ .optional()
82
+ .describe("Maximum rows to return (default all, max 10000)."),
83
+ }),
84
+ http: false,
85
+ readOnly: true,
86
+ run: async (args) => {
87
+ const ctx = getCredentialContext();
88
+ if (!ctx) {
89
+ throw new Error("No authenticated context for query-staged-dataset.");
90
+ }
91
+
92
+ const meta = await getStagedDatasetMeta({
93
+ id: args.datasetId,
94
+ appId: DISPATCH_APP_ID,
95
+ ownerEmail: ctx.userEmail,
96
+ });
97
+ if (!meta) {
98
+ throw new Error(
99
+ `Dataset ${args.datasetId} not found (or belongs to a different owner/app).`,
100
+ );
101
+ }
102
+
103
+ const rows = await getStagedDatasetRows({
104
+ id: args.datasetId,
105
+ appId: DISPATCH_APP_ID,
106
+ ownerEmail: ctx.userEmail,
107
+ });
108
+
109
+ const result = runAggregateQuery(rows, {
110
+ where: args.where,
111
+ groupBy: args.groupBy,
112
+ aggregate: args.aggregate,
113
+ select: args.select,
114
+ orderBy: args.orderBy,
115
+ orderDir: args.orderDir,
116
+ limit: args.limit,
117
+ });
118
+
119
+ return {
120
+ dataset: { id: meta.id, name: meta.name, totalRows: meta.rowCount },
121
+ rowCount: result.length,
122
+ rows: result,
123
+ };
124
+ },
125
+ });
@@ -0,0 +1,33 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ scaffoldWorkspaceAppFromTemplate: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("../server/lib/app-creation-store.js", () => ({
8
+ scaffoldWorkspaceAppFromTemplate: mocks.scaffoldWorkspaceAppFromTemplate,
9
+ }));
10
+
11
+ import action from "./scaffold-workspace-app.js";
12
+
13
+ describe("scaffold-workspace-app action", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ it("surfaces scaffold failures as user-facing action errors", async () => {
19
+ mocks.scaffoldWorkspaceAppFromTemplate.mockRejectedValueOnce(
20
+ new Error(
21
+ "Scaffolding from Dispatch is only available in local development.",
22
+ ),
23
+ );
24
+
25
+ await expect(
26
+ action.run({ template: "brain", appId: null }),
27
+ ).rejects.toMatchObject({
28
+ message:
29
+ "Scaffolding from Dispatch is only available in local development.",
30
+ statusCode: 400,
31
+ });
32
+ });
33
+ });
@@ -3,6 +3,16 @@ import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
3
3
  import { z } from "zod";
4
4
  import { scaffoldWorkspaceAppFromTemplate } from "../server/lib/app-creation-store.js";
5
5
 
6
+ function userFacingActionError(err: unknown): Error & { statusCode: number } {
7
+ const message =
8
+ err instanceof Error && err.message ? err.message : String(err);
9
+ const error = new Error(
10
+ message || "Could not scaffold workspace app.",
11
+ ) as Error & { statusCode: number };
12
+ error.statusCode = 400;
13
+ return error;
14
+ }
15
+
6
16
  export default defineAction({
7
17
  description:
8
18
  "Scaffold a first-party template (mail, calendar, slides, etc.) into apps/<id>/ in the current workspace. Local-dev only — runs `agent-native add-app` as a subprocess. The workspace gateway picks the new app up automatically and serves it at /<id>. For natural-language app creation, call start-workspace-app-creation instead.",
@@ -26,9 +36,14 @@ export default defineAction({
26
36
  "Optional override for the apps/<id>/ directory name; defaults to the template name",
27
37
  ),
28
38
  }),
29
- run: async (input) =>
30
- scaffoldWorkspaceAppFromTemplate({
31
- template: input.template,
32
- appId: input.appId ?? null,
33
- }),
39
+ run: async (input) => {
40
+ try {
41
+ return await scaffoldWorkspaceAppFromTemplate({
42
+ template: input.template,
43
+ appId: input.appId ?? null,
44
+ });
45
+ } catch (err) {
46
+ throw userFacingActionError(err);
47
+ }
48
+ },
34
49
  });
@@ -362,4 +362,17 @@ describe("listWorkspaceApps", () => {
362
362
  expect.arrayContaining(["brain", "assets"]),
363
363
  );
364
364
  });
365
+
366
+ it("hides local scaffold templates in hosted runtimes", async () => {
367
+ stubNoPendingContext();
368
+ vi.stubEnv("NETLIFY", "1");
369
+ stubManifest([{ id: "dispatch", name: "Dispatch", path: "/dispatch" }]);
370
+
371
+ const templates = await runWithRequestContext(
372
+ { userEmail: "dev@example.test" },
373
+ () => listAvailableWorkspaceTemplates(),
374
+ );
375
+
376
+ expect(templates).toEqual([]);
377
+ });
365
378
  });
@@ -1364,6 +1364,8 @@ const ADDABLE_TEMPLATES: AvailableWorkspaceTemplate[] = [
1364
1364
  export async function listAvailableWorkspaceTemplates(): Promise<
1365
1365
  AvailableWorkspaceTemplate[]
1366
1366
  > {
1367
+ if (!isLocalAppCreationRuntime()) return [];
1368
+
1367
1369
  const installed = new Set(
1368
1370
  (await listWorkspaceApps({ includeArchived: true })).map((app) => app.id),
1369
1371
  );
@@ -8,6 +8,7 @@ import {
8
8
  } from "@agent-native/core/provider-api";
9
9
  import { getCredentialContext } from "@agent-native/core/server";
10
10
 
11
+ export const DISPATCH_APP_ID = "dispatch";
11
12
  export const DISPATCH_PROVIDER_API_IDS = PROVIDER_API_IDS;
12
13
  export type DispatchProviderApiId = ProviderApiId;
13
14
  export type { ProviderApiMethod, ProviderApiRequestArgs };
@@ -23,7 +24,7 @@ function requireCtx(action: string) {
23
24
  }
24
25
 
25
26
  const runtime = createProviderApiRuntime({
26
- appId: "dispatch",
27
+ appId: DISPATCH_APP_ID,
27
28
  localCredentialSource: "dispatch_local",
28
29
  getCredentialContext: () => requireCtx("requests"),
29
30
  getCustomProviders: async () => {
@@ -15,6 +15,7 @@ export default createAgentChatPlugin({
15
15
  // a build-time-generated `.generated/actions-registry.ts` (the latter is a
16
16
  // template-only construct that the Vite plugin emits next to actions/).
17
17
  actions: dispatchActions,
18
+ codeExecution: { production: "sandboxed" },
18
19
  systemPrompt: `You are the central dispatch for this workspace.
19
20
 
20
21
  Default posture:
@@ -33,6 +34,7 @@ Use the standard workspace primitives:
33
34
  - If the starter 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 "Starter", "Blank app", or "New app" UI behind.
34
35
  - 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.
35
36
  - 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.
37
+ - 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.
36
38
 
37
39
  When a user asks for something like a digest, reminder, routing rule, or saved behavior:
38
40
  - First decide whether it should be a resource, a recurring job, a destination, or a delegated task.