@akagilnc/pi-workflow-roles 0.1.1751
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/LICENSE +202 -0
- package/README.md +104 -0
- package/README.zh-CN.md +133 -0
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/dist/activation-ledger-git.js +68 -0
- package/dist/activation-ledger-session.js +120 -0
- package/dist/activation-ledger-topology.js +239 -0
- package/dist/activation-reconciliation.js +61 -0
- package/dist/audit-escalation.js +108 -0
- package/dist/auditor-dossier-tool.js +35 -0
- package/dist/canonical-json.js +78 -0
- package/dist/compliance-transport.js +77 -0
- package/dist/doctor-contracts.js +172 -0
- package/dist/dossier-resolution.js +103 -0
- package/dist/evidence-child-executor.js +661 -0
- package/dist/exact-utf8.js +12 -0
- package/dist/git-object-id.js +7 -0
- package/dist/in-process-session.js +50 -0
- package/dist/merger-contracts.js +76 -0
- package/dist/navigator-attendance.js +995 -0
- package/dist/navigator-invocation-identity.js +220 -0
- package/dist/open-tool-schema.js +39 -0
- package/dist/package-contracts/collector-output.js +50 -0
- package/dist/package-contracts/fixer-output.js +72 -0
- package/dist/package-contracts/fixer-packet.js +77 -0
- package/dist/package-contracts/judge-output.js +17 -0
- package/dist/package-contracts/reviewer-output.js +82 -0
- package/dist/package-contracts/terminating-tools.js +173 -0
- package/dist/package-contracts/worker-output.js +13 -0
- package/dist/package-owned-tool-idle.js +104 -0
- package/dist/packaged-role-registry.js +34 -0
- package/dist/public-cli/main.js +23867 -0
- package/dist/public-command-renderer.js +20 -0
- package/dist/reviewer-agent.js +93 -0
- package/dist/reviewer-child-executor.js +23 -0
- package/dist/reviewer-construction.js +95 -0
- package/dist/reviewer-dispatch.js +77 -0
- package/dist/reviewer-execution-ledger.js +160 -0
- package/dist/reviewer-failure-diagnostic.js +17 -0
- package/dist/reviewer-git-snapshot.js +38 -0
- package/dist/reviewer-pinned-git.js +146 -0
- package/dist/reviewer-preflight-error.js +15 -0
- package/dist/reviewer-prompt-identity.js +10 -0
- package/dist/reviewer-scope-prompt.js +21 -0
- package/dist/reviewer-workspace.js +151 -0
- package/dist/sha256.js +5 -0
- package/dist/sitian-record-entry.js +33 -0
- package/dist/stderr-jsonl.js +26 -0
- package/dist/stream-idle-guard.js +75 -0
- package/dist/tool-execution-observation.js +141 -0
- package/dist/uuidv7.js +21 -0
- package/dist/work-subject-identity.js +53 -0
- package/extensions/role-runtime.ts +303 -0
- package/package.json +69 -0
- package/packets/fixer-prerequisites.json +6 -0
- package/packets/fixer-repair.md +5 -0
- package/packets/judge-apply.md +77 -0
- package/packets/judge-authority.md +64 -0
- package/packets/judge-plan.md +55 -0
- package/packets/judge-review.md +49 -0
- package/packets/judge-submission.md +34 -0
- package/resources/methods/code-review/SKILL.md +92 -0
- package/resources/methods/code-review/agents/openai.yaml +3 -0
- package/resources/methods/code-review/provenance.json +26 -0
- package/resources/methods/diagnosing-bugs/SKILL.md +134 -0
- package/resources/methods/diagnosing-bugs/agents/openai.yaml +3 -0
- package/resources/methods/diagnosing-bugs/provenance.json +31 -0
- package/resources/methods/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/resources/methods/resolving-merge-conflicts/SKILL.md +14 -0
- package/resources/methods/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/resources/methods/resolving-merge-conflicts/provenance.json +26 -0
- package/resources/methods/tdd/SKILL.md +38 -0
- package/resources/methods/tdd/agents/openai.yaml +3 -0
- package/resources/methods/tdd/mocking.md +59 -0
- package/resources/methods/tdd/provenance.json +36 -0
- package/resources/methods/tdd/tests.md +77 -0
- package/resources/navigator-route-playbook.md +32 -0
- package/schemas/tool-execution-observation.schema.json +107 -0
- package/scripts/build-package.mjs +65 -0
- package/scripts/generate-tool-execution-observation-schema.ts +7 -0
- package/souls/coder.md +10 -0
- package/souls/collector.md +11 -0
- package/souls/doctor-auditor.md +23 -0
- package/souls/doctor.md +8 -0
- package/souls/fixer-auditor.md +33 -0
- package/souls/fixer.md +13 -0
- package/souls/judge-auditor.md +33 -0
- package/souls/judge.md +74 -0
- package/souls/merger.md +5 -0
- package/souls/navigator.md +5 -0
- package/souls/reviewer-auditor.md +25 -0
- package/souls/reviewer.md +11 -0
- package/src/activation-ledger-git.ts +96 -0
- package/src/activation-ledger-session.ts +188 -0
- package/src/activation-ledger-topology.ts +301 -0
- package/src/activation-ledger.ts +240 -0
- package/src/activation-reconciliation.ts +163 -0
- package/src/activation-trace.ts +38 -0
- package/src/audit-escalation.ts +177 -0
- package/src/auditor-dossier-tool.ts +48 -0
- package/src/auditor-soul.ts +28 -0
- package/src/canonical-json.ts +74 -0
- package/src/canonical-skill-binding.ts +107 -0
- package/src/collector-config.ts +89 -0
- package/src/collector-evidence.ts +461 -0
- package/src/collector-github.ts +656 -0
- package/src/collector-identity.ts +161 -0
- package/src/collector-ledger.ts +827 -0
- package/src/collector-receipt.ts +87 -0
- package/src/collector-role.ts +592 -0
- package/src/collector-tool-schemas.ts +19 -0
- package/src/compliance-transport.ts +130 -0
- package/src/doctor-auditor.ts +53 -0
- package/src/doctor-contracts.ts +166 -0
- package/src/doctor-evidence.ts +47 -0
- package/src/doctor-role.ts +18 -0
- package/src/dossier-resolution.ts +137 -0
- package/src/evidence-child-executor.ts +775 -0
- package/src/exact-utf8.ts +9 -0
- package/src/factory-board.ts +1822 -0
- package/src/git-object-id.ts +11 -0
- package/src/human-format.ts +65 -0
- package/src/in-process-session.ts +78 -0
- package/src/judge-auditor.ts +55 -0
- package/src/judge-recording-anti-forge.ts +53 -0
- package/src/judge-role.ts +160 -0
- package/src/merger-contracts.ts +71 -0
- package/src/merger-git-state.ts +76 -0
- package/src/merger-role.ts +60 -0
- package/src/navigator-attendance.ts +1254 -0
- package/src/navigator-invocation-identity.ts +446 -0
- package/src/open-tool-schema.ts +46 -0
- package/src/package-contracts/collector-output.ts +109 -0
- package/src/package-contracts/fixer-output.ts +81 -0
- package/src/package-contracts/fixer-packet.ts +93 -0
- package/src/package-contracts/judge-output.ts +39 -0
- package/src/package-contracts/reviewer-output.ts +115 -0
- package/src/package-contracts/terminating-tools.ts +259 -0
- package/src/package-contracts/worker-output.ts +36 -0
- package/src/package-owned-tool-idle.ts +134 -0
- package/src/package-resources/method-skill-binding.ts +87 -0
- package/src/package-resources/method-skill.ts +358 -0
- package/src/packaged-role-registry.ts +36 -0
- package/src/public-cli/cli-errors.ts +11 -0
- package/src/public-cli/cli-io.ts +4 -0
- package/src/public-cli/cli.ts +912 -0
- package/src/public-cli/coder-run.ts +575 -0
- package/src/public-cli/collector-run.ts +375 -0
- package/src/public-cli/command-renderer.ts +8 -0
- package/src/public-cli/config.ts +346 -0
- package/src/public-cli/doctor-run.ts +355 -0
- package/src/public-cli/explicit-internal.ts +274 -0
- package/src/public-cli/fixer-run.ts +587 -0
- package/src/public-cli/host-pi-runtime.ts +112 -0
- package/src/public-cli/invocation.ts +1958 -0
- package/src/public-cli/judge-run.ts +507 -0
- package/src/public-cli/main.ts +15 -0
- package/src/public-cli/merger-run.ts +681 -0
- package/src/public-cli/public-run-credentials.ts +71 -0
- package/src/public-cli/registry.ts +153 -0
- package/src/public-cli/reviewer-run.ts +561 -0
- package/src/public-cli/run-lifecycle.ts +884 -0
- package/src/public-cli/settlement.ts +3765 -0
- package/src/public-cli/terminal.ts +325 -0
- package/src/public-command-renderer.ts +43 -0
- package/src/reviewer-agent.ts +94 -0
- package/src/reviewer-auditor.ts +53 -0
- package/src/reviewer-child-executor.ts +31 -0
- package/src/reviewer-construction.ts +137 -0
- package/src/reviewer-dispatch.ts +94 -0
- package/src/reviewer-execution-ledger.ts +206 -0
- package/src/reviewer-failure-diagnostic.ts +18 -0
- package/src/reviewer-git-snapshot.ts +53 -0
- package/src/reviewer-pinned-git.ts +144 -0
- package/src/reviewer-preflight-error.ts +14 -0
- package/src/reviewer-prompt-identity.ts +17 -0
- package/src/reviewer-role.ts +193 -0
- package/src/reviewer-scope-prompt.ts +24 -0
- package/src/reviewer-settlement.ts +63 -0
- package/src/reviewer-workspace.ts +111 -0
- package/src/role-runtime.ts +884 -0
- package/src/sha256.ts +6 -0
- package/src/sitian-record-entry.ts +57 -0
- package/src/stderr-jsonl.ts +28 -0
- package/src/stream-idle-guard.ts +98 -0
- package/src/ticket-snapshot.ts +662 -0
- package/src/ticket-trajectory.ts +1000 -0
- package/src/tool-execution-observation.ts +168 -0
- package/src/uuidv7.ts +1 -0
- package/src/work-subject-identity.ts +94 -0
- package/src/worker-role.ts +434 -0
- package/src/worker-submission-gates.ts +225 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstatSync,
|
|
3
|
+
realpathSync,
|
|
4
|
+
statSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
activationBookDirectory,
|
|
12
|
+
ensureRealDirectoryTree,
|
|
13
|
+
errnoCode,
|
|
14
|
+
errorText,
|
|
15
|
+
pathContainedIn,
|
|
16
|
+
} from "./activation-ledger-topology.ts";
|
|
17
|
+
|
|
18
|
+
export function childSessionManager(parent: { getSessionFile(): string | undefined; getSessionDir(): string } | undefined, cwd: string, childDirectory: string): SessionManager {
|
|
19
|
+
const parentSession = parent?.getSessionFile();
|
|
20
|
+
return parentSession === undefined
|
|
21
|
+
? SessionManager.inMemory(cwd)
|
|
22
|
+
: SessionManager.create(cwd, join(parent!.getSessionDir(), childDirectory), { parentSession });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Durable pointer to the authoritative Pi session file principal (ADR 0048/0049). */
|
|
26
|
+
export type ActivationSessionPointer = {
|
|
27
|
+
readonly kind: "session-file";
|
|
28
|
+
readonly path: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Session manager surface needed to admit (and, when deferred, materialize) the durable principal. */
|
|
32
|
+
export type ActivationSessionManager = {
|
|
33
|
+
getSessionFile?(): string | undefined;
|
|
34
|
+
getHeader?(): { readonly type: string } | null;
|
|
35
|
+
/** Full SessionManager rebind after early header materialization (keeps Pi append path consistent). */
|
|
36
|
+
setSessionFile?(path: string): void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Typed missing durable session principal. Callers discriminate with instanceof/code;
|
|
41
|
+
* never by parsing message prose. Original filesystem causes are retained.
|
|
42
|
+
*/
|
|
43
|
+
export class ActivationSessionFileMissingError extends Error {
|
|
44
|
+
readonly code = "AK_ACTIVATION_SESSION_FILE_MISSING" as const;
|
|
45
|
+
readonly path: string;
|
|
46
|
+
constructor(path: string, options?: { cause?: unknown }) {
|
|
47
|
+
super(
|
|
48
|
+
`Workflow role activation durable session file does not exist: ${path}`,
|
|
49
|
+
options?.cause === undefined ? undefined : { cause: options.cause },
|
|
50
|
+
);
|
|
51
|
+
this.name = "ActivationSessionFileMissingError";
|
|
52
|
+
this.path = path;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function materializeDeferredSessionFile(
|
|
57
|
+
sessionManager: ActivationSessionManager,
|
|
58
|
+
resolvedFile: string,
|
|
59
|
+
ledgerHome: string,
|
|
60
|
+
): void {
|
|
61
|
+
const header = sessionManager.getHeader?.();
|
|
62
|
+
if (header === null || header === undefined || header.type !== "session") {
|
|
63
|
+
throw new ActivationSessionFileMissingError(resolvedFile);
|
|
64
|
+
}
|
|
65
|
+
ensureRealDirectoryTree(ledgerHome, dirname(resolvedFile));
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (errnoCode(error) !== "EEXIST") {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`,
|
|
72
|
+
{ cause: error },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
// Lost a create race — validate the winner below.
|
|
76
|
+
}
|
|
77
|
+
// Rebind full SessionManager so subsequent Pi appends use O_APPEND (flushed=true)
|
|
78
|
+
// instead of exclusive wx create against the file we just wrote.
|
|
79
|
+
if (typeof sessionManager.setSessionFile === "function") {
|
|
80
|
+
sessionManager.setSessionFile(resolvedFile);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Admit only a durable Pi session file principal under the resolved machine ledger
|
|
86
|
+
* book (ADR 0048). Requires an existing regular file at admission: resolve real paths
|
|
87
|
+
* and prove containment under the real book. Reject relative paths, outside-book paths,
|
|
88
|
+
* directories, symlink escapes, and nonexistent paths that cannot be materialized from
|
|
89
|
+
* the live SessionManager header. Original filesystem causes are retained.
|
|
90
|
+
*
|
|
91
|
+
* Upstream Pi defers exclusive create until the first assistant message. When the path
|
|
92
|
+
* is the live SessionManager principal under the book and only the header is in memory,
|
|
93
|
+
* admission materializes that header onto the same path before the fact is written so
|
|
94
|
+
* the role fact never points at a session that may be created later.
|
|
95
|
+
*/
|
|
96
|
+
export function durableSessionPointer(
|
|
97
|
+
sessionManager: ActivationSessionManager,
|
|
98
|
+
options: {
|
|
99
|
+
ledgerHome: string;
|
|
100
|
+
bookKey: string;
|
|
101
|
+
},
|
|
102
|
+
): ActivationSessionPointer {
|
|
103
|
+
const file = sessionManager.getSessionFile?.();
|
|
104
|
+
if (typeof file !== "string" || file.length === 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!isAbsolute(file)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Workflow role activation requires an absolute durable session file path under the machine ledger book; got relative path: ${file}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolvedFile = resolve(file);
|
|
116
|
+
const bookRoot = resolve(activationBookDirectory(options.ledgerHome, options.bookKey));
|
|
117
|
+
if (!pathContainedIn(bookRoot, resolvedFile)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Workflow role activation requires the durable session file principal under the machine ledger book (${bookRoot}); got: ${resolvedFile}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
lstatSync(resolvedFile);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`,
|
|
129
|
+
{ cause: error },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
materializeDeferredSessionFile(sessionManager, resolvedFile, options.ledgerHome);
|
|
134
|
+
} catch (materializeError) {
|
|
135
|
+
// Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
|
|
136
|
+
// Other materialize failures keep their own typed/native identity.
|
|
137
|
+
if (materializeError instanceof ActivationSessionFileMissingError) {
|
|
138
|
+
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
139
|
+
}
|
|
140
|
+
throw materializeError;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let realBook: string;
|
|
145
|
+
try {
|
|
146
|
+
realBook = realpathSync(bookRoot);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Workflow role activation machine ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
|
|
150
|
+
{ cause: error },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let realFile: string;
|
|
155
|
+
try {
|
|
156
|
+
realFile = realpathSync(resolvedFile);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!pathContainedIn(realBook, realFile)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Workflow role activation requires the durable session file principal under the machine ledger book (${realBook}); got: ${realFile}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let info: ReturnType<typeof statSync>;
|
|
168
|
+
try {
|
|
169
|
+
info = statSync(realFile);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`,
|
|
173
|
+
{ cause: error },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (info.isDirectory()) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Workflow role activation durable session principal must be a file, not a directory: ${realFile}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (!info.isFile()) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Workflow role activation durable session principal is not a regular file: ${realFile}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return { kind: "session-file", path: realFile };
|
|
188
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstatSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
realpathSync,
|
|
5
|
+
statSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Typed activation-ledger path/fs failure. Callers discriminate with instanceof/code;
|
|
12
|
+
* never by parsing message prose. Original filesystem causes are retained.
|
|
13
|
+
*/
|
|
14
|
+
export class ActivationLedgerError extends Error {
|
|
15
|
+
readonly code = "AK_ACTIVATION_LEDGER" as const;
|
|
16
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
17
|
+
super(
|
|
18
|
+
message,
|
|
19
|
+
options?.cause === undefined ? undefined : { cause: options.cause },
|
|
20
|
+
);
|
|
21
|
+
this.name = "ActivationLedgerError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sole package-owned machine home (ADR 0048 / #78): one enumerable family under
|
|
27
|
+
* the process home directory. No env override — relative or invocation-varying
|
|
28
|
+
* homes would split the family and can write into a consumer repository.
|
|
29
|
+
* Process home must already be absolute; relative HOME is rejected before any write.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveActivationLedgerHome(home: () => string = homedir): string {
|
|
32
|
+
const processHome = home();
|
|
33
|
+
if (typeof processHome !== "string" || processHome.length === 0 || !isAbsolute(processHome)) {
|
|
34
|
+
throw new ActivationLedgerError(
|
|
35
|
+
`activation ledger process home must be absolute, got ${JSON.stringify(processHome)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return resolve(processHome, ".ak-roles");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Enumerable book directory for one basename key. */
|
|
42
|
+
export function activationBookDirectory(ledgerHome: string, bookKey: string): string {
|
|
43
|
+
return join(ledgerHome, "books", bookKey);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Append-only waiting ledger path for one book. */
|
|
47
|
+
export function activationWaitingLedgerPath(ledgerHome: string, bookKey: string): string {
|
|
48
|
+
return join(activationBookDirectory(ledgerHome, bookKey), "waiting.jsonl");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when candidate resolves strictly inside root (boundary-safe; not a string-prefix check). */
|
|
52
|
+
export function pathContainedIn(root: string, candidate: string): boolean {
|
|
53
|
+
const rel = relative(root, candidate);
|
|
54
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Physical path identity: realpath the longest existing prefix, then rejoin any
|
|
59
|
+
* not-yet-created suffix. Collapses macOS `/var` ↔ `/private/var` so containment
|
|
60
|
+
* checks do not depend on which side was realpath'd first.
|
|
61
|
+
*/
|
|
62
|
+
export function physicalPathIdentity(path: string): string {
|
|
63
|
+
const absolute = resolve(path);
|
|
64
|
+
const missing: string[] = [];
|
|
65
|
+
let cursor = absolute;
|
|
66
|
+
while (true) {
|
|
67
|
+
try {
|
|
68
|
+
const real = realpathSync(cursor);
|
|
69
|
+
return missing.length === 0 ? real : join(real, ...missing);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
72
|
+
// Non-absence failures keep lexical resolve — callers still get a path.
|
|
73
|
+
return absolute;
|
|
74
|
+
}
|
|
75
|
+
const parent = dirname(cursor);
|
|
76
|
+
if (parent === cursor) return absolute;
|
|
77
|
+
missing.unshift(basename(cursor));
|
|
78
|
+
cursor = parent;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Containment under physical path identity (symlink-stable). */
|
|
84
|
+
export function physicallyContainedIn(root: string, candidate: string): boolean {
|
|
85
|
+
return pathContainedIn(physicalPathIdentity(root), physicalPathIdentity(candidate));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function errnoCode(error: unknown): string | undefined {
|
|
89
|
+
return error !== null && typeof error === "object" && "code" in error
|
|
90
|
+
&& typeof (error as { code: unknown }).code === "string"
|
|
91
|
+
? (error as { code: string }).code
|
|
92
|
+
: undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function errorText(error: unknown): string {
|
|
96
|
+
if (!(error instanceof Error)) return String(error);
|
|
97
|
+
return error.message;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Ensure the configured ledger root is a physical directory identity.
|
|
102
|
+
* lstat before create/realpath: a root symlink is never admitted, even when its
|
|
103
|
+
* target is a directory. Missing roots are created; post-create re-lstat covers
|
|
104
|
+
* a concurrent symlink swap. Native stat/mkdir causes are retained.
|
|
105
|
+
*/
|
|
106
|
+
function assertPhysicalLedgerRoot(absoluteRoot: string): void {
|
|
107
|
+
let st: ReturnType<typeof lstatSync> | undefined;
|
|
108
|
+
try {
|
|
109
|
+
st = lstatSync(absoluteRoot);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
112
|
+
throw new ActivationLedgerError(
|
|
113
|
+
`activation ledger failed to stat home (${absoluteRoot}): ${errorText(error)}`,
|
|
114
|
+
{ cause: error },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (st === undefined) {
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(absoluteRoot, { recursive: true });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// The configured root has the same first-creator race as nested components:
|
|
124
|
+
// another process may win after our ENOENT observation. Admit only EEXIST,
|
|
125
|
+
// then type-check the winner below; every other native failure remains fatal.
|
|
126
|
+
if (errnoCode(error) !== "EEXIST") {
|
|
127
|
+
throw new ActivationLedgerError(
|
|
128
|
+
`activation ledger failed to create home (${absoluteRoot}): ${errorText(error)}`,
|
|
129
|
+
{ cause: error },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
st = lstatSync(absoluteRoot);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new ActivationLedgerError(
|
|
137
|
+
`activation ledger failed to stat home (${absoluteRoot}): ${errorText(error)}`,
|
|
138
|
+
{ cause: error },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (st.isSymbolicLink()) {
|
|
144
|
+
throw new ActivationLedgerError(
|
|
145
|
+
`activation ledger home is a symbolic link: ${absoluteRoot}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!st.isDirectory()) {
|
|
149
|
+
throw new ActivationLedgerError(`activation ledger home is not a directory: ${absoluteRoot}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create `targetDir` (and missing parents) under `root` as a physical directory chain.
|
|
155
|
+
* Every path component under the configured root is type-identified with lstat: a
|
|
156
|
+
* pre-existing symlink is never admitted — even when its target remains inside the
|
|
157
|
+
* machine home — so basename book partitions cannot alias across books (ADR 0048).
|
|
158
|
+
* The configured root itself must be a physical directory. Physical realpath
|
|
159
|
+
* containment and native filesystem causes are retained.
|
|
160
|
+
*/
|
|
161
|
+
export function ensureRealDirectoryTree(root: string, targetDir: string): string {
|
|
162
|
+
if (!isAbsolute(root)) {
|
|
163
|
+
throw new ActivationLedgerError(`activation ledger home must be absolute: ${root}`);
|
|
164
|
+
}
|
|
165
|
+
const absoluteRoot = resolve(root);
|
|
166
|
+
const absoluteTarget = resolve(targetDir);
|
|
167
|
+
if (absoluteTarget !== absoluteRoot && !pathContainedIn(absoluteRoot, absoluteTarget)) {
|
|
168
|
+
throw new ActivationLedgerError(
|
|
169
|
+
`activation ledger path escapes ledger home (${absoluteRoot}): ${absoluteTarget}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Type-identity the configured root before realpath/creation. Nested component
|
|
174
|
+
// physical identity and containment below still apply once the root is physical.
|
|
175
|
+
assertPhysicalLedgerRoot(absoluteRoot);
|
|
176
|
+
|
|
177
|
+
let realRoot: string;
|
|
178
|
+
try {
|
|
179
|
+
realRoot = realpathSync(absoluteRoot);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new ActivationLedgerError(
|
|
182
|
+
`activation ledger home is not resolvable (${absoluteRoot}): ${errorText(error)}`,
|
|
183
|
+
{ cause: error },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (!statSync(realRoot).isDirectory()) {
|
|
187
|
+
throw new ActivationLedgerError(`activation ledger home is not a directory: ${realRoot}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rel = absoluteTarget === absoluteRoot ? "" : relative(absoluteRoot, absoluteTarget);
|
|
191
|
+
if (rel === "") return realRoot;
|
|
192
|
+
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
193
|
+
throw new ActivationLedgerError(
|
|
194
|
+
`activation ledger path escapes ledger home (${absoluteRoot}): ${absoluteTarget}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let lexicalCursor = absoluteRoot;
|
|
199
|
+
for (const part of rel.split(sep)) {
|
|
200
|
+
if (part === "" || part === ".") continue;
|
|
201
|
+
if (part === "..") {
|
|
202
|
+
throw new ActivationLedgerError(`activation ledger path contains '..': ${absoluteTarget}`);
|
|
203
|
+
}
|
|
204
|
+
lexicalCursor = join(lexicalCursor, part);
|
|
205
|
+
|
|
206
|
+
let st: ReturnType<typeof lstatSync>;
|
|
207
|
+
try {
|
|
208
|
+
st = lstatSync(lexicalCursor);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
211
|
+
throw new ActivationLedgerError(
|
|
212
|
+
`activation ledger failed to stat path component (${lexicalCursor}): ${errorText(error)}`,
|
|
213
|
+
{ cause: error },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
mkdirSync(lexicalCursor);
|
|
218
|
+
} catch (mkdirError) {
|
|
219
|
+
// Concurrent first-time creators can lose the mkdir race. Only EEXIST is
|
|
220
|
+
// recoverable; re-lstat/realpath validation below still admits the winner.
|
|
221
|
+
if (errnoCode(mkdirError) !== "EEXIST") {
|
|
222
|
+
throw new ActivationLedgerError(
|
|
223
|
+
`activation ledger failed to create directory (${lexicalCursor}): ${errorText(mkdirError)}`,
|
|
224
|
+
{ cause: mkdirError },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
st = lstatSync(lexicalCursor);
|
|
230
|
+
} catch (statError) {
|
|
231
|
+
throw new ActivationLedgerError(
|
|
232
|
+
`activation ledger failed to stat path component (${lexicalCursor}): ${errorText(statError)}`,
|
|
233
|
+
{ cause: statError },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Book partitions and every parent under the machine home must be physical
|
|
239
|
+
// owned directories — in-home directory symlinks alias partitions (cross-book).
|
|
240
|
+
if (st.isSymbolicLink()) {
|
|
241
|
+
throw new ActivationLedgerError(
|
|
242
|
+
`activation ledger path component is a symbolic link: ${lexicalCursor}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!st.isDirectory()) {
|
|
247
|
+
throw new ActivationLedgerError(`activation ledger path component is not a directory: ${lexicalCursor}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let realCursor: string;
|
|
251
|
+
try {
|
|
252
|
+
realCursor = realpathSync(lexicalCursor);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new ActivationLedgerError(
|
|
255
|
+
`activation ledger path component is not resolvable (${lexicalCursor}): ${errorText(error)}`,
|
|
256
|
+
{ cause: error },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (realCursor !== realRoot && !pathContainedIn(realRoot, realCursor)) {
|
|
260
|
+
throw new ActivationLedgerError(
|
|
261
|
+
`activation ledger path component escapes ledger home (${lexicalCursor} -> ${realCursor})`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
return realpathSync(absoluteTarget);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new ActivationLedgerError(
|
|
270
|
+
`activation ledger directory is not resolvable (${absoluteTarget}): ${errorText(error)}`,
|
|
271
|
+
{ cause: error },
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Reject a pre-existing ledger-file symlink before open/write follows it.
|
|
278
|
+
* A computed book may only append to its own regular waiting.jsonl — any symlink,
|
|
279
|
+
* including a target still inside the machine ledger home (cross-book redirect),
|
|
280
|
+
* violates ADR 0048 partition identity. Missing paths are admitted (O_CREAT).
|
|
281
|
+
*/
|
|
282
|
+
export function assertLedgerFileInsideHome(ledgerPath: string, ledgerHome: string): void {
|
|
283
|
+
if (!isAbsolute(ledgerHome)) {
|
|
284
|
+
throw new ActivationLedgerError(`activation ledger home must be absolute: ${ledgerHome}`);
|
|
285
|
+
}
|
|
286
|
+
const resolvedLedger = resolve(ledgerPath);
|
|
287
|
+
try {
|
|
288
|
+
if (!lstatSync(resolvedLedger).isSymbolicLink()) return;
|
|
289
|
+
throw new ActivationLedgerError(
|
|
290
|
+
`activation ledger file is a symbolic link: ${resolvedLedger}`,
|
|
291
|
+
);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
294
|
+
if (error instanceof ActivationLedgerError) throw error;
|
|
295
|
+
throw new ActivationLedgerError(
|
|
296
|
+
`activation ledger failed to stat ledger file (${resolvedLedger}): ${errorText(error)}`,
|
|
297
|
+
{ cause: error },
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|