@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/commands/daemon.d.ts +2 -2
  5. package/dist/types/commands/sdk.d.ts +20 -2
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +56 -6
  8. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
  10. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
  12. package/dist/types/main.d.ts +24 -1
  13. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  14. package/dist/types/modes/components/model-selector.d.ts +2 -0
  15. package/dist/types/modes/components/tool-status-header.d.ts +1 -0
  16. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  21. package/dist/types/sdk/bus/index.d.ts +1 -1
  22. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  23. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  27. package/dist/types/sdk/client/discovery.d.ts +2 -0
  28. package/dist/types/sdk/client/liveness.d.ts +7 -0
  29. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  30. package/dist/types/sdk/transport/index.d.ts +12 -0
  31. package/dist/types/sdk/transport/relay.d.ts +43 -0
  32. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  33. package/dist/types/sdk/transport/socket.d.ts +7 -0
  34. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  35. package/dist/types/session/agent-session.d.ts +28 -5
  36. package/dist/types/session/btw-contract.d.ts +16 -0
  37. package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
  38. package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
  39. package/dist/types/session/session-manager.d.ts +25 -4
  40. package/dist/types/setup/credential-import.d.ts +5 -1
  41. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  42. package/dist/types/ssh/utils.d.ts +1 -0
  43. package/dist/types/task/discovery.d.ts +3 -1
  44. package/dist/types/tools/fetch.d.ts +5 -8
  45. package/dist/types/tools/image-gen.d.ts +16 -0
  46. package/dist/types/tools/skill-discovery.d.ts +7 -0
  47. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  48. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  49. package/dist/types/web/insane/bridge.d.ts +1 -0
  50. package/dist/types/web/insane/url-guard.d.ts +17 -0
  51. package/package.json +8 -8
  52. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  53. package/src/capability/index.ts +50 -42
  54. package/src/capability/types.ts +3 -0
  55. package/src/cli/setup-cli.ts +10 -4
  56. package/src/cli.ts +34 -9
  57. package/src/commands/daemon.ts +22 -5
  58. package/src/commands/sdk.ts +42 -9
  59. package/src/config/keybindings.ts +1 -1
  60. package/src/config/model-profiles.ts +18 -0
  61. package/src/config/settings-schema.ts +57 -6
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  63. package/src/discovery/ssh.ts +12 -1
  64. package/src/eval/py/kernel.ts +1 -21
  65. package/src/eval/py/runner-artifact.ts +84 -0
  66. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  67. package/src/gjc-runtime/ultragoal-guard.ts +88 -10
  68. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  69. package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
  70. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  71. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  72. package/src/internal-urls/docs-index.generated.ts +8 -7
  73. package/src/internal-urls/local-protocol.ts +32 -6
  74. package/src/main.ts +86 -27
  75. package/src/modes/action-registry.ts +1 -1
  76. package/src/modes/components/btw-panel.ts +82 -16
  77. package/src/modes/components/model-selector.ts +31 -3
  78. package/src/modes/components/tool-status-header.ts +6 -1
  79. package/src/modes/controllers/btw-controller.ts +119 -36
  80. package/src/modes/controllers/input-controller.ts +31 -0
  81. package/src/modes/controllers/selector-controller.ts +106 -12
  82. package/src/modes/interactive-mode.ts +16 -7
  83. package/src/modes/types.ts +1 -0
  84. package/src/prompts/system/btw-user.md +3 -8
  85. package/src/prompts/tools/skill-discovery.md +1 -0
  86. package/src/runtime-mcp/client.ts +41 -55
  87. package/src/runtime-mcp/content-limits.ts +71 -0
  88. package/src/runtime-mcp/json-rpc.ts +23 -9
  89. package/src/runtime-mcp/transports/http.ts +50 -16
  90. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  91. package/src/sdk/bus/index.ts +4 -5
  92. package/src/sdk/bus/lifecycle-commands.ts +16 -7
  93. package/src/sdk/bus/notification-service.ts +41 -18
  94. package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
  95. package/src/sdk/bus/telegram-daemon-control.ts +2 -2
  96. package/src/sdk/bus/telegram-daemon.ts +1663 -375
  97. package/src/sdk/bus/topic-registry.ts +451 -12
  98. package/src/sdk/client/discovery.ts +19 -3
  99. package/src/sdk/client/liveness.ts +49 -0
  100. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  101. package/src/sdk/session.ts +29 -5
  102. package/src/sdk/transport/auth-preface.ts +82 -0
  103. package/src/sdk/transport/index.ts +18 -0
  104. package/src/sdk/transport/relay.ts +252 -0
  105. package/src/sdk/transport/serve-cli.ts +105 -0
  106. package/src/sdk/transport/socket.ts +197 -0
  107. package/src/sdk/transport/stdio.ts +17 -0
  108. package/src/session/agent-session.ts +240 -149
  109. package/src/session/blob-store.ts +19 -6
  110. package/src/session/btw-contract.ts +59 -0
  111. package/src/session/internal/managed-session-scope.ts +313 -57
  112. package/src/session/internal/managed-session-storage.ts +146 -35
  113. package/src/session/session-manager.ts +319 -85
  114. package/src/setup/credential-import.ts +28 -3
  115. package/src/setup/provider-onboarding.ts +30 -1
  116. package/src/setup/provider-presets.json +2 -1
  117. package/src/slash-commands/builtin-registry.ts +1 -1
  118. package/src/ssh/utils.ts +14 -0
  119. package/src/task/discovery.ts +8 -2
  120. package/src/tools/ask.ts +1 -1
  121. package/src/tools/fetch.ts +13 -131
  122. package/src/tools/image-gen.ts +301 -20
  123. package/src/tools/read.ts +2 -0
  124. package/src/tools/skill-discovery.ts +39 -3
  125. package/src/tools/sqlite-reader.ts +63 -5
  126. package/src/utils/pasted-image-path.ts +15 -6
  127. package/src/utils/shell-snapshot.ts +173 -63
  128. package/src/web/insane/bridge.ts +5 -1
  129. package/src/web/insane/url-guard.ts +138 -27
  130. package/src/web/scrapers/types.ts +19 -49
  131. package/src/web/scrapers/utils.ts +12 -26
