@gajae-code/coding-agent 0.11.4 → 0.11.5
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 +23 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +48 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/commands/sdk.ts +7 -2
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-runtime.ts +38 -12
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -24
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +321 -0
- package/src/modes/components/transcript-viewer-overlay.ts +39 -17
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +70 -8
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/session-index.ts +122 -64
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1788 -267
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +200 -38
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +129 -32
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/index.ts +4 -0
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
|
@@ -14,13 +14,16 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
acquireManagedLock,
|
|
16
16
|
captureManagedFileNoFollow,
|
|
17
|
+
captureManagedFilePrefixNoFollow,
|
|
17
18
|
copyManagedFileNoReplace,
|
|
18
19
|
ensureManagedDirectory,
|
|
19
20
|
fsyncManagedArtifactTree,
|
|
21
|
+
ManagedSessionDescendantStore,
|
|
20
22
|
type ManagedStorageLock,
|
|
23
|
+
managedDirectoryRoot,
|
|
21
24
|
publishManagedFileNoReplace,
|
|
22
|
-
publishManagedFileNoReplaceSync,
|
|
23
25
|
publishManagedTombstone,
|
|
26
|
+
retainManagedDirectoryAuthority,
|
|
24
27
|
validateManagedArtifactTree,
|
|
25
28
|
} from "./managed-session-storage";
|
|
26
29
|
|
|
@@ -41,6 +44,49 @@ export interface ManagedScope {
|
|
|
41
44
|
platform: "posix" | "win32";
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof managedDirectoryRoot>>();
|
|
48
|
+
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
49
|
+
const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
|
|
50
|
+
|
|
51
|
+
export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
|
|
52
|
+
if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
|
|
53
|
+
return managedDirectoryAuthorities.get(scope);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bigint; ino: bigint } {
|
|
57
|
+
const identity = managedDirectoryIdentities.get(scope);
|
|
58
|
+
if (!identity) throw new Error("Managed session directory identity was not prepared");
|
|
59
|
+
return identity;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function configuredRootPath(scope: ManagedScope): string {
|
|
63
|
+
let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
|
|
64
|
+
for (;;) {
|
|
65
|
+
try {
|
|
66
|
+
const stat = fs.lstatSync(candidate);
|
|
67
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
|
|
68
|
+
return fs.realpathSync.native(candidate);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
71
|
+
const parent = path.dirname(candidate);
|
|
72
|
+
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
73
|
+
candidate = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scopeRoot(scope: ManagedScope) {
|
|
79
|
+
const retained = managedRoots.get(scope);
|
|
80
|
+
if (retained) return retained;
|
|
81
|
+
const root = managedDirectoryRoot(configuredRootPath(scope));
|
|
82
|
+
managedRoots.set(scope, root);
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function managedRootForScope(scope: ManagedScope) {
|
|
87
|
+
return scopeRoot(scope);
|
|
88
|
+
}
|
|
89
|
+
|
|
44
90
|
export type ManagedMigrationPolicy = "copy-retain" | "disabled";
|
|
45
91
|
|
|
46
92
|
export type ManagedScopeErrorCode =
|
|
@@ -228,15 +274,11 @@ function isBinding(value: unknown): value is Binding {
|
|
|
228
274
|
);
|
|
229
275
|
}
|
|
230
276
|
|
|
231
|
-
function
|
|
232
|
-
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
233
|
-
let raw: string;
|
|
277
|
+
function validateBindingRaw(scope: ManagedScope, raw: string): ManagedScopeResolution | undefined {
|
|
234
278
|
let parsed: unknown;
|
|
235
279
|
try {
|
|
236
|
-
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
237
280
|
parsed = JSON.parse(raw);
|
|
238
|
-
} catch
|
|
239
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
281
|
+
} catch {
|
|
240
282
|
return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
|
|
241
283
|
}
|
|
242
284
|
if (!isBinding(parsed))
|
|
@@ -263,6 +305,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
|
|
|
263
305
|
return undefined;
|
|
264
306
|
}
|
|
265
307
|
|
|
308
|
+
function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution | undefined {
|
|
309
|
+
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
310
|
+
let raw: string;
|
|
311
|
+
try {
|
|
312
|
+
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
315
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
|
|
316
|
+
}
|
|
317
|
+
return validateBindingRaw(scope, raw);
|
|
318
|
+
}
|
|
319
|
+
|
|
266
320
|
export function resolveManagedScope(input: {
|
|
267
321
|
cwd: string;
|
|
268
322
|
agentDir: string;
|
|
@@ -270,6 +324,23 @@ export function resolveManagedScope(input: {
|
|
|
270
324
|
}): ManagedScopeResolution {
|
|
271
325
|
const identity = identityFor(input.cwd);
|
|
272
326
|
if (!identity.ok) return nativeFailure(identity.code);
|
|
327
|
+
try {
|
|
328
|
+
if (fs.lstatSync(input.sessionsRoot).isSymbolicLink()) {
|
|
329
|
+
return {
|
|
330
|
+
kind: "error",
|
|
331
|
+
code: "sessions_root_unavailable",
|
|
332
|
+
message: "The sessions root is not a safe directory.",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
337
|
+
return {
|
|
338
|
+
kind: "error",
|
|
339
|
+
code: "sessions_root_unavailable",
|
|
340
|
+
message: "The sessions root could not be inspected.",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
273
344
|
const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
|
|
274
345
|
const agentDir = canonicalizeTrustedPath(input.agentDir);
|
|
275
346
|
const digest = scopeDigest(identity.platform, identity.canonicalPath);
|
|
@@ -390,6 +461,49 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
390
461
|
}
|
|
391
462
|
}
|
|
392
463
|
|
|
464
|
+
type CandidatePreflight =
|
|
465
|
+
| { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
|
|
466
|
+
| {
|
|
467
|
+
kind: "foreign";
|
|
468
|
+
}
|
|
469
|
+
| {
|
|
470
|
+
kind: "invalid";
|
|
471
|
+
code: string;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
function preflightCandidate(filePath: string, scope: ManagedScope): CandidatePreflight {
|
|
475
|
+
try {
|
|
476
|
+
const snapshot = captureManagedFilePrefixNoFollow(filePath, HEADER_MAX_BYTES);
|
|
477
|
+
const lineEnd = snapshot.bytes.indexOf(0x0a);
|
|
478
|
+
if (lineEnd < 0) return { kind: "invalid", code: "invalid_header" };
|
|
479
|
+
const value: unknown = JSON.parse(snapshot.bytes.subarray(0, lineEnd).toString("utf8"));
|
|
480
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
481
|
+
return { kind: "invalid", code: "invalid_header" };
|
|
482
|
+
const header = value as Record<string, unknown>;
|
|
483
|
+
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
|
|
484
|
+
return { kind: "invalid", code: "invalid_header" };
|
|
485
|
+
const candidateIdentity = identityFor(header.cwd);
|
|
486
|
+
if (
|
|
487
|
+
candidateIdentity.ok &&
|
|
488
|
+
(candidateIdentity.platform !== scope.platform || candidateIdentity.canonicalPath !== scope.canonicalCwd)
|
|
489
|
+
)
|
|
490
|
+
return { kind: "foreign" };
|
|
491
|
+
return { kind: "capture", identity: snapshot.identity };
|
|
492
|
+
} catch {
|
|
493
|
+
return { kind: "invalid", code: "unreadable_candidate" };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function matchesPreflightIdentity(candidate: ManagedCandidate, preflight: CandidatePreflight): boolean {
|
|
498
|
+
return (
|
|
499
|
+
preflight.kind === "capture" &&
|
|
500
|
+
candidate.identity.dev === preflight.identity.dev &&
|
|
501
|
+
candidate.identity.ino === preflight.identity.ino &&
|
|
502
|
+
candidate.identity.size === preflight.identity.size &&
|
|
503
|
+
candidate.identity.mtimeNs === preflight.identity.mtimeNs
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
393
507
|
function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): ManagedCandidate | { code: string } {
|
|
394
508
|
try {
|
|
395
509
|
const snapshot = captureManagedFileNoFollow(filePath);
|
|
@@ -400,7 +514,8 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
400
514
|
const header = value as Record<string, unknown>;
|
|
401
515
|
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
|
|
402
516
|
return { code: "invalid_header" };
|
|
403
|
-
|
|
517
|
+
const cwdIdentity = identityFor(header.cwd);
|
|
518
|
+
if (!cwdIdentity.ok) return { code: `cwd_${cwdIdentity.code}` };
|
|
404
519
|
const named = fs.lstatSync(filePath, { bigint: true });
|
|
405
520
|
if (
|
|
406
521
|
!named.isFile() ||
|
|
@@ -450,10 +565,13 @@ function discoveredLegacyDirectoryNames(scope: ManagedScope): readonly string[]
|
|
|
450
565
|
return [...known, ...discovered];
|
|
451
566
|
}
|
|
452
567
|
|
|
568
|
+
type CandidateInspection = ManagedCandidate | { code: string } | { foreign: true };
|
|
569
|
+
|
|
453
570
|
function listDirectoryCandidates(
|
|
454
571
|
directory: string,
|
|
455
572
|
provenance: "v2" | "legacy",
|
|
456
|
-
|
|
573
|
+
scope: ManagedScope,
|
|
574
|
+
): readonly CandidateInspection[] {
|
|
457
575
|
let directoryStat: fs.Stats;
|
|
458
576
|
try {
|
|
459
577
|
directoryStat = fs.lstatSync(directory);
|
|
@@ -465,17 +583,26 @@ function listDirectoryCandidates(
|
|
|
465
583
|
return fs
|
|
466
584
|
.readdirSync(directory, { withFileTypes: true })
|
|
467
585
|
.filter(entry => entry.name.endsWith(".jsonl"))
|
|
468
|
-
.map(entry =>
|
|
586
|
+
.map(entry => {
|
|
587
|
+
const filePath = path.join(directory, entry.name);
|
|
588
|
+
const preflight = preflightCandidate(filePath, scope);
|
|
589
|
+
if (preflight.kind === "invalid") return { code: preflight.code };
|
|
590
|
+
if (preflight.kind === "foreign") return { foreign: true };
|
|
591
|
+
const candidate = inspectCandidate(filePath, provenance);
|
|
592
|
+
if ("code" in candidate) return candidate;
|
|
593
|
+
return matchesPreflightIdentity(candidate, preflight) ? candidate : { code: "source_changed" };
|
|
594
|
+
});
|
|
469
595
|
}
|
|
470
596
|
|
|
471
597
|
export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedScopeResolution> {
|
|
472
598
|
try {
|
|
473
|
-
|
|
474
|
-
ensureManagedDirectory(scope.
|
|
599
|
+
const root = scopeRoot(scope);
|
|
600
|
+
ensureManagedDirectory(scope.sessionsRoot, root);
|
|
601
|
+
ensureManagedDirectory(scope.directoryPath, root);
|
|
475
602
|
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
476
603
|
const binding = `${JSON.stringify(bindingFor(scope))}\n`;
|
|
477
604
|
try {
|
|
478
|
-
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding));
|
|
605
|
+
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root);
|
|
479
606
|
} catch (error) {
|
|
480
607
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
481
608
|
}
|
|
@@ -492,24 +619,46 @@ export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedSc
|
|
|
492
619
|
/** Synchronously create and validate the v2 binding before a default session writer exists. */
|
|
493
620
|
export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): ManagedScopeResolution {
|
|
494
621
|
try {
|
|
495
|
-
|
|
496
|
-
ensureManagedDirectory(scope.
|
|
497
|
-
|
|
622
|
+
const root = scopeRoot(scope);
|
|
623
|
+
ensureManagedDirectory(scope.sessionsRoot, root);
|
|
624
|
+
ensureManagedDirectory(scope.directoryPath, root);
|
|
625
|
+
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
626
|
+
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
627
|
+
throw new Error("Managed session directory changed");
|
|
628
|
+
const retainedAuthority = retainManagedDirectoryAuthority(root, scope.directoryPath, {
|
|
629
|
+
dev: preparedDirectory.dev,
|
|
630
|
+
ino: preparedDirectory.ino,
|
|
631
|
+
});
|
|
632
|
+
const store = new ManagedSessionDescendantStore(
|
|
633
|
+
root,
|
|
634
|
+
scope.directoryPath,
|
|
635
|
+
retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
|
|
636
|
+
);
|
|
637
|
+
const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
|
|
498
638
|
try {
|
|
499
|
-
|
|
500
|
-
bindingPath,
|
|
501
|
-
new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`),
|
|
502
|
-
);
|
|
639
|
+
store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
|
|
503
640
|
} catch (error) {
|
|
504
641
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
505
642
|
}
|
|
506
|
-
const
|
|
643
|
+
const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
644
|
+
if (!capturedBinding) throw new Error("Managed scope binding is unavailable");
|
|
645
|
+
const validated = validateBindingRaw(scope, capturedBinding.bytes.toString("utf8"));
|
|
646
|
+
managedDirectoryAuthorities.set(scope, retainedAuthority);
|
|
647
|
+
const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
648
|
+
if (
|
|
649
|
+
!directoryStat.isDirectory() ||
|
|
650
|
+
directoryStat.isSymbolicLink() ||
|
|
651
|
+
directoryStat.dev !== preparedDirectory.dev ||
|
|
652
|
+
directoryStat.ino !== preparedDirectory.ino
|
|
653
|
+
)
|
|
654
|
+
throw new Error("Managed session directory changed");
|
|
655
|
+
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
507
656
|
if (validated) return validated;
|
|
508
657
|
const internal = managedInternalDirectory(scope);
|
|
509
|
-
ensureManagedDirectory(internal);
|
|
510
|
-
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY));
|
|
511
|
-
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY));
|
|
512
|
-
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY));
|
|
658
|
+
ensureManagedDirectory(internal, root);
|
|
659
|
+
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
|
|
660
|
+
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
|
|
661
|
+
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
|
|
513
662
|
return { kind: "resolved", scope };
|
|
514
663
|
} catch (error) {
|
|
515
664
|
return {
|
|
@@ -544,14 +693,18 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
544
693
|
];
|
|
545
694
|
const seen = new Set<string>();
|
|
546
695
|
for (const directory of directories) {
|
|
547
|
-
for (const candidate of listDirectoryCandidates(directory.path, directory.provenance)) {
|
|
696
|
+
for (const candidate of listDirectoryCandidates(directory.path, directory.provenance, scope)) {
|
|
548
697
|
if ("code" in candidate) {
|
|
549
698
|
invalid.push({ code: candidate.code });
|
|
550
699
|
continue;
|
|
551
700
|
}
|
|
701
|
+
if ("foreign" in candidate) {
|
|
702
|
+
foreignCount++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
552
705
|
const candidateIdentity = identityFor(candidate.cwd);
|
|
553
706
|
if (!candidateIdentity.ok) {
|
|
554
|
-
invalid.push({ code:
|
|
707
|
+
invalid.push({ code: `cwd_${candidateIdentity.code}` });
|
|
555
708
|
continue;
|
|
556
709
|
}
|
|
557
710
|
if (
|
|
@@ -1088,6 +1241,7 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1088
1241
|
typeof item.ino === "string" &&
|
|
1089
1242
|
typeof item.size === "string" &&
|
|
1090
1243
|
typeof item.mtimeNs === "string" &&
|
|
1244
|
+
typeof item.ctimeNs === "string" &&
|
|
1091
1245
|
(item.sha256 === undefined || typeof item.sha256 === "string")
|
|
1092
1246
|
);
|
|
1093
1247
|
})
|
|
@@ -1626,12 +1780,14 @@ function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
|
|
|
1626
1780
|
}
|
|
1627
1781
|
|
|
1628
1782
|
async function copyArtifacts(
|
|
1783
|
+
scope: ManagedScope,
|
|
1629
1784
|
sourceTranscript: string,
|
|
1630
1785
|
destinationTranscript: string,
|
|
1631
1786
|
manifest: readonly ArtifactManifestEntry[],
|
|
1632
1787
|
lock: ManagedStorageLock,
|
|
1633
1788
|
sourceRootOverride?: string,
|
|
1634
1789
|
): Promise<void> {
|
|
1790
|
+
const root = scopeRoot(scope);
|
|
1635
1791
|
if (manifest.length === 0) return;
|
|
1636
1792
|
const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
|
|
1637
1793
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
|
|
@@ -1641,8 +1797,8 @@ async function copyArtifacts(
|
|
|
1641
1797
|
const source = path.join(sourceRoot, entry.path);
|
|
1642
1798
|
const destination = path.join(destinationRoot, entry.path);
|
|
1643
1799
|
if (entry.kind === "directory") {
|
|
1644
|
-
if (entry.path === "") ensureManagedDirectory(destinationRoot);
|
|
1645
|
-
else ensureManagedDirectory(destination);
|
|
1800
|
+
if (entry.path === "") ensureManagedDirectory(destinationRoot, root);
|
|
1801
|
+
else ensureManagedDirectory(destination, root);
|
|
1646
1802
|
continue;
|
|
1647
1803
|
}
|
|
1648
1804
|
const snapshot = captureManagedFileNoFollow(source);
|
|
@@ -1652,7 +1808,7 @@ async function copyArtifacts(
|
|
|
1652
1808
|
)
|
|
1653
1809
|
throw new Error("source_changed");
|
|
1654
1810
|
try {
|
|
1655
|
-
await copyManagedFileNoReplace(source, destination, snapshot);
|
|
1811
|
+
await copyManagedFileNoReplace(source, destination, snapshot, root);
|
|
1656
1812
|
} catch (error) {
|
|
1657
1813
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
1658
1814
|
}
|
|
@@ -1769,6 +1925,7 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
1769
1925
|
lock = await acquireManagedLock(
|
|
1770
1926
|
path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
|
|
1771
1927
|
path.basename(tombstone, ".json"),
|
|
1928
|
+
scopeRoot(scope),
|
|
1772
1929
|
);
|
|
1773
1930
|
const lockedTargets = retiredTargets(scope, tombstone);
|
|
1774
1931
|
if (!lockedTargets) continue;
|
|
@@ -1880,10 +2037,11 @@ export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): P
|
|
|
1880
2037
|
if (prepared.kind === "error") return prepared;
|
|
1881
2038
|
try {
|
|
1882
2039
|
const internal = managedInternalDirectory(scope);
|
|
1883
|
-
|
|
1884
|
-
ensureManagedDirectory(
|
|
1885
|
-
ensureManagedDirectory(path.join(internal,
|
|
1886
|
-
ensureManagedDirectory(path.join(internal,
|
|
2040
|
+
const root = scopeRoot(scope);
|
|
2041
|
+
ensureManagedDirectory(internal, root);
|
|
2042
|
+
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
|
|
2043
|
+
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
|
|
2044
|
+
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
|
|
1887
2045
|
await reconcileManagedTombstones(scope);
|
|
1888
2046
|
return { kind: "resolved", scope };
|
|
1889
2047
|
} catch (error) {
|
|
@@ -1934,7 +2092,7 @@ export async function openManagedCandidateForWrite(
|
|
|
1934
2092
|
let lock: ManagedStorageLock | undefined;
|
|
1935
2093
|
let detachedArtifacts: DetachedArtifactRoot | undefined;
|
|
1936
2094
|
try {
|
|
1937
|
-
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation);
|
|
2095
|
+
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
|
|
1938
2096
|
const afterLock = validateCandidateForScope(scope, current);
|
|
1939
2097
|
if (!afterLock)
|
|
1940
2098
|
return { kind: "error", code: "source_changed", message: "The legacy candidate changed during migration." };
|
|
@@ -2000,11 +2158,11 @@ export async function openManagedCandidateForWrite(
|
|
|
2000
2158
|
if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
|
|
2001
2159
|
detachedArtifacts = artifactPlan ? detachArtifactRootForMigration(artifactPlan) : undefined;
|
|
2002
2160
|
manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
|
|
2003
|
-
await copyArtifacts(afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
|
|
2161
|
+
await copyArtifacts(scope, afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
|
|
2004
2162
|
if (!existing) {
|
|
2005
2163
|
try {
|
|
2006
2164
|
lock.assertOwned();
|
|
2007
|
-
await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot);
|
|
2165
|
+
await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot, scopeRoot(scope));
|
|
2008
2166
|
} catch (error) {
|
|
2009
2167
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
2010
2168
|
}
|
|
@@ -2101,7 +2259,11 @@ export async function deleteManagedSessionCandidate(
|
|
|
2101
2259
|
const operation = path.basename(tombstone, ".json");
|
|
2102
2260
|
let lock: ManagedStorageLock | undefined;
|
|
2103
2261
|
try {
|
|
2104
|
-
lock = await acquireManagedLock(
|
|
2262
|
+
lock = await acquireManagedLock(
|
|
2263
|
+
path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
|
|
2264
|
+
operation,
|
|
2265
|
+
scopeRoot(scope),
|
|
2266
|
+
);
|
|
2105
2267
|
let targets = retiredTargets(scope, tombstone);
|
|
2106
2268
|
if (!targets) {
|
|
2107
2269
|
if (!current) throw new Error("source_changed");
|