@gajae-code/coding-agent 0.16.7 → 0.17.1
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 +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -1286,14 +1286,14 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1286
1286
|
return new EphemeralBlobStore(dir, { adoptVerifiedDir: true, disposalParentDescriptor: parentDescriptor });
|
|
1287
1287
|
}
|
|
1288
1288
|
|
|
1289
|
-
#cachePut(hash: string, data: Buffer): void {
|
|
1289
|
+
#cachePut(hash: string, data: Buffer, ownership?: typeof TAKE_BLOB_BUFFER_OWNERSHIP): void {
|
|
1290
1290
|
const existing = this.#bufferCache.get(hash);
|
|
1291
1291
|
if (existing) {
|
|
1292
1292
|
this.#bufferCache.delete(hash);
|
|
1293
1293
|
this.#bufferCacheBytes -= existing.byteLength;
|
|
1294
1294
|
}
|
|
1295
1295
|
if (data.byteLength > EphemeralBlobStore.#BUFFER_CACHE_MAX_BYTES) return;
|
|
1296
|
-
this.#bufferCache.set(hash, data);
|
|
1296
|
+
this.#bufferCache.set(hash, ownership === TAKE_BLOB_BUFFER_OWNERSHIP ? data : Buffer.from(data));
|
|
1297
1297
|
this.#bufferCacheBytes += data.byteLength;
|
|
1298
1298
|
for (const [oldHash, oldData] of this.#bufferCache) {
|
|
1299
1299
|
if (this.#bufferCacheBytes <= EphemeralBlobStore.#BUFFER_CACHE_MAX_BYTES) break;
|
|
@@ -1309,18 +1309,18 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1309
1309
|
|
|
1310
1310
|
putSync(data: Buffer, ownership?: typeof TAKE_BLOB_BUFFER_OWNERSHIP): BlobPutResult {
|
|
1311
1311
|
const result = this.#adoptedVerifiedDir ? putResidentCacheBlobSync(this.dir, data) : super.putSync(data);
|
|
1312
|
-
this.#cachePut(result.hash,
|
|
1312
|
+
this.#cachePut(result.hash, data, ownership);
|
|
1313
1313
|
return result;
|
|
1314
1314
|
}
|
|
1315
1315
|
|
|
1316
1316
|
putImmutableSync(data: Buffer): CheckedBlobPutResult {
|
|
1317
1317
|
if (this.#adoptedVerifiedDir) {
|
|
1318
1318
|
const result = putResidentCacheBlobSync(this.dir, data);
|
|
1319
|
-
this.#cachePut(result.hash,
|
|
1319
|
+
this.#cachePut(result.hash, data);
|
|
1320
1320
|
return makeBlobPutResult(result.hash, result.path, data.byteLength);
|
|
1321
1321
|
}
|
|
1322
1322
|
const result = super.putImmutableSync(data);
|
|
1323
|
-
this.#cachePut(result.hash,
|
|
1323
|
+
this.#cachePut(result.hash, data);
|
|
1324
1324
|
return result;
|
|
1325
1325
|
}
|
|
1326
1326
|
|
|
@@ -1347,7 +1347,7 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1347
1347
|
const data = this.#adoptedVerifiedDir
|
|
1348
1348
|
? readVerifiedResidentCacheBlobSync(hash, path.join(this.dir, hash))
|
|
1349
1349
|
: super.getSync(hash);
|
|
1350
|
-
if (data) this.#cachePut(hash,
|
|
1350
|
+
if (data) this.#cachePut(hash, data);
|
|
1351
1351
|
return data;
|
|
1352
1352
|
}
|
|
1353
1353
|
|
|
@@ -1363,7 +1363,7 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1363
1363
|
getCheckedSync(hash: string): Buffer | null {
|
|
1364
1364
|
if (this.#adoptedVerifiedDir) return this.getSync(hash);
|
|
1365
1365
|
const data = super.getCheckedSync(hash);
|
|
1366
|
-
if (data) this.#cachePut(hash,
|
|
1366
|
+
if (data) this.#cachePut(hash, data);
|
|
1367
1367
|
return data;
|
|
1368
1368
|
}
|
|
1369
1369
|
|
|
@@ -1478,9 +1478,9 @@ export class MemoryBlobStore extends BlobStore {
|
|
|
1478
1478
|
return this.putSync(data);
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
putSync(data: Buffer): BlobPutResult {
|
|
1481
|
+
putSync(data: Buffer, ownership?: typeof TAKE_BLOB_BUFFER_OWNERSHIP): BlobPutResult {
|
|
1482
1482
|
const hash = sha256Hex(data);
|
|
1483
|
-
this.#store(hash, Buffer.from(data));
|
|
1483
|
+
this.#store(hash, ownership === TAKE_BLOB_BUFFER_OWNERSHIP ? data : Buffer.from(data));
|
|
1484
1484
|
return makeBlobPutResult(hash, `memory:${hash}`);
|
|
1485
1485
|
}
|
|
1486
1486
|
|
|
@@ -242,13 +242,11 @@ export function cappedExponentialWithFullJitter(
|
|
|
242
242
|
/**
|
|
243
243
|
* Legacy auto-compaction retry delay.
|
|
244
244
|
*
|
|
245
|
-
*
|
|
245
|
+
* Unlike managed fallback, this path recovers
|
|
246
246
|
* Retry-After by regex over provider error prose (`#parseRetryAfterMsFromError`),
|
|
247
247
|
* so it follows the documented legacy rule — `retry.maxDelayMs` caps every
|
|
248
248
|
* legacy session retry delay, including provider retry-after hints. Managed
|
|
249
|
-
* fallback
|
|
250
|
-
* compaction has no such budget, so the final candidate would otherwise sleep
|
|
251
|
-
* for the full server-suggested duration.
|
|
249
|
+
* fallback ignores these hints when scheduling its attempt-based retries.
|
|
252
250
|
*
|
|
253
251
|
* `maxDelayMs <= 0` means "no cap", matching `cappedExponentialWithFullJitter`.
|
|
254
252
|
* A missing, NaN, or infinite hint collapses to "no usable hint".
|
|
@@ -303,13 +301,11 @@ export function describeCompactionCandidateFailures(
|
|
|
303
301
|
);
|
|
304
302
|
}
|
|
305
303
|
|
|
306
|
-
/** Retry-After is intentionally uncapped. */
|
|
307
304
|
export function effectiveFallbackDelay(
|
|
308
305
|
baseDelayMs: number,
|
|
309
306
|
maxDelayMs: number,
|
|
310
307
|
attemptK: number,
|
|
311
|
-
retryAfterMs: number | undefined,
|
|
312
308
|
random: () => number = Math.random,
|
|
313
309
|
): number {
|
|
314
|
-
return
|
|
310
|
+
return cappedExponentialWithFullJitter(baseDelayMs, maxDelayMs, attemptK, random);
|
|
315
311
|
}
|
|
@@ -1000,31 +1000,446 @@ function assertOwnerOnlyApplied(pathname: string, kind: "directory" | "file"): v
|
|
|
1000
1000
|
}
|
|
1001
1001
|
|
|
1002
1002
|
/**
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1003
|
+
* Bound the synchronous mode-only self-heal walk so `session/new` latency stays
|
|
1004
|
+
* independent of the on-disk size of a scope's `v2-<cwd>` tree. A heavily used
|
|
1005
|
+
* scope holds every past session's transcripts plus resident-cache blobs, and an
|
|
1006
|
+
* unbounded `lstat` walk on every prepare made `session/new` take ~16s on a 12 GB
|
|
1007
|
+
* `~/.gjc` (#5565). This is a defense-in-depth mode self-heal, not the primary
|
|
1008
|
+
* owner-only-on-write guarantee, so the *synchronous* detection is budgeted:
|
|
1009
|
+
* truncating it never blocks launch. Detection walks entries in `readdir` order
|
|
1010
|
+
* (freshest writes land near the top on most filesystems), so the common drift
|
|
1011
|
+
* source is caught in-budget. Anything past the budget is NOT assumed
|
|
1012
|
+
* owner-only — a bounded, streaming background self-heal
|
|
1013
|
+
* (`runDeferredOwnerOnlySelfHeal`) finishes the tail off the critical path.
|
|
1014
|
+
*
|
|
1015
|
+
* The owner-only *guarantee* for managed content is not this walk: it is enforced
|
|
1016
|
+
* per-write by the secured storage helpers (see `openManagedStore`). This walk is
|
|
1017
|
+
* best-effort repair of drift introduced by non-managed writers. Because the tail
|
|
1018
|
+
* repair is asynchronous, a truncated scan does not itself prove the tail is
|
|
1019
|
+
* owner-only; the contract it upholds is that the tail is eventually repaired and
|
|
1020
|
+
* that a *provable* failure of that repair — an unrepairable path
|
|
1021
|
+
* (`pendingDeferredOwnerOnlyRepairFailures`) or an aborted traversal
|
|
1022
|
+
* (`deferredOwnerOnlySelfHealTraversalFailures`) — fails the next prepare closed
|
|
1023
|
+
* rather than reporting success over an uninspected or still-drifted tail.
|
|
1024
|
+
*/
|
|
1025
|
+
export const OWNER_ONLY_SELF_HEAL_MAX_ENTRIES = 8192;
|
|
1026
|
+
export const OWNER_ONLY_SELF_HEAL_MAX_DURATION_MS = 500;
|
|
1027
|
+
|
|
1028
|
+
interface OwnerOnlySelfHealBudget {
|
|
1029
|
+
deadline: number;
|
|
1030
|
+
remaining: number;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** A managed directory queued for enumeration, tagged with the identity that classified it. */
|
|
1034
|
+
interface OwnerOnlyDirRef {
|
|
1035
|
+
pathname: string;
|
|
1036
|
+
dev: bigint;
|
|
1037
|
+
ino: bigint;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
type OwnerOnlyClassification = { kind: "descend"; dev: bigint; ino: bigint } | { kind: "leaf" } | { kind: "skip" };
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Classify a managed descendant, recording group/other-readable drift. For a
|
|
1044
|
+
* directory it also returns the (dev, ino) identity from the same `lstat`, so a
|
|
1045
|
+
* later no-follow re-open can prove the directory was not replaced out from under
|
|
1046
|
+
* the walk before it is enumerated (see `opendirIfUnchanged`).
|
|
1047
|
+
*/
|
|
1048
|
+
function classifyOwnerOnlyEntry(pathname: string, drifted: string[]): OwnerOnlyClassification {
|
|
1049
|
+
let stat: fs.BigIntStats;
|
|
1050
|
+
try {
|
|
1051
|
+
stat = fs.lstatSync(pathname, { bigint: true });
|
|
1052
|
+
} catch {
|
|
1053
|
+
return { kind: "skip" };
|
|
1054
|
+
}
|
|
1055
|
+
if (stat.isSymbolicLink()) return { kind: "skip" };
|
|
1056
|
+
if ((stat.mode & 0o077n) !== 0n) drifted.push(pathname);
|
|
1057
|
+
return stat.isDirectory() ? { kind: "descend", dev: stat.dev, ino: stat.ino } : { kind: "leaf" };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Open a queued managed directory for enumeration only if it is still the exact
|
|
1062
|
+
* directory the walk classified. A managed scope is same-user writable, so a
|
|
1063
|
+
* concurrent process can replace a queued directory with a symlink (or a
|
|
1064
|
+
* different directory) between the classifying `lstat` and this open — and both
|
|
1065
|
+
* walks pop/yield across that window.
|
|
1066
|
+
*
|
|
1067
|
+
* A bare `lstat(path)` check followed by `opendirSync(path)` is a check-then-use
|
|
1068
|
+
* race: `opendirSync` *follows* a symlink final component, so a pathname swapped
|
|
1069
|
+
* to a symlink after the check enumerates outside the retained scope. Instead,
|
|
1070
|
+
* bind the identity to the descriptor we actually open: `O_NOFOLLOW` refuses a
|
|
1071
|
+
* symlinked final component outright, `O_DIRECTORY` refuses a non-directory, and
|
|
1072
|
+
* the `fstat` on that descriptor (not on a pathname that may already have been
|
|
1073
|
+
* swapped) proves it is the exact directory the walk classified before any entry
|
|
1074
|
+
* is read. Returns `null` on any mismatch or error; the caller skips that
|
|
1075
|
+
* subtree. Windows lacks `O_NOFOLLOW`/`O_DIRECTORY` and reaches this walk only
|
|
1076
|
+
* under the fail-closed verify-first policy, so it keeps the pathname check.
|
|
1077
|
+
*/
|
|
1078
|
+
function opendirIfUnchanged(ref: OwnerOnlyDirRef): fs.Dir | null {
|
|
1079
|
+
if (process.platform === "win32") return opendirIfUnchangedByPath(ref);
|
|
1080
|
+
// Prove the opened descriptor's identity no-follow, so a symlink or
|
|
1081
|
+
// different-directory swap of the pathname cannot be opened or enumerated.
|
|
1082
|
+
let fd: number;
|
|
1083
|
+
try {
|
|
1084
|
+
fd = fs.openSync(ref.pathname, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
|
|
1085
|
+
} catch {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
1090
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || stat.dev !== ref.dev || stat.ino !== ref.ino) return null;
|
|
1091
|
+
} catch {
|
|
1092
|
+
return null;
|
|
1093
|
+
} finally {
|
|
1094
|
+
fs.closeSync(fd);
|
|
1095
|
+
}
|
|
1096
|
+
// The descriptor proved identity a moment ago; open the enumeration stream and
|
|
1097
|
+
// re-verify no-follow that the pathname still resolves to that same inode, so a
|
|
1098
|
+
// swap in the narrow reopen gap is caught before any entry is enumerated. Entry
|
|
1099
|
+
// names are always rejoined to `ref.pathname` (never a stream-resolved path) by
|
|
1100
|
+
// the caller, so even a transient swap cannot escape the classified directory.
|
|
1101
|
+
let dir: fs.Dir;
|
|
1102
|
+
try {
|
|
1103
|
+
dir = fs.opendirSync(ref.pathname);
|
|
1104
|
+
} catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
const recheck = fs.lstatSync(ref.pathname, { bigint: true });
|
|
1109
|
+
if (recheck.isSymbolicLink() || !recheck.isDirectory() || recheck.dev !== ref.dev || recheck.ino !== ref.ino) {
|
|
1110
|
+
dir.closeSync();
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
dir.closeSync();
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
return dir;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Windows check-then-open fallback (no `O_NOFOLLOW`/`O_DIRECTORY`); reached only under fail-closed verify-first. */
|
|
1121
|
+
function opendirIfUnchangedByPath(ref: OwnerOnlyDirRef): fs.Dir | null {
|
|
1122
|
+
let stat: fs.BigIntStats;
|
|
1123
|
+
try {
|
|
1124
|
+
stat = fs.lstatSync(ref.pathname, { bigint: true });
|
|
1125
|
+
} catch {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || stat.dev !== ref.dev || stat.ino !== ref.ino) return null;
|
|
1129
|
+
try {
|
|
1130
|
+
return fs.opendirSync(ref.pathname);
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Bounded, streaming mode-only walk for managed-scope prepare self-heal.
|
|
1138
|
+
*
|
|
1139
|
+
* Enumerates every directory with a streaming `opendir` iterator instead of
|
|
1140
|
+
* `readdirSync`, so a single flat directory with millions of children never
|
|
1141
|
+
* materializes at once (bounded synchronous latency and memory). Collects the
|
|
1142
|
+
* group/other-readable descendants seen within the entry/time budget rather than
|
|
1143
|
+
* throwing on the first, so the caller can repair exactly those paths.
|
|
1144
|
+
*
|
|
1005
1145
|
* Avoids snapshotManagedTree("") which races concurrent session writers (#3906).
|
|
1146
|
+
*
|
|
1147
|
+
* `complete` is `false` when the budget was exhausted before the walk finished:
|
|
1148
|
+
* an empty `drifted` then means "none found within budget", not a proven
|
|
1149
|
+
* owner-only tree, and the caller must schedule the deferred tail repair.
|
|
1006
1150
|
*/
|
|
1007
|
-
function
|
|
1151
|
+
function collectOwnerOnlyModeDrift(
|
|
1152
|
+
root: string,
|
|
1153
|
+
budget: OwnerOnlySelfHealBudget,
|
|
1154
|
+
): { drifted: string[]; complete: boolean } {
|
|
1155
|
+
const drifted: string[] = [];
|
|
1156
|
+
const rootClass = classifyOwnerOnlyEntry(root, drifted);
|
|
1157
|
+
if (rootClass.kind !== "descend") return { drifted, complete: true };
|
|
1158
|
+
// Explicit DFS stack of directory refs; only one directory iterator is open at
|
|
1159
|
+
// a time and it is closed before the next is opened. Each ref carries the
|
|
1160
|
+
// identity captured when the directory was classified so `opendirIfUnchanged`
|
|
1161
|
+
// can refuse to follow a concurrent replacement.
|
|
1162
|
+
const stack: OwnerOnlyDirRef[] = [{ pathname: root, dev: rootClass.dev, ino: rootClass.ino }];
|
|
1163
|
+
while (stack.length > 0) {
|
|
1164
|
+
const ref = stack.pop() as OwnerOnlyDirRef;
|
|
1165
|
+
const dir = opendirIfUnchanged(ref);
|
|
1166
|
+
// A null open means this queued directory was replaced, became a symlink, or
|
|
1167
|
+
// grew inaccessible between classification and open: its subtree was never
|
|
1168
|
+
// enumerated, so this scan did NOT cover the tree. Report incomplete — rather
|
|
1169
|
+
// than skipping it as if clean — so the caller schedules the deferred tail
|
|
1170
|
+
// walk and the descendants under `ref` stay unverified until a walk actually
|
|
1171
|
+
// reaches them.
|
|
1172
|
+
if (dir === null) return { drifted, complete: false };
|
|
1173
|
+
try {
|
|
1174
|
+
let entry = dir.readSync();
|
|
1175
|
+
while (entry !== null) {
|
|
1176
|
+
if (budget.remaining <= 0 || Date.now() >= budget.deadline) return { drifted, complete: false };
|
|
1177
|
+
budget.remaining -= 1;
|
|
1178
|
+
const child = path.join(ref.pathname, entry.name);
|
|
1179
|
+
const childClass = classifyOwnerOnlyEntry(child, drifted);
|
|
1180
|
+
if (childClass.kind === "descend")
|
|
1181
|
+
stack.push({ pathname: child, dev: childClass.dev, ino: childClass.ino });
|
|
1182
|
+
entry = dir.readSync();
|
|
1183
|
+
}
|
|
1184
|
+
} finally {
|
|
1185
|
+
dir.closeSync();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return { drifted, complete: true };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** Re-secure owner-only mode on a single drifted managed path (throws on failure). */
|
|
1192
|
+
function resecureOwnerOnlyPath(pathname: string): void {
|
|
1008
1193
|
let stat: fs.Stats;
|
|
1009
1194
|
try {
|
|
1010
|
-
stat = fs.lstatSync(
|
|
1195
|
+
stat = fs.lstatSync(pathname);
|
|
1011
1196
|
} catch {
|
|
1012
1197
|
return;
|
|
1013
1198
|
}
|
|
1014
1199
|
if (stat.isSymbolicLink()) return;
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1200
|
+
assertOwnerOnlyApplied(pathname, stat.isDirectory() ? "directory" : "file");
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Best-effort variant for the background walk: a concurrent writer must not crash
|
|
1205
|
+
* it, but a genuine repair failure must not be silently dropped either. For a
|
|
1206
|
+
* descendant past the synchronous budget, this deferred tail is the ONLY
|
|
1207
|
+
* enforcement step, so a failed repair is recorded in `failures` — the next
|
|
1208
|
+
* prepare observes it, retries, and fails closed rather than reporting success
|
|
1209
|
+
* over a descendant that is still group/other-readable.
|
|
1210
|
+
*/
|
|
1211
|
+
function resecureOwnerOnlyPathQuietly(pathname: string, failures: string[]): void {
|
|
1018
1212
|
try {
|
|
1019
|
-
|
|
1020
|
-
} catch {
|
|
1021
|
-
|
|
1213
|
+
resecureOwnerOnlyPath(pathname);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
failures.push(pathname);
|
|
1216
|
+
logger.debug("Deferred managed-scope owner-only repair failed; will retry on next prepare", {
|
|
1217
|
+
pathname,
|
|
1218
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1219
|
+
});
|
|
1022
1220
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** Entries inspected between event-loop yields in the deferred background walk. */
|
|
1224
|
+
const OWNER_ONLY_DEFERRED_SELF_HEAL_SLICE = OWNER_ONLY_SELF_HEAL_MAX_ENTRIES;
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Finish the owner-only self-heal that the budgeted synchronous walk truncated.
|
|
1228
|
+
*
|
|
1229
|
+
* Walks the *entire* scope tree — again with streaming `opendir` enumeration so a
|
|
1230
|
+
* flat high-fan-out directory stays bounded — repairing each drifted descendant
|
|
1231
|
+
* as it is found, and yields to the event loop every slice so it never blocks
|
|
1232
|
+
* `session/new` or the ACP server the way the original unbounded walk did.
|
|
1233
|
+
*/
|
|
1234
|
+
async function runDeferredOwnerOnlySelfHeal(root: string, failures: string[]): Promise<void> {
|
|
1235
|
+
// Yield before touching the tree so scheduling never runs a walk slice on the
|
|
1236
|
+
// `session/new` tick — the whole tail repair stays off the critical path.
|
|
1237
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1238
|
+
const rootDrift: string[] = [];
|
|
1239
|
+
const rootClass = classifyOwnerOnlyEntry(root, rootDrift);
|
|
1240
|
+
for (const pathname of rootDrift) resecureOwnerOnlyPathQuietly(pathname, failures);
|
|
1241
|
+
if (rootClass.kind !== "descend") return;
|
|
1242
|
+
const stack: OwnerOnlyDirRef[] = [{ pathname: root, dev: rootClass.dev, ino: rootClass.ino }];
|
|
1243
|
+
let sliceRemaining = OWNER_ONLY_DEFERRED_SELF_HEAL_SLICE;
|
|
1244
|
+
while (stack.length > 0) {
|
|
1245
|
+
const ref = stack.pop() as OwnerOnlyDirRef;
|
|
1246
|
+
const dir = opendirIfUnchanged(ref);
|
|
1247
|
+
// A null open leaves this directory's descendants unvisited, so the tail this
|
|
1248
|
+
// walk exists to enforce was not fully traversed. Throw — like a `Dir.readSync`
|
|
1249
|
+
// failure below — so the scheduler's `.catch` records a traversal failure
|
|
1250
|
+
// (`traversed = false`) instead of the walk finishing as if the tree were
|
|
1251
|
+
// fully covered.
|
|
1252
|
+
if (dir === null)
|
|
1253
|
+
throw Object.assign(
|
|
1254
|
+
new Error("opendir_null: directory identity changed or became inaccessible during traversal"),
|
|
1255
|
+
{ code: "ENOENT" },
|
|
1256
|
+
);
|
|
1257
|
+
try {
|
|
1258
|
+
let entry = dir.readSync();
|
|
1259
|
+
while (entry !== null) {
|
|
1260
|
+
const child = path.join(ref.pathname, entry.name);
|
|
1261
|
+
const childDrift: string[] = [];
|
|
1262
|
+
const childClass = classifyOwnerOnlyEntry(child, childDrift);
|
|
1263
|
+
for (const pathname of childDrift) resecureOwnerOnlyPathQuietly(pathname, failures);
|
|
1264
|
+
if (childClass.kind === "descend")
|
|
1265
|
+
stack.push({ pathname: child, dev: childClass.dev, ino: childClass.ino });
|
|
1266
|
+
if (--sliceRemaining <= 0) {
|
|
1267
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1268
|
+
sliceRemaining = OWNER_ONLY_DEFERRED_SELF_HEAL_SLICE;
|
|
1269
|
+
}
|
|
1270
|
+
entry = dir.readSync();
|
|
1271
|
+
}
|
|
1272
|
+
} finally {
|
|
1273
|
+
dir.closeSync();
|
|
1274
|
+
}
|
|
1025
1275
|
}
|
|
1026
1276
|
}
|
|
1027
1277
|
|
|
1278
|
+
/** In-flight deferred self-heals, keyed by resolved scope directory to dedupe. */
|
|
1279
|
+
const pendingDeferredOwnerOnlySelfHeals = new Map<string, Promise<void>>();
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Scopes whose most recent deferred self-heal left at least one descendant it
|
|
1283
|
+
* could not re-secure (resolved scope directory → the still-drifted paths). This
|
|
1284
|
+
* deferred tail is the sole enforcement for descendants past the synchronous
|
|
1285
|
+
* budget, so a failure here must not be discarded: the next prepare for the scope
|
|
1286
|
+
* retries these paths and fails closed if they still cannot be made owner-only.
|
|
1287
|
+
*/
|
|
1288
|
+
const pendingDeferredOwnerOnlyRepairFailures = new Map<string, string[]>();
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Scopes whose most recent deferred self-heal aborted on a *traversal*-level
|
|
1292
|
+
* filesystem failure — a `Dir.readSync`/iterator or close error, as opposed to
|
|
1293
|
+
* one path's repair, which `resecureOwnerOnlyPathQuietly` records separately. The
|
|
1294
|
+
* walk then stopped mid-tree, so the tail past the synchronous budget was never
|
|
1295
|
+
* inspected and the traversal must not be recorded as having completed. The scope
|
|
1296
|
+
* stays marked here until a later walk reaches the end of the tree (or a
|
|
1297
|
+
* synchronous scan covers it in full), which also keeps a partial walk from
|
|
1298
|
+
* clearing repair failures it never got far enough to re-examine.
|
|
1299
|
+
*/
|
|
1300
|
+
const deferredOwnerOnlySelfHealTraversalFailures = new Set<string>();
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Schedule (at most one per scope) a background self-heal of the tail the budget
|
|
1304
|
+
* skipped.
|
|
1305
|
+
*
|
|
1306
|
+
* Scheduling is deliberately NOT suppressed by a time-based cooldown on the last
|
|
1307
|
+
* completed walk. This is only reached when the synchronous scan truncated, and a
|
|
1308
|
+
* truncated scan is fresh evidence that the tail is unverified *now*: a managed
|
|
1309
|
+
* scope is same-user writable, so a new group/other-readable descendant can land
|
|
1310
|
+
* past the scan prefix moments after a walk completes. Suppressing on elapsed
|
|
1311
|
+
* time alone would let that prepare return success over a tree nobody inspected,
|
|
1312
|
+
* and there is no cheap unchanged-tree signal at the scan boundary that could
|
|
1313
|
+
* prove otherwise — verifying the tail *is* the walk. The in-flight dedupe above
|
|
1314
|
+
* still bounds this to one concurrent traversal per scope.
|
|
1315
|
+
*/
|
|
1316
|
+
function scheduleDeferredOwnerOnlySelfHeal(directory: string): void {
|
|
1317
|
+
const key = path.resolve(directory);
|
|
1318
|
+
if (pendingDeferredOwnerOnlySelfHeals.has(key)) return;
|
|
1319
|
+
const failures: string[] = [];
|
|
1320
|
+
let traversed = true;
|
|
1321
|
+
const task = runDeferredOwnerOnlySelfHeal(directory, failures).catch(error => {
|
|
1322
|
+
// The walk stopped mid-tree, so the tail it exists to enforce was never
|
|
1323
|
+
// inspected. Recording this as a completed traversal would both discard that
|
|
1324
|
+
// fact and let the empty `failures` list clear repair failures the aborted
|
|
1325
|
+
// walk never reached.
|
|
1326
|
+
traversed = false;
|
|
1327
|
+
logger.debug("Deferred managed-scope owner-only self-heal failed", {
|
|
1328
|
+
directory,
|
|
1329
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
pendingDeferredOwnerOnlySelfHeals.set(key, task);
|
|
1333
|
+
void task.finally(() => {
|
|
1334
|
+
// Preserve an unrepairable tail so the next prepare fails closed; clear any
|
|
1335
|
+
// prior failure only once a walk has actually traversed the whole scope.
|
|
1336
|
+
if (failures.length > 0) pendingDeferredOwnerOnlyRepairFailures.set(key, failures);
|
|
1337
|
+
else if (traversed) pendingDeferredOwnerOnlyRepairFailures.delete(key);
|
|
1338
|
+
if (traversed) deferredOwnerOnlySelfHealTraversalFailures.delete(key);
|
|
1339
|
+
else deferredOwnerOnlySelfHealTraversalFailures.add(key);
|
|
1340
|
+
if (pendingDeferredOwnerOnlySelfHeals.get(key) === task) pendingDeferredOwnerOnlySelfHeals.delete(key);
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** Number of scopes with an in-flight deferred owner-only self-heal (test/shutdown observability). */
|
|
1345
|
+
export function pendingDeferredOwnerOnlySelfHealCount(): number {
|
|
1346
|
+
return pendingDeferredOwnerOnlySelfHeals.size;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** Number of scopes with a recorded deferred owner-only repair failure (test/shutdown observability). */
|
|
1350
|
+
export function pendingDeferredOwnerOnlyRepairFailureCount(): number {
|
|
1351
|
+
return pendingDeferredOwnerOnlyRepairFailures.size;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/** Number of scopes whose deferred walk aborted before traversing the tree (test/shutdown observability). */
|
|
1355
|
+
export function pendingDeferredOwnerOnlyTraversalFailureCount(): number {
|
|
1356
|
+
return deferredOwnerOnlySelfHealTraversalFailures.size;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/** Await every in-flight deferred owner-only self-heal (deterministic teardown/shutdown). */
|
|
1360
|
+
export async function drainDeferredOwnerOnlySelfHeals(): Promise<void> {
|
|
1361
|
+
while (pendingDeferredOwnerOnlySelfHeals.size > 0) {
|
|
1362
|
+
await Promise.all([...pendingDeferredOwnerOnlySelfHeals.values()]);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Detect and repair owner-only mode drift under a prepared managed scope.
|
|
1368
|
+
*
|
|
1369
|
+
* Repair is targeted: only the specific descendants found group/other-readable
|
|
1370
|
+
* within the budget are re-secured — never the whole scope — so one early
|
|
1371
|
+
* readable blob cannot trigger an unbounded re-secure plus another full walk.
|
|
1372
|
+
* When the budget truncates the scan, a bounded background self-heal finishes the
|
|
1373
|
+
* remaining tail so a drifted file beyond the scan prefix is still repaired
|
|
1374
|
+
* rather than silently accepted as owner-only.
|
|
1375
|
+
*/
|
|
1376
|
+
export function selfHealOwnerOnlyModeDrift(directory: string, policy: ManagedSessionSecurityPolicy): void {
|
|
1377
|
+
const key = path.resolve(directory);
|
|
1378
|
+
// A prior deferred repair that could not restore owner-only mode is the only
|
|
1379
|
+
// enforcement for its descendants. Retry it before this writer proceeds and
|
|
1380
|
+
// fail closed if it still cannot be secured, so a discarded background repair
|
|
1381
|
+
// never surfaces as an apparently successful prepare (the base implementation
|
|
1382
|
+
// likewise failed closed rather than accepting a group/other-readable tree).
|
|
1383
|
+
const priorFailures = pendingDeferredOwnerOnlyRepairFailures.get(key);
|
|
1384
|
+
if (priorFailures !== undefined) {
|
|
1385
|
+
const stillDrifted: string[] = [];
|
|
1386
|
+
let firstError: unknown;
|
|
1387
|
+
for (const pathname of priorFailures) {
|
|
1388
|
+
try {
|
|
1389
|
+
resecureOwnerOnlyPath(pathname);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
stillDrifted.push(pathname);
|
|
1392
|
+
firstError ??= error;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (stillDrifted.length > 0) {
|
|
1396
|
+
pendingDeferredOwnerOnlyRepairFailures.set(key, stillDrifted);
|
|
1397
|
+
throw firstError instanceof Error ? firstError : new Error("mode_mismatch");
|
|
1398
|
+
}
|
|
1399
|
+
pendingDeferredOwnerOnlyRepairFailures.delete(key);
|
|
1400
|
+
}
|
|
1401
|
+
// A prior deferred walk that aborted mid-traversal left the tail past the
|
|
1402
|
+
// synchronous budget uninspected — unverified state, not a proven owner-only
|
|
1403
|
+
// tree. Consult it alongside the repair-failure record above: if this prepare's
|
|
1404
|
+
// own synchronous scan also cannot reach the end of the tree (below), reporting
|
|
1405
|
+
// success would accept that uninspected tail. The marker is cleared only by a
|
|
1406
|
+
// walk — synchronous or deferred — that actually traverses to the end.
|
|
1407
|
+
const priorTraversalFailure = deferredOwnerOnlySelfHealTraversalFailures.has(key);
|
|
1408
|
+
const budget: OwnerOnlySelfHealBudget = {
|
|
1409
|
+
deadline: Date.now() + OWNER_ONLY_SELF_HEAL_MAX_DURATION_MS,
|
|
1410
|
+
remaining: OWNER_ONLY_SELF_HEAL_MAX_ENTRIES,
|
|
1411
|
+
};
|
|
1412
|
+
const startedAt = Date.now();
|
|
1413
|
+
const { drifted, complete } = collectOwnerOnlyModeDrift(directory, budget);
|
|
1414
|
+
// A verify-first policy demands proof before trust: it must not self-heal, and
|
|
1415
|
+
// an incomplete scan cannot furnish that proof — fail closed on either.
|
|
1416
|
+
const failClosed = process.platform === "win32" && policy === "windows-existing-verify-first";
|
|
1417
|
+
if (drifted.length > 0) {
|
|
1418
|
+
if (failClosed) throw new Error("mode_mismatch");
|
|
1419
|
+
for (const pathname of drifted) resecureOwnerOnlyPath(pathname);
|
|
1420
|
+
}
|
|
1421
|
+
if (!complete) {
|
|
1422
|
+
if (failClosed) throw new Error("mode_mismatch");
|
|
1423
|
+
scheduleDeferredOwnerOnlySelfHeal(directory);
|
|
1424
|
+
logger.debug("Managed scope owner-only self-heal walk truncated; deferred repair scheduled", {
|
|
1425
|
+
directory,
|
|
1426
|
+
elapsedMs: Date.now() - startedAt,
|
|
1427
|
+
maxDurationMs: OWNER_ONLY_SELF_HEAL_MAX_DURATION_MS,
|
|
1428
|
+
maxEntries: OWNER_ONLY_SELF_HEAL_MAX_ENTRIES,
|
|
1429
|
+
});
|
|
1430
|
+
// The tail is still unverified: a prior deferred walk aborted before reaching
|
|
1431
|
+
// it and this synchronous scan truncated short of it too. Fail closed rather
|
|
1432
|
+
// than report success over an uninspected tail; the deferred walk scheduled
|
|
1433
|
+
// just above will clear the traversal-failure marker once it completes, so a
|
|
1434
|
+
// later prepare recovers on its own.
|
|
1435
|
+
if (priorTraversalFailure) throw new Error("mode_mismatch");
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
// This scan reached the end of the tree, so the tail an earlier aborted deferred
|
|
1439
|
+
// walk left uninspected has now been inspected and repaired synchronously.
|
|
1440
|
+
deferredOwnerOnlySelfHealTraversalFailures.delete(key);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1028
1443
|
/**
|
|
1029
1444
|
* True when a managed setup error reflects a fixable owner-only *mode* drift
|
|
1030
1445
|
* (group/other permission bits) rather than an ownership or identity change.
|
|
@@ -1099,25 +1514,26 @@ export function prepareManagedSessionScopeForWriteSync(
|
|
|
1099
1514
|
const next = buildStore();
|
|
1100
1515
|
// #3951 root-store identity binding no longer walks the managed tree on
|
|
1101
1516
|
// construct, so mode drift no longer surfaces as mode_mismatch there.
|
|
1102
|
-
// Detect group/other-readable descendants with a
|
|
1103
|
-
// call snapshotManagedTree("") here:
|
|
1104
|
-
// identity_mismatch races (#3906)
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1517
|
+
// Detect and repair group/other-readable descendants with a bounded,
|
|
1518
|
+
// streaming mode-only walk — do NOT call snapshotManagedTree("") here:
|
|
1519
|
+
// that reintroduces concurrent-writer identity_mismatch races (#3906)
|
|
1520
|
+
// that break SessionManager.moveTo / move.
|
|
1521
|
+
//
|
|
1522
|
+
// This walk is bounded best-effort defense-in-depth, NOT the owner-only
|
|
1523
|
+
// guarantee itself: every managed write below goes through the secured
|
|
1524
|
+
// storage helpers (`ensureManagedDirectory`, `store.publish*`), which
|
|
1525
|
+
// apply and verify owner-only mode on each object they create, so managed
|
|
1526
|
+
// writes are owner-only at the write boundary regardless of the walk. The
|
|
1527
|
+
// walk only heals drift left by non-managed writers (e.g. the resident
|
|
1528
|
+
// cache) into the scope. A completed synchronous scan holds this prepare
|
|
1529
|
+
// until the whole tree is inspected; a truncated scan hands the tail to a
|
|
1530
|
+
// bounded background self-heal, and a *provable* failure of that deferred
|
|
1531
|
+
// enforcement (an unrepairable path or an aborted traversal) fails the
|
|
1532
|
+
// next prepare closed rather than silently accepting a drifted tail.
|
|
1533
|
+
if (retainedAuthority) selfHealOwnerOnlyModeDrift(scope.directoryPath, policy);
|
|
1108
1534
|
return next;
|
|
1109
1535
|
};
|
|
1110
|
-
|
|
1111
|
-
store = openManagedStore();
|
|
1112
|
-
} catch (error) {
|
|
1113
|
-
if (process.platform === "win32" && policy === "windows-existing-verify-first") throw error;
|
|
1114
|
-
if (!isRecoverableOwnerOnlyModeDrift(error)) throw error;
|
|
1115
|
-
// A prior writer left group/other-readable descendants under the scope
|
|
1116
|
-
// (e.g. resident-cache blobs written on the explicit session path).
|
|
1117
|
-
// Re-secure the tree in place and retry once before failing closed.
|
|
1118
|
-
reapplyOwnerOnlyManagedTree(scope.directoryPath);
|
|
1119
|
-
store = openManagedStore();
|
|
1120
|
-
}
|
|
1536
|
+
store = openManagedStore();
|
|
1121
1537
|
const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
|
|
1122
1538
|
stage = "binding_publish";
|
|
1123
1539
|
try {
|