@gajae-code/coding-agent 0.18.10 → 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 +295 -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
package/src/config/settings.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
validateAutoroutingEffective,
|
|
63
63
|
validateAutoroutingLocal,
|
|
64
64
|
} from "./autorouting-contract";
|
|
65
|
+
import type { ConfigHotReloadFileSnapshot } from "./config-hot-reload";
|
|
65
66
|
import { isModelSelectorValue, type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
|
|
66
67
|
|
|
67
68
|
import {
|
|
@@ -92,6 +93,53 @@ export interface RawSettings {
|
|
|
92
93
|
[key: string]: unknown;
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/** Validated candidate view. `get` includes local patches and effective overlays. */
|
|
97
|
+
export interface SettingsGlobalConfigCandidate {
|
|
98
|
+
readonly configPath: string;
|
|
99
|
+
readonly diagnostics: SettingsSchemaReport;
|
|
100
|
+
get<P extends SettingPath>(settingPath: P): SettingValue<P>;
|
|
101
|
+
/** Candidate global values include pending local patches, but exclude other layers. */
|
|
102
|
+
getGlobal<P extends SettingPath>(settingPath: P): SettingValue<P> | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SettingsConfigReloadPublication {
|
|
106
|
+
finalize(): void;
|
|
107
|
+
rollback(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class SettingsGlobalConfigReloadError extends Error {
|
|
111
|
+
constructor(
|
|
112
|
+
message: string,
|
|
113
|
+
readonly configPath: string | null,
|
|
114
|
+
readonly diagnostics?: SettingsSchemaReport,
|
|
115
|
+
) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = "SettingsGlobalConfigReloadError";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface SettingsGlobalConfigCandidateState {
|
|
122
|
+
owner: Settings;
|
|
123
|
+
configPath: string;
|
|
124
|
+
globalWithPendingPatches: RawSettings;
|
|
125
|
+
effective: RawSettings;
|
|
126
|
+
diagnostics: SettingsSchemaReport;
|
|
127
|
+
rawConfig: RawSettings;
|
|
128
|
+
currentGlobal: RawSettings;
|
|
129
|
+
currentProject: RawSettings;
|
|
130
|
+
currentOverrides: RawSettings;
|
|
131
|
+
currentMerged: RawSettings;
|
|
132
|
+
cwd: string;
|
|
133
|
+
sourcePresent: boolean;
|
|
134
|
+
futureSchemaVersion: boolean;
|
|
135
|
+
published: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const settingsGlobalConfigCandidateStates = new WeakMap<
|
|
139
|
+
SettingsGlobalConfigCandidate,
|
|
140
|
+
SettingsGlobalConfigCandidateState
|
|
141
|
+
>();
|
|
142
|
+
|
|
95
143
|
const UNSAFE_EDIT_VARIANT_PATTERNS = new Set(["__proto__", "constructor", "prototype"]);
|
|
96
144
|
|
|
97
145
|
const CONFIG_ROOT_WORKFLOW_MIGRATION_KEYS: readonly WorkflowSettingKey[] = [
|
|
@@ -263,6 +311,12 @@ export class NotificationSettingsOverrideError extends Error {
|
|
|
263
311
|
}
|
|
264
312
|
|
|
265
313
|
const LOCAL_NOTIFICATION_SETTING_KEYS = new Set(["terminalBell", "bellOnComplete", "bellOnApproval", "bellOnAsk"]);
|
|
314
|
+
/** `auth.*` keys that select credentials; startup reads them from global config only, so project copies are dropped. */
|
|
315
|
+
const GLOBAL_ONLY_AUTH_SETTING_KEYS = new Set([
|
|
316
|
+
"credentialPins",
|
|
317
|
+
"credentialPinStoreIdentity",
|
|
318
|
+
"credentialPreferences",
|
|
319
|
+
]);
|
|
266
320
|
const LOCAL_NOTIFICATION_SETTING_PATHS = new Set(
|
|
267
321
|
[...LOCAL_NOTIFICATION_SETTING_KEYS].map(key => `notifications.${key}`),
|
|
268
322
|
);
|
|
@@ -336,6 +390,11 @@ const GLOBAL_ONLY_SETTINGS = new Set<SettingPath>([
|
|
|
336
390
|
"paseo.autoImport",
|
|
337
391
|
"ui.language",
|
|
338
392
|
"telemetry.enabled",
|
|
393
|
+
// Destination of the operator Hindsight server. A project file must not redirect it.
|
|
394
|
+
"hindsight.apiUrl",
|
|
395
|
+
// Operator consent for repository skills. A project file must not flip it.
|
|
396
|
+
"skills.trustProjectSkills",
|
|
397
|
+
"skills.enablePiProject",
|
|
339
398
|
]);
|
|
340
399
|
const LEGACY_THEME_NAME_REPLACEMENTS = {
|
|
341
400
|
dark: "red-claw",
|
|
@@ -544,6 +603,8 @@ export class Settings implements NotificationSettingsReader {
|
|
|
544
603
|
#schemaMigrationPending = false;
|
|
545
604
|
/** A newer config schema must never be rewritten by legacy migrations. */
|
|
546
605
|
#futureSchemaVersion = false;
|
|
606
|
+
/** Set only after a valid disk-backed global config has been loaded or published. */
|
|
607
|
+
#hasAcceptedGlobalConfigFile = false;
|
|
547
608
|
#hasMalformedConfigRoot = false;
|
|
548
609
|
/** YAML syntax was unrecoverable, so the loaded defaults are read-only until config.yml is repaired. */
|
|
549
610
|
#hasRecoveredConfigSyntax = false;
|
|
@@ -745,6 +806,357 @@ export class Settings implements NotificationSettingsReader {
|
|
|
745
806
|
return () => this.#changeListeners.delete(listener);
|
|
746
807
|
}
|
|
747
808
|
|
|
809
|
+
/** The resolved global config.yml path, or null for in-memory settings. */
|
|
810
|
+
getGlobalConfigPath(): string | null {
|
|
811
|
+
return this.#configPath;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Validate the watcher's exact snapshot without writing or rereading it.
|
|
816
|
+
* Rebase pending local patches after prior durable operations settle.
|
|
817
|
+
*/
|
|
818
|
+
async stageGlobalConfigReload(snapshot: ConfigHotReloadFileSnapshot): Promise<SettingsGlobalConfigCandidate> {
|
|
819
|
+
const configPath = this.#configPath;
|
|
820
|
+
if (!configPath) {
|
|
821
|
+
throw new SettingsGlobalConfigReloadError(
|
|
822
|
+
"Global config reload requires a disk-backed Settings instance.",
|
|
823
|
+
null,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (path.resolve(snapshot.path) !== path.resolve(configPath)) {
|
|
828
|
+
throw new SettingsGlobalConfigReloadError("Global config snapshot path does not match Settings.", configPath);
|
|
829
|
+
}
|
|
830
|
+
const pendingAtStage = structuredClone(this.#pendingPatchesInGenerationOrder());
|
|
831
|
+
return enqueueAtomicYamlOperation(configPath, async () => {
|
|
832
|
+
const sourcePresent = snapshot.text !== null;
|
|
833
|
+
const content = snapshot.text ?? "";
|
|
834
|
+
if (!sourcePresent) {
|
|
835
|
+
if (this.#hasAcceptedGlobalConfigFile) {
|
|
836
|
+
const diagnostics: SettingsSchemaReport = {
|
|
837
|
+
valid: false,
|
|
838
|
+
issues: [
|
|
839
|
+
{
|
|
840
|
+
path: "config.yml",
|
|
841
|
+
kind: "invalid",
|
|
842
|
+
detail: "The previously accepted global config.yml is missing.",
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
};
|
|
846
|
+
throw new SettingsGlobalConfigReloadError(
|
|
847
|
+
"Cannot stage global config reload because the previously accepted config.yml is missing.",
|
|
848
|
+
configPath,
|
|
849
|
+
diagnostics,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let rawConfig: RawSettings = {};
|
|
855
|
+
if (content.trim() !== "") {
|
|
856
|
+
let parsed: unknown;
|
|
857
|
+
try {
|
|
858
|
+
parsed = YAML.parse(content);
|
|
859
|
+
} catch {
|
|
860
|
+
const diagnostics: SettingsSchemaReport = {
|
|
861
|
+
valid: false,
|
|
862
|
+
issues: [
|
|
863
|
+
{
|
|
864
|
+
path: "config.yml",
|
|
865
|
+
kind: "invalid",
|
|
866
|
+
detail: "Configuration YAML syntax is invalid; repair config.yml before reloading.",
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
};
|
|
870
|
+
throw new SettingsGlobalConfigReloadError(
|
|
871
|
+
"Cannot stage global config reload because config.yml has invalid YAML syntax.",
|
|
872
|
+
configPath,
|
|
873
|
+
diagnostics,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
if (parsed !== undefined) {
|
|
877
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
878
|
+
const diagnostics: SettingsSchemaReport = {
|
|
879
|
+
valid: false,
|
|
880
|
+
issues: [
|
|
881
|
+
{
|
|
882
|
+
path: "config.yml",
|
|
883
|
+
kind: "invalid",
|
|
884
|
+
detail: "Configuration root must be a YAML mapping.",
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
};
|
|
888
|
+
throw new SettingsGlobalConfigReloadError(
|
|
889
|
+
"Cannot stage global config reload because the config.yml root is not a mapping.",
|
|
890
|
+
configPath,
|
|
891
|
+
diagnostics,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
rawConfig = parsed as RawSettings;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const configSchemaVersion = normalizeConfigSchemaVersion(rawConfig.configSchemaVersion);
|
|
899
|
+
const futureSchemaVersion =
|
|
900
|
+
typeof configSchemaVersion === "number" && configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
901
|
+
const migratedRawConfig = this.#migrateRawSettings(structuredClone(rawConfig));
|
|
902
|
+
const reconciled = reconcileSettingsSchema(migratedRawConfig);
|
|
903
|
+
const diagnostics = reconciled.report;
|
|
904
|
+
for (const [settingPath, pathSegments] of [
|
|
905
|
+
["modelRoles", ["modelRoles"]],
|
|
906
|
+
["task.agentModelOverrides", ["task", "agentModelOverrides"]],
|
|
907
|
+
] as const) {
|
|
908
|
+
const value = getByPath(reconciled.settings, [...pathSegments]);
|
|
909
|
+
if (value === undefined) continue;
|
|
910
|
+
const record = rawSettingsRecord(value);
|
|
911
|
+
if (!record || Object.keys(shallowModelSelectorRecord(record)).length !== Object.keys(record).length) {
|
|
912
|
+
diagnostics.issues.push({
|
|
913
|
+
path: settingPath,
|
|
914
|
+
kind: "invalid",
|
|
915
|
+
detail: "Model selector records contain invalid values.",
|
|
916
|
+
});
|
|
917
|
+
diagnostics.valid = false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (futureSchemaVersion) {
|
|
921
|
+
diagnostics.issues.push({
|
|
922
|
+
path: "configSchemaVersion",
|
|
923
|
+
kind: "pending-migration",
|
|
924
|
+
detail: `Configuration requires schema version ${configSchemaVersion}.`,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (sourcePresent && hasLegacyCustomImageProvider(rawConfig)) {
|
|
928
|
+
diagnostics.issues.push({
|
|
929
|
+
path: "providers.image",
|
|
930
|
+
kind: "invalid",
|
|
931
|
+
detail: LEGACY_CUSTOM_IMAGE_PROVIDER_DIAGNOSTIC,
|
|
932
|
+
});
|
|
933
|
+
diagnostics.valid = false;
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
parseNotificationSettingsSnapshot(rawConfig);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration") throw error;
|
|
939
|
+
diagnostics.issues.push({
|
|
940
|
+
path: "notifications",
|
|
941
|
+
kind: "invalid",
|
|
942
|
+
detail: "Global notification settings are invalid.",
|
|
943
|
+
});
|
|
944
|
+
diagnostics.valid = false;
|
|
945
|
+
}
|
|
946
|
+
const currentGlobal = structuredClone(this.#global);
|
|
947
|
+
const currentProject = structuredClone(this.#project);
|
|
948
|
+
const currentOverrides = structuredClone(this.#overrides);
|
|
949
|
+
const currentMerged = structuredClone(this.#merged);
|
|
950
|
+
const globalWithPendingPatches = structuredClone(reconciled.settings) as RawSettings;
|
|
951
|
+
const rawWithPendingPatches = structuredClone(rawConfig);
|
|
952
|
+
for (const patch of [...pendingAtStage, ...this.#pendingPatchesInGenerationOrder()]) {
|
|
953
|
+
applySettingsPatch(globalWithPendingPatches, { ...patch, value: structuredClone(patch.value) });
|
|
954
|
+
applySettingsPatch(rawWithPendingPatches, { ...patch, value: structuredClone(patch.value) });
|
|
955
|
+
}
|
|
956
|
+
const effective = this.#mergeSettingsLayers(globalWithPendingPatches, currentProject, currentOverrides);
|
|
957
|
+
diagnostics.issues = diagnostics.issues.filter(
|
|
958
|
+
issue =>
|
|
959
|
+
!(
|
|
960
|
+
(issue.kind === "invalid" || issue.kind === "unknown") &&
|
|
961
|
+
(issue.path === "task.autorouting" || issue.path.startsWith("task.autorouting."))
|
|
962
|
+
),
|
|
963
|
+
);
|
|
964
|
+
for (const source of [globalWithPendingPatches, currentProject, currentOverrides]) {
|
|
965
|
+
const fragment = getByPath(source, ["task", "autorouting"]);
|
|
966
|
+
diagnostics.issues.push(
|
|
967
|
+
...validateAutoroutingLocal(fragment).map(localIssue => ({
|
|
968
|
+
path: localIssue.path ? `task.autorouting.${localIssue.path}` : "task.autorouting",
|
|
969
|
+
kind: "invalid" as const,
|
|
970
|
+
detail: localIssue.detail,
|
|
971
|
+
})),
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
const effectiveAutorouting = validateAutoroutingEffective(getByPath(effective, ["task", "autorouting"]));
|
|
975
|
+
if (!effectiveAutorouting.active && effectiveAutorouting.issue) {
|
|
976
|
+
diagnostics.issues.push({
|
|
977
|
+
path: "task.autorouting",
|
|
978
|
+
kind: "invalid",
|
|
979
|
+
detail: effectiveAutorouting.issue.detail,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
diagnostics.valid = !diagnostics.issues.some(issue => issue.kind === "invalid");
|
|
983
|
+
if (!diagnostics.valid) {
|
|
984
|
+
throw new SettingsGlobalConfigReloadError(
|
|
985
|
+
"Cannot stage global config reload because settings validation failed.",
|
|
986
|
+
configPath,
|
|
987
|
+
structuredClone(diagnostics),
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
const state: SettingsGlobalConfigCandidateState = {
|
|
991
|
+
owner: this,
|
|
992
|
+
configPath,
|
|
993
|
+
globalWithPendingPatches,
|
|
994
|
+
effective,
|
|
995
|
+
diagnostics: structuredClone(diagnostics),
|
|
996
|
+
rawConfig: rawWithPendingPatches,
|
|
997
|
+
currentGlobal,
|
|
998
|
+
currentProject,
|
|
999
|
+
currentOverrides,
|
|
1000
|
+
currentMerged,
|
|
1001
|
+
cwd: this.#cwd,
|
|
1002
|
+
sourcePresent,
|
|
1003
|
+
futureSchemaVersion,
|
|
1004
|
+
published: false,
|
|
1005
|
+
};
|
|
1006
|
+
const candidate: SettingsGlobalConfigCandidate = {
|
|
1007
|
+
configPath,
|
|
1008
|
+
diagnostics: structuredClone(diagnostics),
|
|
1009
|
+
get<P extends SettingPath>(settingPath: P): SettingValue<P> {
|
|
1010
|
+
const value = getByPath(state.effective, settingPath.split("."));
|
|
1011
|
+
if (value === undefined) return structuredClone(getDefault(settingPath));
|
|
1012
|
+
const pathScopedValue = resolvePathScopedStringArray(settingPath, value, state.cwd);
|
|
1013
|
+
return structuredClone((pathScopedValue ?? value) as SettingValue<P>);
|
|
1014
|
+
},
|
|
1015
|
+
getGlobal<P extends SettingPath>(settingPath: P): SettingValue<P> | undefined {
|
|
1016
|
+
const value = getByPath(state.globalWithPendingPatches, settingPath.split("."));
|
|
1017
|
+
return value === undefined ? undefined : structuredClone(value as SettingValue<P>);
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
settingsGlobalConfigCandidateStates.set(candidate, state);
|
|
1021
|
+
return Object.freeze(candidate);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Publish a staged global config and its dependent synchronous model commit as
|
|
1027
|
+
* one in-memory transaction. A thrown commit restores settings without hooks.
|
|
1028
|
+
*/
|
|
1029
|
+
publishGlobalConfigReload(
|
|
1030
|
+
candidate: SettingsGlobalConfigCandidate,
|
|
1031
|
+
commitSync: () => void = () => {},
|
|
1032
|
+
): SettingsConfigReloadPublication {
|
|
1033
|
+
const state = settingsGlobalConfigCandidateStates.get(candidate);
|
|
1034
|
+
if (!state || state.owner !== this) {
|
|
1035
|
+
throw new SettingsGlobalConfigReloadError(
|
|
1036
|
+
"The global config candidate does not belong to this Settings instance.",
|
|
1037
|
+
this.#configPath,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
if (state.published) {
|
|
1041
|
+
throw new SettingsGlobalConfigReloadError(
|
|
1042
|
+
"The global config candidate has already been published.",
|
|
1043
|
+
state.configPath,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
if (
|
|
1047
|
+
this.#configPath !== state.configPath ||
|
|
1048
|
+
!util.isDeepStrictEqual(this.#global, state.currentGlobal) ||
|
|
1049
|
+
!util.isDeepStrictEqual(this.#project, state.currentProject) ||
|
|
1050
|
+
!util.isDeepStrictEqual(this.#overrides, state.currentOverrides) ||
|
|
1051
|
+
!util.isDeepStrictEqual(this.#merged, state.currentMerged)
|
|
1052
|
+
) {
|
|
1053
|
+
throw new SettingsGlobalConfigReloadError(
|
|
1054
|
+
"Settings changed while the global config candidate was being prepared; stage it again.",
|
|
1055
|
+
state.configPath,
|
|
1056
|
+
structuredClone(state.diagnostics),
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const previousValues = new Map<SettingPath, unknown>();
|
|
1061
|
+
for (const settingPath of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
1062
|
+
previousValues.set(settingPath, structuredClone(this.get(settingPath)));
|
|
1063
|
+
}
|
|
1064
|
+
const previous = {
|
|
1065
|
+
global: this.#global,
|
|
1066
|
+
overrides: structuredClone(this.#overrides),
|
|
1067
|
+
rawNotificationConfig: this.#rawNotificationConfig,
|
|
1068
|
+
durableRawNotificationConfig: this.#durableRawNotificationConfig,
|
|
1069
|
+
durableNotificationFingerprint: this.#durableNotificationFingerprint,
|
|
1070
|
+
merged: this.#merged,
|
|
1071
|
+
schemaReport: this.#schemaReport,
|
|
1072
|
+
autoroutingEffective: this.#autoroutingEffective,
|
|
1073
|
+
autoroutingLocalIssues: this.#autoroutingLocalIssues,
|
|
1074
|
+
futureSchemaVersion: this.#futureSchemaVersion,
|
|
1075
|
+
hasAcceptedGlobalConfigFile: this.#hasAcceptedGlobalConfigFile,
|
|
1076
|
+
hasMalformedConfigRoot: this.#hasMalformedConfigRoot,
|
|
1077
|
+
hasRecoveredConfigSyntax: this.#hasRecoveredConfigSyntax,
|
|
1078
|
+
hasInvalidNotificationGlobal: this.#hasInvalidNotificationGlobal,
|
|
1079
|
+
notificationValidationGeneration: this.#notificationValidationGeneration,
|
|
1080
|
+
};
|
|
1081
|
+
let publicationState: "pending" | "finalized" | "rolled-back" = "pending";
|
|
1082
|
+
const rollback = () => {
|
|
1083
|
+
if (publicationState === "rolled-back") return;
|
|
1084
|
+
if (publicationState === "finalized") throw new Error("Cannot roll back finalized configuration.");
|
|
1085
|
+
this.#global = previous.global;
|
|
1086
|
+
this.#overrides = previous.overrides;
|
|
1087
|
+
this.#rawNotificationConfig = previous.rawNotificationConfig;
|
|
1088
|
+
this.#durableRawNotificationConfig = previous.durableRawNotificationConfig;
|
|
1089
|
+
this.#durableNotificationFingerprint = previous.durableNotificationFingerprint;
|
|
1090
|
+
this.#merged = previous.merged;
|
|
1091
|
+
this.#schemaReport = previous.schemaReport;
|
|
1092
|
+
this.#autoroutingEffective = previous.autoroutingEffective;
|
|
1093
|
+
this.#autoroutingLocalIssues = previous.autoroutingLocalIssues;
|
|
1094
|
+
this.#futureSchemaVersion = previous.futureSchemaVersion;
|
|
1095
|
+
this.#hasAcceptedGlobalConfigFile = previous.hasAcceptedGlobalConfigFile;
|
|
1096
|
+
this.#hasMalformedConfigRoot = previous.hasMalformedConfigRoot;
|
|
1097
|
+
this.#hasRecoveredConfigSyntax = previous.hasRecoveredConfigSyntax;
|
|
1098
|
+
this.#hasInvalidNotificationGlobal = previous.hasInvalidNotificationGlobal;
|
|
1099
|
+
this.#notificationValidationGeneration = previous.notificationValidationGeneration;
|
|
1100
|
+
publicationState = "rolled-back";
|
|
1101
|
+
};
|
|
1102
|
+
this.#global = structuredClone(state.globalWithPendingPatches);
|
|
1103
|
+
const previousNotificationFingerprint = this.#durableNotificationFingerprint;
|
|
1104
|
+
this.#captureRawNotificationConfig(state.rawConfig);
|
|
1105
|
+
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1106
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1107
|
+
}
|
|
1108
|
+
if (previousNotificationFingerprint !== this.#durableNotificationFingerprint) {
|
|
1109
|
+
this.#notificationValidationGeneration++;
|
|
1110
|
+
}
|
|
1111
|
+
this.#schemaReport = structuredClone(state.diagnostics);
|
|
1112
|
+
this.#futureSchemaVersion = state.futureSchemaVersion;
|
|
1113
|
+
this.#hasMalformedConfigRoot = false;
|
|
1114
|
+
this.#hasRecoveredConfigSyntax = false;
|
|
1115
|
+
this.#hasInvalidNotificationGlobal = false;
|
|
1116
|
+
this.#rebuildMerged();
|
|
1117
|
+
this.#recomputeNotificationValidationFromRaw();
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
const result: unknown = commitSync();
|
|
1121
|
+
if (result && typeof result === "object" && "then" in result) {
|
|
1122
|
+
throw new TypeError("Global config reload commit callback must be synchronous.");
|
|
1123
|
+
}
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
rollback();
|
|
1126
|
+
throw error;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
this.#hasAcceptedGlobalConfigFile ||= state.sourcePresent;
|
|
1130
|
+
state.published = true;
|
|
1131
|
+
return {
|
|
1132
|
+
rollback,
|
|
1133
|
+
finalize: () => {
|
|
1134
|
+
if (publicationState !== "pending") return;
|
|
1135
|
+
publicationState = "finalized";
|
|
1136
|
+
for (const settingPath of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
1137
|
+
const previousValue = previousValues.get(settingPath);
|
|
1138
|
+
const nextValue = this.get(settingPath);
|
|
1139
|
+
if (util.isDeepStrictEqual(previousValue, nextValue)) continue;
|
|
1140
|
+
const hook = SETTING_HOOKS[settingPath];
|
|
1141
|
+
if (hook) {
|
|
1142
|
+
try {
|
|
1143
|
+
hook(nextValue, previousValue);
|
|
1144
|
+
} catch {
|
|
1145
|
+
logger.warn("Configuration reload setting hook failed", { settingPath });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const listener of this.#changeListeners) {
|
|
1149
|
+
try {
|
|
1150
|
+
listener(settingPath);
|
|
1151
|
+
} catch {
|
|
1152
|
+
logger.warn("Configuration reload listener failed", { settingPath });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
748
1160
|
/** Whether durable settings mutations are permitted for the loaded configuration. */
|
|
749
1161
|
canWriteDurableConfig(): boolean {
|
|
750
1162
|
return !this.#persist || !this.#hasRecoveredConfigSyntax;
|
|
@@ -1156,6 +1568,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1156
1568
|
cloned.#autoroutingLocalIssues = structuredClone(this.#autoroutingLocalIssues);
|
|
1157
1569
|
cloned.#schemaMigrationPending = this.#schemaMigrationPending;
|
|
1158
1570
|
cloned.#futureSchemaVersion = this.#futureSchemaVersion;
|
|
1571
|
+
cloned.#hasAcceptedGlobalConfigFile = this.#hasAcceptedGlobalConfigFile;
|
|
1159
1572
|
cloned.#hasMalformedConfigRoot = this.#hasMalformedConfigRoot;
|
|
1160
1573
|
cloned.#hasRecoveredConfigSyntax = this.#hasRecoveredConfigSyntax;
|
|
1161
1574
|
cloned.#hasInvalidNotificationGlobal = this.#hasInvalidNotificationGlobal;
|
|
@@ -1206,10 +1619,12 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1206
1619
|
}
|
|
1207
1620
|
|
|
1208
1621
|
/**
|
|
1209
|
-
*
|
|
1622
|
+
* Shell configuration for bash and the Python executor.
|
|
1623
|
+
* `shellPath` comes from the user/global config only, so a project
|
|
1624
|
+
* `.gjc/settings.json` cannot select the binary those executors run.
|
|
1210
1625
|
*/
|
|
1211
1626
|
getShellConfig() {
|
|
1212
|
-
const shell = this.
|
|
1627
|
+
const shell = this.getGlobal("shellPath");
|
|
1213
1628
|
return resolveShellConfig(shell);
|
|
1214
1629
|
}
|
|
1215
1630
|
|
|
@@ -1516,7 +1931,10 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1516
1931
|
throw error;
|
|
1517
1932
|
}
|
|
1518
1933
|
this.#resetYamlLoadState();
|
|
1519
|
-
if (content.trim() === "")
|
|
1934
|
+
if (content.trim() === "") {
|
|
1935
|
+
if (filePath === this.#configPath) this.#hasAcceptedGlobalConfigFile = true;
|
|
1936
|
+
return {};
|
|
1937
|
+
}
|
|
1520
1938
|
let parsed: unknown;
|
|
1521
1939
|
try {
|
|
1522
1940
|
parsed = YAML.parse(content);
|
|
@@ -1536,7 +1954,10 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1536
1954
|
this.#captureRawNotificationConfig(undefined);
|
|
1537
1955
|
return {};
|
|
1538
1956
|
}
|
|
1539
|
-
if (parsed === undefined)
|
|
1957
|
+
if (parsed === undefined) {
|
|
1958
|
+
if (filePath === this.#configPath) this.#hasAcceptedGlobalConfigFile = true;
|
|
1959
|
+
return {};
|
|
1960
|
+
}
|
|
1540
1961
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1541
1962
|
this.#hasMalformedConfigRoot = true;
|
|
1542
1963
|
this.#schemaReport = {
|
|
@@ -1604,6 +2025,14 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1604
2025
|
});
|
|
1605
2026
|
}
|
|
1606
2027
|
this.#schemaReport = reconciled.report;
|
|
2028
|
+
if (
|
|
2029
|
+
filePath === this.#configPath &&
|
|
2030
|
+
reconciled.report.valid &&
|
|
2031
|
+
!this.#hasInvalidNotificationGlobal &&
|
|
2032
|
+
!hasLegacyCustomImageProvider(parsedRaw)
|
|
2033
|
+
) {
|
|
2034
|
+
this.#hasAcceptedGlobalConfigFile = true;
|
|
2035
|
+
}
|
|
1607
2036
|
return reconciled.settings;
|
|
1608
2037
|
}
|
|
1609
2038
|
|
|
@@ -1625,13 +2054,16 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1625
2054
|
const data = item.path.endsWith(`${path.sep}settings.json`)
|
|
1626
2055
|
? await this.#stripRetiredWorkflowKeys(item.path, structuredClone(item.data as RawSettings))
|
|
1627
2056
|
: (item.data as RawSettings);
|
|
1628
|
-
const { settings, rejectedNotifications,
|
|
2057
|
+
const { settings, rejectedNotifications, rejectedCredentialSelection } =
|
|
1629
2058
|
this.#stripProjectNotificationSettings(data);
|
|
1630
2059
|
if (rejectedNotifications) {
|
|
1631
2060
|
logger.warn("Settings: ignoring project notification settings", { path: item.path });
|
|
1632
2061
|
}
|
|
1633
|
-
if (
|
|
1634
|
-
logger.warn(
|
|
2062
|
+
if (rejectedCredentialSelection) {
|
|
2063
|
+
logger.warn(
|
|
2064
|
+
"Settings: ignoring project auth credential pins/preferences; credential selection is global-only",
|
|
2065
|
+
{ path: item.path },
|
|
2066
|
+
);
|
|
1635
2067
|
}
|
|
1636
2068
|
merged = this.#deepMerge(merged, settings);
|
|
1637
2069
|
}
|
|
@@ -6022,16 +6454,20 @@ export class Settings implements NotificationSettingsReader {
|
|
|
6022
6454
|
}
|
|
6023
6455
|
|
|
6024
6456
|
#rebuildMerged(): void {
|
|
6025
|
-
|
|
6026
|
-
|
|
6457
|
+
this.#merged = this.#mergeSettingsLayers(this.#global, this.#project, this.#overrides);
|
|
6458
|
+
this.#recomputeAutoroutingDiagnostic();
|
|
6459
|
+
}
|
|
6460
|
+
|
|
6461
|
+
#mergeSettingsLayers(global: RawSettings, projectLayer: RawSettings, overrideLayer: RawSettings): RawSettings {
|
|
6462
|
+
const project = structuredClone(projectLayer);
|
|
6463
|
+
const overrides = structuredClone(overrideLayer);
|
|
6027
6464
|
for (const settingPath of GLOBAL_ONLY_SETTINGS) {
|
|
6028
6465
|
const segments = settingPath.split(".");
|
|
6029
6466
|
deleteByPath(project, segments);
|
|
6030
6467
|
deleteByPath(overrides, segments);
|
|
6031
6468
|
}
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
this.#recomputeAutoroutingDiagnostic();
|
|
6469
|
+
const merged = this.#deepMerge(this.#deepMerge({}, global), project);
|
|
6470
|
+
return this.#deepMerge(merged, overrides);
|
|
6035
6471
|
}
|
|
6036
6472
|
|
|
6037
6473
|
#fireAllHooks(): void {
|
|
@@ -6047,27 +6483,25 @@ export class Settings implements NotificationSettingsReader {
|
|
|
6047
6483
|
#stripProjectNotificationSettings(settings: RawSettings): {
|
|
6048
6484
|
settings: RawSettings;
|
|
6049
6485
|
rejectedNotifications: boolean;
|
|
6050
|
-
|
|
6486
|
+
rejectedCredentialSelection: boolean;
|
|
6051
6487
|
} {
|
|
6052
6488
|
let rejectedNotifications = false;
|
|
6053
|
-
let
|
|
6489
|
+
let rejectedCredentialSelection = false;
|
|
6054
6490
|
const sanitized: RawSettings = {};
|
|
6055
6491
|
for (const [key, value] of Object.entries(settings)) {
|
|
6056
6492
|
if (key === "auth" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
6057
6493
|
const authSettings = { ...(value as Record<string, unknown>) };
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
delete authSettings.credentialPinStoreIdentity;
|
|
6064
|
-
rejectedCredentialPins = true;
|
|
6494
|
+
for (const key of GLOBAL_ONLY_AUTH_SETTING_KEYS) {
|
|
6495
|
+
if (Object.hasOwn(authSettings, key)) {
|
|
6496
|
+
delete authSettings[key];
|
|
6497
|
+
rejectedCredentialSelection = true;
|
|
6498
|
+
}
|
|
6065
6499
|
}
|
|
6066
6500
|
if (Object.keys(authSettings).length > 0) sanitized[key] = authSettings;
|
|
6067
6501
|
continue;
|
|
6068
6502
|
}
|
|
6069
|
-
if (key
|
|
6070
|
-
|
|
6503
|
+
if (key.startsWith("auth.") && GLOBAL_ONLY_AUTH_SETTING_KEYS.has(key.slice("auth.".length))) {
|
|
6504
|
+
rejectedCredentialSelection = true;
|
|
6071
6505
|
continue;
|
|
6072
6506
|
}
|
|
6073
6507
|
if (key === "notifications" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -6088,7 +6522,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
6088
6522
|
}
|
|
6089
6523
|
sanitized[key] = value;
|
|
6090
6524
|
}
|
|
6091
|
-
return { settings: sanitized, rejectedNotifications,
|
|
6525
|
+
return { settings: sanitized, rejectedNotifications, rejectedCredentialSelection };
|
|
6092
6526
|
}
|
|
6093
6527
|
|
|
6094
6528
|
#deepMerge(base: RawSettings, overrides: RawSettings): RawSettings {
|
|
@@ -3,6 +3,7 @@ import * as nodeFs from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { canonicalEnvKey, projectEnvSnapshot } from "@gajae-code/utils/env-file";
|
|
6
7
|
import { coordinatorMcpStateRoot, gjcRoot } from "../gjc-runtime/session-layout";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_SESSION_IDLE_TTL_MS,
|
|
@@ -121,6 +122,30 @@ function parseRootList(value: string | undefined): string[] {
|
|
|
121
122
|
.filter(Boolean);
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
const MANAGED_WORKTREE_DIR_ENV = "GJC_WORKTREE_DIR";
|
|
126
|
+
/** Captured at load so a later chdir or dotenv delete cannot hide the declaration. */
|
|
127
|
+
const startupManagedWorktreeSnapshot = projectEnvSnapshot();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Operator value only. A project dotenv declaration, including a dynamic one,
|
|
131
|
+
* must not widen managed worktree roots. An operator value the project does
|
|
132
|
+
* not declare is kept.
|
|
133
|
+
*/
|
|
134
|
+
function trustedManagedWorktreeDir(env: NodeJS.ProcessEnv): string | undefined {
|
|
135
|
+
const raw = env[MANAGED_WORKTREE_DIR_ENV];
|
|
136
|
+
if (!raw) return undefined;
|
|
137
|
+
const key = canonicalEnvKey(MANAGED_WORKTREE_DIR_ENV);
|
|
138
|
+
const declared = startupManagedWorktreeSnapshot.values[key];
|
|
139
|
+
const trimmed = raw.trim();
|
|
140
|
+
if (
|
|
141
|
+
declared !== undefined &&
|
|
142
|
+
(startupManagedWorktreeSnapshot.dynamic.has(key) || declared === raw || declared === trimmed)
|
|
143
|
+
) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
function resolveManagedWorktreeRoot(root: string, configured: string | undefined): string {
|
|
125
150
|
// Function replacers so a `$` in the home or repository path is kept verbatim.
|
|
126
151
|
const template = (configured?.trim() || "{repo}/.worktrees").replace(/^~(?=\/|$)/, () => os.homedir());
|
|
@@ -168,7 +193,7 @@ export function buildCoordinatorMcpConfig(env: NodeJS.ProcessEnv = process.env):
|
|
|
168
193
|
return {
|
|
169
194
|
allowedRoots: parseRootList(env.GJC_COORDINATOR_MCP_WORKDIR_ROOTS).map(root => path.resolve(root)),
|
|
170
195
|
managedWorktreeRoots: parseRootList(env.GJC_COORDINATOR_MCP_WORKDIR_ROOTS).map(root =>
|
|
171
|
-
resolveManagedWorktreeRoot(path.resolve(root), env
|
|
196
|
+
resolveManagedWorktreeRoot(path.resolve(root), trustedManagedWorktreeDir(env)),
|
|
172
197
|
),
|
|
173
198
|
mutationClasses: parseMutationClasses(
|
|
174
199
|
env.GJC_COORDINATOR_MCP_MUTATIONS ?? env.GJC_COORDINATOR_MCP_ENABLE_MUTATION_CLASSES,
|
package/src/crash/sanitize.ts
CHANGED
|
@@ -49,6 +49,10 @@ const RESIDUAL_PATTERNS: readonly RegExp[] = [
|
|
|
49
49
|
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./,
|
|
50
50
|
/\bxox[baprs]-[A-Za-z0-9-]{8,}/,
|
|
51
51
|
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
52
|
+
// Same shapes `redactCrashSecrets` rewrites. A survivor means this scanner
|
|
53
|
+
// is not certain, so the submission is refused rather than sent raw.
|
|
54
|
+
/(?<![A-Za-z0-9/+])(?=[A-Za-z0-9/+]{0,39}[/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/,
|
|
55
|
+
/(?<![A-Za-z0-9_%])session(?:id|[_-]id)%3[Dd][A-Za-z0-9._~%-]{8,}/i,
|
|
52
56
|
// Opaque URIs the URL rule cannot parse into an origin, and inline payloads.
|
|
53
57
|
/\bdata:[^\s]{0,64};base64,/i,
|
|
54
58
|
/:\/\//,
|
|
@@ -538,7 +538,9 @@ export async function buildImportPreview(options: BuildImportPreviewOptions): Pr
|
|
|
538
538
|
read.kind === "value"
|
|
539
539
|
? options.product === "claude-code"
|
|
540
540
|
? normalizeClaudeMcpJson(read.content, sourcePath, options.sourceScope)
|
|
541
|
-
: normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope
|
|
541
|
+
: normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope, {
|
|
542
|
+
omitEnvReferences: options.destinationScope === "project",
|
|
543
|
+
})
|
|
542
544
|
: { items: [], warnings: [] as string[] };
|
|
543
545
|
for (const warning of normalized.warnings) warnings.push(`${sourceLabel} MCPs: ${warning}`);
|
|
544
546
|
const unsupportedNestedAuth =
|