@agent-native/core 0.131.9 → 0.132.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +105 -0
- package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
- package/corpus/core/docs/design/durable-agent-runs.md +15 -11
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +127 -10
- package/corpus/core/src/a2a/index.ts +3 -0
- package/corpus/core/src/action.ts +52 -11
- package/corpus/core/src/agent/durable-background.ts +38 -28
- package/corpus/core/src/agent/harness/background.ts +12 -2
- package/corpus/core/src/agent/production-agent.ts +210 -43
- package/corpus/core/src/agent/run-store.ts +45 -3
- package/corpus/core/src/application-state/handlers.ts +45 -0
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/store.ts +28 -11
- package/corpus/core/src/audit/redact.ts +104 -18
- package/corpus/core/src/client/AgentPanel.tsx +2 -7
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
- package/corpus/core/src/client/agent-chat.ts +4 -9
- package/corpus/core/src/client/application-state.ts +115 -12
- package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
- package/corpus/core/src/client/guided-questions.tsx +12 -23
- package/corpus/core/src/db/client.ts +148 -28
- package/corpus/core/src/db/create-get-db.ts +39 -6
- package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
- package/corpus/core/src/deploy/build.ts +26 -33
- package/corpus/core/src/feature-flags/store.ts +10 -5
- package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
- package/corpus/core/src/integrations/config-store.ts +13 -0
- package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
- package/corpus/core/src/mcp-client/routes.ts +33 -2
- package/corpus/core/src/onboarding/plugin.ts +31 -25
- package/corpus/core/src/org/accept-pending.ts +2 -0
- package/corpus/core/src/org/auto-join-domain.ts +2 -0
- package/corpus/core/src/org/context.ts +35 -10
- package/corpus/core/src/org/handlers.ts +6 -0
- package/corpus/core/src/org/request-org-cache.ts +71 -0
- package/corpus/core/src/resources/store.ts +10 -9
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/secrets/storage.ts +97 -5
- package/corpus/core/src/server/action-change.ts +33 -0
- package/corpus/core/src/server/action-routes.ts +11 -9
- package/corpus/core/src/server/agent-teams.ts +10 -2
- package/corpus/core/src/server/better-auth-instance.ts +39 -12
- package/corpus/core/src/server/core-routes-plugin.ts +19 -2
- package/corpus/core/src/server/credential-provider.ts +57 -14
- package/corpus/core/src/server/http-response-telemetry.ts +1 -0
- package/corpus/core/src/server/poll.ts +95 -57
- package/corpus/core/src/server/voice-providers-status.ts +19 -4
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
- package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
- package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
- package/corpus/templates/analytics/app/i18n-data.ts +40 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
- package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
- package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
- package/corpus/templates/slides/actions/patch-deck.ts +25 -1
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
- package/corpus/templates/slides/app/pages/Index.tsx +2 -2
- package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +8 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
- package/dist/a2a/activity.d.ts +22 -4
- package/dist/a2a/activity.d.ts.map +1 -1
- package/dist/a2a/activity.js +97 -11
- package/dist/a2a/activity.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +41 -11
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +13 -8
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +36 -28
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/harness/background.d.ts.map +1 -1
- package/dist/agent/harness/background.js +12 -2
- package/dist/agent/harness/background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +195 -44
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +44 -3
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/handlers.d.ts +15 -0
- package/dist/application-state/handlers.d.ts.map +1 -1
- package/dist/application-state/handlers.js +42 -2
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/store.d.ts +11 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +23 -10
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/redact.d.ts +25 -2
- package/dist/audit/redact.d.ts.map +1 -1
- package/dist/audit/redact.js +85 -21
- package/dist/audit/redact.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -7
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts +11 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +20 -7
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +4 -6
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/application-state.d.ts +11 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +87 -9
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +4 -13
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +6 -22
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +57 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +122 -31
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +31 -7
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/runtime-diagnostics.d.ts +1 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -1
- package/dist/db/runtime-diagnostics.js +31 -6
- package/dist/db/runtime-diagnostics.js.map +1 -1
- package/dist/deploy/build.d.ts +5 -20
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +18 -30
- package/dist/deploy/build.js.map +1 -1
- package/dist/feature-flags/store.d.ts.map +1 -1
- package/dist/feature-flags/store.js +10 -5
- package/dist/feature-flags/store.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +12 -0
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.d.ts.map +1 -1
- package/dist/integrations/config-store.js +11 -0
- package/dist/integrations/config-store.js.map +1 -1
- package/dist/integrations/google-docs-poller.d.ts.map +1 -1
- package/dist/integrations/google-docs-poller.js +19 -2
- package/dist/integrations/google-docs-poller.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +30 -2
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/onboarding/plugin.d.ts.map +1 -1
- package/dist/onboarding/plugin.js +12 -8
- package/dist/onboarding/plugin.js.map +1 -1
- package/dist/org/accept-pending.d.ts.map +1 -1
- package/dist/org/accept-pending.js +2 -0
- package/dist/org/accept-pending.js.map +1 -1
- package/dist/org/auto-join-domain.d.ts.map +1 -1
- package/dist/org/auto-join-domain.js +2 -0
- package/dist/org/auto-join-domain.js.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/context.js +29 -9
- package/dist/org/context.js.map +1 -1
- package/dist/org/handlers.d.ts.map +1 -1
- package/dist/org/handlers.js +5 -0
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/request-org-cache.d.ts +16 -0
- package/dist/org/request-org-cache.d.ts.map +1 -0
- package/dist/org/request-org-cache.js +62 -0
- package/dist/org/request-org-cache.js.map +1 -0
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +11 -9
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +3 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +82 -5
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-change.d.ts +20 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +29 -0
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +7 -8
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +10 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +31 -14
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +18 -4
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +16 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +49 -13
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +1 -0
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/poll.d.ts +6 -0
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +84 -47
- package/dist/server/poll.js.map +1 -1
- package/dist/server/voice-providers-status.d.ts.map +1 -1
- package/dist/server/voice-providers-status.js +12 -3
- package/dist/server/voice-providers-status.js.map +1 -1
- package/docs/content/durable-background-runs.mdx +15 -6
- package/docs/design/durable-agent-runs.md +15 -11
- package/package.json +2 -2
- package/src/a2a/activity.ts +127 -10
- package/src/a2a/index.ts +3 -0
- package/src/action.ts +52 -11
- package/src/agent/durable-background.ts +38 -28
- package/src/agent/harness/background.ts +12 -2
- package/src/agent/production-agent.ts +210 -43
- package/src/agent/run-store.ts +45 -3
- package/src/application-state/handlers.ts +45 -0
- package/src/application-state/index.ts +3 -0
- package/src/application-state/store.ts +28 -11
- package/src/audit/redact.ts +104 -18
- package/src/client/AgentPanel.tsx +2 -7
- package/src/client/MultiTabAssistantChat.tsx +3 -0
- package/src/client/RunStuckBanner.tsx +29 -6
- package/src/client/agent-chat.ts +4 -9
- package/src/client/application-state.ts +115 -12
- package/src/client/dynamic-suggestions.ts +4 -12
- package/src/client/guided-questions.tsx +12 -23
- package/src/db/client.ts +148 -28
- package/src/db/create-get-db.ts +39 -6
- package/src/db/runtime-diagnostics.ts +41 -6
- package/src/deploy/build.ts +26 -33
- package/src/feature-flags/store.ts +10 -5
- package/src/integrations/a2a-continuations-store.ts +11 -0
- package/src/integrations/config-store.ts +13 -0
- package/src/integrations/google-docs-poller.ts +26 -2
- package/src/mcp-client/routes.ts +33 -2
- package/src/onboarding/plugin.ts +31 -25
- package/src/org/accept-pending.ts +2 -0
- package/src/org/auto-join-domain.ts +2 -0
- package/src/org/context.ts +35 -10
- package/src/org/handlers.ts +6 -0
- package/src/org/request-org-cache.ts +71 -0
- package/src/resources/store.ts +10 -9
- package/src/scripts/runner.ts +6 -3
- package/src/secrets/storage.ts +97 -5
- package/src/server/action-change.ts +33 -0
- package/src/server/action-routes.ts +11 -9
- package/src/server/agent-teams.ts +10 -2
- package/src/server/better-auth-instance.ts +39 -12
- package/src/server/core-routes-plugin.ts +19 -2
- package/src/server/credential-provider.ts +57 -14
- package/src/server/http-response-telemetry.ts +1 -0
- package/src/server/poll.ts +95 -57
- package/src/server/voice-providers-status.ts +19 -4
package/src/mcp-client/routes.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { getSession } from "../server/auth.js";
|
|
|
32
32
|
import { getH3App } from "../server/framework-request-handler.js";
|
|
33
33
|
import { readBody } from "../server/h3-helpers.js";
|
|
34
34
|
import { runWithRequestContext } from "../server/request-context.js";
|
|
35
|
-
import { getAllSettings } from "../settings/store.js";
|
|
35
|
+
import { getAllSettings, getSettingsEmitter } from "../settings/store.js";
|
|
36
36
|
import {
|
|
37
37
|
areBuiltinMcpCapabilitiesSupported,
|
|
38
38
|
BUILTIN_MCP_CAPABILITIES,
|
|
@@ -291,6 +291,13 @@ function sortedConfigSignature(config: McpConfig | null): string {
|
|
|
291
291
|
return JSON.stringify(entries);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* How long the refresh may skip the settings read on the strength of "no
|
|
296
|
+
* in-process settings write since the last one". Bounds how stale a remote MCP
|
|
297
|
+
* server list added by ANOTHER process can be.
|
|
298
|
+
*/
|
|
299
|
+
const MCP_CONFIG_REFRESH_BACKSTOP_MS = 5 * 60 * 1000;
|
|
300
|
+
|
|
294
301
|
function mcpConfigRefreshIntervalMs(): number {
|
|
295
302
|
const raw = process.env.AGENT_NATIVE_MCP_CONFIG_REFRESH_MS;
|
|
296
303
|
if (raw?.trim() === "0") return 0;
|
|
@@ -307,8 +314,24 @@ export function startMcpConfigRefresh(
|
|
|
307
314
|
|
|
308
315
|
let currentSignature = sortedConfigSignature(manager.getConfig());
|
|
309
316
|
let refreshing = false;
|
|
317
|
+
// `buildMergedConfig` reads the entire settings table just to diff a
|
|
318
|
+
// signature, which on an idle app is a full-table round trip every minute
|
|
319
|
+
// forever. Only pay it when an in-process settings write says something might
|
|
320
|
+
// have changed, plus a periodic backstop for writes from another process.
|
|
321
|
+
let settingsDirty = true;
|
|
322
|
+
let lastFullRefresh = 0;
|
|
323
|
+
const markDirty = () => {
|
|
324
|
+
settingsDirty = true;
|
|
325
|
+
};
|
|
326
|
+
getSettingsEmitter().on("settings", markDirty);
|
|
310
327
|
const refresh = async () => {
|
|
311
328
|
if (refreshing) return;
|
|
329
|
+
if (
|
|
330
|
+
!settingsDirty &&
|
|
331
|
+
Date.now() - lastFullRefresh < MCP_CONFIG_REFRESH_BACKSTOP_MS
|
|
332
|
+
) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
312
335
|
refreshing = true;
|
|
313
336
|
try {
|
|
314
337
|
const next = await buildMergedConfig();
|
|
@@ -317,7 +340,12 @@ export function startMcpConfigRefresh(
|
|
|
317
340
|
await manager.reconfigure(next);
|
|
318
341
|
currentSignature = nextSignature;
|
|
319
342
|
}
|
|
343
|
+
settingsDirty = false;
|
|
344
|
+
lastFullRefresh = Date.now();
|
|
320
345
|
} catch (err: any) {
|
|
346
|
+
// Keep this dirty so a transient database or manager failure is retried
|
|
347
|
+
// on the next interval instead of being hidden by the backstop window.
|
|
348
|
+
settingsDirty = true;
|
|
321
349
|
console.warn(
|
|
322
350
|
`[mcp-client] config refresh failed: ${err?.message ?? err}`,
|
|
323
351
|
);
|
|
@@ -328,7 +356,10 @@ export function startMcpConfigRefresh(
|
|
|
328
356
|
|
|
329
357
|
const timer = setInterval(refresh, intervalMs);
|
|
330
358
|
(timer as { unref?: () => void }).unref?.();
|
|
331
|
-
return () =>
|
|
359
|
+
return () => {
|
|
360
|
+
clearInterval(timer);
|
|
361
|
+
getSettingsEmitter().off("settings", markDirty);
|
|
362
|
+
};
|
|
332
363
|
}
|
|
333
364
|
|
|
334
365
|
async function resolveContextForRequest(event: H3Event): Promise<{
|
package/src/onboarding/plugin.ts
CHANGED
|
@@ -81,30 +81,34 @@ async function serializeSteps(
|
|
|
81
81
|
options: { preview?: boolean } = {},
|
|
82
82
|
): Promise<OnboardingStepStatus[]> {
|
|
83
83
|
const steps = listOnboardingSteps();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
// Steps are independent of each other, and each `isComplete()` is itself a
|
|
85
|
+
// chain of credential/settings reads — walking them one at a time made this
|
|
86
|
+
// route cost the SUM of every step's round trips against a remote database
|
|
87
|
+
// instead of the slowest one. `Promise.all` preserves `steps` order.
|
|
88
|
+
return Promise.all(
|
|
89
|
+
steps.map(async (step) => {
|
|
90
|
+
let complete = false;
|
|
91
|
+
if (!options.preview) {
|
|
92
|
+
try {
|
|
93
|
+
complete = (await step.isComplete(context)) === true;
|
|
94
|
+
} catch {
|
|
95
|
+
complete = false;
|
|
96
|
+
}
|
|
97
|
+
if (!complete) {
|
|
98
|
+
complete = await hasOverride(context.sessionId, step.id);
|
|
99
|
+
}
|
|
95
100
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
return out;
|
|
101
|
+
return {
|
|
102
|
+
id: step.id,
|
|
103
|
+
title: step.title,
|
|
104
|
+
description: step.description,
|
|
105
|
+
order: step.order,
|
|
106
|
+
required: step.required ?? false,
|
|
107
|
+
complete,
|
|
108
|
+
methods: step.methods,
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
function withOnboardingRequestContext<T>(
|
|
@@ -240,11 +244,13 @@ export function createOnboardingPlugin(
|
|
|
240
244
|
// doesn't surface as a 500 to the client.
|
|
241
245
|
try {
|
|
242
246
|
return await withOnboardingRequestContext(context, async () => {
|
|
243
|
-
const value = await
|
|
247
|
+
const [value, statuses] = await Promise.all([
|
|
248
|
+
appStateGet(context.sessionId, DISMISSED_KEY),
|
|
249
|
+
serializeSteps(context),
|
|
250
|
+
]);
|
|
244
251
|
const dismissed = !!(
|
|
245
252
|
value && (value as { dismissed?: boolean }).dismissed
|
|
246
253
|
);
|
|
247
|
-
const statuses = await serializeSteps(context);
|
|
248
254
|
return {
|
|
249
255
|
dismissed,
|
|
250
256
|
allComplete: allRequiredComplete(statuses),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDbExec } from "../db/client.js";
|
|
2
2
|
import { setActiveOrgId } from "./active-org.js";
|
|
3
|
+
import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
|
|
3
4
|
|
|
4
5
|
const nanoid = (): string =>
|
|
5
6
|
globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
|
|
@@ -75,6 +76,7 @@ export async function acceptPendingInvitationsForEmail(
|
|
|
75
76
|
ON CONFLICT (org_id, LOWER(email)) DO NOTHING`,
|
|
76
77
|
args: [nanoid(), inv.orgId, email, role, Date.now()],
|
|
77
78
|
});
|
|
79
|
+
invalidateRequestMemberOrgIds();
|
|
78
80
|
}
|
|
79
81
|
await db.execute({
|
|
80
82
|
sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDbExec } from "../db/client.js";
|
|
2
2
|
import { getUserSetting } from "../settings/user-settings.js";
|
|
3
3
|
import { setActiveOrgId } from "./active-org.js";
|
|
4
|
+
import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
|
|
4
5
|
|
|
5
6
|
const nanoid = (): string =>
|
|
6
7
|
globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
|
|
@@ -87,6 +88,7 @@ export async function autoJoinDomainMatchingOrgs(
|
|
|
87
88
|
args: [nanoid(), m.orgId, email, Date.now()],
|
|
88
89
|
});
|
|
89
90
|
joined.push({ orgId: m.orgId });
|
|
91
|
+
invalidateRequestMemberOrgIds();
|
|
90
92
|
} catch {
|
|
91
93
|
// Race with a parallel join (e.g. user accepted an invite to the
|
|
92
94
|
// same org milliseconds earlier). The unique constraint keeps the
|
package/src/org/context.ts
CHANGED
|
@@ -7,6 +7,10 @@ import { getSetting } from "../settings/store.js";
|
|
|
7
7
|
import { getUserSetting } from "../settings/user-settings.js";
|
|
8
8
|
import { setActiveOrgId } from "./active-org.js";
|
|
9
9
|
import { autoJoinDomainMatchingOrgs } from "./auto-join-domain.js";
|
|
10
|
+
import {
|
|
11
|
+
invalidateRequestMemberOrgIds,
|
|
12
|
+
requestMemberOrgIds,
|
|
13
|
+
} from "./request-org-cache.js";
|
|
10
14
|
import type { OrgContext, OrgRole } from "./types.js";
|
|
11
15
|
|
|
12
16
|
const EMPTY_CONTEXT: OrgContext = {
|
|
@@ -210,6 +214,12 @@ async function resolveOrgContextUncached(event: H3Event): Promise<OrgContext> {
|
|
|
210
214
|
|
|
211
215
|
const exec = getDbExec();
|
|
212
216
|
|
|
217
|
+
// Started before the memberships await so the two round trips overlap; the
|
|
218
|
+
// `catch` only covers the early returns below, the later `await` still
|
|
219
|
+
// propagates a real failure.
|
|
220
|
+
const activeOrgSettingPromise = loadActiveOrgSettingForEvent(event, email);
|
|
221
|
+
activeOrgSettingPromise.catch(() => {});
|
|
222
|
+
|
|
213
223
|
let memberships: MembershipRow[] | null;
|
|
214
224
|
try {
|
|
215
225
|
memberships = await loadMembershipsForEvent(event, email);
|
|
@@ -238,7 +248,7 @@ async function resolveOrgContextUncached(event: H3Event): Promise<OrgContext> {
|
|
|
238
248
|
return { email, orgId: null, orgName: null, role: null };
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
const activeOrgSetting = await
|
|
251
|
+
const activeOrgSetting = await activeOrgSettingPromise;
|
|
242
252
|
const explicitPersonal = activeOrgSetting?.orgId === null;
|
|
243
253
|
|
|
244
254
|
const emailDomain = emailDomainOf(email);
|
|
@@ -410,17 +420,26 @@ async function loadMemberships(email: string): Promise<MembershipRow[] | null> {
|
|
|
410
420
|
export async function resolveOrgIdForEmail(
|
|
411
421
|
email: string,
|
|
412
422
|
): Promise<string | null> {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
423
|
+
const idsPromise = requestMemberOrgIds(email, async () => {
|
|
424
|
+
const rows = await queryOrgMembers({
|
|
425
|
+
sql: `SELECT org_id FROM org_members WHERE LOWER(email) = ?
|
|
426
|
+
${MEMBERSHIP_FALLBACK_ORDER_BY}`,
|
|
427
|
+
args: [email.toLowerCase()],
|
|
428
|
+
});
|
|
429
|
+
return rows?.map((r: any) => String(r.org_id)) ?? null;
|
|
417
430
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
431
|
+
// Both reads depend only on the email, so they overlap instead of queueing.
|
|
432
|
+
// The `catch` only keeps the early return below from surfacing an unhandled
|
|
433
|
+
// rejection; the `await` further down still propagates the failure.
|
|
434
|
+
const settingPromise = getUserSetting(
|
|
421
435
|
email,
|
|
422
436
|
"active-org-id",
|
|
423
|
-
)
|
|
437
|
+
) as Promise<ActiveOrgSetting>;
|
|
438
|
+
settingPromise.catch(() => {});
|
|
439
|
+
|
|
440
|
+
const ids = await idsPromise;
|
|
441
|
+
if (!ids?.length) return null;
|
|
442
|
+
const activeOrgSetting = await settingPromise;
|
|
424
443
|
if (activeOrgSetting?.orgId === null) return null;
|
|
425
444
|
if (activeOrgSetting?.orgId && ids.includes(activeOrgSetting.orgId)) {
|
|
426
445
|
return activeOrgSetting.orgId;
|
|
@@ -438,9 +457,13 @@ export async function resolveOrgIdForEmailViaEvent(
|
|
|
438
457
|
event: H3Event,
|
|
439
458
|
email: string,
|
|
440
459
|
): Promise<string | null> {
|
|
460
|
+
// Overlapped, not queued: each is a separate round trip and this pair runs
|
|
461
|
+
// on the session-backfill path of every authenticated request.
|
|
462
|
+
const settingPromise = loadActiveOrgSettingForEvent(event, email);
|
|
463
|
+
settingPromise.catch(() => {});
|
|
441
464
|
const memberships = await loadMembershipsForEvent(event, email);
|
|
442
465
|
if (!memberships || memberships.length === 0) return null;
|
|
443
|
-
const activeOrgSetting = await
|
|
466
|
+
const activeOrgSetting = await settingPromise;
|
|
444
467
|
if (activeOrgSetting?.orgId === null) return null;
|
|
445
468
|
if (
|
|
446
469
|
activeOrgSetting?.orgId &&
|
|
@@ -485,6 +508,7 @@ export async function createOrganization(
|
|
|
485
508
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
|
|
486
509
|
args: [nanoid(), id, email, role, createdAt],
|
|
487
510
|
});
|
|
511
|
+
invalidateRequestMemberOrgIds();
|
|
488
512
|
|
|
489
513
|
await warnOnAdditionalOrganization(exec, email, id, trimmedName);
|
|
490
514
|
|
|
@@ -669,6 +693,7 @@ async function tryCreateDefaultOrg(
|
|
|
669
693
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
|
|
670
694
|
args: [nanoid(), orgId, email, "owner", now],
|
|
671
695
|
});
|
|
696
|
+
invalidateRequestMemberOrgIds();
|
|
672
697
|
|
|
673
698
|
await setActiveOrgId(email, orgId, "auto-created default organization");
|
|
674
699
|
|
package/src/org/handlers.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { readBody } from "../server/h3-helpers.js";
|
|
|
46
46
|
import { setActiveOrgId } from "./active-org.js";
|
|
47
47
|
import { getOrgContext, createOrganization } from "./context.js";
|
|
48
48
|
import { isFreeEmailProvider } from "./free-email-providers.js";
|
|
49
|
+
import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
|
|
49
50
|
import type { OrgRole } from "./types.js";
|
|
50
51
|
import { parseWorkspaceUrl } from "./workspace-url.js";
|
|
51
52
|
|
|
@@ -505,6 +506,7 @@ export const acceptInvitationHandler = defineEventHandler(
|
|
|
505
506
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
|
|
506
507
|
args: [nanoid(), invOrgId, email, inviteRole, Date.now()],
|
|
507
508
|
});
|
|
509
|
+
invalidateRequestMemberOrgIds();
|
|
508
510
|
|
|
509
511
|
await e.execute({
|
|
510
512
|
sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
|
|
@@ -574,6 +576,7 @@ export const removeMemberHandler = defineEventHandler(
|
|
|
574
576
|
sql: `DELETE FROM org_members WHERE org_id = ? AND LOWER(email) = ?`,
|
|
575
577
|
args: [ctx.orgId, memberEmailLower],
|
|
576
578
|
});
|
|
579
|
+
invalidateRequestMemberOrgIds();
|
|
577
580
|
|
|
578
581
|
return { success: true };
|
|
579
582
|
},
|
|
@@ -772,6 +775,8 @@ export const deleteOrgHandler = defineEventHandler(async (event: H3Event) => {
|
|
|
772
775
|
});
|
|
773
776
|
}
|
|
774
777
|
|
|
778
|
+
invalidateRequestMemberOrgIds();
|
|
779
|
+
|
|
775
780
|
const nextRes = await e.execute({
|
|
776
781
|
sql: `SELECT org_id AS "orgId" FROM org_members WHERE LOWER(email) = ? LIMIT 1`,
|
|
777
782
|
args: [ctx.email.toLowerCase()],
|
|
@@ -875,6 +880,7 @@ export const joinByDomainHandler = defineEventHandler(
|
|
|
875
880
|
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, 'member', ?)`,
|
|
876
881
|
args: [nanoid(), orgId, email, Date.now()],
|
|
877
882
|
});
|
|
883
|
+
invalidateRequestMemberOrgIds();
|
|
878
884
|
|
|
879
885
|
await setActiveOrgId(email, orgId, "joined domain-matched organization");
|
|
880
886
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getRequestContext } from "../server/request-context.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-request memo of the `org_members` read behind `resolveOrgIdForEmail`,
|
|
5
|
+
* keyed on the active AsyncLocalStorage `RequestContext` (WeakMap → freed with
|
|
6
|
+
* the request) and then on the lowercased email. Mirrors the settings cache in
|
|
7
|
+
* `settings/store.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The `event.context` caches in `context.ts` only cover call chains that carry
|
|
10
|
+
* an h3 event. Identity resolution for credential lookups, agent runs, A2A,
|
|
11
|
+
* MCP, and adapter-authenticated action calls has no event, so every one of
|
|
12
|
+
* those callers used to pay its own round trip for the same answer — on a
|
|
13
|
+
* remote Postgres that is ~83ms each.
|
|
14
|
+
*
|
|
15
|
+
* TRAP: the key is the email, never "the current request's user". A single
|
|
16
|
+
* request legitimately resolves several addresses (the signed-in caller plus a
|
|
17
|
+
* run owner or credential subject), so a context-only key would answer one
|
|
18
|
+
* identity with another's memberships.
|
|
19
|
+
*/
|
|
20
|
+
const requestOrgIds = new WeakMap<
|
|
21
|
+
object,
|
|
22
|
+
Map<string, Promise<string[] | null>>
|
|
23
|
+
>();
|
|
24
|
+
|
|
25
|
+
function cacheForRequest(
|
|
26
|
+
create: boolean,
|
|
27
|
+
): Map<string, Promise<string[] | null>> | null {
|
|
28
|
+
const ctx = getRequestContext();
|
|
29
|
+
if (!ctx || typeof ctx !== "object") return null;
|
|
30
|
+
let cache = requestOrgIds.get(ctx);
|
|
31
|
+
if (!cache && create) {
|
|
32
|
+
cache = new Map();
|
|
33
|
+
requestOrgIds.set(ctx, cache);
|
|
34
|
+
}
|
|
35
|
+
return cache ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the org ids `email` belongs to, once per request. `null` means the
|
|
40
|
+
* membership rows were unreadable and is cached like any other answer; a
|
|
41
|
+
* rejection is evicted so one transient failure cannot answer every later
|
|
42
|
+
* lookup in the same request.
|
|
43
|
+
*/
|
|
44
|
+
export function requestMemberOrgIds(
|
|
45
|
+
email: string,
|
|
46
|
+
load: () => Promise<string[] | null>,
|
|
47
|
+
): Promise<string[] | null> {
|
|
48
|
+
const cache = cacheForRequest(true);
|
|
49
|
+
if (!cache) return load();
|
|
50
|
+
const key = email.trim().toLowerCase();
|
|
51
|
+
let pending = cache.get(key);
|
|
52
|
+
if (!pending) {
|
|
53
|
+
pending = load().catch((err) => {
|
|
54
|
+
cache.delete(key);
|
|
55
|
+
throw err;
|
|
56
|
+
});
|
|
57
|
+
cache.set(key, pending);
|
|
58
|
+
}
|
|
59
|
+
return pending;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Drop this request's memoized memberships after a write to `org_members`.
|
|
64
|
+
* Clears every email rather than one: deleting an organization or removing a
|
|
65
|
+
* member changes the answer for accounts other than the one being written.
|
|
66
|
+
* Requests already in flight elsewhere keep their own snapshot for the rest of
|
|
67
|
+
* their (short) lifetime, the same tradeoff the settings cache documents.
|
|
68
|
+
*/
|
|
69
|
+
export function invalidateRequestMemberOrgIds(): void {
|
|
70
|
+
cacheForRequest(false)?.clear();
|
|
71
|
+
}
|
package/src/resources/store.ts
CHANGED
|
@@ -1494,18 +1494,19 @@ export async function resourceEffectiveContext(
|
|
|
1494
1494
|
): Promise<EffectiveResourceContext> {
|
|
1495
1495
|
await ensureTable();
|
|
1496
1496
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
});
|
|
1497
|
+
// Four independent reads of the same path under different owners; the
|
|
1498
|
+
// precedence below is pure JS, so serialising them only bought four round
|
|
1499
|
+
// trips of latency. Mirrors `resourceListAccessible`, which already fans out.
|
|
1501
1500
|
const organizationOwner = sharedResourceOwner(options?.orgId);
|
|
1502
|
-
const organization =
|
|
1501
|
+
const [workspace, organization, legacyShared, personal] = await Promise.all([
|
|
1502
|
+
resourceGetByPath(WORKSPACE_OWNER, path, { ...options, userEmail }),
|
|
1503
1503
|
organizationOwner === SHARED_OWNER
|
|
1504
|
-
? null
|
|
1505
|
-
:
|
|
1506
|
-
|
|
1504
|
+
? Promise.resolve(null)
|
|
1505
|
+
: resourceGetByPath(organizationOwner, path),
|
|
1506
|
+
resourceGetByPath(SHARED_OWNER, path),
|
|
1507
|
+
resourceGetByPath(userEmail, path),
|
|
1508
|
+
]);
|
|
1507
1509
|
const shared = organization ?? legacyShared;
|
|
1508
|
-
const personal = await resourceGetByPath(userEmail, path);
|
|
1509
1510
|
const effective = personal ?? shared ?? workspace ?? null;
|
|
1510
1511
|
const effectiveScope: ResourceInheritanceScope | null = personal
|
|
1511
1512
|
? "personal"
|
package/src/scripts/runner.ts
CHANGED
|
@@ -17,7 +17,10 @@ import { pathToFileURL } from "url";
|
|
|
17
17
|
|
|
18
18
|
import type { ActionEntry } from "../agent/production-agent.js";
|
|
19
19
|
import { closeDbExec } from "../db/client.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
actionCallIsReadOnly,
|
|
22
|
+
notifyActionChange,
|
|
23
|
+
} from "../server/action-change.js";
|
|
21
24
|
import {
|
|
22
25
|
runWithRequestContext,
|
|
23
26
|
getRequestOrgId,
|
|
@@ -442,7 +445,7 @@ async function dispatchAction(
|
|
|
442
445
|
) {
|
|
443
446
|
const parsed = parseActionArgs(args, { coerceBooleans: true });
|
|
444
447
|
const result = await handler.run(parsed, cliActionCtx(actionName));
|
|
445
|
-
if (handler
|
|
448
|
+
if (!actionCallIsReadOnly(handler, parsed, false)) {
|
|
446
449
|
await notifyActionChange({ actionName }).catch(() => {});
|
|
447
450
|
}
|
|
448
451
|
if (result) assertCliHandoffLaunched(printActionResult(result));
|
|
@@ -476,7 +479,7 @@ async function dispatchAction(
|
|
|
476
479
|
parsed as Record<string, string>,
|
|
477
480
|
cliActionCtx(actionName),
|
|
478
481
|
);
|
|
479
|
-
if (packageAction
|
|
482
|
+
if (!actionCallIsReadOnly(packageAction, parsed, false)) {
|
|
480
483
|
await notifyActionChange({ actionName }).catch(() => {});
|
|
481
484
|
}
|
|
482
485
|
if (result) assertCliHandoffLaunched(printActionResult(result));
|
package/src/secrets/storage.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
|
|
17
17
|
import { getDbExec, isPostgres } from "../db/client.js";
|
|
18
18
|
import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
|
|
19
|
+
import { getRequestContext } from "../server/request-context.js";
|
|
19
20
|
import {
|
|
20
21
|
encryptSecretValue as encryptLegacyValue,
|
|
21
22
|
encryptSharedSecretValue as encryptValue,
|
|
@@ -217,6 +218,8 @@ export async function writeAppSecret(args: WriteSecretArgs): Promise<string> {
|
|
|
217
218
|
now,
|
|
218
219
|
];
|
|
219
220
|
|
|
221
|
+
invalidateRequestSecret(args);
|
|
222
|
+
|
|
220
223
|
if (isPostgres()) {
|
|
221
224
|
const { rows } = await client.execute({
|
|
222
225
|
sql: `${upsertSql} RETURNING id`,
|
|
@@ -339,6 +342,52 @@ async function populateSharedAppSecret(
|
|
|
339
342
|
}
|
|
340
343
|
}
|
|
341
344
|
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Per-request read memo
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Per-request memo of secret reads, keyed on the active AsyncLocalStorage
|
|
351
|
+
* RequestContext (WeakMap → freed with the request). Mirrors the settings
|
|
352
|
+
* store's cache, including its staleness rule: a write in THIS request is
|
|
353
|
+
* written through, while other in-flight requests keep their snapshot for
|
|
354
|
+
* their own (short) lifetime.
|
|
355
|
+
*
|
|
356
|
+
* The map key is `scope|scopeId|key`, which is the secret's full identity —
|
|
357
|
+
* `user:<email>`, `org:<id>`, `solo:<email>`, `workspace` — so a hit can never
|
|
358
|
+
* answer one caller with another caller's secret. Do NOT reduce the key to
|
|
359
|
+
* just `key`.
|
|
360
|
+
*
|
|
361
|
+
* This exists because one credential resolution is a WATERFALL: user scope,
|
|
362
|
+
* then org, then workspace, then solo. Callers re-resolve the same credential
|
|
363
|
+
* several times per request (engine detection, config resolution, usability
|
|
364
|
+
* checks), and against a remote database each probe is a network round trip.
|
|
365
|
+
*/
|
|
366
|
+
const _requestSecretsCache = new WeakMap<
|
|
367
|
+
object,
|
|
368
|
+
Map<string, ReadSecretResult | null>
|
|
369
|
+
>();
|
|
370
|
+
|
|
371
|
+
function requestSecretsCache(): Map<string, ReadSecretResult | null> | null {
|
|
372
|
+
const ctx = getRequestContext();
|
|
373
|
+
if (!ctx || typeof ctx !== "object") return null;
|
|
374
|
+
let cache = _requestSecretsCache.get(ctx);
|
|
375
|
+
if (!cache) {
|
|
376
|
+
cache = new Map();
|
|
377
|
+
_requestSecretsCache.set(ctx, cache);
|
|
378
|
+
}
|
|
379
|
+
return cache;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function secretCacheKey(ref: SecretRef): string {
|
|
383
|
+
return `${ref.scope}|${ref.scopeId}|${ref.key}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Drop this request's memo for a secret whose stored value just changed. */
|
|
387
|
+
function invalidateRequestSecret(ref: SecretRef): void {
|
|
388
|
+
requestSecretsCache()?.delete(secretCacheKey(ref));
|
|
389
|
+
}
|
|
390
|
+
|
|
342
391
|
type AppSecretsReadQuery = { sql: string; args: unknown[] };
|
|
343
392
|
|
|
344
393
|
/**
|
|
@@ -379,6 +428,17 @@ async function executeAppSecretsRead(query: AppSecretsReadQuery) {
|
|
|
379
428
|
*/
|
|
380
429
|
export async function readAppSecret(
|
|
381
430
|
ref: SecretRef,
|
|
431
|
+
): Promise<ReadSecretResult | null> {
|
|
432
|
+
const cache = requestSecretsCache();
|
|
433
|
+
const cacheKey = secretCacheKey(ref);
|
|
434
|
+
if (cache?.has(cacheKey)) return cache.get(cacheKey) ?? null;
|
|
435
|
+
const result = await readAppSecretUncached(ref);
|
|
436
|
+
cache?.set(cacheKey, result);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function readAppSecretUncached(
|
|
441
|
+
ref: SecretRef,
|
|
382
442
|
): Promise<ReadSecretResult | null> {
|
|
383
443
|
const { key, scope, scopeId } = ref;
|
|
384
444
|
const { rows } = await executeAppSecretsRead({
|
|
@@ -417,15 +477,41 @@ export async function readAppSecrets(args: {
|
|
|
417
477
|
scope: SecretScope;
|
|
418
478
|
scopeId: string;
|
|
419
479
|
}): Promise<Map<string, ReadSecretResult>> {
|
|
420
|
-
const
|
|
421
|
-
if (
|
|
480
|
+
const requested = [...new Set(args.keys.filter(Boolean))];
|
|
481
|
+
if (requested.length === 0) return new Map();
|
|
482
|
+
|
|
483
|
+
const cache = requestSecretsCache();
|
|
484
|
+
const results = new Map<string, ReadSecretResult>();
|
|
485
|
+
const keys: string[] = [];
|
|
486
|
+
for (const key of requested) {
|
|
487
|
+
const cacheKey = secretCacheKey({
|
|
488
|
+
key,
|
|
489
|
+
scope: args.scope,
|
|
490
|
+
scopeId: args.scopeId,
|
|
491
|
+
});
|
|
492
|
+
if (cache?.has(cacheKey)) {
|
|
493
|
+
const cached = cache.get(cacheKey);
|
|
494
|
+
if (cached) results.set(key, cached);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
keys.push(key);
|
|
498
|
+
}
|
|
499
|
+
if (keys.length === 0) return results;
|
|
422
500
|
|
|
423
501
|
const placeholders = keys.map(() => "?").join(", ");
|
|
424
502
|
const { rows } = await executeAppSecretsRead({
|
|
425
503
|
sql: `SELECT key, encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key IN (${placeholders})`,
|
|
426
504
|
args: [args.scope, args.scopeId, ...keys],
|
|
427
505
|
});
|
|
428
|
-
|
|
506
|
+
// The statement covered every uncached key in this scope, so a key missing
|
|
507
|
+
// from `rows` is genuinely absent — memo it as such rather than leaving a
|
|
508
|
+
// single-key read to go ask again.
|
|
509
|
+
for (const key of keys) {
|
|
510
|
+
cache?.set(
|
|
511
|
+
secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }),
|
|
512
|
+
null,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
429
515
|
for (const row of rows) {
|
|
430
516
|
const key = String(row.key ?? "");
|
|
431
517
|
if (!key) continue;
|
|
@@ -442,11 +528,16 @@ export async function readAppSecrets(args: {
|
|
|
442
528
|
decrypted.sharedCiphertextToReplace ?? null,
|
|
443
529
|
);
|
|
444
530
|
}
|
|
445
|
-
|
|
531
|
+
const result = {
|
|
446
532
|
value: decrypted.value,
|
|
447
533
|
last4: last4(decrypted.value),
|
|
448
534
|
updatedAt: Number(row.updated_at ?? 0),
|
|
449
|
-
}
|
|
535
|
+
};
|
|
536
|
+
results.set(key, result);
|
|
537
|
+
cache?.set(
|
|
538
|
+
secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }),
|
|
539
|
+
result,
|
|
540
|
+
);
|
|
450
541
|
} catch {
|
|
451
542
|
// Match readAppSecret: corrupted or stale ciphertext behaves as missing.
|
|
452
543
|
}
|
|
@@ -589,6 +680,7 @@ function parseAllowlist(raw: string | null): string[] | null {
|
|
|
589
680
|
|
|
590
681
|
export async function deleteAppSecret(ref: SecretRef): Promise<boolean> {
|
|
591
682
|
await ensureTable();
|
|
683
|
+
invalidateRequestSecret(ref);
|
|
592
684
|
const { key, scope, scopeId } = ref;
|
|
593
685
|
const client = getDbExec();
|
|
594
686
|
const { rowsAffected } = await client.execute({
|
|
@@ -4,10 +4,43 @@ import {
|
|
|
4
4
|
actionChangeMarkerValue,
|
|
5
5
|
type ActionChangeTarget,
|
|
6
6
|
} from "../action-change-marker.js";
|
|
7
|
+
import type { ActionPlanModeConfig } from "../action.js";
|
|
7
8
|
import { appStatePut } from "../application-state/store.js";
|
|
8
9
|
import { recordChange } from "./poll.js";
|
|
9
10
|
import { getRequestOrgId, getRequestUserEmail } from "./request-context.js";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Whether THIS invocation should announce a change.
|
|
14
|
+
*
|
|
15
|
+
* `readOnly` describes an action name, but a tool that discriminates on an
|
|
16
|
+
* argument reads on some calls and writes on others — `manage-agent-engine`
|
|
17
|
+
* with `action: "list"` is a status poll, with `action: "set"` a write. Such
|
|
18
|
+
* actions already publish the per-call answer as a Plan-mode `effect`
|
|
19
|
+
* predicate, so read it here instead of treating every call as a write: a poll
|
|
20
|
+
* that bumps the `"action"` change version makes every query keyed on it
|
|
21
|
+
* refetch forever.
|
|
22
|
+
*
|
|
23
|
+
* `readOnly` stays authoritative otherwise. Widening the flag to cover mixed
|
|
24
|
+
* tools would also open them to read-only A2A peers and to the agent's
|
|
25
|
+
* read-result cache, where it would be false.
|
|
26
|
+
*/
|
|
27
|
+
export function actionCallIsReadOnly(
|
|
28
|
+
entry: { readOnly?: boolean; planMode?: ActionPlanModeConfig<any> },
|
|
29
|
+
params: unknown,
|
|
30
|
+
fallback: boolean,
|
|
31
|
+
): boolean {
|
|
32
|
+
const effect = entry.planMode?.effect;
|
|
33
|
+
if (typeof effect === "function") {
|
|
34
|
+
try {
|
|
35
|
+
return effect(params) === "read";
|
|
36
|
+
} catch {
|
|
37
|
+
// A predicate that throws says nothing; fall through to the flag.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (typeof entry.readOnly === "boolean") return entry.readOnly;
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
11
44
|
export interface NotifyActionChangeOptions {
|
|
12
45
|
actionName: string;
|
|
13
46
|
owner?: string;
|