@gajae-code/coding-agent 0.18.11 → 0.18.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +293 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -3634,7 +3634,15 @@ function isLinkedWorktreePeer(a: string, b: string): boolean {
|
|
|
3634
3634
|
* Read the terminal breadcrumb for the current terminal, scoped to a cwd.
|
|
3635
3635
|
* Returns the session file path if it exists and matches the cwd, null otherwise.
|
|
3636
3636
|
*/
|
|
3637
|
-
async function readTerminalBreadcrumb(
|
|
3637
|
+
async function readTerminalBreadcrumb(
|
|
3638
|
+
cwd: string,
|
|
3639
|
+
options: {
|
|
3640
|
+
sessionsRoot?: string;
|
|
3641
|
+
allowExplicitOutsideRoot?: boolean;
|
|
3642
|
+
/** Managed continuation inventory, including project-local transcripts. */
|
|
3643
|
+
continuationInventory?: readonly SessionInfo[];
|
|
3644
|
+
} = {},
|
|
3645
|
+
): Promise<string | null> {
|
|
3638
3646
|
const terminalId = getTerminalId();
|
|
3639
3647
|
if (!terminalId) return null;
|
|
3640
3648
|
|
|
@@ -3656,9 +3664,9 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
3656
3664
|
|
|
3657
3665
|
if (isStagedSessionPath(sessionFile)) return null;
|
|
3658
3666
|
const inspected = inspectResumeSessionFile(sessionFile, new FileSessionStorage());
|
|
3667
|
+
const sessionsRoot = options.sessionsRoot ?? getSessionsDir();
|
|
3659
3668
|
if ("kind" in inspected) {
|
|
3660
3669
|
if (inspected.reason !== "missing") return null;
|
|
3661
|
-
const sessionsRoot = getSessionsDir();
|
|
3662
3670
|
const resolved = resolveManagedScope({
|
|
3663
3671
|
cwd: breadcrumbCwd,
|
|
3664
3672
|
agentDir: path.resolve(sessionsRoot, ".."),
|
|
@@ -3675,7 +3683,6 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
3675
3683
|
}
|
|
3676
3684
|
const header = inspected.entries[0] as SessionHeader;
|
|
3677
3685
|
if (resolveEquivalentPath(header.cwd) !== resolveEquivalentPath(breadcrumbCwd)) return null;
|
|
3678
|
-
const sessionsRoot = getSessionsDir();
|
|
3679
3686
|
const resolved = resolveManagedScope({
|
|
3680
3687
|
cwd: header.cwd,
|
|
3681
3688
|
agentDir: path.resolve(sessionsRoot, ".."),
|
|
@@ -3693,7 +3700,22 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
3693
3700
|
candidate => !isStagedSessionPath(candidate.path) && candidate.sessionId === header.id,
|
|
3694
3701
|
);
|
|
3695
3702
|
if (byIdentity) return byIdentity.path;
|
|
3696
|
-
|
|
3703
|
+
// Not an owned managed candidate. A file inside the sessions root stays
|
|
3704
|
+
// rejected. A file outside that root is resumed only when this continuation
|
|
3705
|
+
// inventory already contains it. An explicit destination still receives an
|
|
3706
|
+
// unmatched outside path; the caller applies containment.
|
|
3707
|
+
const resolvedFile = path.resolve(sessionFile);
|
|
3708
|
+
const insideSessionsRoot = pathIsWithin(sessionsRoot, resolvedFile);
|
|
3709
|
+
if (
|
|
3710
|
+
!insideSessionsRoot &&
|
|
3711
|
+
options.continuationInventory?.some(
|
|
3712
|
+
session => session.id === header.id && path.resolve(session.path) === resolvedFile,
|
|
3713
|
+
)
|
|
3714
|
+
) {
|
|
3715
|
+
return resolvedFile;
|
|
3716
|
+
}
|
|
3717
|
+
if (!options.allowExplicitOutsideRoot || insideSessionsRoot) return null;
|
|
3718
|
+
return resolvedFile;
|
|
3697
3719
|
} catch (err) {
|
|
3698
3720
|
if (!isEnoent(err)) logger.debug("Terminal breadcrumb read failed", { err });
|
|
3699
3721
|
// Breadcrumb doesn't exist or is corrupt — fall through
|
|
@@ -7576,14 +7598,14 @@ type ManagedDestinationTransition = {
|
|
|
7576
7598
|
/**
|
|
7577
7599
|
* Freshness snapshot captured with every async whole-session persistence
|
|
7578
7600
|
* preparation. Immediately before the synchronous persistence transaction, the
|
|
7579
|
-
* live values are compared: a
|
|
7580
|
-
*
|
|
7601
|
+
* live values are compared: a logical lifecycle change aborts; a verified storage
|
|
7602
|
+
* relocation or revision change discards prepared bytes and re-prepares (bounded),
|
|
7581
7603
|
* so a stale snapshot is never published.
|
|
7582
7604
|
*/
|
|
7583
7605
|
export interface PersistenceInputToken {
|
|
7584
7606
|
/** Canonical session file path the prepared bytes target. */
|
|
7585
7607
|
sessionFile: string;
|
|
7586
|
-
/**
|
|
7608
|
+
/** Logical lifecycle identity; storage-only relocation preserves its verified origin. */
|
|
7587
7609
|
lifecycleId: string;
|
|
7588
7610
|
/** Revision of #fileEntries affecting persisted bytes. */
|
|
7589
7611
|
entryRevision: number;
|
|
@@ -7800,6 +7822,10 @@ export class SessionManager {
|
|
|
7800
7822
|
#artifactManager: ArtifactManager | null = null;
|
|
7801
7823
|
#artifactManagerSessionFile: string | null = null;
|
|
7802
7824
|
#managedTranscriptStoreCache: { directory: string; store: ManagedSessionDescendantStore } | null = null;
|
|
7825
|
+
#managedTranscriptRelocationResolver: (() => ArtifactManager) | undefined;
|
|
7826
|
+
#managedTranscriptRelocationResolverToken: symbol | undefined;
|
|
7827
|
+
#managedTranscriptRelocationFileOrigin: string | undefined;
|
|
7828
|
+
#managedTranscriptRelocationParentArtifacts: ArtifactManager | undefined;
|
|
7803
7829
|
#ownedManagedAuthority: ManagedSessionDescendantStore | undefined;
|
|
7804
7830
|
#managedSidecarCacheStore: EphemeralBlobStore | undefined;
|
|
7805
7831
|
readonly #managedSidecarCleanupStores = new Set<EphemeralBlobStore>();
|
|
@@ -10289,6 +10315,7 @@ export class SessionManager {
|
|
|
10289
10315
|
}
|
|
10290
10316
|
this.#artifactManager = null;
|
|
10291
10317
|
this.#artifactManagerSessionFile = null;
|
|
10318
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
10292
10319
|
this.#artifactLifecycle = Symbol("session-artifact-adoption");
|
|
10293
10320
|
installedGeneration = this.#artifactLifecycle;
|
|
10294
10321
|
installedSessionId = this.#sessionId;
|
|
@@ -10402,6 +10429,7 @@ export class SessionManager {
|
|
|
10402
10429
|
throw error;
|
|
10403
10430
|
}
|
|
10404
10431
|
managedTransition?.adopt();
|
|
10432
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
10405
10433
|
this.#sessionFile = resolvedSessionFile;
|
|
10406
10434
|
this.#sessionId = sessionId;
|
|
10407
10435
|
this.#sessionName = header?.title;
|
|
@@ -10489,6 +10517,7 @@ export class SessionManager {
|
|
|
10489
10517
|
prepared.dispose();
|
|
10490
10518
|
throw error;
|
|
10491
10519
|
}
|
|
10520
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
10492
10521
|
managedTransition?.settle();
|
|
10493
10522
|
this.#pendingStrictAdoption = undefined;
|
|
10494
10523
|
await this.#rewriteFile();
|
|
@@ -10958,6 +10987,7 @@ export class SessionManager {
|
|
|
10958
10987
|
this.#adoptedArtifactManager = null;
|
|
10959
10988
|
this.#managedPersistExpectedIdentity = expectedIdentity;
|
|
10960
10989
|
this.#commitResidentTextStoreTransition(transition);
|
|
10990
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
10961
10991
|
this.#retireEphemeralArtifacts();
|
|
10962
10992
|
stage.committed = true;
|
|
10963
10993
|
this.#preparedNewSessions.delete(stage);
|
|
@@ -12196,6 +12226,9 @@ export class SessionManager {
|
|
|
12196
12226
|
path.basename(oldArtifactDir),
|
|
12197
12227
|
managedPublishedArtifacts,
|
|
12198
12228
|
);
|
|
12229
|
+
// The restored namespace has a new directory identity, even at the same path.
|
|
12230
|
+
this.#artifactManager = null;
|
|
12231
|
+
this.#artifactManagerSessionFile = null;
|
|
12199
12232
|
}
|
|
12200
12233
|
if (managedTranscript && !managedSourceStore.readExpected(path.basename(oldSessionFile)))
|
|
12201
12234
|
await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
|
|
@@ -12303,6 +12336,8 @@ export class SessionManager {
|
|
|
12303
12336
|
path.basename(oldArtifactDir),
|
|
12304
12337
|
managedPublishedArtifacts,
|
|
12305
12338
|
);
|
|
12339
|
+
this.#artifactManager = null;
|
|
12340
|
+
this.#artifactManagerSessionFile = null;
|
|
12306
12341
|
const restored = managedSourceStore.captureTree(path.basename(oldArtifactDir));
|
|
12307
12342
|
if (!retainedTreeSnapshotEqualsAfterRename(restored, managedArtifacts))
|
|
12308
12343
|
throw new Error("managed_move_source_restore_mismatch");
|
|
@@ -12653,7 +12688,7 @@ export class SessionManager {
|
|
|
12653
12688
|
#capturePersistenceInputToken(): PersistenceInputToken {
|
|
12654
12689
|
return {
|
|
12655
12690
|
sessionFile: this.#sessionFile ?? "",
|
|
12656
|
-
lifecycleId: `${this.#sessionId}@${this.#sessionFile ?? ""}`,
|
|
12691
|
+
lifecycleId: `${this.#sessionId}@${this.#managedTranscriptRelocationFileOrigin ?? this.#sessionFile ?? ""}`,
|
|
12657
12692
|
entryRevision: this.#entryRevision,
|
|
12658
12693
|
headerRevision: this.#headerExportRevision,
|
|
12659
12694
|
residentBlobRevision: this.#residentBlobRevision,
|
|
@@ -12667,9 +12702,10 @@ export class SessionManager {
|
|
|
12667
12702
|
*/
|
|
12668
12703
|
#persistenceInputTokenMatches(token: PersistenceInputToken): boolean {
|
|
12669
12704
|
const live = this.#capturePersistenceInputToken();
|
|
12670
|
-
if (live.
|
|
12705
|
+
if (live.lifecycleId !== token.lifecycleId) {
|
|
12671
12706
|
throw new Error("session_persistence_lifecycle_changed");
|
|
12672
12707
|
}
|
|
12708
|
+
if (live.sessionFile !== token.sessionFile) return false;
|
|
12673
12709
|
return (
|
|
12674
12710
|
live.entryRevision === token.entryRevision &&
|
|
12675
12711
|
live.headerRevision === token.headerRevision &&
|
|
@@ -12935,10 +12971,162 @@ export class SessionManager {
|
|
|
12935
12971
|
this.#ownedManagedAuthority = undefined;
|
|
12936
12972
|
}
|
|
12937
12973
|
|
|
12974
|
+
/**
|
|
12975
|
+
* Register the same-owner task capability used to follow a parent's managed
|
|
12976
|
+
* artifact root after its session move. The synchronous resolver must return
|
|
12977
|
+
* only the current ArtifactManager for the same parent ID and parent transcript
|
|
12978
|
+
* basename; it retains the last verified manager on a logical successor.
|
|
12979
|
+
* Registration captures the exact original manager identity. Unchanged identity
|
|
12980
|
+
* keeps using the child's retained store; a changed identity triggers strict
|
|
12981
|
+
* destination transcript verification before re-rooting.
|
|
12982
|
+
*/
|
|
12983
|
+
registerManagedTranscriptRelocationResolver(resolver: () => ArtifactManager): () => void {
|
|
12984
|
+
if (this.destination.kind !== "managed")
|
|
12985
|
+
throw new Error("Managed transcript relocation requires a managed session");
|
|
12986
|
+
if (this.#managedTranscriptRelocationResolver)
|
|
12987
|
+
throw new Error("Managed transcript relocation resolver is already registered");
|
|
12988
|
+
const originalParentArtifacts = resolver();
|
|
12989
|
+
const registrationToken = Symbol("managed-transcript-relocation");
|
|
12990
|
+
this.#managedTranscriptRelocationResolver = resolver;
|
|
12991
|
+
this.#managedTranscriptRelocationResolverToken = registrationToken;
|
|
12992
|
+
this.#managedTranscriptRelocationParentArtifacts = originalParentArtifacts;
|
|
12993
|
+
return () => {
|
|
12994
|
+
if (this.#managedTranscriptRelocationResolverToken === registrationToken)
|
|
12995
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
12996
|
+
};
|
|
12997
|
+
}
|
|
12998
|
+
|
|
12999
|
+
#clearManagedTranscriptRelocationResolver(): void {
|
|
13000
|
+
this.#managedTranscriptRelocationResolver = undefined;
|
|
13001
|
+
this.#managedTranscriptRelocationResolverToken = undefined;
|
|
13002
|
+
this.#managedTranscriptRelocationFileOrigin = undefined;
|
|
13003
|
+
this.#managedTranscriptRelocationParentArtifacts = undefined;
|
|
13004
|
+
}
|
|
13005
|
+
|
|
13006
|
+
#refreshManagedTranscriptRelocation(sessionFile = this.#sessionFile): void {
|
|
13007
|
+
const currentSessionFile = this.#sessionFile;
|
|
13008
|
+
const resolver = this.#managedTranscriptRelocationResolver;
|
|
13009
|
+
if (this.destination.kind !== "managed" || !currentSessionFile || sessionFile !== currentSessionFile || !resolver)
|
|
13010
|
+
return;
|
|
13011
|
+
|
|
13012
|
+
const parentArtifacts = resolver();
|
|
13013
|
+
const nextDirectory = path.resolve(parentArtifacts.dir);
|
|
13014
|
+
const parentArtifactsChanged = this.#managedTranscriptRelocationParentArtifacts !== parentArtifacts;
|
|
13015
|
+
if (nextDirectory === path.resolve(this.destination.directory) && !parentArtifactsChanged) return;
|
|
13016
|
+
parentArtifacts.assertManagedBinding();
|
|
13017
|
+
const parentStore = parentArtifacts.getManagedStore();
|
|
13018
|
+
if (!parentStore || path.resolve(parentStore.dir) !== nextDirectory)
|
|
13019
|
+
throw new Error("managed_transcript_relocation_parent_authority_unavailable");
|
|
13020
|
+
parentStore.assertBound();
|
|
13021
|
+
if (path.resolve(path.dirname(currentSessionFile)) !== path.resolve(this.destination.directory))
|
|
13022
|
+
throw new Error("managed_transcript_relocation_source_path_unverified");
|
|
13023
|
+
const transcriptName = path.basename(currentSessionFile);
|
|
13024
|
+
const nextSessionFile = path.join(nextDirectory, transcriptName);
|
|
13025
|
+
if (path.basename(nextSessionFile) !== transcriptName)
|
|
13026
|
+
throw new Error("managed_transcript_relocation_transcript_name_mismatch");
|
|
13027
|
+
|
|
13028
|
+
if (this.#stagedPublication) throw new Error("managed_transcript_relocation_staged_publication_unavailable");
|
|
13029
|
+
if (this.#persistWriter || this.#persistWriterPath)
|
|
13030
|
+
throw new Error("managed_transcript_relocation_writer_unavailable");
|
|
13031
|
+
if (this.#boundedManagedSource) throw new Error("managed_transcript_relocation_bounded_source_unavailable");
|
|
13032
|
+
if (this.#coldSidecarActive() || (this.#sidecarRuntime?.coldEntries.size ?? 0) > 0)
|
|
13033
|
+
throw new Error("managed_transcript_relocation_cold_sidecar_unavailable");
|
|
13034
|
+
if (this.#recoveryHydrationContext || this.#recoveryPromotionTranscriptPath || this.#pendingStrictAdoption)
|
|
13035
|
+
throw new Error("managed_transcript_relocation_recovery_state_unavailable");
|
|
13036
|
+
|
|
13037
|
+
const expectedIdentity = this.#managedPersistExpectedIdentity;
|
|
13038
|
+
if (
|
|
13039
|
+
!expectedIdentity ||
|
|
13040
|
+
!Number.isSafeInteger(expectedIdentity.size) ||
|
|
13041
|
+
expectedIdentity.size < 0 ||
|
|
13042
|
+
typeof expectedIdentity.sha256 !== "string" ||
|
|
13043
|
+
!/^[0-9a-f]{64}$/.test(expectedIdentity.sha256)
|
|
13044
|
+
)
|
|
13045
|
+
throw new Error("managed_transcript_relocation_original_identity_unavailable");
|
|
13046
|
+
|
|
13047
|
+
let nextDestination: SessionDestination | undefined;
|
|
13048
|
+
let nextStore: ManagedSessionDescendantStore | undefined;
|
|
13049
|
+
let adopted = false;
|
|
13050
|
+
try {
|
|
13051
|
+
nextDestination = SessionManager.nestedManagedDestination(parentStore, nextDirectory);
|
|
13052
|
+
if (nextDestination.kind !== "managed")
|
|
13053
|
+
throw new Error("managed_transcript_relocation_destination_unavailable");
|
|
13054
|
+
nextStore = managedStoreFromContext(nextDestination.securityContext, nextDirectory);
|
|
13055
|
+
nextStore.verifyRootSecurity();
|
|
13056
|
+
nextStore.assertBound();
|
|
13057
|
+
const destinationTranscript = nextStore.readExpected(transcriptName);
|
|
13058
|
+
if (!destinationTranscript) throw new Error("managed_transcript_relocation_destination_missing");
|
|
13059
|
+
const destinationSha256 = crypto.createHash("sha256").update(destinationTranscript.bytes).digest("hex");
|
|
13060
|
+
if (
|
|
13061
|
+
destinationTranscript.identity.size !== expectedIdentity.size ||
|
|
13062
|
+
destinationTranscript.bytes.byteLength !== expectedIdentity.size ||
|
|
13063
|
+
destinationTranscript.identity.sha256 !== expectedIdentity.sha256 ||
|
|
13064
|
+
destinationSha256 !== expectedIdentity.sha256
|
|
13065
|
+
)
|
|
13066
|
+
throw new Error("managed_transcript_relocation_destination_identity_mismatch");
|
|
13067
|
+
|
|
13068
|
+
parentArtifacts.assertManagedBinding();
|
|
13069
|
+
parentStore.assertBound();
|
|
13070
|
+
nextStore.assertBound();
|
|
13071
|
+
|
|
13072
|
+
const previousDestination = this.destination;
|
|
13073
|
+
const previousCache = this.#managedTranscriptStoreCache;
|
|
13074
|
+
const previousOwnedAuthority = this.#ownedManagedAuthority;
|
|
13075
|
+
const previousArtifactStore = this.#artifactManager?.getManagedStore();
|
|
13076
|
+
// A hot/shadow sidecar remains a disposable cache over the same verified
|
|
13077
|
+
// transcript bytes. Keep its resident state and cache root; only retag the
|
|
13078
|
+
// cache key. Cold and bounded source state was rejected above because it
|
|
13079
|
+
// pins descriptor/path state that cannot be retargeted without rebuilding.
|
|
13080
|
+
if (this.#managedSidecarCacheStore) this.#managedSidecarCacheSessionFile = nextSessionFile;
|
|
13081
|
+
this.#boundedReadStorageProxy = undefined;
|
|
13082
|
+
this.#managedTranscriptRelocationFileOrigin ??= currentSessionFile;
|
|
13083
|
+
this.destination = nextDestination;
|
|
13084
|
+
this.sessionDir = nextDirectory;
|
|
13085
|
+
this.#sessionFile = nextSessionFile;
|
|
13086
|
+
this.#managedTranscriptStoreCache = { directory: nextDirectory, store: nextStore };
|
|
13087
|
+
this.#managedPersistExpectedIdentity = { ...destinationTranscript.identity };
|
|
13088
|
+
this.#managedRangeExpectedDescriptor = {
|
|
13089
|
+
dev: destinationTranscript.identity.dev,
|
|
13090
|
+
ino: destinationTranscript.identity.ino,
|
|
13091
|
+
nlink: destinationTranscript.identity.nlink,
|
|
13092
|
+
size: destinationTranscript.identity.size,
|
|
13093
|
+
mtimeNs: destinationTranscript.identity.mtimeNs,
|
|
13094
|
+
ctimeNs: destinationTranscript.identity.ctimeNs,
|
|
13095
|
+
};
|
|
13096
|
+
this.#adoptedArtifactManager = parentArtifacts;
|
|
13097
|
+
this.#managedTranscriptRelocationParentArtifacts = parentArtifacts;
|
|
13098
|
+
this.#artifactManager = null;
|
|
13099
|
+
this.#artifactManagerSessionFile = null;
|
|
13100
|
+
this.#ownedManagedAuthority = undefined;
|
|
13101
|
+
adopted = true;
|
|
13102
|
+
|
|
13103
|
+
// These stores were created by this manager. Their close methods release
|
|
13104
|
+
// only authorities they actually own; the borrowed parent manager remains
|
|
13105
|
+
// untouched.
|
|
13106
|
+
previousCache?.store.close();
|
|
13107
|
+
if (previousOwnedAuthority && previousOwnedAuthority !== previousCache?.store) previousOwnedAuthority.close();
|
|
13108
|
+
if (previousArtifactStore && previousArtifactStore !== parentStore && previousArtifactStore !== nextStore)
|
|
13109
|
+
previousArtifactStore.close();
|
|
13110
|
+
if (previousDestination.kind === "managed") previousDestination.securityContext.retainedAuthority?.close();
|
|
13111
|
+
} catch (error) {
|
|
13112
|
+
if (!adopted) {
|
|
13113
|
+
nextStore?.close();
|
|
13114
|
+
if (nextDestination?.kind === "managed") nextDestination.securityContext.retainedAuthority?.close();
|
|
13115
|
+
}
|
|
13116
|
+
throw error;
|
|
13117
|
+
}
|
|
13118
|
+
}
|
|
13119
|
+
|
|
12938
13120
|
#managedTranscriptStore(sessionFile = this.#sessionFile): ManagedSessionDescendantStore {
|
|
12939
13121
|
if (this.destination.kind !== "managed" || !sessionFile) {
|
|
12940
13122
|
throw new Error("Managed transcript authority is unavailable");
|
|
12941
13123
|
}
|
|
13124
|
+
const previousSessionFile = this.#sessionFile;
|
|
13125
|
+
if (sessionFile === previousSessionFile) {
|
|
13126
|
+
this.#refreshManagedTranscriptRelocation(sessionFile);
|
|
13127
|
+
if (this.#sessionFile !== previousSessionFile) sessionFile = this.#sessionFile;
|
|
13128
|
+
}
|
|
13129
|
+
if (!sessionFile) throw new Error("Managed transcript authority is unavailable");
|
|
12942
13130
|
const sessionDir = path.resolve(path.dirname(sessionFile));
|
|
12943
13131
|
if (sessionDir !== path.resolve(this.destination.directory)) {
|
|
12944
13132
|
throw new Error("Managed transcript escaped its session directory");
|
|
@@ -16592,6 +16780,7 @@ export class SessionManager {
|
|
|
16592
16780
|
// aborts. A stale snapshot is never published.
|
|
16593
16781
|
let written = false;
|
|
16594
16782
|
for (let attempt = 0; attempt <= 2 && !written; attempt++) {
|
|
16783
|
+
this.#refreshManagedTranscriptRelocation();
|
|
16595
16784
|
this.#ensureFullHotView();
|
|
16596
16785
|
const token = this.#capturePersistenceInputToken();
|
|
16597
16786
|
await this.#closePersistWriterInternal();
|
|
@@ -16601,6 +16790,7 @@ export class SessionManager {
|
|
|
16601
16790
|
),
|
|
16602
16791
|
);
|
|
16603
16792
|
written = this.#withSessionPersistenceFenceSync(() => {
|
|
16793
|
+
this.#refreshManagedTranscriptRelocation();
|
|
16604
16794
|
if (!this.#persistenceInputTokenMatches(token)) return false;
|
|
16605
16795
|
this.#writeEntriesAtomicallySync(entries);
|
|
16606
16796
|
return true;
|
|
@@ -16624,6 +16814,7 @@ export class SessionManager {
|
|
|
16624
16814
|
|
|
16625
16815
|
#rewriteFileSync(): void {
|
|
16626
16816
|
if (!this.persist || !this.#sessionFile) return;
|
|
16817
|
+
this.#refreshManagedTranscriptRelocation();
|
|
16627
16818
|
this.#withSessionPersistenceFenceSync(() => {
|
|
16628
16819
|
this.#ensureFullHotView();
|
|
16629
16820
|
// Sync lane: capture and check the token immediately (always fresh absent
|
|
@@ -16958,6 +17149,7 @@ export class SessionManager {
|
|
|
16958
17149
|
}
|
|
16959
17150
|
|
|
16960
17151
|
#releaseClosedSessionState(): void {
|
|
17152
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
16961
17153
|
const runtime = this.#sidecarRuntime;
|
|
16962
17154
|
if (runtime) {
|
|
16963
17155
|
runtime.enabled = false;
|
|
@@ -18552,6 +18744,7 @@ export class SessionManager {
|
|
|
18552
18744
|
}
|
|
18553
18745
|
|
|
18554
18746
|
#appendManagedRecordsSync(records: readonly (FileEntry | SessionPatchRecord)[]): void {
|
|
18747
|
+
this.#refreshManagedTranscriptRelocation();
|
|
18555
18748
|
if (!this.#sessionFile) throw new Error("Managed transcript path is unavailable");
|
|
18556
18749
|
this.#withSessionPersistenceFenceSync(() => {
|
|
18557
18750
|
const sessionFile = this.#sessionFile!;
|
|
@@ -18580,12 +18773,23 @@ export class SessionManager {
|
|
|
18580
18773
|
|
|
18581
18774
|
async #persistPatches(records: readonly SessionPatchRecord[]): Promise<void> {
|
|
18582
18775
|
if (records.length === 0) return;
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18776
|
+
try {
|
|
18777
|
+
this.#refreshManagedTranscriptRelocation();
|
|
18778
|
+
} catch (error) {
|
|
18779
|
+
this.#recordPersistError(error);
|
|
18780
|
+
throw this.#persistError ?? toError(error);
|
|
18781
|
+
}
|
|
18782
|
+
if (!this.persist || !this.#sessionFile || !this.#storage.existsSync(this.#sessionFile)) {
|
|
18783
|
+
if (this.#coldSidecarActive()) this.#deactivateColdForBranchMutation();
|
|
18784
|
+
return;
|
|
18785
|
+
}
|
|
18786
|
+
let sessionFile = this.#sessionFile;
|
|
18586
18787
|
const publishResumeBreadcrumb = this.#readOnlyResume;
|
|
18587
18788
|
await this.#queuePersistTask(async () => {
|
|
18588
18789
|
for (let attempt = 0; attempt <= 2; attempt++) {
|
|
18790
|
+
this.#refreshManagedTranscriptRelocation();
|
|
18791
|
+
if (this.#coldSidecarActive()) this.#deactivateColdForBranchMutation();
|
|
18792
|
+
sessionFile = this.#sessionFile!;
|
|
18589
18793
|
const token = this.#capturePersistenceInputToken();
|
|
18590
18794
|
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
18591
18795
|
if (
|
|
@@ -18594,6 +18798,7 @@ export class SessionManager {
|
|
|
18594
18798
|
(header?.version ?? 1) < CURRENT_SESSION_VERSION
|
|
18595
18799
|
) {
|
|
18596
18800
|
await this.#rewriteFileContents();
|
|
18801
|
+
sessionFile = this.#sessionFile!;
|
|
18597
18802
|
if (publishResumeBreadcrumb) writeTerminalBreadcrumb(this.cwd, sessionFile);
|
|
18598
18803
|
this.#readOnlyResume = false;
|
|
18599
18804
|
return;
|
|
@@ -18613,6 +18818,7 @@ export class SessionManager {
|
|
|
18613
18818
|
SessionManagerTestHooks.beforePersistPatchFence?.(attempt);
|
|
18614
18819
|
let persisted = false;
|
|
18615
18820
|
const written = this.#withSessionPersistenceFenceSync(() => {
|
|
18821
|
+
this.#refreshManagedTranscriptRelocation();
|
|
18616
18822
|
if (!this.#persistenceInputTokenMatches(token)) return false;
|
|
18617
18823
|
if (this.destination.kind === "managed") {
|
|
18618
18824
|
this.#appendManagedRecordsSync(persistedRecords);
|
|
@@ -18645,8 +18851,14 @@ export class SessionManager {
|
|
|
18645
18851
|
_persist(entry: SessionEntry): void {
|
|
18646
18852
|
if (!this.persist || !this.#sessionFile) return;
|
|
18647
18853
|
if (this.#strictClosePending) throw new Error("Session manager is closing.");
|
|
18648
|
-
const publishResumeBreadcrumb = this.#readOnlyResume;
|
|
18649
18854
|
if (this.#persistError) throw this.#persistError;
|
|
18855
|
+
try {
|
|
18856
|
+
this.#refreshManagedTranscriptRelocation();
|
|
18857
|
+
} catch (error) {
|
|
18858
|
+
this.#recordPersistError(error);
|
|
18859
|
+
throw this.#persistError ?? toError(error);
|
|
18860
|
+
}
|
|
18861
|
+
const publishResumeBreadcrumb = this.#readOnlyResume;
|
|
18650
18862
|
|
|
18651
18863
|
// Normally we wait for the first assistant message before persisting to avoid
|
|
18652
18864
|
// creating files for sessions that never produce output. Once ensureOnDisk() has
|
|
@@ -20553,6 +20765,7 @@ export class SessionManager {
|
|
|
20553
20765
|
}
|
|
20554
20766
|
this.#sessionId = newSessionId;
|
|
20555
20767
|
this.#sessionFile = newSessionFile;
|
|
20768
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
20556
20769
|
this.#flushed = true;
|
|
20557
20770
|
this.#commitResidentTextStoreTransition(transition);
|
|
20558
20771
|
this.#adoptManagedPersistIdentity(newSessionFile);
|
|
@@ -20587,6 +20800,7 @@ export class SessionManager {
|
|
|
20587
20800
|
"retain-and-throw",
|
|
20588
20801
|
);
|
|
20589
20802
|
this.#sessionId = newSessionId;
|
|
20803
|
+
this.#clearManagedTranscriptRelocationResolver();
|
|
20590
20804
|
this.#commitResidentTextStoreTransition(transition);
|
|
20591
20805
|
return undefined;
|
|
20592
20806
|
}
|
|
@@ -23045,7 +23259,15 @@ export class SessionManager {
|
|
|
23045
23259
|
storage,
|
|
23046
23260
|
);
|
|
23047
23261
|
|
|
23048
|
-
const terminalSession = await readTerminalBreadcrumb(cwd
|
|
23262
|
+
const terminalSession = await readTerminalBreadcrumb(cwd, {
|
|
23263
|
+
...(destination.kind === "managed"
|
|
23264
|
+
? {
|
|
23265
|
+
sessionsRoot: destination.securityContext.sessionsRoot,
|
|
23266
|
+
continuationInventory: managedCandidates,
|
|
23267
|
+
}
|
|
23268
|
+
: {}),
|
|
23269
|
+
allowExplicitOutsideRoot: destination.kind === "explicit",
|
|
23270
|
+
});
|
|
23049
23271
|
const terminalSessionIsInExplicitRoot = (() => {
|
|
23050
23272
|
if (destination.kind === "managed" || !terminalSession) return destination.kind === "managed";
|
|
23051
23273
|
const canonicalRoot = canonicalizeTrustedPath(destination.directory);
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
AuthCredentialSelector,
|
|
14
|
+
ConfiguredCredentialPreference,
|
|
15
|
+
CredentialRankingMode,
|
|
16
|
+
} from "@gajae-code/ai/core";
|
|
13
17
|
import { $credentialEnv, getAgentDbPath, getAgentDir, getConfigRootDir, isEnoent, logger } from "@gajae-code/utils";
|
|
14
18
|
import { YAML } from "bun";
|
|
15
19
|
import { readSecureTokenFile } from "./secure-token-file";
|
|
@@ -27,6 +31,8 @@ export interface StartupAuthConfigSnapshot {
|
|
|
27
31
|
credentialPinStoreIdentity?: string;
|
|
28
32
|
credentialRankingMode: CredentialRankingMode;
|
|
29
33
|
credentialPins: Readonly<Record<string, string>>;
|
|
34
|
+
/** Ordered soft per-provider preferences; only portable email/`account:` selectors are accepted. */
|
|
35
|
+
credentialPreferences: Readonly<Record<string, readonly ConfiguredCredentialPreference[]>>;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export type StartupAuthConfigErrorKind =
|
|
@@ -37,7 +43,8 @@ export type StartupAuthConfigErrorKind =
|
|
|
37
43
|
| "invalid-broker"
|
|
38
44
|
| "invalid-gateway"
|
|
39
45
|
| "invalid-ranking-mode"
|
|
40
|
-
| "invalid-credential-pins"
|
|
46
|
+
| "invalid-credential-pins"
|
|
47
|
+
| "invalid-credential-preferences";
|
|
41
48
|
|
|
42
49
|
/** A malformed startup-auth config aborts resolution rather than downgrading to local authority. */
|
|
43
50
|
export class StartupAuthConfigError extends Error {
|
|
@@ -64,6 +71,7 @@ const LEGACY_LITERAL_AUTH_KEYS = new Set([
|
|
|
64
71
|
"auth.broker.token",
|
|
65
72
|
"auth.credentialRankingMode",
|
|
66
73
|
"auth.credentialPins",
|
|
74
|
+
"auth.credentialPreferences",
|
|
67
75
|
]);
|
|
68
76
|
|
|
69
77
|
function configPathForAgentDir(agentDir: string): string {
|
|
@@ -78,7 +86,7 @@ function shortenConfigPath(configPath: string): string {
|
|
|
78
86
|
return absolutePath.startsWith(homePrefix) ? `~${absolutePath.slice(homePath.length)}` : absolutePath;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
function startupAuthConfigError(agentDir: string, kind: StartupAuthConfigErrorKind): StartupAuthConfigError {
|
|
89
|
+
export function startupAuthConfigError(agentDir: string, kind: StartupAuthConfigErrorKind): StartupAuthConfigError {
|
|
82
90
|
return new StartupAuthConfigError(kind, configPathForAgentDir(agentDir));
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -100,6 +108,69 @@ export function isValidPersistedCredentialSelector(value: string): boolean {
|
|
|
100
108
|
return parsePersistedCredentialSelector(value) !== undefined;
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
/** JSON-schema pattern for a preference account; shared with {@link parsePreferenceAccount}. */
|
|
112
|
+
export const PREFERENCE_ACCOUNT_PATTERN = "^(email:[^@\\s]+@[^@\\s]+|account:\\S+|[^@\\s:]+@[^@\\s]+)$";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse one `auth.credentialPreferences` account: a bare email, `email:<address>`, or `account:<id>`. Numeric row ids
|
|
116
|
+
* are excluded: they are bound to one credential store, and a preference has no store-identity binding to detect a
|
|
117
|
+
* retargeted row.
|
|
118
|
+
*/
|
|
119
|
+
export function parsePreferenceAccount(value: string): AuthCredentialSelector | undefined {
|
|
120
|
+
const trimmed = value.trim();
|
|
121
|
+
const selector = parsePersistedCredentialSelector(trimmed);
|
|
122
|
+
if (selector) return selector.kind === "id" ? undefined : selector;
|
|
123
|
+
return /^[^@\s:]+@[^@\s]+$/.test(trimmed) ? { kind: "email", value: trimmed } : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A preference cap accepts (0, 100]; 100 moves past the account only once it is blocked. */
|
|
127
|
+
export function isValidCredentialPreferenceCap(value: unknown): value is number {
|
|
128
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 100;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Canonical config text for one preference entry: the account, or a one-key `{ account: cap }` mapping. */
|
|
132
|
+
export type CredentialPreferenceEntryValue = string | Readonly<Record<string, number>>;
|
|
133
|
+
|
|
134
|
+
function parseCredentialPreferenceEntry(value: unknown): ConfiguredCredentialPreference | undefined {
|
|
135
|
+
if (typeof value === "string") {
|
|
136
|
+
const selector = parsePreferenceAccount(value);
|
|
137
|
+
return selector ? { selector } : undefined;
|
|
138
|
+
}
|
|
139
|
+
const entry = asRecord(value);
|
|
140
|
+
const fields = entry ? Object.entries(entry) : [];
|
|
141
|
+
if (fields.length !== 1) return undefined;
|
|
142
|
+
const [account, cap] = fields[0]!;
|
|
143
|
+
const selector = parsePreferenceAccount(account);
|
|
144
|
+
if (!selector || !isValidCredentialPreferenceCap(cap)) return undefined;
|
|
145
|
+
return cap === 100 ? { selector } : { selector, maxUsedFraction: cap / 100 };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function preferenceSelectorKey(selector: AuthCredentialSelector): string {
|
|
149
|
+
return `${selector.kind}:${selector.kind === "email" ? selector.value.toLowerCase() : selector.value}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse one provider's `auth.credentialPreferences` value: a single account, or a non-empty ordered list whose
|
|
154
|
+
* entries are an account (`me@example.com`) or an account with a usage cap in percent (`me@example.com: 75`).
|
|
155
|
+
* Returns undefined for any invalid value, including an account listed twice, so a typo fails closed instead of
|
|
156
|
+
* silently changing the order.
|
|
157
|
+
*/
|
|
158
|
+
export function parseCredentialPreferenceList(value: unknown): ConfiguredCredentialPreference[] | undefined {
|
|
159
|
+
const rawEntries = Array.isArray(value) ? value : [value];
|
|
160
|
+
if (rawEntries.length === 0) return undefined;
|
|
161
|
+
const entries: ConfiguredCredentialPreference[] = [];
|
|
162
|
+
const seen = new Set<string>();
|
|
163
|
+
for (const rawEntry of rawEntries) {
|
|
164
|
+
const entry = parseCredentialPreferenceEntry(rawEntry);
|
|
165
|
+
if (!entry) return undefined;
|
|
166
|
+
const key = preferenceSelectorKey(entry.selector);
|
|
167
|
+
if (seen.has(key)) return undefined;
|
|
168
|
+
seen.add(key);
|
|
169
|
+
entries.push(entry);
|
|
170
|
+
}
|
|
171
|
+
return entries;
|
|
172
|
+
}
|
|
173
|
+
|
|
103
174
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
104
175
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
105
176
|
? (value as Record<string, unknown>)
|
|
@@ -145,6 +216,10 @@ function throwLegacyLiteralKeyError(keys: string[]): never {
|
|
|
145
216
|
" credentialRankingMode: balanced\n" +
|
|
146
217
|
" credentialPins:\n" +
|
|
147
218
|
" <provider>: id:<positive-id>\n" +
|
|
219
|
+
" credentialPreferences:\n" +
|
|
220
|
+
" <provider>:\n" +
|
|
221
|
+
" - <email>\n" +
|
|
222
|
+
" - <email>: <max used percent, above 0 and at most 100>\n" +
|
|
148
223
|
"Do not copy secret values into command output; rewrite the file by hand.",
|
|
149
224
|
);
|
|
150
225
|
}
|
|
@@ -172,23 +247,53 @@ function readCredentialPins(
|
|
|
172
247
|
return result;
|
|
173
248
|
}
|
|
174
249
|
|
|
250
|
+
function readCredentialPreferences(
|
|
251
|
+
auth: Record<string, unknown> | undefined,
|
|
252
|
+
configPath: string,
|
|
253
|
+
): Readonly<Record<string, readonly ConfiguredCredentialPreference[]>> {
|
|
254
|
+
const rawPreferences = auth?.credentialPreferences;
|
|
255
|
+
if (rawPreferences === undefined) return {};
|
|
256
|
+
const preferences = asRecord(rawPreferences);
|
|
257
|
+
if (!preferences) throw new StartupAuthConfigError("invalid-credential-preferences", configPath);
|
|
258
|
+
|
|
259
|
+
const result: Record<string, readonly ConfiguredCredentialPreference[]> = {};
|
|
260
|
+
for (const [provider, rawList] of Object.entries(preferences)) {
|
|
261
|
+
const normalizedProvider = provider.trim();
|
|
262
|
+
if (normalizedProvider.length === 0 || /[\u0000-\u001f\u007f]/u.test(normalizedProvider)) {
|
|
263
|
+
throw new StartupAuthConfigError("invalid-credential-preferences", configPath);
|
|
264
|
+
}
|
|
265
|
+
const entries = parseCredentialPreferenceList(rawList);
|
|
266
|
+
if (!entries) throw new StartupAuthConfigError("invalid-credential-preferences", configPath);
|
|
267
|
+
result[normalizedProvider] = entries;
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
175
272
|
interface GlobalStartupAuthYaml {
|
|
176
273
|
auth: Record<string, unknown> | undefined;
|
|
177
274
|
credentialPinStoreIdentity: string | undefined;
|
|
178
275
|
credentialPins: Readonly<Record<string, string>>;
|
|
276
|
+
credentialPreferences: Readonly<Record<string, readonly ConfiguredCredentialPreference[]>>;
|
|
179
277
|
}
|
|
180
278
|
|
|
279
|
+
const EMPTY_GLOBAL_STARTUP_AUTH: GlobalStartupAuthYaml = {
|
|
280
|
+
auth: undefined,
|
|
281
|
+
credentialPinStoreIdentity: undefined,
|
|
282
|
+
credentialPins: {},
|
|
283
|
+
credentialPreferences: {},
|
|
284
|
+
};
|
|
285
|
+
|
|
181
286
|
async function readGlobalStartupAuthYaml(agentDir: string): Promise<GlobalStartupAuthYaml> {
|
|
182
287
|
const configPath = configPathForAgentDir(agentDir);
|
|
183
288
|
let raw: string;
|
|
184
289
|
try {
|
|
185
290
|
raw = await Bun.file(configPath).text();
|
|
186
291
|
} catch (error) {
|
|
187
|
-
if (isEnoent(error)) return
|
|
292
|
+
if (isEnoent(error)) return EMPTY_GLOBAL_STARTUP_AUTH;
|
|
188
293
|
logger.warn("startup auth config.yml unreadable", { path: shortenConfigPath(configPath), cause: "unreadable" });
|
|
189
294
|
throw startupAuthConfigError(agentDir, "unreadable");
|
|
190
295
|
}
|
|
191
|
-
if (raw.trim() === "") return
|
|
296
|
+
if (raw.trim() === "") return EMPTY_GLOBAL_STARTUP_AUTH;
|
|
192
297
|
|
|
193
298
|
let parsed: unknown;
|
|
194
299
|
try {
|
|
@@ -211,7 +316,7 @@ async function readGlobalStartupAuthYaml(agentDir: string): Promise<GlobalStartu
|
|
|
211
316
|
const legacyKeys = Object.keys(root).filter(key => LEGACY_LITERAL_AUTH_KEYS.has(key));
|
|
212
317
|
if (legacyKeys.length > 0) throwLegacyLiteralKeyError(legacyKeys);
|
|
213
318
|
|
|
214
|
-
if (root.auth === undefined) return
|
|
319
|
+
if (root.auth === undefined) return EMPTY_GLOBAL_STARTUP_AUTH;
|
|
215
320
|
const auth = asRecord(root.auth);
|
|
216
321
|
if (!auth) throw startupAuthConfigError(agentDir, "invalid-auth");
|
|
217
322
|
|
|
@@ -234,12 +339,13 @@ async function readGlobalStartupAuthYaml(agentDir: string): Promise<GlobalStartu
|
|
|
234
339
|
throw startupAuthConfigError(agentDir, "invalid-ranking-mode");
|
|
235
340
|
}
|
|
236
341
|
const credentialPins = readCredentialPins(auth, configPath);
|
|
342
|
+
const credentialPreferences = readCredentialPreferences(auth, configPath);
|
|
237
343
|
const rawCredentialPinStoreIdentity = auth.credentialPinStoreIdentity;
|
|
238
344
|
const credentialPinStoreIdentity =
|
|
239
345
|
typeof rawCredentialPinStoreIdentity === "string" && rawCredentialPinStoreIdentity.trim().length > 0
|
|
240
346
|
? rawCredentialPinStoreIdentity.trim()
|
|
241
347
|
: undefined;
|
|
242
|
-
return { auth, credentialPinStoreIdentity, credentialPins };
|
|
348
|
+
return { auth, credentialPinStoreIdentity, credentialPins, credentialPreferences };
|
|
243
349
|
}
|
|
244
350
|
|
|
245
351
|
async function readTokenFile(): Promise<string | undefined> {
|
|
@@ -253,7 +359,8 @@ async function readTokenFile(): Promise<string | undefined> {
|
|
|
253
359
|
* cannot influence credential selection.
|
|
254
360
|
*/
|
|
255
361
|
export async function resolveStartupAuthConfig(agentDir: string = getAgentDir()): Promise<StartupAuthConfigSnapshot> {
|
|
256
|
-
const { auth, credentialPinStoreIdentity, credentialPins } =
|
|
362
|
+
const { auth, credentialPinStoreIdentity, credentialPins, credentialPreferences } =
|
|
363
|
+
await readGlobalStartupAuthYaml(agentDir);
|
|
257
364
|
const broker = asRecord(auth?.broker);
|
|
258
365
|
const envUrl = $credentialEnv("GJC_AUTH_BROKER_URL")?.trim();
|
|
259
366
|
const envToken = $credentialEnv("GJC_AUTH_BROKER_TOKEN")?.trim();
|
|
@@ -293,5 +400,6 @@ export async function resolveStartupAuthConfig(agentDir: string = getAgentDir())
|
|
|
293
400
|
...(credentialPinStoreIdentity ? { credentialPinStoreIdentity } : {}),
|
|
294
401
|
credentialRankingMode: rankingMode,
|
|
295
402
|
credentialPins,
|
|
403
|
+
credentialPreferences,
|
|
296
404
|
};
|
|
297
405
|
}
|