@litfamily/litopencode 1.0.1 → 1.0.2
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 +7 -0
- package/README-Ko-KR.md +8 -7
- package/README.md +8 -7
- package/dist/activation-prompt-utils.d.ts +1 -1
- package/dist/activation-routing.d.ts +1 -1
- package/dist/activation-routing.js +1 -8
- package/dist/activation-workflow-prompts.d.ts +0 -1
- package/dist/activation-workflow-prompts.js +0 -8
- package/dist/activation.d.ts +1 -1
- package/dist/activation.js +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +1 -31
- package/dist/cli/loop.js +0 -19
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +0 -2
- package/dist/cli/types.d.ts +0 -4
- package/dist/cli.js +0 -14
- package/dist/commands.d.ts +1 -10
- package/dist/commands.js +1 -11
- package/dist/features.d.ts +1 -22
- package/dist/features.js +0 -43
- package/dist/hooks.js +0 -13
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -5
- package/dist/skills.d.ts +1 -8
- package/dist/skills.js +0 -13
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/privacy.md +2 -2
- package/docs/reference.md +5 -48
- package/package.json +2 -1
- package/skills/frontend-ui-ux/references/complete-contract.md +1 -1
- package/skills/lit-plan/SKILL.md +1 -1
- package/skills/managed-skill-manifest.json +2 -9
- package/skills/visual-qa/references/complete-contract.md +1 -1
- package/tools/check-payload-substance.mjs +74 -27
- package/tools/payload-substance-parity.json +10 -25
- package/tools/version-manifests.json +2 -2
- package/dist/cli/skill-loop.d.ts +0 -3
- package/dist/cli/skill-loop.js +0 -649
- package/dist/skill-loop/apply.d.ts +0 -19
- package/dist/skill-loop/apply.js +0 -483
- package/dist/skill-loop/config.d.ts +0 -50
- package/dist/skill-loop/config.js +0 -215
- package/dist/skill-loop/curator.d.ts +0 -18
- package/dist/skill-loop/curator.js +0 -232
- package/dist/skill-loop/ledger.d.ts +0 -39
- package/dist/skill-loop/ledger.js +0 -344
- package/dist/skill-loop/proposals.d.ts +0 -48
- package/dist/skill-loop/proposals.js +0 -290
- package/dist/skill-loop/storage.d.ts +0 -72
- package/dist/skill-loop/storage.js +0 -817
- package/dist/skill-loop/time.d.ts +0 -2
- package/dist/skill-loop/time.js +0 -23
- package/dist/skill-loop/transaction.d.ts +0 -62
- package/dist/skill-loop/transaction.js +0 -836
- package/dist/skill-loop/usage.d.ts +0 -31
- package/dist/skill-loop/usage.js +0 -146
- package/dist/skill-observer.d.ts +0 -22
- package/dist/skill-observer.js +0 -1154
- package/skills/skill-observer/SKILL.md +0 -148
- package/skills/skill-observer/references/review-contract.md +0 -95
|
@@ -1,836 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { lstatIfPresent } from "../cli/native-skill-tree.js";
|
|
5
|
-
import { parseStrictJson } from "../strict-json.js";
|
|
6
|
-
import { skillLoopPaths } from "./config.js";
|
|
7
|
-
import { parseSkillSnapshot, readSkillLedger, readSnapshotFile, restoreSnapshot, snapshotSkill } from "./ledger.js";
|
|
8
|
-
import { assertPortableRelativeFile, canonicalRelativeIdentity, parseSkillProposal } from "./proposals.js";
|
|
9
|
-
import { assertSkillLoopLockCapability, assertSkillLoopRootPins, anchoredAssertSkillPathAbsent, anchoredCopyIntoSkillsRoot, anchoredReadFileFromSkillsRoot, anchoredRemoveFromSkillsRoot, anchoredVerifySkillTree, anchoredWriteFileToSkillsRoot, ensureSafeRuntimeDirectory, readSafeRuntimeFile, removeSafeRuntimeFile, writeSafeRuntimeFile } from "./storage.js";
|
|
10
|
-
const journalLimit = 32 * 1024 * 1024;
|
|
11
|
-
const pinLimit = 4096;
|
|
12
|
-
const receiptLimit = 128 * 1024;
|
|
13
|
-
function invalid() {
|
|
14
|
-
throw new Error("SKILL_TRANSACTION_INVALID");
|
|
15
|
-
}
|
|
16
|
-
function equalSnapshots(left, right) {
|
|
17
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
18
|
-
}
|
|
19
|
-
function digest(bytes) {
|
|
20
|
-
return createHash("sha256").update(bytes).digest("hex");
|
|
21
|
-
}
|
|
22
|
-
function pinPath(skillsRoot) {
|
|
23
|
-
return path.join(skillsRoot, ".litopencode-skill-loop.pin");
|
|
24
|
-
}
|
|
25
|
-
function rejectReceiptPath(paths, requestId) {
|
|
26
|
-
const name = digest(Buffer.from(`litopencode-reject-receipt/v1\0${requestId}`, "utf8"));
|
|
27
|
-
return path.join(paths.runtimeRoot, "skill-transaction-receipts", `reject-${name}.json`);
|
|
28
|
-
}
|
|
29
|
-
function runtimeImageFile(paths, image) {
|
|
30
|
-
return image.key === "ledger" ? paths.ledgerFile
|
|
31
|
-
: image.key === "usage" ? paths.usageFile
|
|
32
|
-
: path.join(paths.proposalsDir, `${image.proposalId}.json`);
|
|
33
|
-
}
|
|
34
|
-
async function runtimeImage(projectRoot, configRoot, key, proposalId) {
|
|
35
|
-
const paths = skillLoopPaths(projectRoot, configRoot);
|
|
36
|
-
const file = key === "ledger" ? paths.ledgerFile : key === "usage" ? paths.usageFile : path.join(paths.proposalsDir, `${proposalId}.json`);
|
|
37
|
-
const bytes = await readSafeRuntimeFile(projectRoot, file, { maxBytes: key === "ledger" ? 16 * 1024 * 1024 : key === "usage" ? 2 * 1024 * 1024 : 64 * 1024 });
|
|
38
|
-
return { key, ...(proposalId === undefined ? {} : { proposalId }), before: bytes === undefined ? null : bytes.toString("base64") };
|
|
39
|
-
}
|
|
40
|
-
async function currentRuntimeImage(projectRoot, paths, image) {
|
|
41
|
-
const bytes = await readSafeRuntimeFile(projectRoot, runtimeImageFile(paths, image), {
|
|
42
|
-
maxBytes: image.key === "ledger" ? 16 * 1024 * 1024 : image.key === "usage" ? 2 * 1024 * 1024 : 64 * 1024
|
|
43
|
-
});
|
|
44
|
-
return bytes === undefined ? null : bytes.toString("base64");
|
|
45
|
-
}
|
|
46
|
-
function parseReceiptImage(value) {
|
|
47
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
48
|
-
invalid();
|
|
49
|
-
const image = value;
|
|
50
|
-
if (image.key === "ledger") {
|
|
51
|
-
if (Object.keys(image).length !== 4 || Object.keys(image).some((key) => key !== "key" && key !== "size" && key !== "digest" && key !== "headId") ||
|
|
52
|
-
!Number.isInteger(image.size) || Number(image.size) < 1 || Number(image.size) > 16 * 1024 * 1024 ||
|
|
53
|
-
typeof image.digest !== "string" || !/^[a-f0-9]{64}$/u.test(image.digest) ||
|
|
54
|
-
typeof image.headId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(image.headId))
|
|
55
|
-
invalid();
|
|
56
|
-
return Object.freeze({ key: "ledger", size: Number(image.size), digest: image.digest, headId: image.headId });
|
|
57
|
-
}
|
|
58
|
-
if (image.key !== "proposal" || Object.keys(image).length !== 3 ||
|
|
59
|
-
Object.keys(image).some((key) => key !== "key" && key !== "proposalId" && key !== "after") ||
|
|
60
|
-
typeof image.proposalId !== "string" || typeof image.after !== "string" || Buffer.byteLength(image.after, "utf8") > 96 * 1024)
|
|
61
|
-
invalid();
|
|
62
|
-
{
|
|
63
|
-
const decoded = Buffer.from(image.after, "base64");
|
|
64
|
-
if (decoded.toString("base64") !== image.after)
|
|
65
|
-
invalid();
|
|
66
|
-
}
|
|
67
|
-
return Object.freeze({ key: "proposal", proposalId: image.proposalId, after: image.after });
|
|
68
|
-
}
|
|
69
|
-
function parseRejectReceipt(value, paths, expected) {
|
|
70
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
71
|
-
invalid();
|
|
72
|
-
const record = value;
|
|
73
|
-
const keys = ["schema", "state", "id", "journalDigest", "projectRoot", "request", "proposalId", "ownershipMarker", "runtime"];
|
|
74
|
-
if (Object.keys(record).length !== keys.length || Object.keys(record).some((key) => !keys.includes(key)) ||
|
|
75
|
-
record.schema !== "litfamily.skill-reject-receipt/v1" || record.state !== "terminal" ||
|
|
76
|
-
typeof record.id !== "string" || !/^tx-[a-f0-9-]{36}$/u.test(record.id) ||
|
|
77
|
-
typeof record.journalDigest !== "string" || !/^[a-f0-9]{64}$/u.test(record.journalDigest) ||
|
|
78
|
-
record.projectRoot !== path.resolve(paths.projectRoot) || record.proposalId !== expected.requestId ||
|
|
79
|
-
record.ownershipMarker !== "litopencodeAgentGenerated" || !Array.isArray(record.runtime) || record.runtime.length !== 2)
|
|
80
|
-
invalid();
|
|
81
|
-
const request = parseRequest(record.request);
|
|
82
|
-
if (request.kind !== "reject" || request.requestId !== expected.requestId)
|
|
83
|
-
invalid();
|
|
84
|
-
const runtime = record.runtime.map(parseReceiptImage);
|
|
85
|
-
const identities = new Set(runtime.map((image) => `${image.key}:${image.key === "proposal" ? image.proposalId : ""}`));
|
|
86
|
-
if (identities.size !== 2 || !identities.has("ledger:") || !identities.has(`proposal:${expected.requestId}`))
|
|
87
|
-
invalid();
|
|
88
|
-
return Object.freeze({
|
|
89
|
-
schema: "litfamily.skill-reject-receipt/v1",
|
|
90
|
-
state: "terminal",
|
|
91
|
-
id: record.id,
|
|
92
|
-
journalDigest: record.journalDigest,
|
|
93
|
-
projectRoot: record.projectRoot,
|
|
94
|
-
request: { kind: "reject", requestId: expected.requestId },
|
|
95
|
-
proposalId: expected.requestId,
|
|
96
|
-
ownershipMarker: "litopencodeAgentGenerated",
|
|
97
|
-
runtime: Object.freeze(runtime)
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
async function readRejectReceipt(projectRoot, paths, expected) {
|
|
101
|
-
const bytes = await readSafeRuntimeFile(projectRoot, rejectReceiptPath(paths, expected.requestId), { maxBytes: receiptLimit });
|
|
102
|
-
return bytes === undefined ? undefined : parseRejectReceipt(parseStrictJson(bytes.toString("utf8")), paths, expected);
|
|
103
|
-
}
|
|
104
|
-
async function assertRejectReceiptRuntime(projectRoot, paths, receipt, allowLedgerExtension) {
|
|
105
|
-
for (const image of receipt.runtime) {
|
|
106
|
-
if (image.key === "ledger") {
|
|
107
|
-
const observed = await readSafeRuntimeFile(projectRoot, paths.ledgerFile, { maxBytes: 16 * 1024 * 1024 });
|
|
108
|
-
if (observed === undefined || observed.byteLength < image.size || (!allowLedgerExtension && observed.byteLength !== image.size) ||
|
|
109
|
-
digest(observed.subarray(0, image.size)) !== image.digest)
|
|
110
|
-
invalid();
|
|
111
|
-
const entries = await readSkillLedger(projectRoot);
|
|
112
|
-
if (!entries.some((entry) => entry.id === image.headId) || (!allowLedgerExtension && entries.at(-1)?.id !== image.headId))
|
|
113
|
-
invalid();
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (await currentRuntimeImage(projectRoot, paths, image) !== image.after)
|
|
117
|
-
invalid();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async function writeRejectReceipt(projectRoot, paths, receipt) {
|
|
121
|
-
const file = rejectReceiptPath(paths, receipt.request.requestId);
|
|
122
|
-
const existing = await readSafeRuntimeFile(projectRoot, file, { maxBytes: receiptLimit });
|
|
123
|
-
const bytes = Buffer.from(`${JSON.stringify(receipt)}\n`, "utf8");
|
|
124
|
-
if (bytes.byteLength > receiptLimit)
|
|
125
|
-
invalid();
|
|
126
|
-
if (existing !== undefined) {
|
|
127
|
-
const parsed = parseRejectReceipt(parseStrictJson(existing.toString("utf8")), paths, receipt.request);
|
|
128
|
-
if (JSON.stringify(parsed) !== JSON.stringify(receipt))
|
|
129
|
-
invalid();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
await writeSafeRuntimeFile(projectRoot, file, bytes);
|
|
133
|
-
}
|
|
134
|
-
function parseRequest(value) {
|
|
135
|
-
if (typeof value !== "object" || value === null || Array.isArray(value) ||
|
|
136
|
-
Object.keys(value).length !== 2 || Object.keys(value).some((key) => key !== "kind" && key !== "requestId"))
|
|
137
|
-
invalid();
|
|
138
|
-
const record = value;
|
|
139
|
-
if ((record.kind !== "apply" && record.kind !== "reject" && record.kind !== "rollback" && record.kind !== "curator") ||
|
|
140
|
-
typeof record.requestId !== "string" || record.requestId.length < 1 || record.requestId.length > 128)
|
|
141
|
-
invalid();
|
|
142
|
-
try {
|
|
143
|
-
assertPortableRelativeFile(record.requestId);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
invalid();
|
|
147
|
-
}
|
|
148
|
-
return Object.freeze({ kind: record.kind, requestId: record.requestId });
|
|
149
|
-
}
|
|
150
|
-
function parseJournal(value, paths) {
|
|
151
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
152
|
-
invalid();
|
|
153
|
-
const record = value;
|
|
154
|
-
const keys = ["schema", "id", "kind", "requestId", "projectRoot", "skillsRoot", "proposalId", "action", "ownershipMarker", "targets", "runtime", "backup"];
|
|
155
|
-
if (Object.keys(record).length !== keys.length || Object.keys(record).some((key) => !keys.includes(key)) ||
|
|
156
|
-
record.schema !== "litfamily.skill-transaction/v2" || typeof record.id !== "string" || !/^tx-[a-f0-9-]{36}$/u.test(record.id) ||
|
|
157
|
-
(record.kind !== "apply" && record.kind !== "reject" && record.kind !== "rollback" && record.kind !== "curator") ||
|
|
158
|
-
typeof record.requestId !== "string" || record.requestId.length < 1 || record.requestId.length > 128 ||
|
|
159
|
-
record.projectRoot !== path.resolve(paths.projectRoot) || record.skillsRoot !== path.resolve(paths.skillsRoot) ||
|
|
160
|
-
(record.proposalId !== null && typeof record.proposalId !== "string") || record.ownershipMarker !== "litopencodeAgentGenerated" ||
|
|
161
|
-
!Array.isArray(record.targets) || record.targets.length > 32 || !Array.isArray(record.runtime) || record.runtime.length > 3)
|
|
162
|
-
invalid();
|
|
163
|
-
try {
|
|
164
|
-
assertPortableRelativeFile(record.id);
|
|
165
|
-
assertPortableRelativeFile(record.requestId);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
invalid();
|
|
169
|
-
}
|
|
170
|
-
const identities = new Set();
|
|
171
|
-
const targets = record.targets.map((value) => {
|
|
172
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
173
|
-
invalid();
|
|
174
|
-
const target = value;
|
|
175
|
-
const allowed = ["targetSkill", "proposalId", "action", "before", "after", "archivePath", "archiveStagePath"];
|
|
176
|
-
if (Object.keys(target).some((key) => !allowed.includes(key)) || typeof target.targetSkill !== "string" || target.targetSkill.includes("/") ||
|
|
177
|
-
(target.proposalId !== null && typeof target.proposalId !== "string") ||
|
|
178
|
-
(target.action !== "patch" && target.action !== "create" && target.action !== "add-reference" && target.action !== "archive" && target.action !== "rollback" && target.action !== "curator-archive") ||
|
|
179
|
-
(target.archivePath !== undefined && typeof target.archivePath !== "string") ||
|
|
180
|
-
(target.archiveStagePath !== undefined && typeof target.archiveStagePath !== "string"))
|
|
181
|
-
invalid();
|
|
182
|
-
try {
|
|
183
|
-
assertPortableRelativeFile(target.targetSkill);
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
invalid();
|
|
187
|
-
}
|
|
188
|
-
const identity = canonicalRelativeIdentity(target.targetSkill);
|
|
189
|
-
if (identities.has(identity))
|
|
190
|
-
invalid();
|
|
191
|
-
identities.add(identity);
|
|
192
|
-
let before;
|
|
193
|
-
let after;
|
|
194
|
-
try {
|
|
195
|
-
before = parseSkillSnapshot(target.before);
|
|
196
|
-
after = parseSkillSnapshot(target.after);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
invalid();
|
|
200
|
-
}
|
|
201
|
-
if (target.action === "rollback" && equalSnapshots(before, after))
|
|
202
|
-
invalid();
|
|
203
|
-
let archivePath;
|
|
204
|
-
let archiveStagePath;
|
|
205
|
-
if ((target.archivePath === undefined) !== (target.archiveStagePath === undefined) ||
|
|
206
|
-
(target.archivePath !== undefined && target.action !== "archive" && target.action !== "curator-archive"))
|
|
207
|
-
invalid();
|
|
208
|
-
if (target.archivePath !== undefined && target.archiveStagePath !== undefined) {
|
|
209
|
-
const archive = path.resolve(target.archivePath);
|
|
210
|
-
const archiveStage = path.resolve(target.archiveStagePath);
|
|
211
|
-
const relative = path.relative(paths.runtimeRoot, archive);
|
|
212
|
-
const stageRelative = path.relative(paths.runtimeRoot, archiveStage);
|
|
213
|
-
if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) ||
|
|
214
|
-
stageRelative === "" || stageRelative === ".." || stageRelative.startsWith(`..${path.sep}`) || path.isAbsolute(stageRelative) ||
|
|
215
|
-
archive === archiveStage)
|
|
216
|
-
invalid();
|
|
217
|
-
archivePath = archive;
|
|
218
|
-
archiveStagePath = archiveStage;
|
|
219
|
-
}
|
|
220
|
-
return Object.freeze({
|
|
221
|
-
targetSkill: target.targetSkill,
|
|
222
|
-
proposalId: target.proposalId,
|
|
223
|
-
action: target.action,
|
|
224
|
-
before,
|
|
225
|
-
after,
|
|
226
|
-
...(archivePath === undefined ? {} : { archivePath, archiveStagePath })
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
const runtimeKeys = new Set();
|
|
230
|
-
const runtime = record.runtime.map((value) => {
|
|
231
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
232
|
-
invalid();
|
|
233
|
-
const image = value;
|
|
234
|
-
if (Object.keys(image).some((key) => key !== "key" && key !== "proposalId" && key !== "before") ||
|
|
235
|
-
(image.key !== "ledger" && image.key !== "usage" && image.key !== "proposal") ||
|
|
236
|
-
(image.key === "proposal") !== (typeof image.proposalId === "string") ||
|
|
237
|
-
(image.before !== null && (typeof image.before !== "string" || Buffer.byteLength(image.before, "utf8") > journalLimit)))
|
|
238
|
-
invalid();
|
|
239
|
-
const identity = `${String(image.key)}:${String(image.proposalId ?? "")}`;
|
|
240
|
-
if (runtimeKeys.has(identity))
|
|
241
|
-
invalid();
|
|
242
|
-
runtimeKeys.add(identity);
|
|
243
|
-
if (typeof image.before === "string") {
|
|
244
|
-
const decoded = Buffer.from(image.before, "base64");
|
|
245
|
-
if (decoded.toString("base64") !== image.before)
|
|
246
|
-
invalid();
|
|
247
|
-
}
|
|
248
|
-
return { key: image.key, ...(typeof image.proposalId === "string" ? { proposalId: image.proposalId } : {}), before: image.before };
|
|
249
|
-
});
|
|
250
|
-
const action = record.action;
|
|
251
|
-
if ((record.kind === "apply" && (action !== "patch" && action !== "create" && action !== "add-reference" && action !== "archive")) ||
|
|
252
|
-
(record.kind === "reject" && action !== "reject") || (record.kind === "rollback" && action !== "rollback") ||
|
|
253
|
-
(record.kind === "curator" && action !== "curator"))
|
|
254
|
-
invalid();
|
|
255
|
-
if (((record.kind === "apply" || record.kind === "reject") && typeof record.proposalId !== "string") ||
|
|
256
|
-
(record.kind === "curator" && record.proposalId !== null) ||
|
|
257
|
-
(record.kind === "apply" && targets.length !== 1) || (record.kind === "reject" && targets.length !== 0) ||
|
|
258
|
-
(record.kind === "rollback" && targets.length !== 1) ||
|
|
259
|
-
targets.some((target) => target.proposalId !== record.proposalId ||
|
|
260
|
-
(record.kind === "curator" ? target.action !== "curator-archive" : record.kind === "rollback" ? target.action !== "rollback" : record.kind === "apply" ? target.action !== action : true)))
|
|
261
|
-
invalid();
|
|
262
|
-
const keysPresent = new Set(runtime.map((image) => image.key));
|
|
263
|
-
if (!keysPresent.has("ledger") || (record.kind !== "reject" && !keysPresent.has("usage")) ||
|
|
264
|
-
((record.kind === "apply" || record.kind === "reject") && !keysPresent.has("proposal")))
|
|
265
|
-
invalid();
|
|
266
|
-
let backup = null;
|
|
267
|
-
if (record.backup !== null) {
|
|
268
|
-
if (record.kind !== "curator" || typeof record.backup !== "object" || Array.isArray(record.backup))
|
|
269
|
-
invalid();
|
|
270
|
-
const source = record.backup;
|
|
271
|
-
if (Object.keys(source).length !== 4 || typeof source.path !== "string" || typeof source.stagePath !== "string" ||
|
|
272
|
-
typeof source.markerToken !== "string" || !/^[a-f0-9-]{36}$/u.test(source.markerToken) ||
|
|
273
|
-
!Array.isArray(source.skills) || source.skills.length > 256)
|
|
274
|
-
invalid();
|
|
275
|
-
const backupPath = path.resolve(source.path);
|
|
276
|
-
const stagePath = path.resolve(source.stagePath);
|
|
277
|
-
const backupRelative = path.relative(paths.backupsDir, backupPath);
|
|
278
|
-
const stageRelative = path.relative(paths.backupsDir, stagePath);
|
|
279
|
-
if (backupRelative === "" || backupRelative === ".." || backupRelative.startsWith(`..${path.sep}`) || path.isAbsolute(backupRelative) ||
|
|
280
|
-
stageRelative === "" || stageRelative === ".." || stageRelative.startsWith(`..${path.sep}`) || path.isAbsolute(stageRelative) || stagePath === backupPath)
|
|
281
|
-
invalid();
|
|
282
|
-
const skillIdentities = new Set();
|
|
283
|
-
const skills = source.skills.map((value) => {
|
|
284
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
285
|
-
invalid();
|
|
286
|
-
const skill = value;
|
|
287
|
-
if (Object.keys(skill).length !== 2 || typeof skill.skillId !== "string" || skill.skillId.includes("/"))
|
|
288
|
-
invalid();
|
|
289
|
-
try {
|
|
290
|
-
assertPortableRelativeFile(skill.skillId);
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
invalid();
|
|
294
|
-
}
|
|
295
|
-
const identity = canonicalRelativeIdentity(skill.skillId);
|
|
296
|
-
if (skillIdentities.has(identity))
|
|
297
|
-
invalid();
|
|
298
|
-
skillIdentities.add(identity);
|
|
299
|
-
let snapshot;
|
|
300
|
-
try {
|
|
301
|
-
snapshot = parseSkillSnapshot(skill.snapshot);
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
invalid();
|
|
305
|
-
}
|
|
306
|
-
return Object.freeze({ skillId: skill.skillId, snapshot });
|
|
307
|
-
});
|
|
308
|
-
backup = Object.freeze({ path: backupPath, stagePath, markerToken: source.markerToken, skills: Object.freeze(skills) });
|
|
309
|
-
}
|
|
310
|
-
if ((record.kind === "curator") !== (backup !== null))
|
|
311
|
-
invalid();
|
|
312
|
-
return Object.freeze({
|
|
313
|
-
schema: "litfamily.skill-transaction/v2",
|
|
314
|
-
id: record.id,
|
|
315
|
-
kind: record.kind,
|
|
316
|
-
requestId: record.requestId,
|
|
317
|
-
projectRoot: record.projectRoot,
|
|
318
|
-
skillsRoot: record.skillsRoot,
|
|
319
|
-
proposalId: record.proposalId,
|
|
320
|
-
action: action,
|
|
321
|
-
ownershipMarker: "litopencodeAgentGenerated",
|
|
322
|
-
targets: Object.freeze(targets),
|
|
323
|
-
runtime: Object.freeze(runtime),
|
|
324
|
-
backup
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
function parsePin(value) {
|
|
328
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
329
|
-
invalid();
|
|
330
|
-
const record = value;
|
|
331
|
-
const keys = ["schema", "state", "id", "journalDigest", "projectRoot", "skillsRoot", "request", "ownershipMarker"];
|
|
332
|
-
if (Object.keys(record).length !== keys.length || Object.keys(record).some((key) => !keys.includes(key)) ||
|
|
333
|
-
record.schema !== "litfamily.skill-transaction-pin/v2" ||
|
|
334
|
-
(record.state !== "active" && record.state !== "terminal" && record.state !== "stale") ||
|
|
335
|
-
typeof record.id !== "string" || !/^tx-[a-f0-9-]{36}$/u.test(record.id) ||
|
|
336
|
-
typeof record.journalDigest !== "string" || !/^[a-f0-9]{64}$/u.test(record.journalDigest) ||
|
|
337
|
-
typeof record.projectRoot !== "string" || !path.isAbsolute(record.projectRoot) ||
|
|
338
|
-
typeof record.skillsRoot !== "string" || !path.isAbsolute(record.skillsRoot) ||
|
|
339
|
-
record.ownershipMarker !== "litopencodeAgentGenerated")
|
|
340
|
-
invalid();
|
|
341
|
-
return Object.freeze({
|
|
342
|
-
schema: "litfamily.skill-transaction-pin/v2",
|
|
343
|
-
state: record.state,
|
|
344
|
-
id: record.id,
|
|
345
|
-
journalDigest: record.journalDigest,
|
|
346
|
-
projectRoot: path.resolve(record.projectRoot),
|
|
347
|
-
skillsRoot: path.resolve(record.skillsRoot),
|
|
348
|
-
request: parseRequest(record.request),
|
|
349
|
-
ownershipMarker: "litopencodeAgentGenerated"
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
async function readPin(skillsRoot, capability) {
|
|
353
|
-
await assertSkillLoopRootPins(capability);
|
|
354
|
-
const observed = await anchoredReadFileFromSkillsRoot(capability, path.basename(pinPath(skillsRoot)), pinLimit);
|
|
355
|
-
if (observed === undefined)
|
|
356
|
-
return undefined;
|
|
357
|
-
return parsePin(parseStrictJson(observed.bytes.toString("utf8")));
|
|
358
|
-
}
|
|
359
|
-
async function writePin(skillsRoot, value, capability) {
|
|
360
|
-
await assertSkillLoopRootPins(capability);
|
|
361
|
-
const target = path.basename(pinPath(skillsRoot));
|
|
362
|
-
const before = await anchoredReadFileFromSkillsRoot(capability, target, pinLimit);
|
|
363
|
-
await anchoredWriteFileToSkillsRoot(capability, target, Buffer.from(`${JSON.stringify(value)}\n`, "utf8"), before === undefined ? null : {
|
|
364
|
-
dev: before.dev,
|
|
365
|
-
ino: before.ino,
|
|
366
|
-
ctimeMs: before.ctimeMs,
|
|
367
|
-
birthtimeMs: before.birthtimeMs
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
function pinFor(journal, bytes, state) {
|
|
371
|
-
return Object.freeze({
|
|
372
|
-
schema: "litfamily.skill-transaction-pin/v2",
|
|
373
|
-
state,
|
|
374
|
-
id: journal.id,
|
|
375
|
-
journalDigest: digest(bytes),
|
|
376
|
-
projectRoot: journal.projectRoot,
|
|
377
|
-
skillsRoot: journal.skillsRoot,
|
|
378
|
-
request: { kind: journal.kind, requestId: journal.requestId },
|
|
379
|
-
ownershipMarker: "litopencodeAgentGenerated"
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
function pinMatches(pin, journal, bytes) {
|
|
383
|
-
return pin.id === journal.id && pin.journalDigest === digest(bytes) && pin.projectRoot === journal.projectRoot &&
|
|
384
|
-
pin.skillsRoot === journal.skillsRoot && pin.request.kind === journal.kind && pin.request.requestId === journal.requestId &&
|
|
385
|
-
pin.ownershipMarker === journal.ownershipMarker;
|
|
386
|
-
}
|
|
387
|
-
export function createSkillTransactionId() {
|
|
388
|
-
return `tx-${randomUUID()}`;
|
|
389
|
-
}
|
|
390
|
-
function decodedImage(image) {
|
|
391
|
-
return image.before === null ? undefined : Buffer.from(image.before, "base64");
|
|
392
|
-
}
|
|
393
|
-
function expectedRejectedProposalBytes(journal, paths) {
|
|
394
|
-
const image = journal.runtime.find((candidate) => candidate.key === "proposal");
|
|
395
|
-
const before = image === undefined ? undefined : decodedImage(image);
|
|
396
|
-
if (before === undefined)
|
|
397
|
-
invalid();
|
|
398
|
-
const proposal = parseSkillProposal(parseStrictJson(before.toString("utf8")), paths.skillsRoot);
|
|
399
|
-
if (proposal.id !== journal.requestId || proposal.id !== journal.proposalId || proposal.status !== "pending")
|
|
400
|
-
invalid();
|
|
401
|
-
return Buffer.from(`${JSON.stringify({ ...proposal, status: "rejected" }, null, 2)}\n`, "utf8");
|
|
402
|
-
}
|
|
403
|
-
async function recognizedRejectAfterImages(projectRoot, paths, journal) {
|
|
404
|
-
const ledgerImage = journal.runtime.find((candidate) => candidate.key === "ledger");
|
|
405
|
-
const proposalImage = journal.runtime.find((candidate) => candidate.key === "proposal");
|
|
406
|
-
if (ledgerImage === undefined || proposalImage === undefined)
|
|
407
|
-
invalid();
|
|
408
|
-
const ledgerBefore = decodedImage(ledgerImage) ?? Buffer.alloc(0);
|
|
409
|
-
const ledgerAfterBase64 = await currentRuntimeImage(projectRoot, paths, ledgerImage);
|
|
410
|
-
const proposalAfterBase64 = await currentRuntimeImage(projectRoot, paths, proposalImage);
|
|
411
|
-
if (ledgerAfterBase64 === null || proposalAfterBase64 === null)
|
|
412
|
-
invalid();
|
|
413
|
-
const ledgerAfter = Buffer.from(ledgerAfterBase64, "base64");
|
|
414
|
-
const proposalAfter = Buffer.from(proposalAfterBase64, "base64");
|
|
415
|
-
const expectedProposal = expectedRejectedProposalBytes(journal, paths);
|
|
416
|
-
if (!proposalAfter.equals(expectedProposal) || ledgerAfter.byteLength <= ledgerBefore.byteLength ||
|
|
417
|
-
!ledgerAfter.subarray(0, ledgerBefore.byteLength).equals(ledgerBefore))
|
|
418
|
-
invalid();
|
|
419
|
-
const appended = ledgerAfter.subarray(ledgerBefore.byteLength).toString("utf8");
|
|
420
|
-
if (!appended.endsWith("\n") || appended.slice(0, -1).includes("\n") || appended.length <= 1)
|
|
421
|
-
invalid();
|
|
422
|
-
const entries = await readSkillLedger(projectRoot);
|
|
423
|
-
const terminal = entries.at(-1);
|
|
424
|
-
const proposal = parseSkillProposal(parseStrictJson(expectedProposal.toString("utf8")), paths.skillsRoot);
|
|
425
|
-
if (terminal === undefined || terminal.operation !== "reject" || terminal.actor !== "user" ||
|
|
426
|
-
terminal.proposalId !== journal.proposalId || terminal.targetSkill !== proposal.targetSkill ||
|
|
427
|
-
path.resolve(terminal.targetRoot) !== path.resolve(paths.skillsRoot) || terminal.before.length !== 0 || terminal.after.length !== 0 ||
|
|
428
|
-
terminal.restoresLedgerEntryId !== undefined || terminal.reason !== proposal.rationale)
|
|
429
|
-
invalid();
|
|
430
|
-
return Object.freeze([
|
|
431
|
-
Object.freeze({ key: "ledger", size: ledgerAfter.byteLength, digest: digest(ledgerAfter), headId: terminal.id }),
|
|
432
|
-
Object.freeze({ key: "proposal", proposalId: journal.requestId, after: proposalAfterBase64 })
|
|
433
|
-
]);
|
|
434
|
-
}
|
|
435
|
-
async function rejectRuntimeMatchesBefore(projectRoot, paths, journal) {
|
|
436
|
-
for (const image of journal.runtime) {
|
|
437
|
-
if (await currentRuntimeImage(projectRoot, paths, image) !== image.before)
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
async function rollbackPartialReject(projectRoot, paths, journal) {
|
|
443
|
-
const ledgerImage = journal.runtime.find((candidate) => candidate.key === "ledger");
|
|
444
|
-
const proposalImage = journal.runtime.find((candidate) => candidate.key === "proposal");
|
|
445
|
-
if (ledgerImage === undefined || proposalImage === undefined)
|
|
446
|
-
invalid();
|
|
447
|
-
const currentProposal = await currentRuntimeImage(projectRoot, paths, proposalImage);
|
|
448
|
-
if (currentProposal !== proposalImage.before)
|
|
449
|
-
return false;
|
|
450
|
-
const expectedProposal = expectedRejectedProposalBytes(journal, paths);
|
|
451
|
-
const ledgerBefore = decodedImage(ledgerImage) ?? Buffer.alloc(0);
|
|
452
|
-
const ledgerAfterBase64 = await currentRuntimeImage(projectRoot, paths, ledgerImage);
|
|
453
|
-
if (ledgerAfterBase64 === null)
|
|
454
|
-
return false;
|
|
455
|
-
const ledgerAfter = Buffer.from(ledgerAfterBase64, "base64");
|
|
456
|
-
if (ledgerAfter.byteLength <= ledgerBefore.byteLength || !ledgerAfter.subarray(0, ledgerBefore.byteLength).equals(ledgerBefore))
|
|
457
|
-
return false;
|
|
458
|
-
const appended = ledgerAfter.subarray(ledgerBefore.byteLength).toString("utf8");
|
|
459
|
-
if (!appended.endsWith("\n") || appended.slice(0, -1).includes("\n") || appended.length <= 1)
|
|
460
|
-
return false;
|
|
461
|
-
const entries = await readSkillLedger(projectRoot);
|
|
462
|
-
const terminal = entries.at(-1);
|
|
463
|
-
const proposal = parseSkillProposal(parseStrictJson(expectedProposal.toString("utf8")), paths.skillsRoot);
|
|
464
|
-
if (terminal === undefined || terminal.operation !== "reject" || terminal.actor !== "user" ||
|
|
465
|
-
terminal.proposalId !== journal.proposalId || terminal.targetSkill !== proposal.targetSkill ||
|
|
466
|
-
path.resolve(terminal.targetRoot) !== path.resolve(paths.skillsRoot) || terminal.before.length !== 0 || terminal.after.length !== 0 ||
|
|
467
|
-
terminal.restoresLedgerEntryId !== undefined || terminal.reason !== proposal.rationale)
|
|
468
|
-
return false;
|
|
469
|
-
if (ledgerImage.before === null)
|
|
470
|
-
await removeSafeRuntimeFile(projectRoot, runtimeImageFile(paths, ledgerImage));
|
|
471
|
-
else
|
|
472
|
-
await writeSafeRuntimeFile(projectRoot, runtimeImageFile(paths, ledgerImage), ledgerBefore);
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
function backupMarkerBytes(journal) {
|
|
476
|
-
const backup = journal.backup;
|
|
477
|
-
if (backup === null)
|
|
478
|
-
invalid();
|
|
479
|
-
return Buffer.from(`${JSON.stringify({
|
|
480
|
-
schema: "litfamily.curator-backup/v1",
|
|
481
|
-
transactionId: journal.id,
|
|
482
|
-
markerToken: backup.markerToken,
|
|
483
|
-
backupPath: backup.path
|
|
484
|
-
})}\n`, "utf8");
|
|
485
|
-
}
|
|
486
|
-
async function expectedSnapshotFiles(projectRoot, snapshot, prefix = "") {
|
|
487
|
-
const expected = new Map();
|
|
488
|
-
for (const item of snapshot) {
|
|
489
|
-
const bytes = await readSnapshotFile(projectRoot, snapshot, item.file);
|
|
490
|
-
if (bytes === undefined)
|
|
491
|
-
invalid();
|
|
492
|
-
expected.set(prefix === "" ? item.file : path.posix.join(prefix, item.file), bytes);
|
|
493
|
-
}
|
|
494
|
-
return expected;
|
|
495
|
-
}
|
|
496
|
-
async function expectedBackupFiles(projectRoot, journal) {
|
|
497
|
-
const backup = journal.backup;
|
|
498
|
-
if (backup === null)
|
|
499
|
-
invalid();
|
|
500
|
-
const expected = new Map([[".litopencode-curator-backup.json", backupMarkerBytes(journal)]]);
|
|
501
|
-
for (const skill of backup.skills) {
|
|
502
|
-
for (const [file, bytes] of await expectedSnapshotFiles(projectRoot, skill.snapshot, skill.skillId))
|
|
503
|
-
expected.set(file, bytes);
|
|
504
|
-
}
|
|
505
|
-
return expected;
|
|
506
|
-
}
|
|
507
|
-
async function validateBackupTree(projectRoot, root, expected, allowPartial) {
|
|
508
|
-
const observed = await lstatIfPresent(root);
|
|
509
|
-
if (observed === null)
|
|
510
|
-
return false;
|
|
511
|
-
if (observed.isSymbolicLink() || !observed.isDirectory())
|
|
512
|
-
invalid();
|
|
513
|
-
const seen = new Set();
|
|
514
|
-
const walk = async (directory, prefix = "") => {
|
|
515
|
-
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
516
|
-
if (entry.isSymbolicLink())
|
|
517
|
-
invalid();
|
|
518
|
-
const relative = prefix === "" ? entry.name : path.posix.join(prefix, entry.name);
|
|
519
|
-
const target = path.join(directory, entry.name);
|
|
520
|
-
if (entry.isDirectory()) {
|
|
521
|
-
if (![...expected.keys()].some((file) => file.startsWith(`${relative}/`)))
|
|
522
|
-
invalid();
|
|
523
|
-
await walk(target, relative);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (!entry.isFile() || seen.has(relative))
|
|
527
|
-
invalid();
|
|
528
|
-
const wanted = expected.get(relative);
|
|
529
|
-
if (wanted === undefined)
|
|
530
|
-
invalid();
|
|
531
|
-
const bytes = await readSafeRuntimeFile(projectRoot, target, { allowMissing: false, maxBytes: wanted.byteLength });
|
|
532
|
-
if (bytes === undefined || (allowPartial
|
|
533
|
-
? bytes.byteLength > wanted.byteLength || !wanted.subarray(0, bytes.byteLength).equals(bytes)
|
|
534
|
-
: !wanted.equals(bytes)))
|
|
535
|
-
invalid();
|
|
536
|
-
seen.add(relative);
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
await walk(root);
|
|
540
|
-
if (!allowPartial && (seen.size !== expected.size || [...expected.keys()].some((file) => !seen.has(file))))
|
|
541
|
-
invalid();
|
|
542
|
-
return true;
|
|
543
|
-
}
|
|
544
|
-
export async function materializeTransactionSnapshot(projectRoot, stagePath, destinationPath, snapshot) {
|
|
545
|
-
await ensureSafeRuntimeDirectory(projectRoot, path.dirname(stagePath));
|
|
546
|
-
if (await lstatIfPresent(stagePath) !== null || await lstatIfPresent(destinationPath) !== null)
|
|
547
|
-
throw new Error("TARGET_ALREADY_EXISTS");
|
|
548
|
-
await fs.mkdir(stagePath, { mode: 0o700 });
|
|
549
|
-
const expected = await expectedSnapshotFiles(projectRoot, snapshot);
|
|
550
|
-
for (const [relative, bytes] of expected) {
|
|
551
|
-
const destination = path.join(stagePath, ...relative.split("/"));
|
|
552
|
-
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
553
|
-
await fs.writeFile(destination, bytes, { mode: 0o600, flag: "wx" });
|
|
554
|
-
}
|
|
555
|
-
await validateBackupTree(projectRoot, stagePath, expected, false);
|
|
556
|
-
await fs.rename(stagePath, destinationPath);
|
|
557
|
-
}
|
|
558
|
-
async function cleanupJournalArchives(projectRoot, journal) {
|
|
559
|
-
for (const target of journal.targets) {
|
|
560
|
-
if (target.archivePath === undefined || target.archiveStagePath === undefined)
|
|
561
|
-
continue;
|
|
562
|
-
const expected = await expectedSnapshotFiles(projectRoot, target.before);
|
|
563
|
-
if (await validateBackupTree(projectRoot, target.archiveStagePath, expected, true)) {
|
|
564
|
-
await fs.rm(target.archiveStagePath, { recursive: true, force: false });
|
|
565
|
-
}
|
|
566
|
-
if (await validateBackupTree(projectRoot, target.archivePath, expected, false)) {
|
|
567
|
-
await fs.rm(target.archivePath, { recursive: true, force: false });
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
async function cleanupJournalBackup(projectRoot, journal) {
|
|
572
|
-
const backup = journal.backup;
|
|
573
|
-
if (backup === null)
|
|
574
|
-
return;
|
|
575
|
-
const expected = await expectedBackupFiles(projectRoot, journal);
|
|
576
|
-
if (await validateBackupTree(projectRoot, backup.stagePath, expected, true)) {
|
|
577
|
-
await fs.rm(backup.stagePath, { recursive: true, force: false });
|
|
578
|
-
}
|
|
579
|
-
if (await validateBackupTree(projectRoot, backup.path, expected, false)) {
|
|
580
|
-
await fs.rm(backup.path, { recursive: true, force: false });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
async function prePinJournalIsUnchanged(projectRoot, paths, journal, validateTarget, targetPinned, capability) {
|
|
584
|
-
for (const image of journal.runtime) {
|
|
585
|
-
if (await currentRuntimeImage(projectRoot, paths, image) !== image.before)
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
try {
|
|
589
|
-
for (const target of journal.targets) {
|
|
590
|
-
await assertSkillLoopRootPins(capability);
|
|
591
|
-
validateTarget(target.targetSkill);
|
|
592
|
-
if (await targetPinned(target.targetSkill))
|
|
593
|
-
return false;
|
|
594
|
-
if (target.before.length === 0)
|
|
595
|
-
await anchoredAssertSkillPathAbsent(capability, target.targetSkill);
|
|
596
|
-
else
|
|
597
|
-
await anchoredVerifySkillTree(capability, target.targetSkill, target.before);
|
|
598
|
-
await anchoredAssertSkillPathAbsent(capability, `.${target.targetSkill}.${journal.id}.stage`);
|
|
599
|
-
await anchoredAssertSkillPathAbsent(capability, `.${target.targetSkill}.${journal.id}.backup`);
|
|
600
|
-
if (target.archivePath !== undefined && await lstatIfPresent(target.archivePath) !== null)
|
|
601
|
-
return false;
|
|
602
|
-
if (target.archiveStagePath !== undefined && await lstatIfPresent(target.archiveStagePath) !== null)
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
catch (error) {
|
|
607
|
-
if (error instanceof Error && error.message === "SKILL_LOOP_CONFLICT")
|
|
608
|
-
return false;
|
|
609
|
-
throw error;
|
|
610
|
-
}
|
|
611
|
-
const recoveryRoot = path.join(paths.runtimeRoot, "skill-transaction-recovery", journal.id);
|
|
612
|
-
if (await lstatIfPresent(recoveryRoot) !== null)
|
|
613
|
-
return false;
|
|
614
|
-
if (journal.backup !== null &&
|
|
615
|
-
(await lstatIfPresent(journal.backup.path) !== null || await lstatIfPresent(journal.backup.stagePath) !== null))
|
|
616
|
-
return false;
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
|
-
export async function recoverSkillTransaction(projectRoot, configRoot, expected, validateTarget, targetPinned, capability) {
|
|
620
|
-
const paths = skillLoopPaths(projectRoot, configRoot);
|
|
621
|
-
assertSkillLoopLockCapability(capability, paths.projectRoot, expected.kind === "reject" ? undefined : paths.skillsRoot);
|
|
622
|
-
if (expected.kind !== "reject")
|
|
623
|
-
await assertSkillLoopRootPins(capability);
|
|
624
|
-
const journalBytes = await readSafeRuntimeFile(projectRoot, paths.transactionFile, { maxBytes: journalLimit });
|
|
625
|
-
if (journalBytes === undefined) {
|
|
626
|
-
if (expected.kind !== "reject")
|
|
627
|
-
return false;
|
|
628
|
-
const rejectRequest = { kind: "reject", requestId: expected.requestId };
|
|
629
|
-
const receipt = await readRejectReceipt(projectRoot, paths, rejectRequest);
|
|
630
|
-
if (receipt === undefined)
|
|
631
|
-
return false;
|
|
632
|
-
await assertRejectReceiptRuntime(projectRoot, paths, receipt, true);
|
|
633
|
-
return "committed";
|
|
634
|
-
}
|
|
635
|
-
let journal;
|
|
636
|
-
try {
|
|
637
|
-
journal = parseJournal(parseStrictJson(journalBytes.toString("utf8")), paths);
|
|
638
|
-
}
|
|
639
|
-
catch {
|
|
640
|
-
invalid();
|
|
641
|
-
}
|
|
642
|
-
if (journal.kind !== expected.kind || journal.requestId !== expected.requestId)
|
|
643
|
-
throw new Error("SKILL_TRANSACTION_PENDING");
|
|
644
|
-
if (journal.kind === "reject") {
|
|
645
|
-
const rejectRequest = { kind: "reject", requestId: journal.requestId };
|
|
646
|
-
const receipt = await readRejectReceipt(projectRoot, paths, rejectRequest);
|
|
647
|
-
if (receipt !== undefined) {
|
|
648
|
-
if (receipt.id !== journal.id || receipt.journalDigest !== digest(journalBytes))
|
|
649
|
-
invalid();
|
|
650
|
-
await assertRejectReceiptRuntime(projectRoot, paths, receipt, false);
|
|
651
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
652
|
-
return "committed";
|
|
653
|
-
}
|
|
654
|
-
if (await rejectRuntimeMatchesBefore(projectRoot, paths, journal)) {
|
|
655
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
656
|
-
return "rolled-back";
|
|
657
|
-
}
|
|
658
|
-
try {
|
|
659
|
-
const after = await recognizedRejectAfterImages(projectRoot, paths, journal);
|
|
660
|
-
const terminal = Object.freeze({
|
|
661
|
-
schema: "litfamily.skill-reject-receipt/v1",
|
|
662
|
-
state: "terminal",
|
|
663
|
-
id: journal.id,
|
|
664
|
-
journalDigest: digest(journalBytes),
|
|
665
|
-
projectRoot: journal.projectRoot,
|
|
666
|
-
request: rejectRequest,
|
|
667
|
-
proposalId: journal.requestId,
|
|
668
|
-
ownershipMarker: "litopencodeAgentGenerated",
|
|
669
|
-
runtime: after
|
|
670
|
-
});
|
|
671
|
-
await writeRejectReceipt(projectRoot, paths, terminal);
|
|
672
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
673
|
-
return "committed";
|
|
674
|
-
}
|
|
675
|
-
catch (error) {
|
|
676
|
-
if (!(error instanceof Error && error.message === "SKILL_TRANSACTION_INVALID"))
|
|
677
|
-
throw error;
|
|
678
|
-
}
|
|
679
|
-
if (await rollbackPartialReject(projectRoot, paths, journal)) {
|
|
680
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
681
|
-
return "rolled-back";
|
|
682
|
-
}
|
|
683
|
-
invalid();
|
|
684
|
-
}
|
|
685
|
-
const pin = await readPin(paths.skillsRoot, capability);
|
|
686
|
-
const matchesPin = pin !== undefined && pinMatches(pin, journal, journalBytes);
|
|
687
|
-
if (pin === undefined || (!matchesPin && pin.state !== "active")) {
|
|
688
|
-
if (!await prePinJournalIsUnchanged(projectRoot, paths, journal, validateTarget, targetPinned, capability))
|
|
689
|
-
invalid();
|
|
690
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
691
|
-
return "rolled-back";
|
|
692
|
-
}
|
|
693
|
-
if (!matchesPin)
|
|
694
|
-
invalid();
|
|
695
|
-
if (pin.state === "terminal") {
|
|
696
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
697
|
-
return "committed";
|
|
698
|
-
}
|
|
699
|
-
if (pin.state === "stale") {
|
|
700
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
701
|
-
return "rolled-back";
|
|
702
|
-
}
|
|
703
|
-
for (const target of journal.targets) {
|
|
704
|
-
await assertSkillLoopRootPins(capability);
|
|
705
|
-
validateTarget(target.targetSkill);
|
|
706
|
-
if (await targetPinned(target.targetSkill))
|
|
707
|
-
throw new Error("TARGET_PINNED");
|
|
708
|
-
const skillRoot = path.join(paths.skillsRoot, target.targetSkill);
|
|
709
|
-
const current = await snapshotSkill(projectRoot, skillRoot);
|
|
710
|
-
const backup = path.join(paths.skillsRoot, `.${target.targetSkill}.${journal.id}.backup`);
|
|
711
|
-
if (!equalSnapshots(current, target.before) && !equalSnapshots(current, target.after)) {
|
|
712
|
-
if (current.length !== 0)
|
|
713
|
-
invalid();
|
|
714
|
-
const backupStat = await lstatIfPresent(backup);
|
|
715
|
-
if (backupStat === null || !equalSnapshots(await snapshotSkill(projectRoot, backup), target.before))
|
|
716
|
-
invalid();
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
for (const image of journal.runtime) {
|
|
720
|
-
const file = image.key === "ledger" ? paths.ledgerFile : image.key === "usage" ? paths.usageFile : path.join(paths.proposalsDir, `${image.proposalId}.json`);
|
|
721
|
-
if (image.before === null)
|
|
722
|
-
await removeSafeRuntimeFile(projectRoot, file);
|
|
723
|
-
else
|
|
724
|
-
await writeSafeRuntimeFile(projectRoot, file, Buffer.from(image.before, "base64"));
|
|
725
|
-
}
|
|
726
|
-
for (const target of journal.targets) {
|
|
727
|
-
await assertSkillLoopRootPins(capability);
|
|
728
|
-
const recoveryStage = path.join(paths.runtimeRoot, "skill-transaction-recovery", journal.id, target.targetSkill);
|
|
729
|
-
if (target.before.length > 0) {
|
|
730
|
-
await ensureSafeRuntimeDirectory(projectRoot, path.dirname(recoveryStage));
|
|
731
|
-
await restoreSnapshot(projectRoot, recoveryStage, target.before);
|
|
732
|
-
}
|
|
733
|
-
await anchoredRemoveFromSkillsRoot(capability, target.targetSkill);
|
|
734
|
-
if (target.before.length > 0) {
|
|
735
|
-
await anchoredCopyIntoSkillsRoot(capability, recoveryStage, target.targetSkill, target.before);
|
|
736
|
-
await anchoredVerifySkillTree(capability, target.targetSkill, target.before);
|
|
737
|
-
}
|
|
738
|
-
else
|
|
739
|
-
await anchoredAssertSkillPathAbsent(capability, target.targetSkill);
|
|
740
|
-
await anchoredRemoveFromSkillsRoot(capability, `.${target.targetSkill}.${journal.id}.stage`);
|
|
741
|
-
await anchoredRemoveFromSkillsRoot(capability, `.${target.targetSkill}.${journal.id}.backup`);
|
|
742
|
-
await fs.rm(path.join(paths.runtimeRoot, "skill-transaction-recovery", journal.id), { recursive: true, force: true });
|
|
743
|
-
}
|
|
744
|
-
await cleanupJournalBackup(projectRoot, journal);
|
|
745
|
-
await cleanupJournalArchives(projectRoot, journal);
|
|
746
|
-
await writePin(paths.skillsRoot, pinFor(journal, journalBytes, "stale"), capability);
|
|
747
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
748
|
-
return "rolled-back";
|
|
749
|
-
}
|
|
750
|
-
export async function beginSkillTransaction(projectRoot, configRoot, input) {
|
|
751
|
-
const paths = skillLoopPaths(projectRoot, configRoot);
|
|
752
|
-
assertSkillLoopLockCapability(input.capability, paths.projectRoot, input.request.kind === "reject" ? undefined : paths.skillsRoot);
|
|
753
|
-
if (input.request.kind !== "reject")
|
|
754
|
-
await assertSkillLoopRootPins(input.capability);
|
|
755
|
-
if (await readSafeRuntimeFile(projectRoot, paths.transactionFile, { maxBytes: journalLimit }) !== undefined)
|
|
756
|
-
throw new Error("SKILL_TRANSACTION_PENDING");
|
|
757
|
-
const runtime = [];
|
|
758
|
-
if (input.includeLedger)
|
|
759
|
-
runtime.push(await runtimeImage(projectRoot, configRoot, "ledger"));
|
|
760
|
-
if (input.includeUsage)
|
|
761
|
-
runtime.push(await runtimeImage(projectRoot, configRoot, "usage"));
|
|
762
|
-
if (input.includeProposal)
|
|
763
|
-
runtime.push(await runtimeImage(projectRoot, configRoot, "proposal", input.proposalId));
|
|
764
|
-
const journal = parseJournal({
|
|
765
|
-
schema: "litfamily.skill-transaction/v2",
|
|
766
|
-
id: input.id,
|
|
767
|
-
kind: input.request.kind,
|
|
768
|
-
requestId: input.request.requestId,
|
|
769
|
-
projectRoot: paths.projectRoot,
|
|
770
|
-
skillsRoot: paths.skillsRoot,
|
|
771
|
-
proposalId: input.proposalId,
|
|
772
|
-
action: input.action,
|
|
773
|
-
ownershipMarker: "litopencodeAgentGenerated",
|
|
774
|
-
targets: input.targets,
|
|
775
|
-
runtime,
|
|
776
|
-
backup: input.backup ?? null
|
|
777
|
-
}, paths);
|
|
778
|
-
const bytes = Buffer.from(`${JSON.stringify(journal)}\n`, "utf8");
|
|
779
|
-
if (bytes.byteLength > journalLimit)
|
|
780
|
-
invalid();
|
|
781
|
-
await writeSafeRuntimeFile(projectRoot, paths.transactionFile, bytes);
|
|
782
|
-
if (input.request.kind === "reject")
|
|
783
|
-
return;
|
|
784
|
-
try {
|
|
785
|
-
await assertSkillLoopRootPins(input.capability);
|
|
786
|
-
const priorPin = await readPin(paths.skillsRoot, input.capability);
|
|
787
|
-
if (priorPin?.state === "active")
|
|
788
|
-
throw new Error("SKILL_TRANSACTION_PENDING");
|
|
789
|
-
await writePin(paths.skillsRoot, pinFor(journal, bytes, "active"), input.capability);
|
|
790
|
-
}
|
|
791
|
-
catch (error) {
|
|
792
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
793
|
-
throw error;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
export async function completeSkillTransaction(projectRoot, configRoot, expected, capability) {
|
|
797
|
-
const paths = skillLoopPaths(projectRoot, configRoot);
|
|
798
|
-
assertSkillLoopLockCapability(capability, paths.projectRoot, expected.kind === "reject" ? undefined : paths.skillsRoot);
|
|
799
|
-
if (expected.kind !== "reject")
|
|
800
|
-
await assertSkillLoopRootPins(capability);
|
|
801
|
-
const bytes = await readSafeRuntimeFile(projectRoot, paths.transactionFile, { allowMissing: false, maxBytes: journalLimit });
|
|
802
|
-
const journal = parseJournal(parseStrictJson(bytes.toString("utf8")), paths);
|
|
803
|
-
if (journal.kind !== expected.kind || journal.requestId !== expected.requestId)
|
|
804
|
-
throw new Error("SKILL_TRANSACTION_PENDING");
|
|
805
|
-
if (journal.kind === "reject") {
|
|
806
|
-
const request = { kind: "reject", requestId: journal.requestId };
|
|
807
|
-
const after = await recognizedRejectAfterImages(projectRoot, paths, journal);
|
|
808
|
-
await writeRejectReceipt(projectRoot, paths, Object.freeze({
|
|
809
|
-
schema: "litfamily.skill-reject-receipt/v1",
|
|
810
|
-
state: "terminal",
|
|
811
|
-
id: journal.id,
|
|
812
|
-
journalDigest: digest(bytes),
|
|
813
|
-
projectRoot: journal.projectRoot,
|
|
814
|
-
request,
|
|
815
|
-
proposalId: journal.requestId,
|
|
816
|
-
ownershipMarker: "litopencodeAgentGenerated",
|
|
817
|
-
runtime: after
|
|
818
|
-
}));
|
|
819
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
const pin = await readPin(paths.skillsRoot, capability);
|
|
823
|
-
if (pin === undefined || pin.state !== "active" || !pinMatches(pin, journal, bytes))
|
|
824
|
-
invalid();
|
|
825
|
-
for (const target of journal.targets) {
|
|
826
|
-
await assertSkillLoopRootPins(capability);
|
|
827
|
-
if (target.after.length === 0)
|
|
828
|
-
await anchoredAssertSkillPathAbsent(capability, target.targetSkill);
|
|
829
|
-
else
|
|
830
|
-
await anchoredVerifySkillTree(capability, target.targetSkill, target.after);
|
|
831
|
-
await anchoredRemoveFromSkillsRoot(capability, `.${target.targetSkill}.${journal.id}.stage`);
|
|
832
|
-
await anchoredRemoveFromSkillsRoot(capability, `.${target.targetSkill}.${journal.id}.backup`);
|
|
833
|
-
}
|
|
834
|
-
await writePin(paths.skillsRoot, pinFor(journal, bytes, "terminal"), capability);
|
|
835
|
-
await removeSafeRuntimeFile(projectRoot, paths.transactionFile);
|
|
836
|
-
}
|