@gajae-code/coding-agent 0.18.11 → 0.18.12
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 +293 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -5,7 +5,7 @@ import type { CompactionOutcome } from "@gajae-code/agent-core/compaction";
|
|
|
5
5
|
import { type Model, modelsAreEqual } from "@gajae-code/ai/core";
|
|
6
6
|
import { Container, type KeyId, Markdown, Spacer, Text } from "@gajae-code/tui";
|
|
7
7
|
import { isEnoent, prompt } from "@gajae-code/utils";
|
|
8
|
-
import { resolveLocalUrlToPath } from "../../internal-urls";
|
|
8
|
+
import { resolveLocalRoot, resolveLocalUrlToPath } from "../../internal-urls";
|
|
9
9
|
import { humanizePlanTitle, type PlanApprovalDetails, resolvePlanTitle } from "../../plan-mode/approved-plan";
|
|
10
10
|
import planModeApprovedPrompt from "../../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
11
11
|
import planModeCompactInstructionsPrompt from "../../prompts/system/plan-mode-compact-instructions.md" with {
|
|
@@ -13,6 +13,7 @@ import planModeCompactInstructionsPrompt from "../../prompts/system/plan-mode-co
|
|
|
13
13
|
};
|
|
14
14
|
import type { AgentSession, TemporaryProviderSessionScope } from "../../session/agent-session";
|
|
15
15
|
import type { SessionContext, SessionManager } from "../../session/session-manager";
|
|
16
|
+
import { writeFileAtomically } from "../../tools/atomic-file-write";
|
|
16
17
|
import { normalizeLocalScheme } from "../../tools/path-utils";
|
|
17
18
|
import { type ResolveToolDetails, runResolveInvocation } from "../../tools/resolve";
|
|
18
19
|
import { ToolError } from "../../tools/tool-errors";
|
|
@@ -338,13 +339,16 @@ export class PlanModeController {
|
|
|
338
339
|
throw error;
|
|
339
340
|
}
|
|
340
341
|
}
|
|
342
|
+
#localProtocolOptions() {
|
|
343
|
+
return {
|
|
344
|
+
getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
|
|
345
|
+
isManagedDestination: () => this.ctx.sessionManager.isManagedDestination(),
|
|
346
|
+
getSessionId: () => this.ctx.sessionManager.getSessionId(),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
341
349
|
#resolvePath(planFilePath: string): string {
|
|
342
350
|
return planFilePath.startsWith("local:")
|
|
343
|
-
? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath),
|
|
344
|
-
getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
|
|
345
|
-
isManagedDestination: () => this.ctx.sessionManager.isManagedDestination(),
|
|
346
|
-
getSessionId: () => this.ctx.sessionManager.getSessionId(),
|
|
347
|
-
})
|
|
351
|
+
? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), this.#localProtocolOptions())
|
|
348
352
|
: path.resolve(this.ctx.sessionManager.getCwd(), planFilePath);
|
|
349
353
|
}
|
|
350
354
|
async #finalizeApprovedPlan(planContent: string, planFilePath: string, finalPlanFilePath: string): Promise<void> {
|
|
@@ -420,7 +424,13 @@ export class PlanModeController {
|
|
|
420
424
|
trimTrailingNewline: false,
|
|
421
425
|
});
|
|
422
426
|
if (result !== null) {
|
|
423
|
-
|
|
427
|
+
// Lexical local:// checks do not see a symlink. Bind this save to the
|
|
428
|
+
// same local root so a link inside it cannot publish outside.
|
|
429
|
+
if (planFilePath.startsWith("local:")) {
|
|
430
|
+
await writeFileAtomically(resolved, result, {
|
|
431
|
+
trustBoundary: path.resolve(resolveLocalRoot(this.#localProtocolOptions())),
|
|
432
|
+
});
|
|
433
|
+
} else await Bun.write(resolved, result);
|
|
424
434
|
this.ctx.showStatus("Plan updated in external editor.");
|
|
425
435
|
}
|
|
426
436
|
return result;
|
|
@@ -107,7 +107,10 @@ import { ensureTelegramDaemonRunningDetailed, resolveTelegramSetupPreflight } fr
|
|
|
107
107
|
import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control";
|
|
108
108
|
import { runTelegramSetup, type TelegramSetupPreflight } from "../../sdk/bus/telegram-setup";
|
|
109
109
|
import { clearPersistentPinForRemovedRows } from "../../session/account-inventory";
|
|
110
|
-
import type {
|
|
110
|
+
import type {
|
|
111
|
+
AgentSessionProfileInstalledOverrideState,
|
|
112
|
+
DefaultFallbackRuntimeState,
|
|
113
|
+
} from "../../session/agent-session";
|
|
111
114
|
import { CREDENTIAL_STORE_UNREADABLE_MESSAGE } from "../../session/credential-store-errors";
|
|
112
115
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
113
116
|
import { getTreeForInternalRead } from "../../session/session-manager-internal";
|
|
@@ -1214,6 +1217,7 @@ interface DefaultAssignmentRollbackSnapshot {
|
|
|
1214
1217
|
profileOverride: SettingValue<"modelProfile.default"> | undefined;
|
|
1215
1218
|
chain: { entries: readonly string[]; origin: string; identity?: string; explicitHead?: boolean } | undefined;
|
|
1216
1219
|
activeProfile: string | undefined;
|
|
1220
|
+
profileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
|
|
1217
1221
|
canonicalVariant: string | undefined;
|
|
1218
1222
|
resumeDefaultSelector: string | undefined;
|
|
1219
1223
|
fallbackRuntimeState: DefaultFallbackRuntimeState;
|
|
@@ -1248,6 +1252,10 @@ function rawAutoroutingState(current: RawSettings): {
|
|
|
1248
1252
|
|
|
1249
1253
|
export class SelectorController {
|
|
1250
1254
|
#smartRoutingInFlight?: Promise<unknown>;
|
|
1255
|
+
#modelSelector?: ModelSelectorComponent;
|
|
1256
|
+
#settingsSelector?: SettingsSelectorComponent;
|
|
1257
|
+
#statusLinePreview?: Partial<StatusLineSettings>;
|
|
1258
|
+
#statusLinePreviewActive = false;
|
|
1251
1259
|
#transcriptViewerOpen = false;
|
|
1252
1260
|
#transcriptViewer?: TranscriptViewerOverlay;
|
|
1253
1261
|
#sessionsDashboardOpen = false;
|
|
@@ -1277,6 +1285,7 @@ export class SelectorController {
|
|
|
1277
1285
|
profileOverride: this.ctx.settings.getOverride("modelProfile.default"),
|
|
1278
1286
|
chain: this.ctx.session.getConfiguredModelChainState("default"),
|
|
1279
1287
|
activeProfile: this.ctx.session.getActiveModelProfile?.(),
|
|
1288
|
+
profileInstalledOverrideState: this.ctx.session.getProfileInstalledOverrideState?.(),
|
|
1280
1289
|
canonicalVariant: this.ctx.session.modelRegistry.getSessionCanonicalVariant?.(this.ctx.session.sessionId),
|
|
1281
1290
|
resumeDefaultSelector: this.ctx.session.sessionManager.buildSessionContext().models.default,
|
|
1282
1291
|
fallbackRuntimeState: this.ctx.session.getDefaultFallbackRuntimeState(),
|
|
@@ -1354,6 +1363,11 @@ export class SelectorController {
|
|
|
1354
1363
|
restore(() => this.ctx.session.recordResumeDefaultModel(snapshot.resumeDefaultSelector));
|
|
1355
1364
|
}
|
|
1356
1365
|
restore(() => this.ctx.session.setActiveModelProfile?.(snapshot.activeProfile));
|
|
1366
|
+
if (snapshot.profileInstalledOverrideState) {
|
|
1367
|
+
restore(() =>
|
|
1368
|
+
this.ctx.session.restoreProfileInstalledOverrideState?.(snapshot.profileInstalledOverrideState!),
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1357
1371
|
try {
|
|
1358
1372
|
await this.ctx.settings.flushOrThrow();
|
|
1359
1373
|
} catch (rollbackError) {
|
|
@@ -1401,7 +1415,15 @@ export class SelectorController {
|
|
|
1401
1415
|
*/
|
|
1402
1416
|
showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
|
|
1403
1417
|
if (this.ctx.isStopped?.()) return;
|
|
1418
|
+
this.#modelSelector = undefined;
|
|
1419
|
+
this.#settingsSelector = undefined;
|
|
1420
|
+
this.#statusLinePreview = undefined;
|
|
1421
|
+
this.#statusLinePreviewActive = false;
|
|
1404
1422
|
const done = () => {
|
|
1423
|
+
this.#modelSelector = undefined;
|
|
1424
|
+
this.#settingsSelector = undefined;
|
|
1425
|
+
this.#statusLinePreview = undefined;
|
|
1426
|
+
this.#statusLinePreviewActive = false;
|
|
1405
1427
|
if (this.ctx.isStopped?.()) return;
|
|
1406
1428
|
// Prefer the pet-aware composer restore (InteractiveMode.restoreComposer); fall back
|
|
1407
1429
|
// to a plain editor swap for contexts that predate it (e.g. lightweight test doubles).
|
|
@@ -1424,6 +1446,28 @@ export class SelectorController {
|
|
|
1424
1446
|
this.ctx.ui.requestRender();
|
|
1425
1447
|
}
|
|
1426
1448
|
|
|
1449
|
+
/** Refresh accepted disk configuration without discarding an open preview. */
|
|
1450
|
+
refreshConfiguration(): void {
|
|
1451
|
+
if (this.ctx.isStopped?.()) return;
|
|
1452
|
+
this.#applyStatusLineSettings();
|
|
1453
|
+
this.#modelSelector?.refreshRoleAssignments({
|
|
1454
|
+
currentModel: this.ctx.session.model,
|
|
1455
|
+
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
1456
|
+
activeModelProfile: this.ctx.session.getActiveModelProfile(),
|
|
1457
|
+
});
|
|
1458
|
+
this.#modelSelector?.refreshPresetProfiles();
|
|
1459
|
+
this.#settingsSelector?.refreshFromConfiguration([...this.ctx.session.modelRegistry.getModelProfiles().keys()]);
|
|
1460
|
+
this.ctx.updateEditorTopBorder();
|
|
1461
|
+
this.ctx.ui.requestRender();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
#applyStatusLineSettings(): void {
|
|
1465
|
+
this.ctx.statusLine.updateSettings({
|
|
1466
|
+
...buildStatusLineSettings(this.ctx.settings),
|
|
1467
|
+
...(this.#statusLinePreviewActive ? this.#statusLinePreview : undefined),
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1427
1471
|
showCommandPalette(
|
|
1428
1472
|
commands: SlashCommand[],
|
|
1429
1473
|
actions: CommandPaletteAction[],
|
|
@@ -1990,11 +2034,19 @@ export class SelectorController {
|
|
|
1990
2034
|
},
|
|
1991
2035
|
onPetCommit: mode => this.ctx.commitPetPreviewMode(mode as PetMode),
|
|
1992
2036
|
onStatusLinePreview: previewSettings => {
|
|
1993
|
-
|
|
1994
|
-
this
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2037
|
+
if (this.#statusLinePreviewActive) this.#statusLinePreview = previewSettings;
|
|
2038
|
+
this.#applyStatusLineSettings();
|
|
2039
|
+
this.ctx.updateEditorTopBorder();
|
|
2040
|
+
this.ctx.ui.requestRender();
|
|
2041
|
+
},
|
|
2042
|
+
onStatusLinePreviewStart: () => {
|
|
2043
|
+
this.#statusLinePreviewActive = true;
|
|
2044
|
+
this.#statusLinePreview = undefined;
|
|
2045
|
+
},
|
|
2046
|
+
onStatusLinePreviewEnd: () => {
|
|
2047
|
+
this.#statusLinePreviewActive = false;
|
|
2048
|
+
this.#statusLinePreview = undefined;
|
|
2049
|
+
this.#applyStatusLineSettings();
|
|
1998
2050
|
this.ctx.updateEditorTopBorder();
|
|
1999
2051
|
this.ctx.ui.requestRender();
|
|
2000
2052
|
},
|
|
@@ -2068,6 +2120,7 @@ export class SelectorController {
|
|
|
2068
2120
|
},
|
|
2069
2121
|
notificationsOperations,
|
|
2070
2122
|
);
|
|
2123
|
+
this.#settingsSelector = selector;
|
|
2071
2124
|
return { component: selector, focus: selector };
|
|
2072
2125
|
});
|
|
2073
2126
|
});
|
|
@@ -2849,6 +2902,7 @@ export class SelectorController {
|
|
|
2849
2902
|
if (!materializedProfile) {
|
|
2850
2903
|
for (const targetRole of targetRoles) {
|
|
2851
2904
|
const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetRole];
|
|
2905
|
+
this.ctx.session.markProfileRoleOverrideManual?.(target.settingsPath, targetRole);
|
|
2852
2906
|
if (target.settingsPath === "modelRoles") {
|
|
2853
2907
|
this.ctx.settings.setModelRole(targetRole, value);
|
|
2854
2908
|
} else {
|
|
@@ -2952,6 +3006,7 @@ export class SelectorController {
|
|
|
2952
3006
|
});
|
|
2953
3007
|
if (!materializedProfile) {
|
|
2954
3008
|
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
3009
|
+
this.ctx.session.markProfileRoleOverrideManual?.(target.settingsPath, role);
|
|
2955
3010
|
if (target.settingsPath === "modelRoles") {
|
|
2956
3011
|
this.ctx.settings.setModelRole(role, value);
|
|
2957
3012
|
} else {
|
|
@@ -2996,6 +3051,7 @@ export class SelectorController {
|
|
|
2996
3051
|
smartRoutingOnly: options?.smartRoutingOnly,
|
|
2997
3052
|
},
|
|
2998
3053
|
);
|
|
3054
|
+
this.#modelSelector = modelSelector;
|
|
2999
3055
|
return { component: modelSelector, focus: modelSelector };
|
|
3000
3056
|
});
|
|
3001
3057
|
}
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { APP_NAME, adjustHsv, getAgentDir, getProjectDir, logger, postmortem, sanitizeText } from "@gajae-code/utils";
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { AsyncJobManager } from "../async";
|
|
19
|
+
import { ConfigHotReloadWatcher } from "../config/config-hot-reload";
|
|
20
|
+
import { isConfigHotReloadSupported, isConfigHotReloadTrusted } from "../config/config-hot-reload-trust";
|
|
19
21
|
import {
|
|
20
22
|
type AppKeybinding,
|
|
21
23
|
defaultMessageQueueKeysForPlatform,
|
|
@@ -573,6 +575,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
573
575
|
// paint, which renders the full session anyway.
|
|
574
576
|
#initialTranscriptPainted = false;
|
|
575
577
|
#stopListeners = new Set<() => void>();
|
|
578
|
+
#configHotReload?: ConfigHotReloadWatcher;
|
|
576
579
|
#eventBus?: EventBus;
|
|
577
580
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
578
581
|
#welcomeComponent?: WelcomeComponent;
|
|
@@ -999,6 +1002,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
999
1002
|
this.updateEditorChrome();
|
|
1000
1003
|
this.#syncEditorMaxHeight();
|
|
1001
1004
|
this.isInitialized = true;
|
|
1005
|
+
await this.#startConfigHotReload();
|
|
1006
|
+
if (this.#stopped) return;
|
|
1002
1007
|
this.syncActivityIndicator();
|
|
1003
1008
|
if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
|
|
1004
1009
|
this.#syncIrcSidebarAvailabilityFromSettings();
|
|
@@ -1691,6 +1696,65 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1691
1696
|
return this.#stopped;
|
|
1692
1697
|
}
|
|
1693
1698
|
|
|
1699
|
+
async #startConfigHotReload(): Promise<void> {
|
|
1700
|
+
const paths = this.session.getConfigurationPaths();
|
|
1701
|
+
if (!paths) return;
|
|
1702
|
+
if (!isConfigHotReloadSupported()) {
|
|
1703
|
+
logger.debug("Configuration hot reload is unsupported on this platform", { platform: process.platform });
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const trusted = await isConfigHotReloadTrusted(paths);
|
|
1707
|
+
if (this.#stopped || this.#isShuttingDown) return;
|
|
1708
|
+
if (!trusted) {
|
|
1709
|
+
this.showWarning(
|
|
1710
|
+
"Configuration watching could not start because directory ownership and privacy could not be verified.",
|
|
1711
|
+
);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const assertTrustedPaths = async (): Promise<void> => {
|
|
1715
|
+
if (await isConfigHotReloadTrusted(paths)) return;
|
|
1716
|
+
const error = new Error("Configuration directory ownership and privacy could not be verified");
|
|
1717
|
+
Object.assign(error, { code: "EACCES" });
|
|
1718
|
+
throw error;
|
|
1719
|
+
};
|
|
1720
|
+
const watcher = new ConfigHotReloadWatcher({
|
|
1721
|
+
onValidate: async candidate => {
|
|
1722
|
+
await assertTrustedPaths();
|
|
1723
|
+
await this.session.validateConfiguration(candidate);
|
|
1724
|
+
},
|
|
1725
|
+
onCandidate: async (candidate, signal) => {
|
|
1726
|
+
if (signal.aborted || this.#stopped || this.#isShuttingDown) return;
|
|
1727
|
+
await assertTrustedPaths();
|
|
1728
|
+
if (signal.aborted || this.#stopped || this.#isShuttingDown) return;
|
|
1729
|
+
const result = await this.session.reloadConfiguration(candidate, signal);
|
|
1730
|
+
if (signal.aborted || this.#stopped || this.#isShuttingDown || !result.applied) return;
|
|
1731
|
+
this.#selectorController.refreshConfiguration();
|
|
1732
|
+
this.#syncEditorMaxHeight();
|
|
1733
|
+
this.#syncIrcSidebarAvailabilityFromSettings();
|
|
1734
|
+
this.updateEditorChrome();
|
|
1735
|
+
this.showStatus(
|
|
1736
|
+
result.settingsChanged
|
|
1737
|
+
? "Configuration reloaded. Startup-only services require a restart."
|
|
1738
|
+
: "Model configuration reloaded.",
|
|
1739
|
+
);
|
|
1740
|
+
this.ui.requestRender();
|
|
1741
|
+
},
|
|
1742
|
+
onError: error => {
|
|
1743
|
+
if (!this.#stopped && !this.#isShuttingDown) {
|
|
1744
|
+
this.showWarning(`${error.message}. Repair the configuration file and save again.`);
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
});
|
|
1748
|
+
this.#configHotReload = watcher;
|
|
1749
|
+
try {
|
|
1750
|
+
await watcher.start(paths);
|
|
1751
|
+
} catch {
|
|
1752
|
+
watcher.dispose();
|
|
1753
|
+
this.#configHotReload = undefined;
|
|
1754
|
+
this.showWarning("Configuration watching could not start. Restart after repairing file access.");
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1694
1758
|
onStop(callback: () => void): () => void {
|
|
1695
1759
|
if (this.#stopped) {
|
|
1696
1760
|
callback();
|
|
@@ -1703,6 +1767,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1703
1767
|
stop(): void {
|
|
1704
1768
|
const wasInitialized = this.isInitialized;
|
|
1705
1769
|
this.#stopped = true;
|
|
1770
|
+
this.#configHotReload?.dispose();
|
|
1771
|
+
this.#configHotReload = undefined;
|
|
1706
1772
|
this.ui.setRenderPreparationLifecycleCallbacks(undefined);
|
|
1707
1773
|
this.#inputController.discardDeferredSubmission();
|
|
1708
1774
|
for (const listener of this.#stopListeners) {
|
|
@@ -1776,6 +1842,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1776
1842
|
return;
|
|
1777
1843
|
}
|
|
1778
1844
|
this.#isShuttingDown = true;
|
|
1845
|
+
this.#configHotReload?.dispose();
|
|
1846
|
+
this.#configHotReload = undefined;
|
|
1779
1847
|
|
|
1780
1848
|
// `/btw` owns the shared composer while its panel is open. Never persist a
|
|
1781
1849
|
// side-chat draft or pending side-chat images into the main-session draft.
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -1260,9 +1260,11 @@ const langMap: Record<string, SymbolKey> = {
|
|
|
1260
1260
|
};
|
|
1261
1261
|
|
|
1262
1262
|
export class Theme {
|
|
1263
|
+
#svgColorValues: Record<string, string | number>;
|
|
1263
1264
|
#fgColors: Record<ThemeColor, string>;
|
|
1264
1265
|
#bgColors: Record<ThemeBg, string>;
|
|
1265
1266
|
#symbols: SymbolMap;
|
|
1267
|
+
#svgPalette: Readonly<Record<string, string>>;
|
|
1266
1268
|
|
|
1267
1269
|
constructor(
|
|
1268
1270
|
fgColors: Record<ThemeColor, string | number>,
|
|
@@ -1271,10 +1273,19 @@ export class Theme {
|
|
|
1271
1273
|
private readonly symbolPreset: SymbolPreset,
|
|
1272
1274
|
symbolOverrides: Partial<Record<SymbolKey, string>>,
|
|
1273
1275
|
) {
|
|
1276
|
+
this.#svgColorValues = { ...fgColors, ...bgColors };
|
|
1274
1277
|
this.#fgColors = {} as Record<ThemeColor, string>;
|
|
1275
1278
|
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
|
1276
1279
|
this.#fgColors[key] = fgAnsi(value, mode);
|
|
1277
1280
|
}
|
|
1281
|
+
const textColor = this.#svgColorValues.text;
|
|
1282
|
+
const foreground = typeof textColor === "number" ? ansi256ToHex(textColor) : textColor || "#e5e5e7";
|
|
1283
|
+
const svgPalette: Record<string, string> = { fg: foreground };
|
|
1284
|
+
for (const color of THEME_COLOR_KEYS) {
|
|
1285
|
+
const value = this.#svgColorValues[color];
|
|
1286
|
+
svgPalette[color] = typeof value === "number" ? ansi256ToHex(value) : value || foreground;
|
|
1287
|
+
}
|
|
1288
|
+
this.#svgPalette = Object.freeze(svgPalette);
|
|
1278
1289
|
this.#bgColors = {} as Record<ThemeBg, string>;
|
|
1279
1290
|
for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
|
|
1280
1291
|
this.#bgColors[key] = bgAnsi(value, mode);
|
|
@@ -1329,6 +1340,10 @@ export class Theme {
|
|
|
1329
1340
|
return ansi;
|
|
1330
1341
|
}
|
|
1331
1342
|
|
|
1343
|
+
getSvgPalette(): Readonly<Record<string, string>> {
|
|
1344
|
+
return this.#svgPalette;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1332
1347
|
getBgAnsi(color: ThemeBg): string {
|
|
1333
1348
|
const ansi = this.#bgColors[color];
|
|
1334
1349
|
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
|
@@ -15,7 +15,7 @@ Use for launching or attaching debuggers, setting breakpoints, stepping through
|
|
|
15
15
|
<caution>
|
|
16
16
|
- Only one active debug session is supported at a time.
|
|
17
17
|
- Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.
|
|
18
|
-
- Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -m debugpy.adapter`, `dlv dap`.
|
|
18
|
+
- Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -I -m debugpy.adapter`, `dlv dap`.
|
|
19
19
|
- `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.
|
|
20
20
|
</caution>
|
|
21
21
|
|
|
@@ -73,7 +73,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
73
73
|
|
|
74
74
|
`agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
|
|
75
75
|
|
|
76
|
-
Bundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments
|
|
76
|
+
Bundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/…` identifiers.
|
|
77
77
|
|
|
78
78
|
<critical>
|
|
79
79
|
- Always include `path`; never call `read` with `{}`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Records a tool that behaved inconsistently with its own description, for local quality tracking.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- Call this when a tool's output contradicts what its description or parameters promise: ignored or misapplied parameters, wrong or truncated results, misleading errors, silent no-ops.
|
|
5
|
+
- `tool` is the misbehaving tool's name; `report` states what you called, what you expected, and what actually happened, in one or two sentences.
|
|
6
|
+
- Do not report your own mistakes, user-caused failures, or expected errors (missing files, failing tests, network outages).
|
|
7
|
+
- Do not paste secrets, tokens, credentials, or raw tool output into report; summarize it instead.
|
|
8
|
+
- False positives are acceptable. Report once per distinct problem, then continue the task; this never fixes or retries anything.
|
|
9
|
+
</instruction>
|
|
10
|
+
|
|
11
|
+
<output>
|
|
12
|
+
Confirms whether the report was recorded. Reports stay on this machine.
|
|
13
|
+
</output>
|
|
@@ -23,7 +23,7 @@ Use `irc` for live coordination when it is available.
|
|
|
23
23
|
{{#if customSchemaEnabled}}- `schema`: JTD schema for expected structured output (do not put format rules in assignments){{/if}}
|
|
24
24
|
- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.
|
|
25
25
|
{{#if isolationEnabled}}- `isolated`: run in an isolated environment; REQUIRED when the user explicitly requests a worktree (for example, "use worktree"), and use when tasks edit overlapping files{{/if}}
|
|
26
|
-
- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the
|
|
26
|
+
- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the session's committed cwd and cannot itself authorize relocation. After an authorized session move, new work uses the committed target; already-admitted work retains its source execution inputs. Verified storage relocation of that same parent does not change child execution cwd; a logical session switch does not substitute successor storage. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`) or use the existing user-authorized relocation surface; `move_session` still only narrows to a descendant directory.
|
|
27
27
|
</parameters>
|
|
28
28
|
|
|
29
29
|
<rules>
|
|
@@ -6,7 +6,7 @@ export interface CliCredentialSelector {
|
|
|
6
6
|
raw: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const SELECTOR_KINDS = new Set<AuthCredentialSelectorKind>(["id", "email", "account", "project"]);
|
|
9
|
+
const SELECTOR_KINDS = new Set<AuthCredentialSelectorKind>(["id", "email", "account", "project", "org"]);
|
|
10
10
|
|
|
11
11
|
function parseSelectorBody(body: string): AuthCredentialSelector | undefined {
|
|
12
12
|
const separator = body.indexOf(":");
|
|
@@ -28,7 +28,7 @@ export function parseCliCredentialSelector(raw: string): CliCredentialSelector {
|
|
|
28
28
|
const selector = parseSelectorBody(body);
|
|
29
29
|
if (!selector || provider === "") {
|
|
30
30
|
throw new Error(
|
|
31
|
-
`Invalid --credential selector "${raw}". Use email:name@example.com, id:123, account:<id>, project:<id>, or provider/email:name@example.com.`,
|
|
31
|
+
`Invalid --credential selector "${raw}". Use email:name@example.com, id:123, account:<id>, project:<id>, org:<organization id or name>, or provider/email:name@example.com.`,
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
return { ...(provider ? { provider } : {}), selector, raw };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export const MCP_MAX_CONTENT_BYTES = 16 * 1024 * 1024;
|
|
2
|
+
// Match the pasted-image count limit; bound decode attempts as well as output count.
|
|
3
|
+
export const MCP_MAX_IMAGES = 16;
|
|
4
|
+
// Bound decoded source complexity before native image decoding.
|
|
5
|
+
export const MCP_MAX_IMAGE_SOURCE_DIMENSION = 8_192;
|
|
6
|
+
export const MCP_MAX_IMAGE_SOURCE_PIXELS = 16_000_000;
|
|
7
|
+
// Decoded image-file bytes after resizing, before base64 encoding (per result).
|
|
8
|
+
export const MCP_MAX_IMAGE_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
2
9
|
export const MCP_MAX_ERROR_BYTES = 16 * 1024;
|
|
3
10
|
export const MCP_MAX_SSE_REQUEST_MESSAGES = 1024;
|
|
4
11
|
export const MCP_MAX_SSE_BATCH_MESSAGES = 1024;
|
|
@@ -2,6 +2,7 @@ import { expect, test, vi } from "bun:test";
|
|
|
2
2
|
import { rm } from "node:fs/promises";
|
|
3
3
|
import { legacyMcpMethodNotFound } from "../../test/mcp-test-utils";
|
|
4
4
|
import * as configValue from "../config/resolve-config-value";
|
|
5
|
+
import { wrapUntrustedContent } from "../tools/fetch";
|
|
5
6
|
import { MCPManager } from "./manager";
|
|
6
7
|
import { MCPConnectionPool } from "./pool";
|
|
7
8
|
import { computeMCPPoolKey } from "./pool-key";
|
|
@@ -1386,8 +1387,8 @@ test("shared noReplay request failure coordinates one replacement without resend
|
|
|
1386
1387
|
expect(second.getTools()[0]?.mcpToolName).toBe("shared-2");
|
|
1387
1388
|
const firstResult = await first.getTools()[0]!.execute("first-later", {}, undefined, {} as never);
|
|
1388
1389
|
const secondResult = await second.getTools()[0]!.execute("second-later", {}, undefined, {} as never);
|
|
1389
|
-
expect(firstResult.content).toEqual([{ type: "text", text: "ok-2" }]);
|
|
1390
|
-
expect(secondResult.content).toEqual([{ type: "text", text: "ok-2" }]);
|
|
1390
|
+
expect(firstResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
|
|
1391
|
+
expect(secondResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
|
|
1391
1392
|
} finally {
|
|
1392
1393
|
await first.disconnectAll().catch(() => {});
|
|
1393
1394
|
await second.disconnectAll().catch(() => {});
|
|
@@ -1474,8 +1475,8 @@ test("shared restart rebinds every surviving manager lease before subsequent cal
|
|
|
1474
1475
|
expect(second.getTools()[0]?.mcpToolName).toBe("shared-2");
|
|
1475
1476
|
const firstResult = await first.getTools()[0]!.execute("first", {}, undefined, {} as never);
|
|
1476
1477
|
const secondResult = await second.getTools()[0]!.execute("second", {}, undefined, {} as never);
|
|
1477
|
-
expect(firstResult.content).toEqual([{ type: "text", text: "ok-2" }]);
|
|
1478
|
-
expect(secondResult.content).toEqual([{ type: "text", text: "ok-2" }]);
|
|
1478
|
+
expect(firstResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
|
|
1479
|
+
expect(secondResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
|
|
1479
1480
|
} finally {
|
|
1480
1481
|
await first.disconnectAll().catch(() => {});
|
|
1481
1482
|
await second.disconnectAll().catch(() => {});
|
|
@@ -303,6 +303,30 @@ export interface MCPToolCatalogSnapshot {
|
|
|
303
303
|
generation: number;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/** Drop project stdio servers unless the user/global opt-in is set. */
|
|
307
|
+
export function omitUnapprovedProjectStdio(
|
|
308
|
+
configs: Record<string, MCPServerConfig>,
|
|
309
|
+
sources: Record<string, SourceMeta>,
|
|
310
|
+
allowProjectStdio: boolean,
|
|
311
|
+
toolsOnly = false,
|
|
312
|
+
): void {
|
|
313
|
+
if (allowProjectStdio) return;
|
|
314
|
+
for (const name of Object.keys(configs)) {
|
|
315
|
+
const config = configs[name];
|
|
316
|
+
if (!config || config.type === "http" || config.type === "sse") continue;
|
|
317
|
+
if (!isUntrustedProjectMcpSource(sources[name], toolsOnly)) continue;
|
|
318
|
+
delete configs[name];
|
|
319
|
+
delete sources[name];
|
|
320
|
+
if (!toolsOnly) {
|
|
321
|
+
logger.warn("Skipping MCP autoload registration", {
|
|
322
|
+
path: `mcp:${name}`,
|
|
323
|
+
serverName: name,
|
|
324
|
+
reason: "project stdio server is not user-approved",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
306
330
|
/** Options for discovering and connecting to MCP servers */
|
|
307
331
|
export interface MCPDiscoverOptions {
|
|
308
332
|
/** Whether to load project-level config (default: true) */
|
|
@@ -330,6 +354,11 @@ export interface MCPDiscoverOptions {
|
|
|
330
354
|
export interface MCPManagerOptions {
|
|
331
355
|
/** Restrict this instance to tools from an explicit MCP config. */
|
|
332
356
|
toolsOnly?: boolean;
|
|
357
|
+
/**
|
|
358
|
+
* Spawn stdio servers loaded from a project MCP file. Default false.
|
|
359
|
+
* User-scope servers and an explicit `--mcp-config` file are unaffected.
|
|
360
|
+
*/
|
|
361
|
+
allowUntrustedProjectStdio?: boolean;
|
|
333
362
|
/**
|
|
334
363
|
* Ceiling for the startup wait, in milliseconds. ACP lifecycle launches and
|
|
335
364
|
* plugin MCP startup use bounded caller-owned budgets; other consumers keep
|
|
@@ -460,6 +489,7 @@ export class MCPManager {
|
|
|
460
489
|
#scopedLifecycle: "open" | "disconnecting" | "reconnecting" = "open";
|
|
461
490
|
#scopedLifecycleEpoch = 0;
|
|
462
491
|
readonly #toolsOnly: boolean;
|
|
492
|
+
readonly #allowUntrustedProjectStdio: boolean;
|
|
463
493
|
#toolCacheServerNames: Set<string> | undefined;
|
|
464
494
|
#toolsOnlyConfigLoaded = false;
|
|
465
495
|
#connectionSetMutationBlocked = false;
|
|
@@ -995,6 +1025,7 @@ export class MCPManager {
|
|
|
995
1025
|
options: MCPManagerOptions = {},
|
|
996
1026
|
) {
|
|
997
1027
|
this.#toolsOnly = options.toolsOnly === true;
|
|
1028
|
+
this.#allowUntrustedProjectStdio = options.allowUntrustedProjectStdio === true;
|
|
998
1029
|
this.#toolCacheServerNames = options.toolCacheServerNames ? new Set(options.toolCacheServerNames) : undefined;
|
|
999
1030
|
this.#maxStartupTimeoutMs = options.maxStartupTimeoutMs;
|
|
1000
1031
|
this.#sleep = options.sleep ?? delay;
|
|
@@ -1185,6 +1216,7 @@ export class MCPManager {
|
|
|
1185
1216
|
nativeOnly: options?.nativeOnly,
|
|
1186
1217
|
configPath: options?.configPath,
|
|
1187
1218
|
});
|
|
1219
|
+
omitUnapprovedProjectStdio(configs, sources, this.#allowUntrustedProjectStdio, this.#toolsOnly);
|
|
1188
1220
|
if (this.#toolCacheServerNames !== undefined) {
|
|
1189
1221
|
this.#toolCacheServerNames = new Set(
|
|
1190
1222
|
Object.keys(configs).filter(name => sources[name]?.provider !== "gjc-plugins"),
|
|
@@ -9,6 +9,7 @@ import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callba
|
|
|
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
11
|
import { guardedPublicFetch } from "../web/insane/url-guard";
|
|
12
|
+
import { cancelMCPStream } from "./content-limits";
|
|
12
13
|
import { assertPublicOAuthUrl } from "./oauth-public-url";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_PORT = 3000;
|
|
@@ -104,6 +105,34 @@ function resolveCallbackOptions(config: MCPOAuthConfig): OAuthCallbackFlowOption
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function isOAuthRedirect(response: Response): boolean {
|
|
109
|
+
return response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Dial the URL on an address `guardedPublicFetch` just accepted. That helper
|
|
114
|
+
* forces `redirect: "manual"`, so a 3xx is a refusal rather than a followed hop.
|
|
115
|
+
*/
|
|
116
|
+
async function fetchPinnedOAuthEndpoint(
|
|
117
|
+
url: string,
|
|
118
|
+
init: { method: string; headers: Record<string, string>; body?: string; signal?: AbortSignal },
|
|
119
|
+
): Promise<Response> {
|
|
120
|
+
const dialed = await guardedPublicFetch(url, {
|
|
121
|
+
method: init.method,
|
|
122
|
+
headers: init.headers,
|
|
123
|
+
body: init.body,
|
|
124
|
+
signal: init.signal,
|
|
125
|
+
});
|
|
126
|
+
if (!dialed.ok) {
|
|
127
|
+
throw new Error(`Refusing non-public OAuth endpoint: ${dialed.reason}`);
|
|
128
|
+
}
|
|
129
|
+
if (isOAuthRedirect(dialed.response)) {
|
|
130
|
+
cancelMCPStream(dialed.response.body);
|
|
131
|
+
throw new Error("Refusing non-public OAuth endpoint: redirect");
|
|
132
|
+
}
|
|
133
|
+
return dialed.response;
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
/**
|
|
108
137
|
* Canonical MCP server resource URI per RFC 8707 §2: absolute URI, lowercase
|
|
109
138
|
* scheme/host, no fragment, no trailing slash on an empty path.
|
|
@@ -264,9 +293,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
264
293
|
}
|
|
265
294
|
|
|
266
295
|
await assertPublicOAuthUrl(this.config.tokenUrl, { signal: this.ctrl.signal });
|
|
267
|
-
const response = await
|
|
296
|
+
const response = await fetchPinnedOAuthEndpoint(this.config.tokenUrl, {
|
|
268
297
|
method: "POST",
|
|
269
|
-
redirect: "error",
|
|
270
298
|
headers: {
|
|
271
299
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
272
300
|
},
|
|
@@ -345,9 +373,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
345
373
|
|
|
346
374
|
try {
|
|
347
375
|
await assertPublicOAuthUrl(registrationEndpoint, { signal });
|
|
348
|
-
const response = await
|
|
376
|
+
const response = await fetchPinnedOAuthEndpoint(registrationEndpoint, {
|
|
349
377
|
method: "POST",
|
|
350
|
-
redirect: "error",
|
|
351
378
|
headers: {
|
|
352
379
|
"Content-Type": "application/json",
|
|
353
380
|
Accept: "application/json",
|
|
@@ -387,9 +414,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
387
414
|
const authorizationEndpoint = new URL(this.config.authorizationUrl);
|
|
388
415
|
const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationEndpoint.origin);
|
|
389
416
|
await assertPublicOAuthUrl(metadataUrl.toString(), { signal });
|
|
390
|
-
const response = await
|
|
417
|
+
const response = await fetchPinnedOAuthEndpoint(metadataUrl.toString(), {
|
|
391
418
|
method: "GET",
|
|
392
|
-
redirect: "error",
|
|
393
419
|
headers: { Accept: "application/json" },
|
|
394
420
|
signal,
|
|
395
421
|
});
|