@gajae-code/coding-agent 0.11.6 → 0.11.8
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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type AddressResolver, guardedPublicFetch } from "../web/insane/url-guard";
|
|
2
|
+
import { cancelMCPStream } from "./content-limits";
|
|
3
|
+
import type { MCPHttpServerConfig, MCPSseServerConfig } from "./types";
|
|
4
|
+
|
|
5
|
+
const PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY: unique symbol = Symbol("plugin-mcp-public-network-boundary");
|
|
6
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
7
|
+
const MAX_REDIRECTS = 20;
|
|
8
|
+
const REQUEST_BODY_HEADERS = [
|
|
9
|
+
"content-encoding",
|
|
10
|
+
"content-language",
|
|
11
|
+
"content-length",
|
|
12
|
+
"content-location",
|
|
13
|
+
"content-type",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/** Mark an in-memory plugin-bundle remote MCP config for connection-bound public-network enforcement. */
|
|
17
|
+
export function bindPluginMcpToPublicNetwork<T extends MCPHttpServerConfig | MCPSseServerConfig>(config: T): T {
|
|
18
|
+
return Object.assign(config, { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The marker is an internal symbol, so persisted user MCP configs cannot opt into or forge this path. */
|
|
22
|
+
export function isPluginMcpPublicNetworkBound(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
|
|
23
|
+
return (
|
|
24
|
+
(config as MCPHttpServerConfig & { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]?: boolean })[
|
|
25
|
+
PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY
|
|
26
|
+
] === true
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function blocked(reason: string): Error {
|
|
31
|
+
return new Error(`Plugin MCP network request blocked: ${reason}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseHttpsUrl(rawUrl: string): URL {
|
|
35
|
+
let url: URL;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(rawUrl);
|
|
38
|
+
} catch {
|
|
39
|
+
throw blocked("invalid URL");
|
|
40
|
+
}
|
|
41
|
+
if (url.protocol !== "https:") throw blocked("HTTPS is required");
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rewritesToGet(status: number, method: string): boolean {
|
|
46
|
+
return (
|
|
47
|
+
((status === 301 || status === 302) && method === "POST") ||
|
|
48
|
+
(status === 303 && method !== "GET" && method !== "HEAD")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch a plugin-bundle MCP request through a DNS-pinned public address. Every
|
|
54
|
+
* redirect is handled manually and revalidated before the next connection.
|
|
55
|
+
*/
|
|
56
|
+
export async function fetchPluginMcpRequest(
|
|
57
|
+
rawUrl: string,
|
|
58
|
+
init: BunFetchRequestInit,
|
|
59
|
+
options: { resolver?: AddressResolver; maxRedirects?: number } = {},
|
|
60
|
+
): Promise<Response> {
|
|
61
|
+
let currentUrl = parseHttpsUrl(rawUrl);
|
|
62
|
+
let currentInit: BunFetchRequestInit = { ...init };
|
|
63
|
+
const maxRedirects = options.maxRedirects ?? MAX_REDIRECTS;
|
|
64
|
+
|
|
65
|
+
for (let redirectCount = 0; ; redirectCount++) {
|
|
66
|
+
const dial = await guardedPublicFetch(currentUrl.toString(), currentInit, { resolver: options.resolver });
|
|
67
|
+
if (!dial.ok) throw blocked(dial.reason);
|
|
68
|
+
const { response, logicalUrl } = dial;
|
|
69
|
+
if (!REDIRECT_STATUSES.has(response.status)) return response;
|
|
70
|
+
|
|
71
|
+
const location = response.headers.get("location");
|
|
72
|
+
if (!location) return response;
|
|
73
|
+
if (redirectCount >= maxRedirects) {
|
|
74
|
+
cancelMCPStream(response.body);
|
|
75
|
+
throw blocked("redirect limit exceeded");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let nextUrl: URL;
|
|
79
|
+
try {
|
|
80
|
+
nextUrl = parseHttpsUrl(new URL(location, logicalUrl).toString());
|
|
81
|
+
} catch (error) {
|
|
82
|
+
cancelMCPStream(response.body);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (logicalUrl.origin !== nextUrl.origin) {
|
|
86
|
+
cancelMCPStream(response.body);
|
|
87
|
+
throw blocked("cross-origin redirects are not allowed");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const headers = new Headers(currentInit.headers);
|
|
91
|
+
const method = (currentInit.method ?? "GET").toUpperCase();
|
|
92
|
+
if (rewritesToGet(response.status, method)) {
|
|
93
|
+
for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
|
|
94
|
+
currentInit = { ...currentInit, method: "GET", headers, body: undefined };
|
|
95
|
+
} else {
|
|
96
|
+
currentInit = { ...currentInit, headers };
|
|
97
|
+
}
|
|
98
|
+
currentUrl = nextUrl;
|
|
99
|
+
cancelMCPStream(response.body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -16,6 +16,15 @@ import type {
|
|
|
16
16
|
MCPTransport,
|
|
17
17
|
} from "../../runtime-mcp/types";
|
|
18
18
|
import { MCPExpectedFailure, toJsonRpcError } from "../../runtime-mcp/types";
|
|
19
|
+
import {
|
|
20
|
+
cancelMCPStream,
|
|
21
|
+
MCP_MAX_CONTENT_BYTES,
|
|
22
|
+
MCP_MAX_ERROR_BYTES,
|
|
23
|
+
MCP_MAX_SSE_BATCH_MESSAGES,
|
|
24
|
+
MCP_MAX_SSE_REQUEST_MESSAGES,
|
|
25
|
+
readMCPResponseText,
|
|
26
|
+
} from "../content-limits";
|
|
27
|
+
import { fetchPluginMcpRequest, isPluginMcpPublicNetworkBound } from "../plugin-network-boundary";
|
|
19
28
|
|
|
20
29
|
/**
|
|
21
30
|
* HTTP transport for MCP servers.
|
|
@@ -54,6 +63,12 @@ export class HttpTransport implements MCPTransport {
|
|
|
54
63
|
this.#connected = true;
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
#fetch(init: BunFetchRequestInit): Promise<Response> {
|
|
67
|
+
return isPluginMcpPublicNetworkBound(this.config)
|
|
68
|
+
? fetchPluginMcpRequest(this.config.url, init)
|
|
69
|
+
: fetch(this.config.url, init);
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
#trackReader(promise: Promise<void>, controller?: AbortController): void {
|
|
58
73
|
if (controller) this.#streamControllers.add(controller);
|
|
59
74
|
this.#streamReaders.add(promise);
|
|
@@ -73,6 +88,9 @@ export class HttpTransport implements MCPTransport {
|
|
|
73
88
|
if (this.#sseConnection) return;
|
|
74
89
|
|
|
75
90
|
const sseConnection = new AbortController();
|
|
91
|
+
const headerController = new AbortController();
|
|
92
|
+
const headerTimeout = this.config.timeout ?? 30000;
|
|
93
|
+
const headerTimeoutId = setTimeout(() => headerController.abort(), headerTimeout);
|
|
76
94
|
this.#sseConnection = sseConnection;
|
|
77
95
|
const headers: Record<string, string> = {
|
|
78
96
|
Accept: "text/event-stream",
|
|
@@ -85,21 +103,25 @@ export class HttpTransport implements MCPTransport {
|
|
|
85
103
|
|
|
86
104
|
let response: Response;
|
|
87
105
|
try {
|
|
88
|
-
response = await fetch(
|
|
106
|
+
response = await this.#fetch({
|
|
89
107
|
method: "GET",
|
|
90
108
|
headers,
|
|
91
|
-
signal: sseConnection.signal,
|
|
109
|
+
signal: AbortSignal.any([sseConnection.signal, headerController.signal]),
|
|
92
110
|
});
|
|
93
111
|
} catch (error) {
|
|
94
112
|
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
95
|
-
if (
|
|
113
|
+
if (headerController.signal.aborted && !sseConnection.signal.aborted) {
|
|
114
|
+
this.onError?.(new Error(`SSE connection timeout after ${headerTimeout}ms`));
|
|
115
|
+
} else if (error instanceof Error && error.name !== "AbortError") {
|
|
96
116
|
this.onError?.(error);
|
|
97
117
|
}
|
|
98
118
|
return;
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(headerTimeoutId);
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
102
|
-
|
|
124
|
+
cancelMCPStream(response.body);
|
|
103
125
|
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
104
126
|
return;
|
|
105
127
|
}
|
|
@@ -117,8 +139,13 @@ export class HttpTransport implements MCPTransport {
|
|
|
117
139
|
}
|
|
118
140
|
async #readSSEStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
|
|
119
141
|
try {
|
|
120
|
-
for await (const message of readSseJson<JsonRpcMessage>(body, signal
|
|
142
|
+
for await (const message of readSseJson<JsonRpcMessage>(body, signal, undefined, {
|
|
143
|
+
maxEventBytes: MCP_MAX_CONTENT_BYTES,
|
|
144
|
+
})) {
|
|
121
145
|
if (!this.#connected) break;
|
|
146
|
+
if (Array.isArray(message) && message.length > MCP_MAX_SSE_BATCH_MESSAGES) {
|
|
147
|
+
throw new Error("MCP SSE batch exceeds message limit");
|
|
148
|
+
}
|
|
122
149
|
this.#dispatchSSEMessage(message);
|
|
123
150
|
}
|
|
124
151
|
} catch (error) {
|
|
@@ -207,7 +234,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
207
234
|
: abortController.signal;
|
|
208
235
|
|
|
209
236
|
try {
|
|
210
|
-
const response = await fetch(
|
|
237
|
+
const response = await this.#fetch({
|
|
211
238
|
method: "POST",
|
|
212
239
|
headers,
|
|
213
240
|
body: JSON.stringify(body),
|
|
@@ -221,7 +248,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
if (!response.ok) {
|
|
224
|
-
const text = await response
|
|
251
|
+
const text = await readMCPResponseText(response, MCP_MAX_ERROR_BYTES, true, operationSignal);
|
|
225
252
|
const wwwAuthenticate = response.headers.get("WWW-Authenticate");
|
|
226
253
|
const mcpAuthServer = response.headers.get("Mcp-Auth-Server");
|
|
227
254
|
const authHints = [
|
|
@@ -245,7 +272,9 @@ export class HttpTransport implements MCPTransport {
|
|
|
245
272
|
if (!response.body) {
|
|
246
273
|
throw new MCPExpectedFailure();
|
|
247
274
|
}
|
|
248
|
-
const parsedResult =
|
|
275
|
+
const parsedResult = JSON.parse(
|
|
276
|
+
await readMCPResponseText(response, MCP_MAX_CONTENT_BYTES, false, operationSignal),
|
|
277
|
+
) as unknown;
|
|
249
278
|
|
|
250
279
|
if (
|
|
251
280
|
typeof parsedResult !== "object" ||
|
|
@@ -293,6 +322,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
293
322
|
|
|
294
323
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
295
324
|
let captured = false;
|
|
325
|
+
let messageCount = 0;
|
|
296
326
|
|
|
297
327
|
// Drain this per-request SSE response from a single iterator. Once the
|
|
298
328
|
// matching response arrives, resolve/reject and abort the reader so the
|
|
@@ -304,8 +334,20 @@ export class HttpTransport implements MCPTransport {
|
|
|
304
334
|
this.#streamControllers.add(drainController);
|
|
305
335
|
const drain = async (): Promise<void> => {
|
|
306
336
|
try {
|
|
307
|
-
for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(
|
|
337
|
+
for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(
|
|
338
|
+
response.body!,
|
|
339
|
+
operationSignal,
|
|
340
|
+
undefined,
|
|
341
|
+
{
|
|
342
|
+
maxEventBytes: MCP_MAX_CONTENT_BYTES,
|
|
343
|
+
maxTotalBytes: MCP_MAX_CONTENT_BYTES,
|
|
344
|
+
},
|
|
345
|
+
)) {
|
|
308
346
|
const messages = Array.isArray(raw) ? raw : [raw];
|
|
347
|
+
if (messages.length > MCP_MAX_SSE_BATCH_MESSAGES) throw new Error("MCP SSE batch exceeds message limit");
|
|
348
|
+
messageCount += messages.length;
|
|
349
|
+
if (messageCount > MCP_MAX_SSE_REQUEST_MESSAGES)
|
|
350
|
+
throw new Error("MCP SSE response exceeds message limit");
|
|
309
351
|
for (const message of messages) {
|
|
310
352
|
if (
|
|
311
353
|
!captured &&
|
|
@@ -352,7 +394,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
352
394
|
} finally {
|
|
353
395
|
clearTimeout(timeoutId);
|
|
354
396
|
this.#streamControllers.delete(drainController);
|
|
355
|
-
|
|
397
|
+
cancelMCPStream(response.body);
|
|
356
398
|
}
|
|
357
399
|
};
|
|
358
400
|
|
|
@@ -388,7 +430,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
388
430
|
headers["Mcp-Session-Id"] = this.#sessionId;
|
|
389
431
|
}
|
|
390
432
|
try {
|
|
391
|
-
const resp = await fetch(
|
|
433
|
+
const resp = await this.#fetch({
|
|
392
434
|
method: "POST",
|
|
393
435
|
headers,
|
|
394
436
|
body: JSON.stringify(body),
|
|
@@ -396,23 +438,23 @@ export class HttpTransport implements MCPTransport {
|
|
|
396
438
|
});
|
|
397
439
|
// Retry once on auth failure if onAuthError is wired
|
|
398
440
|
if (this.onAuthError && (resp.status === 401 || resp.status === 403)) {
|
|
399
|
-
|
|
441
|
+
cancelMCPStream(resp.body);
|
|
400
442
|
const newHeaders = await this.onAuthError();
|
|
401
443
|
if (newHeaders) {
|
|
402
444
|
this.config.headers ??= {};
|
|
403
445
|
Object.assign(this.config.headers, newHeaders);
|
|
404
446
|
Object.assign(headers, newHeaders);
|
|
405
|
-
const retry = await fetch(
|
|
447
|
+
const retry = await this.#fetch({
|
|
406
448
|
method: "POST",
|
|
407
449
|
headers,
|
|
408
450
|
body: JSON.stringify(body),
|
|
409
451
|
signal: AbortSignal.timeout(this.config.timeout ?? 30000),
|
|
410
452
|
});
|
|
411
|
-
|
|
453
|
+
cancelMCPStream(retry.body);
|
|
412
454
|
return;
|
|
413
455
|
}
|
|
414
456
|
}
|
|
415
|
-
|
|
457
|
+
cancelMCPStream(resp.body);
|
|
416
458
|
} catch {
|
|
417
459
|
// Best-effort response delivery — server may have disconnected
|
|
418
460
|
}
|
|
@@ -445,18 +487,16 @@ export class HttpTransport implements MCPTransport {
|
|
|
445
487
|
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
446
488
|
|
|
447
489
|
try {
|
|
448
|
-
const response = await fetch(
|
|
490
|
+
const response = await this.#fetch({
|
|
449
491
|
method: "POST",
|
|
450
492
|
headers,
|
|
451
493
|
body: JSON.stringify(body),
|
|
452
494
|
signal: abortController.signal,
|
|
453
495
|
});
|
|
454
496
|
|
|
455
|
-
clearTimeout(timeoutId);
|
|
456
|
-
|
|
457
497
|
// 202 Accepted is success for notifications
|
|
458
498
|
if (!response.ok && response.status !== 202) {
|
|
459
|
-
const text = await response.
|
|
499
|
+
const text = await readMCPResponseText(response, MCP_MAX_ERROR_BYTES, true, abortController.signal);
|
|
460
500
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
461
501
|
}
|
|
462
502
|
|
|
@@ -472,14 +512,15 @@ export class HttpTransport implements MCPTransport {
|
|
|
472
512
|
const reader = this.#readSSEStream(response.body, AbortSignal.any(signals));
|
|
473
513
|
this.#trackReader(reader, streamController);
|
|
474
514
|
} else {
|
|
475
|
-
|
|
515
|
+
cancelMCPStream(response.body);
|
|
476
516
|
}
|
|
477
517
|
} catch (error) {
|
|
478
|
-
clearTimeout(timeoutId);
|
|
479
518
|
if (error instanceof Error && error.name === "AbortError") {
|
|
480
519
|
throw new MCPExpectedFailure(new Error(`Notify timeout after ${timeout}ms`));
|
|
481
520
|
}
|
|
482
521
|
throw error instanceof MCPExpectedFailure ? error : new MCPExpectedFailure(error);
|
|
522
|
+
} finally {
|
|
523
|
+
clearTimeout(timeoutId);
|
|
483
524
|
}
|
|
484
525
|
}
|
|
485
526
|
|
|
@@ -508,7 +549,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
508
549
|
"Mcp-Session-Id": this.#sessionId,
|
|
509
550
|
};
|
|
510
551
|
|
|
511
|
-
await fetch(
|
|
552
|
+
await this.#fetch({
|
|
512
553
|
method: "DELETE",
|
|
513
554
|
headers,
|
|
514
555
|
signal: AbortSignal.timeout(timeout),
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { BigIntStats } from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
6
|
+
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
5
7
|
import {
|
|
6
8
|
type DirectoryMigrationPolicy,
|
|
7
9
|
listManagedSessionCandidates,
|
|
@@ -141,6 +143,7 @@ export type BrokerResponse =
|
|
|
141
143
|
error: {
|
|
142
144
|
code: BrokerErrorCode;
|
|
143
145
|
message: string;
|
|
146
|
+
details?: ModelProfileErrorDetails;
|
|
144
147
|
endpoint?: "unavailable";
|
|
145
148
|
cleanup?: BrokerCleanupEvidence;
|
|
146
149
|
};
|
|
@@ -362,6 +365,7 @@ type BrokerLockSnapshot = {
|
|
|
362
365
|
ownerId?: string;
|
|
363
366
|
pid: number;
|
|
364
367
|
identity: string;
|
|
368
|
+
lockIdentity: string;
|
|
365
369
|
};
|
|
366
370
|
|
|
367
371
|
const BROKER_PUBLICATION_CADENCE_MS = 5_000;
|
|
@@ -415,7 +419,7 @@ export class Broker {
|
|
|
415
419
|
#lockRecordPath(): string {
|
|
416
420
|
return path.join(this.#lock, BROKER_LOCK_RECORD);
|
|
417
421
|
}
|
|
418
|
-
#lockSnapshot(raw: string): BrokerLockSnapshot {
|
|
422
|
+
async #lockSnapshot(raw: string, lockIdentity: string): Promise<BrokerLockSnapshot> {
|
|
419
423
|
try {
|
|
420
424
|
const lock = JSON.parse(raw) as { ownerId?: unknown; pid?: unknown };
|
|
421
425
|
if (
|
|
@@ -425,32 +429,37 @@ export class Broker {
|
|
|
425
429
|
Number.isInteger(lock.pid) &&
|
|
426
430
|
lock.pid > 0
|
|
427
431
|
)
|
|
428
|
-
return { ownerId: lock.ownerId, pid: lock.pid, identity: `owner:${lock.ownerId}
|
|
432
|
+
return { ownerId: lock.ownerId, pid: lock.pid, identity: `owner:${lock.ownerId}`, lockIdentity };
|
|
429
433
|
} catch {}
|
|
430
|
-
return { pid: 0, identity: `contents:${createHash("sha256").update(raw).digest("hex")}
|
|
434
|
+
return { pid: 0, identity: `contents:${createHash("sha256").update(raw).digest("hex")}`, lockIdentity };
|
|
431
435
|
}
|
|
432
436
|
async #readLock(): Promise<BrokerLockSnapshot | null> {
|
|
437
|
+
let lock: BigIntStats;
|
|
433
438
|
try {
|
|
434
|
-
|
|
439
|
+
lock = await fs.lstat(this.#lock, { bigint: true });
|
|
435
440
|
} catch (e) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
return this.#lockSnapshot(await fs.readFile(this.#lock, "utf8"));
|
|
440
|
-
} catch (legacyError) {
|
|
441
|
-
if ((legacyError as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
442
|
-
throw legacyError;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
if (code !== "ENOENT") throw e;
|
|
441
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
442
|
+
throw e;
|
|
446
443
|
}
|
|
444
|
+
const lockIdentity = `${lock.dev}:${lock.ino}`;
|
|
445
|
+
let raw: string;
|
|
447
446
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
raw = lock.isDirectory()
|
|
448
|
+
? await fs.readFile(path.join(this.#lock, BROKER_LOCK_RECORD), "utf8")
|
|
449
|
+
: await fs.readFile(this.#lock, "utf8");
|
|
450
|
+
} catch (e) {
|
|
451
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
452
|
+
raw = "";
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const current = await fs.lstat(this.#lock, { bigint: true });
|
|
456
|
+
if (`${current.dev}:${current.ino}` !== lockIdentity || current.isDirectory() !== lock.isDirectory())
|
|
457
|
+
return null;
|
|
450
458
|
} catch (e) {
|
|
451
459
|
if ((e as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
452
460
|
throw e;
|
|
453
461
|
}
|
|
462
|
+
return this.#lockSnapshot(raw, lockIdentity);
|
|
454
463
|
}
|
|
455
464
|
async #createLock(): Promise<void> {
|
|
456
465
|
await fs.mkdir(this.#lock, { mode: 0o700 });
|
|
@@ -478,13 +487,18 @@ export class Broker {
|
|
|
478
487
|
}
|
|
479
488
|
async #reclaimStaleLock(snapshot: BrokerLockSnapshot): Promise<void> {
|
|
480
489
|
const current = await this.#readLock();
|
|
481
|
-
if (
|
|
490
|
+
if (
|
|
491
|
+
!current ||
|
|
492
|
+
current.identity !== snapshot.identity ||
|
|
493
|
+
current.lockIdentity !== snapshot.lockIdentity ||
|
|
494
|
+
(current.pid > 0 && isPidAlive(current.pid))
|
|
495
|
+
)
|
|
496
|
+
return;
|
|
482
497
|
|
|
483
|
-
//
|
|
484
|
-
// holding an old snapshot from renaming a newly-created directory lock.
|
|
498
|
+
// Snapshot validation and the deterministic instance suffix protect successor locks.
|
|
485
499
|
const tombstone = path.join(
|
|
486
500
|
path.dirname(this.#lock),
|
|
487
|
-
`.broker.lock.stale-${createHash("sha256").update(snapshot.
|
|
501
|
+
`.broker.lock.stale-${createHash("sha256").update(snapshot.lockIdentity).digest("hex")}`,
|
|
488
502
|
);
|
|
489
503
|
try {
|
|
490
504
|
await fs.rename(this.#lock, tombstone);
|