@akagilnc/pi-workflow-roles 0.1.1876 → 0.1.1883
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.
|
@@ -1,113 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import {
|
|
2
|
+
lstatSync,
|
|
3
|
+
realpathSync,
|
|
4
|
+
statSync,
|
|
5
|
+
writeFileSync
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { isAbsolute, resolve } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
errnoCode,
|
|
10
|
+
errorText
|
|
11
|
+
} from "./activation-ledger-topology.js";
|
|
12
|
+
class ActivationSessionFileMissingError extends Error {
|
|
13
|
+
code = "AK_ACTIVATION_SESSION_FILE_MISSING";
|
|
14
|
+
path;
|
|
15
|
+
constructor(path, options) {
|
|
16
|
+
super(
|
|
17
|
+
`Workflow role activation durable session file does not exist: ${path}`,
|
|
18
|
+
options?.cause === void 0 ? void 0 : { cause: options.cause }
|
|
19
|
+
);
|
|
20
|
+
this.name = "ActivationSessionFileMissingError";
|
|
21
|
+
this.path = path;
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
|
-
function materializeDeferredSessionFile(sessionManager, resolvedFile
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
24
|
+
function materializeDeferredSessionFile(sessionManager, resolvedFile) {
|
|
25
|
+
const header = sessionManager.getHeader?.();
|
|
26
|
+
if (header === null || header === void 0 || header.type !== "session") {
|
|
27
|
+
throw new ActivationSessionFileMissingError(resolvedFile);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
writeFileSync(resolvedFile, `${JSON.stringify(header)}
|
|
31
|
+
`, { flag: "wx" });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (errnoCode(error) !== "EEXIST") {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`,
|
|
36
|
+
{ cause: error }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (typeof sessionManager.setSessionFile === "function") {
|
|
41
|
+
sessionManager.setSessionFile(resolvedFile);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const bookRoot = resolve(activationBookDirectory(options.ledgerHome, options.bookKey));
|
|
60
|
-
if (!pathContainedIn(bookRoot, resolvedFile)) {
|
|
61
|
-
throw new Error(`Workflow role activation requires the durable session file principal under the machine ledger book (${bookRoot}); got: ${resolvedFile}`);
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
lstatSync(resolvedFile);
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
if (errnoCode(error) !== "ENOENT") {
|
|
68
|
-
throw new Error(`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
materializeDeferredSessionFile(sessionManager, resolvedFile, options.ledgerHome);
|
|
72
|
-
}
|
|
73
|
-
catch (materializeError) {
|
|
74
|
-
// Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
|
|
75
|
-
// Other materialize failures keep their own typed/native identity.
|
|
76
|
-
if (materializeError instanceof ActivationSessionFileMissingError) {
|
|
77
|
-
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
78
|
-
}
|
|
79
|
-
throw materializeError;
|
|
80
|
-
}
|
|
44
|
+
function durableSessionPointer(sessionManager) {
|
|
45
|
+
const file = sessionManager.getSessionFile?.();
|
|
46
|
+
if (typeof file !== "string" || file.length === 0) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!isAbsolute(file)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Workflow role activation requires an absolute durable session file path; got relative path: ${file}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const resolvedFile = resolve(file);
|
|
57
|
+
try {
|
|
58
|
+
lstatSync(resolvedFile);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`,
|
|
63
|
+
{ cause: error }
|
|
64
|
+
);
|
|
81
65
|
}
|
|
82
|
-
let realBook;
|
|
83
66
|
try {
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
throw new Error(`Workflow role activation machine ledger book is not resolvable (${bookRoot}): ${errorText(error)}`, { cause: error });
|
|
88
|
-
}
|
|
89
|
-
let realFile;
|
|
90
|
-
try {
|
|
91
|
-
realFile = realpathSync(resolvedFile);
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
67
|
+
materializeDeferredSessionFile(sessionManager, resolvedFile);
|
|
68
|
+
} catch (materializeError) {
|
|
69
|
+
if (materializeError instanceof ActivationSessionFileMissingError) {
|
|
94
70
|
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
71
|
+
}
|
|
72
|
+
throw materializeError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let realFile;
|
|
76
|
+
try {
|
|
77
|
+
realFile = realpathSync(resolvedFile);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
80
|
+
}
|
|
81
|
+
let info;
|
|
82
|
+
try {
|
|
83
|
+
info = statSync(realFile);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`,
|
|
87
|
+
{ cause: error }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (info.isDirectory()) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Workflow role activation durable session principal must be a file, not a directory: ${realFile}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!info.isFile()) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Workflow role activation durable session principal is not a regular file: ${realFile}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { kind: "session-file", path: realFile };
|
|
113
101
|
}
|
|
102
|
+
export {
|
|
103
|
+
ActivationSessionFileMissingError,
|
|
104
|
+
durableSessionPointer
|
|
105
|
+
};
|
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve, join } from "node:path";
|
|
2
|
+
import { existsSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, resolve, join, relative, sep } from "node:path";
|
|
4
4
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
6
6
|
import {
|
|
7
7
|
roleRunSessionCoordinates
|
|
8
8
|
} from "./sitian-role-run-coordinates.js";
|
|
9
9
|
import {
|
|
10
|
+
ActivationLedgerError,
|
|
10
11
|
activationBookDirectory,
|
|
11
12
|
ensureRealDirectoryTree,
|
|
13
|
+
errorText,
|
|
14
|
+
pathContainedIn,
|
|
12
15
|
physicallyContainedIn,
|
|
13
16
|
resolveActivationLedgerHome
|
|
14
17
|
} from "./activation-ledger-topology.js";
|
|
15
18
|
const { findMostRecentSession } = await import(new URL("./core/session-manager.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href);
|
|
16
19
|
const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
|
|
20
|
+
function assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile) {
|
|
21
|
+
const absoluteHome = resolve(ledgerHome);
|
|
22
|
+
const absoluteSessionDir = resolve(sessionDir);
|
|
23
|
+
const absoluteFile = resolve(recentFile);
|
|
24
|
+
const relToHome = relative(absoluteHome, absoluteSessionDir);
|
|
25
|
+
const segments = relToHome.split(sep);
|
|
26
|
+
if (relToHome === "" || isAbsolute(relToHome) || relToHome === ".." || relToHome.startsWith(`..${sep}`) || segments[0] !== "books" || segments[1] === void 0 || segments[1] === "" || segments[1] === "." || segments[1] === "..") {
|
|
27
|
+
throw new ActivationLedgerError(
|
|
28
|
+
`sitian record sessionDir must be under a ledger book (${ledgerHome}): ${sessionDir}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const bookRoot = join(absoluteHome, "books", segments[1]);
|
|
32
|
+
let realBookRoot;
|
|
33
|
+
try {
|
|
34
|
+
realBookRoot = realpathSync(bookRoot);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new ActivationLedgerError(
|
|
37
|
+
`activation ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
|
|
38
|
+
{ cause: error }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
let realFile;
|
|
42
|
+
try {
|
|
43
|
+
realFile = realpathSync(absoluteFile);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new ActivationLedgerError(
|
|
46
|
+
`sitian record session file is not resolvable (${absoluteFile}): ${errorText(error)}`,
|
|
47
|
+
{ cause: error }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (realFile !== realBookRoot && !pathContainedIn(realBookRoot, realFile)) {
|
|
51
|
+
throw new ActivationLedgerError(
|
|
52
|
+
`sitian record session must be under the ledger book (${bookRoot}): ${recentFile}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
17
56
|
function createRecordSession(options) {
|
|
18
57
|
const cwd = options.cwd;
|
|
19
58
|
const parentFile = options.parent?.getSessionFile();
|
|
@@ -39,7 +78,10 @@ function createRecordSession(options) {
|
|
|
39
78
|
const mayResumeSameNest = options.subject !== void 0 || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
40
79
|
if (mayResumeSameNest) {
|
|
41
80
|
const recentFile = findMostRecentSession(sessionDir, cwd);
|
|
42
|
-
if (recentFile !== null)
|
|
81
|
+
if (recentFile !== null) {
|
|
82
|
+
assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile);
|
|
83
|
+
return SessionManager.open(recentFile, sessionDir, cwd);
|
|
84
|
+
}
|
|
43
85
|
}
|
|
44
86
|
const session = SessionManager.create(
|
|
45
87
|
cwd,
|
package/package.json
CHANGED
|
@@ -4,13 +4,10 @@ import {
|
|
|
4
4
|
statSync,
|
|
5
5
|
writeFileSync,
|
|
6
6
|
} from "node:fs";
|
|
7
|
-
import {
|
|
7
|
+
import { isAbsolute, resolve } from "node:path";
|
|
8
8
|
import {
|
|
9
|
-
activationBookDirectory,
|
|
10
|
-
ensureRealDirectoryTree,
|
|
11
9
|
errnoCode,
|
|
12
10
|
errorText,
|
|
13
|
-
pathContainedIn,
|
|
14
11
|
} from "./activation-ledger-topology.ts";
|
|
15
12
|
|
|
16
13
|
/** Durable pointer to the authoritative Pi session file principal (ADR 0048/0049). */
|
|
@@ -47,13 +44,14 @@ export class ActivationSessionFileMissingError extends Error {
|
|
|
47
44
|
function materializeDeferredSessionFile(
|
|
48
45
|
sessionManager: ActivationSessionManager,
|
|
49
46
|
resolvedFile: string,
|
|
50
|
-
ledgerHome: string,
|
|
51
47
|
): void {
|
|
52
48
|
const header = sessionManager.getHeader?.();
|
|
53
49
|
if (header === null || header === undefined || header.type !== "session") {
|
|
54
50
|
throw new ActivationSessionFileMissingError(resolvedFile);
|
|
55
51
|
}
|
|
56
|
-
|
|
52
|
+
// Activation does not own record placement (ADR 0065 / #221). Parent directory is the
|
|
53
|
+
// SessionManager principal's own dir — write the header in place; do not pull ledgerHome
|
|
54
|
+
// containment or ensureRealDirectoryTree back onto this admission path.
|
|
57
55
|
try {
|
|
58
56
|
writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
|
|
59
57
|
} catch (error) {
|
|
@@ -73,23 +71,23 @@ function materializeDeferredSessionFile(
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
/**
|
|
76
|
-
* Admit
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
74
|
+
* Admit a durable Pi session file principal (ADR 0048/0065).
|
|
75
|
+
* Requires an existing regular file at admission (or a live SessionManager header that
|
|
76
|
+
* can be materialized onto the same path). Rejects relative paths, directories,
|
|
77
|
+
* non-files, and nonexistent paths that cannot be materialized. Original filesystem
|
|
78
|
+
* causes are retained.
|
|
79
|
+
*
|
|
80
|
+
* Record-placement (session must live under the ledger book) is NOT enforced here —
|
|
81
|
+
* that check lives on the sitian record entry (createRecordSession). Activation only
|
|
82
|
+
* binds the durable principal identity for the role run.
|
|
81
83
|
*
|
|
82
84
|
* Upstream Pi defers exclusive create until the first assistant message. When the path
|
|
83
|
-
* is the live SessionManager principal
|
|
84
|
-
*
|
|
85
|
-
*
|
|
85
|
+
* is the live SessionManager principal and only the header is in memory, admission
|
|
86
|
+
* materializes that header onto the same path before the fact is written so the role
|
|
87
|
+
* fact never points at a session that may be created later.
|
|
86
88
|
*/
|
|
87
89
|
export function durableSessionPointer(
|
|
88
90
|
sessionManager: ActivationSessionManager,
|
|
89
|
-
options: {
|
|
90
|
-
ledgerHome: string;
|
|
91
|
-
bookKey: string;
|
|
92
|
-
},
|
|
93
91
|
): ActivationSessionPointer {
|
|
94
92
|
const file = sessionManager.getSessionFile?.();
|
|
95
93
|
if (typeof file !== "string" || file.length === 0) {
|
|
@@ -99,17 +97,11 @@ export function durableSessionPointer(
|
|
|
99
97
|
}
|
|
100
98
|
if (!isAbsolute(file)) {
|
|
101
99
|
throw new Error(
|
|
102
|
-
`Workflow role activation requires an absolute durable session file path
|
|
100
|
+
`Workflow role activation requires an absolute durable session file path; got relative path: ${file}`,
|
|
103
101
|
);
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
const resolvedFile = resolve(file);
|
|
107
|
-
const bookRoot = resolve(activationBookDirectory(options.ledgerHome, options.bookKey));
|
|
108
|
-
if (!pathContainedIn(bookRoot, resolvedFile)) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Workflow role activation requires the durable session file principal under the machine ledger book (${bookRoot}); got: ${resolvedFile}`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
105
|
|
|
114
106
|
try {
|
|
115
107
|
lstatSync(resolvedFile);
|
|
@@ -121,7 +113,7 @@ export function durableSessionPointer(
|
|
|
121
113
|
);
|
|
122
114
|
}
|
|
123
115
|
try {
|
|
124
|
-
materializeDeferredSessionFile(sessionManager, resolvedFile
|
|
116
|
+
materializeDeferredSessionFile(sessionManager, resolvedFile);
|
|
125
117
|
} catch (materializeError) {
|
|
126
118
|
// Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
|
|
127
119
|
// Other materialize failures keep their own typed/native identity.
|
|
@@ -132,16 +124,6 @@ export function durableSessionPointer(
|
|
|
132
124
|
}
|
|
133
125
|
}
|
|
134
126
|
|
|
135
|
-
let realBook: string;
|
|
136
|
-
try {
|
|
137
|
-
realBook = realpathSync(bookRoot);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`Workflow role activation machine ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
|
|
141
|
-
{ cause: error },
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
127
|
let realFile: string;
|
|
146
128
|
try {
|
|
147
129
|
realFile = realpathSync(resolvedFile);
|
|
@@ -149,12 +131,6 @@ export function durableSessionPointer(
|
|
|
149
131
|
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
150
132
|
}
|
|
151
133
|
|
|
152
|
-
if (!pathContainedIn(realBook, realFile)) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Workflow role activation requires the durable session file principal under the machine ledger book (${realBook}); got: ${realFile}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
134
|
let info: ReturnType<typeof statSync>;
|
|
159
135
|
try {
|
|
160
136
|
info = statSync(realFile);
|
package/src/role-runtime.ts
CHANGED
|
@@ -869,7 +869,7 @@ export function createRoleRuntimeExtension(
|
|
|
869
869
|
const bookKey = resolveBookKeyFromGit(ctx.cwd);
|
|
870
870
|
const correlation = correlationIdentityFromEnv();
|
|
871
871
|
const ledgerHome = resolveActivationLedgerHome();
|
|
872
|
-
const session = durableSessionPointer(ctx.sessionManager
|
|
872
|
+
const session = durableSessionPointer(ctx.sessionManager);
|
|
873
873
|
|
|
874
874
|
if (dependencies.createNavigatorAttendance !== undefined) {
|
|
875
875
|
let work: NavigatorWorkContext;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { dirname, resolve, join } from "node:path";
|
|
7
|
+
import { existsSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, isAbsolute, resolve, join, relative, sep } from "node:path";
|
|
9
9
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
|
|
11
11
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
|
|
@@ -14,8 +14,11 @@ export {
|
|
|
14
14
|
type RoleRunSessionCoordinates,
|
|
15
15
|
} from "./sitian-role-run-coordinates.ts";
|
|
16
16
|
import {
|
|
17
|
+
ActivationLedgerError,
|
|
17
18
|
activationBookDirectory,
|
|
18
19
|
ensureRealDirectoryTree,
|
|
20
|
+
errorText,
|
|
21
|
+
pathContainedIn,
|
|
19
22
|
physicallyContainedIn,
|
|
20
23
|
resolveActivationLedgerHome,
|
|
21
24
|
} from "./activation-ledger-topology.ts";
|
|
@@ -46,9 +49,74 @@ export type CreateRecordSessionOptions = {
|
|
|
46
49
|
/** Authorized no-subject kind that may resume the most recent same-nest peer (ADR 0066). Sole string true source for gate resume identity. */
|
|
47
50
|
export const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
|
|
48
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Sole file-level placement lock for a resumed same-nest principal (ADR 0065 / #221).
|
|
54
|
+
* ensureRealDirectoryTree already owns the sessionDir chain; a final .jsonl symlink is
|
|
55
|
+
* invisible to that directory walk, so this runs once before SessionManager.open.
|
|
56
|
+
* Circle is the book physical root that owns sessionDir — not merely ledger home — so a
|
|
57
|
+
* books/A nest cannot resume a final file whose realpath lands in books/B.
|
|
58
|
+
* realpath/stat failures stay typed ActivationLedgerError with original cause — never
|
|
59
|
+
* wash through physicalPathIdentity's non-ENOENT lexical fallback.
|
|
60
|
+
*/
|
|
61
|
+
function assertRecentFinalFileUnderLedgerHome(
|
|
62
|
+
ledgerHome: string,
|
|
63
|
+
sessionDir: string,
|
|
64
|
+
recentFile: string,
|
|
65
|
+
): void {
|
|
66
|
+
const absoluteHome = resolve(ledgerHome);
|
|
67
|
+
const absoluteSessionDir = resolve(sessionDir);
|
|
68
|
+
const absoluteFile = resolve(recentFile);
|
|
69
|
+
// sessionDir just passed ensureRealDirectoryTree: derive the owning book lexically.
|
|
70
|
+
const relToHome = relative(absoluteHome, absoluteSessionDir);
|
|
71
|
+
const segments = relToHome.split(sep);
|
|
72
|
+
if (
|
|
73
|
+
relToHome === ""
|
|
74
|
+
|| isAbsolute(relToHome)
|
|
75
|
+
|| relToHome === ".."
|
|
76
|
+
|| relToHome.startsWith(`..${sep}`)
|
|
77
|
+
|| segments[0] !== "books"
|
|
78
|
+
|| segments[1] === undefined
|
|
79
|
+
|| segments[1] === ""
|
|
80
|
+
|| segments[1] === "."
|
|
81
|
+
|| segments[1] === ".."
|
|
82
|
+
) {
|
|
83
|
+
throw new ActivationLedgerError(
|
|
84
|
+
`sitian record sessionDir must be under a ledger book (${ledgerHome}): ${sessionDir}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const bookRoot = join(absoluteHome, "books", segments[1]);
|
|
88
|
+
let realBookRoot: string;
|
|
89
|
+
try {
|
|
90
|
+
realBookRoot = realpathSync(bookRoot);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new ActivationLedgerError(
|
|
93
|
+
`activation ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
|
|
94
|
+
{ cause: error },
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
let realFile: string;
|
|
98
|
+
try {
|
|
99
|
+
realFile = realpathSync(absoluteFile);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new ActivationLedgerError(
|
|
102
|
+
`sitian record session file is not resolvable (${absoluteFile}): ${errorText(error)}`,
|
|
103
|
+
{ cause: error },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (realFile !== realBookRoot && !pathContainedIn(realBookRoot, realFile)) {
|
|
107
|
+
throw new ActivationLedgerError(
|
|
108
|
+
`sitian record session must be under the ledger book (${bookRoot}): ${recentFile}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
49
113
|
/**
|
|
50
114
|
* Sole package entry that constructs a durable Pi session record (ADR 0065).
|
|
51
115
|
* No destination/path parameters — location is computed from ledger topology only.
|
|
116
|
+
* SessionDir placement is owned by ensureRealDirectoryTree; resumed recent final-file
|
|
117
|
+
* identity is checked once before SessionManager.open (directory walk cannot see a
|
|
118
|
+
* trailing .jsonl symlink). New principals mint under the already-validated sessionDir
|
|
119
|
+
* via destination-free SessionManager.create — no derived postcondition.
|
|
52
120
|
* Resume via Pi findMostRecentSession is limited to subject-keyed identity and the
|
|
53
121
|
* authorized worker-submission-gate durable path. Ordinary no-subject children
|
|
54
122
|
* (evidence-children, auditor-roles, …) always mint a fresh session — never reopen
|
|
@@ -88,6 +156,7 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
88
156
|
parentSession = parentFile;
|
|
89
157
|
}
|
|
90
158
|
|
|
159
|
+
// Directory-chain ownership: containment + physical components (no parallel assert).
|
|
91
160
|
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
92
161
|
// Subject-keyed nests continue by subject digest; gate durable resume is the only
|
|
93
162
|
// authorized no-subject same-nest continuation. All other kinds mint fresh.
|
|
@@ -95,7 +164,10 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
|
|
|
95
164
|
options.subject !== undefined || options.kind === WORKER_SUBMISSION_GATE_KIND;
|
|
96
165
|
if (mayResumeSameNest) {
|
|
97
166
|
const recentFile = findMostRecentSession(sessionDir, cwd);
|
|
98
|
-
if (recentFile !== null)
|
|
167
|
+
if (recentFile !== null) {
|
|
168
|
+
assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile);
|
|
169
|
+
return SessionManager.open(recentFile, sessionDir, cwd);
|
|
170
|
+
}
|
|
99
171
|
}
|
|
100
172
|
|
|
101
173
|
const session = SessionManager.create(
|