@gajae-code/coding-agent 0.18.12 → 0.19.0
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 +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
|
@@ -97,7 +97,7 @@ export async function refreshMissingQualifiedModelProviders(
|
|
|
97
97
|
.getRefreshableProviders()
|
|
98
98
|
.find(candidate => candidate.toLowerCase() === parsedSelector.provider.toLowerCase());
|
|
99
99
|
if (!provider || refreshedProviders.has(provider)) continue;
|
|
100
|
-
const availableModels = modelRegistry.getAvailable();
|
|
100
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
101
101
|
const rawModelId = selector.slice(selector.indexOf("/") + 1);
|
|
102
102
|
if (
|
|
103
103
|
resolveProviderModelReference(provider, rawModelId, availableModels) ||
|
|
@@ -1047,9 +1047,10 @@ export function resolveModelOverride(
|
|
|
1047
1047
|
settings?: Settings,
|
|
1048
1048
|
sessionId?: string,
|
|
1049
1049
|
aliasIntent?: "preset-equivalent" | "reject",
|
|
1050
|
+
credentialSessionId?: string,
|
|
1050
1051
|
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
|
|
1051
1052
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
1052
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1053
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
1053
1054
|
const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
|
|
1054
1055
|
for (const pattern of modelPatterns) {
|
|
1055
1056
|
const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
|
|
@@ -1141,13 +1142,13 @@ async function resolveModelChainEntries(
|
|
|
1141
1142
|
startIndex: number,
|
|
1142
1143
|
circuitPolicy: { skipOpenCircuits: boolean },
|
|
1143
1144
|
): Promise<ModelChainResolution & { firstOpenCircuit?: { index: number; skipCount: number } }> {
|
|
1144
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1145
|
-
const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
|
|
1146
|
-
const skips: Array<{ selector: string; reason: string }> = [];
|
|
1147
|
-
const authFailedModels = new Set<string>();
|
|
1148
1145
|
const canonicalSessionId =
|
|
1149
1146
|
options?.canonicalSessionId === null ? undefined : (options?.canonicalSessionId ?? sessionId);
|
|
1150
1147
|
const credentialSessionId = options?.credentialSessionId ?? sessionId;
|
|
1148
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
1149
|
+
const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
|
|
1150
|
+
const skips: Array<{ selector: string; reason: string }> = [];
|
|
1151
|
+
const authFailedModels = new Set<string>();
|
|
1151
1152
|
let firstOpenCircuit: { index: number; skipCount: number } | undefined;
|
|
1152
1153
|
for (let activeIndex = startIndex; activeIndex < modelPatterns.length; activeIndex += 1) {
|
|
1153
1154
|
const selector = modelPatterns[activeIndex];
|
|
@@ -1279,7 +1280,8 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1279
1280
|
parentFallbackSelector?: string;
|
|
1280
1281
|
skips: Array<{ selector: string; reason: string }>;
|
|
1281
1282
|
}> {
|
|
1282
|
-
const
|
|
1283
|
+
const credentialSessionId = options?.credentialSessionId ?? authSessionId;
|
|
1284
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
1283
1285
|
const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
|
|
1284
1286
|
let requestedModel: Model<Api> | undefined;
|
|
1285
1287
|
let requestedResolution: ResolvedModelRoleValue | undefined;
|
|
@@ -1294,6 +1296,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1294
1296
|
settings,
|
|
1295
1297
|
undefined,
|
|
1296
1298
|
options?.aliasIntent,
|
|
1299
|
+
credentialSessionId,
|
|
1297
1300
|
).model;
|
|
1298
1301
|
if (parentActiveModel) {
|
|
1299
1302
|
modelRegistry.seedCanonicalVariant?.(canonicalScope, parentActiveModel);
|
|
@@ -1315,7 +1318,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1315
1318
|
matchPreferences,
|
|
1316
1319
|
modelRegistry,
|
|
1317
1320
|
sessionId: canonicalScope,
|
|
1318
|
-
credentialSessionId
|
|
1321
|
+
credentialSessionId,
|
|
1319
1322
|
aliasIntent: options?.aliasIntent,
|
|
1320
1323
|
});
|
|
1321
1324
|
if (!requestedModel && candidate.model) {
|
|
@@ -1335,7 +1338,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1335
1338
|
continue;
|
|
1336
1339
|
}
|
|
1337
1340
|
}
|
|
1338
|
-
const key = await modelRegistry.getApiKey(candidate.model,
|
|
1341
|
+
const key = await modelRegistry.getApiKey(candidate.model, credentialSessionId);
|
|
1339
1342
|
if (isAuthenticatedOrKeyless(key)) {
|
|
1340
1343
|
return { ...candidate, requestedModel: candidate.model, authFallbackUsed: false, activeIndex, skips };
|
|
1341
1344
|
}
|
|
@@ -1345,8 +1348,9 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1345
1348
|
) {
|
|
1346
1349
|
const equivalentRetry = await resolveModelChainWithAuth([pattern], modelRegistry, settings, authSessionId, {
|
|
1347
1350
|
managedFallback: options.managedFallback,
|
|
1348
|
-
aliasIntent: options
|
|
1351
|
+
aliasIntent: options?.aliasIntent,
|
|
1349
1352
|
canonicalSessionId: canonicalScope,
|
|
1353
|
+
credentialSessionId,
|
|
1350
1354
|
});
|
|
1351
1355
|
if (equivalentRetry.model) {
|
|
1352
1356
|
return {
|
|
@@ -1367,10 +1371,17 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1367
1371
|
return { explicitThinkingLevel: false, requestedModel, authFallbackUsed: false, activeIndex, skips };
|
|
1368
1372
|
}
|
|
1369
1373
|
const fallback = parentActiveModelPattern
|
|
1370
|
-
? resolveModelOverride(
|
|
1374
|
+
? resolveModelOverride(
|
|
1375
|
+
[parentActiveModelPattern],
|
|
1376
|
+
modelRegistry,
|
|
1377
|
+
settings,
|
|
1378
|
+
credentialSessionId,
|
|
1379
|
+
options?.aliasIntent,
|
|
1380
|
+
credentialSessionId,
|
|
1381
|
+
)
|
|
1371
1382
|
: { explicitThinkingLevel: false };
|
|
1372
1383
|
if (fallback.model) {
|
|
1373
|
-
const fallbackKey = await modelRegistry.getApiKey(fallback.model,
|
|
1384
|
+
const fallbackKey = await modelRegistry.getApiKey(fallback.model, credentialSessionId);
|
|
1374
1385
|
if (isAuthenticatedOrKeyless(fallbackKey)) {
|
|
1375
1386
|
const isParentSubstitution = requestedModel === undefined || !modelsAreEqual(fallback.model, requestedModel);
|
|
1376
1387
|
return {
|
|
@@ -1452,8 +1463,9 @@ export async function resolveModelScope(
|
|
|
1452
1463
|
patterns: string[],
|
|
1453
1464
|
modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
|
|
1454
1465
|
preferences?: ModelMatchPreferences,
|
|
1466
|
+
credentialSessionId?: string,
|
|
1455
1467
|
): Promise<ScopedModel[]> {
|
|
1456
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1468
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
1457
1469
|
|
|
1458
1470
|
const scopedModels: ScopedModel[] = [];
|
|
1459
1471
|
const addScopedModel = (selection: ScopedModel): void => {
|
|
@@ -1558,13 +1570,14 @@ export async function resolveAllowedModels(
|
|
|
1558
1570
|
modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
|
|
1559
1571
|
settings: Settings | undefined,
|
|
1560
1572
|
preferences?: ModelMatchPreferences,
|
|
1573
|
+
credentialSessionId?: string,
|
|
1561
1574
|
): Promise<Model<Api>[]> {
|
|
1562
|
-
const available = modelRegistry.getAvailable();
|
|
1575
|
+
const available = modelRegistry.getAvailable(credentialSessionId);
|
|
1563
1576
|
const patterns = settings?.get("enabledModels");
|
|
1564
1577
|
if (!patterns || patterns.length === 0) {
|
|
1565
1578
|
return available;
|
|
1566
1579
|
}
|
|
1567
|
-
const scoped = await resolveModelScope(patterns, modelRegistry, preferences);
|
|
1580
|
+
const scoped = await resolveModelScope(patterns, modelRegistry, preferences, credentialSessionId);
|
|
1568
1581
|
if (scoped.length === 0) {
|
|
1569
1582
|
return [];
|
|
1570
1583
|
}
|
|
@@ -1688,6 +1701,7 @@ export async function findInitialModel(options: {
|
|
|
1688
1701
|
defaultModelId?: string;
|
|
1689
1702
|
defaultThinkingSelector?: ThinkingLevel;
|
|
1690
1703
|
modelRegistry: InitialModelRegistry;
|
|
1704
|
+
credentialSessionId?: string;
|
|
1691
1705
|
}): Promise<InitialModelResult> {
|
|
1692
1706
|
const {
|
|
1693
1707
|
cliProvider,
|
|
@@ -1698,6 +1712,7 @@ export async function findInitialModel(options: {
|
|
|
1698
1712
|
defaultModelId,
|
|
1699
1713
|
defaultThinkingSelector,
|
|
1700
1714
|
modelRegistry,
|
|
1715
|
+
credentialSessionId,
|
|
1701
1716
|
} = options;
|
|
1702
1717
|
|
|
1703
1718
|
let model: Model<Api> | undefined;
|
|
@@ -1705,7 +1720,11 @@ export async function findInitialModel(options: {
|
|
|
1705
1720
|
|
|
1706
1721
|
// 1. CLI args take priority
|
|
1707
1722
|
if (cliProvider && cliModel) {
|
|
1708
|
-
const found = resolveProviderModelReference(
|
|
1723
|
+
const found = resolveProviderModelReference(
|
|
1724
|
+
cliProvider,
|
|
1725
|
+
cliModel,
|
|
1726
|
+
modelRegistry.getAvailable(credentialSessionId),
|
|
1727
|
+
);
|
|
1709
1728
|
|
|
1710
1729
|
if (!found) {
|
|
1711
1730
|
console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
|
|
@@ -1730,7 +1749,11 @@ export async function findInitialModel(options: {
|
|
|
1730
1749
|
|
|
1731
1750
|
// 3. Try saved default from settings
|
|
1732
1751
|
if (defaultProvider && defaultModelId) {
|
|
1733
|
-
const found = resolveProviderModelReference(
|
|
1752
|
+
const found = resolveProviderModelReference(
|
|
1753
|
+
defaultProvider,
|
|
1754
|
+
defaultModelId,
|
|
1755
|
+
modelRegistry.getAvailable(credentialSessionId),
|
|
1756
|
+
);
|
|
1734
1757
|
|
|
1735
1758
|
if (found) {
|
|
1736
1759
|
model = found;
|
|
@@ -1745,7 +1768,7 @@ export async function findInitialModel(options: {
|
|
|
1745
1768
|
}
|
|
1746
1769
|
|
|
1747
1770
|
// 4. Try first available model with valid API key
|
|
1748
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1771
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
1749
1772
|
|
|
1750
1773
|
if (availableModels.length > 0) {
|
|
1751
1774
|
// Try to find a default model from known providers
|
|
@@ -1774,11 +1797,16 @@ export async function restoreModelFromSession(
|
|
|
1774
1797
|
currentModel: Model<Api> | undefined,
|
|
1775
1798
|
shouldPrintMessages: boolean,
|
|
1776
1799
|
modelRegistry: RestorableModelRegistry,
|
|
1800
|
+
credentialSessionId?: string,
|
|
1777
1801
|
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
|
|
1778
|
-
const restoredModel = resolveProviderModelReference(
|
|
1802
|
+
const restoredModel = resolveProviderModelReference(
|
|
1803
|
+
savedProvider,
|
|
1804
|
+
savedModelId,
|
|
1805
|
+
modelRegistry.getAvailable(credentialSessionId),
|
|
1806
|
+
);
|
|
1779
1807
|
|
|
1780
1808
|
// Check if restored model exists and has a valid API key
|
|
1781
|
-
const hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;
|
|
1809
|
+
const hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel, credentialSessionId)) : false;
|
|
1782
1810
|
|
|
1783
1811
|
if (restoredModel && hasApiKey) {
|
|
1784
1812
|
if (shouldPrintMessages) {
|
|
@@ -1806,7 +1834,7 @@ export async function restoreModelFromSession(
|
|
|
1806
1834
|
}
|
|
1807
1835
|
|
|
1808
1836
|
// Try to find any available model
|
|
1809
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1837
|
+
const availableModels = modelRegistry.getAvailable(credentialSessionId);
|
|
1810
1838
|
|
|
1811
1839
|
if (availableModels.length > 0) {
|
|
1812
1840
|
// Try to find a default model from known providers
|
|
@@ -505,6 +505,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
505
505
|
description: "Grace period before a master-spawned child orphaned by confirmed master loss is closed.",
|
|
506
506
|
validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 3_600_000,
|
|
507
507
|
},
|
|
508
|
+
"sdk.brokerAbandonedGraceMs": {
|
|
509
|
+
type: "number",
|
|
510
|
+
default: 120_000,
|
|
511
|
+
description:
|
|
512
|
+
"Grace before a detached broker with no client connection, no live session host, and no open spawn authority exits on its own. A supervised `gjc sdk broker run` service never reaps itself.",
|
|
513
|
+
validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 3_600_000,
|
|
514
|
+
},
|
|
508
515
|
|
|
509
516
|
// Notifications (shared daemon with Telegram/Discord/Slack presentation adapters)
|
|
510
517
|
"notifications.enabled": { type: "boolean", default: false },
|
|
@@ -1803,7 +1810,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1803
1810
|
tab: "interaction",
|
|
1804
1811
|
label: "Prompt Suggestions",
|
|
1805
1812
|
description:
|
|
1806
|
-
"Predict your likely next prompt after each turn (
|
|
1813
|
+
"Predict your likely next prompt after each turn (one extra default-role model call) and show it as ghost text; Tab accepts",
|
|
1807
1814
|
},
|
|
1808
1815
|
},
|
|
1809
1816
|
|
|
@@ -3596,6 +3603,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
3596
3603
|
default: [] as string[],
|
|
3597
3604
|
},
|
|
3598
3605
|
|
|
3606
|
+
"mcp.discoveryDeferAutoload": {
|
|
3607
|
+
type: "boolean",
|
|
3608
|
+
default: false,
|
|
3609
|
+
},
|
|
3610
|
+
|
|
3599
3611
|
"mcp.notifications": {
|
|
3600
3612
|
type: "boolean",
|
|
3601
3613
|
default: false,
|
package/src/config/settings.ts
CHANGED
|
@@ -392,6 +392,8 @@ const GLOBAL_ONLY_SETTINGS = new Set<SettingPath>([
|
|
|
392
392
|
"telemetry.enabled",
|
|
393
393
|
// Destination of the operator Hindsight server. A project file must not redirect it.
|
|
394
394
|
"hindsight.apiUrl",
|
|
395
|
+
// Destination of generic remote compaction. A project file must not redirect the transcript POST.
|
|
396
|
+
"compaction.remoteEndpoint",
|
|
395
397
|
// Operator consent for repository skills. A project file must not flip it.
|
|
396
398
|
"skills.trustProjectSkills",
|
|
397
399
|
"skills.enablePiProject",
|
|
@@ -2157,19 +2157,54 @@ const eventWebhookTails = new Map<string, Promise<void>>();
|
|
|
2157
2157
|
const eventWebhookStartupTails = new Map<string, Promise<void>>();
|
|
2158
2158
|
const EVENT_WEBHOOK_ERROR_CAP = 240;
|
|
2159
2159
|
|
|
2160
|
+
const EVENT_WEBHOOK_DIAGNOSTIC_CAP = 512;
|
|
2161
|
+
|
|
2162
|
+
/** Printable ASCII only, so a journal id cannot inject another diagnostic line. */
|
|
2163
|
+
function webhookEventDiagnosticToken(eventId: string, maxLength: number): string {
|
|
2164
|
+
if (maxLength <= 0) return "";
|
|
2165
|
+
let token = "";
|
|
2166
|
+
for (let index = 0; index < eventId.length && token.length < maxLength; index += 1) {
|
|
2167
|
+
const code = eventId.charCodeAt(index);
|
|
2168
|
+
if (code >= 0x20 && code <= 0x7e) token += eventId[index];
|
|
2169
|
+
}
|
|
2170
|
+
return token;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/** Test-only access for generated normalization and truncation properties. */
|
|
2174
|
+
export function webhookEventDiagnosticTokenForTest(eventId: string, maxLength: number): string {
|
|
2175
|
+
return webhookEventDiagnosticToken(eventId, maxLength);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2160
2178
|
async function appendEventWebhookDiagnostic(namespaceDir: string, eventId: string, error: unknown): Promise<void> {
|
|
2161
2179
|
const code =
|
|
2162
2180
|
error instanceof Error && /^[a-z0-9_]+$/.test(error.message)
|
|
2163
2181
|
? error.message.slice(0, EVENT_WEBHOOK_ERROR_CAP)
|
|
2164
2182
|
: "event_webhook_delivery_failed";
|
|
2165
|
-
const
|
|
2183
|
+
const timestamp = new Date().toISOString();
|
|
2184
|
+
const prefix = `${timestamp} event=`;
|
|
2185
|
+
const suffix = ` error=${code}\n`;
|
|
2186
|
+
const token = webhookEventDiagnosticToken(eventId, EVENT_WEBHOOK_DIAGNOSTIC_CAP - prefix.length - suffix.length);
|
|
2187
|
+
const line = `${prefix}${token}${suffix}`;
|
|
2166
2188
|
try {
|
|
2167
|
-
await fs.appendFile(
|
|
2189
|
+
await fs.appendFile(
|
|
2190
|
+
path.join(namespaceDir, "event-webhook-errors.log"),
|
|
2191
|
+
line.slice(0, EVENT_WEBHOOK_DIAGNOSTIC_CAP),
|
|
2192
|
+
{ mode: 0o600 },
|
|
2193
|
+
);
|
|
2168
2194
|
} catch {
|
|
2169
2195
|
// Diagnostics are best-effort; sink problems must never propagate upward.
|
|
2170
2196
|
}
|
|
2171
2197
|
}
|
|
2172
2198
|
|
|
2199
|
+
/** Test-only access for diagnostic record boundary coverage. */
|
|
2200
|
+
export async function appendEventWebhookDiagnosticForTest(
|
|
2201
|
+
namespaceDir: string,
|
|
2202
|
+
eventId: string,
|
|
2203
|
+
error: unknown,
|
|
2204
|
+
): Promise<void> {
|
|
2205
|
+
await appendEventWebhookDiagnostic(namespaceDir, eventId, error);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2173
2208
|
function enqueueEventWebhook(namespaceDir: string, event: CoordinatorEvent): void {
|
|
2174
2209
|
const config = eventWebhookConfigs.get(namespaceDir);
|
|
2175
2210
|
if (config === null || config === undefined) return;
|
package/src/crash/sanitize.ts
CHANGED
|
@@ -99,8 +99,10 @@ export function sanitizeExternalCrashV1(text: string, maxBytes: number = CRASH_F
|
|
|
99
99
|
let value = text.replace(/\r\n?/g, "\n");
|
|
100
100
|
value = value.replace(OSC_PATTERN, " ").replace(CSI_PATTERN, "");
|
|
101
101
|
value = value.replace(CONTROL_PATTERN, "").replace(INVISIBLE_PATTERN, "");
|
|
102
|
+
// Redact each URL separately so an unquoted query value cannot absorb prose
|
|
103
|
+
// after the URL; then sanitize the remaining crash text normally.
|
|
104
|
+
value = value.replace(URL_PATTERN, match => sanitizeUrl(redactCrashSecrets(match)));
|
|
102
105
|
value = redactCrashSecrets(value);
|
|
103
|
-
value = value.replace(URL_PATTERN, match => sanitizeUrl(match));
|
|
104
106
|
value = replaceAbsolutePaths(value);
|
|
105
107
|
value = value.replace(UUID_PATTERN, "<uuid>");
|
|
106
108
|
value = value.replace(HEX_RUN_PATTERN, match => (/[a-fA-F]/.test(match) ? "<hex>" : "<num>"));
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* - Hooks: Claude `.claude/hooks/<pre|post>/<file>` and Codex flat
|
|
9
9
|
* `.codex/hooks/<phase>-<tool>.ts|js` conventions, normalized through the
|
|
10
10
|
* canonical hook IR (`normalizeDirectoryHook`, #4286/#4289) and written to
|
|
11
|
-
* the
|
|
11
|
+
* the canonical `<root>/hooks/<pre|post>/` layout. Project-scope hooks are
|
|
12
|
+
* stored-only; ordinary sessions do not load repository hook modules.
|
|
12
13
|
* - MCPs: `normalizeClaudeMcpJson` / `normalizeCodexMcpToml` plus
|
|
13
14
|
* `validateMCPCompatServer` (#4284 bounded compatibility adapters), merged
|
|
14
15
|
* through the canonical atomic config writer.
|
|
@@ -17,12 +18,16 @@
|
|
|
17
18
|
* secret values live only in the separate `ImportPlan`. `applyImport` runs as
|
|
18
19
|
* a validated transaction: every destination path is containment- and
|
|
19
20
|
* symlink-checked before any write, destinations are revalidated against the
|
|
20
|
-
* preview decisions, files are published
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* preview decisions, and skill/hook files are published through a no-follow
|
|
22
|
+
* directory descriptor with same-directory temp+rename. Any failure restores
|
|
23
|
+
* exactly the files this transaction wrote (pre-existing symlinks are never
|
|
24
|
+
* touched). Verification failures roll back too.
|
|
23
25
|
*/
|
|
26
|
+
import { dlopen, FFIType, read } from "bun:ffi";
|
|
24
27
|
import type { Dirent } from "node:fs";
|
|
28
|
+
import * as fsSync from "node:fs";
|
|
25
29
|
import * as fs from "node:fs/promises";
|
|
30
|
+
import * as os from "node:os";
|
|
26
31
|
import * as path from "node:path";
|
|
27
32
|
import { parseFrontmatter, tryParseJson } from "@gajae-code/utils";
|
|
28
33
|
import { YAML } from "bun";
|
|
@@ -351,6 +356,11 @@ export async function buildImportPreview(options: BuildImportPreviewOptions): Pr
|
|
|
351
356
|
const entries: ImportPreviewEntry[] = [];
|
|
352
357
|
const payloads: (NormalizedPayload | undefined)[] = [];
|
|
353
358
|
const warnings: string[] = [];
|
|
359
|
+
if (surfaces.includes("hooks") && options.destinationScope === "project") {
|
|
360
|
+
warnings.push(
|
|
361
|
+
"Project-scope hooks are stored-only: ordinary sessions do not load repository hook modules. Import hooks into Global .gjc or configure an explicit hook path to activate them.",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
354
364
|
const productName = productLabel(options.product);
|
|
355
365
|
const sourceLabel = `${productName} ${sourceScopeLabel(options.sourceScope)}`;
|
|
356
366
|
|
|
@@ -636,6 +646,9 @@ interface FileSnapshot {
|
|
|
636
646
|
path: string;
|
|
637
647
|
/** Previous content, or null when the file did not exist. */
|
|
638
648
|
previous: string | null;
|
|
649
|
+
/** Set when publication was bound to this directory descriptor. */
|
|
650
|
+
parentFd?: number;
|
|
651
|
+
name?: string;
|
|
639
652
|
}
|
|
640
653
|
|
|
641
654
|
interface PlannedFileWrite {
|
|
@@ -675,6 +688,13 @@ async function findSymlinkedAncestor(root: string, target: string): Promise<stri
|
|
|
675
688
|
|
|
676
689
|
/** Remove stale staging temp files from an interrupted earlier apply. */
|
|
677
690
|
async function sweepStagingTemps(dir: string): Promise<void> {
|
|
691
|
+
try {
|
|
692
|
+
const stat = await fs.lstat(dir);
|
|
693
|
+
// readdir follows a swapped directory symlink; never clean through one.
|
|
694
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return;
|
|
695
|
+
} catch {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
678
698
|
let names: string[];
|
|
679
699
|
try {
|
|
680
700
|
names = await fs.readdir(dir);
|
|
@@ -687,6 +707,209 @@ async function sweepStagingTemps(dir: string): Promise<void> {
|
|
|
687
707
|
}
|
|
688
708
|
}
|
|
689
709
|
|
|
710
|
+
const { ENOENT, EEXIST, ELOOP, ENOTDIR } = os.constants.errno;
|
|
711
|
+
const CLOEXEC = (fsSync.constants as { O_CLOEXEC?: number }).O_CLOEXEC ?? 0;
|
|
712
|
+
const NOFOLLOW = fsSync.constants.O_NOFOLLOW ?? 0;
|
|
713
|
+
const DIR_FLAGS = fsSync.constants.O_RDONLY | (fsSync.constants.O_DIRECTORY ?? 0) | NOFOLLOW | CLOEXEC;
|
|
714
|
+
const READ_FLAGS = fsSync.constants.O_RDONLY | (fsSync.constants.O_NONBLOCK ?? 0) | NOFOLLOW | CLOEXEC;
|
|
715
|
+
const WRITE_FLAGS = fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | NOFOLLOW | CLOEXEC;
|
|
716
|
+
const MAX_PINNED_IMPORT_PARENTS = 128;
|
|
717
|
+
|
|
718
|
+
interface PinnedDestination {
|
|
719
|
+
fds: number[];
|
|
720
|
+
parentFd: number;
|
|
721
|
+
name: string;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
type DirectoryOps = {
|
|
725
|
+
mkdirat: (dirfd: number, name: string, mode: number) => number;
|
|
726
|
+
openat: (dirfd: number, name: string, flags: number, mode: number) => number;
|
|
727
|
+
renameat: (fromfd: number, from: string, tofd: number, to: string) => number;
|
|
728
|
+
unlinkat: (dirfd: number, name: string, flags: number) => number;
|
|
729
|
+
errno: () => number;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
let directoryOpsCache: DirectoryOps | undefined;
|
|
733
|
+
|
|
734
|
+
/** POSIX only. Windows has no O_NOFOLLOW, so publication keeps the lexical recheck. */
|
|
735
|
+
function pinnedPublicationAvailable(): boolean {
|
|
736
|
+
return (
|
|
737
|
+
process.platform !== "win32" &&
|
|
738
|
+
fsSync.constants.O_NOFOLLOW !== undefined &&
|
|
739
|
+
fsSync.constants.O_DIRECTORY !== undefined
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function directoryOps(): DirectoryOps {
|
|
744
|
+
if (directoryOpsCache) return directoryOpsCache;
|
|
745
|
+
const darwin = process.platform === "darwin";
|
|
746
|
+
const lib = dlopen(darwin ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
|
|
747
|
+
mkdirat: { args: [FFIType.i32, FFIType.cstring, FFIType.u32], returns: FFIType.i32 },
|
|
748
|
+
openat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32, FFIType.u32], returns: FFIType.i32 },
|
|
749
|
+
renameat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32, FFIType.cstring], returns: FFIType.i32 },
|
|
750
|
+
unlinkat: { args: [FFIType.i32, FFIType.cstring, FFIType.i32], returns: FFIType.i32 },
|
|
751
|
+
...(darwin
|
|
752
|
+
? { __error: { args: [], returns: FFIType.ptr } }
|
|
753
|
+
: { __errno_location: { args: [], returns: FFIType.ptr } }),
|
|
754
|
+
});
|
|
755
|
+
const symbols = lib.symbols as unknown as DirectoryOps & {
|
|
756
|
+
__error?: () => Parameters<typeof read.i32>[0];
|
|
757
|
+
__errno_location?: () => Parameters<typeof read.i32>[0];
|
|
758
|
+
};
|
|
759
|
+
directoryOpsCache = {
|
|
760
|
+
mkdirat: symbols.mkdirat,
|
|
761
|
+
openat: symbols.openat,
|
|
762
|
+
renameat: symbols.renameat,
|
|
763
|
+
unlinkat: symbols.unlinkat,
|
|
764
|
+
errno: () => read.i32(darwin ? symbols.__error!() : symbols.__errno_location!()),
|
|
765
|
+
};
|
|
766
|
+
return directoryOpsCache;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function closeRawFds(fds: number[]): void {
|
|
770
|
+
for (const fd of fds.splice(0).reverse()) fsSync.closeSync(fd);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function symlinkRefusal(location: string): Error {
|
|
774
|
+
return new Error(`refusing to write through symlinked ancestor: ${location}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Lexical shape of an import destination. Symlink checks are separate. */
|
|
778
|
+
export function importDestinationShapeAllowed(root: string, target: string): boolean {
|
|
779
|
+
const relative = path.relative(root, path.dirname(target));
|
|
780
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
781
|
+
const segments = relative === "" ? [] : relative.split(path.sep);
|
|
782
|
+
return [...segments, path.basename(target)].every(isSafeName);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Walk from `root` with openat/mkdirat and retain only the final parent descriptor. */
|
|
786
|
+
function pinImportParent(root: string, target: string): PinnedDestination {
|
|
787
|
+
if (!importDestinationShapeAllowed(root, target)) throw new Error(`refusing unsafe import destination: ${target}`);
|
|
788
|
+
const relative = path.relative(root, path.dirname(target));
|
|
789
|
+
const segments = relative === "" ? [] : relative.split(path.sep);
|
|
790
|
+
const name = path.basename(target);
|
|
791
|
+
const ops = directoryOps();
|
|
792
|
+
const fds: number[] = [];
|
|
793
|
+
try {
|
|
794
|
+
let parentFd: number;
|
|
795
|
+
try {
|
|
796
|
+
parentFd = fsSync.openSync(root, DIR_FLAGS);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
799
|
+
if (code === "ELOOP" || code === "ENOTDIR") throw symlinkRefusal(root);
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
fds.push(parentFd);
|
|
803
|
+
let cursor = root;
|
|
804
|
+
for (const segment of segments) {
|
|
805
|
+
cursor = path.join(cursor, segment);
|
|
806
|
+
let fd = ops.openat(parentFd, segment, DIR_FLAGS, 0);
|
|
807
|
+
if (fd < 0 && ops.errno() === ENOENT) {
|
|
808
|
+
if (ops.mkdirat(parentFd, segment, 0o700) < 0 && ops.errno() !== EEXIST) {
|
|
809
|
+
throw new Error(`cannot create import directory ${cursor}: errno ${ops.errno()}`);
|
|
810
|
+
}
|
|
811
|
+
fd = ops.openat(parentFd, segment, DIR_FLAGS, 0);
|
|
812
|
+
}
|
|
813
|
+
if (fd < 0) {
|
|
814
|
+
const err = ops.errno();
|
|
815
|
+
if (err === ELOOP || err === ENOTDIR) throw symlinkRefusal(cursor);
|
|
816
|
+
throw new Error(`cannot open import directory ${cursor}: errno ${err}`);
|
|
817
|
+
}
|
|
818
|
+
let isDirectory: boolean;
|
|
819
|
+
try {
|
|
820
|
+
isDirectory = fsSync.fstatSync(fd).isDirectory();
|
|
821
|
+
} catch (error) {
|
|
822
|
+
fsSync.closeSync(fd);
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
if (!isDirectory) {
|
|
826
|
+
fsSync.closeSync(fd);
|
|
827
|
+
throw symlinkRefusal(cursor);
|
|
828
|
+
}
|
|
829
|
+
closeRawFds(fds);
|
|
830
|
+
fds.push(fd);
|
|
831
|
+
parentFd = fd;
|
|
832
|
+
}
|
|
833
|
+
const named = fsSync.lstatSync(path.dirname(target));
|
|
834
|
+
const opened = fsSync.fstatSync(parentFd);
|
|
835
|
+
if (named.isSymbolicLink() || !named.isDirectory() || named.dev !== opened.dev || named.ino !== opened.ino) {
|
|
836
|
+
throw symlinkRefusal(path.dirname(target));
|
|
837
|
+
}
|
|
838
|
+
return { fds, parentFd, name };
|
|
839
|
+
} catch (error) {
|
|
840
|
+
closeRawFds(fds);
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function readRegularAt(parentFd: number, name: string, lexicalPath: string): string | null {
|
|
846
|
+
const ops = directoryOps();
|
|
847
|
+
const fd = ops.openat(parentFd, name, READ_FLAGS, 0);
|
|
848
|
+
if (fd < 0) {
|
|
849
|
+
const err = ops.errno();
|
|
850
|
+
if (err === ENOENT) return null;
|
|
851
|
+
if (err === ELOOP || err === ENOTDIR) throw new Error(`refusing to write through symlink: ${lexicalPath}`);
|
|
852
|
+
throw new Error(`cannot read ${lexicalPath}: errno ${err}`);
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
if (!fsSync.fstatSync(fd).isFile()) throw new Error(`not a regular file: ${lexicalPath}`);
|
|
856
|
+
return fsSync.readFileSync(fd, "utf-8");
|
|
857
|
+
} finally {
|
|
858
|
+
fsSync.closeSync(fd);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function writeRegularAt(parentFd: number, name: string, content: string, index: number, lexicalPath: string): void {
|
|
863
|
+
const ops = directoryOps();
|
|
864
|
+
const tmp = `.gjc-import-${process.pid}-${index}-${name}.tmp`;
|
|
865
|
+
const fd = ops.openat(parentFd, tmp, WRITE_FLAGS, 0o600);
|
|
866
|
+
if (fd < 0) throw new Error(`cannot stage import file ${lexicalPath}: errno ${ops.errno()}`);
|
|
867
|
+
try {
|
|
868
|
+
fsSync.writeFileSync(fd, content, "utf-8");
|
|
869
|
+
fsSync.fchmodSync(fd, 0o600);
|
|
870
|
+
} finally {
|
|
871
|
+
fsSync.closeSync(fd);
|
|
872
|
+
}
|
|
873
|
+
if (ops.renameat(parentFd, tmp, parentFd, name) < 0) {
|
|
874
|
+
const err = ops.errno();
|
|
875
|
+
ops.unlinkat(parentFd, tmp, 0);
|
|
876
|
+
throw new Error(`cannot publish import file ${lexicalPath}: errno ${err}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** The lexical parent must still be the directory the descriptor published into. */
|
|
881
|
+
function assertPinnedParentStillAt(parentFd: number, lexicalParent: string, lexicalPath: string): void {
|
|
882
|
+
let named: fsSync.Stats;
|
|
883
|
+
try {
|
|
884
|
+
named = fsSync.lstatSync(lexicalParent);
|
|
885
|
+
} catch {
|
|
886
|
+
throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
|
|
887
|
+
}
|
|
888
|
+
const opened = fsSync.fstatSync(parentFd);
|
|
889
|
+
if (named.isSymbolicLink() || !named.isDirectory() || named.dev !== opened.dev || named.ino !== opened.ino) {
|
|
890
|
+
throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const leaf = fsSync.lstatSync(lexicalPath);
|
|
894
|
+
if (leaf.isSymbolicLink() || !leaf.isFile()) {
|
|
895
|
+
throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
|
|
896
|
+
}
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error instanceof Error && error.message.startsWith("post-import verification failed:")) throw error;
|
|
899
|
+
throw new Error(`post-import verification failed: ${lexicalPath} was not persisted`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function restoreRegularAt(parentFd: number, name: string, previous: string | null, lexicalPath: string): void {
|
|
904
|
+
if (previous === null) {
|
|
905
|
+
if (directoryOps().unlinkat(parentFd, name, 0) < 0 && directoryOps().errno() !== ENOENT) {
|
|
906
|
+
throw new Error(`cannot remove ${lexicalPath}: errno ${directoryOps().errno()}`);
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
writeRegularAt(parentFd, name, previous, 1_000_000 + process.pid, lexicalPath);
|
|
911
|
+
}
|
|
912
|
+
|
|
690
913
|
/**
|
|
691
914
|
* Apply a confirmed plan. The transaction contract:
|
|
692
915
|
*
|
|
@@ -850,12 +1073,44 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
|
|
|
850
1073
|
|
|
851
1074
|
// --- Phase 2: publication ------------------------------------------------
|
|
852
1075
|
const snapshots: FileSnapshot[] = [];
|
|
1076
|
+
const pins: PinnedDestination[] = [];
|
|
1077
|
+
const pinsByParent = new Map<string, PinnedDestination>();
|
|
853
1078
|
for (const dir of [destination.skillsDir, destination.hooksDir]) {
|
|
854
1079
|
await sweepStagingTemps(dir).catch(() => {});
|
|
855
1080
|
}
|
|
856
1081
|
|
|
857
1082
|
try {
|
|
1083
|
+
if (fileWrites.length > 0 && pinnedPublicationAvailable()) {
|
|
1084
|
+
// Scope root only. Children are created with mkdirat on the pinned fd.
|
|
1085
|
+
await fs.mkdir(destination.root, { recursive: true, mode: 0o700 });
|
|
1086
|
+
for (const write of fileWrites) {
|
|
1087
|
+
const parentPath = path.dirname(write.path);
|
|
1088
|
+
if (pinsByParent.has(parentPath)) continue;
|
|
1089
|
+
if (pinsByParent.size >= MAX_PINNED_IMPORT_PARENTS) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`import batch exceeds the limit of ${MAX_PINNED_IMPORT_PARENTS} distinct destination directories`,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
const pin = pinImportParent(destination.root, write.path);
|
|
1095
|
+
pins.push(pin);
|
|
1096
|
+
pinsByParent.set(parentPath, pin);
|
|
1097
|
+
}
|
|
1098
|
+
// Fail before publishing anything if pinned directories leave no
|
|
1099
|
+
// descriptor available for staging or rollback.
|
|
1100
|
+
const descriptorProbe = fsSync.openSync(os.devNull, fsSync.constants.O_RDONLY | CLOEXEC);
|
|
1101
|
+
fsSync.closeSync(descriptorProbe);
|
|
1102
|
+
}
|
|
858
1103
|
for (const [index, write] of fileWrites.entries()) {
|
|
1104
|
+
if (pinnedPublicationAvailable()) {
|
|
1105
|
+
const pin = pinsByParent.get(path.dirname(write.path));
|
|
1106
|
+
if (!pin) throw new Error(`cannot pin import destination directory: ${path.dirname(write.path)}`);
|
|
1107
|
+
const previous = readRegularAt(pin.parentFd, pin.name, write.path);
|
|
1108
|
+
writeRegularAt(pin.parentFd, pin.name, write.content, index, write.path);
|
|
1109
|
+
snapshots.push({ path: write.path, previous, parentFd: pin.parentFd, name: pin.name });
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
const symlinked = await findSymlinkedAncestor(destination.root, write.path);
|
|
1113
|
+
if (symlinked) throw new Error(`refusing to write through symlinked ancestor: ${symlinked}`);
|
|
859
1114
|
await fs.mkdir(path.dirname(write.path), { recursive: true, mode: 0o700 });
|
|
860
1115
|
const stat = await fs.lstat(write.path).catch(() => null);
|
|
861
1116
|
if (stat?.isSymbolicLink()) throw new Error(`refusing to write through symlink: ${write.path}`);
|
|
@@ -891,8 +1146,18 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
|
|
|
891
1146
|
|
|
892
1147
|
// --- Phase 3: verification (inside the transaction) ------------------
|
|
893
1148
|
for (const write of fileWrites) {
|
|
894
|
-
const
|
|
895
|
-
|
|
1149
|
+
const pinned = snapshots.find(snapshot => snapshot.path === write.path && snapshot.parentFd !== undefined);
|
|
1150
|
+
let persisted: string | null;
|
|
1151
|
+
if (pinned?.parentFd !== undefined && pinned.name) {
|
|
1152
|
+
assertPinnedParentStillAt(pinned.parentFd, path.dirname(write.path), write.path);
|
|
1153
|
+
persisted = readRegularAt(pinned.parentFd, pinned.name, write.path);
|
|
1154
|
+
assertPinnedParentStillAt(pinned.parentFd, path.dirname(write.path), write.path);
|
|
1155
|
+
} else {
|
|
1156
|
+
persisted = await readStructured(write.path).then(result =>
|
|
1157
|
+
result.kind === "value" ? result.content : null,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
if (persisted !== write.content) {
|
|
896
1161
|
throw new Error(`post-import verification failed: ${write.path} was not persisted`);
|
|
897
1162
|
}
|
|
898
1163
|
}
|
|
@@ -910,7 +1175,9 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
|
|
|
910
1175
|
const rollbackErrors: string[] = [];
|
|
911
1176
|
for (const snapshot of snapshots.reverse()) {
|
|
912
1177
|
try {
|
|
913
|
-
if (snapshot.
|
|
1178
|
+
if (snapshot.parentFd !== undefined && snapshot.name) {
|
|
1179
|
+
restoreRegularAt(snapshot.parentFd, snapshot.name, snapshot.previous, snapshot.path);
|
|
1180
|
+
} else if (snapshot.previous === null) {
|
|
914
1181
|
await fs.rm(snapshot.path, { force: true });
|
|
915
1182
|
} else {
|
|
916
1183
|
const staged = `${snapshot.path}.gjc-rollback-${process.pid}.tmp`;
|
|
@@ -928,6 +1195,8 @@ export async function applyImport(plan: ImportPlan, options: { cwd: string }): P
|
|
|
928
1195
|
`import failed and was rolled back: ${reason}` +
|
|
929
1196
|
(rollbackErrors.length > 0 ? `; ROLLBACK ERRORS (manual repair needed): ${rollbackErrors.join("; ")}` : ""),
|
|
930
1197
|
);
|
|
1198
|
+
} finally {
|
|
1199
|
+
for (const pin of pins) closeRawFds(pin.fds);
|
|
931
1200
|
}
|
|
932
1201
|
|
|
933
1202
|
for (const { entry } of writable) {
|