@gajae-code/coding-agent 0.11.6 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +56 -6
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +57 -6
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +86 -27
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/index.ts +4 -5
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +1663 -375
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +240 -149
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +319 -85
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -18,7 +18,6 @@ import * as native from "@gajae-code/natives";
|
|
|
18
18
|
import { getTerminalId } from "@gajae-code/tui";
|
|
19
19
|
import {
|
|
20
20
|
getBlobsDir,
|
|
21
|
-
getAgentDir as getDefaultAgentDir,
|
|
22
21
|
getProjectDir,
|
|
23
22
|
getSessionsDir,
|
|
24
23
|
getTerminalSessionsDir,
|
|
@@ -58,6 +57,7 @@ import {
|
|
|
58
57
|
canonicalizeTrustedPath,
|
|
59
58
|
deleteManagedSessionCandidate,
|
|
60
59
|
listManagedCandidates,
|
|
60
|
+
type ManagedCandidateWriteAuthority,
|
|
61
61
|
type ManagedMigrationPolicy,
|
|
62
62
|
type ManagedScope,
|
|
63
63
|
managedDirectoryAuthorityForScope,
|
|
@@ -65,11 +65,14 @@ import {
|
|
|
65
65
|
openManagedCandidateForWrite,
|
|
66
66
|
prepareManagedSessionScopeForWriteSync,
|
|
67
67
|
resolveManagedScope,
|
|
68
|
+
resolveManagedScopeForWrite,
|
|
68
69
|
} from "./internal/managed-session-scope";
|
|
69
70
|
import {
|
|
71
|
+
assertManagedDirectoryRoot,
|
|
70
72
|
type ManagedDirectoryRoot,
|
|
71
73
|
type ManagedFileSnapshot,
|
|
72
74
|
ManagedSessionDescendantStore,
|
|
75
|
+
type ManagedSessionSecurityPolicy,
|
|
73
76
|
retainManagedDirectoryAuthority,
|
|
74
77
|
} from "./internal/managed-session-storage";
|
|
75
78
|
|
|
@@ -1324,9 +1327,12 @@ function computeDefaultSessionDir(
|
|
|
1324
1327
|
sessionsRoot: string = getSessionsDir(),
|
|
1325
1328
|
): string {
|
|
1326
1329
|
if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
|
|
1327
|
-
const resolved =
|
|
1330
|
+
const resolved = resolveManagedScopeForWrite({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
1328
1331
|
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
1329
|
-
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1332
|
+
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1333
|
+
resolved.scope,
|
|
1334
|
+
process.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
1335
|
+
);
|
|
1330
1336
|
if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
|
|
1331
1337
|
return prepared.scope.directoryPath;
|
|
1332
1338
|
}
|
|
@@ -1408,6 +1414,13 @@ function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: Mana
|
|
|
1408
1414
|
);
|
|
1409
1415
|
}
|
|
1410
1416
|
const trustedSessionDestinations = new WeakSet<SessionDestination>();
|
|
1417
|
+
const managedSecurityPolicies = new WeakMap<ManagedSessionSecurityContext, ManagedSessionSecurityPolicy>();
|
|
1418
|
+
|
|
1419
|
+
function managedSecurityPolicyForContext(securityContext: ManagedSessionSecurityContext): ManagedSessionSecurityPolicy {
|
|
1420
|
+
const policy = managedSecurityPolicies.get(securityContext);
|
|
1421
|
+
if (!policy) throw new Error("Managed session security policy unavailable");
|
|
1422
|
+
return policy;
|
|
1423
|
+
}
|
|
1411
1424
|
|
|
1412
1425
|
function managedStoreFromContext(
|
|
1413
1426
|
securityContext: ManagedSessionSecurityContext,
|
|
@@ -1419,6 +1432,7 @@ function managedStoreFromContext(
|
|
|
1419
1432
|
securityContext.retainedAuthority
|
|
1420
1433
|
? { authority: securityContext.retainedAuthority, authorityBaseDir: directory }
|
|
1421
1434
|
: undefined,
|
|
1435
|
+
managedSecurityPolicyForContext(securityContext),
|
|
1422
1436
|
);
|
|
1423
1437
|
}
|
|
1424
1438
|
|
|
@@ -1431,6 +1445,10 @@ function freezeManagedDestination(scope: ManagedScope): SessionDestination {
|
|
|
1431
1445
|
rootAuthority,
|
|
1432
1446
|
retainedAuthority: managedDirectoryAuthorityForScope(scope),
|
|
1433
1447
|
});
|
|
1448
|
+
managedSecurityPolicies.set(
|
|
1449
|
+
securityContext,
|
|
1450
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
1451
|
+
);
|
|
1434
1452
|
const destination = Object.freeze({ kind: "managed" as const, directory: scope.directoryPath, securityContext });
|
|
1435
1453
|
trustedSessionDestinations.add(destination);
|
|
1436
1454
|
return destination;
|
|
@@ -1445,9 +1463,16 @@ function explicitDestination(directory: string): SessionDestination {
|
|
|
1445
1463
|
function managedDestination(cwd: string, storage: SessionStorage, agentDir?: string): SessionDestination {
|
|
1446
1464
|
if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
|
|
1447
1465
|
const sessionsRoot = getSessionsDir(agentDir);
|
|
1448
|
-
const resolved =
|
|
1466
|
+
const resolved = resolveManagedScopeForWrite({
|
|
1467
|
+
cwd,
|
|
1468
|
+
agentDir: agentDir ?? path.resolve(sessionsRoot, ".."),
|
|
1469
|
+
sessionsRoot,
|
|
1470
|
+
});
|
|
1449
1471
|
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
1450
|
-
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1472
|
+
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1473
|
+
resolved.scope,
|
|
1474
|
+
process.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
1475
|
+
);
|
|
1451
1476
|
if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
|
|
1452
1477
|
return freezeManagedDestination(prepared.scope);
|
|
1453
1478
|
}
|
|
@@ -1814,12 +1839,46 @@ function inspectResumeSessionFile(
|
|
|
1814
1839
|
mtimeNs: snapshot.mtimeNs,
|
|
1815
1840
|
sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
1816
1841
|
};
|
|
1817
|
-
return { identity, content:
|
|
1842
|
+
return { identity, content: bytes, entries, context, migrationApplied };
|
|
1818
1843
|
} catch {
|
|
1819
1844
|
return { kind: "error", reason: "malformed" };
|
|
1820
1845
|
}
|
|
1821
1846
|
}
|
|
1822
1847
|
|
|
1848
|
+
/** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
|
|
1849
|
+
function revalidateResumeSessionIdentity(
|
|
1850
|
+
filePath: string,
|
|
1851
|
+
storage: SessionStorage,
|
|
1852
|
+
expected: ResumeSessionIdentity,
|
|
1853
|
+
): StrictSessionOpenFailure | { kind: "valid" } {
|
|
1854
|
+
const canonicalPath = resolveEquivalentPath(path.resolve(filePath));
|
|
1855
|
+
let before: SessionStorageStat;
|
|
1856
|
+
let snapshot: SessionStorageSnapshot;
|
|
1857
|
+
let after: SessionStorageStat;
|
|
1858
|
+
try {
|
|
1859
|
+
before = storage.statSync(canonicalPath);
|
|
1860
|
+
if (!before.isFile || !storage.readSnapshotSync) return { kind: "error", reason: "read-failed" };
|
|
1861
|
+
snapshot = storage.readSnapshotSync(canonicalPath);
|
|
1862
|
+
after = storage.statSync(canonicalPath);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
return resumeReadFailure(error, storage, canonicalPath);
|
|
1865
|
+
}
|
|
1866
|
+
if (!sameResumeStat(before, snapshot.stat) || !sameResumeStat(snapshot.stat, after)) {
|
|
1867
|
+
return { kind: "error", reason: "unstable" };
|
|
1868
|
+
}
|
|
1869
|
+
const observed: ResumeSessionIdentity = {
|
|
1870
|
+
canonicalPath,
|
|
1871
|
+
sessionId: expected.sessionId,
|
|
1872
|
+
dev: snapshot.stat.dev,
|
|
1873
|
+
ino: snapshot.stat.ino,
|
|
1874
|
+
size: snapshot.stat.size,
|
|
1875
|
+
mtimeMs: snapshot.stat.mtimeMs,
|
|
1876
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
1877
|
+
sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
1878
|
+
};
|
|
1879
|
+
return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1823
1882
|
/**
|
|
1824
1883
|
* Convert legacy persisted blob references in loaded entries into resident sentinels.
|
|
1825
1884
|
* Images then materialize lazily at provider/display/export chokepoints instead of
|
|
@@ -3867,9 +3926,11 @@ export async function resolveResumableSession(
|
|
|
3867
3926
|
cwd: string,
|
|
3868
3927
|
sessionDir?: string,
|
|
3869
3928
|
storage: SessionStorage = new FileSessionStorage(),
|
|
3929
|
+
managedAgentDir?: string,
|
|
3870
3930
|
): Promise<ResolvedSessionMatch | undefined> {
|
|
3871
|
-
const
|
|
3872
|
-
|
|
3931
|
+
const localSessions = sessionDir
|
|
3932
|
+
? await SessionManager.list(cwd, sessionDir, storage)
|
|
3933
|
+
: await SessionManager.listManagedForResumePickerReadOnly(cwd, managedAgentDir, storage);
|
|
3873
3934
|
const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
3874
3935
|
if (localMatch) {
|
|
3875
3936
|
return { session: localMatch, scope: "local" };
|
|
@@ -3879,7 +3940,7 @@ export async function resolveResumableSession(
|
|
|
3879
3940
|
return undefined;
|
|
3880
3941
|
}
|
|
3881
3942
|
|
|
3882
|
-
const globalSessions = await SessionManager.listAll(storage);
|
|
3943
|
+
const globalSessions = await SessionManager.listAll(storage, managedAgentDir);
|
|
3883
3944
|
const globalMatch = globalSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
3884
3945
|
if (!globalMatch) {
|
|
3885
3946
|
return undefined;
|
|
@@ -3911,6 +3972,7 @@ export class SessionManager {
|
|
|
3911
3972
|
#ensuredOnDisk: boolean = false;
|
|
3912
3973
|
#recoveryHydrationContext: RecoveryHydrationContext | undefined;
|
|
3913
3974
|
#fileEntries: FileEntry[] = [];
|
|
3975
|
+
#pendingStrictAdoption: { canonicalPath: string; identity: ResumeSessionIdentity } | undefined;
|
|
3914
3976
|
#byId: Map<string, SessionEntry> = new Map();
|
|
3915
3977
|
#labelsById: Map<string, string> = new Map();
|
|
3916
3978
|
#leafId: string | null = null;
|
|
@@ -4157,15 +4219,16 @@ export class SessionManager {
|
|
|
4157
4219
|
}
|
|
4158
4220
|
|
|
4159
4221
|
async #hydrateExistingSession(sessionFile: string, entries: FileEntry[], migrationApplied: boolean): Promise<void> {
|
|
4160
|
-
|
|
4161
|
-
|
|
4222
|
+
// Strict inspection creates these entries solely for this hydration path. Adopt that
|
|
4223
|
+
// final, validated representation instead of cloning the complete transcript again.
|
|
4224
|
+
const header = entries[0] as SessionHeader;
|
|
4162
4225
|
this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
4163
4226
|
this.#sessionId = header.id;
|
|
4164
4227
|
this.#sessionName = header.title;
|
|
4165
4228
|
this.#titleSource = header.titleSource;
|
|
4166
4229
|
this.#needsFullRewriteOnNextPersist = migrationApplied;
|
|
4167
|
-
await resolveBlobRefsInEntries(
|
|
4168
|
-
this.#fileEntries =
|
|
4230
|
+
await resolveBlobRefsInEntries(entries, this.#blobStore);
|
|
4231
|
+
this.#fileEntries = entries;
|
|
4169
4232
|
this.#resetResidentTextBlobStore();
|
|
4170
4233
|
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
4171
4234
|
prepareEntryForResidentSync(entry, this.#residentBlobStores()),
|
|
@@ -4184,12 +4247,25 @@ export class SessionManager {
|
|
|
4184
4247
|
|
|
4185
4248
|
/** Switch to a different session file (used for resume and branching) */
|
|
4186
4249
|
async setSessionFile(sessionFile: string): Promise<void> {
|
|
4250
|
+
const resolvedSessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
4251
|
+
const strictAdoption = this.#pendingStrictAdoption;
|
|
4252
|
+
this.#pendingStrictAdoption = undefined;
|
|
4253
|
+
if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
4254
|
+
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
4255
|
+
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
4256
|
+
throw new Error("Prepared managed session changed before strict adoption.");
|
|
4257
|
+
}
|
|
4187
4258
|
await this.#closePersistWriter();
|
|
4188
4259
|
this.#persistError = undefined;
|
|
4189
4260
|
this.#persistErrorReported = false;
|
|
4190
|
-
this.#sessionFile =
|
|
4261
|
+
this.#sessionFile = resolvedSessionFile;
|
|
4191
4262
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
4192
4263
|
this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
|
|
4264
|
+
if (strictAdoption) {
|
|
4265
|
+
const inspected = inspectResumeSessionFile(this.#sessionFile, this.storage);
|
|
4266
|
+
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
4267
|
+
throw new Error("Prepared managed session changed during strict adoption.");
|
|
4268
|
+
}
|
|
4193
4269
|
if (this.#fileEntries.length > 0) {
|
|
4194
4270
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
4195
4271
|
this.#sessionId = header?.id ?? createSessionId();
|
|
@@ -4533,17 +4609,14 @@ export class SessionManager {
|
|
|
4533
4609
|
const previousDestination = this.destination;
|
|
4534
4610
|
const previousArtifactDir = previousSessionFile?.slice(0, -6);
|
|
4535
4611
|
|
|
4536
|
-
const nextDestination =
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
: computeDefaultSessionDir(resolvedCwd, this.storage);
|
|
4545
|
-
return explicitDestination(directory);
|
|
4546
|
-
})();
|
|
4612
|
+
const nextDestination =
|
|
4613
|
+
this.storage instanceof FileSessionStorage
|
|
4614
|
+
? managedDestination(
|
|
4615
|
+
resolvedCwd,
|
|
4616
|
+
this.storage,
|
|
4617
|
+
this.destination.kind === "managed" ? this.destination.securityContext.agentDir : undefined,
|
|
4618
|
+
)
|
|
4619
|
+
: this.destination;
|
|
4547
4620
|
const newSessionDir = nextDestination.directory;
|
|
4548
4621
|
let hadSessionFile = false;
|
|
4549
4622
|
const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
|
|
@@ -5523,6 +5596,17 @@ export class SessionManager {
|
|
|
5523
5596
|
return this.sessionDir;
|
|
5524
5597
|
}
|
|
5525
5598
|
|
|
5599
|
+
/** Lists picker candidates within this manager's captured destination authority. */
|
|
5600
|
+
async listForResumePickerReadOnly(): Promise<SessionInfo[]> {
|
|
5601
|
+
return this.destination.kind === "managed"
|
|
5602
|
+
? await SessionManager.listManagedForResumePickerReadOnly(
|
|
5603
|
+
this.cwd,
|
|
5604
|
+
this.destination.securityContext.agentDir,
|
|
5605
|
+
this.storage,
|
|
5606
|
+
)
|
|
5607
|
+
: await SessionManager.listForResumePickerReadOnly(this.cwd, this.destination.directory, this.storage);
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5526
5610
|
getSessionId(): string {
|
|
5527
5611
|
return this.#sessionId;
|
|
5528
5612
|
}
|
|
@@ -5552,13 +5636,22 @@ export class SessionManager {
|
|
|
5552
5636
|
getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
|
|
5553
5637
|
if (this.destination.kind !== "managed" || !this.#sessionFile || this.#adoptedArtifactManager) return null;
|
|
5554
5638
|
const store = this.#managedTranscriptStore();
|
|
5639
|
+
const legacyArtifactsRoot = path.basename(this.#sessionFile.slice(0, -6));
|
|
5640
|
+
const legacyLocalRoot = path.posix.join(legacyArtifactsRoot, "local");
|
|
5555
5641
|
return {
|
|
5556
5642
|
capture: async () => {
|
|
5557
5643
|
let snapshot: native.NativeDirectoryTreeSnapshot;
|
|
5558
5644
|
try {
|
|
5559
|
-
snapshot = store.captureTree(
|
|
5645
|
+
snapshot = store.captureTree(legacyLocalRoot);
|
|
5560
5646
|
} catch (error) {
|
|
5561
5647
|
if (error instanceof Error && error.message === "not_found") return null;
|
|
5648
|
+
if (error instanceof Error && error.message === "reparse_point") {
|
|
5649
|
+
try {
|
|
5650
|
+
store.captureTree(legacyArtifactsRoot);
|
|
5651
|
+
} catch (artifactsError) {
|
|
5652
|
+
if (artifactsError instanceof Error && artifactsError.message === "not_found") return null;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5562
5655
|
throw error;
|
|
5563
5656
|
}
|
|
5564
5657
|
let totalBytes = 0;
|
|
@@ -5566,7 +5659,7 @@ export class SessionManager {
|
|
|
5566
5659
|
if (totalBytes > 64 * 1024 * 1024) throw new Error("Legacy local:// migration exceeds the safe size limit");
|
|
5567
5660
|
const entries = snapshot.entries.map(entry => {
|
|
5568
5661
|
if (entry.kind === "directory") return { relativePath: entry.relativePath, kind: "directory" as const };
|
|
5569
|
-
const captured = store.readExpected(path.posix.join(
|
|
5662
|
+
const captured = store.readExpected(path.posix.join(legacyLocalRoot, entry.relativePath));
|
|
5570
5663
|
if (
|
|
5571
5664
|
!captured ||
|
|
5572
5665
|
captured.identity.dev.toString() !== entry.dev ||
|
|
@@ -5582,12 +5675,12 @@ export class SessionManager {
|
|
|
5582
5675
|
sha256: entry.sha256,
|
|
5583
5676
|
};
|
|
5584
5677
|
});
|
|
5585
|
-
const verified = store.captureTree(
|
|
5678
|
+
const verified = store.captureTree(legacyLocalRoot);
|
|
5586
5679
|
if (JSON.stringify(verified) !== JSON.stringify(snapshot))
|
|
5587
5680
|
throw new Error("Legacy local:// migration source changed during capture");
|
|
5588
5681
|
return { snapshot, entries };
|
|
5589
5682
|
},
|
|
5590
|
-
retire: snapshot => store.removeTreeExpected(
|
|
5683
|
+
retire: snapshot => store.removeTreeExpected(legacyLocalRoot, snapshot),
|
|
5591
5684
|
};
|
|
5592
5685
|
}
|
|
5593
5686
|
|
|
@@ -7080,6 +7173,12 @@ export class SessionManager {
|
|
|
7080
7173
|
authority instanceof ManagedSessionDescendantStore
|
|
7081
7174
|
? authority.retainAuthority()
|
|
7082
7175
|
: retainManagedDirectoryAuthority(rootAuthority, directory);
|
|
7176
|
+
const policy =
|
|
7177
|
+
authority instanceof ManagedSessionDescendantStore
|
|
7178
|
+
? authority.securityPolicy
|
|
7179
|
+
: process.platform === "win32"
|
|
7180
|
+
? "windows-existing-verify-first"
|
|
7181
|
+
: "default";
|
|
7083
7182
|
const securityContext = createManagedSessionSecurityContext({
|
|
7084
7183
|
agentDir: rootAuthority.canonicalPath,
|
|
7085
7184
|
sessionsRoot: rootAuthority.canonicalPath,
|
|
@@ -7087,6 +7186,7 @@ export class SessionManager {
|
|
|
7087
7186
|
rootAuthority,
|
|
7088
7187
|
retainedAuthority,
|
|
7089
7188
|
});
|
|
7189
|
+
managedSecurityPolicies.set(securityContext, policy);
|
|
7090
7190
|
const destination = Object.freeze({ kind: "managed" as const, directory, securityContext });
|
|
7091
7191
|
trustedSessionDestinations.add(destination);
|
|
7092
7192
|
return destination;
|
|
@@ -7115,20 +7215,59 @@ export class SessionManager {
|
|
|
7115
7215
|
return manager;
|
|
7116
7216
|
}
|
|
7117
7217
|
|
|
7218
|
+
/** Prepare a selected candidate using this manager's captured managed destination authority. */
|
|
7219
|
+
async prepareManagedCandidateForWrite(
|
|
7220
|
+
filePath: string,
|
|
7221
|
+
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7222
|
+
expectedIdentity?: ResumeSessionIdentity,
|
|
7223
|
+
): Promise<string> {
|
|
7224
|
+
return SessionManager.prepareManagedCandidateForWrite(
|
|
7225
|
+
filePath,
|
|
7226
|
+
migrationPolicy,
|
|
7227
|
+
this.destination,
|
|
7228
|
+
expectedIdentity,
|
|
7229
|
+
);
|
|
7230
|
+
}
|
|
7231
|
+
|
|
7232
|
+
/** Prepare a managed candidate and retain its exact post-preparation identity for the next adoption. */
|
|
7233
|
+
async prepareManagedCandidateForStrictAdoption(
|
|
7234
|
+
filePath: string,
|
|
7235
|
+
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7236
|
+
expectedIdentity: ResumeSessionIdentity,
|
|
7237
|
+
): Promise<string> {
|
|
7238
|
+
const preparedPath = await this.prepareManagedCandidateForWrite(filePath, migrationPolicy, expectedIdentity);
|
|
7239
|
+
const inspected = inspectResumeSessionFile(preparedPath, this.storage);
|
|
7240
|
+
if ("kind" in inspected) throw new Error(`Could not inspect prepared managed session: ${inspected.reason}`);
|
|
7241
|
+
this.#pendingStrictAdoption = { canonicalPath: inspected.identity.canonicalPath, identity: inspected.identity };
|
|
7242
|
+
return preparedPath;
|
|
7243
|
+
}
|
|
7244
|
+
|
|
7118
7245
|
/** Resolve a default-managed candidate through binding validation and copy-retain migration before mutation. */
|
|
7119
7246
|
static async prepareManagedCandidateForWrite(
|
|
7120
7247
|
filePath: string,
|
|
7121
7248
|
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7249
|
+
destination: SessionDestination,
|
|
7250
|
+
expectedIdentity?: ResumeSessionIdentity,
|
|
7122
7251
|
): Promise<string> {
|
|
7252
|
+
if (!trustedSessionDestinations.has(destination) || destination.kind !== "managed")
|
|
7253
|
+
throw new Error("Managed candidate preparation requires a trusted managed destination authority");
|
|
7123
7254
|
const storage = new FileSessionStorage();
|
|
7255
|
+
const managedDestinationStore = managedStoreFromContext(destination.securityContext, destination.directory);
|
|
7256
|
+
const assertManagedDestinationBound = (): void => {
|
|
7257
|
+
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
7258
|
+
managedDestinationStore.assertBound();
|
|
7259
|
+
};
|
|
7260
|
+
assertManagedDestinationBound();
|
|
7124
7261
|
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
7125
7262
|
if ("kind" in inspected) {
|
|
7126
7263
|
if (inspected.reason === "missing") return filePath;
|
|
7127
7264
|
throw new Error(`Could not inspect managed session: ${inspected.reason}`);
|
|
7128
7265
|
}
|
|
7266
|
+
if (expectedIdentity && !sameResumeIdentity(expectedIdentity, inspected.identity))
|
|
7267
|
+
throw new Error("Managed session changed before migration authority was adopted.");
|
|
7129
7268
|
const header = inspected.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
7130
7269
|
if (!header?.cwd) return filePath;
|
|
7131
|
-
const sessionsRoot =
|
|
7270
|
+
const sessionsRoot = destination.securityContext.sessionsRoot;
|
|
7132
7271
|
const resolvedPath = path.resolve(filePath);
|
|
7133
7272
|
const relativeToManagedRoot = path.relative(sessionsRoot, resolvedPath);
|
|
7134
7273
|
const isManagedPath =
|
|
@@ -7136,14 +7275,17 @@ export class SessionManager {
|
|
|
7136
7275
|
(!relativeToManagedRoot.startsWith("..") && !path.isAbsolute(relativeToManagedRoot));
|
|
7137
7276
|
|
|
7138
7277
|
if (!isManagedPath) return filePath;
|
|
7278
|
+
assertManagedDestinationBound();
|
|
7139
7279
|
const resolved = resolveManagedScope({
|
|
7140
7280
|
cwd: header.cwd,
|
|
7141
|
-
agentDir:
|
|
7281
|
+
agentDir: destination.securityContext.agentDir,
|
|
7142
7282
|
sessionsRoot,
|
|
7143
7283
|
});
|
|
7144
7284
|
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
7145
7285
|
|
|
7286
|
+
assertManagedDestinationBound();
|
|
7146
7287
|
const listing = listManagedCandidates(resolved.scope);
|
|
7288
|
+
assertManagedDestinationBound();
|
|
7147
7289
|
if (listing.kind !== "complete") throw new Error(`Managed session scan failed: ${listing.message}`);
|
|
7148
7290
|
|
|
7149
7291
|
const candidate = listing.owned.find(item => path.resolve(item.path) === resolvedPath);
|
|
@@ -7160,7 +7302,24 @@ export class SessionManager {
|
|
|
7160
7302
|
candidate.identity.sha256 !== inspected.identity.sha256
|
|
7161
7303
|
)
|
|
7162
7304
|
throw new Error("Managed session changed before migration authority was adopted.");
|
|
7163
|
-
|
|
7305
|
+
assertManagedDestinationBound();
|
|
7306
|
+
const authority: ManagedCandidateWriteAuthority = {
|
|
7307
|
+
rootAuthority: destination.securityContext.rootAuthority,
|
|
7308
|
+
...(path.resolve(destination.directory) === path.resolve(resolved.scope.directoryPath)
|
|
7309
|
+
? {
|
|
7310
|
+
retainedAuthority: destination.securityContext.retainedAuthority,
|
|
7311
|
+
retainedDirectory: destination.directory,
|
|
7312
|
+
}
|
|
7313
|
+
: {}),
|
|
7314
|
+
};
|
|
7315
|
+
const opened = await openManagedCandidateForWrite(
|
|
7316
|
+
resolved.scope,
|
|
7317
|
+
candidate,
|
|
7318
|
+
expectedIdentity ?? inspected.identity,
|
|
7319
|
+
migrationPolicy,
|
|
7320
|
+
authority,
|
|
7321
|
+
);
|
|
7322
|
+
assertManagedDestinationBound();
|
|
7164
7323
|
if (opened.kind === "error") {
|
|
7165
7324
|
if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
7166
7325
|
throw new Error(`Could not open managed session: ${opened.message}`);
|
|
@@ -7182,7 +7341,7 @@ export class SessionManager {
|
|
|
7182
7341
|
const destination = destinationFor(cwd, destinationInput, storage);
|
|
7183
7342
|
const managedSourcePath =
|
|
7184
7343
|
destination.kind === "managed" && storage instanceof FileSessionStorage
|
|
7185
|
-
? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy)
|
|
7344
|
+
? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy, destination)
|
|
7186
7345
|
: sourcePath;
|
|
7187
7346
|
const dir = destination.directory;
|
|
7188
7347
|
const manager = new SessionManager(cwd, dir, true, storage, destination);
|
|
@@ -7246,10 +7405,9 @@ export class SessionManager {
|
|
|
7246
7405
|
return manager;
|
|
7247
7406
|
}
|
|
7248
7407
|
|
|
7249
|
-
const
|
|
7250
|
-
const inspected = inspectResumeSessionFile(managedPath, storage);
|
|
7408
|
+
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
7251
7409
|
if ("kind" in inspected) {
|
|
7252
|
-
if (inspected.reason === "missing"
|
|
7410
|
+
if (inspected.reason === "missing") {
|
|
7253
7411
|
const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
|
|
7254
7412
|
await manager.#initSessionFile(filePath);
|
|
7255
7413
|
return manager;
|
|
@@ -7317,46 +7475,47 @@ export class SessionManager {
|
|
|
7317
7475
|
return manager;
|
|
7318
7476
|
}
|
|
7319
7477
|
/**
|
|
7320
|
-
* List sessions for the resume picker without recovery or other
|
|
7478
|
+
* List default-managed sessions for the resume picker without recovery or other
|
|
7479
|
+
* maintenance writes. This is the only picker inventory that includes legacy
|
|
7480
|
+
* sibling directories.
|
|
7481
|
+
*/
|
|
7482
|
+
static async listManagedForResumePickerReadOnly(
|
|
7483
|
+
cwd: string,
|
|
7484
|
+
managedAgentDir?: string,
|
|
7485
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7486
|
+
): Promise<SessionInfo[]> {
|
|
7487
|
+
if (!(storage instanceof FileSessionStorage)) return [];
|
|
7488
|
+
const sessionsRoot = getSessionsDir(managedAgentDir);
|
|
7489
|
+
const resolved = resolveManagedScope({
|
|
7490
|
+
cwd,
|
|
7491
|
+
agentDir: managedAgentDir ?? path.resolve(sessionsRoot, ".."),
|
|
7492
|
+
sessionsRoot,
|
|
7493
|
+
});
|
|
7494
|
+
if (resolved.kind === "error") return [];
|
|
7495
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
7496
|
+
if (listing.kind === "error") return [];
|
|
7497
|
+
return await collectSessionsFromFiles(
|
|
7498
|
+
listing.owned.map(candidate => candidate.path),
|
|
7499
|
+
storage,
|
|
7500
|
+
);
|
|
7501
|
+
}
|
|
7502
|
+
|
|
7503
|
+
/**
|
|
7504
|
+
* List sessions from an explicitly supplied picker directory without recovery
|
|
7505
|
+
* or other maintenance writes. Unlike managed inventory, this never scans
|
|
7506
|
+
* legacy sibling directories.
|
|
7321
7507
|
*/
|
|
7322
7508
|
static async listForResumePickerReadOnly(
|
|
7323
7509
|
cwd: string,
|
|
7324
7510
|
sessionDir?: string,
|
|
7325
7511
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7326
7512
|
): Promise<SessionInfo[]> {
|
|
7327
|
-
if (!sessionDir
|
|
7328
|
-
if (sessionDir && !resolveManagedSessionRoot(sessionDir, cwd)) {
|
|
7329
|
-
try {
|
|
7330
|
-
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
7331
|
-
} catch {
|
|
7332
|
-
return [];
|
|
7333
|
-
}
|
|
7334
|
-
}
|
|
7335
|
-
const sessionsRoot = sessionDir
|
|
7336
|
-
? (resolveManagedSessionRoot(sessionDir, cwd) ?? getSessionsDir())
|
|
7337
|
-
: getSessionsDir();
|
|
7338
|
-
const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
7339
|
-
if (resolved.kind === "error") return [];
|
|
7340
|
-
if (storage instanceof FileSessionStorage) {
|
|
7341
|
-
const listing = listManagedCandidates(resolved.scope);
|
|
7342
|
-
if (listing.kind === "error") return [];
|
|
7343
|
-
return await collectSessionsFromFiles(
|
|
7344
|
-
listing.owned.map(candidate => candidate.path),
|
|
7345
|
-
storage,
|
|
7346
|
-
);
|
|
7347
|
-
}
|
|
7348
|
-
const legacyDirectory = `--${resolved.scope.canonicalCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
7349
|
-
const files = new Set<string>();
|
|
7513
|
+
if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
|
|
7350
7514
|
try {
|
|
7351
|
-
|
|
7352
|
-
for (const file of storage.listFilesSync(directory, "*.jsonl")) files.add(file);
|
|
7353
|
-
}
|
|
7515
|
+
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
7354
7516
|
} catch {
|
|
7355
7517
|
return [];
|
|
7356
7518
|
}
|
|
7357
|
-
return (await collectSessionsFromFiles([...files], storage)).filter(
|
|
7358
|
-
session => resolveEquivalentPath(session.cwd) === resolved.scope.canonicalCwd,
|
|
7359
|
-
);
|
|
7360
7519
|
}
|
|
7361
7520
|
|
|
7362
7521
|
/** Tombstone and exact-delete a default-managed candidate. */
|
|
@@ -7658,36 +7817,39 @@ export class SessionManager {
|
|
|
7658
7817
|
let sessionPath = identity.canonicalPath;
|
|
7659
7818
|
if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
|
|
7660
7819
|
try {
|
|
7661
|
-
sessionPath = await SessionManager.prepareManagedCandidateForWrite(
|
|
7820
|
+
sessionPath = await SessionManager.prepareManagedCandidateForWrite(
|
|
7821
|
+
sessionPath,
|
|
7822
|
+
migrationPolicy,
|
|
7823
|
+
destination,
|
|
7824
|
+
identity,
|
|
7825
|
+
);
|
|
7662
7826
|
} catch (error) {
|
|
7663
7827
|
if (error instanceof SessionMigrationPolicyError)
|
|
7664
7828
|
return { kind: "error", reason: "legacy_migration_disabled" };
|
|
7829
|
+
if (
|
|
7830
|
+
error instanceof Error &&
|
|
7831
|
+
(error.message.includes("source_changed") || error.message.includes("changed before migration"))
|
|
7832
|
+
)
|
|
7833
|
+
return { kind: "error", reason: "identity-mismatch" };
|
|
7665
7834
|
throw error;
|
|
7666
7835
|
}
|
|
7667
7836
|
}
|
|
7668
7837
|
const inspected = inspectResumeSessionFile(sessionPath, storage);
|
|
7669
7838
|
if ("kind" in inspected) return inspected;
|
|
7670
|
-
if (
|
|
7671
|
-
(sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) ||
|
|
7672
|
-
(sessionPath !== identity.canonicalPath &&
|
|
7673
|
-
(identity.sessionId !== inspected.identity.sessionId || identity.sha256 !== inspected.identity.sha256))
|
|
7674
|
-
) {
|
|
7839
|
+
if (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) {
|
|
7675
7840
|
return { kind: "error", reason: "identity-mismatch" };
|
|
7676
7841
|
}
|
|
7842
|
+
|
|
7677
7843
|
const entries = inspected.entries;
|
|
7678
7844
|
const header = entries[0] as SessionHeader;
|
|
7679
7845
|
const dir = destination.directory;
|
|
7680
7846
|
const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage, destination);
|
|
7681
7847
|
await manager.#hydrateExistingSession(sessionPath, entries, inspected.migrationApplied);
|
|
7682
|
-
const ownershipInspection =
|
|
7683
|
-
if (
|
|
7848
|
+
const ownershipInspection = revalidateResumeSessionIdentity(sessionPath, storage, inspected.identity);
|
|
7849
|
+
if (ownershipInspection.kind === "error") {
|
|
7684
7850
|
await manager.close();
|
|
7685
7851
|
return ownershipInspection;
|
|
7686
7852
|
}
|
|
7687
|
-
if (!sameResumeIdentity(inspected.identity, ownershipInspection.identity)) {
|
|
7688
|
-
await manager.close();
|
|
7689
|
-
return { kind: "error", reason: "identity-mismatch" };
|
|
7690
|
-
}
|
|
7691
7853
|
try {
|
|
7692
7854
|
await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
7693
7855
|
writeTerminalBreadcrumb(manager.cwd, sessionPath);
|
|
@@ -7731,9 +7893,20 @@ export class SessionManager {
|
|
|
7731
7893
|
const managedCandidates =
|
|
7732
7894
|
destination.kind === "explicit"
|
|
7733
7895
|
? []
|
|
7734
|
-
: await SessionManager.
|
|
7896
|
+
: await SessionManager.listManagedForResumePickerReadOnly(
|
|
7897
|
+
cwd,
|
|
7898
|
+
destination.securityContext.agentDir,
|
|
7899
|
+
storage,
|
|
7900
|
+
);
|
|
7901
|
+
|
|
7735
7902
|
const terminalSession = await readTerminalBreadcrumb(cwd);
|
|
7736
|
-
|
|
7903
|
+
const terminalSessionIsInExplicitRoot = (() => {
|
|
7904
|
+
if (destination.kind === "managed" || !terminalSession) return destination.kind === "managed";
|
|
7905
|
+
const canonicalRoot = canonicalizeTrustedPath(destination.directory);
|
|
7906
|
+
const canonicalSessionPath = canonicalizeTrustedPath(terminalSession);
|
|
7907
|
+
return pathIsWithin(canonicalRoot, canonicalSessionPath);
|
|
7908
|
+
})();
|
|
7909
|
+
if (terminalSession && terminalSessionIsInExplicitRoot) {
|
|
7737
7910
|
const opened = await openSelectedStrictly(terminalSession);
|
|
7738
7911
|
if (opened) return opened;
|
|
7739
7912
|
}
|
|
@@ -7771,7 +7944,7 @@ export class SessionManager {
|
|
|
7771
7944
|
sessionDir?: string,
|
|
7772
7945
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7773
7946
|
): Promise<SessionInfo[]> {
|
|
7774
|
-
if (!sessionDir) return await SessionManager.
|
|
7947
|
+
if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
|
|
7775
7948
|
try {
|
|
7776
7949
|
await recoverOrphanedBackups(sessionDir, storage);
|
|
7777
7950
|
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
@@ -7783,9 +7956,12 @@ export class SessionManager {
|
|
|
7783
7956
|
/**
|
|
7784
7957
|
* List all sessions across all project directories.
|
|
7785
7958
|
*/
|
|
7786
|
-
static async listAll(
|
|
7959
|
+
static async listAll(
|
|
7960
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7961
|
+
managedAgentDir?: string,
|
|
7962
|
+
): Promise<SessionInfo[]> {
|
|
7787
7963
|
if (!(storage instanceof FileSessionStorage)) return [];
|
|
7788
|
-
const sessionsRoot =
|
|
7964
|
+
const sessionsRoot = getSessionsDir(managedAgentDir);
|
|
7789
7965
|
try {
|
|
7790
7966
|
const directories = await fs.promises.readdir(sessionsRoot, { withFileTypes: true });
|
|
7791
7967
|
const seedFiles = directories
|
|
@@ -7821,6 +7997,19 @@ export class SessionManager {
|
|
|
7821
7997
|
return [];
|
|
7822
7998
|
}
|
|
7823
7999
|
}
|
|
8000
|
+
/**
|
|
8001
|
+
* Strict inventory bound to this manager's captured session authority. Managed
|
|
8002
|
+
* managers include authorized legacy and current candidates; explicit managers
|
|
8003
|
+
* authorize only their exact directory.
|
|
8004
|
+
*/
|
|
8005
|
+
inventorySessionsStrict(): StrictInventoryResult {
|
|
8006
|
+
return SessionManager.inventorySessionsStrict(this.cwd, {
|
|
8007
|
+
sessionDir: this.destination.kind === "explicit" ? this.destination.directory : undefined,
|
|
8008
|
+
storage: this.storage,
|
|
8009
|
+
destination: this.destination,
|
|
8010
|
+
});
|
|
8011
|
+
}
|
|
8012
|
+
|
|
7824
8013
|
/**
|
|
7825
8014
|
* Strict raw scoped inventory for ACP authorization. Enumerates the scoped
|
|
7826
8015
|
* session directory without suppressing any root/scan/lstat/read/parse/stat/
|
|
@@ -7830,10 +8019,55 @@ export class SessionManager {
|
|
|
7830
8019
|
*/
|
|
7831
8020
|
static inventorySessionsStrict(
|
|
7832
8021
|
cwd: string,
|
|
7833
|
-
options?: { sessionDir?: string; storage?: SessionStorage },
|
|
8022
|
+
options?: { sessionDir?: string; storage?: SessionStorage; destination?: SessionDestination },
|
|
7834
8023
|
): StrictInventoryResult {
|
|
7835
8024
|
const storage = options?.storage ?? new FileSessionStorage();
|
|
7836
|
-
const
|
|
8025
|
+
const destination = options?.destination;
|
|
8026
|
+
const dir =
|
|
8027
|
+
options?.sessionDir ?? destination?.directory ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
8028
|
+
if (destination?.kind === "managed") {
|
|
8029
|
+
if (!trustedSessionDestinations.has(destination) || !(storage instanceof FileSessionStorage)) {
|
|
8030
|
+
return {
|
|
8031
|
+
kind: "failure",
|
|
8032
|
+
failures: [{ kind: "scan", message: "Managed strict inventory authority is unavailable", path: dir }],
|
|
8033
|
+
};
|
|
8034
|
+
}
|
|
8035
|
+
try {
|
|
8036
|
+
const store = managedStoreFromContext(destination.securityContext, destination.directory);
|
|
8037
|
+
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
8038
|
+
store.assertBound();
|
|
8039
|
+
const resolved = resolveManagedScope({
|
|
8040
|
+
cwd,
|
|
8041
|
+
agentDir: destination.securityContext.agentDir,
|
|
8042
|
+
sessionsRoot: destination.securityContext.sessionsRoot,
|
|
8043
|
+
});
|
|
8044
|
+
if (resolved.kind === "error")
|
|
8045
|
+
return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
|
|
8046
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
8047
|
+
if (listing.kind === "error")
|
|
8048
|
+
return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
|
|
8049
|
+
const failures: StrictInventoryFailure[] = [];
|
|
8050
|
+
const candidates: StrictInventoryCandidate[] = [];
|
|
8051
|
+
for (const managedCandidate of listing.owned) {
|
|
8052
|
+
const candidate = inventoryReadCandidate(
|
|
8053
|
+
storage,
|
|
8054
|
+
managedCandidate.path,
|
|
8055
|
+
cwd,
|
|
8056
|
+
path.dirname(managedCandidate.path),
|
|
8057
|
+
);
|
|
8058
|
+
if ("failures" in candidate) failures.push(...candidate.failures);
|
|
8059
|
+
else candidates.push(candidate.candidate);
|
|
8060
|
+
}
|
|
8061
|
+
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
8062
|
+
store.assertBound();
|
|
8063
|
+
return failures.length > 0 ? { kind: "failure", failures } : { kind: "complete", candidates };
|
|
8064
|
+
} catch {
|
|
8065
|
+
return {
|
|
8066
|
+
kind: "failure",
|
|
8067
|
+
failures: [{ kind: "root", message: "Managed strict inventory authority changed", path: dir }],
|
|
8068
|
+
};
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
7837
8071
|
if (!storage.listFilesStrictSync) {
|
|
7838
8072
|
return {
|
|
7839
8073
|
kind: "failure",
|