experimental-ash 0.25.1 → 0.26.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.
- package/CHANGELOG.md +12 -0
- package/bin/ash.d.ts +4 -4
- package/bin/ash.js +12 -8
- package/dist/docs/public/channels/README.md +26 -2
- package/dist/docs/public/channels/discord.md +159 -0
- package/dist/docs/public/channels/slack.md +14 -2
- package/dist/src/channel/routes.d.ts +6 -1
- package/dist/src/channel/send.js +5 -2
- package/dist/src/channel/session-callback.d.ts +10 -0
- package/dist/src/channel/session-callback.js +65 -0
- package/dist/src/channel/types.d.ts +19 -0
- package/dist/src/chunks/{client-BShLWzR6.js → client-ZqNLLMZB.js} +3 -3
- package/dist/src/chunks/{compile-agent-CyP6FrL8.js → compile-agent-DrIyb818.js} +1 -1
- package/dist/src/chunks/{dev-authored-source-watcher-DIWfVUsu.js → dev-authored-source-watcher-C1WUVv9F.js} +1 -1
- package/dist/src/chunks/host-CwAcCrg7.js +70 -0
- package/dist/src/chunks/paths-CWZN-XRX.js +85 -0
- package/dist/src/chunks/{token-BOkIxJeV.js → token-YW4VSeBB.js} +1 -1
- package/dist/src/chunks/types-BJSR0JNV.js +1 -0
- package/dist/src/cli/commands/channels.d.ts +15 -0
- package/dist/src/cli/commands/channels.js +9 -0
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/dev/repl.js +3 -3
- package/dist/src/cli/run.js +1 -1
- package/dist/src/client/message-reducer.js +6 -0
- package/dist/src/context/keys.d.ts +1 -1
- package/dist/src/context/keys.js +1 -1
- package/dist/src/context/seed-keys.d.ts +5 -1
- package/dist/src/context/seed-keys.js +4 -0
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/await-authorization-orchestrator.js +1 -1
- package/dist/src/execution/node-step.js +13 -0
- package/dist/src/execution/remote-agent-dispatch.d.ts +15 -0
- package/dist/src/execution/remote-agent-dispatch.js +79 -0
- package/dist/src/execution/runtime-context.js +4 -1
- package/dist/src/execution/session-callback-step.d.ts +16 -0
- package/dist/src/execution/session-callback-step.js +72 -0
- package/dist/src/execution/subagent-invocation.d.ts +16 -0
- package/dist/src/execution/subagent-invocation.js +16 -0
- package/dist/src/execution/subagent-tool.js +5 -8
- package/dist/src/execution/workflow-entry.js +21 -1
- package/dist/src/execution/workflow-steps.d.ts +6 -1
- package/dist/src/execution/workflow-steps.js +76 -25
- package/dist/src/harness/execute-tool.d.ts +3 -3
- package/dist/src/harness/runtime-actions.d.ts +1 -0
- package/dist/src/harness/runtime-actions.js +18 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/process/pnpm.d.ts +28 -0
- package/dist/src/internal/process/pnpm.js +50 -0
- package/dist/src/protocol/message.d.ts +6 -0
- package/dist/src/protocol/message.js +1 -0
- package/dist/src/protocol/routes.d.ts +11 -0
- package/dist/src/protocol/routes.js +13 -0
- package/dist/src/public/channels/ash.js +25 -1
- package/dist/src/public/channels/discord/api.d.ts +99 -0
- package/dist/src/public/channels/discord/api.js +167 -0
- package/dist/src/public/channels/discord/defaults.d.ts +9 -0
- package/dist/src/public/channels/discord/defaults.js +74 -0
- package/dist/src/public/channels/discord/discordChannel.d.ts +132 -0
- package/dist/src/public/channels/discord/discordChannel.js +402 -0
- package/dist/src/public/channels/discord/hitl.d.ts +34 -0
- package/dist/src/public/channels/discord/hitl.js +194 -0
- package/dist/src/public/channels/discord/inbound.d.ts +97 -0
- package/dist/src/public/channels/discord/inbound.js +238 -0
- package/dist/src/public/channels/discord/index.d.ts +7 -0
- package/dist/src/public/channels/discord/index.js +6 -0
- package/dist/src/public/channels/discord/responses.d.ts +11 -0
- package/dist/src/public/channels/discord/responses.js +40 -0
- package/dist/src/public/channels/discord/verify.d.ts +38 -0
- package/dist/src/public/channels/discord/verify.js +72 -0
- package/dist/src/public/channels/discord/verifyInbound.d.ts +6 -0
- package/dist/src/public/channels/discord/verifyInbound.js +19 -0
- package/dist/src/public/channels/slack/constants.d.ts +7 -0
- package/dist/src/public/channels/slack/constants.js +7 -0
- package/dist/src/public/channels/slack/slackChannel.js +2 -1
- package/dist/src/runtime/actions/keys.js +2 -0
- package/dist/src/runtime/actions/types.d.ts +47 -1
- package/dist/src/runtime/actions/types.js +23 -0
- package/dist/src/runtime/connections/callback-route.d.ts +1 -1
- package/dist/src/runtime/connections/callback-route.js +1 -1
- package/dist/src/runtime/connections/mcp-client.d.ts +1 -2
- package/dist/src/runtime/connections/mcp-client.js +69 -3
- package/dist/src/runtime/framework-channels/index.js +7 -2
- package/dist/src/runtime/session-callback-route.d.ts +6 -0
- package/dist/src/runtime/session-callback-route.js +87 -0
- package/package.json +9 -3
- package/dist/src/chunks/host-BxT35q6K.js +0 -70
- package/dist/src/chunks/paths-B2hLA0Fn.js +0 -85
- package/dist/src/chunks/types-CjIyrcYo.js +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord inbound-interaction verification.
|
|
3
|
+
*
|
|
4
|
+
* Discord signs interaction webhooks with Ed25519. The signed payload is
|
|
5
|
+
* the `X-Signature-Timestamp` header concatenated with the exact raw
|
|
6
|
+
* request body string.
|
|
7
|
+
*/
|
|
8
|
+
import { createPublicKey, verify } from "node:crypto";
|
|
9
|
+
import { createLogger } from "#internal/logging.js";
|
|
10
|
+
const log = createLogger("discord.verify");
|
|
11
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
12
|
+
/** Resolves a Discord public key, falling back to `DISCORD_PUBLIC_KEY`. */
|
|
13
|
+
export async function resolveDiscordPublicKey(publicKey) {
|
|
14
|
+
const source = publicKey ?? process.env.DISCORD_PUBLIC_KEY;
|
|
15
|
+
if (!source)
|
|
16
|
+
throw new Error("DISCORD_PUBLIC_KEY is required.");
|
|
17
|
+
return typeof source === "function" ? await source() : source;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Verifies an inbound Discord interaction request and returns the raw body.
|
|
21
|
+
*
|
|
22
|
+
* Throws when no public key/verifier is configured, required signature
|
|
23
|
+
* headers are missing, timestamp checks fail, or the signature check fails.
|
|
24
|
+
*/
|
|
25
|
+
export async function verifyDiscordRequest(request, options) {
|
|
26
|
+
const body = await request.text();
|
|
27
|
+
if (options.webhookVerifier !== undefined) {
|
|
28
|
+
const result = await options.webhookVerifier(request, body);
|
|
29
|
+
if (!result) {
|
|
30
|
+
throw new Error("discordChannel: inbound webhook verifier rejected the request.");
|
|
31
|
+
}
|
|
32
|
+
return typeof result === "string" ? result : body;
|
|
33
|
+
}
|
|
34
|
+
const publicKey = await resolveDiscordPublicKey(options.publicKey);
|
|
35
|
+
const signature = request.headers.get("x-signature-ed25519") ?? "";
|
|
36
|
+
const timestamp = request.headers.get("x-signature-timestamp") ?? "";
|
|
37
|
+
if (!signature || !timestamp) {
|
|
38
|
+
throw new Error("discordChannel: inbound request missing Discord signature headers.");
|
|
39
|
+
}
|
|
40
|
+
const timestampSeconds = Number(timestamp);
|
|
41
|
+
if (!Number.isFinite(timestampSeconds)) {
|
|
42
|
+
throw new Error("discordChannel: inbound request has malformed timestamp.");
|
|
43
|
+
}
|
|
44
|
+
const maxSkew = options.maxSkewSeconds ?? 60 * 5;
|
|
45
|
+
const now = Math.floor(Date.now() / 1000);
|
|
46
|
+
if (Math.abs(now - timestampSeconds) > maxSkew) {
|
|
47
|
+
throw new Error("discordChannel: inbound request timestamp outside allowed skew.");
|
|
48
|
+
}
|
|
49
|
+
if (!verifyDiscordSignature({ body, publicKey, signature, timestamp })) {
|
|
50
|
+
throw new Error("discordChannel: inbound request signature mismatch.");
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
/** Verifies one Discord Ed25519 signature. */
|
|
55
|
+
export function verifyDiscordSignature(input) {
|
|
56
|
+
try {
|
|
57
|
+
const publicKeyBytes = Buffer.from(input.publicKey, "hex");
|
|
58
|
+
const signatureBytes = Buffer.from(input.signature, "hex");
|
|
59
|
+
if (publicKeyBytes.length !== 32 || signatureBytes.length !== 64)
|
|
60
|
+
return false;
|
|
61
|
+
const key = createPublicKey({
|
|
62
|
+
format: "der",
|
|
63
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, publicKeyBytes]),
|
|
64
|
+
type: "spki",
|
|
65
|
+
});
|
|
66
|
+
return verify(null, Buffer.from(`${input.timestamp}${input.body}`), key, signatureBytes);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
log.debug("Discord signature verification threw", { error });
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DiscordChannelCredentials } from "#public/channels/discord/discordChannel.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verifies an inbound Discord request and returns its raw body, or
|
|
4
|
+
* `null` when verification fails.
|
|
5
|
+
*/
|
|
6
|
+
export declare function verifyDiscordInbound(req: Request, credentials: DiscordChannelCredentials | undefined): Promise<string | null>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createLogger } from "#internal/logging.js";
|
|
2
|
+
import { verifyDiscordRequest } from "#public/channels/discord/verify.js";
|
|
3
|
+
const log = createLogger("discord.channel");
|
|
4
|
+
/**
|
|
5
|
+
* Verifies an inbound Discord request and returns its raw body, or
|
|
6
|
+
* `null` when verification fails.
|
|
7
|
+
*/
|
|
8
|
+
export async function verifyDiscordInbound(req, credentials) {
|
|
9
|
+
try {
|
|
10
|
+
return await verifyDiscordRequest(req, {
|
|
11
|
+
publicKey: credentials?.webhookVerifier ? undefined : credentials?.publicKey,
|
|
12
|
+
webhookVerifier: credentials?.webhookVerifier,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
log.warn("discord inbound verification failed", { error });
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default route that `slackChannel({})` mounts on. The scaffold's slack
|
|
3
|
+
* setup (`vercel connect attach --trigger-path ...`) must point at this
|
|
4
|
+
* exact path or Connect-forwarded Slack events 404 against the Ash
|
|
5
|
+
* framework router.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SLACK_CHANNEL_DEFAULT_ROUTE = "/ash/v1/slack";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default route that `slackChannel({})` mounts on. The scaffold's slack
|
|
3
|
+
* setup (`vercel connect attach --trigger-path ...`) must point at this
|
|
4
|
+
* exact path or Connect-forwarded Slack events 404 against the Ash
|
|
5
|
+
* framework router.
|
|
6
|
+
*/
|
|
7
|
+
export const SLACK_CHANNEL_DEFAULT_ROUTE = "/ash/v1/slack";
|
|
@@ -3,6 +3,7 @@ import { buildSlackBinding, slackContinuationToken, } from "#public/channels/sla
|
|
|
3
3
|
import { buildSlackTurnMessage, collectInboundFileParts, createSlackFetchFile, } from "#public/channels/slack/attachments.js";
|
|
4
4
|
import { defaultEvents, defaultInputRequestedHandler, defaultOnAppMention, defaultOnDirectMessage, } from "#public/channels/slack/defaults.js";
|
|
5
5
|
import { parseAppMentionEvent, parseDirectMessageEvent, prependSlackContext, } from "#public/channels/slack/inbound.js";
|
|
6
|
+
import { SLACK_CHANNEL_DEFAULT_ROUTE } from "#public/channels/slack/constants.js";
|
|
6
7
|
import { handleInteractionPost } from "#public/channels/slack/interactions.js";
|
|
7
8
|
import { mergeUploadPolicy, } from "#public/channels/upload-policy.js";
|
|
8
9
|
import { verifySlackRequest } from "#public/channels/slack/verify.js";
|
|
@@ -58,7 +59,7 @@ export function slackChannel(config = {}) {
|
|
|
58
59
|
return rebuildSlackContext(state, session, config.credentials);
|
|
59
60
|
},
|
|
60
61
|
routes: [
|
|
61
|
-
POST(config.route ??
|
|
62
|
+
POST(config.route ?? SLACK_CHANNEL_DEFAULT_ROUTE, async (req, { send, waitUntil }) => {
|
|
62
63
|
const body = await verifyInbound(req, config.credentials);
|
|
63
64
|
if (body === null)
|
|
64
65
|
return new Response("unauthorized", { status: 401 });
|
|
@@ -6,6 +6,8 @@ export function getRuntimeActionRequestKey(action) {
|
|
|
6
6
|
switch (action.kind) {
|
|
7
7
|
case "load-skill":
|
|
8
8
|
return `runtime-action:${action.kind}:${action.callId}`;
|
|
9
|
+
case "remote-agent-call":
|
|
10
|
+
return `subagent-call:${action.remoteAgentName}:${action.callId}`;
|
|
9
11
|
case "subagent-call":
|
|
10
12
|
return `subagent-call:${action.subagentName}:${action.callId}`;
|
|
11
13
|
case "tool-call":
|
|
@@ -30,6 +30,23 @@ declare const runtimeSubagentCallActionRequestSchema: z.ZodObject<{
|
|
|
30
30
|
nodeId: z.ZodString;
|
|
31
31
|
subagentName: z.ZodString;
|
|
32
32
|
}, z.core.$strict>;
|
|
33
|
+
/**
|
|
34
|
+
* Runtime-owned remote-agent-call request surfaced by a harness and executed
|
|
35
|
+
* later by workflow-backed runtime code.
|
|
36
|
+
*/
|
|
37
|
+
export type RuntimeRemoteAgentCallActionRequest = z.infer<typeof runtimeRemoteAgentCallActionRequestSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* Zod schema for one runtime-owned remote-agent-call action request.
|
|
40
|
+
*/
|
|
41
|
+
export declare const runtimeRemoteAgentCallActionRequestSchema: z.ZodObject<{
|
|
42
|
+
callId: z.ZodString;
|
|
43
|
+
description: z.ZodString;
|
|
44
|
+
input: z.ZodType<import("../../shared/json.js").JsonObject, unknown, z.core.$ZodTypeInternals<import("../../shared/json.js").JsonObject, unknown>>;
|
|
45
|
+
kind: z.ZodLiteral<"remote-agent-call">;
|
|
46
|
+
name: z.ZodString;
|
|
47
|
+
nodeId: z.ZodString;
|
|
48
|
+
remoteAgentName: z.ZodString;
|
|
49
|
+
}, z.core.$strict>;
|
|
33
50
|
/**
|
|
34
51
|
* Runtime-owned action request surfaced by a harness.
|
|
35
52
|
*
|
|
@@ -51,7 +68,36 @@ declare const runtimeLoadSkillActionRequestSchema: z.ZodObject<{
|
|
|
51
68
|
* Harness-native capabilities such as `bash` do not cross the harness boundary
|
|
52
69
|
* as runtime actions. Only runtime-executed requests use this taxonomy.
|
|
53
70
|
*/
|
|
54
|
-
export type RuntimeActionRequest = RuntimeLoadSkillActionRequest | RuntimeSubagentCallActionRequest | RuntimeToolCallActionRequest;
|
|
71
|
+
export type RuntimeActionRequest = RuntimeLoadSkillActionRequest | RuntimeRemoteAgentCallActionRequest | RuntimeSubagentCallActionRequest | RuntimeToolCallActionRequest;
|
|
72
|
+
/**
|
|
73
|
+
* Zod schema for one runtime action request.
|
|
74
|
+
*/
|
|
75
|
+
export declare const runtimeActionRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
76
|
+
callId: z.ZodString;
|
|
77
|
+
input: z.ZodType<import("../../shared/json.js").JsonObject, unknown, z.core.$ZodTypeInternals<import("../../shared/json.js").JsonObject, unknown>>;
|
|
78
|
+
kind: z.ZodLiteral<"load-skill">;
|
|
79
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
80
|
+
callId: z.ZodString;
|
|
81
|
+
description: z.ZodString;
|
|
82
|
+
input: z.ZodType<import("../../shared/json.js").JsonObject, unknown, z.core.$ZodTypeInternals<import("../../shared/json.js").JsonObject, unknown>>;
|
|
83
|
+
kind: z.ZodLiteral<"remote-agent-call">;
|
|
84
|
+
name: z.ZodString;
|
|
85
|
+
nodeId: z.ZodString;
|
|
86
|
+
remoteAgentName: z.ZodString;
|
|
87
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
88
|
+
callId: z.ZodString;
|
|
89
|
+
description: z.ZodString;
|
|
90
|
+
input: z.ZodType<import("../../shared/json.js").JsonObject, unknown, z.core.$ZodTypeInternals<import("../../shared/json.js").JsonObject, unknown>>;
|
|
91
|
+
kind: z.ZodLiteral<"subagent-call">;
|
|
92
|
+
name: z.ZodString;
|
|
93
|
+
nodeId: z.ZodString;
|
|
94
|
+
subagentName: z.ZodString;
|
|
95
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
96
|
+
callId: z.ZodString;
|
|
97
|
+
input: z.ZodType<import("../../shared/json.js").JsonObject, unknown, z.core.$ZodTypeInternals<import("../../shared/json.js").JsonObject, unknown>>;
|
|
98
|
+
kind: z.ZodLiteral<"tool-call">;
|
|
99
|
+
toolName: z.ZodString;
|
|
100
|
+
}, z.core.$strict>], "kind">;
|
|
55
101
|
/**
|
|
56
102
|
* Runtime-owned authored tool-result projected back into a harness resume call.
|
|
57
103
|
*/
|
|
@@ -25,6 +25,20 @@ const runtimeSubagentCallActionRequestSchema = z
|
|
|
25
25
|
subagentName: z.string(),
|
|
26
26
|
})
|
|
27
27
|
.strict();
|
|
28
|
+
/**
|
|
29
|
+
* Zod schema for one runtime-owned remote-agent-call action request.
|
|
30
|
+
*/
|
|
31
|
+
export const runtimeRemoteAgentCallActionRequestSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
callId: z.string(),
|
|
34
|
+
description: z.string(),
|
|
35
|
+
input: jsonObjectSchema,
|
|
36
|
+
kind: z.literal("remote-agent-call"),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
nodeId: z.string(),
|
|
39
|
+
remoteAgentName: z.string(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
28
42
|
/**
|
|
29
43
|
* Zod schema for one runtime-owned load-skill action request.
|
|
30
44
|
*/
|
|
@@ -35,6 +49,15 @@ const runtimeLoadSkillActionRequestSchema = z
|
|
|
35
49
|
kind: z.literal("load-skill"),
|
|
36
50
|
})
|
|
37
51
|
.strict();
|
|
52
|
+
/**
|
|
53
|
+
* Zod schema for one runtime action request.
|
|
54
|
+
*/
|
|
55
|
+
export const runtimeActionRequestSchema = z.discriminatedUnion("kind", [
|
|
56
|
+
runtimeLoadSkillActionRequestSchema,
|
|
57
|
+
runtimeRemoteAgentCallActionRequestSchema,
|
|
58
|
+
runtimeSubagentCallActionRequestSchema,
|
|
59
|
+
runtimeToolCallActionRequestSchema,
|
|
60
|
+
]);
|
|
38
61
|
/**
|
|
39
62
|
* Zod schema for one runtime-owned authored tool-result action result.
|
|
40
63
|
*/
|
|
@@ -29,7 +29,7 @@ import type { ResolvedChannelDefinition } from "#runtime/types.js";
|
|
|
29
29
|
* channel. The trailing method segment (`get` or `post`) keeps each
|
|
30
30
|
* `(method, urlPath)` pair distinct in the channel registry.
|
|
31
31
|
*/
|
|
32
|
-
export declare const HTTP_CONNECTION_CALLBACK_CHANNEL_NAME_PREFIX = "
|
|
32
|
+
export declare const HTTP_CONNECTION_CALLBACK_CHANNEL_NAME_PREFIX = "ash/v1/connections/callback";
|
|
33
33
|
/**
|
|
34
34
|
* Returns the framework-shipped channel definitions that mount the
|
|
35
35
|
* connection callback route at {@link ASH_CONNECTION_CALLBACK_ROUTE_PATTERN}.
|
|
@@ -30,7 +30,7 @@ import { buildAuthorizationCompletePage } from "#runtime/connections/authorizati
|
|
|
30
30
|
* channel. The trailing method segment (`get` or `post`) keeps each
|
|
31
31
|
* `(method, urlPath)` pair distinct in the channel registry.
|
|
32
32
|
*/
|
|
33
|
-
export const HTTP_CONNECTION_CALLBACK_CHANNEL_NAME_PREFIX = "
|
|
33
|
+
export const HTTP_CONNECTION_CALLBACK_CHANNEL_NAME_PREFIX = "ash/v1/connections/callback";
|
|
34
34
|
/**
|
|
35
35
|
* HTTP methods accepted by the connection callback route.
|
|
36
36
|
*
|
|
@@ -13,8 +13,7 @@ export declare class McpConnectionClient implements ConnectionClient {
|
|
|
13
13
|
constructor(connection: ResolvedConnectionDefinition);
|
|
14
14
|
/**
|
|
15
15
|
* Connects to the MCP server, trying Streamable HTTP first and
|
|
16
|
-
* falling back to SSE
|
|
17
|
-
* only supports the older SSE transport).
|
|
16
|
+
* falling back to SSE for transport-compatibility failures.
|
|
18
17
|
*
|
|
19
18
|
* Concurrent callers share the same connection promise to avoid
|
|
20
19
|
* duplicate connections.
|
|
@@ -2,6 +2,7 @@ import { createMCPClient } from "#compiled/@ai-sdk/mcp/index.js";
|
|
|
2
2
|
import { contextStorage } from "#context/container.js";
|
|
3
3
|
import { readCachedToken, writeCachedToken } from "#runtime/connections/authorization-tokens.js";
|
|
4
4
|
import { principalKey, resolveConnectionPrincipal } from "#runtime/connections/principal.js";
|
|
5
|
+
import { isObject } from "#shared/guards.js";
|
|
5
6
|
/**
|
|
6
7
|
* Wraps one `MCPClient` from `@ai-sdk/mcp` for a single connection.
|
|
7
8
|
*
|
|
@@ -19,8 +20,7 @@ export class McpConnectionClient {
|
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Connects to the MCP server, trying Streamable HTTP first and
|
|
22
|
-
* falling back to SSE
|
|
23
|
-
* only supports the older SSE transport).
|
|
23
|
+
* falling back to SSE for transport-compatibility failures.
|
|
24
24
|
*
|
|
25
25
|
* Concurrent callers share the same connection promise to avoid
|
|
26
26
|
* duplicate connections.
|
|
@@ -50,7 +50,10 @@ export class McpConnectionClient {
|
|
|
50
50
|
transport: { type: "http", url, headers },
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
catch {
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!isMcpHttpFallbackRetryableError(error)) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
54
57
|
return await createMCPClient({
|
|
55
58
|
transport: { type: "sse", url, headers },
|
|
56
59
|
});
|
|
@@ -137,6 +140,69 @@ export class McpConnectionClient {
|
|
|
137
140
|
this.#tools = undefined;
|
|
138
141
|
}
|
|
139
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Decides whether an error thrown while creating a streamable HTTP MCP
|
|
145
|
+
* client should trigger a fallback attempt against the legacy SSE
|
|
146
|
+
* transport.
|
|
147
|
+
*
|
|
148
|
+
* Per the MCP backwards-compatibility rules, clients should fall back
|
|
149
|
+
* to SSE when the streamable HTTP probe returns `400 Bad Request`,
|
|
150
|
+
* `404 Not Found`, or `405 Method Not Allowed`. All other failures
|
|
151
|
+
* (auth errors, network errors, server errors) should propagate so the
|
|
152
|
+
* caller sees the real problem instead of a misleading SSE failure.
|
|
153
|
+
*
|
|
154
|
+
* See: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#backwards-compatibility
|
|
155
|
+
*/
|
|
156
|
+
function isMcpHttpFallbackRetryableError(error) {
|
|
157
|
+
const status = readHttpStatus(error);
|
|
158
|
+
return status === 400 || status === 404 || status === 405;
|
|
159
|
+
}
|
|
160
|
+
function readHttpStatus(error) {
|
|
161
|
+
for (const candidate of walkErrorChain(error)) {
|
|
162
|
+
if (!isObject(candidate)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const status = readStatusField(candidate);
|
|
166
|
+
if (status !== undefined) {
|
|
167
|
+
return status;
|
|
168
|
+
}
|
|
169
|
+
const response = candidate.response;
|
|
170
|
+
if (isObject(response)) {
|
|
171
|
+
const responseStatus = readStatusField(response);
|
|
172
|
+
if (responseStatus !== undefined) {
|
|
173
|
+
return responseStatus;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (typeof candidate.message === "string") {
|
|
177
|
+
const match = /\bHTTP\s+(\d{3})\b/u.exec(candidate.message);
|
|
178
|
+
if (match?.[1] !== undefined) {
|
|
179
|
+
return Number(match[1]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
function readStatusField(value) {
|
|
186
|
+
if (typeof value.status === "number") {
|
|
187
|
+
return value.status;
|
|
188
|
+
}
|
|
189
|
+
if (typeof value.statusCode === "number") {
|
|
190
|
+
return value.statusCode;
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
function* walkErrorChain(error) {
|
|
195
|
+
let current = error;
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
while (current !== undefined && current !== null && !seen.has(current)) {
|
|
198
|
+
seen.add(current);
|
|
199
|
+
yield current;
|
|
200
|
+
if (!isObject(current) || !("cause" in current)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
current = current.cause;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
140
206
|
/**
|
|
141
207
|
* Returns `true` when a tool name passes the configured filter.
|
|
142
208
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { none, vercelOidc } from "#public/channels/auth.js";
|
|
2
2
|
import { ashChannel } from "#public/channels/ash.js";
|
|
3
3
|
import { getConnectionCallbackChannelDefinitions, getConnectionCallbackChannelNames, } from "#runtime/connections/callback-route.js";
|
|
4
|
+
import { getSessionCallbackChannelDefinitions, getSessionCallbackChannelNames, } from "#runtime/session-callback-route.js";
|
|
4
5
|
const ASH_CHANNEL_NAME = "ash";
|
|
5
6
|
export function getFrameworkChannelDefinitions() {
|
|
6
7
|
const auth = resolveFrameworkAshAuth();
|
|
@@ -19,11 +20,15 @@ export function getFrameworkChannelDefinitions() {
|
|
|
19
20
|
sourceKind: "module",
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
|
-
result.push(...getConnectionCallbackChannelDefinitions());
|
|
23
|
+
result.push(...getConnectionCallbackChannelDefinitions(), ...getSessionCallbackChannelDefinitions());
|
|
23
24
|
return result;
|
|
24
25
|
}
|
|
25
26
|
export function getAllFrameworkChannelNames() {
|
|
26
|
-
return new Set([
|
|
27
|
+
return new Set([
|
|
28
|
+
ASH_CHANNEL_NAME,
|
|
29
|
+
...getConnectionCallbackChannelNames(),
|
|
30
|
+
...getSessionCallbackChannelNames(),
|
|
31
|
+
]);
|
|
27
32
|
}
|
|
28
33
|
function resolveFrameworkAshAuth() {
|
|
29
34
|
if (process.env.VERCEL) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RouteContext } from "#public/definitions/channel.js";
|
|
2
|
+
import type { ResolvedChannelDefinition } from "#runtime/types.js";
|
|
3
|
+
export declare const HTTP_SESSION_CALLBACK_CHANNEL_NAME_PREFIX = "ash/v1/callback";
|
|
4
|
+
export declare function getSessionCallbackChannelDefinitions(): readonly ResolvedChannelDefinition[];
|
|
5
|
+
export declare function getSessionCallbackChannelNames(): ReadonlySet<string>;
|
|
6
|
+
export declare function handleSessionCallbackRequest(request: Request, ctx: RouteContext): Promise<Response>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { resumeHook } from "#compiled/@workflow/core/runtime.js";
|
|
2
|
+
import { ASH_CALLBACK_ROUTE_PATTERN } from "#protocol/routes.js";
|
|
3
|
+
export const HTTP_SESSION_CALLBACK_CHANNEL_NAME_PREFIX = "ash/v1/callback";
|
|
4
|
+
const HANDLED_METHODS = ["POST"];
|
|
5
|
+
export function getSessionCallbackChannelDefinitions() {
|
|
6
|
+
return HANDLED_METHODS.map((method) => buildCallbackChannelDefinition(method));
|
|
7
|
+
}
|
|
8
|
+
export function getSessionCallbackChannelNames() {
|
|
9
|
+
return new Set(HANDLED_METHODS.map(channelNameForMethod));
|
|
10
|
+
}
|
|
11
|
+
function buildCallbackChannelDefinition(method) {
|
|
12
|
+
const name = channelNameForMethod(method);
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
method,
|
|
16
|
+
urlPath: ASH_CALLBACK_ROUTE_PATTERN,
|
|
17
|
+
fetch: handleSessionCallbackRequest,
|
|
18
|
+
logicalPath: `framework://channels/${name}`,
|
|
19
|
+
sourceId: `ash:framework:session-callback-${method.toLowerCase()}`,
|
|
20
|
+
sourceKind: "module",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function channelNameForMethod(method) {
|
|
24
|
+
return `${HTTP_SESSION_CALLBACK_CHANNEL_NAME_PREFIX}/${method.toLowerCase()}`;
|
|
25
|
+
}
|
|
26
|
+
export async function handleSessionCallbackRequest(request, ctx) {
|
|
27
|
+
const token = ctx.params.token;
|
|
28
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
29
|
+
return Response.json({ error: "Missing callback token.", ok: false }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
let body;
|
|
32
|
+
try {
|
|
33
|
+
body = await request.json();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return Response.json({ error: "Invalid JSON body.", ok: false }, { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
const result = projectSessionCallbackResult(body);
|
|
39
|
+
if (result instanceof Response) {
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await resumeHook(token, {
|
|
44
|
+
kind: "runtime-action-result",
|
|
45
|
+
results: [result],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return Response.json({ error: "Session callback not pending.", ok: false }, { status: 404 });
|
|
50
|
+
}
|
|
51
|
+
return Response.json({ ok: true }, { status: 202 });
|
|
52
|
+
}
|
|
53
|
+
function projectSessionCallbackResult(value) {
|
|
54
|
+
if (value === null || typeof value !== "object") {
|
|
55
|
+
return Response.json({ error: "Expected a JSON object.", ok: false }, { status: 400 });
|
|
56
|
+
}
|
|
57
|
+
const payload = value;
|
|
58
|
+
if (typeof payload.callId !== "string" || payload.callId.length === 0) {
|
|
59
|
+
return Response.json({ error: "Missing callback callId.", ok: false }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
if (typeof payload.subagentName !== "string" || payload.subagentName.length === 0) {
|
|
62
|
+
return Response.json({ error: "Missing callback subagentName.", ok: false }, { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
if (payload.kind === "session.completed") {
|
|
65
|
+
return {
|
|
66
|
+
callId: payload.callId,
|
|
67
|
+
kind: "subagent-result",
|
|
68
|
+
output: typeof payload.output === "string" ? payload.output : "",
|
|
69
|
+
subagentName: payload.subagentName,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (payload.kind === "session.failed") {
|
|
73
|
+
return {
|
|
74
|
+
callId: payload.callId,
|
|
75
|
+
isError: true,
|
|
76
|
+
kind: "subagent-result",
|
|
77
|
+
output: payload.error === undefined
|
|
78
|
+
? {
|
|
79
|
+
code: "REMOTE_AGENT_FAILED",
|
|
80
|
+
message: "Remote agent failed.",
|
|
81
|
+
}
|
|
82
|
+
: payload.error,
|
|
83
|
+
subagentName: payload.subagentName,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return Response.json({ error: "Unsupported callback kind.", ok: false }, { status: 400 });
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "experimental-ash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"bin": {
|
|
5
5
|
"ash": "./bin/ash.js",
|
|
6
6
|
"experimental-ash": "./bin/ash.js"
|
|
@@ -146,6 +146,11 @@
|
|
|
146
146
|
"import": "./dist/src/public/channels/slack/index.js",
|
|
147
147
|
"default": "./dist/src/public/channels/slack/index.js"
|
|
148
148
|
},
|
|
149
|
+
"./channels/discord": {
|
|
150
|
+
"types": "./dist/src/public/channels/discord/index.d.ts",
|
|
151
|
+
"import": "./dist/src/public/channels/discord/index.js",
|
|
152
|
+
"default": "./dist/src/public/channels/discord/index.js"
|
|
153
|
+
},
|
|
149
154
|
"./channels/twilio": {
|
|
150
155
|
"types": "./dist/src/public/channels/twilio/index.d.ts",
|
|
151
156
|
"import": "./dist/src/public/channels/twilio/index.js",
|
|
@@ -192,7 +197,8 @@
|
|
|
192
197
|
"react-test-renderer": "19.2.6",
|
|
193
198
|
"turndown": "7.2.4",
|
|
194
199
|
"zod": "4.4.3",
|
|
195
|
-
"zod-validation-error": "5.0.0"
|
|
200
|
+
"zod-validation-error": "5.0.0",
|
|
201
|
+
"@vercel/ash-scaffold": "0.0.0"
|
|
196
202
|
},
|
|
197
203
|
"peerDependencies": {
|
|
198
204
|
"@opentelemetry/api": "^1.0.0",
|
|
@@ -220,7 +226,7 @@
|
|
|
220
226
|
},
|
|
221
227
|
"scripts": {
|
|
222
228
|
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
|
223
|
-
"build": "pnpm run build:js && node ./scripts/copy-
|
|
229
|
+
"build": "pnpm run build:js && node ./scripts/copy-docs.mjs && node ./scripts/stamp-version-tokens.mjs",
|
|
224
230
|
"build:compiled": "node ./scripts/vendor-compiled.mjs",
|
|
225
231
|
"build:js": "pnpm run build:compiled && pnpm run clean && tsgo -p tsconfig.build.json && node ./scripts/copy-compiled-assets.mjs && node ./scripts/bundle-js.mjs",
|
|
226
232
|
"build:types": "pnpm run build:compiled && tsgo -p tsconfig.build.json --emitDeclarationOnly",
|