@gajae-code/coding-agent 0.18.6 → 0.18.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 +318 -1
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +8 -3
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +3 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +18 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +71 -0
- package/dist/types/session/session-storage.d.ts +62 -48
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/resident-memory-bench.ts +174 -20
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/job-manager.ts +228 -122
- package/src/capability/index.ts +18 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/config-cli.ts +8 -18
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/cli-main.ts +7 -8
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/sdk.ts +140 -29
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +307 -68
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/settings.ts +44 -4
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +3 -0
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +168 -23
- package/src/discovery/mcp-json.ts +6 -0
- package/src/discovery/ssh.ts +9 -4
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +380 -120
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/index.ts +5 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +6 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -8
- package/src/gjc-runtime/launch-worktree.ts +14 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +83 -8
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +19 -18
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/modes/acp/acp-agent.ts +163 -7
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +74 -29
- package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
- package/src/modes/controllers/selector-controller.ts +28 -15
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/runtime-mcp/oauth-flow.ts +5 -0
- package/src/runtime-mcp/oauth-public-url.ts +8 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +2 -2
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +93 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +83 -7
- package/src/sdk/broker/ensure.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +820 -73
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +241 -46
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/bus/telegram-daemon.ts +4 -2
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2805 -411
- package/src/sdk/host/session-runtime.ts +428 -111
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +94 -14
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +81 -38
- package/src/session/agent-session.ts +674 -224
- package/src/session/artifacts.ts +174 -70
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3457 -462
- package/src/session/internal/managed-session-storage.ts +482 -61
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +245 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
- package/src/session/session-manager.ts +939 -133
- package/src/session/session-retirement.ts +714 -0
- package/src/session/session-storage.ts +418 -174
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +641 -0
- package/src/session/task-artifact-owner-retirement.ts +477 -0
- package/src/session/task-artifact-owner.ts +289 -0
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +132 -2
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit.ts +4 -0
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +187 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -451,6 +451,9 @@ function isModernMethodNotFound(error: unknown): boolean {
|
|
|
451
451
|
* The original request is retried with `inputResponses` and a verbatim
|
|
452
452
|
* `requestState` echo under a fresh JSON-RPC id, at most MAX_MRTR_RETRIES times;
|
|
453
453
|
* the retry is fenced to the originating exchange by a stable correlation id.
|
|
454
|
+
* `roots/list` input requests are answered locally; every other input request
|
|
455
|
+
* requires `options.inputHandler`, and its absence fails the exchange before any
|
|
456
|
+
* retry is sent (no write replay, no implicit accept).
|
|
454
457
|
*/
|
|
455
458
|
async function requestWithInputHandling<T>(
|
|
456
459
|
connection: MCPServerConnection,
|
|
@@ -474,17 +477,20 @@ async function requestWithInputHandling<T>(
|
|
|
474
477
|
if (!inputRequired) return stripResultType(initial);
|
|
475
478
|
|
|
476
479
|
const handler = options?.inputHandler;
|
|
477
|
-
|
|
478
|
-
|
|
480
|
+
const correlationId = crypto.randomUUID();
|
|
481
|
+
|
|
482
|
+
const missingHandler = () =>
|
|
483
|
+
new MCPExpectedFailure(
|
|
479
484
|
new Error(
|
|
480
485
|
`MCP server "${connection.name}" requested additional input (input_required) for ${method}, but no interactive input handler is available`,
|
|
481
486
|
),
|
|
482
487
|
);
|
|
483
|
-
}
|
|
484
|
-
const correlationId = crypto.randomUUID();
|
|
485
488
|
|
|
486
489
|
for (let attempt = 0; attempt < MAX_MRTR_RETRIES; attempt++) {
|
|
487
490
|
const inputResponses: Record<string, unknown> = {};
|
|
491
|
+
// A requestState-only round has no local answer to give; without an
|
|
492
|
+
// interactive handler it is never replayed unattended.
|
|
493
|
+
if (!handler && Object.keys(inputRequired.inputRequests).length === 0) throw missingHandler();
|
|
488
494
|
for (const [key, request] of Object.entries(inputRequired.inputRequests)) {
|
|
489
495
|
if (options?.signal?.aborted) {
|
|
490
496
|
throw options.signal.reason instanceof Error ? options.signal.reason : new Error("Aborted");
|
|
@@ -494,6 +500,9 @@ async function requestWithInputHandling<T>(
|
|
|
494
500
|
inputResponses[key] = await defaultRequestHandler("roots/list", undefined);
|
|
495
501
|
continue;
|
|
496
502
|
}
|
|
503
|
+
// Everything else (elicitation, sampling, ...) needs a real interactive
|
|
504
|
+
// handler. It is never answered implicitly; fail before any retry is sent.
|
|
505
|
+
if (!handler) throw missingHandler();
|
|
497
506
|
const outcome = await handler(key, request, {
|
|
498
507
|
serverName: connection.name,
|
|
499
508
|
originMethod: method,
|
|
@@ -50,13 +50,17 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
|
|
|
50
50
|
const dir = path.dirname(filePath);
|
|
51
51
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// A predictable `${file}.tmp` can already be a symlink planted in the project.
|
|
54
|
+
// Write a unique sibling and fail closed if that path exists.
|
|
55
|
+
const tmpPath = `${filePath}.${crypto.randomUUID()}.tmp`;
|
|
55
56
|
const content = JSON.stringify(withSchema(config), null, 2);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
try {
|
|
58
|
+
await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
59
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
await fs.promises.rm(tmpPath, { force: true });
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
60
64
|
// Invalidate the capability fs cache so subsequent reads see the new content
|
|
61
65
|
invalidateFsCache(filePath);
|
|
62
66
|
}
|
|
@@ -103,6 +103,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
|
|
|
103
103
|
url: server.url ?? "",
|
|
104
104
|
};
|
|
105
105
|
if (server.headers) config.headers = server.headers;
|
|
106
|
+
if (server.publicNetwork === true) config.publicNetwork = true;
|
|
106
107
|
canonicalizeMCPEndpoint(config.url);
|
|
107
108
|
return config;
|
|
108
109
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { untilAborted } from "@gajae-code/utils";
|
|
2
|
+
import type { ExtensionUIContext } from "../extensibility/extensions/types";
|
|
3
|
+
import type { AskAnswerSource, AskRemoteReceipt } from "../tools";
|
|
4
|
+
import type { MCPInputRequestHandler } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface MCPFormInputDependencies {
|
|
7
|
+
getUi(): { ui?: Pick<ExtensionUIContext, "select" | "input">; hasUI: boolean };
|
|
8
|
+
getAskAnswerSource(): AskAnswerSource | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FormField {
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
type: "string" | "boolean";
|
|
15
|
+
choices?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function displayText(value: string): string {
|
|
23
|
+
return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ").slice(0, 4000);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Unsupported constraints are rejected, never silently discarded. */
|
|
27
|
+
function decodeField(schema: unknown): FormField | undefined {
|
|
28
|
+
if (!isRecord(schema) || schema.type !== "object" || !isRecord(schema.properties)) return undefined;
|
|
29
|
+
const allowed = new Set(["type", "properties", "required", "additionalProperties", "title", "description"]);
|
|
30
|
+
if (Object.keys(schema).some(key => !allowed.has(key))) return undefined;
|
|
31
|
+
if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== "boolean") return undefined;
|
|
32
|
+
const entries = Object.entries(schema.properties);
|
|
33
|
+
if (entries.length !== 1) return undefined;
|
|
34
|
+
const [key, property] = entries[0]!;
|
|
35
|
+
if (!isRecord(property) || (property.type !== "string" && property.type !== "boolean")) return undefined;
|
|
36
|
+
if (
|
|
37
|
+
schema.required !== undefined &&
|
|
38
|
+
(!Array.isArray(schema.required) || schema.required.some(value => value !== key))
|
|
39
|
+
)
|
|
40
|
+
return undefined;
|
|
41
|
+
const propertyKeys = new Set(["type", "title", "description", "enum"]);
|
|
42
|
+
if (Object.keys(property).some(name => !propertyKeys.has(name))) return undefined;
|
|
43
|
+
if (property.title !== undefined && typeof property.title !== "string") return undefined;
|
|
44
|
+
if (property.description !== undefined && typeof property.description !== "string") return undefined;
|
|
45
|
+
let choices: string[] | undefined;
|
|
46
|
+
if (property.enum !== undefined) {
|
|
47
|
+
if (
|
|
48
|
+
property.type !== "string" ||
|
|
49
|
+
!Array.isArray(property.enum) ||
|
|
50
|
+
property.enum.length === 0 ||
|
|
51
|
+
!property.enum.every(value => typeof value === "string") ||
|
|
52
|
+
new Set(property.enum).size !== property.enum.length ||
|
|
53
|
+
property.enum.some(value => displayText(value) !== value)
|
|
54
|
+
)
|
|
55
|
+
return undefined;
|
|
56
|
+
choices = property.enum;
|
|
57
|
+
}
|
|
58
|
+
const description = typeof property.description === "string" ? ` — ${property.description}` : "";
|
|
59
|
+
return { key, label: displayText(`${property.title ?? key}${description}`), type: property.type, choices };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Register on an owned manager; dependencies are resolved per call for late UI registration. */
|
|
63
|
+
export function createMCPFormInputHandler(deps: MCPFormInputDependencies): MCPInputRequestHandler {
|
|
64
|
+
return async (_key, request, context) => {
|
|
65
|
+
if (context.signal?.aborted) return { kind: "failed", reason: "cancelled" };
|
|
66
|
+
if (request.method !== "elicitation/create") return { kind: "failed", reason: "unavailable" };
|
|
67
|
+
const params = request.params;
|
|
68
|
+
if (!params || typeof params.message !== "string" || (params.mode !== undefined && params.mode !== "form"))
|
|
69
|
+
return { kind: "failed", reason: "unavailable", message: "Unsupported MCP form request" };
|
|
70
|
+
const field = decodeField(params.requestedSchema);
|
|
71
|
+
if (!field) return { kind: "failed", reason: "unavailable", message: "Unsupported MCP form schema" };
|
|
72
|
+
const local = deps.getUi();
|
|
73
|
+
const ui = local.hasUI ? local.ui : undefined;
|
|
74
|
+
const source = ui ? undefined : deps.getAskAnswerSource();
|
|
75
|
+
if (!ui && !source)
|
|
76
|
+
return { kind: "failed", reason: "unavailable", message: "No user input surface is available" };
|
|
77
|
+
const signal = context.signal;
|
|
78
|
+
const ask = async (question: string, choices?: string[]): Promise<string | undefined> => {
|
|
79
|
+
return untilAborted(signal, async () => {
|
|
80
|
+
if (ui)
|
|
81
|
+
return choices ? ui.select(question, choices, { signal }) : ui.input(question, undefined, { signal });
|
|
82
|
+
if (!source) return undefined;
|
|
83
|
+
const answer = source.awaitAnswerRequest
|
|
84
|
+
? await source.awaitAnswerRequest(
|
|
85
|
+
{
|
|
86
|
+
question,
|
|
87
|
+
options: choices ?? [],
|
|
88
|
+
interaction: choices ? "selector" : "custom_editor",
|
|
89
|
+
controls: [],
|
|
90
|
+
},
|
|
91
|
+
signal,
|
|
92
|
+
)
|
|
93
|
+
: await source.awaitAnswer(question, choices ?? [], signal);
|
|
94
|
+
let receipt: AskRemoteReceipt | undefined;
|
|
95
|
+
let value: string | undefined;
|
|
96
|
+
if (typeof answer === "string") value = answer;
|
|
97
|
+
else if (answer) {
|
|
98
|
+
receipt = answer;
|
|
99
|
+
if (answer.interaction.kind === "value") value = answer.interaction.value;
|
|
100
|
+
}
|
|
101
|
+
if (signal?.aborted) {
|
|
102
|
+
await receipt?.settle({ kind: "resolve_without_commit", reason: "aborted" });
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (receipt?.interaction.kind === "control") {
|
|
106
|
+
await receipt.settle({ kind: "invalid", reason: "invalid_control" });
|
|
107
|
+
throw new Error("Invalid MCP form control");
|
|
108
|
+
}
|
|
109
|
+
if (value !== undefined && choices && !choices.includes(value)) {
|
|
110
|
+
await receipt?.settle({ kind: "invalid", reason: "invalid_option" });
|
|
111
|
+
throw new Error("Invalid MCP form answer");
|
|
112
|
+
}
|
|
113
|
+
const settlement = await receipt?.settle(
|
|
114
|
+
value === undefined ? { kind: "resolve_without_commit", reason: "cancelled" } : { kind: "commit" },
|
|
115
|
+
);
|
|
116
|
+
if (receipt && value !== undefined && settlement?.kind !== "committed") return undefined;
|
|
117
|
+
return value;
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
const title = `MCP server ${displayText(context.serverName)} requests input (untrusted server text): ${displayText(params.message)}`;
|
|
122
|
+
const action = await ask(title, ["Accept", "Decline"]);
|
|
123
|
+
if (signal?.aborted) return { kind: "failed", reason: "cancelled" };
|
|
124
|
+
if (action === undefined) return { kind: "result", result: { action: "cancel" } };
|
|
125
|
+
if (action === "Decline") return { kind: "result", result: { action: "decline" } };
|
|
126
|
+
if (action !== "Accept") return { kind: "failed", reason: "error", message: "Invalid MCP form action" };
|
|
127
|
+
const choices = field.type === "boolean" ? ["Yes", "No"] : field.choices;
|
|
128
|
+
const answer = await ask(
|
|
129
|
+
`MCP server ${displayText(context.serverName)} (untrusted server text): ${field.label}`,
|
|
130
|
+
choices,
|
|
131
|
+
);
|
|
132
|
+
if (signal?.aborted) return { kind: "failed", reason: "cancelled" };
|
|
133
|
+
if (answer === undefined) return { kind: "result", result: { action: "cancel" } };
|
|
134
|
+
if (choices && !choices.includes(answer))
|
|
135
|
+
return { kind: "failed", reason: "error", message: "Invalid MCP form answer" };
|
|
136
|
+
const value = field.type === "boolean" ? answer === "Yes" : answer;
|
|
137
|
+
return { kind: "result", result: { action: "accept", content: { [field.key]: value } } };
|
|
138
|
+
} catch {
|
|
139
|
+
return { kind: "failed", reason: signal?.aborted ? "cancelled" : "error" };
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callback-server";
|
|
9
9
|
import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
|
|
10
10
|
import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
|
|
11
|
+
import { assertPublicOAuthUrl } from "./oauth-public-url";
|
|
11
12
|
|
|
12
13
|
const DEFAULT_PORT = 3000;
|
|
13
14
|
const CALLBACK_PATH = "/callback";
|
|
@@ -261,8 +262,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
261
262
|
params.set("client_secret", clientSecret);
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
await assertPublicOAuthUrl(this.config.tokenUrl);
|
|
264
266
|
const response = await fetch(this.config.tokenUrl, {
|
|
265
267
|
method: "POST",
|
|
268
|
+
redirect: "error",
|
|
266
269
|
headers: {
|
|
267
270
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
268
271
|
},
|
|
@@ -340,8 +343,10 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
340
343
|
if (!registrationEndpoint) return;
|
|
341
344
|
|
|
342
345
|
try {
|
|
346
|
+
await assertPublicOAuthUrl(registrationEndpoint);
|
|
343
347
|
const response = await fetch(registrationEndpoint, {
|
|
344
348
|
method: "POST",
|
|
349
|
+
redirect: "error",
|
|
345
350
|
headers: {
|
|
346
351
|
"Content-Type": "application/json",
|
|
347
352
|
Accept: "application/json",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { validatePublicHttpUrl } from "../web/insane/url-guard";
|
|
2
|
+
|
|
3
|
+
export async function assertPublicOAuthUrl(url: string): Promise<void> {
|
|
4
|
+
const checked = await validatePublicHttpUrl(url);
|
|
5
|
+
if (!checked.ok) {
|
|
6
|
+
throw new Error(`Refusing non-public OAuth endpoint: ${checked.reason ?? url}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -19,6 +19,10 @@ export function bindPluginMcpToPublicNetwork<T extends MCPHttpServerConfig | MCP
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** The marker is an internal symbol, so persisted user MCP configs cannot opt into or forge this path. */
|
|
22
|
+
export function usesPublicNetworkMcpFetch(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
|
|
23
|
+
return ("publicNetwork" in config && config.publicNetwork === true) || isPluginMcpPublicNetworkBound(config);
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
export function isPluginMcpPublicNetworkBound(config: MCPHttpServerConfig | MCPSseServerConfig): boolean {
|
|
23
27
|
return (
|
|
24
28
|
(config as MCPHttpServerConfig & { [PLUGIN_MCP_PUBLIC_NETWORK_BOUNDARY]?: boolean })[
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "@gajae-code/utils";
|
|
2
|
+
import { type AddressResolver, validatePublicHttpUrl } from "../web/insane/url-guard";
|
|
2
3
|
import type { MCPServerConfig } from "./types";
|
|
3
4
|
|
|
4
5
|
const SMITHERY_REGISTRY_BASE_URL = "https://registry.smithery.ai";
|
|
@@ -289,6 +290,20 @@ function chooseConnection(
|
|
|
289
290
|
return null;
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
export async function resolveSmitheryServerConfig(
|
|
294
|
+
qualifiedName: string,
|
|
295
|
+
selected: { connection: SmitheryConnection; useDirectHttp: boolean },
|
|
296
|
+
options?: { resolver?: AddressResolver },
|
|
297
|
+
): Promise<MCPServerConfig | null> {
|
|
298
|
+
let useDirectHttp = selected.useDirectHttp;
|
|
299
|
+
const deploymentUrl = selected.connection.deploymentUrl;
|
|
300
|
+
if (useDirectHttp && selected.connection.type === "http" && deploymentUrl) {
|
|
301
|
+
const checked = await validatePublicHttpUrl(deploymentUrl, { resolver: options?.resolver });
|
|
302
|
+
if (!checked.ok) useDirectHttp = false;
|
|
303
|
+
}
|
|
304
|
+
return createConfig(qualifiedName, { connection: selected.connection, useDirectHttp });
|
|
305
|
+
}
|
|
306
|
+
|
|
292
307
|
function createConfig(
|
|
293
308
|
qualifiedName: string,
|
|
294
309
|
selected: { connection: SmitheryConnection; useDirectHttp: boolean },
|
|
@@ -297,6 +312,7 @@ function createConfig(
|
|
|
297
312
|
return {
|
|
298
313
|
type: "http",
|
|
299
314
|
url: selected.connection.deploymentUrl,
|
|
315
|
+
publicNetwork: true,
|
|
300
316
|
};
|
|
301
317
|
}
|
|
302
318
|
|
|
@@ -335,7 +351,10 @@ async function fetchServerDetailsFromEntry(
|
|
|
335
351
|
return null;
|
|
336
352
|
}
|
|
337
353
|
|
|
338
|
-
function toSearchResult(
|
|
354
|
+
async function toSearchResult(
|
|
355
|
+
entry: SmitherySearchEntry,
|
|
356
|
+
details: SmitheryServerDetails,
|
|
357
|
+
): Promise<SmitherySearchResult | null> {
|
|
339
358
|
if (!entry.id) return null;
|
|
340
359
|
const qualifiedName = normalizeQualifiedName(
|
|
341
360
|
details.qualifiedName ?? entry.qualifiedName ?? `${entry.namespace}/${entry.slug}`,
|
|
@@ -343,7 +362,7 @@ function toSearchResult(entry: SmitherySearchEntry, details: SmitheryServerDetai
|
|
|
343
362
|
const selected = chooseConnection(details);
|
|
344
363
|
if (!selected) return null;
|
|
345
364
|
|
|
346
|
-
const config =
|
|
365
|
+
const config = await resolveSmitheryServerConfig(qualifiedName, selected);
|
|
347
366
|
if (!config) return null;
|
|
348
367
|
|
|
349
368
|
const requiredInputs = getSchemaInputs(selected.connection.configSchema);
|
|
@@ -450,7 +469,7 @@ export async function searchSmitheryRegistry(
|
|
|
450
469
|
try {
|
|
451
470
|
const details = await fetchServerDetailsFromEntry(entry, { apiKey: options?.apiKey });
|
|
452
471
|
if (!details) return null;
|
|
453
|
-
return toSearchResult(entry, details);
|
|
472
|
+
return await toSearchResult(entry, details);
|
|
454
473
|
} catch (error) {
|
|
455
474
|
detailFailures.push({
|
|
456
475
|
identity: getEntryIdentityKey(entry) ?? entry.id ?? "unknown",
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
MCP_MAX_SSE_REQUEST_MESSAGES,
|
|
29
29
|
readMCPResponseText,
|
|
30
30
|
} from "../content-limits";
|
|
31
|
-
import { fetchPluginMcpRequest,
|
|
31
|
+
import { fetchPluginMcpRequest, usesPublicNetworkMcpFetch } from "../plugin-network-boundary";
|
|
32
32
|
import { buildModernMcpHeaders, type MCPModernClientContext, type MCPProtocolEra, withModernMeta } from "../protocol";
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -155,7 +155,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
#fetch(init: BunFetchRequestInit): Promise<Response> {
|
|
158
|
-
return
|
|
158
|
+
return usesPublicNetworkMcpFetch(this.config)
|
|
159
159
|
? fetchPluginMcpRequest(this.config.url, init)
|
|
160
160
|
: fetch(this.config.url, init);
|
|
161
161
|
}
|
package/src/runtime-mcp/types.ts
CHANGED
|
@@ -139,6 +139,12 @@ export interface MCPHttpServerConfig extends MCPServerConfigBase {
|
|
|
139
139
|
type: "http";
|
|
140
140
|
url: string;
|
|
141
141
|
headers?: Record<string, string>;
|
|
142
|
+
/**
|
|
143
|
+
* When true, every request and redirect for this URL is rechecked against
|
|
144
|
+
* the public-network policy. Persisted so a later process cannot fall back
|
|
145
|
+
* to an unguarded fetch. Ordinary localhost MCP servers leave this unset.
|
|
146
|
+
*/
|
|
147
|
+
publicNetwork?: boolean;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
/** SSE server configuration (deprecated, use HTTP) */
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { logger } from "@gajae-code/utils";
|
|
3
3
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_SDK_REQUEST_TIMEOUT_MS,
|
|
6
|
+
type SdkClient,
|
|
7
|
+
SdkClientError,
|
|
8
|
+
type SdkDispatchContext,
|
|
9
|
+
type SdkSentRecord,
|
|
10
|
+
} from "../client";
|
|
5
11
|
import type { AbortScope } from "../host/control/operations";
|
|
6
12
|
import { assertReverseResponseFrame, ReverseLeaseError } from "../host/reverse-leases";
|
|
7
13
|
import {
|
|
@@ -143,6 +149,7 @@ const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
|
|
|
143
149
|
"readiness_timeout",
|
|
144
150
|
"spawn_failed",
|
|
145
151
|
"worktree_in_use",
|
|
152
|
+
"uncertain_after_send",
|
|
146
153
|
]);
|
|
147
154
|
|
|
148
155
|
/**
|
|
@@ -531,7 +538,11 @@ export class AcpSdkAdapter {
|
|
|
531
538
|
return envelope?.result ?? response;
|
|
532
539
|
}
|
|
533
540
|
|
|
534
|
-
async #requestSession(
|
|
541
|
+
async #requestSession(
|
|
542
|
+
frame: JsonObject,
|
|
543
|
+
raw = false,
|
|
544
|
+
options?: { timeoutMs: number; deadline?: number },
|
|
545
|
+
): Promise<unknown> {
|
|
535
546
|
const router = this.#router;
|
|
536
547
|
if (!router)
|
|
537
548
|
throw new AcpSdkAdapterError(
|
|
@@ -603,11 +614,68 @@ export class AcpSdkAdapter {
|
|
|
603
614
|
// request that named no readiness budget is queued for the default one, so it
|
|
604
615
|
// needs the same extension rather than the client's generic request deadline.
|
|
605
616
|
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
606
|
-
const
|
|
617
|
+
const budgetMs = timeoutMs ?? DEFAULT_SDK_REQUEST_TIMEOUT_MS;
|
|
618
|
+
const deadline = Date.now() + budgetMs;
|
|
619
|
+
let sent = false;
|
|
620
|
+
let sentRecord: SdkSentRecord | undefined;
|
|
621
|
+
let uncertainty: SdkClientError | undefined;
|
|
622
|
+
const deadlineFailure = () =>
|
|
623
|
+
uncertainty ??
|
|
624
|
+
(sent
|
|
625
|
+
? new SdkClientError(
|
|
626
|
+
"uncertain_after_send",
|
|
627
|
+
"ACP lifecycle deadline elapsed after dispatch.",
|
|
628
|
+
sentRecord ?? {
|
|
629
|
+
operation,
|
|
630
|
+
idempotencyKey,
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
: new SdkClientError("timeout", "ACP lifecycle deadline elapsed before dispatch.", undefined, undefined, {
|
|
634
|
+
transport: true,
|
|
635
|
+
}));
|
|
636
|
+
const options = {
|
|
607
637
|
idempotencyKey,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
638
|
+
deadline,
|
|
639
|
+
// SdkClient reads this after connecting, so reconnect and replay spend the same allowance.
|
|
640
|
+
get timeoutMs() {
|
|
641
|
+
return Math.max(0, deadline - Date.now());
|
|
642
|
+
},
|
|
643
|
+
beforeDispatch: () => {
|
|
644
|
+
if (Date.now() >= deadline) throw deadlineFailure();
|
|
645
|
+
},
|
|
646
|
+
onDispatch: (context: SdkDispatchContext) => {
|
|
647
|
+
sent = true;
|
|
648
|
+
if (typeof context.frame.id === "string") sentRecord = client.getSentRecord(context.frame.id);
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
const expired = Promise.withResolvers<never>();
|
|
652
|
+
const timer = setTimeout(() => expired.reject(deadlineFailure()), budgetMs);
|
|
653
|
+
const recover = async () => {
|
|
654
|
+
try {
|
|
655
|
+
const result = await client.global(operation, input, options);
|
|
656
|
+
if (Date.now() >= deadline) throw deadlineFailure();
|
|
657
|
+
return result;
|
|
658
|
+
} catch (firstError) {
|
|
659
|
+
if (!(firstError instanceof SdkClientError) || firstError.code !== "uncertain_after_send") throw firstError;
|
|
660
|
+
uncertainty = firstError;
|
|
661
|
+
// Same-key replay joins the original durable claim, within the original deadline.
|
|
662
|
+
// Failure to recover does not prove that the committed operation failed.
|
|
663
|
+
if (Date.now() >= deadline) throw firstError;
|
|
664
|
+
try {
|
|
665
|
+
const result = await client.global(operation, input, options);
|
|
666
|
+
if (Date.now() >= deadline) throw firstError;
|
|
667
|
+
return result;
|
|
668
|
+
} catch (replayError) {
|
|
669
|
+
if (replayError !== firstError) Object.assign(firstError, { recovery: replayError });
|
|
670
|
+
throw firstError;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
try {
|
|
675
|
+
return await Promise.race([recover(), expired.promise]);
|
|
676
|
+
} finally {
|
|
677
|
+
clearTimeout(timer);
|
|
678
|
+
}
|
|
611
679
|
}
|
|
612
680
|
|
|
613
681
|
async sdkControl(params: { operation: string; input?: JsonObject }): Promise<unknown> {
|
|
@@ -646,7 +714,7 @@ export class AcpSdkAdapter {
|
|
|
646
714
|
throw new AcpSdkAdapterError("method_not_found", `Unsupported ACP SDK method: ${method}`);
|
|
647
715
|
}
|
|
648
716
|
|
|
649
|
-
async registerProvider(provider: AcpProviderRegistration): Promise<void> {
|
|
717
|
+
async registerProvider(provider: AcpProviderRegistration, timeoutMs?: number, deadline?: number): Promise<void> {
|
|
650
718
|
if (!this.#router)
|
|
651
719
|
throw new AcpSdkAdapterError(
|
|
652
720
|
"operation_prohibited",
|
|
@@ -665,10 +733,15 @@ export class AcpSdkAdapter {
|
|
|
665
733
|
...(previousLeaseId ? { expectedLeaseId: previousLeaseId } : {}),
|
|
666
734
|
},
|
|
667
735
|
true,
|
|
736
|
+
timeoutMs === undefined ? undefined : { timeoutMs, ...(deadline === undefined ? {} : { deadline }) },
|
|
668
737
|
);
|
|
669
738
|
const result = object(object(response)?.result) ?? object(response) ?? {};
|
|
670
739
|
if (typeof result.leaseId !== "string")
|
|
671
740
|
throw new AcpSdkAdapterError("invalid_reverse_frame", "Provider registration omitted leaseId.");
|
|
741
|
+
if (this.#closed)
|
|
742
|
+
throw new AcpSdkAdapterError("connection_closed", "ACP provider activation closed before acknowledgement.");
|
|
743
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
744
|
+
throw new AcpSdkAdapterError("provider_activation_exhausted", "ACP provider activation deadline elapsed.");
|
|
672
745
|
this.#leases.set(provider.capability, result.leaseId);
|
|
673
746
|
if (previousLeaseId && previousLeaseId !== result.leaseId) this.#abortReverseForCapability(provider.capability);
|
|
674
747
|
}
|
|
@@ -694,9 +767,20 @@ export class AcpSdkAdapter {
|
|
|
694
767
|
const connectionId = attachment?.connectionId;
|
|
695
768
|
try {
|
|
696
769
|
for (const provider of this.#providers) {
|
|
770
|
+
const providerRemainingMs = PROVIDER_ACTIVATION_BUDGET_MS - (Date.now() - startedAt);
|
|
771
|
+
if (providerRemainingMs <= 0) throw this.#providerActivationExhausted(attempt, startedAt);
|
|
697
772
|
try {
|
|
698
|
-
await this.registerProvider(
|
|
773
|
+
await this.registerProvider(
|
|
774
|
+
provider,
|
|
775
|
+
Math.max(1, providerRemainingMs),
|
|
776
|
+
startedAt + PROVIDER_ACTIVATION_BUDGET_MS,
|
|
777
|
+
);
|
|
778
|
+
if (Date.now() - startedAt >= PROVIDER_ACTIVATION_BUDGET_MS)
|
|
779
|
+
throw this.#providerActivationExhausted(attempt, startedAt);
|
|
699
780
|
} catch (error) {
|
|
781
|
+
if (providerErrorCode(error) === "uncertain_after_send") throw error;
|
|
782
|
+
if (Date.now() - startedAt >= PROVIDER_ACTIVATION_BUDGET_MS)
|
|
783
|
+
throw this.#providerActivationExhausted(attempt, startedAt);
|
|
700
784
|
if (providerErrorCode(error) === "provider_lease_conflict") {
|
|
701
785
|
this.#leases.delete(provider.capability);
|
|
702
786
|
this.#abortReverseForCapability(provider.capability);
|
|
@@ -725,6 +809,7 @@ export class AcpSdkAdapter {
|
|
|
725
809
|
this.#providersActivated = true;
|
|
726
810
|
return;
|
|
727
811
|
} catch (error) {
|
|
812
|
+
if (providerErrorCode(error) === "uncertain_after_send") throw error;
|
|
728
813
|
if (this.#router && attachment !== this.#attachment && !this.#closed) {
|
|
729
814
|
this.#providersActivated = false;
|
|
730
815
|
continue;
|
|
@@ -22,6 +22,7 @@ export type BrokerFenceReason = "suspect-unpublished" | "observation-ambiguous"
|
|
|
22
22
|
export type BrokerExitReason =
|
|
23
23
|
| "ownership-fence-expired"
|
|
24
24
|
| "publication-liveness-timeout"
|
|
25
|
+
| "heartbeat-renewal-blocked"
|
|
25
26
|
| "restart-committed"
|
|
26
27
|
| "shutdown-request"
|
|
27
28
|
| "startup-failure"
|
|
@@ -39,6 +40,8 @@ export interface BrokerExitRecord {
|
|
|
39
40
|
fenceReason: BrokerFenceReason | null;
|
|
40
41
|
fencedForMs: number;
|
|
41
42
|
uptimeMs: number;
|
|
43
|
+
/** For a blocked heartbeat renewal: retained removal transition path. */
|
|
44
|
+
blockingLockPath?: string;
|
|
42
45
|
pid: number;
|
|
43
46
|
signal: "SIGINT" | "SIGTERM" | null;
|
|
44
47
|
writtenAt: number;
|
|
@@ -47,7 +50,7 @@ export interface BrokerExitRecord {
|
|
|
47
50
|
export interface BrokerStartupExitRecord {
|
|
48
51
|
version: 1;
|
|
49
52
|
mode: "startup";
|
|
50
|
-
reason: "startup-deadline" | "startup-signal";
|
|
53
|
+
reason: "startup-deadline" | "startup-signal" | "startup-lock-blocked";
|
|
51
54
|
fenceReason: null;
|
|
52
55
|
fencedForMs: 0;
|
|
53
56
|
uptimeMs: number;
|
|
@@ -56,6 +59,8 @@ export interface BrokerStartupExitRecord {
|
|
|
56
59
|
exitCode: 1 | 130 | 143;
|
|
57
60
|
timeoutMs: number | null;
|
|
58
61
|
writtenAt: number;
|
|
62
|
+
/** For lock-blocked reason: path to the retained removal transition. */
|
|
63
|
+
blockingLockPath?: string;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export type BrokerStartupExitWriteStatus =
|
|
@@ -71,6 +76,7 @@ const lastWriteGenerationByPath = new Map<string, number>();
|
|
|
71
76
|
const EXIT_REASONS = new Set<BrokerExitReason>([
|
|
72
77
|
"ownership-fence-expired",
|
|
73
78
|
"publication-liveness-timeout",
|
|
79
|
+
"heartbeat-renewal-blocked",
|
|
74
80
|
"restart-committed",
|
|
75
81
|
"shutdown-request",
|
|
76
82
|
"startup-failure",
|
|
@@ -94,11 +100,20 @@ function isBrokerExitRecord(value: unknown): value is BrokerExitRecord {
|
|
|
94
100
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
95
101
|
const record = value as Partial<BrokerExitRecord>;
|
|
96
102
|
const keys = Object.keys(record).sort();
|
|
103
|
+
const requiredKeys = [
|
|
104
|
+
"fenceReason",
|
|
105
|
+
"fencedForMs",
|
|
106
|
+
"mode",
|
|
107
|
+
"pid",
|
|
108
|
+
"reason",
|
|
109
|
+
"signal",
|
|
110
|
+
"uptimeMs",
|
|
111
|
+
"version",
|
|
112
|
+
"writtenAt",
|
|
113
|
+
];
|
|
97
114
|
return (
|
|
98
|
-
keys.
|
|
99
|
-
|
|
100
|
-
.sort()
|
|
101
|
-
.join(",") &&
|
|
115
|
+
keys.every(key => requiredKeys.includes(key) || key === "blockingLockPath") &&
|
|
116
|
+
requiredKeys.every(key => keys.includes(key)) &&
|
|
102
117
|
record.version === 1 &&
|
|
103
118
|
(record.mode === "owned-root" || record.mode === "lost-root") &&
|
|
104
119
|
typeof record.reason === "string" &&
|
|
@@ -115,6 +130,14 @@ function isBrokerExitRecord(value: unknown): value is BrokerExitRecord {
|
|
|
115
130
|
(record.pid as number) > 0 &&
|
|
116
131
|
(record.signal === null || record.signal === "SIGINT" || record.signal === "SIGTERM") &&
|
|
117
132
|
(record.reason === "signal") === (record.signal !== null) &&
|
|
133
|
+
(record.reason === "heartbeat-renewal-blocked"
|
|
134
|
+
? record.mode === "owned-root" &&
|
|
135
|
+
record.fenceReason === null &&
|
|
136
|
+
record.fencedForMs === 0 &&
|
|
137
|
+
record.signal === null &&
|
|
138
|
+
typeof record.blockingLockPath === "string" &&
|
|
139
|
+
record.blockingLockPath.length > 0
|
|
140
|
+
: record.blockingLockPath === undefined) &&
|
|
118
141
|
Number.isSafeInteger(record.writtenAt) &&
|
|
119
142
|
(record.writtenAt as number) > 0
|
|
120
143
|
);
|
|
@@ -125,26 +148,29 @@ function isBrokerStartupExitRecord(value: unknown): value is BrokerStartupExitRe
|
|
|
125
148
|
const record = value as Partial<BrokerStartupExitRecord>;
|
|
126
149
|
const signalExitCode = record.signal === "SIGINT" ? 130 : record.signal === "SIGTERM" ? 143 : null;
|
|
127
150
|
const keys = Object.keys(record).sort();
|
|
151
|
+
const requiredKeys = [
|
|
152
|
+
"exitCode",
|
|
153
|
+
"fenceReason",
|
|
154
|
+
"fencedForMs",
|
|
155
|
+
"mode",
|
|
156
|
+
"pid",
|
|
157
|
+
"reason",
|
|
158
|
+
"signal",
|
|
159
|
+
"timeoutMs",
|
|
160
|
+
"uptimeMs",
|
|
161
|
+
"version",
|
|
162
|
+
"writtenAt",
|
|
163
|
+
];
|
|
164
|
+
const isValidShape =
|
|
165
|
+
keys.every(k => requiredKeys.includes(k) || k === "blockingLockPath") &&
|
|
166
|
+
requiredKeys.every(k => keys.includes(k));
|
|
128
167
|
return (
|
|
129
|
-
|
|
130
|
-
[
|
|
131
|
-
"exitCode",
|
|
132
|
-
"fenceReason",
|
|
133
|
-
"fencedForMs",
|
|
134
|
-
"mode",
|
|
135
|
-
"pid",
|
|
136
|
-
"reason",
|
|
137
|
-
"signal",
|
|
138
|
-
"timeoutMs",
|
|
139
|
-
"uptimeMs",
|
|
140
|
-
"version",
|
|
141
|
-
"writtenAt",
|
|
142
|
-
]
|
|
143
|
-
.sort()
|
|
144
|
-
.join(",") &&
|
|
168
|
+
isValidShape &&
|
|
145
169
|
record.version === 1 &&
|
|
146
170
|
record.mode === "startup" &&
|
|
147
|
-
(record.reason === "startup-deadline" ||
|
|
171
|
+
(record.reason === "startup-deadline" ||
|
|
172
|
+
record.reason === "startup-signal" ||
|
|
173
|
+
record.reason === "startup-lock-blocked") &&
|
|
148
174
|
record.fenceReason === null &&
|
|
149
175
|
record.fencedForMs === 0 &&
|
|
150
176
|
Number.isSafeInteger(record.uptimeMs) &&
|
|
@@ -154,7 +180,10 @@ function isBrokerStartupExitRecord(value: unknown): value is BrokerStartupExitRe
|
|
|
154
180
|
(record.signal === null || record.signal === "SIGINT" || record.signal === "SIGTERM") &&
|
|
155
181
|
(record.exitCode === 1 || record.exitCode === 130 || record.exitCode === 143) &&
|
|
156
182
|
(record.timeoutMs === null || (Number.isSafeInteger(record.timeoutMs) && (record.timeoutMs as number) > 0)) &&
|
|
157
|
-
(record.reason === "startup-
|
|
183
|
+
(record.reason === "startup-lock-blocked"
|
|
184
|
+
? typeof record.blockingLockPath === "string" && record.blockingLockPath.length > 0
|
|
185
|
+
: record.blockingLockPath === undefined) &&
|
|
186
|
+
(record.reason === "startup-deadline" || record.reason === "startup-lock-blocked"
|
|
158
187
|
? record.exitCode === 1 && record.signal === null && record.timeoutMs !== null
|
|
159
188
|
: record.exitCode === signalExitCode && signalExitCode !== null && record.timeoutMs === null) &&
|
|
160
189
|
Number.isSafeInteger(record.writtenAt) &&
|