@gajae-code/coding-agent 0.11.7 → 0.11.9
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 +73 -1
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/keybindings.d.ts +33 -3
- 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/model-profiles.d.ts +2 -3
- package/dist/types/config/settings-schema.d.ts +15 -2
- 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/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +4 -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 +5 -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 +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- 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/runtime-mcp/redaction.d.ts +2 -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 +7 -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 +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -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/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- 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/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -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 +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +50 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commands/sdk.ts +2 -2
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +2 -3
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -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/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- 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 +78 -4
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/state-writer.ts +166 -2
- 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 +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +8 -6
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- 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 +20 -4
- package/src/modes/components/welcome.ts +32 -20
- 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 +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +70 -36
- 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 +3 -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/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +7 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/json-rpc.ts +7 -4
- 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/redaction.ts +30 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +20 -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 +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- 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 +27 -2
- 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 +15 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +853 -112
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- 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 +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +138 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +998 -133
- package/src/session/internal/managed-session-storage.ts +268 -203
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +406 -360
- package/src/session/session-storage.ts +151 -11
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- 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 -88
- 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/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -95,7 +95,7 @@ export interface AskSelectedAckRecoveryParticipant {
|
|
|
95
95
|
|
|
96
96
|
/** SDK-native surface for emitting a workflow gate and awaiting its answer. */
|
|
97
97
|
export interface WorkflowGateEmitter {
|
|
98
|
-
|
|
98
|
+
supportsRemoteGateAnswers(): boolean;
|
|
99
99
|
emitGate(input: OpenGateInput): Promise<unknown>;
|
|
100
100
|
onGateEmitted?(listener: (gate: WorkflowGate) => void): () => void;
|
|
101
101
|
resolveGate?(response: WorkflowGateResponse): Promise<WorkflowGateResolution>;
|
|
@@ -167,7 +167,7 @@ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
supportsRemoteGateAnswers(): boolean {
|
|
171
171
|
return true;
|
|
172
172
|
}
|
|
173
173
|
setRuntimeTurnProvider(provider: (() => string | undefined) | null): void {
|
package/src/modes/types.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
|
32
32
|
import type { StatusLineComponent } from "./components/tool-status-header";
|
|
33
33
|
import type { IrcObservationLedger } from "./irc-observation-ledger";
|
|
34
34
|
import type { OAuthManualInputManager } from "./oauth-manual-input";
|
|
35
|
+
import type { PromptSuggestionController } from "./prompt-suggestion-controller";
|
|
35
36
|
import type { Theme } from "./theme/theme";
|
|
36
37
|
import type { ParsedIrcMessage } from "./utils/irc-message";
|
|
37
38
|
|
|
@@ -156,6 +157,8 @@ export interface InteractiveModeContext {
|
|
|
156
157
|
skillCommands: Map<string, Skill>;
|
|
157
158
|
oauthManualInput: OAuthManualInputManager;
|
|
158
159
|
todoPhases: TodoPhase[];
|
|
160
|
+
/** Ghost-text next-prompt prediction; absent in ACP/lightweight test contexts. */
|
|
161
|
+
promptSuggestion?: PromptSuggestionController;
|
|
159
162
|
|
|
160
163
|
// Lifecycle
|
|
161
164
|
init(): Promise<void>;
|
|
@@ -1,60 +1,88 @@
|
|
|
1
|
+
import type { Keybinding } from "@gajae-code/tui";
|
|
1
2
|
import type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
|
|
2
3
|
|
|
3
4
|
export interface HotkeysMarkdownBindings {
|
|
4
|
-
keybindings: Pick<KeybindingsManager, "
|
|
5
|
+
keybindings: Pick<KeybindingsManager, "formatAccessibleKeyHint" | "getAccessibleDisplayString">;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function key(bindings: HotkeysMarkdownBindings, action: Keybinding): string {
|
|
9
|
+
return bindings.keybindings.getAccessibleDisplayString(action) || "Disabled";
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
function appKey(bindings: HotkeysMarkdownBindings, action: AppKeybinding): string {
|
|
8
|
-
return bindings
|
|
13
|
+
return key(bindings, action);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatHotkeyMarkdownCode(label: string, escapeTablePipes = true): string {
|
|
17
|
+
const escaped = escapeTablePipes ? label.replace(/\|/g, "\\|") : label;
|
|
18
|
+
let longestBacktickRun = 0;
|
|
19
|
+
for (const match of escaped.matchAll(/`+/g)) {
|
|
20
|
+
longestBacktickRun = Math.max(longestBacktickRun, match[0].length);
|
|
21
|
+
}
|
|
22
|
+
const fence = "`".repeat(longestBacktickRun + 1);
|
|
23
|
+
const padding = escaped.startsWith("`") || escaped.endsWith("`") ? " " : "";
|
|
24
|
+
return `${fence}${padding}${escaped}${padding}${fence}`;
|
|
9
25
|
}
|
|
10
26
|
|
|
11
27
|
export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
|
|
28
|
+
const displayKey = (action: Keybinding): string => formatHotkeyMarkdownCode(key(bindings, action));
|
|
29
|
+
const displayAppKey = (action: AppKeybinding): string => formatHotkeyMarkdownCode(appKey(bindings, action));
|
|
30
|
+
const displayKeys = (...labels: string[]): string => formatHotkeyMarkdownCode(labels.join("/"));
|
|
31
|
+
const formatFixedKey = (key: string): string => bindings.keybindings.formatAccessibleKeyHint(key);
|
|
32
|
+
const displayKeyWithFixed = (action: Keybinding, fixedKey: string): string => {
|
|
33
|
+
const configured = key(bindings, action);
|
|
34
|
+
const fixed = formatFixedKey(fixedKey);
|
|
35
|
+
const labels = configured.split("/");
|
|
36
|
+
if (!labels.includes(fixed)) labels.push(fixed);
|
|
37
|
+
return displayKeys(...labels);
|
|
38
|
+
};
|
|
39
|
+
|
|
12
40
|
return [
|
|
13
41
|
"**Navigation**",
|
|
14
42
|
"| Key | Action |",
|
|
15
43
|
"|-----|--------|",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
44
|
+
`| ${displayKeys(key(bindings, "tui.editor.cursorUp"), key(bindings, "tui.editor.cursorDown"))} | Move cursor / browse history (Up when empty) |`,
|
|
45
|
+
`| ${displayKeys(key(bindings, "tui.editor.cursorWordLeft"), key(bindings, "tui.editor.cursorWordRight"))} | Move by word |`,
|
|
46
|
+
`| ${displayKeyWithFixed("tui.editor.cursorLineStart", "ctrl+a")} | Start of line |`,
|
|
47
|
+
`| ${displayKeyWithFixed("tui.editor.cursorLineEnd", "ctrl+e")} | End of line |`,
|
|
20
48
|
"",
|
|
21
49
|
"**Editing**",
|
|
22
50
|
"| Key | Action |",
|
|
23
51
|
"|-----|--------|",
|
|
24
|
-
"
|
|
25
|
-
`|
|
|
26
|
-
`|
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
`|
|
|
32
|
-
`|
|
|
52
|
+
`| ${displayKey("tui.input.submit")} | Send / queue while busy |`,
|
|
53
|
+
`| ${displayAppKey("app.message.queue")} | Queue message for next turn |`,
|
|
54
|
+
`| ${displayAppKey("app.message.dequeue")} | Select queued message to edit |`,
|
|
55
|
+
`| ${displayKeys(key(bindings, "tui.input.newLine"), formatFixedKey("ctrl+j"))} | New line |`,
|
|
56
|
+
`| ${displayKeys(formatFixedKey("ctrl+w"), formatFixedKey("alt+backspace"))} | Delete word backwards |`,
|
|
57
|
+
`| ${displayKeys(formatFixedKey("ctrl+u"))} | Delete to start of line |`,
|
|
58
|
+
`| ${displayKeys(formatFixedKey("ctrl+k"))} | Delete to end of line |`,
|
|
59
|
+
`| ${displayAppKey("app.clipboard.copyLine")} | Copy current line |`,
|
|
60
|
+
`| ${displayAppKey("app.clipboard.copyPrompt")} | Copy whole prompt |`,
|
|
33
61
|
"",
|
|
34
62
|
"**Other**",
|
|
35
63
|
"| Key | Action |",
|
|
36
64
|
"|-----|--------|",
|
|
37
|
-
"
|
|
38
|
-
`|
|
|
39
|
-
`|
|
|
40
|
-
`|
|
|
41
|
-
`|
|
|
42
|
-
`|
|
|
43
|
-
`|
|
|
44
|
-
`|
|
|
45
|
-
`|
|
|
46
|
-
`|
|
|
47
|
-
`|
|
|
48
|
-
`|
|
|
49
|
-
`|
|
|
50
|
-
`|
|
|
51
|
-
`|
|
|
52
|
-
`|
|
|
53
|
-
`|
|
|
54
|
-
`|
|
|
55
|
-
`|
|
|
56
|
-
`|
|
|
57
|
-
`|
|
|
65
|
+
`| ${displayKey("tui.input.tab")} | Path completion / accept autocomplete |`,
|
|
66
|
+
`| ${displayAppKey("app.interrupt")} | Cancel autocomplete / interrupt active work |`,
|
|
67
|
+
`| ${displayAppKey("app.clear")} | Clear editor (first) / exit (second) |`,
|
|
68
|
+
`| ${displayAppKey("app.exit")} | Exit (when editor is empty) |`,
|
|
69
|
+
`| ${displayAppKey("app.suspend")} | Suspend to background |`,
|
|
70
|
+
`| ${displayAppKey("app.commandPalette.open")} | Open command palette |`,
|
|
71
|
+
`| ${displayAppKey("app.session.new")} | Start a fresh session |`,
|
|
72
|
+
`| ${displayAppKey("app.thinking.cycle")} | Cycle thinking level |`,
|
|
73
|
+
`| ${displayAppKey("app.model.cycleForward")} | Cycle configured model roles |`,
|
|
74
|
+
`| ${displayAppKey("app.model.cycleBackward")} | Cycle configured model roles temporarily |`,
|
|
75
|
+
`| ${displayAppKey("app.model.selectTemporary")} | Select model (temporary) |`,
|
|
76
|
+
`| ${displayAppKey("app.model.select")} | Select default model |`,
|
|
77
|
+
`| ${displayAppKey("app.plan.toggle")} | Toggle plan mode |`,
|
|
78
|
+
`| ${displayAppKey("app.history.search")} | Search prompt history |`,
|
|
79
|
+
`| ${displayAppKey("app.tools.expand")} | Toggle tool output expansion |`,
|
|
80
|
+
`| ${displayAppKey("app.tool.backgroundFold")} twice | Fold supported foreground bash into a background job |`,
|
|
81
|
+
`| ${displayAppKey("app.thinking.toggle")} | Toggle thinking block visibility |`,
|
|
82
|
+
`| ${displayAppKey("app.editor.external")} | Edit message in external editor |`,
|
|
83
|
+
`| ${displayAppKey("app.clipboard.pasteImage")} | Paste image from clipboard |`,
|
|
84
|
+
`| ${displayAppKey("app.stt.toggle")} | Toggle speech-to-text recording |`,
|
|
85
|
+
`| ${displayAppKey("app.irc.sidebar.toggle")} | Toggle IRC sidebar |`,
|
|
58
86
|
"| `?` | Show help / shortcuts |",
|
|
59
87
|
"| `#` | Prompt actions (command-palette style actions) |",
|
|
60
88
|
"| `/` | Slash commands (try `/help` or `/new`) |",
|
|
@@ -916,9 +916,11 @@ export class UiHelpers {
|
|
|
916
916
|
const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
|
|
917
917
|
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
918
918
|
}
|
|
919
|
-
const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue")
|
|
920
|
-
|
|
921
|
-
|
|
919
|
+
const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue");
|
|
920
|
+
if (dequeueKey) {
|
|
921
|
+
const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
|
|
922
|
+
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
|
923
|
+
}
|
|
922
924
|
}
|
|
923
925
|
}
|
|
924
926
|
|
|
@@ -33,7 +33,6 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
|
|
|
33
33
|
- Never approve CRITICAL or HIGH severity issues.
|
|
34
34
|
- Do not skip spec compliance to jump to style nitpicks.
|
|
35
35
|
- Be constructive: explain why an issue matters and how to fix it or strengthen the design.
|
|
36
|
-
- Practical sufficiency beats endless logical completeness: treat preemptive logic for edge cases with no observed or evidenced trigger as over-engineering, but never downgrade genuine CRITICAL/HIGH architecture, extensibility, or robustness risks that have observability-based evidence.
|
|
37
36
|
</constraints>
|
|
38
37
|
|
|
39
38
|
<review_stages>
|
|
@@ -23,7 +23,6 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
|
|
|
23
23
|
- Do not invent problems; report no issues found when the plan passes.
|
|
24
24
|
- Escalate routing needs upward: planner for plan revision, the deep-interview skill for requirements gathering, architect for code analysis.
|
|
25
25
|
- For consensus planning, reject shallow alternatives, driver contradictions, vague risks, weak verification, missing acceptance criteria, or under-specified areas needing expansion before execution.
|
|
26
|
-
- Prefer practical sufficiency over endless logical completeness: do not demand preemptive logic for edge cases that are not actually observed or evidenced, yet still require extensibility and robustness when observability-based evidence shows the investment is worth the token and time.
|
|
27
26
|
</constraints>
|
|
28
27
|
|
|
29
28
|
<execution_loop>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[SUGGESTION MODE: Suggest what the user might naturally type next into the coding agent.]
|
|
2
|
+
|
|
3
|
+
FIRST: Look at the user's recent messages and original request.
|
|
4
|
+
|
|
5
|
+
Your job is to predict what THEY would type - not what you think they should do.
|
|
6
|
+
|
|
7
|
+
THE TEST: Would they think "I was just about to type that"?
|
|
8
|
+
|
|
9
|
+
EXAMPLES:
|
|
10
|
+
User asked "fix the bug and run tests", bug is fixed → "run the tests"
|
|
11
|
+
After code written → "try it out"
|
|
12
|
+
Agent offers options → suggest the one the user would likely pick, based on conversation
|
|
13
|
+
Agent asks to continue → "yes" or "go ahead"
|
|
14
|
+
Task complete, obvious follow-up → "commit this" or "push it"
|
|
15
|
+
After error or misunderstanding → silence (let them assess/correct)
|
|
16
|
+
|
|
17
|
+
Be specific: "run the tests" beats "continue".
|
|
18
|
+
|
|
19
|
+
NEVER SUGGEST:
|
|
20
|
+
- Evaluative ("looks good", "thanks")
|
|
21
|
+
- Questions ("what about…?")
|
|
22
|
+
- Agent-voice ("Let me…", "I'll…", "Here's…")
|
|
23
|
+
- New ideas they didn't ask about
|
|
24
|
+
- Multiple sentences
|
|
25
|
+
|
|
26
|
+
Stay silent if the next step isn't obvious from what the user said.
|
|
27
|
+
|
|
28
|
+
Stay silent if a suggestion could be unsafe or inappropriate — including any sensitive topic (security incidents, credentials, harm, private data). Even when the user is doing legitimate security or cybersecurity work, do not predict potentially unsafe actions.
|
|
29
|
+
|
|
30
|
+
Format: 2-12 words, match the user's style. Or nothing.
|
|
31
|
+
|
|
32
|
+
Reply with ONLY the suggestion, no quotes or explanation.
|
|
@@ -19,12 +19,16 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
|
|
|
19
19
|
{{#unless subagent}}
|
|
20
20
|
<gjc-runtime>
|
|
21
21
|
<routing>
|
|
22
|
-
- Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony.
|
|
22
|
+
- Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony. Small verification needs do not turn a clear request into a planning workflow.
|
|
23
|
+
- Ambiguous implementation asks with a missing target, scope, acceptance criteria, or safety boundary require clarification or the appropriate planning workflow before mutation.
|
|
23
24
|
- Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
|
|
24
|
-
- Vague requirements use `/skill:deep-interview
|
|
25
|
+
- Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
|
|
26
|
+
- Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
|
|
27
|
+
- Deep-interview state is runtime-owned: diagnose with `gjc deep-interview sanity-check`, inspect only the affected bounded selector, and repair through typed operations using CLI-generated/edited drafts (`draft create|edit|show|check|rebase|discard`) consumed with `--draft-id`. Inline JSON request flags are compatibility-only; normal flow must never reconstruct a payload or full envelope.
|
|
25
28
|
- Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
|
|
26
29
|
- Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
|
|
27
|
-
- Active skills are authoritative:
|
|
30
|
+
- Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
|
|
31
|
+
- Before explicit execution approval, planning and interview workflows NEVER edit product source, run mutating shell commands, commit, push, open PRs, or delegate implementation.
|
|
28
32
|
</routing>
|
|
29
33
|
</gjc-runtime>
|
|
30
34
|
{{/unless}}
|
|
@@ -206,7 +210,6 @@ Guardrails? None. Restrictions? None. Freedom? Unlimited.
|
|
|
206
210
|
- **Do your homework first** - Read the files, search, and understand the context before asking
|
|
207
211
|
- **Don’t Stop at Analysis** - Once you’ve assessed the situation, take the next step (session, comment, merge, fix, cleanup) on your own
|
|
208
212
|
- **No Inflating Operational/Meta Issues** - Just because the backlog is empty doesn’t mean you should force operational follow-ups, verifiability checks, or meta issues. Only raise issues for actual repository problems; otherwise, stop.
|
|
209
|
-
- **Practical sufficiency over completeness** - Don’t add logic that preemptively handles edge cases you haven’t actually observed; endless logical completeness loses to practical sufficiency. Extensibility and robustness matter, but they must be observability-based and evidence-based—any engineering without underlying evidence that it’s worth the token and time is over-engineering.
|
|
210
213
|
- **Always maintain execution** - Do not end your turn with just reporting, summarizing, or analyzing. If an action is possible, execute it immediately during the same turn.
|
|
211
214
|
- **Always be in action mode** - The default stance is intervention, not observation. If you see something that needs cleaning up, clean it up; if you see a session that needs opening, open it; if you see a PR that needs blocking, block it; if you see a PR that needs merging, review it and merge it.
|
|
212
215
|
- **No begging for permission** - For obvious follow-up actions (cleaning up sessions, marking items as stale, fixing routing, updating documentation to reflect Doctrine), don’t ask—just do it.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks
|
|
1
|
+
Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks re-run an agent prompt automatically on an interval when every firing is allowed to produce a normal assistant response, such as a reminder or a scheduled status report.
|
|
2
|
+
|
|
3
|
+
Cron is not a silent polling primitive. Every firing starts an agent turn, and hiding the injected cron message does not hide that turn's assistant response. For ongoing logs, file watching, or PR/CI polling that should report only meaningful state changes, use `monitor` with a script that writes a line only when there is an event to process, and set `persistent: true` so the monitor survives the first emitted event. Do not schedule a cron prompt that asks the agent to suppress routine polls; prompt wording cannot make a cron-triggered assistant turn reliably silent.
|
|
2
4
|
|
|
3
5
|
Use a single `op` field to select the operation:
|
|
4
6
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Lightweight utilities for calling MCP servers directly via HTTP
|
|
5
5
|
* without maintaining persistent connections.
|
|
6
6
|
*/
|
|
7
|
-
// biome-ignore assist/source/organizeImports: Keep independent MCP security imports on separate merge anchors.
|
|
8
|
-
import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
|
|
9
7
|
import { logger } from "@gajae-code/utils";
|
|
8
|
+
import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
|
|
9
|
+
import { redactMCPDiagnosticValue, redactMCPEndpoint } from "./redaction";
|
|
10
10
|
|
|
11
11
|
/** Parse SSE response format (lines starting with "data: ") */
|
|
12
12
|
export function parseSSE(text: string): unknown {
|
|
@@ -82,7 +82,7 @@ export async function callMCP<T = unknown>(
|
|
|
82
82
|
if (!response.ok) {
|
|
83
83
|
cancelMCPStream(response.body);
|
|
84
84
|
const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
|
85
|
-
logger.error(errorMsg, { url, method, params });
|
|
85
|
+
logger.error(errorMsg, { url: redactMCPEndpoint(url), method, params: redactMCPDiagnosticValue(params) });
|
|
86
86
|
throw new Error(errorMsg);
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -90,7 +90,10 @@ export async function callMCP<T = unknown>(
|
|
|
90
90
|
const result = parseSSE(text) as JsonRpcResponse<T> | null;
|
|
91
91
|
|
|
92
92
|
if (!result) {
|
|
93
|
-
logger.error("Failed to parse MCP response", {
|
|
93
|
+
logger.error("Failed to parse MCP response", {
|
|
94
|
+
url: redactMCPEndpoint(url),
|
|
95
|
+
method,
|
|
96
|
+
});
|
|
94
97
|
throw new Error("Failed to parse MCP response");
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as url from "node:url";
|
|
9
|
-
import type
|
|
9
|
+
import { isCanonicalMCPOAuthBinding, resolveMCPOAuthResourceOrigin, type TSchema } from "@gajae-code/ai";
|
|
10
10
|
import { logger } from "@gajae-code/utils";
|
|
11
11
|
import type { SourceMeta } from "../capability/types";
|
|
12
12
|
import * as configValue from "../config/resolve-config-value";
|
|
13
13
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
14
|
-
import type { AuthStorage } from "../session/auth-storage";
|
|
14
|
+
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
15
15
|
import {
|
|
16
16
|
connectToServer,
|
|
17
17
|
disconnectServer,
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
unsubscribeFromResources,
|
|
28
28
|
} from "./client";
|
|
29
29
|
import { loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
30
|
-
import { refreshMCPOAuthToken } from "./oauth-flow";
|
|
31
30
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
32
31
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
33
32
|
import type { MCPToolCache } from "./tool-cache";
|
|
@@ -1469,64 +1468,63 @@ export class MCPManager {
|
|
|
1469
1468
|
let resolved: MCPServerConfig = { ...config };
|
|
1470
1469
|
|
|
1471
1470
|
const auth = config.auth;
|
|
1472
|
-
if (auth?.type === "oauth"
|
|
1471
|
+
if (auth?.type === "oauth") {
|
|
1472
|
+
if (!auth.credentialId || !this.#authStorage || (config.type !== "http" && config.type !== "sse")) {
|
|
1473
|
+
throw new MCPExpectedFailure();
|
|
1474
|
+
}
|
|
1473
1475
|
const credentialId = auth.credentialId;
|
|
1474
1476
|
let credential = this.#authStorage.get(credentialId);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1477
|
+
const binding = credential?.type === "oauth" ? credential.mcpBinding : undefined;
|
|
1478
|
+
const endpointOrigin = resolveMCPOAuthResourceOrigin(config.url);
|
|
1479
|
+
if (
|
|
1480
|
+
credential?.type !== "oauth" ||
|
|
1481
|
+
!binding ||
|
|
1482
|
+
!isCanonicalMCPOAuthBinding(binding) ||
|
|
1483
|
+
binding.resourceOrigin !== endpointOrigin ||
|
|
1484
|
+
(auth.tokenUrl !== undefined && auth.tokenUrl !== binding.tokenEndpoint)
|
|
1485
|
+
) {
|
|
1486
|
+
throw new MCPExpectedFailure();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Proactive refresh: 5-minute buffer before expiry
|
|
1490
|
+
// Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
|
|
1491
|
+
const REFRESH_BUFFER_MS = 5 * 60_000;
|
|
1492
|
+
const shouldRefresh =
|
|
1493
|
+
forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
|
|
1494
|
+
if (shouldRefresh && credential.refresh) {
|
|
1495
|
+
let refreshedCredential: OAuthCredential | undefined;
|
|
1496
|
+
try {
|
|
1497
|
+
refreshedCredential = await this.#authStorage.forceRefreshOAuthCredential(credentialId, credential, {
|
|
1498
|
+
clientId: auth.clientId,
|
|
1499
|
+
clientSecret: auth.clientSecret,
|
|
1500
|
+
});
|
|
1501
|
+
} catch {
|
|
1502
|
+
if (forceRefresh) throw new MCPExpectedFailure();
|
|
1503
|
+
if (this.#toolsOnly) {
|
|
1504
|
+
logger.debug("MCP OAuth refresh failed");
|
|
1505
|
+
} else {
|
|
1506
|
+
logger.warn("MCP OAuth refresh failed, using existing token");
|
|
1505
1507
|
}
|
|
1506
1508
|
}
|
|
1507
1509
|
if (refreshedCredential) {
|
|
1508
|
-
|
|
1510
|
+
const refreshedBinding = refreshedCredential.mcpBinding;
|
|
1511
|
+
if (
|
|
1512
|
+
!refreshedBinding ||
|
|
1513
|
+
refreshedBinding.resourceOrigin !== binding.resourceOrigin ||
|
|
1514
|
+
refreshedBinding.tokenEndpoint !== binding.tokenEndpoint
|
|
1515
|
+
) {
|
|
1516
|
+
throw new MCPExpectedFailure();
|
|
1517
|
+
}
|
|
1509
1518
|
credential = refreshedCredential;
|
|
1510
1519
|
}
|
|
1511
|
-
|
|
1512
|
-
if (resolved.type === "http" || resolved.type === "sse") {
|
|
1513
|
-
resolved = {
|
|
1514
|
-
...resolved,
|
|
1515
|
-
headers: {
|
|
1516
|
-
...resolved.headers,
|
|
1517
|
-
Authorization: `Bearer ${credential.access}`,
|
|
1518
|
-
},
|
|
1519
|
-
};
|
|
1520
|
-
} else {
|
|
1521
|
-
resolved = {
|
|
1522
|
-
...resolved,
|
|
1523
|
-
env: {
|
|
1524
|
-
...resolved.env,
|
|
1525
|
-
OAUTH_ACCESS_TOKEN: credential.access,
|
|
1526
|
-
},
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
1520
|
}
|
|
1521
|
+
resolved = {
|
|
1522
|
+
...config,
|
|
1523
|
+
headers: {
|
|
1524
|
+
...config.headers,
|
|
1525
|
+
Authorization: `Bearer ${credential.access}`,
|
|
1526
|
+
},
|
|
1527
|
+
};
|
|
1530
1528
|
}
|
|
1531
1529
|
if (this.#toolsOnly && resolved.type === "stdio") {
|
|
1532
1530
|
resolved = { ...resolved, noInheritEnv: true };
|
|
@@ -8,29 +8,10 @@
|
|
|
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 { MCPExpectedFailure } from "./types";
|
|
12
11
|
|
|
13
12
|
const DEFAULT_PORT = 3000;
|
|
14
13
|
const CALLBACK_PATH = "/callback";
|
|
15
14
|
const CALLBACK_BIND_HOSTNAME = "127.0.0.1";
|
|
16
|
-
type RefreshTokenResponse = {
|
|
17
|
-
access_token: string;
|
|
18
|
-
refresh_token?: string;
|
|
19
|
-
expires_in?: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function isRefreshTokenResponse(value: unknown): value is RefreshTokenResponse {
|
|
23
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
24
|
-
const payload = value as Record<string, unknown>;
|
|
25
|
-
return (
|
|
26
|
-
typeof payload.access_token === "string" &&
|
|
27
|
-
payload.access_token.length > 0 &&
|
|
28
|
-
(payload.refresh_token === undefined || typeof payload.refresh_token === "string") &&
|
|
29
|
-
(payload.expires_in === undefined ||
|
|
30
|
-
(typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in >= 0))
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
15
|
function isLoopbackHostname(hostname: string): boolean {
|
|
35
16
|
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
36
17
|
}
|
|
@@ -384,56 +365,3 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
384
365
|
}
|
|
385
366
|
}
|
|
386
367
|
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Refresh an MCP OAuth token using the standard refresh_token grant.
|
|
390
|
-
* Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
|
|
391
|
-
*/
|
|
392
|
-
export async function refreshMCPOAuthToken(
|
|
393
|
-
tokenUrl: string,
|
|
394
|
-
refreshToken: string,
|
|
395
|
-
clientId?: string,
|
|
396
|
-
clientSecret?: string,
|
|
397
|
-
): Promise<OAuthCredentials> {
|
|
398
|
-
const params = new URLSearchParams({
|
|
399
|
-
grant_type: "refresh_token",
|
|
400
|
-
refresh_token: refreshToken,
|
|
401
|
-
});
|
|
402
|
-
if (clientId) params.set("client_id", clientId);
|
|
403
|
-
if (clientSecret) params.set("client_secret", clientSecret);
|
|
404
|
-
|
|
405
|
-
let response: Response;
|
|
406
|
-
try {
|
|
407
|
-
response = await fetch(tokenUrl, {
|
|
408
|
-
method: "POST",
|
|
409
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
410
|
-
body: params.toString(),
|
|
411
|
-
});
|
|
412
|
-
} catch (error) {
|
|
413
|
-
throw new MCPExpectedFailure(error);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (!response.ok) {
|
|
417
|
-
let text: string;
|
|
418
|
-
try {
|
|
419
|
-
text = await response.text();
|
|
420
|
-
} catch (error) {
|
|
421
|
-
throw new MCPExpectedFailure(error);
|
|
422
|
-
}
|
|
423
|
-
throw new MCPExpectedFailure(new Error(`MCP OAuth refresh failed: ${response.status} ${text}`));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
let data: unknown;
|
|
427
|
-
try {
|
|
428
|
-
data = await response.json();
|
|
429
|
-
} catch {
|
|
430
|
-
throw new MCPExpectedFailure();
|
|
431
|
-
}
|
|
432
|
-
if (!isRefreshTokenResponse(data)) throw new MCPExpectedFailure();
|
|
433
|
-
const expiresIn = data.expires_in ?? 3600;
|
|
434
|
-
return {
|
|
435
|
-
access: data.access_token,
|
|
436
|
-
refresh: data.refresh_token ?? refreshToken,
|
|
437
|
-
expires: Date.now() + expiresIn * 1000,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
@@ -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
|
+
}
|