@agent-native/core 0.147.0 → 0.148.0

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 (172) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +115 -68
  3. package/corpus/templates/analytics/DEVELOPING.md +50 -3
  4. package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +190 -0
  5. package/corpus/templates/analytics/app/i18n/index.ts +14 -29
  6. package/corpus/templates/analytics/app/i18n-data.ts +10 -10
  7. package/corpus/templates/analytics/app/pages/Settings.tsx +5 -21
  8. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -3
  9. package/corpus/templates/analytics/scripts/first-party-analytics-bigquery-backfill.ts +197 -18
  10. package/corpus/templates/analytics/server/db/schema.ts +23 -0
  11. package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +3 -1
  12. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +1 -1
  13. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +113 -13
  14. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +140 -0
  15. package/corpus/templates/analytics/server/lib/first-party-analytics-volume.ts +207 -0
  16. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +40 -9
  17. package/corpus/templates/analytics/server/lib/gcloud.ts +148 -20
  18. package/corpus/templates/analytics/server/plugins/db.ts +42 -0
  19. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +115 -68
  20. package/corpus/templates/assets/app/i18n/index.ts +14 -29
  21. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +115 -68
  22. package/corpus/templates/brain/app/i18n/index.ts +14 -29
  23. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +115 -68
  24. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
  25. package/corpus/templates/chat/app/i18n/index.ts +14 -29
  26. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +115 -68
  27. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -7
  28. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +18 -13
  29. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +87 -370
  30. package/corpus/templates/clips/app/components/settings/slack-section.tsx +59 -101
  31. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +136 -213
  32. package/corpus/templates/clips/app/i18n/en-US.ts +11 -8
  33. package/corpus/templates/clips/desktop/src/app.tsx +1 -1
  34. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +115 -68
  35. package/corpus/templates/content/app/i18n/index.ts +14 -29
  36. package/corpus/templates/crm/app/i18n/index.ts +14 -29
  37. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +115 -68
  38. package/corpus/templates/design/actions/analyze-brand-assets.ts +15 -15
  39. package/corpus/templates/design/actions/import-code.ts +2 -43
  40. package/corpus/templates/design/app/i18n/index.ts +14 -29
  41. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +115 -68
  42. package/corpus/templates/dispatch/app/i18n-data.ts +1 -1
  43. package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +115 -68
  44. package/corpus/templates/factory/AGENTS.md +40 -49
  45. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +66 -0
  46. package/corpus/templates/factory/actions/evaluate-triage-item.ts +37 -0
  47. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -0
  48. package/corpus/templates/factory/actions/get-triage-item.ts +25 -4
  49. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +55 -0
  50. package/corpus/templates/factory/actions/ingest-github-observation.ts +21 -0
  51. package/corpus/templates/factory/actions/list-factory-audit.ts +118 -0
  52. package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
  53. package/corpus/templates/factory/actions/list-triage-items.ts +24 -2
  54. package/corpus/templates/factory/actions/poll-github-sources.ts +60 -0
  55. package/corpus/templates/factory/actions/poll-sentry-errors.ts +39 -0
  56. package/corpus/templates/factory/actions/poll-slack-channel.ts +45 -1
  57. package/corpus/templates/factory/actions/run-factory-automation.ts +1 -1
  58. package/corpus/templates/factory/actions/save-factory-automation.ts +2 -2
  59. package/corpus/templates/factory/actions/start-builder-for-item.ts +51 -0
  60. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +685 -0
  61. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +6 -0
  62. package/corpus/templates/factory/app/i18n/en-US.ts +25 -0
  63. package/corpus/templates/factory/app/routes/factory.tsx +95 -42
  64. package/corpus/templates/factory/server/db/schema.ts +37 -0
  65. package/corpus/templates/factory/server/lib/require-factory-automation.ts +5 -1
  66. package/corpus/templates/factory/server/plugins/factory-migrations.ts +29 -0
  67. package/corpus/templates/factory/server/triage/audit.ts +79 -0
  68. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +115 -68
  69. package/corpus/templates/forms/app/i18n/index.ts +14 -29
  70. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +115 -68
  71. package/corpus/templates/macros/app/i18n/index.ts +14 -29
  72. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +115 -68
  73. package/corpus/templates/mail/app/i18n/index.ts +14 -29
  74. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +115 -68
  75. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +7 -7
  76. package/corpus/templates/plan/app/i18n/index.ts +14 -29
  77. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +115 -68
  78. package/corpus/templates/slides/actions/analyze-brand-assets.ts +15 -15
  79. package/corpus/templates/slides/actions/import-code.ts +2 -43
  80. package/corpus/templates/slides/app/i18n/index.ts +14 -29
  81. package/corpus/templates/slides/server/routes/api/preview-images.get.ts +4 -1
  82. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +115 -68
  83. package/corpus/templates/tasks/app/components/layout/HeaderActions.tsx +1 -84
  84. package/corpus/templates/tasks/app/hooks/use-mobile.tsx +1 -21
  85. package/corpus/templates/tasks/app/i18n/index.ts +14 -29
  86. package/dist/agent/engine/ai-sdk-engine.js +2 -1
  87. package/dist/agent/engine/openai-compatible-endpoint.d.ts +2 -0
  88. package/dist/agent/engine/openai-compatible-endpoint.js +5 -1
  89. package/dist/agent/engine/provider-endpoint-validation.d.ts +9 -0
  90. package/dist/agent/engine/provider-endpoint-validation.js +26 -0
  91. package/dist/agent/engine/provider-env-vars.d.ts +1 -1
  92. package/dist/agent/engine/provider-env-vars.js +1 -1
  93. package/dist/agent/engine/registry.d.ts +8 -6
  94. package/dist/agent/engine/registry.js +37 -24
  95. package/dist/agent/tool-search.d.ts +1 -0
  96. package/dist/agent/tool-search.js +17 -1
  97. package/dist/automations/service.js +4 -10
  98. package/dist/brand-kit/brand-signals.d.ts +13 -0
  99. package/dist/brand-kit/brand-signals.js +15 -0
  100. package/dist/brand-kit/index.d.ts +2 -2
  101. package/dist/brand-kit/index.js +1 -1
  102. package/dist/cli/plan-blocks.d.ts +3 -18
  103. package/dist/cli/plan-blocks.js +2 -95
  104. package/dist/cli/plan-publish-store.d.ts +2 -61
  105. package/dist/cli/plan-publish-store.js +2 -127
  106. package/dist/cli/skills-content/connection.d.ts +1 -1
  107. package/dist/cli/skills-content/connection.js +1 -52
  108. package/dist/cli/skills-content/visual-recap-skill.d.ts +1 -1
  109. package/dist/cli/skills-content/visual-recap-skill.js +1 -553
  110. package/dist/client/agent-engine-key.d.ts +14 -4
  111. package/dist/client/agent-engine-key.js +51 -5
  112. package/dist/client/agent-provider-catalog.d.ts +20 -0
  113. package/dist/client/agent-provider-catalog.js +100 -0
  114. package/dist/client/blocks/index.d.ts +1 -0
  115. package/dist/client/blocks/index.js +1 -0
  116. package/dist/client/chat/run-recovery.js +4 -48
  117. package/dist/client/chat-first.js +4 -3
  118. package/dist/client/i18n.d.ts +14 -0
  119. package/dist/client/i18n.js +18 -0
  120. package/dist/client/settings/ProviderSetupForm.d.ts +21 -0
  121. package/dist/client/settings/ProviderSetupForm.js +145 -0
  122. package/dist/client/settings/SettingsPanel.js +52 -49
  123. package/dist/client/settings/index.d.ts +2 -0
  124. package/dist/client/settings/index.js +2 -0
  125. package/dist/coding-tools/run-code.d.ts +5 -0
  126. package/dist/coding-tools/run-code.js +196 -36
  127. package/dist/coding-tools/tool-orchestration.d.ts +10 -0
  128. package/dist/coding-tools/tool-orchestration.js +107 -0
  129. package/dist/collab/awareness.d.ts +2 -2
  130. package/dist/jobs/frontmatter.d.ts +9 -0
  131. package/dist/jobs/frontmatter.js +16 -0
  132. package/dist/jobs/scheduler.js +8 -8
  133. package/dist/jobs/tools.d.ts +1 -1
  134. package/dist/jobs/tools.js +2 -7
  135. package/dist/localization/default-messages.d.ts +20 -0
  136. package/dist/localization/default-messages.js +25 -5
  137. package/dist/observability/routes.d.ts +3 -3
  138. package/dist/onboarding/app-profile.js +1 -1
  139. package/dist/progress/routes.d.ts +1 -1
  140. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  141. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  142. package/dist/resources/handlers.d.ts +1 -1
  143. package/dist/scripts/agent-engines/test-agent-engine.js +34 -10
  144. package/dist/secrets/routes.d.ts +9 -9
  145. package/dist/server/agent-chat/run-code-tools.d.ts +1 -1
  146. package/dist/server/agent-chat/run-code-tools.js +3 -1
  147. package/dist/server/agent-chat-plugin.js +6 -6
  148. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  149. package/dist/server/agent-engine-api-key-route.js +43 -12
  150. package/dist/server/agents-bundle.js +2 -1
  151. package/dist/server/auth.js +21 -5
  152. package/dist/server/credential-provider.js +1 -0
  153. package/dist/server/design-token-utils.d.ts +18 -0
  154. package/dist/server/design-token-utils.js +31 -0
  155. package/dist/server/onboarding-html.js +22 -2
  156. package/dist/server/realtime-token.d.ts +1 -1
  157. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
  158. package/dist/templates/chat/app/i18n/index.ts +14 -29
  159. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +115 -68
  160. package/dist/templates/default/app/i18n/index.ts +14 -29
  161. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +22 -9
  162. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +115 -68
  163. package/dist/triggers/dispatcher.js +1 -8
  164. package/docs/content/environment-variables.mdx +39 -38
  165. package/package.json +4 -4
  166. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
  167. package/src/templates/chat/app/i18n/index.ts +14 -29
  168. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +115 -68
  169. package/src/templates/default/app/i18n/index.ts +14 -29
  170. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +22 -9
  171. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +115 -68
  172. package/corpus/templates/plan/app/components/plan/wireframe/sanitize-html.ts +0 -343
