@agent-native/core 0.119.5 → 0.119.6
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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +1 -0
- package/corpus/core/src/deploy/build.ts +144 -2
- package/corpus/core/src/deploy/workspace-deploy.ts +123 -1
- package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +22 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +190 -0
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +175 -87
- package/corpus/core/src/integrations/pending-tasks-store.ts +114 -11
- package/corpus/core/src/integrations/plugin.ts +113 -60
- package/corpus/core/src/integrations/task-queue-stats.ts +68 -15
- package/corpus/core/src/integrations/webhook-handler.ts +21 -46
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/core/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/core/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/templates/dispatch/changelog/2026-07-21-recover-stalled-slack-agent-work.md +6 -0
- package/dist/agent/durable-background.d.ts +1 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +1 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +106 -3
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +103 -2
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/integration-durable-dispatch-config.d.ts +7 -0
- package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -0
- package/dist/integrations/integration-durable-dispatch-config.js +16 -0
- package/dist/integrations/integration-durable-dispatch-config.js.map +1 -0
- package/dist/integrations/integration-durable-dispatch.d.ts +23 -0
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -0
- package/dist/integrations/integration-durable-dispatch.js +115 -0
- package/dist/integrations/integration-durable-dispatch.js.map +1 -0
- package/dist/integrations/pending-tasks-retry-job.d.ts +13 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +133 -74
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.d.ts +14 -2
- package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-store.js +75 -11
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +88 -55
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/task-queue-stats.d.ts +15 -3
- package/dist/integrations/task-queue-stats.d.ts.map +1 -1
- package/dist/integrations/task-queue-stats.js +45 -15
- package/dist/integrations/task-queue-stats.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +18 -42
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/dist/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/dist/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/package.json +1 -1
- package/src/agent/durable-background.ts +1 -0
- package/src/deploy/build.ts +144 -2
- package/src/deploy/workspace-deploy.ts +123 -1
- package/src/integrations/integration-durable-dispatch-config.ts +22 -0
- package/src/integrations/integration-durable-dispatch.ts +190 -0
- package/src/integrations/pending-tasks-retry-job.ts +175 -87
- package/src/integrations/pending-tasks-store.ts +114 -11
- package/src/integrations/plugin.ts +113 -60
- package/src/integrations/task-queue-stats.ts +68 -15
- package/src/integrations/webhook-handler.ts +21 -46
- package/src/server/auth.ts +6 -0
- package/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.119.
|
|
3
|
+
"version": "0.119.6",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -85,6 +85,7 @@ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_B
|
|
|
85
85
|
*/
|
|
86
86
|
export const AGENT_BACKGROUND_PROCESSOR_FIELD = "__agentNativeProcessor";
|
|
87
87
|
export const AGENT_BACKGROUND_PROCESSOR_A2A = "a2a";
|
|
88
|
+
export const AGENT_BACKGROUND_PROCESSOR_INTEGRATION = "integration";
|
|
88
89
|
export const AGENT_BACKGROUND_PROCESSOR_ROUTE = "route";
|
|
89
90
|
export const AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD =
|
|
90
91
|
"__agentNativeProcessorRoute";
|
|
@@ -24,10 +24,16 @@ import {
|
|
|
24
24
|
AGENT_BACKGROUND_FUNCTION_NAME,
|
|
25
25
|
AGENT_BACKGROUND_PROCESSOR_A2A,
|
|
26
26
|
AGENT_BACKGROUND_PROCESSOR_FIELD,
|
|
27
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
27
28
|
AGENT_BACKGROUND_PROCESSOR_ROUTE,
|
|
28
29
|
AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
|
|
29
30
|
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
30
31
|
} from "../agent/durable-background.js";
|
|
32
|
+
import {
|
|
33
|
+
INTEGRATION_RETRY_SWEEP_PATH,
|
|
34
|
+
INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
|
|
35
|
+
isIntegrationDurableDispatchConfigured,
|
|
36
|
+
} from "../integrations/integration-durable-dispatch-config.js";
|
|
31
37
|
import { normalizeAppBasePath } from "../server/app-base-path.js";
|
|
32
38
|
import {
|
|
33
39
|
DEFAULT_SSR_CDN_CACHE_CONTROL,
|
|
@@ -2369,6 +2375,13 @@ export function isDurableBackgroundDeployEnabled(): boolean {
|
|
|
2369
2375
|
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
2370
2376
|
}
|
|
2371
2377
|
|
|
2378
|
+
export const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME =
|
|
2379
|
+
"server-integration-recovery";
|
|
2380
|
+
|
|
2381
|
+
export function isIntegrationDurableDispatchDeployEnabled(): boolean {
|
|
2382
|
+
return isIntegrationDurableDispatchConfigured();
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2372
2385
|
const NETLIFY_KEEP_WARM_FUNCTION_NAME = "agent-native-keep-warm";
|
|
2373
2386
|
|
|
2374
2387
|
/**
|
|
@@ -2536,10 +2549,16 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
|
|
|
2536
2549
|
|
|
2537
2550
|
const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
|
|
2538
2551
|
const a2aProcessTaskPath = JSON.stringify("/_agent-native/a2a/_process-task");
|
|
2552
|
+
const integrationProcessTaskPath = JSON.stringify(
|
|
2553
|
+
"/_agent-native/integrations/process-task",
|
|
2554
|
+
);
|
|
2539
2555
|
const backgroundProcessorField = JSON.stringify(
|
|
2540
2556
|
AGENT_BACKGROUND_PROCESSOR_FIELD,
|
|
2541
2557
|
);
|
|
2542
2558
|
const backgroundProcessorA2A = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A);
|
|
2559
|
+
const backgroundProcessorIntegration = JSON.stringify(
|
|
2560
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
2561
|
+
);
|
|
2543
2562
|
const backgroundProcessorRoute = JSON.stringify(
|
|
2544
2563
|
AGENT_BACKGROUND_PROCESSOR_ROUTE,
|
|
2545
2564
|
);
|
|
@@ -2558,8 +2577,10 @@ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
|
2558
2577
|
// The framework route the Nitro router dispatches to (the _process-run plugin).
|
|
2559
2578
|
const PROCESS_RUN_PATH = ${processRunPath};
|
|
2560
2579
|
const A2A_PROCESS_TASK_PATH = ${a2aProcessTaskPath};
|
|
2580
|
+
const INTEGRATION_PROCESS_TASK_PATH = ${integrationProcessTaskPath};
|
|
2561
2581
|
const BACKGROUND_PROCESSOR_FIELD = ${backgroundProcessorField};
|
|
2562
2582
|
const BACKGROUND_PROCESSOR_A2A = ${backgroundProcessorA2A};
|
|
2583
|
+
const BACKGROUND_PROCESSOR_INTEGRATION = ${backgroundProcessorIntegration};
|
|
2563
2584
|
const BACKGROUND_PROCESSOR_ROUTE = ${backgroundProcessorRoute};
|
|
2564
2585
|
const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${backgroundProcessorRouteField};
|
|
2565
2586
|
|
|
@@ -2570,6 +2591,12 @@ function processorPathFromBody(body) {
|
|
|
2570
2591
|
if (parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_A2A) {
|
|
2571
2592
|
return A2A_PROCESS_TASK_PATH;
|
|
2572
2593
|
}
|
|
2594
|
+
if (
|
|
2595
|
+
parsed?.[BACKGROUND_PROCESSOR_FIELD] ===
|
|
2596
|
+
BACKGROUND_PROCESSOR_INTEGRATION
|
|
2597
|
+
) {
|
|
2598
|
+
return INTEGRATION_PROCESS_TASK_PATH;
|
|
2599
|
+
}
|
|
2573
2600
|
const route = parsed?.[BACKGROUND_PROCESSOR_ROUTE_FIELD];
|
|
2574
2601
|
if (
|
|
2575
2602
|
parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
|
|
@@ -2658,6 +2685,78 @@ export const config = {
|
|
|
2658
2685
|
);
|
|
2659
2686
|
}
|
|
2660
2687
|
|
|
2688
|
+
export function emitSingleTemplateNetlifyIntegrationRecoveryFunction(
|
|
2689
|
+
projectCwd: string,
|
|
2690
|
+
): void {
|
|
2691
|
+
const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
|
|
2692
|
+
const serverDir = path.join(internalDir, "server");
|
|
2693
|
+
if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
|
|
2694
|
+
console.warn(
|
|
2695
|
+
"[build] Integration recovery emit skipped: expected Nitro Netlify function " +
|
|
2696
|
+
"at .netlify/functions-internal/server/main.mjs was not found.",
|
|
2697
|
+
);
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
const functionName = NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME;
|
|
2701
|
+
const dest = path.join(internalDir, functionName);
|
|
2702
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
2703
|
+
copyDir(serverDir, dest);
|
|
2704
|
+
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
2705
|
+
|
|
2706
|
+
const entry = `import { createHmac } from "node:crypto";
|
|
2707
|
+
|
|
2708
|
+
const SWEEP_PATH = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_PATH)};
|
|
2709
|
+
const SWEEP_SUBJECT = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT)};
|
|
2710
|
+
|
|
2711
|
+
function enabled() {
|
|
2712
|
+
const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
|
|
2713
|
+
if (!raw) return false;
|
|
2714
|
+
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function token(secret) {
|
|
2718
|
+
const timestamp = Date.now();
|
|
2719
|
+
const signature = createHmac("sha256", secret)
|
|
2720
|
+
.update(\`${INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
|
|
2721
|
+
.digest("hex");
|
|
2722
|
+
return \`\${timestamp}.\${signature}\`;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
let cachedHandler;
|
|
2726
|
+
|
|
2727
|
+
export default async function handler(request, context) {
|
|
2728
|
+
if (!enabled()) return new Response(null, { status: 204 });
|
|
2729
|
+
const secret = process.env.A2A_SECRET;
|
|
2730
|
+
if (!secret) {
|
|
2731
|
+
console.error("[integration-recovery] A2A_SECRET is required; sweep skipped");
|
|
2732
|
+
return new Response(null, { status: 204 });
|
|
2733
|
+
}
|
|
2734
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
2735
|
+
const url = new URL(request.url);
|
|
2736
|
+
url.pathname = SWEEP_PATH;
|
|
2737
|
+
const rewritten = new Request(url.toString(), {
|
|
2738
|
+
method: "POST",
|
|
2739
|
+
headers: {
|
|
2740
|
+
Authorization: \`Bearer \${token(secret)}\`,
|
|
2741
|
+
"Content-Type": "application/json",
|
|
2742
|
+
},
|
|
2743
|
+
body: JSON.stringify({ taskId: SWEEP_SUBJECT }),
|
|
2744
|
+
});
|
|
2745
|
+
return cachedHandler(rewritten, context);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
export const config = {
|
|
2749
|
+
name: "integration pending-task recovery",
|
|
2750
|
+
generator: "agent-native build",
|
|
2751
|
+
schedule: "* * * * *",
|
|
2752
|
+
nodeBundler: "none",
|
|
2753
|
+
includedFiles: ["**"],
|
|
2754
|
+
preferStatic: false,
|
|
2755
|
+
};
|
|
2756
|
+
`;
|
|
2757
|
+
fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2661
2760
|
/**
|
|
2662
2761
|
* Nitro's Netlify preset can emit a harmful fallback rewrite to
|
|
2663
2762
|
* `/.netlify/functions/server`. With `config.path: "/*"`, that default URL is
|
|
@@ -2950,7 +3049,10 @@ export function assertSingleTemplateNetlifyBuildOutput(
|
|
|
2950
3049
|
);
|
|
2951
3050
|
}
|
|
2952
3051
|
|
|
2953
|
-
if (
|
|
3052
|
+
if (
|
|
3053
|
+
isDurableBackgroundDeployEnabled() ||
|
|
3054
|
+
isIntegrationDurableDispatchDeployEnabled()
|
|
3055
|
+
) {
|
|
2954
3056
|
const backgroundDir = path.join(
|
|
2955
3057
|
internalDir,
|
|
2956
3058
|
AGENT_BACKGROUND_FUNCTION_NAME,
|
|
@@ -2987,6 +3089,32 @@ export function assertSingleTemplateNetlifyBuildOutput(
|
|
|
2987
3089
|
}
|
|
2988
3090
|
}
|
|
2989
3091
|
|
|
3092
|
+
if (isIntegrationDurableDispatchDeployEnabled()) {
|
|
3093
|
+
const recoveryEntryPath = path.join(
|
|
3094
|
+
internalDir,
|
|
3095
|
+
NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME,
|
|
3096
|
+
`${NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME}.mjs`,
|
|
3097
|
+
);
|
|
3098
|
+
if (!fs.existsSync(recoveryEntryPath)) {
|
|
3099
|
+
failures.push(
|
|
3100
|
+
`integration durable dispatch is enabled but ${path.relative(
|
|
3101
|
+
projectCwd,
|
|
3102
|
+
recoveryEntryPath,
|
|
3103
|
+
)} was not emitted`,
|
|
3104
|
+
);
|
|
3105
|
+
} else {
|
|
3106
|
+
const recoveryEntry = fs.readFileSync(recoveryEntryPath, "utf-8");
|
|
3107
|
+
if (!/\bschedule\s*:\s*["']\* \* \* \* \*["']/.test(recoveryEntry)) {
|
|
3108
|
+
failures.push(
|
|
3109
|
+
`integration recovery entry ${path.relative(
|
|
3110
|
+
projectCwd,
|
|
3111
|
+
recoveryEntryPath,
|
|
3112
|
+
)} is missing the one-minute schedule`,
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
|
|
2990
3118
|
if (failures.length > 0) {
|
|
2991
3119
|
throw new Error(
|
|
2992
3120
|
"[deploy] Netlify deploy guard failed; refusing to publish an output " +
|
|
@@ -3638,7 +3766,10 @@ export default bundle;
|
|
|
3638
3766
|
// bundle, so the chat `_process-run` POST lands on Netlify's async (15-min)
|
|
3639
3767
|
// function. When not opted in this is a no-op and the single-function
|
|
3640
3768
|
// deploy is byte-for-byte unchanged.
|
|
3641
|
-
if (
|
|
3769
|
+
if (
|
|
3770
|
+
isDurableBackgroundDeployEnabled() ||
|
|
3771
|
+
isIntegrationDurableDispatchDeployEnabled()
|
|
3772
|
+
) {
|
|
3642
3773
|
try {
|
|
3643
3774
|
emitSingleTemplateNetlifyBackgroundFunction(cwd);
|
|
3644
3775
|
} catch (err) {
|
|
@@ -3649,6 +3780,17 @@ export default bundle;
|
|
|
3649
3780
|
}
|
|
3650
3781
|
}
|
|
3651
3782
|
|
|
3783
|
+
if (isIntegrationDurableDispatchDeployEnabled()) {
|
|
3784
|
+
try {
|
|
3785
|
+
emitSingleTemplateNetlifyIntegrationRecoveryFunction(cwd);
|
|
3786
|
+
} catch (err) {
|
|
3787
|
+
console.warn(
|
|
3788
|
+
"[build] Failed to emit integration recovery Netlify function (non-fatal):",
|
|
3789
|
+
err instanceof Error ? err.message : err,
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3652
3794
|
writeSingleTemplateNetlifyRedirects(cwd);
|
|
3653
3795
|
assertSingleTemplateNetlifyBuildOutput(cwd);
|
|
3654
3796
|
}
|
|
@@ -21,10 +21,16 @@ import path from "path";
|
|
|
21
21
|
import {
|
|
22
22
|
AGENT_BACKGROUND_PROCESSOR_A2A,
|
|
23
23
|
AGENT_BACKGROUND_PROCESSOR_FIELD,
|
|
24
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
24
25
|
AGENT_BACKGROUND_PROCESSOR_ROUTE,
|
|
25
26
|
AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
|
|
26
27
|
AGENT_CHAT_PROCESS_RUN_PATH,
|
|
27
28
|
} from "../agent/durable-background.js";
|
|
29
|
+
import {
|
|
30
|
+
INTEGRATION_RETRY_SWEEP_PATH,
|
|
31
|
+
INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
|
|
32
|
+
isIntegrationDurableDispatchConfigured,
|
|
33
|
+
} from "../integrations/integration-durable-dispatch-config.js";
|
|
28
34
|
import { findWorkspaceRoot } from "../scripts/utils.js";
|
|
29
35
|
import {
|
|
30
36
|
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
@@ -677,9 +683,19 @@ function copyNetlifyFunctionIntoWorkspace(
|
|
|
677
683
|
// Durable background agent runs (default-ON; opt out with a falsy
|
|
678
684
|
// AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: when explicitly opted out
|
|
679
685
|
// this emits nothing and the single-function deploy is unchanged.
|
|
680
|
-
|
|
686
|
+
const integrationDurableDispatch =
|
|
687
|
+
app === "dispatch" && isIntegrationDurableDispatchConfigured();
|
|
688
|
+
if (isDurableBackgroundDeployEnabled() || integrationDurableDispatch) {
|
|
681
689
|
emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
|
|
682
690
|
}
|
|
691
|
+
if (integrationDurableDispatch) {
|
|
692
|
+
emitNetlifyIntegrationRecoveryFunction(
|
|
693
|
+
workspaceRoot,
|
|
694
|
+
app,
|
|
695
|
+
src,
|
|
696
|
+
workspaceApps,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
683
699
|
}
|
|
684
700
|
|
|
685
701
|
/**
|
|
@@ -753,6 +769,7 @@ function emitNetlifyBackgroundFunction(
|
|
|
753
769
|
// incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
|
|
754
770
|
const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
|
|
755
771
|
const a2aProcessTaskPath = `${basePath}/_agent-native/a2a/_process-task`;
|
|
772
|
+
const integrationProcessTaskPath = `${basePath}/_agent-native/integrations/process-task`;
|
|
756
773
|
const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
|
|
757
774
|
// is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
|
|
758
775
|
// function (the deployed Lambda name is not guaranteed to end in -background). A
|
|
@@ -764,8 +781,10 @@ const basePath = ${JSON.stringify(basePath)};
|
|
|
764
781
|
// The base-path-prefixed framework route the Nitro router dispatches to.
|
|
765
782
|
const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
|
|
766
783
|
const A2A_PROCESS_TASK_PATH = ${JSON.stringify(a2aProcessTaskPath)};
|
|
784
|
+
const INTEGRATION_PROCESS_TASK_PATH = ${JSON.stringify(integrationProcessTaskPath)};
|
|
767
785
|
const BACKGROUND_PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
|
|
768
786
|
const BACKGROUND_PROCESSOR_A2A = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A)};
|
|
787
|
+
const BACKGROUND_PROCESSOR_INTEGRATION = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION)};
|
|
769
788
|
const BACKGROUND_PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
|
|
770
789
|
const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
|
|
771
790
|
|
|
@@ -776,6 +795,12 @@ function processorPathFromBody(body) {
|
|
|
776
795
|
if (parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_A2A) {
|
|
777
796
|
return A2A_PROCESS_TASK_PATH;
|
|
778
797
|
}
|
|
798
|
+
if (
|
|
799
|
+
parsed?.[BACKGROUND_PROCESSOR_FIELD] ===
|
|
800
|
+
BACKGROUND_PROCESSOR_INTEGRATION
|
|
801
|
+
) {
|
|
802
|
+
return INTEGRATION_PROCESS_TASK_PATH;
|
|
803
|
+
}
|
|
779
804
|
const route = parsed?.[BACKGROUND_PROCESSOR_ROUTE_FIELD];
|
|
780
805
|
if (
|
|
781
806
|
parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
|
|
@@ -862,6 +887,103 @@ export const config = {
|
|
|
862
887
|
);
|
|
863
888
|
}
|
|
864
889
|
|
|
890
|
+
function emitNetlifyIntegrationRecoveryFunction(
|
|
891
|
+
workspaceRoot: string,
|
|
892
|
+
app: string,
|
|
893
|
+
srcServerDir: string,
|
|
894
|
+
workspaceApps: WorkspaceAppManifestEntry[],
|
|
895
|
+
): void {
|
|
896
|
+
const functionName = `${app}-integration-recovery`;
|
|
897
|
+
const dest = path.join(netlifyFunctionsDir(workspaceRoot), functionName);
|
|
898
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
899
|
+
copyDir(srcServerDir, dest);
|
|
900
|
+
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
901
|
+
|
|
902
|
+
const basePath = `/${app}`;
|
|
903
|
+
const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
|
|
904
|
+
const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
|
|
905
|
+
workspaceApps,
|
|
906
|
+
app,
|
|
907
|
+
);
|
|
908
|
+
const sweepPath = `${basePath}${INTEGRATION_RETRY_SWEEP_PATH}`;
|
|
909
|
+
const entry = `import { createHmac } from "node:crypto";
|
|
910
|
+
|
|
911
|
+
const basePath = ${JSON.stringify(basePath)};
|
|
912
|
+
const SWEEP_PATH = ${JSON.stringify(sweepPath)};
|
|
913
|
+
const SWEEP_SUBJECT = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT)};
|
|
914
|
+
|
|
915
|
+
function setBasePathEnv() {
|
|
916
|
+
const processRef = globalThis.process ??= { env: {} };
|
|
917
|
+
processRef.env ??= {};
|
|
918
|
+
Object.assign(processRef.env, {
|
|
919
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
920
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
921
|
+
APP_BASE_PATH: basePath,
|
|
922
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
923
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
924
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
925
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
926
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
|
|
927
|
+
VITE_APP_BASE_PATH: basePath,
|
|
928
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
|
|
929
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
|
|
930
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
|
|
931
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
932
|
+
${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function enabled() {
|
|
937
|
+
const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
|
|
938
|
+
if (!raw) return false;
|
|
939
|
+
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function token(secret) {
|
|
943
|
+
const timestamp = Date.now();
|
|
944
|
+
const signature = createHmac("sha256", secret)
|
|
945
|
+
.update(\`${INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
|
|
946
|
+
.digest("hex");
|
|
947
|
+
return \`\${timestamp}.\${signature}\`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
setBasePathEnv();
|
|
951
|
+
let cachedHandler;
|
|
952
|
+
|
|
953
|
+
export default async function handler(request, context) {
|
|
954
|
+
setBasePathEnv();
|
|
955
|
+
if (!enabled()) return new Response(null, { status: 204 });
|
|
956
|
+
const secret = process.env.A2A_SECRET;
|
|
957
|
+
if (!secret) {
|
|
958
|
+
console.error("[integration-recovery] A2A_SECRET is required; sweep skipped");
|
|
959
|
+
return new Response(null, { status: 204 });
|
|
960
|
+
}
|
|
961
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
962
|
+
const url = new URL(request.url);
|
|
963
|
+
url.pathname = SWEEP_PATH;
|
|
964
|
+
const rewritten = new Request(url.toString(), {
|
|
965
|
+
method: "POST",
|
|
966
|
+
headers: {
|
|
967
|
+
Authorization: \`Bearer \${token(secret)}\`,
|
|
968
|
+
"Content-Type": "application/json",
|
|
969
|
+
},
|
|
970
|
+
body: JSON.stringify({ taskId: SWEEP_SUBJECT }),
|
|
971
|
+
});
|
|
972
|
+
return cachedHandler(rewritten, context);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export const config = {
|
|
976
|
+
name: ${JSON.stringify(`${app} integration pending-task recovery`)},
|
|
977
|
+
generator: "agent-native workspace deploy",
|
|
978
|
+
schedule: "* * * * *",
|
|
979
|
+
nodeBundler: "none",
|
|
980
|
+
includedFiles: ["**"],
|
|
981
|
+
preferStatic: false,
|
|
982
|
+
};
|
|
983
|
+
`;
|
|
984
|
+
fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
|
|
985
|
+
}
|
|
986
|
+
|
|
865
987
|
function patchNetlifyFunctionEntry(
|
|
866
988
|
functionDir: string,
|
|
867
989
|
app: string,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const INTEGRATION_DURABLE_DISPATCH_ENV =
|
|
2
|
+
"AGENT_INTEGRATION_DURABLE_DISPATCH";
|
|
3
|
+
export const INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV =
|
|
4
|
+
"AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES";
|
|
5
|
+
export const INTEGRATION_PROCESS_TASK_PATH =
|
|
6
|
+
"/_agent-native/integrations/process-task";
|
|
7
|
+
export const INTEGRATION_RETRY_SWEEP_PATH =
|
|
8
|
+
"/_agent-native/integrations/retry-stuck-tasks";
|
|
9
|
+
export const INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT =
|
|
10
|
+
"integration-pending-tasks-sweep";
|
|
11
|
+
|
|
12
|
+
export function isIntegrationDurableDispatchConfigured(): boolean {
|
|
13
|
+
const value = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
|
|
14
|
+
if (!value) return false;
|
|
15
|
+
const normalized = value.trim().toLowerCase();
|
|
16
|
+
return (
|
|
17
|
+
normalized === "1" ||
|
|
18
|
+
normalized === "true" ||
|
|
19
|
+
normalized === "yes" ||
|
|
20
|
+
normalized === "on"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { hasConfiguredA2ASecret } from "../a2a/auth-policy.js";
|
|
2
|
+
import {
|
|
3
|
+
AGENT_BACKGROUND_PROCESSOR_FIELD,
|
|
4
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
5
|
+
dispatchPathTargetsNetlifyBackgroundFunction,
|
|
6
|
+
resolveDurableBackgroundDispatchPath,
|
|
7
|
+
} from "../agent/durable-background.js";
|
|
8
|
+
import { fireInternalDispatch } from "../server/self-dispatch.js";
|
|
9
|
+
import {
|
|
10
|
+
INTEGRATION_DURABLE_DISPATCH_ENV,
|
|
11
|
+
INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV,
|
|
12
|
+
INTEGRATION_PROCESS_TASK_PATH,
|
|
13
|
+
INTEGRATION_RETRY_SWEEP_PATH,
|
|
14
|
+
INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
|
|
15
|
+
isIntegrationDurableDispatchConfigured,
|
|
16
|
+
} from "./integration-durable-dispatch-config.js";
|
|
17
|
+
import { recordPendingTaskDispatchAttempt } from "./pending-tasks-store.js";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
INTEGRATION_DURABLE_DISPATCH_ENV,
|
|
21
|
+
INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV,
|
|
22
|
+
INTEGRATION_PROCESS_TASK_PATH,
|
|
23
|
+
INTEGRATION_RETRY_SWEEP_PATH,
|
|
24
|
+
INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
|
|
25
|
+
isIntegrationDurableDispatchConfigured,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type IntegrationDispatchOutcome =
|
|
29
|
+
| "background-acknowledged"
|
|
30
|
+
| "portable-unconfirmed"
|
|
31
|
+
| "failed";
|
|
32
|
+
|
|
33
|
+
export interface IntegrationDispatchTaskScope {
|
|
34
|
+
platform: string;
|
|
35
|
+
externalThreadId: string;
|
|
36
|
+
platformContext?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IntegrationDurableDispatchScope {
|
|
40
|
+
platform: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function integrationDispatchScopeValue(
|
|
45
|
+
task: IntegrationDispatchTaskScope,
|
|
46
|
+
): string | null {
|
|
47
|
+
const value = task.platformContext?.channelId;
|
|
48
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function configuredIntegrationDurableDispatchScopes():
|
|
52
|
+
| IntegrationDurableDispatchScope[]
|
|
53
|
+
| null {
|
|
54
|
+
const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES;
|
|
55
|
+
if (!raw?.trim()) return null;
|
|
56
|
+
return raw
|
|
57
|
+
.split(",")
|
|
58
|
+
.map((value) => value.trim())
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((scope) => {
|
|
61
|
+
const separator = scope.indexOf(":");
|
|
62
|
+
return {
|
|
63
|
+
platform: separator > 0 ? scope.slice(0, separator) : "",
|
|
64
|
+
value: separator > 0 ? scope.slice(separator + 1) : "",
|
|
65
|
+
};
|
|
66
|
+
})
|
|
67
|
+
.filter((scope) => scope.platform && scope.value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function taskScopeCandidates(task: IntegrationDispatchTaskScope): string[] {
|
|
71
|
+
const candidates = [
|
|
72
|
+
`${task.platform}:*`,
|
|
73
|
+
`${task.platform}:${task.externalThreadId}`,
|
|
74
|
+
];
|
|
75
|
+
const explicitScope = integrationDispatchScopeValue(task);
|
|
76
|
+
if (explicitScope) {
|
|
77
|
+
candidates.push(`${task.platform}:${explicitScope}`);
|
|
78
|
+
} else if (task.platform === "slack") {
|
|
79
|
+
const channelId = task.externalThreadId.split(":")[2];
|
|
80
|
+
if (channelId) candidates.push(`slack:${channelId}`);
|
|
81
|
+
}
|
|
82
|
+
return candidates;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isIntegrationDurableDispatchEnabledForTask(
|
|
86
|
+
task: IntegrationDispatchTaskScope,
|
|
87
|
+
): boolean {
|
|
88
|
+
if (!isIntegrationDurableDispatchConfigured()) return false;
|
|
89
|
+
const scopes = configuredIntegrationDurableDispatchScopes();
|
|
90
|
+
if (
|
|
91
|
+
scopes &&
|
|
92
|
+
!taskScopeCandidates(task).some((candidate) =>
|
|
93
|
+
scopes.some((scope) => `${scope.platform}:${scope.value}` === candidate),
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const path = resolveDurableBackgroundDispatchPath(
|
|
99
|
+
INTEGRATION_PROCESS_TASK_PATH,
|
|
100
|
+
);
|
|
101
|
+
return (
|
|
102
|
+
dispatchPathTargetsNetlifyBackgroundFunction(path) &&
|
|
103
|
+
hasConfiguredA2ASecret()
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function recordDispatch(
|
|
108
|
+
taskId: string,
|
|
109
|
+
outcome: IntegrationDispatchOutcome,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
await recordPendingTaskDispatchAttempt(taskId, outcome);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[integrations] Failed to record dispatch outcome for ${taskId}:`,
|
|
116
|
+
error,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function logDispatch(
|
|
122
|
+
taskId: string,
|
|
123
|
+
outcome: IntegrationDispatchOutcome,
|
|
124
|
+
startedAt: number,
|
|
125
|
+
): void {
|
|
126
|
+
console.info("[integrations] pending task dispatch", {
|
|
127
|
+
taskId,
|
|
128
|
+
outcome,
|
|
129
|
+
durationMs: Date.now() - startedAt,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function dispatchPendingIntegrationTask(input: {
|
|
134
|
+
taskId: string;
|
|
135
|
+
task: IntegrationDispatchTaskScope;
|
|
136
|
+
event?: unknown;
|
|
137
|
+
baseUrl?: string;
|
|
138
|
+
portableSettleMs?: number;
|
|
139
|
+
}): Promise<IntegrationDispatchOutcome> {
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
const durable = isIntegrationDurableDispatchEnabledForTask(input.task);
|
|
142
|
+
if (durable) {
|
|
143
|
+
const backgroundPath = resolveDurableBackgroundDispatchPath(
|
|
144
|
+
INTEGRATION_PROCESS_TASK_PATH,
|
|
145
|
+
);
|
|
146
|
+
try {
|
|
147
|
+
await fireInternalDispatch({
|
|
148
|
+
event: input.event,
|
|
149
|
+
baseUrl: input.baseUrl,
|
|
150
|
+
path: backgroundPath,
|
|
151
|
+
taskId: input.taskId,
|
|
152
|
+
body: {
|
|
153
|
+
[AGENT_BACKGROUND_PROCESSOR_FIELD]:
|
|
154
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
155
|
+
},
|
|
156
|
+
awaitResponse: true,
|
|
157
|
+
responseTimeoutMs: 2_000,
|
|
158
|
+
});
|
|
159
|
+
await recordDispatch(input.taskId, "background-acknowledged");
|
|
160
|
+
logDispatch(input.taskId, "background-acknowledged", startedAt);
|
|
161
|
+
return "background-acknowledged";
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(
|
|
164
|
+
`[integrations] Background dispatch failed for ${input.taskId}; trying the portable processor:`,
|
|
165
|
+
error,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await fireInternalDispatch({
|
|
172
|
+
event: input.event,
|
|
173
|
+
baseUrl: input.baseUrl,
|
|
174
|
+
path: INTEGRATION_PROCESS_TASK_PATH,
|
|
175
|
+
taskId: input.taskId,
|
|
176
|
+
settleMs: input.portableSettleMs,
|
|
177
|
+
});
|
|
178
|
+
await recordDispatch(input.taskId, "portable-unconfirmed");
|
|
179
|
+
logDispatch(input.taskId, "portable-unconfirmed", startedAt);
|
|
180
|
+
return "portable-unconfirmed";
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(
|
|
183
|
+
`[integrations] Portable dispatch failed for ${input.taskId}:`,
|
|
184
|
+
error,
|
|
185
|
+
);
|
|
186
|
+
await recordDispatch(input.taskId, "failed");
|
|
187
|
+
logDispatch(input.taskId, "failed", startedAt);
|
|
188
|
+
return "failed";
|
|
189
|
+
}
|
|
190
|
+
}
|