@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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Database, type Statement } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import * as fsPromises from "node:fs/promises";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { getHistoryDbPath, logger } from "@gajae-code/utils";
|
|
6
5
|
|
|
@@ -26,6 +25,155 @@ function escapeLikePattern(text: string): string {
|
|
|
26
25
|
return text.replace(/[\\%_]/g, "\\$&");
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
const HISTORY_SUFFIXES = ["", "-wal", "-shm"] as const;
|
|
29
|
+
let openingHistory: Database | undefined;
|
|
30
|
+
|
|
31
|
+
function secureHistoryPath(pathname: string, kind: "directory" | "file"): void {
|
|
32
|
+
if (process.platform !== "win32") return;
|
|
33
|
+
const native = require("@gajae-code/natives") as typeof import("@gajae-code/natives");
|
|
34
|
+
const applied = native.applyOwnerOnlyPathSecurity(pathname, kind);
|
|
35
|
+
const verified = native.verifyOwnerOnlyPathSecurity(pathname, kind);
|
|
36
|
+
if (applied?.ok !== true || verified?.ok !== true) {
|
|
37
|
+
throw new Error("history database owner-only protection could not be established");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function historyErrno(error: unknown): string | undefined {
|
|
42
|
+
return (error as NodeJS.ErrnoException | undefined)?.code;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function historyLeaf(dbPath: string, suffix: string): fs.BigIntStats | undefined {
|
|
46
|
+
try {
|
|
47
|
+
return fs.lstatSync(`${dbPath}${suffix}`, { bigint: true });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (historyErrno(error) !== "ENOENT") throw error;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertRegularHistoryLeaf(existing: fs.BigIntStats | undefined): void {
|
|
55
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
|
|
56
|
+
throw new Error("history database path is not a regular file");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertHistoryLeaves(dbPath: string): Set<string> {
|
|
61
|
+
const present = new Set<string>();
|
|
62
|
+
for (const suffix of HISTORY_SUFFIXES) {
|
|
63
|
+
const existing = historyLeaf(dbPath, suffix);
|
|
64
|
+
assertRegularHistoryLeaf(existing);
|
|
65
|
+
if (existing) present.add(suffix);
|
|
66
|
+
}
|
|
67
|
+
return present;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeNewHistoryLeaves(): void {
|
|
71
|
+
// A WAL reader can stay live while BEGIN EXCLUSIVE succeeds, so failure
|
|
72
|
+
// cleanup must not unlink a database another opener has adopted.
|
|
73
|
+
try {
|
|
74
|
+
openingHistory?.close();
|
|
75
|
+
} catch {
|
|
76
|
+
// The caller reports the original open failure.
|
|
77
|
+
}
|
|
78
|
+
openingHistory = undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readHistoryDirectory(dir: string): fs.Stats | undefined {
|
|
82
|
+
let existing: fs.Stats;
|
|
83
|
+
try {
|
|
84
|
+
existing = fs.lstatSync(dir);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (historyErrno(error) !== "ENOENT") throw error;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
90
|
+
throw new Error("history database directory is not a real directory");
|
|
91
|
+
}
|
|
92
|
+
return existing;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tightenHistoryDirectory(dir: string): void {
|
|
96
|
+
let existing = readHistoryDirectory(dir);
|
|
97
|
+
if (!existing) {
|
|
98
|
+
try {
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (historyErrno(error) !== "EEXIST") throw error;
|
|
102
|
+
}
|
|
103
|
+
existing = readHistoryDirectory(dir);
|
|
104
|
+
if (!existing) throw new Error("history database directory is not a real directory");
|
|
105
|
+
}
|
|
106
|
+
secureHistoryPath(dir, "directory");
|
|
107
|
+
if (process.platform === "win32") return;
|
|
108
|
+
// 0300/0700 are already owner-only. Sticky directories are shared and stay as they are.
|
|
109
|
+
// chmod(2) needs no read bit, so mode 0333 becomes 0700. A symlink swap is
|
|
110
|
+
// followed by chmod; the recheck then refuses and no database file is created.
|
|
111
|
+
if ((existing.mode & 0o077) === 0 || (existing.mode & 0o1000) !== 0) return;
|
|
112
|
+
try {
|
|
113
|
+
fs.chmodSync(dir, 0o700);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (historyErrno(error) === "ELOOP") throw new Error("history database directory is not a real directory");
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
const after = readHistoryDirectory(dir);
|
|
119
|
+
if (!after || (after.mode & 0o077) !== 0) {
|
|
120
|
+
throw new Error("history database directory could not be tightened to mode 0700");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sameHistoryFile(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
|
|
125
|
+
return left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function restrictHistoryFiles(dbPath: string, createMissingDb: boolean): void {
|
|
129
|
+
const noFollow = process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0);
|
|
130
|
+
for (const suffix of HISTORY_SUFFIXES) {
|
|
131
|
+
const target = `${dbPath}${suffix}`;
|
|
132
|
+
let existing = historyLeaf(dbPath, suffix);
|
|
133
|
+
assertRegularHistoryLeaf(existing);
|
|
134
|
+
if (!existing && suffix === "" && createMissingDb) {
|
|
135
|
+
let created: number | undefined;
|
|
136
|
+
try {
|
|
137
|
+
created = fs.openSync(
|
|
138
|
+
target,
|
|
139
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
|
|
140
|
+
0o600,
|
|
141
|
+
);
|
|
142
|
+
fs.fchmodSync(created, 0o600);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (historyErrno(error) === "ELOOP") throw new Error("history database path is not a regular file");
|
|
145
|
+
if (historyErrno(error) !== "EEXIST") throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
if (created !== undefined) fs.closeSync(created);
|
|
148
|
+
}
|
|
149
|
+
existing = historyLeaf(dbPath, suffix);
|
|
150
|
+
assertRegularHistoryLeaf(existing);
|
|
151
|
+
if (!existing) throw new Error("history database path is not a regular file");
|
|
152
|
+
}
|
|
153
|
+
if (!existing) continue;
|
|
154
|
+
secureHistoryPath(target, "file");
|
|
155
|
+
if (process.platform === "win32" || (Number(existing.mode) & 0o077) === 0) continue;
|
|
156
|
+
let fd: number | undefined;
|
|
157
|
+
try {
|
|
158
|
+
fd = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
|
|
159
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
160
|
+
const again = historyLeaf(dbPath, suffix);
|
|
161
|
+
if (!again || !opened.isFile() || !sameHistoryFile(existing, opened) || !sameHistoryFile(opened, again)) {
|
|
162
|
+
throw new Error("history database path is not a regular file");
|
|
163
|
+
}
|
|
164
|
+
fs.fchmodSync(fd, 0o600);
|
|
165
|
+
if ((Number(fs.fstatSync(fd, { bigint: true }).mode) & 0o777) !== 0o600) {
|
|
166
|
+
throw new Error("history database path could not be tightened to mode 0600");
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (historyErrno(error) === "ELOOP") throw new Error("history database path is not a regular file");
|
|
170
|
+
throw error;
|
|
171
|
+
} finally {
|
|
172
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
29
177
|
class AsyncDrain<T> {
|
|
30
178
|
#queue?: T[];
|
|
31
179
|
#promise = Promise.resolve();
|
|
@@ -83,6 +231,7 @@ export class HistoryStorage {
|
|
|
83
231
|
|
|
84
232
|
private constructor(dbPath: string) {
|
|
85
233
|
this.#db = new Database(dbPath);
|
|
234
|
+
openingHistory = this.#db;
|
|
86
235
|
|
|
87
236
|
const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
|
|
88
237
|
|
|
@@ -137,12 +286,32 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
137
286
|
|
|
138
287
|
const last = this.#lastPromptStmt.get() as { prompt?: string } | undefined;
|
|
139
288
|
this.#lastPromptCache = last?.prompt ?? null;
|
|
289
|
+
try {
|
|
290
|
+
restrictHistoryFiles(dbPath, false);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.#db.close();
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
openingHistory = undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
static #openNew(dbPath: string): HistoryStorage {
|
|
299
|
+
openingHistory = undefined;
|
|
300
|
+
assertHistoryLeaves(dbPath);
|
|
301
|
+
try {
|
|
302
|
+
tightenHistoryDirectory(path.dirname(dbPath));
|
|
303
|
+
assertHistoryLeaves(dbPath);
|
|
304
|
+
restrictHistoryFiles(dbPath, true);
|
|
305
|
+
return new HistoryStorage(dbPath);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
removeNewHistoryLeaves();
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
140
310
|
}
|
|
141
311
|
|
|
142
312
|
static open(dbPath: string = getHistoryDbPath()): HistoryStorage {
|
|
143
313
|
if (!HistoryStorage.#instance) {
|
|
144
|
-
|
|
145
|
-
HistoryStorage.#instance = new HistoryStorage(dbPath);
|
|
314
|
+
HistoryStorage.#instance = HistoryStorage.#openNew(dbPath);
|
|
146
315
|
}
|
|
147
316
|
return HistoryStorage.#instance;
|
|
148
317
|
}
|
|
@@ -150,16 +319,17 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
150
319
|
static async openAsync(dbPath: string = getHistoryDbPath()): Promise<HistoryStorage> {
|
|
151
320
|
if (HistoryStorage.#instance) return HistoryStorage.#instance;
|
|
152
321
|
if (!HistoryStorage.#opening) {
|
|
153
|
-
HistoryStorage.#opening = (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
322
|
+
HistoryStorage.#opening = Promise.resolve()
|
|
323
|
+
.then(() => {
|
|
324
|
+
const storage = HistoryStorage.#openNew(dbPath);
|
|
325
|
+
HistoryStorage.#instance ??= storage;
|
|
326
|
+
return HistoryStorage.#instance;
|
|
327
|
+
})
|
|
328
|
+
.finally(() => {
|
|
329
|
+
HistoryStorage.#opening = undefined;
|
|
330
|
+
});
|
|
161
331
|
}
|
|
162
|
-
return
|
|
332
|
+
return HistoryStorage.#opening;
|
|
163
333
|
}
|
|
164
334
|
|
|
165
335
|
/** @internal Reset the singleton — test-only. */
|
|
@@ -228,14 +228,22 @@ function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandid
|
|
|
228
228
|
function assertRetainedManagedDirectoryIdentity(scope: ManagedScope): void {
|
|
229
229
|
const expected = managedDirectoryIdentities.get(scope);
|
|
230
230
|
if (!expected) return;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
assertSameManagedDirectoryIdentity(fs.lstatSync(scope.directoryPath, { bigint: true }), expected);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Fence a managed directory to the identity it was bound with. The decision is
|
|
236
|
+
* the same for every mismatch; only the reported reason differs. The same
|
|
237
|
+
* inode on a different `dev` is the shape a remount, drive-letter change or
|
|
238
|
+
* volume-serial change leaves behind (#6750). It is still refused, because an
|
|
239
|
+
* inode is only unique within one volume, but it is named so an operator is
|
|
240
|
+
* not sent looking for a corrupted binding.
|
|
241
|
+
*/
|
|
242
|
+
function assertSameManagedDirectoryIdentity(current: fs.BigIntStats, expected: { dev: bigint; ino: bigint }): void {
|
|
243
|
+
if (!current.isDirectory() || current.isSymbolicLink()) throw new Error("Managed session directory changed");
|
|
244
|
+
if (current.dev === expected.dev && current.ino === expected.ino) return;
|
|
245
|
+
if (current.ino === expected.ino) throw new Error("volume_identity_changed");
|
|
246
|
+
throw new Error("Managed session directory changed");
|
|
239
247
|
}
|
|
240
248
|
|
|
241
249
|
export function managedDirectoryAuthorityForScope(scope: ManagedScope): RecoveryFsRoot | undefined {
|
|
@@ -319,7 +327,22 @@ export type ManagedScopeResolution =
|
|
|
319
327
|
* propagating them keeps startup redaction intact while telling the operator
|
|
320
328
|
* which invariant actually failed instead of a blanket `binding_invalid`.
|
|
321
329
|
*/
|
|
322
|
-
const managedScopeIdentityFailureCodes = new Set([
|
|
330
|
+
const managedScopeIdentityFailureCodes = new Set([
|
|
331
|
+
"identity_mismatch",
|
|
332
|
+
"volume_identity_changed",
|
|
333
|
+
"reparse_point",
|
|
334
|
+
"not_directory",
|
|
335
|
+
"not_file",
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Fixed, path-free tokens from the replacement-cleanup receipt preflight that
|
|
340
|
+
* runs before every no-replace publish. A stale receipt the preflight refuses
|
|
341
|
+
* (for example one whose filename encodes a device number that changed after a
|
|
342
|
+
* reboot, #6820) blocks an otherwise healthy binding publish; reporting it as
|
|
343
|
+
* `binding_invalid` sends operators to the binding instead of the receipt.
|
|
344
|
+
*/
|
|
345
|
+
const managedScopeReceiptFailureCodes = new Set(["managed_replace_cleanup_receipt_invalid"]);
|
|
323
346
|
|
|
324
347
|
/**
|
|
325
348
|
* Fixed, path-free errno tokens. A managed scope that fails to prepare because
|
|
@@ -376,6 +399,7 @@ function isManagedScopeCapacityMessage(message: string): boolean {
|
|
|
376
399
|
function managedScopeResidualClassification(error: unknown): string | undefined {
|
|
377
400
|
if (!(error instanceof Error)) return undefined;
|
|
378
401
|
if (managedScopeIdentityFailureCodes.has(error.message)) return error.message;
|
|
402
|
+
if (managedScopeReceiptFailureCodes.has(error.message)) return error.message;
|
|
379
403
|
const errno = (error as NodeJS.ErrnoException).code;
|
|
380
404
|
return errno !== undefined && managedScopeErrnoFailureCodes.has(errno) ? errno : undefined;
|
|
381
405
|
}
|
|
@@ -508,6 +532,7 @@ type NativeIdentity =
|
|
|
508
532
|
type CanonicalNativeIdentity = Extract<NativeIdentity, { ok: true }>;
|
|
509
533
|
|
|
510
534
|
type NativeIdentityFailureCode =
|
|
535
|
+
| "access_denied"
|
|
511
536
|
| "not_found"
|
|
512
537
|
| "not_directory"
|
|
513
538
|
| "not_utf8"
|
|
@@ -618,6 +643,12 @@ function nativeFailure(code: NativeIdentityFailureCode): ManagedScopeResolution
|
|
|
618
643
|
return { kind: "error", code: "cwd_missing", message: "The workspace directory does not exist." };
|
|
619
644
|
if (code === "not_directory")
|
|
620
645
|
return { kind: "error", code: "cwd_not_directory", message: "The workspace path is not a directory." };
|
|
646
|
+
if (code === "access_denied")
|
|
647
|
+
return {
|
|
648
|
+
kind: "error",
|
|
649
|
+
code: "permission_denied",
|
|
650
|
+
message: "Permission was denied while identifying the workspace directory.",
|
|
651
|
+
};
|
|
621
652
|
if (code === "network_unsupported") {
|
|
622
653
|
return {
|
|
623
654
|
kind: "error",
|
|
@@ -628,6 +659,10 @@ function nativeFailure(code: NativeIdentityFailureCode): ManagedScopeResolution
|
|
|
628
659
|
return { kind: "error", code: "identity_unavailable", message: "Workspace directory identity is unavailable." };
|
|
629
660
|
}
|
|
630
661
|
|
|
662
|
+
function nativeIdentityDiagnosticCode(code: NativeIdentityFailureCode): string {
|
|
663
|
+
return code === "access_denied" ? "permission_denied" : `cwd_${code}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
631
666
|
function bindingFor(scope: ManagedScope): Binding {
|
|
632
667
|
return {
|
|
633
668
|
schemaVersion: 1,
|
|
@@ -1107,7 +1142,7 @@ function inspectCandidate(
|
|
|
1107
1142
|
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
|
|
1108
1143
|
return { code: "invalid_header" };
|
|
1109
1144
|
const cwdIdentity = identityFor(header.cwd);
|
|
1110
|
-
if (!cwdIdentity.ok) return { code:
|
|
1145
|
+
if (!cwdIdentity.ok) return { code: nativeIdentityDiagnosticCode(cwdIdentity.code) };
|
|
1111
1146
|
const named = fs.lstatSync(filePath, { bigint: true });
|
|
1112
1147
|
if (
|
|
1113
1148
|
!named.isFile() ||
|
|
@@ -1872,13 +1907,7 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
1872
1907
|
managedDirectoryAuthorities.set(scope, retainedAuthority);
|
|
1873
1908
|
stage = "scope_revalidate";
|
|
1874
1909
|
const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
1875
|
-
|
|
1876
|
-
!directoryStat.isDirectory() ||
|
|
1877
|
-
directoryStat.isSymbolicLink() ||
|
|
1878
|
-
directoryStat.dev !== preparedDirectory.dev ||
|
|
1879
|
-
directoryStat.ino !== preparedDirectory.ino
|
|
1880
|
-
)
|
|
1881
|
-
throw new Error("Managed session directory changed");
|
|
1910
|
+
assertSameManagedDirectoryIdentity(directoryStat, preparedDirectory);
|
|
1882
1911
|
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
1883
1912
|
if (validated) {
|
|
1884
1913
|
if (validated.kind !== "error") throw new Error("Unexpected managed scope binding result");
|
|
@@ -1954,7 +1983,7 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
1954
1983
|
const candidateIdentity = identityFor(candidate.cwd);
|
|
1955
1984
|
if (!candidateIdentity.ok) {
|
|
1956
1985
|
invalid.push({
|
|
1957
|
-
code:
|
|
1986
|
+
code: nativeIdentityDiagnosticCode(candidateIdentity.code),
|
|
1958
1987
|
...(safeManagedCandidateSessionId(candidate.sessionId) === undefined
|
|
1959
1988
|
? {}
|
|
1960
1989
|
: { sessionId: candidate.sessionId }),
|
|
@@ -170,7 +170,11 @@ function exactUnlinkCompleted(result: NativeExactUnlinkResult): boolean {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
function isRetryableReplacementReceiptCleanupError(error: unknown): boolean {
|
|
173
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
error instanceof Error &&
|
|
175
|
+
(error.message === "managed_replace_receipt_cleanup_pending:io_error" ||
|
|
176
|
+
error.message === "managed_replace_receipt_cleanup_pending:access_denied")
|
|
177
|
+
);
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
/**
|
|
@@ -450,6 +454,17 @@ function replacementReceiptIsOwned(bytes: Uint8Array): boolean {
|
|
|
450
454
|
return observed.status !== "present" || observed.incarnation === publisher.incarnation;
|
|
451
455
|
}
|
|
452
456
|
|
|
457
|
+
/** Only positive local process-incarnation evidence proves a mismatched receipt's publisher is stale. */
|
|
458
|
+
function replacementReceiptPublisherIsVerifiedDead(bytes: Uint8Array): boolean {
|
|
459
|
+
const publisher = replacementReceiptPublisher(bytes);
|
|
460
|
+
if (!publisher || publisher.host !== os.hostname()) return false;
|
|
461
|
+
if (publisher.pid === process.pid && publisher.ownerId === replacementPublisherOwnerId) return false;
|
|
462
|
+
const observed = observeProcessIncarnation(publisher.pid);
|
|
463
|
+
return (
|
|
464
|
+
observed.status === "absent" || (observed.status === "present" && observed.incarnation !== publisher.incarnation)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
453
468
|
type LegacyReplacementCleanupIdentity = {
|
|
454
469
|
dev: bigint;
|
|
455
470
|
ino: bigint;
|
|
@@ -1100,6 +1115,7 @@ const GENERAL_FAILURE_CODES = new Set([
|
|
|
1100
1115
|
"identity_unavailable",
|
|
1101
1116
|
"identity_mismatch",
|
|
1102
1117
|
"owner_mismatch",
|
|
1118
|
+
"access_denied",
|
|
1103
1119
|
"mode_mismatch",
|
|
1104
1120
|
"io_error",
|
|
1105
1121
|
]);
|
|
@@ -1964,6 +1980,59 @@ export class ManagedSessionDescendantStore {
|
|
|
1964
1980
|
return true;
|
|
1965
1981
|
}
|
|
1966
1982
|
|
|
1983
|
+
#recoverRenumberedReplacementCleanupReceipt(
|
|
1984
|
+
receiptPath: string,
|
|
1985
|
+
binding: { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } },
|
|
1986
|
+
receipt: ManagedFileSnapshot,
|
|
1987
|
+
): boolean {
|
|
1988
|
+
const parsed = parseReplacementCleanupReceipt(receipt.bytes);
|
|
1989
|
+
const predecessor = parsed ? parseReplacementIdentity(parsed.predecessor) : undefined;
|
|
1990
|
+
if (
|
|
1991
|
+
receipt.identity.dev === binding.receipt.dev ||
|
|
1992
|
+
receipt.identity.ino !== binding.receipt.ino ||
|
|
1993
|
+
!parsed ||
|
|
1994
|
+
!predecessor ||
|
|
1995
|
+
predecessor.dev !== binding.predecessor.dev ||
|
|
1996
|
+
predecessor.ino !== binding.predecessor.ino ||
|
|
1997
|
+
!replacementReceiptPublisherIsVerifiedDead(receipt.bytes)
|
|
1998
|
+
)
|
|
1999
|
+
return false;
|
|
2000
|
+
|
|
2001
|
+
// The filename's device is the only identity component allowed to drift.
|
|
2002
|
+
// The no-follow snapshot supplies the exact current size and digest, and
|
|
2003
|
+
// detachOnly preserves those bytes under a non-canonical quarantine name.
|
|
2004
|
+
const quarantineName = replacementReceiptRetirementName(receipt.identity, binding.predecessor);
|
|
2005
|
+
const detached = nativeSessionStorage().exactUnlink(receiptPath, {
|
|
2006
|
+
dev: receipt.identity.dev,
|
|
2007
|
+
ino: receipt.identity.ino,
|
|
2008
|
+
nlink: receipt.identity.nlink,
|
|
2009
|
+
parentDev: this.#subtreeRoot.dev,
|
|
2010
|
+
parentIno: this.#subtreeRoot.ino,
|
|
2011
|
+
size: BigInt(receipt.identity.size),
|
|
2012
|
+
mtimeNs: receipt.identity.mtimeNs,
|
|
2013
|
+
sha256: receipt.identity.sha256,
|
|
2014
|
+
detachOnly: true,
|
|
2015
|
+
quarantineName,
|
|
2016
|
+
});
|
|
2017
|
+
if (isPeerOwnedReceiptRetirement(detached)) return true;
|
|
2018
|
+
if (
|
|
2019
|
+
!detached.ok &&
|
|
2020
|
+
detached.code === "not_found" &&
|
|
2021
|
+
detached.retainedSuccessorPath === undefined &&
|
|
2022
|
+
detached.retainedUnknownPath === undefined
|
|
2023
|
+
)
|
|
2024
|
+
return true;
|
|
2025
|
+
if (
|
|
2026
|
+
(!detached.ok && detached.code !== "cleanup_pending") ||
|
|
2027
|
+
detached.detachedPath !== path.join(this.#baseDir, quarantineName) ||
|
|
2028
|
+
detached.retainedSuccessorPath ||
|
|
2029
|
+
detached.retainedUnknownPath
|
|
2030
|
+
)
|
|
2031
|
+
throw new Error(`managed_replace_receipt_cleanup_pending:${detached.code ?? "unknown"}`);
|
|
2032
|
+
fsyncDirectory(this.#baseDir);
|
|
2033
|
+
return true;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1967
2036
|
#detachReplacementCleanupReceipt(receiptPath: string): void {
|
|
1968
2037
|
const binding = replacementCleanupReceiptBinding(path.basename(receiptPath));
|
|
1969
2038
|
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
@@ -1975,6 +2044,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1975
2044
|
throw error;
|
|
1976
2045
|
}
|
|
1977
2046
|
if (receipt.identity.dev !== binding.receipt.dev || receipt.identity.ino !== binding.receipt.ino) {
|
|
2047
|
+
if (this.#recoverRenumberedReplacementCleanupReceipt(receiptPath, binding, receipt)) return;
|
|
1978
2048
|
if (this.#recoverReplacementCleanupPlaceholder(receiptPath, binding, receipt)) return;
|
|
1979
2049
|
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
1980
2050
|
}
|
|
@@ -540,6 +540,76 @@ function isUnwritableResidentCacheFailure(error: unknown): boolean {
|
|
|
540
540
|
);
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
/** Nearest real directory at or above `start`. A file or missing path is not a root. */
|
|
544
|
+
function nearestExistingDirectory(start: string): string | null {
|
|
545
|
+
let current = path.resolve(start);
|
|
546
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
547
|
+
try {
|
|
548
|
+
if (fs.lstatSync(current).isDirectory()) return current;
|
|
549
|
+
} catch {
|
|
550
|
+
// Missing components are not worktree roots.
|
|
551
|
+
}
|
|
552
|
+
const parent = path.dirname(current);
|
|
553
|
+
if (parent === current) return null;
|
|
554
|
+
current = parent;
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Canonical path of `start`, following a symlink ancestor.
|
|
561
|
+
* The unmatched suffix is appended so a not-yet-created cache root classifies
|
|
562
|
+
* the directory `openVerifiedResidentCacheInstanceDir` would create it in.
|
|
563
|
+
*/
|
|
564
|
+
function canonicalCachePath(start: string): string | null {
|
|
565
|
+
let current = path.resolve(start);
|
|
566
|
+
const pending: string[] = [];
|
|
567
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
568
|
+
try {
|
|
569
|
+
const stat = fs.lstatSync(current);
|
|
570
|
+
if (stat.isDirectory() || stat.isSymbolicLink()) {
|
|
571
|
+
const real = fs.realpathSync(current);
|
|
572
|
+
return pending.length === 0 ? real : path.join(real, ...pending.reverse());
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
// Missing components are resolved from the nearest existing ancestor.
|
|
576
|
+
}
|
|
577
|
+
const parent = path.dirname(current);
|
|
578
|
+
if (parent === current) return null;
|
|
579
|
+
pending.push(path.basename(current));
|
|
580
|
+
current = parent;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function cacheDirectoryInsideGitWorktree(directory: string): boolean {
|
|
586
|
+
return git.repo.resolveSync(directory) !== null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resident text is a second plaintext copy of assistant and tool output.
|
|
591
|
+
* A cache root inside a Git worktree can be staged by a later `git add`, so
|
|
592
|
+
* that root stays in memory. Both the lexical directory and the canonical
|
|
593
|
+
* directory are classified: a symlink outside the worktree must not hide a
|
|
594
|
+
* target inside it, and a lexical worktree path stays refused when its
|
|
595
|
+
* canonical target is outside. A missing or non-directory root still reaches
|
|
596
|
+
* the resident-cache trust check when both views are outside a worktree.
|
|
597
|
+
* No existing directory within 64 ancestors, or a resolver error, stays in memory.
|
|
598
|
+
*/
|
|
599
|
+
export function residentTextCacheRootAllowsDisk(cacheRoot: string): boolean {
|
|
600
|
+
try {
|
|
601
|
+
const lexical = nearestExistingDirectory(cacheRoot);
|
|
602
|
+
if (lexical === null || cacheDirectoryInsideGitWorktree(lexical)) return false;
|
|
603
|
+
const canonical = canonicalCachePath(cacheRoot);
|
|
604
|
+
if (canonical === null) return false;
|
|
605
|
+
const canonicalDirectory = nearestExistingDirectory(canonical);
|
|
606
|
+
if (canonicalDirectory === null || cacheDirectoryInsideGitWorktree(canonicalDirectory)) return false;
|
|
607
|
+
return true;
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
543
613
|
export interface SessionEntryBase {
|
|
544
614
|
type: string;
|
|
545
615
|
id: string;
|
|
@@ -7311,6 +7381,17 @@ function sessionMatchesResumeArg(session: SessionInfo, sessionArg: string): bool
|
|
|
7311
7381
|
return fileSessionId.startsWith(normalizedArg);
|
|
7312
7382
|
}
|
|
7313
7383
|
|
|
7384
|
+
/**
|
|
7385
|
+
* Equal session id first, the same `id ===` check managed resume already uses.
|
|
7386
|
+
* Prefix matching stays for every other argument, in list order.
|
|
7387
|
+
*/
|
|
7388
|
+
export function pickResumableSession(sessions: readonly SessionInfo[], sessionArg: string): SessionInfo | undefined {
|
|
7389
|
+
return (
|
|
7390
|
+
sessions.find(session => session.id === sessionArg) ??
|
|
7391
|
+
sessions.find(session => sessionMatchesResumeArg(session, sessionArg))
|
|
7392
|
+
);
|
|
7393
|
+
}
|
|
7394
|
+
|
|
7314
7395
|
export async function resolveResumableSession(
|
|
7315
7396
|
sessionArg: string,
|
|
7316
7397
|
cwd: string,
|
|
@@ -7321,7 +7402,7 @@ export async function resolveResumableSession(
|
|
|
7321
7402
|
const localSessions = sessionDir
|
|
7322
7403
|
? await SessionManager.list(cwd, sessionDir, storage)
|
|
7323
7404
|
: await SessionManager.listManagedForResumePickerReadOnly(cwd, managedAgentDir, storage);
|
|
7324
|
-
const localMatch = localSessions
|
|
7405
|
+
const localMatch = pickResumableSession(localSessions, sessionArg);
|
|
7325
7406
|
if (localMatch) {
|
|
7326
7407
|
return { session: localMatch, scope: "local" };
|
|
7327
7408
|
}
|
|
@@ -7331,7 +7412,7 @@ export async function resolveResumableSession(
|
|
|
7331
7412
|
}
|
|
7332
7413
|
|
|
7333
7414
|
const globalSessions = await SessionManager.listAll(storage, managedAgentDir);
|
|
7334
|
-
const globalMatch = globalSessions
|
|
7415
|
+
const globalMatch = pickResumableSession(globalSessions, sessionArg);
|
|
7335
7416
|
if (!globalMatch) {
|
|
7336
7417
|
return undefined;
|
|
7337
7418
|
}
|
|
@@ -8092,9 +8173,13 @@ export class SessionManager {
|
|
|
8092
8173
|
this.#residentCacheWin32FallbackCount++;
|
|
8093
8174
|
return { store: this.#newCanonicalResidentTextStore(), ownsStore: false };
|
|
8094
8175
|
}
|
|
8095
|
-
const
|
|
8096
|
-
|
|
8097
|
-
)
|
|
8176
|
+
const cacheRoot = getResidentCacheRootDir(this.#residentCacheProfileAgentDir());
|
|
8177
|
+
// Not a provisioning failure: retain-and-throw must still commit, with no file to stage.
|
|
8178
|
+
if (!residentTextCacheRootAllowsDisk(cacheRoot)) {
|
|
8179
|
+
this.#residentCacheAdoptFallbackCount++;
|
|
8180
|
+
return { store: this.#newCanonicalResidentTextStore(), ownsStore: false };
|
|
8181
|
+
}
|
|
8182
|
+
const instanceDir = openVerifiedResidentCacheInstanceDir(cacheRoot);
|
|
8098
8183
|
return { store: EphemeralBlobStore.adoptVerifiedDir(instanceDir), ownsStore: true };
|
|
8099
8184
|
}
|
|
8100
8185
|
|
|
@@ -11,7 +11,6 @@ export const IMPORT_REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
|
11
11
|
/** Bumped when patterns change; persisted in import provenance. */
|
|
12
12
|
export const IMPORT_SANITIZER_VERSION = 2;
|
|
13
13
|
|
|
14
|
-
const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/gu;
|
|
15
14
|
const UNSAFE_CONTROL =
|
|
16
15
|
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/gu;
|
|
17
16
|
const HEADER_CREDENTIAL = /(\b(?:authorization|cookie|set-cookie)\b[ \t]*:[ \t]*)([^\r\n]*)/giu;
|
|
@@ -80,6 +79,58 @@ const REDACTION_RULES: readonly RedactionRule[] = [
|
|
|
80
79
|
},
|
|
81
80
|
];
|
|
82
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Linear CSI/OSC strip. An unterminated `ESC ]` used to make the global
|
|
84
|
+
* replace rescan every later suffix, which is quadratic in the prefix count.
|
|
85
|
+
*/
|
|
86
|
+
function stripImportedAnsiEscapes(input: string): string {
|
|
87
|
+
if (!input.includes("\u001b")) return input;
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
let from = 0;
|
|
90
|
+
let i = 0;
|
|
91
|
+
let oscLive = true;
|
|
92
|
+
const n = input.length;
|
|
93
|
+
while (i < n) {
|
|
94
|
+
const esc = input.indexOf("\u001b", i);
|
|
95
|
+
if (esc < 0 || esc + 1 >= n) break;
|
|
96
|
+
const kind = input.charCodeAt(esc + 1);
|
|
97
|
+
let end = -1;
|
|
98
|
+
if (kind === 0x5b) end = csiSequenceEnd(input, esc + 2);
|
|
99
|
+
else if (kind === 0x5d && oscLive) end = oscSequenceEnd(input, esc + 2);
|
|
100
|
+
// No BEL and no ST anywhere after this prefix, so later OSC cannot terminate.
|
|
101
|
+
if (kind === 0x5d && end < 0) oscLive = false;
|
|
102
|
+
if (end < 0) {
|
|
103
|
+
i = esc + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (esc > from) parts.push(input.slice(from, esc));
|
|
107
|
+
from = end;
|
|
108
|
+
i = end;
|
|
109
|
+
}
|
|
110
|
+
if (from === 0) return input;
|
|
111
|
+
if (from < n) parts.push(input.slice(from));
|
|
112
|
+
return parts.join("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function csiSequenceEnd(input: string, j: number): number {
|
|
116
|
+
const n = input.length;
|
|
117
|
+
while (j < n && input.charCodeAt(j) >= 0x30 && input.charCodeAt(j) <= 0x3f) j++;
|
|
118
|
+
while (j < n && input.charCodeAt(j) >= 0x20 && input.charCodeAt(j) <= 0x2f) j++;
|
|
119
|
+
const finalByte = input.charCodeAt(j);
|
|
120
|
+
return finalByte >= 0x40 && finalByte <= 0x7e ? j + 1 : -1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function oscSequenceEnd(input: string, index: number): number {
|
|
124
|
+
// One forward scan. A second search to the end of the suffix is quadratic for repeated OSC sequences.
|
|
125
|
+
const n = input.length;
|
|
126
|
+
for (let j = index; j < n; j++) {
|
|
127
|
+
const code = input.charCodeAt(j);
|
|
128
|
+
if (code === 0x07) return j + 1;
|
|
129
|
+
if (code === 0x1b && j + 1 < n && input.charCodeAt(j + 1) === 0x5c) return j + 2;
|
|
130
|
+
}
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
133
|
+
|
|
83
134
|
export interface ImportRedactionResult {
|
|
84
135
|
value: string;
|
|
85
136
|
redacted: number;
|
|
@@ -91,7 +142,7 @@ export interface ImportRedactionResult {
|
|
|
91
142
|
* is used for all kinds so a partial pattern overlap cannot leak fragments.
|
|
92
143
|
*/
|
|
93
144
|
export function redactImportedText(input: string): ImportRedactionResult {
|
|
94
|
-
let value = input
|
|
145
|
+
let value = stripImportedAnsiEscapes(input).replace(UNSAFE_CONTROL, "");
|
|
95
146
|
let redacted = 0;
|
|
96
147
|
const kinds = new Set<string>();
|
|
97
148
|
value = value.replace(HEADER_CREDENTIAL, (_match, prefix: string, raw: string) => {
|