@@ -6,8 +6,9 @@ import { IconChevronDown, IconChevronRight, IconCheck, IconExternalLink, IconBra
6
6
  import React, { Suspense, lazy, useState, useEffect, useCallback, useMemo, useRef, } from "react";
7
7
  import { PROVIDER_ENV_PLACEHOLDERS } from "../../agent/engine/provider-env-vars.js";
8
8
  import { buildSettingsRoute } from "../../navigation/index.js";
9
- import { saveAgentEngineProviderSettings } from "../agent-engine-key.js";
9
+ import { saveAgentEngineProviderSettings, setAgentEngineProvider, } from "../agent-engine-key.js";
10
10
  import { AgentWorkspaceContent } from "../agent-page/AgentWorkspaceContent.js";
11
+ import { getAgentProviderOption, providerIdForEngine, } from "../agent-provider-catalog.js";
11
12
  import { agentNativePath } from "../api-path.js";
12
13
  import { BuilderBMark } from "../builder-mark.js";
13
14
  import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover.js";
@@ -24,6 +25,7 @@ import { AgentsSection } from "./AgentsSection.js";
24
25
  import { AutomationsSection } from "./AutomationsSection.js";
25
26
  import { DemoModeSection } from "./DemoModeSection.js";
26
27
  import { ExtensionsSettingsContent } from "./ExtensionsSettingsContent.js";
28
+ import { AgentProviderPicker } from "./ProviderSetupForm.js";
27
29
  import { SecretsSection } from "./SecretsSection.js";
28
30
  import { SettingsGroup, SettingsRow } from "./SettingsRow.js";
29
31
  import { SettingsSection, SettingsSurfaceProvider, useSettingsSurface, } from "./SettingsSection.js";
@@ -343,8 +345,8 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
343
345
  const [baseUrlConfigured, setBaseUrlConfigured] = useState(false);
344
346
  const [clearBaseUrl, setClearBaseUrl] = useState(false);
345
347
  const [advancedOpen, setAdvancedOpen] = useState(false);
346
- const [manualSetupOpen, setManualSetupOpen] = useState(false);
347
348
  const [applyNote, setApplyNote] = useState(false);
349
+ const [applyError, setApplyError] = useState(null);
348
350
  const [testing, setTesting] = useState(false);
349
351
  const [testResult, setTestResult] = useState(null);
350
352
  const [settingsStatus, setSettingsStatus] = useState(null);
@@ -420,32 +422,26 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
420
422
  envVar,
421
423
  builderConnected,
422
424
  });
423
- const manualSetupHint = selectedEngine === "ai-sdk:openrouter"
424
- ? "Provide an OpenRouter key to use OpenRouter models like GLM 5.2."
425
- : "Choose your AI provider and model.";
425
+ const manualSetupHint = "Select a provider.";
426
426
  const engineChanged = selectedEngine !== currentEngine || selectedModel !== currentModel;