@@ -3,7 +3,11 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import * as native from "@gajae-code/natives";
6
- import { canonicalExistingDirectoryIdentity, verifyOwnerOnlyPathSecurity } from "@gajae-code/natives";
6
+ import {
7
+ canonicalExistingDirectoryIdentity,
8
+ verifyOwnerOnlyPathSecurity,
9
+ verifyOwnerOnlyPathSecurityExpected,
10
+ } from "@gajae-code/natives";
7
11
  import { pathIsWithin } from "@gajae-code/utils";
8
12
  import type { ResumeSessionIdentity } from "../session-manager";
9
13
  import {
@@ -13,18 +17,22 @@ import {
13
17
  } from "../session-storage";
14
18
  import {
15
19
  acquireManagedLock,
20
+ assertManagedDirectoryRoot,
16
21
  captureManagedFileNoFollow,
17
22
  captureManagedFilePrefixNoFollow,
18
23
  copyManagedFileNoReplace,
19
24
  ensureManagedDirectory,
20
25
  fsyncManagedArtifactTree,
26
+ type ManagedDirectoryRoot,
21
27
  ManagedSessionDescendantStore,
28
+ type ManagedSessionSecurityPolicy,
22
29
  type ManagedStorageLock,
23
30
  managedDirectoryRoot,
24
31
  publishManagedFileNoReplace,
25
32
  publishManagedTombstone,
26
33
  retainManagedDirectoryAuthority,
27
34
  validateManagedArtifactTree,
35
+ validateNativeSecurityResult,
28
36
  } from "./managed-session-storage";
29
37
 
30
38
  export const MANAGED_SESSION_LAYOUT_VERSION = 2 as const;
@@ -44,9 +52,37 @@ export interface ManagedScope {
44
52
  platform: "posix" | "win32";
45
53
  }
46
54
 
55
+ /**
56
+ * Opaque managed writer authority captured by a trusted destination. The open
57
+ * transaction must use this authority rather than reacquiring its root from a
58
+ * pathname after resume inspection.
59
+ */
60
+ export interface ManagedCandidateWriteAuthority {
61
+ readonly rootAuthority: ManagedDirectoryRoot;
62
+ readonly retainedAuthority?: native.RecoveryFsRoot;
63
+ readonly retainedDirectory?: string;
64
+ }
65
+
47
66
  const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof managedDirectoryRoot>>();
48
67
  const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
49
68
  const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
69
+ const boundManagedWriteAuthorities = new WeakMap<ManagedScope, ManagedCandidateWriteAuthority>();
70
+
71
+ function bindManagedWriteAuthority(scope: ManagedScope, authority: ManagedCandidateWriteAuthority): void {
72
+ assertManagedDirectoryRoot(authority.rootAuthority);
73
+ if (
74
+ authority.retainedAuthority &&
75
+ authority.retainedDirectory !== undefined &&
76
+ path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
77
+ ) {
78
+ new ManagedSessionDescendantStore(authority.rootAuthority, scope.directoryPath, {
79
+ authority: authority.retainedAuthority,
80
+ authorityBaseDir: scope.directoryPath,
81
+ }).assertBound();
82
+ }
83
+ managedRoots.set(scope, authority.rootAuthority);
84
+ boundManagedWriteAuthorities.set(scope, authority);
85
+ }
50
86
 
51
87
  export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
52
88
  if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
@@ -76,6 +112,11 @@ function configuredRootPath(scope: ManagedScope): string {
76
112
  }
77
113
 
78
114
  function scopeRoot(scope: ManagedScope) {
115
+ const bound = boundManagedWriteAuthorities.get(scope);
116
+ if (bound) {
117
+ bindManagedWriteAuthority(scope, bound);
118
+ return bound.rootAuthority;
119
+ }
79
120
  const retained = managedRoots.get(scope);
80
121
  if (retained) return retained;
81
122
  const root = managedDirectoryRoot(configuredRootPath(scope));
@@ -155,8 +196,6 @@ type NativeIdentityFailureCode =
155
196
  | "identity_unavailable"
156
197
  | "io_error";
157
198
 
158
- type NativeSecurity = { ok: true } | { ok: false; code: string };
159
-
160
199
  interface Binding {
161
200
  schemaVersion: 1;
162
201
  layoutVersion: 2;
@@ -196,6 +235,22 @@ function identityFor(cwd: string): NativeIdentity {
196
235
  return canonicalExistingDirectoryIdentity(cwd) as NativeIdentity;
197
236
  }
198
237
 
238
+ function verifyExistingManagedScopeDirectory(pathname: string) {
239
+ if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
240
+ const expected = fs.lstatSync(pathname, { bigint: true });
241
+ if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("Unsafe managed directory");
242
+ const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
243
+ const current = fs.lstatSync(pathname, { bigint: true });
244
+ if (
245
+ !current.isDirectory() ||
246
+ current.isSymbolicLink() ||
247
+ current.dev !== expected.dev ||
248
+ current.ino !== expected.ino
249
+ )
250
+ throw new Error("Managed session directory changed");
251
+ return verified;
252
+ }
253
+
199
254
  function canonicalExistingPathForIo(base: string, identity: CanonicalNativeIdentity): string {
200
255
  if (identity.platform !== "win32") return identity.canonicalPath;
201
256
  try {
@@ -317,11 +372,16 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
317
372
  return validateBindingRaw(scope, raw);
318
373
  }
319
374
 
320
- export function resolveManagedScope(input: {
375
+ interface ManagedScopeInput {
321
376
  cwd: string;
322
377
  agentDir: string;
323
378
  sessionsRoot: string;
324
- }): ManagedScopeResolution {
379
+ }
380
+
381
+ function resolveManagedScopeInternal(
382
+ input: ManagedScopeInput,
383
+ allowRepairableAclFailure: boolean,
384
+ ): ManagedScopeResolution {
325
385
  const identity = identityFor(input.cwd);
326
386
  if (!identity.ok) return nativeFailure(identity.code);
327
387
  try {
@@ -379,13 +439,18 @@ export function resolveManagedScope(input: {
379
439
  if (!directory.isDirectory() || directory.isSymbolicLink()) {
380
440
  return { kind: "error", code: "binding_invalid", message: "The managed scope path is not a safe directory." };
381
441
  }
382
- const security = verifyOwnerOnlyPathSecurity(scope.directoryPath, "directory") as NativeSecurity;
383
- if (!security.ok)
442
+ const security = validateNativeSecurityResult(
443
+ verifyExistingManagedScopeDirectory(scope.directoryPath),
444
+ "verify",
445
+ "directory",
446
+ );
447
+ if (!security.ok && (!allowRepairableAclFailure || security.code !== "acl_verify_failed")) {
384
448
  return {
385
449
  kind: "error",
386
450
  code: "binding_invalid",
387
451
  message: "The managed scope security could not be verified.",
388
452
  };
453
+ }
389
454
  } catch (error) {
390
455
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
391
456
  return { kind: "error", code: "binding_invalid", message: "The managed scope path could not be inspected." };
@@ -394,6 +459,15 @@ export function resolveManagedScope(input: {
394
459
  return validateExistingBinding(scope) ?? { kind: "resolved", scope };
395
460
  }
396
461
 
462
+ export function resolveManagedScope(input: ManagedScopeInput): ManagedScopeResolution {
463
+ return resolveManagedScopeInternal(input, false);
464
+ }
465
+
466
+ /** Resolve a scope for a synchronous write without mutating an existing ACL mismatch. */
467
+ export function resolveManagedScopeForWrite(input: ManagedScopeInput): ManagedScopeResolution {
468
+ return resolveManagedScopeInternal(input, true);
469
+ }
470
+
397
471
  function legacyDirectoryNames(
398
472
  platform: ManagedScope["platform"],
399
473
  canonicalCwd: string,
@@ -594,15 +668,18 @@ function listDirectoryCandidates(
594
668
  });
595
669
  }
596
670
 
597
- export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedScopeResolution> {
671
+ export async function ensureManagedScope(
672
+ scope: ManagedScope,
673
+ policy: ManagedSessionSecurityPolicy = "default",
674
+ ): Promise<ManagedScopeResolution> {
598
675
  try {
599
676
  const root = scopeRoot(scope);
600
- ensureManagedDirectory(scope.sessionsRoot, root);
601
- ensureManagedDirectory(scope.directoryPath, root);
677
+ ensureManagedDirectory(scope.sessionsRoot, root, policy);
678
+ ensureManagedDirectory(scope.directoryPath, root, policy);
602
679
  const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
603
680
  const binding = `${JSON.stringify(bindingFor(scope))}\n`;
604
681
  try {
605
- await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root);
682
+ await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
606
683
  } catch (error) {
607
684
  if ((error as Error).message !== "destination_conflict") throw error;
608
685
  }
@@ -616,24 +693,107 @@ export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedSc
616
693
  }
617
694
  }
618
695
 
696
+ /**
697
+ * Re-apply owner-only security to every descendant of a managed scope directory.
698
+ *
699
+ * A managed scope can accumulate group/other-readable descendants when a
700
+ * different code path writes into it without the secured managed-storage
701
+ * helpers — notably the resident-cache `EphemeralBlobStore` created on the
702
+ * explicit session path. The managed-tree snapshot fails closed on the first
703
+ * such descendant (`mode_mismatch`), which would otherwise abort launch with an
704
+ * uncaught exception. Re-securing the tree in place lets a drifted scope
705
+ * recover on the next launch instead of trapping the user behind a fatal error.
706
+ */
707
+ function reapplyOwnerOnlyManagedTree(directory: string): void {
708
+ let entries: fs.Dirent[];
709
+ try {
710
+ entries = fs.readdirSync(directory, { withFileTypes: true });
711
+ } catch {
712
+ return;
713
+ }
714
+ for (const entry of entries) {
715
+ const child = path.join(directory, entry.name);
716
+ let stat: fs.Stats;
717
+ try {
718
+ stat = fs.lstatSync(child);
719
+ } catch {
720
+ continue;
721
+ }
722
+ if (stat.isSymbolicLink()) continue;
723
+ if (stat.isDirectory()) {
724
+ reapplyOwnerOnlyManagedTree(child);
725
+ try {
726
+ native.applyOwnerOnlyPathSecurity(child, "directory");
727
+ } catch {
728
+ // Best-effort: the managed-tree snapshot re-verifies and reports genuine failures.
729
+ }
730
+ } else if (stat.isFile()) {
731
+ try {
732
+ native.applyOwnerOnlyPathSecurity(child, "file");
733
+ } catch {
734
+ // Best-effort, as above.
735
+ }
736
+ }
737
+ }
738
+ try {
739
+ native.applyOwnerOnlyPathSecurity(directory, "directory");
740
+ } catch {
741
+ // Best-effort, as above.
742
+ }
743
+ }
744
+
745
+ /**
746
+ * True when a managed setup error reflects a fixable owner-only *mode* drift
747
+ * (group/other permission bits) rather than an ownership or identity change.
748
+ * Only mode drift can be self-healed by re-applying owner-only permissions.
749
+ */
750
+ function isRecoverableOwnerOnlyModeDrift(error: unknown): boolean {
751
+ const message = error instanceof Error ? error.message : "";
752
+ return message === "mode_mismatch" || message.endsWith(": mode_mismatch");
753
+ }
754
+
619
755
  /** Synchronously create and validate the v2 binding before a default session writer exists. */
620
- export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): ManagedScopeResolution {
756
+ export function prepareManagedSessionScopeForWriteSync(
757
+ scope: ManagedScope,
758
+ policy: ManagedSessionSecurityPolicy = "default",
759
+ authority?: ManagedCandidateWriteAuthority,
760
+ ): ManagedScopeResolution {
621
761
  try {
622
- const root = scopeRoot(scope);
623
- ensureManagedDirectory(scope.sessionsRoot, root);
624
- ensureManagedDirectory(scope.directoryPath, root);
762
+ const root = authority?.rootAuthority ?? scopeRoot(scope);
763
+ if (authority) bindManagedWriteAuthority(scope, authority);
764
+ ensureManagedDirectory(scope.sessionsRoot, root, policy);
765
+ ensureManagedDirectory(scope.directoryPath, root, policy);
625
766
  const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
626
767
  if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
627
768
  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
- );
769
+ const retainedAuthority =
770
+ authority?.retainedAuthority &&
771
+ authority.retainedDirectory !== undefined &&
772
+ path.resolve(authority.retainedDirectory) === path.resolve(scope.directoryPath)
773
+ ? authority.retainedAuthority
774
+ : retainManagedDirectoryAuthority(root, scope.directoryPath, {
775
+ dev: preparedDirectory.dev,
776
+ ino: preparedDirectory.ino,
777
+ });
778
+ const buildStore = () =>
779
+ new ManagedSessionDescendantStore(
780
+ root,
781
+ scope.directoryPath,
782
+ retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
783
+ policy,
784
+ );
785
+ let store: ManagedSessionDescendantStore;
786
+ try {
787
+ store = buildStore();
788
+ } catch (error) {
789
+ if (process.platform === "win32" && policy === "windows-existing-verify-first") throw error;
790
+ if (!isRecoverableOwnerOnlyModeDrift(error)) throw error;
791
+ // A prior writer left group/other-readable descendants under the scope
792
+ // (e.g. resident-cache blobs written on the explicit session path).
793
+ // Re-secure the tree in place and retry once before failing closed.
794
+ reapplyOwnerOnlyManagedTree(scope.directoryPath);
795
+ store = buildStore();
796
+ }
637
797
  const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
638
798
  try {
639
799
  store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
@@ -655,10 +815,10 @@ export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): Man
655
815
  managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
656
816
  if (validated) return validated;
657
817
  const internal = managedInternalDirectory(scope);
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);
818
+ ensureManagedDirectory(internal, root, policy);
819
+ ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
820
+ ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
821
+ ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
662
822
  return { kind: "resolved", scope };
663
823
  } catch (error) {
664
824
  return {
@@ -804,6 +964,29 @@ function sameCandidate(left: ManagedCandidate, right: ManagedCandidate): boolean
804
964
  );
805
965
  }
806
966
 
967
+ function matchesExpectedResumeIdentity(candidate: ManagedCandidate, expected: ResumeSessionIdentity): boolean {
968
+ return (
969
+ path.resolve(candidate.identity.canonicalPath) === path.resolve(expected.canonicalPath) &&
970
+ candidate.identity.sessionId === expected.sessionId &&
971
+ candidate.identity.dev === expected.dev &&
972
+ candidate.identity.ino === expected.ino &&
973
+ candidate.identity.size === expected.size &&
974
+ candidate.identity.mtimeMs === expected.mtimeMs &&
975
+ candidate.identity.mtimeNs === expected.mtimeNs &&
976
+ candidate.identity.sha256 === expected.sha256
977
+ );
978
+ }
979
+
980
+ function revalidatePickerConsent(
981
+ scope: ManagedScope,
982
+ candidate: ManagedCandidate,
983
+ expectedIdentity: ResumeSessionIdentity,
984
+ ): ManagedCandidate {
985
+ const current = validateCandidateForScope(scope, candidate);
986
+ if (!current || !matchesExpectedResumeIdentity(current, expectedIdentity)) throw new Error("source_changed");
987
+ return current;
988
+ }
989
+
807
990
  function receiptPathFor(
808
991
  scope: ManagedScope,
809
992
  source: ManagedCandidate,
@@ -1653,11 +1836,19 @@ function planArtifactRootForMigration(sourceTranscript: string, operation: strin
1653
1836
  throw error;
1654
1837
  }
1655
1838
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
1839
+ const tree = snapshotArtifactTree(originalPath);
1840
+ const root = tree.entries.find(entry => entry.relativePath === "" && entry.kind === "directory");
1841
+ if (!root) throw new Error("unsafe_artifacts");
1656
1842
  return {
1657
1843
  originalPath,
1658
1844
  detachedPath: path.join(path.dirname(originalPath), `.gjc-migrate-${operation}-artifacts`),
1659
- identity: { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeNs: stat.mtimeNs },
1660
- tree: snapshotArtifactTree(originalPath),
1845
+ identity: {
1846
+ dev: stat.dev,
1847
+ ino: stat.ino,
1848
+ size: process.platform === "win32" ? BigInt(root.size) : stat.size,
1849
+ mtimeNs: stat.mtimeNs,
1850
+ },
1851
+ tree,
1661
1852
  };
1662
1853
  }
1663
1854
 
@@ -1704,7 +1895,10 @@ function detachArtifactRootForMigration(plan: DetachedArtifactRoot): DetachedArt
1704
1895
  });
1705
1896
  if (!result.ok || !result.detachedPath || !matchesDetachedArtifactRoot(result.detachedPath, plan))
1706
1897
  throw new Error("durability_failed");
1707
- return { ...plan, detachedPath: result.detachedPath };
1898
+ const detachedPath =
1899
+ process.platform === "win32" ? fs.realpathSync.native(result.detachedPath) : result.detachedPath;
1900
+ if (!matchesDetachedArtifactRoot(detachedPath, plan)) throw new Error("durability_failed");
1901
+ return { ...plan, detachedPath };
1708
1902
  }
1709
1903
 
1710
1904
  function restorePreparedArtifactRoot(scope: ManagedScope, source: ManagedCandidate): void {
@@ -1785,6 +1979,8 @@ async function copyArtifacts(
1785
1979
  destinationTranscript: string,
1786
1980
  manifest: readonly ArtifactManifestEntry[],
1787
1981
  lock: ManagedStorageLock,
1982
+ expectedCandidate: ManagedCandidate,
1983
+ expectedIdentity: ResumeSessionIdentity,
1788
1984
  sourceRootOverride?: string,
1789
1985
  ): Promise<void> {
1790
1986
  const root = scopeRoot(scope);
@@ -1793,12 +1989,18 @@ async function copyArtifacts(
1793
1989
  if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
1794
1990
  const destinationRoot = destinationTranscript.slice(0, -6);
1795
1991
  for (const entry of manifest) {
1992
+ revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
1796
1993
  lock.assertOwned();
1797
1994
  const source = path.join(sourceRoot, entry.path);
1798
1995
  const destination = path.join(destinationRoot, entry.path);
1799
1996
  if (entry.kind === "directory") {
1800
- if (entry.path === "") ensureManagedDirectory(destinationRoot, root);
1801
- else ensureManagedDirectory(destination, root);
1997
+ if (entry.path === "") {
1998
+ revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
1999
+ ensureManagedDirectory(destinationRoot, root);
2000
+ } else {
2001
+ revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
2002
+ ensureManagedDirectory(destination, root);
2003
+ }
1802
2004
  continue;
1803
2005
  }
1804
2006
  const snapshot = captureManagedFileNoFollow(source);
@@ -1808,6 +2010,7 @@ async function copyArtifacts(
1808
2010
  )
1809
2011
  throw new Error("source_changed");
1810
2012
  try {
2013
+ revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
1811
2014
  await copyManagedFileNoReplace(source, destination, snapshot, root);
1812
2015
  } catch (error) {
1813
2016
  if ((error as Error).message !== "destination_conflict") throw error;
@@ -1905,6 +2108,7 @@ function receiptPair(scope: ManagedScope, candidate: ManagedCandidate): ManagedC
1905
2108
  }
1906
2109
 
1907
2110
  function validateCandidateForScope(scope: ManagedScope, candidate: ManagedCandidate): ManagedCandidate | undefined {
2111
+ scopeRoot(scope);
1908
2112
  const inspected = inspectCandidate(candidate.path, candidate.provenance);
1909
2113
  if ("code" in inspected || !sameCandidate(inspected, candidate)) return undefined;
1910
2114
  const identity = identityFor(inspected.cwd);
@@ -2032,16 +2236,24 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
2032
2236
  }
2033
2237
 
2034
2238
  /** Create the v2 binding and private write protocol directories before managed writes. */
2035
- export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): Promise<ManagedScopeResolution> {
2036
- const prepared = await ensureManagedScope(scope);
2239
+ export async function prepareManagedSessionScopeForWrite(
2240
+ scope: ManagedScope,
2241
+ policy: ManagedSessionSecurityPolicy = "default",
2242
+ authority?: ManagedCandidateWriteAuthority,
2243
+ expectedCandidate?: ManagedCandidate,
2244
+ expectedIdentity?: ResumeSessionIdentity,
2245
+ ): Promise<ManagedScopeResolution> {
2246
+ if (expectedCandidate && expectedIdentity) revalidatePickerConsent(scope, expectedCandidate, expectedIdentity);
2247
+ if (authority) bindManagedWriteAuthority(scope, authority);
2248
+ const prepared = await ensureManagedScope(scope, policy);
2037
2249
  if (prepared.kind === "error") return prepared;
2038
2250
  try {
2039
2251
  const internal = managedInternalDirectory(scope);
2040
2252
  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);
2253
+ ensureManagedDirectory(internal, root, policy);
2254
+ ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
2255
+ ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
2256
+ ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
2045
2257
  await reconcileManagedTombstones(scope);
2046
2258
  return { kind: "resolved", scope };
2047
2259
  } catch (error) {
@@ -2061,28 +2273,44 @@ export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): P
2061
2273
  export async function openManagedCandidateForWrite(
2062
2274
  scope: ManagedScope,
2063
2275
  candidate: ManagedCandidate,
2064
- migrationPolicy: ManagedMigrationPolicy = "copy-retain",
2276
+ expectedIdentityOrMigrationPolicy: ResumeSessionIdentity | ManagedMigrationPolicy = "copy-retain",
2277
+ migrationPolicy: ManagedMigrationPolicy = typeof expectedIdentityOrMigrationPolicy === "string"
2278
+ ? expectedIdentityOrMigrationPolicy
2279
+ : "copy-retain",
2280
+ authority?: ManagedCandidateWriteAuthority,
2065
2281
  ): Promise<ManagedOpenCandidateResult> {
2282
+ const expectedIdentity =
2283
+ typeof expectedIdentityOrMigrationPolicy === "string" ? candidate.identity : expectedIdentityOrMigrationPolicy;
2066
2284
  if (migrationPolicy === "disabled" && candidate.provenance === "legacy")
2067
2285
  return {
2068
2286
  kind: "error",
2069
2287
  code: "legacy_migration_disabled",
2070
2288
  message: "Legacy session migration is disabled for this workspace.",
2071
2289
  };
2072
- const prepared = await prepareManagedSessionScopeForWrite(scope);
2073
- if (prepared.kind === "error")
2074
- return {
2075
- kind: "error",
2076
- code: prepared.code === "binding_conflict" ? "binding_conflict" : "binding_invalid",
2077
- message: prepared.message,
2078
- };
2079
- const current = validateCandidateForScope(scope, candidate);
2080
- if (!current)
2290
+ let prepared: ManagedScopeResolution;
2291
+ let current: ManagedCandidate;
2292
+ try {
2293
+ prepared = await prepareManagedSessionScopeForWrite(
2294
+ scope,
2295
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
2296
+ authority,
2297
+ candidate,
2298
+ expectedIdentity,
2299
+ );
2300
+ if (prepared.kind === "error")
2301
+ return {
2302
+ kind: "error",
2303
+ code: prepared.code === "binding_conflict" ? "binding_conflict" : "binding_invalid",
2304
+ message: prepared.message,
2305
+ };
2306
+ current = revalidatePickerConsent(scope, candidate, expectedIdentity);
2307
+ } catch (error) {
2081
2308
  return {
2082
2309
  kind: "error",
2083
- code: "source_changed",
2084
- message: "The managed candidate changed before it could be opened.",
2310
+ code: expectedFailure(error),
2311
+ message: error instanceof Error ? error.message : "Managed migration failed.",
2085
2312
  };
2313
+ }
2086
2314
  if (isRetired(scope, current))
2087
2315
  return { kind: "error", code: "migration_retired", message: "The managed session has been retired." };
2088
2316
  if (current.provenance === "v2") return { kind: "opened", path: current.path, candidate: current, migrated: false };
@@ -2092,10 +2320,10 @@ export async function openManagedCandidateForWrite(
2092
2320
  let lock: ManagedStorageLock | undefined;
2093
2321
  let detachedArtifacts: DetachedArtifactRoot | undefined;
2094
2322
  try {
2323
+ revalidatePickerConsent(scope, current, expectedIdentity);
2095
2324
  lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
2096
- const afterLock = validateCandidateForScope(scope, current);
2097
- if (!afterLock)
2098
- return { kind: "error", code: "source_changed", message: "The legacy candidate changed during migration." };
2325
+ const heldLock = lock;
2326
+ const afterLock = revalidatePickerConsent(scope, current, expectedIdentity);
2099
2327
  const listing = listManagedCandidates(scope);
2100
2328
  if (listing.kind === "error") return { kind: "error", code: "binding_invalid", message: listing.message };
2101
2329
  const destination = path.join(scope.directoryPath, path.basename(afterLock.path));
@@ -2107,11 +2335,18 @@ export async function openManagedCandidateForWrite(
2107
2335
  code: "destination_conflict",
2108
2336
  message: "A distinct v2 transcript already owns this session id.",
2109
2337
  };
2338
+ scopeRoot(scope);
2339
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2110
2340
  restorePreparedArtifactRoot(scope, afterLock);
2341
+ scopeRoot(scope);
2111
2342
  const sourceSnapshot = captureManagedFileNoFollow(afterLock.path);
2112
2343
  let manifest: readonly ArtifactManifestEntry[] = [];
2113
2344
  const artifactPlan = planArtifactRootForMigration(afterLock.path, operation);
2114
2345
  const intendedDestination = { path: destination, sessionId: afterLock.sessionId, cwd: afterLock.cwd };
2346
+ const assertPublicationConsent = (): void => {
2347
+ heldLock.assertOwned();
2348
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2349
+ };
2115
2350
  if (existing && existing.identity.sha256 !== afterLock.identity.sha256) {
2116
2351
  return {
2117
2352
  kind: "error",
@@ -2128,6 +2363,7 @@ export async function openManagedCandidateForWrite(
2128
2363
  }
2129
2364
  const preparedReceipt = receiptPathFor(scope, afterLock, "prepared");
2130
2365
  try {
2366
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2131
2367
  lock.assertOwned();
2132
2368
  await publishManagedFileNoReplace(
2133
2369
  preparedReceipt,
@@ -2147,7 +2383,8 @@ export async function openManagedCandidateForWrite(
2147
2383
  }
2148
2384
  : undefined,
2149
2385
  ),
2150
- lock.assertOwned,
2386
+ assertPublicationConsent,
2387
+ scopeRoot(scope),
2151
2388
  );
2152
2389
  } catch (error) {
2153
2390
  if ((error as Error).message !== "destination_conflict") throw error;
@@ -2156,11 +2393,23 @@ export async function openManagedCandidateForWrite(
2156
2393
  if (!preparedReceiptMatches(preparedReceipt, scope, afterLock, intendedDestination, artifactPlan))
2157
2394
  throw new Error("durability_failed");
2158
2395
  if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
2396
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2397
+ scopeRoot(scope);
2159
2398
  detachedArtifacts = artifactPlan ? detachArtifactRootForMigration(artifactPlan) : undefined;
2160
2399
  manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
2161
- await copyArtifacts(scope, afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
2400
+ await copyArtifacts(
2401
+ scope,
2402
+ afterLock.path,
2403
+ destination,
2404
+ manifest,
2405
+ lock,
2406
+ afterLock,
2407
+ expectedIdentity,
2408
+ detachedArtifacts?.detachedPath,
2409
+ );
2162
2410
  if (!existing) {
2163
2411
  try {
2412
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2164
2413
  lock.assertOwned();
2165
2414
  await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot, scopeRoot(scope));
2166
2415
  } catch (error) {
@@ -2168,6 +2417,7 @@ export async function openManagedCandidateForWrite(
2168
2417
  }
2169
2418
  }
2170
2419
  // Artifact files, directories, and the transcript must be durable before a receipt can grant shadow authority.
2420
+ scopeRoot(scope);
2171
2421
  if (manifest.length > 0) fsyncManagedArtifactTree(destination.slice(0, -6));
2172
2422
  lock.assertOwned();
2173
2423
  const migrated = inspectCandidate(destination, "v2");
@@ -2180,6 +2430,8 @@ export async function openManagedCandidateForWrite(
2180
2430
  )
2181
2431
  throw new Error("durability_failed");
2182
2432
  if (detachedArtifacts) {
2433
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2434
+ scopeRoot(scope);
2183
2435
  restoreDetachedArtifactRoot(detachedArtifacts);
2184
2436
  detachedArtifacts = undefined;
2185
2437
  }
@@ -2193,11 +2445,13 @@ export async function openManagedCandidateForWrite(
2193
2445
  for (const state of ["published", "committed"] as const) {
2194
2446
  const receipt = receiptPathFor(scope, afterLock, state);
2195
2447
  try {
2448
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2196
2449
  lock.assertOwned();
2197
2450
  await publishManagedFileNoReplace(
2198
2451
  receipt,
2199
2452
  migrationReceipt(scope, lock, state, afterLock, migrated, manifest),
2200
- lock.assertOwned,
2453
+ assertPublicationConsent,
2454
+ scopeRoot(scope),
2201
2455
  );
2202
2456
  } catch (error) {
2203
2457
  if ((error as Error).message !== "destination_conflict") throw error;
@@ -2211,6 +2465,8 @@ export async function openManagedCandidateForWrite(
2211
2465
  code: "durability_failed",
2212
2466
  message: "The migration receipt does not bind the copied v2 transcript and artifacts.",
2213
2467
  };
2468
+ revalidatePickerConsent(scope, afterLock, expectedIdentity);
2469
+ scopeRoot(scope);
2214
2470
  await removeStagedReceipts(scope, afterLock);
2215
2471
  return {
2216
2472
  kind: "opened",