@gajae-code/coding-agent 0.11.5 → 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 +67 -7
- 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 +4 -0
- 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 +13 -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 +35 -4
- package/dist/types/session/yield-queue.d.ts +8 -0
- 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 +3 -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 +71 -7
- 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 +9 -8
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +87 -28
- 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/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- 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/runtime-init.ts +16 -2
- 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 +20 -7
- package/src/sdk/bus/index.ts +8 -7
- 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 +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +26 -8
- package/src/sdk/bus/telegram-daemon.ts +1736 -338
- 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/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -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 +1212 -768
- 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 +385 -85
- package/src/session/yield-queue.ts +20 -0
- 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 +37 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask-answer-registry.ts +15 -1
- 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
|
@@ -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 {
|
|
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
|
-
|
|
375
|
+
interface ManagedScopeInput {
|
|
321
376
|
cwd: string;
|
|
322
377
|
agentDir: string;
|
|
323
378
|
sessionsRoot: string;
|
|
324
|
-
}
|
|
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 =
|
|
383
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
624
|
-
ensureManagedDirectory(scope.
|
|
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 =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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: {
|
|
1660
|
-
|
|
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
|
-
|
|
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 === "")
|
|
1801
|
-
|
|
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(
|
|
2036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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:
|
|
2084
|
-
message:
|
|
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
|
|
2097
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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",
|