427
- const isOpenAiEngine = selectedEngine === "ai-sdk:openai";
428
- const endpointChanged = isOpenAiEngine && (!!baseUrl.trim() || clearBaseUrl);
427
+ const selectedProvider = providerIdForEngine(selectedEngine) ?? "anthropic";
428
+ const selectedProviderOption = getAgentProviderOption(selectedProvider);
429
+ const isEndpointProvider = selectedProviderOption.supportsEndpoint === true;
430
+ const endpointChanged = isEndpointProvider && (!!baseUrl.trim() || clearBaseUrl);
429
431
  const providerSettingsChanged = !!apiKey.trim() || endpointChanged;
430
- // Hide the Anthropic-via-AI-SDK alias (redundant with the native entry)
431
- // and Ollama (no API key to set here). The currently-selected engine is
432
- // always kept so a stale setting doesn't vanish from the picker.
433
- const providerOptions = engines
434
- .filter((e) => e.name === selectedEngine ||
435
- (e.name !== "ai-sdk:anthropic" && e.name !== "ai-sdk:ollama"))
436
- .map((e) => ({ value: e.name, label: e.label }));
437
432
  const modelOptions = latestModelsOnly(selectedEngineInfo?.supportedModels ?? []).map((m) => ({ value: m, label: friendlyModelName(m) }));
438
433
  const handleSave = async () => {
439
- if (!providerSettingsChanged || !envVar)
434
+ if (!providerSettingsChanged || (!envVar && !isEndpointProvider))
440
435
  return;
441
436
  setSaving(true);
442
437
  try {
443
- const nextBaseUrl = isOpenAiEngine ? baseUrl.trim() : "";
438
+ const nextBaseUrl = isEndpointProvider ? baseUrl.trim() : "";
444
439
  await saveAgentEngineProviderSettings({
445
- key: envVar,
440
+ provider: selectedProvider,
441
+ ...(envVar ? { key: envVar } : {}),
446
442
  ...(apiKey.trim() ? { apiKey } : {}),
447
443
  ...(nextBaseUrl ? { baseUrl: nextBaseUrl } : {}),
448
- ...(isOpenAiEngine && clearBaseUrl ? { clearBaseUrl: true } : {}),
444
+ ...(isEndpointProvider && clearBaseUrl ? { clearBaseUrl: true } : {}),
449
445
  });
450
446
  setSaved(true);
451
447
  setApiKey("");
@@ -527,40 +523,46 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
527
523
  }
528
524
  };
