@gajae-code/coding-agent 0.12.4 → 0.12.6
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 +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
|
@@ -23,6 +23,7 @@ function canonicalPathSync(value: string): string {
|
|
|
23
23
|
export interface SessionStorageStat {
|
|
24
24
|
dev: bigint;
|
|
25
25
|
ino: bigint;
|
|
26
|
+
nlink?: bigint;
|
|
26
27
|
|
|
27
28
|
size: number;
|
|
28
29
|
mtimeMs: number;
|
|
@@ -41,6 +42,7 @@ function statFromNode(stats: fs.BigIntStats): SessionStorageStat {
|
|
|
41
42
|
return {
|
|
42
43
|
dev: stats.dev,
|
|
43
44
|
ino: stats.ino,
|
|
45
|
+
nlink: stats.nlink,
|
|
44
46
|
|
|
45
47
|
size: Number(stats.size),
|
|
46
48
|
mtimeMs: Number(stats.mtimeMs),
|
|
@@ -210,6 +212,7 @@ export interface SessionStorage {
|
|
|
210
212
|
export interface SessionStorageFileIdentity {
|
|
211
213
|
dev: bigint;
|
|
212
214
|
ino: bigint;
|
|
215
|
+
nlink?: bigint;
|
|
213
216
|
size: number;
|
|
214
217
|
mtimeNs: bigint;
|
|
215
218
|
sha256: string;
|
|
@@ -257,12 +260,14 @@ export interface VerifiedSessionDeleteTarget {
|
|
|
257
260
|
cwd: string;
|
|
258
261
|
/** Expected transcript file `(dev, ino)` captured at authorization. */
|
|
259
262
|
transcriptIdentity: SessionStorageFileIdentity;
|
|
263
|
+
transcriptParentIdentity?: { dev: bigint; ino: bigint };
|
|
260
264
|
/**
|
|
261
265
|
* For retry after an `artifacts` `cleanup_pending`: the recorded artifact
|
|
262
266
|
* directory identity to re-accept. A replacement/different artifact directory
|
|
263
267
|
* fails closed. Omit on first attempt or to accept recorded absence.
|
|
264
268
|
*/
|
|
265
269
|
expectedArtifactsIdentity?: SessionStorageFileIdentity;
|
|
270
|
+
artifactsAbsentAtAuthorization?: true;
|
|
266
271
|
/** Stable native recursive-tree evidence captured before artifact detachment. */
|
|
267
272
|
expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
|
|
268
273
|
/** Identity-bound quarantine path retained when recursive artifact cleanup failed. */
|
|
@@ -356,8 +361,11 @@ type NativeExactUnlink = (
|
|
|
356
361
|
identity: {
|
|
357
362
|
dev: bigint;
|
|
358
363
|
ino: bigint;
|
|
364
|
+
nlink?: bigint;
|
|
359
365
|
size: bigint;
|
|
360
366
|
mtimeNs: bigint;
|
|
367
|
+
parentDev?: bigint;
|
|
368
|
+
parentIno?: bigint;
|
|
361
369
|
/** Required for regular-file deletion; directories are identity-bound only. */
|
|
362
370
|
sha256?: string;
|
|
363
371
|
directory?: boolean;
|
|
@@ -371,10 +379,13 @@ function nativeExactUnlink(
|
|
|
371
379
|
identity: {
|
|
372
380
|
dev: bigint;
|
|
373
381
|
ino: bigint;
|
|
382
|
+
nlink?: bigint;
|
|
374
383
|
size: bigint;
|
|
375
384
|
mtimeNs: bigint;
|
|
376
385
|
/** Required for regular-file deletion; directories are identity-bound only. */
|
|
377
386
|
sha256?: string;
|
|
387
|
+
parentDev?: bigint;
|
|
388
|
+
parentIno?: bigint;
|
|
378
389
|
directory?: boolean;
|
|
379
390
|
quarantineName?: string;
|
|
380
391
|
},
|
|
@@ -387,6 +398,7 @@ type NativeDirectoryTreeEntry = {
|
|
|
387
398
|
kind: string;
|
|
388
399
|
dev: string;
|
|
389
400
|
ino: string;
|
|
401
|
+
nlink: string;
|
|
390
402
|
size: string;
|
|
391
403
|
mtimeNs: string;
|
|
392
404
|
ctimeNs: string;
|
|
@@ -402,7 +414,11 @@ type NativeDirectoryTreeResult =
|
|
|
402
414
|
| { ok: false; code: string; snapshot?: undefined };
|
|
403
415
|
type NativeDirectoryTreeApi = {
|
|
404
416
|
snapshotDirectoryTree(pathname: string): NativeDirectoryTreeResult;
|
|
405
|
-
exactRemoveDirectoryTree(
|
|
417
|
+
exactRemoveDirectoryTree(
|
|
418
|
+
pathname: string,
|
|
419
|
+
snapshot: NativeDirectoryTreeSnapshot,
|
|
420
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
421
|
+
): NativeExactUnlinkResult;
|
|
406
422
|
};
|
|
407
423
|
function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
|
|
408
424
|
return native as unknown as NativeDirectoryTreeApi;
|
|
@@ -416,8 +432,25 @@ function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
|
|
|
416
432
|
);
|
|
417
433
|
return result.snapshot;
|
|
418
434
|
}
|
|
419
|
-
function
|
|
420
|
-
|
|
435
|
+
function retainedTreeDoesNotExpandAuthority(
|
|
436
|
+
expected: NativeDirectoryTreeSnapshot,
|
|
437
|
+
retained: NativeDirectoryTreeSnapshot,
|
|
438
|
+
): boolean {
|
|
439
|
+
if (expected.rootDev !== retained.rootDev || expected.rootIno !== retained.rootIno) return false;
|
|
440
|
+
const expectedEntries = new Map(expected.entries.map(entry => [entry.relativePath, entry]));
|
|
441
|
+
return retained.entries.every(entry => {
|
|
442
|
+
if (entry.relativePath === "") return entry.kind === "directory";
|
|
443
|
+
const authorized = expectedEntries.get(entry.relativePath);
|
|
444
|
+
return authorized !== undefined && JSON.stringify(authorized) === JSON.stringify(entry);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function removeDirectoryTreeExact(
|
|
449
|
+
pathname: string,
|
|
450
|
+
snapshot: NativeDirectoryTreeSnapshot,
|
|
451
|
+
parentIdentity: { dev: bigint; ino: bigint },
|
|
452
|
+
): NativeExactUnlinkResult {
|
|
453
|
+
return nativeDirectoryTreeApi().exactRemoveDirectoryTree(pathname, snapshot, parentIdentity);
|
|
421
454
|
}
|
|
422
455
|
|
|
423
456
|
function exactUnlinkFailure(result: NativeExactUnlinkResult): SessionDeleteVerificationError {
|
|
@@ -454,7 +487,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
454
487
|
): void {
|
|
455
488
|
if (securityContext && !isValidManagedSecurityContext(securityContext))
|
|
456
489
|
throw new Error("Invalid managed session security context");
|
|
457
|
-
if (process.platform !== "linux"
|
|
490
|
+
if (process.platform !== "linux" || !securityContext) {
|
|
458
491
|
if (operation === "apply") {
|
|
459
492
|
const applied = validateNativeSecurityResult(
|
|
460
493
|
native.applyOwnerOnlyPathSecurity(pathname, "file"),
|
|
@@ -471,7 +504,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
471
504
|
if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
|
|
472
505
|
return;
|
|
473
506
|
}
|
|
474
|
-
if (
|
|
507
|
+
if (!pathIsWithin(securityContext.sessionDir, pathname))
|
|
475
508
|
throw new Error(`Managed writer escaped its session directory: ${pathname}`);
|
|
476
509
|
const result = validateNativeSecurityResult(
|
|
477
510
|
operation === "apply"
|
|
@@ -822,7 +855,9 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
822
855
|
sessionId,
|
|
823
856
|
cwd,
|
|
824
857
|
transcriptIdentity,
|
|
858
|
+
transcriptParentIdentity,
|
|
825
859
|
expectedArtifactsIdentity,
|
|
860
|
+
artifactsAbsentAtAuthorization,
|
|
826
861
|
expectedArtifactsTree,
|
|
827
862
|
detachedArtifactsPath,
|
|
828
863
|
retainedArtifactsSuccessorPath,
|
|
@@ -893,43 +928,16 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
893
928
|
const retainedArtifactRoot = (input: string): string =>
|
|
894
929
|
input.endsWith(".removing") ? input : `${input}.removing`;
|
|
895
930
|
|
|
896
|
-
const initial = hasDetachedTranscript ? undefined : this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
|
|
897
|
-
const initialStat = initial?.snapshot.stat;
|
|
898
|
-
const initialDigest = initial ? createHash("sha256").update(initial.snapshot.bytes).digest("hex") : undefined;
|
|
899
|
-
if (
|
|
900
|
-
initialStat &&
|
|
901
|
-
(initialStat.dev !== transcriptIdentity.dev ||
|
|
902
|
-
initialStat.ino !== transcriptIdentity.ino ||
|
|
903
|
-
initialStat.size !== transcriptIdentity.size ||
|
|
904
|
-
initialStat.mtimeNs !== transcriptIdentity.mtimeNs ||
|
|
905
|
-
initialDigest !== transcriptIdentity.sha256)
|
|
906
|
-
) {
|
|
907
|
-
throw new SessionDeleteVerificationError("identity", "Transcript identity does not match authorization");
|
|
908
|
-
}
|
|
909
|
-
|
|
910
931
|
const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if (
|
|
921
|
-
!durableParent.isDirectory() ||
|
|
922
|
-
durableParent.dev !== parentIdentity.dev ||
|
|
923
|
-
durableParent.ino !== parentIdentity.ino
|
|
924
|
-
)
|
|
925
|
-
throw new Error("parent_changed");
|
|
926
|
-
fs.fsyncSync(descriptor);
|
|
927
|
-
} catch (error) {
|
|
928
|
-
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
929
|
-
} finally {
|
|
930
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
931
|
-
}
|
|
932
|
-
};
|
|
932
|
+
if (
|
|
933
|
+
transcriptParentIdentity &&
|
|
934
|
+
(parentIdentity.dev !== transcriptParentIdentity.dev || parentIdentity.ino !== transcriptParentIdentity.ino)
|
|
935
|
+
)
|
|
936
|
+
throw new SessionDeleteVerificationError(
|
|
937
|
+
"identity",
|
|
938
|
+
"Transcript parent identity does not match authorization",
|
|
939
|
+
);
|
|
940
|
+
const authorizedTranscriptParentIdentity = transcriptParentIdentity ?? parentIdentity;
|
|
933
941
|
if (detachedArtifactsPath) {
|
|
934
942
|
if (
|
|
935
943
|
!expectedArtifactsIdentity ||
|
|
@@ -952,7 +960,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
952
960
|
"artifacts",
|
|
953
961
|
"Detached artifact cleanup requires a persisted tree snapshot",
|
|
954
962
|
);
|
|
955
|
-
const removal = removeDirectoryTreeExact(
|
|
963
|
+
const removal = removeDirectoryTreeExact(
|
|
964
|
+
detachedArtifactsPath,
|
|
965
|
+
expectedArtifactsTree,
|
|
966
|
+
authorizedTranscriptParentIdentity,
|
|
967
|
+
);
|
|
956
968
|
if (!removal.ok) {
|
|
957
969
|
const retainedRoot = removal.detachedPath ?? detachedArtifactsPath;
|
|
958
970
|
if (retainedRoot !== detachedArtifactsPath && retainedRoot !== retainedArtifactRoot(detachedArtifactsPath))
|
|
@@ -960,7 +972,21 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
960
972
|
"artifacts",
|
|
961
973
|
"Native artifact removal returned an unauthorized root",
|
|
962
974
|
);
|
|
963
|
-
|
|
975
|
+
const retainedTree = snapshotDirectoryTree(retainedRoot);
|
|
976
|
+
if (
|
|
977
|
+
retainedTree.rootDev !== String(expectedArtifactsIdentity.dev) ||
|
|
978
|
+
retainedTree.rootIno !== String(expectedArtifactsIdentity.ino)
|
|
979
|
+
)
|
|
980
|
+
throw new SessionDeleteVerificationError(
|
|
981
|
+
"artifacts",
|
|
982
|
+
"Retained artifact root identity changed during partial cleanup",
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
if (!retainedTreeDoesNotExpandAuthority(expectedArtifactsTree, retainedTree))
|
|
986
|
+
throw new SessionDeleteVerificationError(
|
|
987
|
+
"artifacts",
|
|
988
|
+
"Partial artifact cleanup expanded retained tree authority",
|
|
989
|
+
);
|
|
964
990
|
return {
|
|
965
991
|
kind: "cleanup_pending",
|
|
966
992
|
phase: "artifacts",
|
|
@@ -970,7 +996,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
970
996
|
),
|
|
971
997
|
artifactsIdentity: expectedArtifactsIdentity,
|
|
972
998
|
detachedArtifactsPath: retainedRoot,
|
|
973
|
-
artifactsTree:
|
|
999
|
+
artifactsTree: retainedTree,
|
|
974
1000
|
...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
|
|
975
1001
|
? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
|
|
976
1002
|
: {}),
|
|
@@ -983,10 +1009,36 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
983
1009
|
transcriptIdentity,
|
|
984
1010
|
};
|
|
985
1011
|
}
|
|
1012
|
+
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1013
|
+
}
|
|
1014
|
+
if (transcriptIdentity.nlink === undefined || transcriptIdentity.nlink !== 1n)
|
|
1015
|
+
throw new SessionDeleteVerificationError(
|
|
1016
|
+
"identity",
|
|
1017
|
+
"Single-link transcript authority is required for exact deletion",
|
|
1018
|
+
);
|
|
1019
|
+
const initial = hasDetachedTranscript ? undefined : this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
|
|
1020
|
+
const initialStat = initial?.snapshot.stat;
|
|
1021
|
+
const initialDigest = initial ? createHash("sha256").update(initial.snapshot.bytes).digest("hex") : undefined;
|
|
1022
|
+
if (
|
|
1023
|
+
initialStat &&
|
|
1024
|
+
(initialStat.nlink === undefined ||
|
|
1025
|
+
initialStat.dev !== transcriptIdentity.dev ||
|
|
1026
|
+
initialStat.ino !== transcriptIdentity.ino ||
|
|
1027
|
+
initialStat.nlink !== transcriptIdentity.nlink ||
|
|
1028
|
+
initialStat.size !== transcriptIdentity.size ||
|
|
1029
|
+
initialStat.mtimeNs !== transcriptIdentity.mtimeNs ||
|
|
1030
|
+
initialDigest !== transcriptIdentity.sha256)
|
|
1031
|
+
) {
|
|
1032
|
+
throw new SessionDeleteVerificationError("identity", "Transcript identity does not match authorization");
|
|
986
1033
|
}
|
|
987
1034
|
|
|
988
1035
|
const artifactsDir = transcriptPath.slice(0, -6);
|
|
989
1036
|
const artifactsIdentity = this.#optionalDirectoryIdentity(artifactsDir);
|
|
1037
|
+
if (artifactsAbsentAtAuthorization && artifactsIdentity)
|
|
1038
|
+
throw new SessionDeleteVerificationError(
|
|
1039
|
+
"artifacts",
|
|
1040
|
+
"Artifact directory appeared after absence authorization",
|
|
1041
|
+
);
|
|
990
1042
|
if (artifactsRemoved && artifactsIdentity) {
|
|
991
1043
|
throw new SessionDeleteVerificationError(
|
|
992
1044
|
"artifacts",
|
|
@@ -1001,7 +1053,6 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1001
1053
|
"artifacts",
|
|
1002
1054
|
"Authorized artifact removal root remains after restart",
|
|
1003
1055
|
);
|
|
1004
|
-
durablyRecordArtifactPhase();
|
|
1005
1056
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1006
1057
|
}
|
|
1007
1058
|
|
|
@@ -1035,8 +1086,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1035
1086
|
const detach = nativeExactUnlink(artifactsDir, {
|
|
1036
1087
|
dev: artifactStat.dev,
|
|
1037
1088
|
ino: artifactStat.ino,
|
|
1089
|
+
nlink: artifactStat.nlink,
|
|
1038
1090
|
size: artifactStat.size,
|
|
1039
1091
|
mtimeNs: artifactStat.mtimeNs,
|
|
1092
|
+
parentDev: authorizedTranscriptParentIdentity.dev,
|
|
1093
|
+
parentIno: authorizedTranscriptParentIdentity.ino,
|
|
1040
1094
|
directory: true,
|
|
1041
1095
|
quarantineName: path.basename(plannedArtifactsPath),
|
|
1042
1096
|
});
|
|
@@ -1046,7 +1100,27 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1046
1100
|
`Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
|
|
1047
1101
|
);
|
|
1048
1102
|
}
|
|
1049
|
-
if (!detach.ok)
|
|
1103
|
+
if (!detach.ok && process.platform !== "win32") {
|
|
1104
|
+
let descriptor: number | undefined;
|
|
1105
|
+
try {
|
|
1106
|
+
descriptor = fs.openSync(
|
|
1107
|
+
path.dirname(transcriptPath),
|
|
1108
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1109
|
+
);
|
|
1110
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1111
|
+
if (
|
|
1112
|
+
!durableParent.isDirectory() ||
|
|
1113
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
1114
|
+
durableParent.ino !== parentIdentity.ino
|
|
1115
|
+
)
|
|
1116
|
+
throw new Error("parent_changed");
|
|
1117
|
+
fs.fsyncSync(descriptor);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1120
|
+
} finally {
|
|
1121
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1050
1124
|
if (!detach.ok) {
|
|
1051
1125
|
return {
|
|
1052
1126
|
kind: "cleanup_pending",
|
|
@@ -1062,7 +1136,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1062
1136
|
transcriptIdentity,
|
|
1063
1137
|
};
|
|
1064
1138
|
}
|
|
1065
|
-
const removal = removeDirectoryTreeExact(
|
|
1139
|
+
const removal = removeDirectoryTreeExact(
|
|
1140
|
+
detach.detachedPath,
|
|
1141
|
+
artifactsTree,
|
|
1142
|
+
authorizedTranscriptParentIdentity,
|
|
1143
|
+
);
|
|
1066
1144
|
if (!removal.ok) {
|
|
1067
1145
|
const retainedRoot = removal.detachedPath ?? detach.detachedPath;
|
|
1068
1146
|
if (retainedRoot !== detach.detachedPath && retainedRoot !== retainedArtifactRoot(detach.detachedPath))
|
|
@@ -1070,7 +1148,21 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1070
1148
|
"artifacts",
|
|
1071
1149
|
"Native artifact removal returned an unauthorized root",
|
|
1072
1150
|
);
|
|
1073
|
-
|
|
1151
|
+
const retainedTree = snapshotDirectoryTree(retainedRoot);
|
|
1152
|
+
if (
|
|
1153
|
+
retainedTree.rootDev !== String(artifactsIdentity.dev) ||
|
|
1154
|
+
retainedTree.rootIno !== String(artifactsIdentity.ino)
|
|
1155
|
+
)
|
|
1156
|
+
throw new SessionDeleteVerificationError(
|
|
1157
|
+
"artifacts",
|
|
1158
|
+
"Retained artifact root identity changed during partial cleanup",
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
if (!retainedTreeDoesNotExpandAuthority(artifactsTree, retainedTree))
|
|
1162
|
+
throw new SessionDeleteVerificationError(
|
|
1163
|
+
"artifacts",
|
|
1164
|
+
"Partial artifact cleanup expanded retained tree authority",
|
|
1165
|
+
);
|
|
1074
1166
|
return {
|
|
1075
1167
|
kind: "cleanup_pending",
|
|
1076
1168
|
phase: "artifacts",
|
|
@@ -1080,7 +1172,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1080
1172
|
),
|
|
1081
1173
|
artifactsIdentity,
|
|
1082
1174
|
detachedArtifactsPath: retainedRoot,
|
|
1083
|
-
artifactsTree,
|
|
1175
|
+
artifactsTree: retainedTree,
|
|
1084
1176
|
...((removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath)
|
|
1085
1177
|
? { retainedSuccessorPath: removal.retainedSuccessorPath ?? retainedArtifactsSuccessorPath }
|
|
1086
1178
|
: {}),
|
|
@@ -1095,15 +1187,38 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1095
1187
|
}
|
|
1096
1188
|
}
|
|
1097
1189
|
if (!artifactsRemoved) {
|
|
1098
|
-
|
|
1190
|
+
if (process.platform !== "win32") {
|
|
1191
|
+
let descriptor: number | undefined;
|
|
1192
|
+
try {
|
|
1193
|
+
descriptor = fs.openSync(
|
|
1194
|
+
path.dirname(transcriptPath),
|
|
1195
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1196
|
+
);
|
|
1197
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1198
|
+
if (
|
|
1199
|
+
!durableParent.isDirectory() ||
|
|
1200
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
1201
|
+
durableParent.ino !== parentIdentity.ino
|
|
1202
|
+
)
|
|
1203
|
+
throw new Error("parent_changed");
|
|
1204
|
+
fs.fsyncSync(descriptor);
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1207
|
+
} finally {
|
|
1208
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1099
1211
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1100
1212
|
}
|
|
1101
1213
|
if (hasDetachedTranscript) {
|
|
1102
1214
|
const deletion = nativeExactUnlink(cleanupTranscriptPath, {
|
|
1103
1215
|
dev: transcriptIdentity.dev,
|
|
1104
1216
|
ino: transcriptIdentity.ino,
|
|
1217
|
+
nlink: transcriptIdentity.nlink,
|
|
1105
1218
|
size: BigInt(transcriptIdentity.size),
|
|
1106
1219
|
mtimeNs: transcriptIdentity.mtimeNs,
|
|
1220
|
+
parentDev: authorizedTranscriptParentIdentity.dev,
|
|
1221
|
+
parentIno: authorizedTranscriptParentIdentity.ino,
|
|
1107
1222
|
sha256: transcriptIdentity.sha256,
|
|
1108
1223
|
quarantineName: path.basename(plannedTranscriptPath),
|
|
1109
1224
|
});
|
|
@@ -1164,8 +1279,11 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1164
1279
|
const deletion = nativeExactUnlink(transcriptPath, {
|
|
1165
1280
|
dev: initialStat.dev,
|
|
1166
1281
|
ino: initialStat.ino,
|
|
1282
|
+
nlink: initialStat.nlink,
|
|
1167
1283
|
size: BigInt(initialStat.size),
|
|
1168
1284
|
mtimeNs: initialStat.mtimeNs,
|
|
1285
|
+
parentDev: authorizedTranscriptParentIdentity.dev,
|
|
1286
|
+
parentIno: authorizedTranscriptParentIdentity.ino,
|
|
1169
1287
|
sha256: initialDigest,
|
|
1170
1288
|
quarantineName: path.basename(plannedTranscriptPath),
|
|
1171
1289
|
});
|
|
@@ -1267,7 +1385,14 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1267
1385
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1268
1386
|
throw new SessionDeleteVerificationError("symlink", "Directory is a symlink or not a directory");
|
|
1269
1387
|
}
|
|
1270
|
-
return {
|
|
1388
|
+
return {
|
|
1389
|
+
dev: stat.dev,
|
|
1390
|
+
ino: stat.ino,
|
|
1391
|
+
nlink: stat.nlink,
|
|
1392
|
+
size: Number(stat.size),
|
|
1393
|
+
mtimeNs: stat.mtimeNs,
|
|
1394
|
+
sha256: "",
|
|
1395
|
+
};
|
|
1271
1396
|
}
|
|
1272
1397
|
|
|
1273
1398
|
#optionalDirectoryIdentity(dirPath: string): SessionStorageFileIdentity | undefined {
|
|
@@ -1290,7 +1415,14 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1290
1415
|
// Fail closed before any mutation.
|
|
1291
1416
|
throw new SessionDeleteVerificationError("artifacts", "Artifact path exists but is not a directory");
|
|
1292
1417
|
}
|
|
1293
|
-
return {
|
|
1418
|
+
return {
|
|
1419
|
+
dev: stat.dev,
|
|
1420
|
+
ino: stat.ino,
|
|
1421
|
+
nlink: stat.nlink,
|
|
1422
|
+
size: Number(stat.size),
|
|
1423
|
+
mtimeNs: stat.mtimeNs,
|
|
1424
|
+
sha256: "",
|
|
1425
|
+
};
|
|
1294
1426
|
}
|
|
1295
1427
|
}
|
|
1296
1428
|
|
|
@@ -1421,6 +1553,7 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1421
1553
|
return {
|
|
1422
1554
|
dev: 0n,
|
|
1423
1555
|
ino: entry.ino,
|
|
1556
|
+
nlink: 1n,
|
|
1424
1557
|
size: entry.content.byteLength,
|
|
1425
1558
|
mtimeMs: entry.mtimeMs,
|
|
1426
1559
|
mtimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
|
|
@@ -1558,7 +1691,11 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1558
1691
|
const entry = this.#files.get(transcriptPath);
|
|
1559
1692
|
if (!entry) return Promise.resolve({ kind: "deleted" });
|
|
1560
1693
|
const snapshot = this.readSnapshotSync(transcriptPath);
|
|
1561
|
-
if (
|
|
1694
|
+
if (
|
|
1695
|
+
snapshot.stat.dev !== transcriptIdentity.dev ||
|
|
1696
|
+
snapshot.stat.ino !== transcriptIdentity.ino ||
|
|
1697
|
+
snapshot.stat.nlink !== transcriptIdentity.nlink
|
|
1698
|
+
) {
|
|
1562
1699
|
return Promise.reject(new SessionDeleteVerificationError("identity", "Transcript identity mismatch"));
|
|
1563
1700
|
}
|
|
1564
1701
|
const header = parseFirstJsonlLine(snapshot.bytes);
|