529
525
  const handleApply = async () => {
526
+ setApplyError(null);
530
527
  try {
531
- const res = await fetch(agentNativePath("/_agent-native/actions/manage-agent-engine"), {
532
- method: "POST",
533
- headers: { "Content-Type": "application/json" },
534
- body: JSON.stringify({
535
- action: "set",
536
- engine: selectedEngine,
537
- model: selectedModel,
538
- }),
528
+ await setAgentEngineProvider({
529
+ provider: selectedProvider,
530
+ model: selectedModel,
539
531
  });
540
- if (res.ok) {
541
- setCurrentEngine(selectedEngine);
542
- setCurrentModel(selectedModel);
543
- setApplyNote(true);
544
- refreshSettingsStatus();
545
- notifyConfigChanged();
546
- setTimeout(() => setApplyNote(false), 4000);
547
- }
532
+ setCurrentEngine(selectedEngine);
533
+ setCurrentModel(selectedModel);
534
+ setApplyNote(true);
535
+ refreshSettingsStatus();
536
+ notifyConfigChanged();
537
+ setTimeout(() => setApplyNote(false), 4000);
538
+ }
539
+ catch (err) {
540
+ setApplyError(err instanceof Error ? err.message : String(err));
548
541
  }
549
- catch { }
550
542
  };
551
- return (_jsx(SettingsSection, { id: settingsSectionDomId("llm"), icon: _jsx(IconBrain, { size: 14 }), title: "LLM", subtitle: "Use Builder.io free credits or your own LLM provider.", required: true, connected: initialLoading ? undefined : anyKeyConfigured, grouped: isPage && grouped, open: open, onToggle: onToggle, children: initialLoading ? (_jsx(SettingsLoadingRow, { controlCount: 2 })) : (_jsxs("div", { className: cn(isPage
543
+ return (_jsx(SettingsSection, { id: settingsSectionDomId("llm"), icon: _jsx(IconBrain, { size: 14 }), title: "LLM", required: true, connected: initialLoading ? undefined : anyKeyConfigured, subtitle: isPage
544
+ ? undefined
545
+ : t("agentPanel.builderOrOwnKeys", {
546
+ defaultValue: "Choose Builder.io or custom keys.",
547
+ }), grouped: isPage && grouped, open: open, onToggle: onToggle, children: initialLoading ? (_jsx(SettingsLoadingRow, { controlCount: 2 })) : (_jsxs("div", { className: cn(isPage
552
548
  ? "flex items-center justify-between gap-4 px-5 py-4 sm:px-6"
553
- : "space-y-2"), children: [isPage && (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Connect an LLM" }), _jsx("p", { className: "mt-1 text-sm leading-6 text-muted-foreground", children: "Use Builder.io free credits or your own provider." })] })), _jsxs("div", { className: cn(isPage && "flex flex-wrap items-center justify-end gap-2"), children: [_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "llm_settings", trackingFlow: "connect_llm", label: "Connect Builder.io", compact: isPage }), builderConnected && !isPage && (_jsxs(Button, { type: "button", intent: "neutral", emphasis: "ghost", "aria-expanded": manualSetupOpen, "aria-controls": "llm-manual-setup", onClick: () => setManualSetupOpen((open) => !open), className: cn("inline-flex items-center gap-1 px-0.5 text-muted-foreground hover:text-foreground", isPage ? "text-xs" : "text-[10px]"), children: [t("agentPanel.addOwnKeys", {
554
- defaultValue: "Add your own keys",
555
- }), _jsx(IconChevronDown, { size: isPage ? 14 : 11, className: cn("transition-transform", manualSetupOpen && "rotate-180") })] })), (!builderConnected || manualSetupOpen || isPage) && (_jsx(ManualSetupCard, { id: "llm-manual-setup", title: "Custom keys", hint: manualSetupHint, sourceBadge: builderConnected ? undefined : sourceBadge, bare: isPage, popover: isPage, popoverLabel: "Custom keys", children: _jsxs("div", { className: "space-y-2 mb-1", children: [_jsx(SettingsSelect, { label: "Provider", value: selectedEngine, options: providerOptions, onValueChange: (val) => {
556
- setSelectedEngine(val);
557
- const info = engines.find((e) => e.name === val);
558
- setSelectedModel(info?.defaultModel ?? "");
549
+ : "flex items-center justify-between gap-2"), children: [isPage && (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "AI provider" }), _jsx("p", { className: "mt-1 text-sm leading-6 text-muted-foreground", children: t("agentPanel.builderOrOwnKeys", {
550
+ defaultValue: "Choose Builder.io or custom keys.",
551
+ }) })] })), _jsxs("div", { className: cn("flex flex-wrap items-center justify-end gap-2"), children: [_jsx(UseBuilderCard, { builderFlow: builderFlow, connectUrl: connectUrl, connected: connected, orgName: orgName, envManaged: envManaged, credentialSource: credentialSource, trackingSource: "llm_settings", trackingFlow: "connect_llm", label: "Connect Builder.io", compact: true }), _jsx(ManualSetupCard, { id: "llm-manual-setup", title: "Custom keys", hint: manualSetupHint, sourceBadge: builderConnected ? undefined : sourceBadge, bare: isPage, popover: true, popoverLabel: settingsConfigured
552
+ ? "Manage"
553
+ : t("agentPanel.addOwnKeys", {
554
+ defaultValue: "Custom keys",
555
+ }), children: _jsxs("div", { className: "space-y-2 mb-1", children: [_jsx(AgentProviderPicker, { value: selectedProvider, layout: isPage ? "page" : "compact", onChange: (provider) => {
556
+ const option = getAgentProviderOption(provider);
557
+ setSelectedEngine(option.engine);
558
+ setSelectedModel(option.defaultModel);
559
559
  setApiKey("");
560
560
  setBaseUrl("");
561
561
  setClearBaseUrl(false);
562
562
  setAdvancedOpen(false);
563
- } }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: fieldLabelClass(isPage), children: "Model" }), _jsx("input", { type: "text", list: `model-suggestions-${selectedEngine}`, value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), placeholder: selectedEngineInfo?.defaultModel ?? "e.g. model-id", spellCheck: false, autoComplete: "off", className: textInputClass(isPage), style: isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE }), modelOptions.length > 0 && (_jsx("datalist", { id: `model-suggestions-${selectedEngine}`, children: modelOptions.map((opt) => (_jsx("option", { value: opt.value, label: opt.label }, opt.value))) }))] }), isOpenAiEngine && (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs(Button, { type: "button", onClick: () => setAdvancedOpen((v) => !v), className: "flex w-full cursor-pointer items-center justify-between gap-2 rounded px-0.5 py-1 text-left hover:text-foreground", children: [_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5 text-[11px] font-medium text-foreground", children: [advancedOpen ? (_jsx(IconChevronDown, { size: 12 })) : (_jsx(IconChevronRight, { size: 12, className: "rtl:-scale-x-100" })), "Advanced"] }), _jsx("span", { className: "truncate text-[10px] text-muted-foreground", children: "OpenAI-compatible endpoint" })] }), advancedOpen && (_jsxs("div", { className: "mt-1.5 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-[12px] font-medium text-foreground", children: "Endpoint URL" }), _jsx("span", { className: "text-[10px] text-muted-foreground", children: baseUrlConfigured ? "Configured" : "Optional" })] }), _jsx("input", { type: "url", value: baseUrl, onChange: (e) => {
563
+ } }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("p", { className: fieldLabelClass(isPage), children: "Model" }), _jsx("input", { type: "text", list: `model-suggestions-${selectedEngine}`, value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), placeholder: selectedEngineInfo?.defaultModel ?? "e.g. model-id", spellCheck: false, autoComplete: "off", className: textInputClass(isPage), style: isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE }), modelOptions.length > 0 && (_jsx("datalist", { id: `model-suggestions-${selectedEngine}`, children: modelOptions.map((opt) => (_jsx("option", { value: opt.value, label: opt.label }, opt.value))) }))] }), isEndpointProvider && (_jsxs("div", { className: "border-t border-border/70 pt-2", children: [_jsxs(Button, { type: "button", onClick: () => setAdvancedOpen((v) => !v), className: "flex w-full cursor-pointer items-center justify-between gap-2 rounded px-0.5 py-1 text-left hover:text-foreground", children: [_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5 text-[11px] font-medium text-foreground", children: [advancedOpen ? (_jsx(IconChevronDown, { size: 12 })) : (_jsx(IconChevronRight, { size: 12, className: "rtl:-scale-x-100" })), "Advanced"] }), _jsx("span", { className: "truncate text-[10px] text-muted-foreground", children: selectedProvider === "ollama"
564
+ ? "Local Ollama endpoint"
565
+ : "OpenAI-compatible endpoint" })] }), advancedOpen && (_jsxs("div", { className: "mt-1.5 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-[12px] font-medium text-foreground", children: "Endpoint URL" }), _jsx("span", { className: "text-[10px] text-muted-foreground", children: baseUrlConfigured ? "Configured" : "Optional" })] }), _jsx("input", { type: "url", value: baseUrl, onChange: (e) => {
564
566
  setBaseUrl(e.target.value);
565
567
  if (e.target.value.trim())
566
568
  setClearBaseUrl(false);
@@ -569,14 +571,16 @@ function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, o
569
571
  handleSave();
570
572
  }, placeholder: baseUrlConfigured
571
573
  ? "Leave blank to keep current endpoint"
572
- : "https://gateway.example/v1", disabled: clearBaseUrl, spellCheck: false, autoComplete: "off", className: "flex h-9 w-full rounded-md border border-border bg-background px-3 text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent disabled:opacity-50 placeholder:text-muted-foreground/50", style: CONTROL_STYLE }), _jsx("p", { className: "text-[10px] leading-relaxed text-muted-foreground", children: "Use for LiteLLM or another OpenAI-compatible chat gateway. Leave blank for OpenAI." }), baseUrlConfigured && (_jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground", children: [_jsx(Checkbox, { checked: clearBaseUrl, onChange: (checked) => {
574
+ : selectedProvider === "ollama"
575
+ ? "http://localhost:11434"
576
+ : "https://gateway.example/v1", disabled: clearBaseUrl, spellCheck: false, autoComplete: "off", className: "flex h-9 w-full rounded-md border border-border bg-background px-3 text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent disabled:opacity-50 placeholder:text-muted-foreground/50", style: CONTROL_STYLE }), baseUrlConfigured && (_jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-muted-foreground", children: [_jsx(Checkbox, { checked: clearBaseUrl, onChange: (checked) => {
573
577
  setClearBaseUrl(checked);
574
578
  if (checked)
575
579
  setBaseUrl("");
576
- }, "aria-label": "Clear saved endpoint override", className: "shrink-0" }), "Clear saved endpoint override"] })), envVar && envConfigured && endpointChanged && (_jsx(Button, { type: "button", intent: "neutral", emphasis: "solid", onClick: handleSave, disabled: saving, className: "rounded bg-accent px-2.5 py-1 text-[10px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40", children: saving ? (_jsx(IconLoader2, { size: 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: 10 })) : ("Save endpoint") }))] }))] })), envVar && envConfigured ? (_jsxs("div", { className: cn("flex items-center gap-1.5 text-primary", isPage ? "text-xs" : "text-[10px]"), children: [_jsx(IconCheck, { size: isPage ? 14 : 10 }), envVar, " configured"] })) : envVar ? (_jsxs("div", { className: "flex gap-1.5", children: [_jsx("input", { type: "password", value: apiKey, onChange: (e) => setApiKey(e.target.value), onKeyDown: (e) => {
580
+ }, "aria-label": "Clear saved endpoint override", className: "shrink-0" }), "Clear saved endpoint override"] })), endpointChanged && (_jsx(Button, { type: "button", intent: "neutral", emphasis: "solid", onClick: handleSave, disabled: saving, className: "rounded bg-accent px-2.5 py-1 text-[10px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40", children: saving ? (_jsx(IconLoader2, { size: 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: 10 })) : ("Save endpoint") }))] }))] })), envVar && envConfigured ? (_jsxs("div", { className: cn("flex items-center gap-1.5 text-primary", isPage ? "text-xs" : "text-[10px]"), children: [_jsx(IconCheck, { size: isPage ? 14 : 10 }), envVar, " configured"] })) : envVar ? (_jsxs("div", { className: "flex gap-1.5", children: [_jsx("input", { type: "password", value: apiKey, onChange: (e) => setApiKey(e.target.value), onKeyDown: (e) => {
577
581
  if (e.key === "Enter")
578
582
  handleSave();
579
- }, placeholder: PROVIDER_ENV_PLACEHOLDERS[envVar] ?? "...", className: cn(textInputClass(isPage), "flex-1"), style: isPage ? CONTROL_STYLE_PAGE : undefined }), _jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleSave, disabled: !providerSettingsChanged || saving, className: pillButtonClass(isPage, "solid"), children: saving ? (_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: isPage ? 14 : 10 })) : ("Save") })] })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { intent: "neutral", emphasis: "outline", onClick: handleTest, disabled: testing, className: pillButtonClass(isPage, "outline"), children: testing ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" }), "Testing\u2026"] })) : ("Test") }), PROVIDER_DOCS[selectedEngine] ? (_jsxs("a", { href: PROVIDER_DOCS[selectedEngine], target: "_blank", rel: "noopener noreferrer", className: cn(pillButtonClass(isPage, "outline"), "no-underline"), children: ["Get an API key", _jsx(IconExternalLink, { size: isPage ? 14 : 10 })] })) : null, engineChanged && (_jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleApply, className: pillButtonClass(isPage, "solid"), children: "Apply" })), settingsStatus != null && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { intent: "danger", emphasis: "outline", onClick: handleDisconnect, className: cn(pillButtonClass(isPage, "outline"), "ms-auto text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/40"), children: "Disconnect" }) }), _jsx(TooltipContent, { children: "Clear the saved engine \u2014 the app will fall back to the default until you re-apply." })] }))] }), testResult && testResult.ok && (_jsxs("p", { className: cn("flex items-center gap-1 text-primary", isPage ? "text-xs" : "text-[10px]"), children: [_jsx(IconCheck, { size: isPage ? 14 : 10 }), "Test passed \u2014 ", testResult.latencyMs, "ms"] })), testResult && testResult.ok === false && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Test failed: ", testResult.error] })), disconnectError && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Disconnect failed: ", disconnectError] })), applyNote && (_jsx("p", { className: cn("text-muted-foreground", isPage ? "text-xs" : "text-[10px]"), children: "Changes take effect on next conversation" }))] }) }))] })] })) }));
583
+ }, placeholder: PROVIDER_ENV_PLACEHOLDERS[envVar] ?? "...", className: cn(textInputClass(isPage), "flex-1"), style: isPage ? CONTROL_STYLE_PAGE : undefined }), _jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleSave, disabled: !providerSettingsChanged || saving, className: pillButtonClass(isPage, "solid"), children: saving ? (_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" })) : saved ? (_jsx(IconCheck, { size: isPage ? 14 : 10 })) : ("Save") })] })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { intent: "neutral", emphasis: "outline", onClick: handleTest, disabled: testing, className: pillButtonClass(isPage, "outline"), children: testing ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconLoader2, { size: isPage ? 14 : 10, className: "animate-spin" }), "Testing\u2026"] })) : ("Test") }), PROVIDER_DOCS[selectedEngine] ? (_jsxs("a", { href: PROVIDER_DOCS[selectedEngine], target: "_blank", rel: "noopener noreferrer", className: cn(pillButtonClass(isPage, "outline"), "no-underline"), children: ["Get an API key", _jsx(IconExternalLink, { size: isPage ? 14 : 10 })] })) : null, engineChanged && (_jsx(Button, { intent: "primary", emphasis: "solid", onClick: handleApply, className: pillButtonClass(isPage, "solid"), children: "Apply" })), settingsStatus != null && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { intent: "danger", emphasis: "outline", onClick: handleDisconnect, className: cn(pillButtonClass(isPage, "outline"), "ms-auto text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/40"), children: "Disconnect" }) }), _jsx(TooltipContent, { children: "Clear the saved engine \u2014 the app will fall back to the default until you re-apply." })] }))] }), testResult && testResult.ok && (_jsxs("p", { className: cn("flex items-center gap-1 text-primary", isPage ? "text-xs" : "text-[10px]"), children: [_jsx(IconCheck, { size: isPage ? 14 : 10 }), "Test passed \u2014 ", testResult.latencyMs, "ms"] })), testResult && testResult.ok === false && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Test failed: ", testResult.error] })), disconnectError && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Disconnect failed: ", disconnectError] })), applyError && (_jsxs("p", { className: cn("text-destructive", isPage ? "text-xs" : "text-[10px]"), children: ["Apply failed: ", applyError] })), applyNote && (_jsx("p", { className: cn("text-muted-foreground", isPage ? "text-xs" : "text-[10px]"), children: "Changes take effect on next conversation" }))] }) })] })] })) }));
580
584
  }
581
585
  function friendlyAppName(appId) {
582
586
  return appId
@@ -587,7 +591,7 @@ function friendlyAppName(appId) {
587
591
  }
588
592
  function AppDefaultModelPicker({ engines, value, disabled, onChange, }) {
589
593
  const [open, setOpen] = useState(false);
590
- const visibleEngines = engines.filter((engine) => engine.name !== "ai-sdk:anthropic" && engine.name !== "ai-sdk:ollama");
594
+ const visibleEngines = engines.filter((engine) => engine.name !== "ai-sdk:anthropic");
591
595
  const selectedModel = value.includes("::")
592
596
  ? value.slice(value.indexOf("::") + 2)
593
597
  : null;
@@ -664,8 +668,7 @@ function AppModelDefaultsSectionInner({ grouped = false, open, onToggle, }) {
664
668
  useEffect(() => load(), [load]);
665
669
  const selectedEngineInfo = settings?.engines.find((engine) => engine.name === selectedEngine) ?? null;
666
670
  const engineOptions = (settings?.engines ?? [])
667
- .filter((engine) => engine.name === selectedEngine ||
668
- (engine.name !== "ai-sdk:anthropic" && engine.name !== "ai-sdk:ollama"))
671
+ .filter((engine) => engine.name === selectedEngine || engine.name !== "ai-sdk:anthropic")
669
672
  .map((engine) => ({
670
673
  value: engine.name,
671
674
  label: engine.name === "builder"
@@ -8,3 +8,5 @@ export { SettingsGroup, SettingsRow, type SettingsGroupProps, type SettingsRowPr
8
8
  export { SettingsSection, SettingsSurfaceProvider, useSettingsSurface, type SettingsSurface, } from "./SettingsSection.js";
9
9
  export { SettingsLoadingRow, SettingsSkeleton, type SettingsLoadingRowProps, type SettingsSkeletonProps, } from "./SettingsSkeleton.js";
10
10
  export { normalizeSettingsSection, settingsSectionDomId, useSettingsPanelController, type SettingsPanelController, type SettingsPanelControllerOptions, } from "./useSettingsPanelController.js";
11
+ export { AGENT_PROVIDER_CATALOG, getAgentProviderOption, providerIdForEngine, type AgentProviderId, type AgentProviderOption, } from "../agent-provider-catalog.js";
12
+ export { AgentProviderPicker, AgentProviderSetupForm, type AgentProviderPickerProps, type AgentProviderSetupFormProps, } from "./ProviderSetupForm.js";
@@ -8,3 +8,5 @@ export { SettingsGroup, SettingsRow, } from "./SettingsRow.js";
8
8
  export { SettingsSection, SettingsSurfaceProvider, useSettingsSurface, } from "./SettingsSection.js";
9
9
  export { SettingsLoadingRow, SettingsSkeleton, } from "./SettingsSkeleton.js";
10
10
  export { normalizeSettingsSection, settingsSectionDomId, useSettingsPanelController, } from "./useSettingsPanelController.js";
11
+ export { AGENT_PROVIDER_CATALOG, getAgentProviderOption, providerIdForEngine, } from "../agent-provider-catalog.js";
12
+ export { AgentProviderPicker, AgentProviderSetupForm, } from "./ProviderSetupForm.js";
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import type { ActionRunContext } from "../action.js";
32
32
  import type { ActionEntry } from "../agent/production-agent.js";
33
+ export type SandboxCodeMode = "run-code" | "tool-orchestration";
33
34
  export interface RunCodeOptions {
34
35
  /**
35
36
  * Extra tool names (beyond the default set) that the sandbox bridge will
@@ -54,6 +55,10 @@ export interface ExecuteSandboxCodeOptions {
54
55
  getActions: () => Record<string, ActionEntry>;
55
56
  /** Extra bridge tool names beyond the defaults. */
56
57
  extraBridgeTools?: Set<string>;
58
+ /** Host policy and helper surface to use inside the sandbox. */
59
+ mode?: SandboxCodeMode;
60
+ /** Maximum number of bridged child-tool calls for orchestration mode. */
61
+ maxToolCalls?: number;
57
62
  /** Request context (owner/org) applied to bridged tool calls. */
58
63
  context?: ActionRunContext;
59
64
  }
@@ -37,6 +37,9 @@ const DEFAULT_TIMEOUT_MS = 120_000;
37
37
  const MAX_TIMEOUT_MS = 600_000;
38
38
  const DEFAULT_MAX_OUTPUT_CHARS = 50_000;
39
39
  const MAX_OUTPUT_CHARS = 200_000;
40
+ const TOOL_ORCHESTRATION_DEFAULT_MAX_CALLS = 32;
41
+ const TOOL_ORCHESTRATION_MAX_CALLS = 128;
42
+ const TOOL_ORCHESTRATION_MAX_PROVIDER_PAGES = 20;
40
43
  /** Hard cap on bridge request bodies so sandboxed code can't exhaust parent memory. */
41
44
  const BRIDGE_MAX_BODY_BYTES = 10 * 1024 * 1024;
42
45
  /** Tools callable via the sandbox bridge by default. */
@@ -203,8 +206,9 @@ export function createRunCodeEntry(getActions, opts = {}) {
203
206
  export async function executeSandboxCode(options) {
204
207
  const actions = options.getActions();
205
208
  const bridgeToken = crypto.randomBytes(32).toString("hex");
209
+ const mode = options.mode ?? "run-code";
206
210
  // Start bridge server — resolves once the server is listening.
207
- const { bridgePort, getUsedTools, cleanup: cleanupBridge, } = await startBridgeServer(bridgeToken, actions, options.context, DEFAULT_BRIDGE_TOOLS, options.extraBridgeTools ?? new Set());
211
+ const { bridgePort, getUsedTools, cleanup: cleanupBridge, } = await startBridgeServer(bridgeToken, actions, options.context, DEFAULT_BRIDGE_TOOLS, options.extraBridgeTools ?? new Set(), mode, options.maxToolCalls);
208
212
  try {
209
213
  // Build scrubbed env — only safe POSIX vars, no secrets. The adapter
210
214
  // points TMPDIR/TEMP/TMP at the sandbox's own temp dir.
@@ -228,7 +232,7 @@ export async function executeSandboxCode(options) {
228
232
  // bridge, env scrub, module, and output formatting stay in the parent
229
233
  // regardless of adapter.
230
234
  const { stdout, stderr, exitCode, timedOut } = await resolveExecutionSandboxAdapter().run({
231
- moduleSource: buildSandboxModule(options.code, bridgePort, bridgeToken),
235
+ moduleSource: buildSandboxModule(options.code, bridgePort, bridgeToken, mode),
232
236
  env: safeEnv,
233
237
  timeoutMs: options.timeoutMs,
234
238
  bridgePort,
@@ -457,8 +461,14 @@ export function createGetCodeExecutionEntry() {
457
461
  },
458
462
  };
459
463
  }
460
- async function startBridgeServer(token, actions, context, defaultTools, extraTools) {
464
+ async function startBridgeServer(token, actions, context, defaultTools, extraTools, mode, maxToolCalls) {
461
465
  const usedTools = new Set();
466
+ const callBudget = mode === "tool-orchestration"
467
+ ? {
468
+ count: 0,
469
+ max: normalizeToolCallBudget(maxToolCalls),
470
+ }
471
+ : undefined;
462
472
  const server = http.createServer((req, res) => {
463
473
  if (req.method !== "POST" || req.url !== "/tool") {
464
474
  res.writeHead(404);
@@ -489,7 +499,7 @@ async function startBridgeServer(token, actions, context, defaultTools, extraToo
489
499
  req.on("end", () => {
490
500
  if (rejected)
491
501
  return;
492
- handleBridgeRequest(body, actions, context, defaultTools, extraTools, usedTools, res);
502
+ handleBridgeRequest(body, actions, context, defaultTools, extraTools, usedTools, mode, callBudget, res);
493
503
  });
494
504
  req.on("error", () => {
495
505
  res.writeHead(500);
@@ -515,7 +525,7 @@ async function startBridgeServer(token, actions, context, defaultTools, extraToo
515
525
  cleanup,
516
526
  };
517
527
  }
518
- function handleBridgeRequest(rawBody, actions, context, defaultTools, extraTools, usedTools, res) {
528
+ function handleBridgeRequest(rawBody, actions, context, defaultTools, extraTools, usedTools, mode, callBudget, res) {
519
529
  let parsed;
520
530
  try {
521
531
  parsed = JSON.parse(rawBody);
@@ -546,7 +556,27 @@ function handleBridgeRequest(rawBody, actions, context, defaultTools, extraTools
546
556
  const isReadOnlyAction = entry.readOnly === true &&
547
557
  entry.agentTool !== false &&
548
558
  entry.toolCallable !== false;
549
- if (!defaultTools.has(toolName) &&
559
+ const toolArgs = parsed.args ?? {};
560
+ const childArgs = mode === "tool-orchestration"
561
+ ? boundToolOrchestrationArgs(toolName, toolArgs)
562
+ : toolArgs;
563
+ if (mode === "tool-orchestration") {
564
+ if (callBudget && callBudget.count >= callBudget.max) {
565
+ res.writeHead(429, { "Content-Type": "application/json" });
566
+ res.end(JSON.stringify({
567
+ error: `Tool-orchestration child-call budget exceeded (${callBudget.max}). Aggregate the current results or increase maxToolCalls within its limit.`,
568
+ }));
569
+ return;
570
+ }
571
+ if (!isAllowedToolOrchestrationCall(toolName, childArgs, entry)) {
572
+ res.writeHead(403, { "Content-Type": "application/json" });
573
+ res.end(JSON.stringify({
574
+ error: `Tool "${toolName}" is not permitted by tool-orchestration. Only read-only tools and GET/HEAD provider or web reads without staging or saving files are allowed; call mutating tools directly.`,
575
+ }));
576
+ return;
577
+ }
578
+ }
579
+ else if (!defaultTools.has(toolName) &&
550
580
  !extraTools.has(toolName) &&
551
581
  !isReadOnlyAction) {
552
582
  // A registered, agent-exposed action that isn't read-only is a mutation.
@@ -563,12 +593,13 @@ function handleBridgeRequest(rawBody, actions, context, defaultTools, extraTools
563
593
  }));
564
594
  return;
565
595
  }
566
- const toolArgs = parsed.args ?? {};
596
+ if (callBudget)
597
+ callBudget.count += 1;
567
598
  usedTools.add(toolName);
568
599
  // Run the tool with the parent request context so auth/org/owner resolution
569
600
  // works exactly as it does in the normal agent loop.
570
601
  entry
571
- .run(toolArgs, context)
602
+ .run(childArgs, context)
572
603
  .then((result) => {
573
604
  const body = typeof result === "string" ? result : JSON.stringify(result, null, 2);
574
605
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -580,6 +611,105 @@ function handleBridgeRequest(rawBody, actions, context, defaultTools, extraTools
580
611
  res.end(JSON.stringify({ error: message }));
581
612
  });
582
613
  }
614
+ function normalizeToolCallBudget(value) {
615
+ if (!Number.isFinite(value) || !value || value <= 0) {
616
+ return TOOL_ORCHESTRATION_DEFAULT_MAX_CALLS;
617
+ }
618
+ return Math.min(Math.floor(value), TOOL_ORCHESTRATION_MAX_CALLS);
619
+ }
620
+ function isAllowedToolOrchestrationCall(toolName, args, entry) {
621
+ if (toolName === "workspace-files") {
622
+ // This is an internal bridge action by design, so agentTool/toolCallable
623
+ // are intentionally not required here. Its operation enum is the policy
624
+ // boundary for the read-only workspace surface.
625
+ return isCallableWithoutApproval(entry) && isReadOnlyWorkspaceRequest(args);
626
+ }
627
+ if (toolName === "provider-api-request") {
628
+ return isReadOnlyProviderApiRequest(args, entry);
629
+ }
630
+ return isReadOnlyOrchestrationAction(args, entry);
631
+ }
632
+ function isCallableWithoutApproval(entry) {
633
+ // The bridge calls ActionEntry.run directly for child reads. A predicate
634
+ // approval gate must therefore fail closed rather than being silently
635
+ // bypassed by this bounded path.
636
+ return entry.needsApproval === undefined || entry.needsApproval === false;
637
+ }
638
+ function isReadOnlyWorkspaceRequest(args) {
639
+ const action = typeof args.action === "string" ? args.action : "";
640
+ return action === "read" || action === "list" || action === "grep";
641
+ }
642
+ function isReadOnlyProviderApiRequest(args, entry) {
643
+ if (!entry.tool ||
644
+ !isCallableWithoutApproval(entry) ||
645
+ entry.agentTool === false ||
646
+ entry.allowInPlanMode === false) {
647
+ return false;
648
+ }
649
+ const method = String(args.method ?? "GET").toUpperCase();
650
+ return ((method === "GET" || method === "HEAD") &&
651
+ args.stageAs == null &&
652
+ args.saveToFile == null);
653
+ }
654
+ function isReadOnlyOrchestrationAction(args, entry) {
655
+ if (!entry.tool ||
656
+ !isCallableWithoutApproval(entry) ||
657
+ entry.agentTool === false ||
658
+ entry.toolCallable === false ||
659
+ entry.allowInPlanMode === false) {
660
+ return false;
661
+ }
662
+ const planMode = entry.planMode;
663
+ if (!planMode)
664
+ return entry.readOnly === true;
665
+ const input = args;
666
+ if (planMode.allowedProperties &&
667
+ Object.keys(args).some((key) => !planMode.allowedProperties.includes(key))) {
668
+ return false;
669
+ }
670
+ if (planMode.omittedProperties?.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
671
+ return false;
672
+ }
673
+ if (Object.entries(planMode.allowedValues ?? {}).some(([key, allowed]) => {
674
+ const value = args[key];
675
+ return (value !== null &&
676
+ value !== undefined &&
677
+ (typeof value !== "string" || !allowed.includes(value)));
678
+ })) {
679
+ return false;
680
+ }
681
+ try {
682
+ const effect = typeof planMode.effect === "function"
683
+ ? planMode.effect(input)
684
+ : planMode.effect;
685
+ return effect === "read";
686
+ }
687
+ catch (error) {
688
+ console.warn("[run-code] tool-orchestration read-only policy classifier failed closed:", error instanceof Error ? error.message : String(error));
689
+ return false;
690
+ }
691
+ }
692
+ function boundToolOrchestrationArgs(toolName, args) {
693
+ if (toolName !== "provider-api-request")
694
+ return args;
695
+ const fetchAllPages = args.fetchAllPages;
696
+ if (!fetchAllPages ||
697
+ typeof fetchAllPages !== "object" ||
698
+ Array.isArray(fetchAllPages)) {
699
+ return args;
700
+ }
701
+ const requestedMaxPages = Number(fetchAllPages.maxPages);
702
+ const maxPages = Number.isInteger(requestedMaxPages) && requestedMaxPages > 0
703
+ ? Math.min(Math.floor(requestedMaxPages), TOOL_ORCHESTRATION_MAX_PROVIDER_PAGES)
704
+ : TOOL_ORCHESTRATION_MAX_PROVIDER_PAGES;
705
+ return {
706
+ ...args,
707
+ fetchAllPages: {
708
+ ...fetchAllPages,
709
+ maxPages,
710
+ },
711
+ };
712
+ }
583
713
  // ---------------------------------------------------------------------------
584
714
  // Sandbox module template
585
715
  // ---------------------------------------------------------------------------
@@ -589,7 +719,61 @@ function handleBridgeRequest(rawBody, actions, context, defaultTools, extraTools
589
719
  * `providerSearchAll`, and `webFetch` helpers via the bridge.
590
720
  * 2. Runs the user's code as top-level await in an async IIFE.
591
721
  */
592
- function buildSandboxModule(userCode, bridgePort, bridgeToken) {
722
+ function buildSandboxModule(userCode, bridgePort, bridgeToken, mode = "run-code") {
723
+ const orchestrationHelpers = mode === "tool-orchestration"
724
+ ? `
725
+ /**
726
+ * Search the live registry for read-only or conditionally read-only tools, then call one
727
+ * of the returned names with toolCall(). The parent host enforces the same
728
+ * policy again, so this is discovery guidance rather than an authorization
729
+ * boundary.
730
+ */
731
+ async function toolSearch(query = "", options = {}) {
732
+ const rawResult = await _bridgeCall("tool-search", {
733
+ ...(query ? { query } : {}),
734
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
735
+ includeSchemas: true,
736
+ readOnlyOnly: true,
737
+ });
738
+ return _parseBridgeResult(rawResult);
739
+ }
740
+
741
+ async function toolCall(name, args = {}) {
742
+ return _parseBridgeResult(await _bridgeCall(name, args));
743
+ }
744
+ `
745
+ : "";
746
+ const workspaceMutationHelpers = mode === "run-code"
747
+ ? `
748
+ /**
749
+ * Write (create or overwrite) a workspace file. Use \`scratch/...\` for
750
+ * temporary staging files.
751
+ * \`content\` must be a string. Returns metadata
752
+ * { resourceId, path, contentType, sizeBytes, updatedAt }.
753
+ */
754
+ async function workspaceWrite(path, content, contentType = "text/plain") {
755
+ const rawResult = await _bridgeCall("workspace-files", {
756
+ action: "write",
757
+ path,
758
+ content: typeof content === "string" ? content : JSON.stringify(content),
759
+ contentType,
760
+ });
761
+ try { return typeof rawResult === "string" ? JSON.parse(rawResult) : rawResult; } catch { return rawResult; }
762
+ }
763
+
764
+ /**
765
+ * Append text to a workspace file (creates if absent).
766
+ */
767
+ async function workspaceAppend(path, content) {
768
+ const rawResult = await _bridgeCall("workspace-files", {
769
+ action: "append",
770
+ path,
771
+ content: typeof content === "string" ? content : JSON.stringify(content),
772
+ });
773
+ try { return typeof rawResult === "string" ? JSON.parse(rawResult) : rawResult; } catch { return rawResult; }
774
+ }
775
+ `
776
+ : "";
593
777
  return `
594
778
  import { createRequire } from "node:module";
595
779
  const require = createRequire(import.meta.url);
@@ -646,6 +830,8 @@ async function appAction(name, args = {}) {
646
830
  return _parseBridgeResult(await _bridgeCall(name, args));
647
831
  }
648
832
 
833
+ ${orchestrationHelpers}
834
+
649
835
  async function providerRequest(provider, apiPath, init = {}) {
650
836
  const method = (init.method || "GET").toUpperCase();
651
837
  const rawResult = await _bridgeCall("provider-api-request", {
@@ -1388,33 +1574,7 @@ async function workspaceReadMeta(path, opts = {}) {
1388
1574
  return _parseBridgeResult(rawResult);
1389
1575
  }
1390
1576
 
1391
- /**
1392
- * Write (create or overwrite) a workspace file. Use \`scratch/...\` for
1393
- * temporary staging files.
1394
- * \`content\` must be a string. Returns metadata
1395
- * { resourceId, path, contentType, sizeBytes, updatedAt }.
1396
- */
1397
- async function workspaceWrite(path, content, contentType = "text/plain") {
1398
- const rawResult = await _bridgeCall("workspace-files", {
1399
- action: "write",
1400
- path,
1401
- content: typeof content === "string" ? content : JSON.stringify(content),
1402
- contentType,
1403
- });
1404
- try { return typeof rawResult === "string" ? JSON.parse(rawResult) : rawResult; } catch { return rawResult; }
1405
- }
1406
-
1407
- /**
1408
- * Append text to a workspace file (creates if absent).
1409
- */
1410
- async function workspaceAppend(path, content) {
1411
- const rawResult = await _bridgeCall("workspace-files", {
1412
- action: "append",
1413
- path,
1414
- content: typeof content === "string" ? content : JSON.stringify(content),
1415
- });
1416
- try { return typeof rawResult === "string" ? JSON.parse(rawResult) : rawResult; } catch { return rawResult; }
1417
- }
1577
+ ${workspaceMutationHelpers}
1418
1578
 
1419
1579
  /**
1420
1580
  * List workspace files, optionally filtered by path prefix.
@@ -0,0 +1,10 @@
1
+ import type { ActionEntry } from "../agent/production-agent.js";
2
+ /**
3
+ * Create the bounded read-only orchestration tool.
4
+ *
5
+ * This deliberately has a separate registry entry from run-code. The latter
6
+ * retains its existing workspace staging and background-execution contract;
7
+ * this entry is for short fan-out, reduction, and aggregation over tools that
8
+ * the host can prove are read-only for the supplied arguments.
9
+ */
10
+ export declare function createToolOrchestrationEntry(getActions: () => Record<string, ActionEntry>): ActionEntry;