@litfamily/litcodex 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/README.md +18 -21
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -38
- package/dist/install/generated-skill-payload-hashes.d.ts +1 -6
- package/dist/install/generated-skill-payload-hashes.js +1 -6
- package/dist/install/skill-catalog.d.ts +1 -1
- package/dist/install/skill-catalog.js +1 -6
- package/dist/install/skill-resource-hashes.d.ts +1 -1
- package/dist/install/skill-resource-hashes.js +1 -1
- package/marketplace/plugins/litcodex/.codex-plugin/plugin.json +1 -1
- package/marketplace/plugins/litcodex/components/auto-update/package.json +1 -1
- package/marketplace/plugins/litcodex/components/comment-checker/package.json +1 -1
- package/marketplace/plugins/litcodex/components/git-bash/package.json +1 -1
- package/marketplace/plugins/litcodex/components/lit-loop/dist/cli.js +3 -20
- package/marketplace/plugins/litcodex/components/lit-loop/dist/codex-hook.js +1 -8
- package/marketplace/plugins/litcodex/components/lit-loop/dist/hook-cli.d.ts +2 -1
- package/marketplace/plugins/litcodex/components/lit-loop/dist/hook-cli.js +4 -27
- package/marketplace/plugins/litcodex/components/lit-loop/dist/markers.d.ts +0 -2
- package/marketplace/plugins/litcodex/components/lit-loop/dist/markers.js +0 -2
- package/marketplace/plugins/litcodex/components/lit-loop/dist/modes.d.ts +1 -3
- package/marketplace/plugins/litcodex/components/lit-loop/dist/modes.js +1 -10
- package/marketplace/plugins/litcodex/components/lit-loop/package.json +1 -1
- package/marketplace/plugins/litcodex/components/lsp/package.json +1 -1
- package/marketplace/plugins/litcodex/components/rules/dist/static-injection.js +2 -2
- package/marketplace/plugins/litcodex/components/rules/package.json +1 -1
- package/marketplace/plugins/litcodex/components/start-work-continuation/package.json +1 -1
- package/marketplace/plugins/litcodex/components/telemetry/package.json +1 -1
- package/marketplace/plugins/litcodex/components/wikify-knowledge/package.json +1 -1
- package/marketplace/plugins/litcodex/skills/browser-drive/scripts/capability-probe.mjs +13 -5
- package/node_modules/@litcodex/lit-loop/README.md +1 -7
- package/node_modules/@litcodex/lit-loop/dist/cli.js +3 -20
- package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +1 -8
- package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +2 -1
- package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +4 -27
- package/node_modules/@litcodex/lit-loop/dist/markers.d.ts +0 -2
- package/node_modules/@litcodex/lit-loop/dist/markers.js +0 -2
- package/node_modules/@litcodex/lit-loop/dist/modes.d.ts +1 -3
- package/node_modules/@litcodex/lit-loop/dist/modes.js +1 -10
- package/node_modules/@litcodex/lit-loop/package.json +1 -1
- package/node_modules/@litcodex/lit-loop/skills/browser-drive/scripts/capability-probe.mjs +13 -5
- package/package.json +2 -2
- package/readme-assets/badge-version.svg +1 -1
- package/readme-assets/cover-motion.webp +0 -0
- package/marketplace/plugins/litcodex/components/lit-loop/directives/skill-observer.md +0 -105
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/apply.d.ts +0 -16
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/apply.js +0 -227
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/config.d.ts +0 -13
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/config.js +0 -12
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/curator.d.ts +0 -14
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/curator.js +0 -112
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/hook.d.ts +0 -19
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/hook.js +0 -435
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/ownership.d.ts +0 -6
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/ownership.js +0 -145
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/paths.d.ts +0 -18
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/paths.js +0 -43
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/review.d.ts +0 -28
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/review.js +0 -702
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/store.d.ts +0 -32
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/store.js +0 -667
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/transaction.d.ts +0 -26
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/transaction.js +0 -524
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/types.d.ts +0 -81
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/types.js +0 -246
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/usage.d.ts +0 -6
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/usage.js +0 -119
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop-cli.d.ts +0 -8
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop-cli.js +0 -111
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-cli.d.ts +0 -3
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-cli.js +0 -116
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-route.d.ts +0 -6
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-route.js +0 -12
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer.d.ts +0 -18
- package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer.js +0 -946
- package/marketplace/plugins/litcodex/skills/skill-observer/SKILL.md +0 -215
- package/marketplace/plugins/litcodex/skills/skill-observer/agents/openai.yaml +0 -6
- package/marketplace/plugins/litcodex/skills/skill-observer/references/review-contract.md +0 -77
- package/node_modules/@litcodex/lit-loop/directives/skill-observer.md +0 -105
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/apply.d.ts +0 -16
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/apply.js +0 -227
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/config.d.ts +0 -13
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/config.js +0 -12
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/curator.d.ts +0 -14
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/curator.js +0 -112
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/hook.d.ts +0 -19
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/hook.js +0 -435
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/ownership.d.ts +0 -6
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/ownership.js +0 -145
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/paths.d.ts +0 -18
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/paths.js +0 -43
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/review.d.ts +0 -28
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/review.js +0 -702
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/store.d.ts +0 -32
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/store.js +0 -667
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/transaction.d.ts +0 -26
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/transaction.js +0 -524
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/types.d.ts +0 -81
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/types.js +0 -246
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/usage.d.ts +0 -6
- package/node_modules/@litcodex/lit-loop/dist/skill-loop/usage.js +0 -119
- package/node_modules/@litcodex/lit-loop/dist/skill-loop-cli.d.ts +0 -8
- package/node_modules/@litcodex/lit-loop/dist/skill-loop-cli.js +0 -111
- package/node_modules/@litcodex/lit-loop/dist/skill-observer-cli.d.ts +0 -3
- package/node_modules/@litcodex/lit-loop/dist/skill-observer-cli.js +0 -116
- package/node_modules/@litcodex/lit-loop/dist/skill-observer-route.d.ts +0 -6
- package/node_modules/@litcodex/lit-loop/dist/skill-observer-route.js +0 -12
- package/node_modules/@litcodex/lit-loop/dist/skill-observer.d.ts +0 -18
- package/node_modules/@litcodex/lit-loop/dist/skill-observer.js +0 -946
- package/node_modules/@litcodex/lit-loop/skills/skill-observer/SKILL.md +0 -215
- package/node_modules/@litcodex/lit-loop/skills/skill-observer/references/review-contract.md +0 -77
|
@@ -1,946 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { performance } from "node:perf_hooks";
|
|
5
|
-
import { TextDecoder } from "node:util";
|
|
6
|
-
import { redactSecrets } from "./redaction.js";
|
|
7
|
-
export const OBSERVER_SIGNALS = Object.freeze(["correction", "repetition", "coverage-gap"]);
|
|
8
|
-
export const OBSERVER_RECORD_STATE_UNKNOWN = "OBSERVER_RECORD_STATE_UNKNOWN";
|
|
9
|
-
export const OBSERVER_RECORD_COMMITTED_CLEANUP_FAILED = "OBSERVER_RECORD_COMMITTED_CLEANUP_FAILED";
|
|
10
|
-
const INPUT_FIELDS = Object.freeze(["signal", "skillId", "observed", "proposal"]);
|
|
11
|
-
const PERSISTED_FIELDS = Object.freeze([...INPUT_FIELDS, "applied"]);
|
|
12
|
-
const TEXT_CEILING = 1000;
|
|
13
|
-
const SKILL_ID = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
14
|
-
const MAX_LOG_BYTES = 1024 * 1024;
|
|
15
|
-
const LOCK_WAIT_MS = 250;
|
|
16
|
-
const LOCK_POLL_MS = 10;
|
|
17
|
-
const LOCK_WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
18
|
-
const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
19
|
-
const OBSERVER_DIR = "skill-observer";
|
|
20
|
-
const LOG_NAME = "observations.jsonl";
|
|
21
|
-
const LOCK_SUFFIX = ".lock";
|
|
22
|
-
const LOCK_OWNER_NAME = "owner";
|
|
23
|
-
const LOCK_OWNER_MAX_BYTES = 512;
|
|
24
|
-
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
-
const CREDENTIAL_URI_PATTERN = /[a-z][a-z0-9+.-]*:\/\/[^/?#\s]*@/iu;
|
|
26
|
-
const WINDOWS_DRIVE_ABSOLUTE = /^[A-Za-z]:[\\/]/u;
|
|
27
|
-
const WINDOWS_UNC_ABSOLUTE = /^\\\\/u;
|
|
28
|
-
const LOCK_OWNER_TOKEN = /^[A-Za-z0-9:_-]{1,128}$/u;
|
|
29
|
-
export class SkillObserverError extends Error {
|
|
30
|
-
constructor(code, message) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.name = "SkillObserverError";
|
|
33
|
-
this.code = code;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function safeRootPath(root) {
|
|
37
|
-
if (typeof root !== "string") {
|
|
38
|
-
throw new SkillObserverError("OBSERVER_PATH_UNSAFE", "the project root must be a path string");
|
|
39
|
-
}
|
|
40
|
-
if (process.platform !== "win32" && (WINDOWS_DRIVE_ABSOLUTE.test(root) || WINDOWS_UNC_ABSOLUTE.test(root))) {
|
|
41
|
-
throw new SkillObserverError("OBSERVER_PATH_UNSAFE", "the project root uses a foreign absolute path format");
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
return resolve(root);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
throw new SkillObserverError("OBSERVER_PATH_UNSAFE", "the project root could not be resolved safely");
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export function observationLogPath(root = process.cwd()) {
|
|
51
|
-
return join(safeRootPath(root), ".litcodex", OBSERVER_DIR, LOG_NAME);
|
|
52
|
-
}
|
|
53
|
-
function errnoCode(error) {
|
|
54
|
-
return typeof error === "object" && error !== null && "code" in error
|
|
55
|
-
? error.code
|
|
56
|
-
: undefined;
|
|
57
|
-
}
|
|
58
|
-
function pathIsWithin(root, candidate) {
|
|
59
|
-
const child = relative(root, candidate);
|
|
60
|
-
return child === "" || (!isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`));
|
|
61
|
-
}
|
|
62
|
-
function pathsFor(root) {
|
|
63
|
-
const absoluteRoot = safeRootPath(root);
|
|
64
|
-
const stateDir = join(absoluteRoot, ".litcodex");
|
|
65
|
-
const observerDir = join(stateDir, OBSERVER_DIR);
|
|
66
|
-
const log = join(observerDir, LOG_NAME);
|
|
67
|
-
const lock = join(absoluteRoot, `${OBSERVER_DIR}${LOCK_SUFFIX}`);
|
|
68
|
-
if (![stateDir, observerDir, log, lock].every((path) => pathIsWithin(absoluteRoot, path))) {
|
|
69
|
-
throw new SkillObserverError("OBSERVER_PATH_UNSAFE", "the observation path escapes the project root");
|
|
70
|
-
}
|
|
71
|
-
return { root: absoluteRoot, stateDir, observerDir, log, lock };
|
|
72
|
-
}
|
|
73
|
-
function pathUnsafe(path, detail) {
|
|
74
|
-
return new SkillObserverError("OBSERVER_PATH_UNSAFE", `${detail}: ${path}`);
|
|
75
|
-
}
|
|
76
|
-
function isDarwinSystemAlias(path, target) {
|
|
77
|
-
return (process.platform === "darwin" &&
|
|
78
|
-
((path === "/var" && target === "/private/var") || (path === "/tmp" && target === "/private/tmp")));
|
|
79
|
-
}
|
|
80
|
-
function assertSafeRootParents(root) {
|
|
81
|
-
let current = root;
|
|
82
|
-
for (;;) {
|
|
83
|
-
let entry;
|
|
84
|
-
try {
|
|
85
|
-
entry = lstatSync(current);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
if (errnoCode(error) !== "ENOENT") {
|
|
89
|
-
throw pathUnsafe(current, `cannot inspect a requested-root parent (${errnoCode(error) ?? "unknown error"})`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (entry?.isSymbolicLink()) {
|
|
93
|
-
if (current === root)
|
|
94
|
-
throw pathUnsafe(current, "the requested root is a symbolic link");
|
|
95
|
-
let target;
|
|
96
|
-
try {
|
|
97
|
-
target = realpathSync(current);
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
throw pathUnsafe(current, `cannot resolve a requested-root parent (${errnoCode(error) ?? "unknown error"})`);
|
|
101
|
-
}
|
|
102
|
-
if (!isDarwinSystemAlias(current, target)) {
|
|
103
|
-
throw pathUnsafe(current, "a requested-root parent is a symbolic link");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const parent = dirname(current);
|
|
107
|
-
if (parent === current)
|
|
108
|
-
return;
|
|
109
|
-
current = parent;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function rootRealPath(paths, create) {
|
|
113
|
-
let rootStat;
|
|
114
|
-
assertSafeRootParents(paths.root);
|
|
115
|
-
try {
|
|
116
|
-
rootStat = lstatSync(paths.root);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
if (errnoCode(error) !== "ENOENT") {
|
|
120
|
-
throw pathUnsafe(paths.root, `cannot inspect the project root (${errnoCode(error) ?? "unknown error"})`);
|
|
121
|
-
}
|
|
122
|
-
if (!create)
|
|
123
|
-
return undefined;
|
|
124
|
-
try {
|
|
125
|
-
mkdirSync(paths.root, { mode: 0o700, recursive: true });
|
|
126
|
-
rootStat = lstatSync(paths.root);
|
|
127
|
-
}
|
|
128
|
-
catch (createError) {
|
|
129
|
-
throw pathUnsafe(paths.root, `cannot create the project root (${errnoCode(createError) ?? "unknown error"})`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (rootStat === undefined)
|
|
133
|
-
throw pathUnsafe(paths.root, "the project root is unavailable");
|
|
134
|
-
if (rootStat.isSymbolicLink())
|
|
135
|
-
throw pathUnsafe(paths.root, "the project root is a symbolic link");
|
|
136
|
-
if (!rootStat.isDirectory())
|
|
137
|
-
throw pathUnsafe(paths.root, "the project root is not a directory");
|
|
138
|
-
try {
|
|
139
|
-
return realpathSync(paths.root);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
throw pathUnsafe(paths.root, `cannot resolve the project root (${errnoCode(error) ?? "unknown error"})`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function checkDirectory(path, rootReal, create) {
|
|
146
|
-
let directory;
|
|
147
|
-
try {
|
|
148
|
-
directory = lstatSync(path);
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
if (errnoCode(error) !== "ENOENT") {
|
|
152
|
-
throw pathUnsafe(path, `cannot inspect an observer directory (${errnoCode(error) ?? "unknown error"})`);
|
|
153
|
-
}
|
|
154
|
-
if (!create)
|
|
155
|
-
return undefined;
|
|
156
|
-
try {
|
|
157
|
-
mkdirSync(path, { mode: 0o700 });
|
|
158
|
-
directory = lstatSync(path);
|
|
159
|
-
}
|
|
160
|
-
catch (createError) {
|
|
161
|
-
if (errnoCode(createError) !== "EEXIST") {
|
|
162
|
-
throw pathUnsafe(path, `cannot create the observer directory (${errnoCode(createError) ?? "unknown error"})`);
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
directory = lstatSync(path);
|
|
166
|
-
}
|
|
167
|
-
catch (raceError) {
|
|
168
|
-
throw pathUnsafe(path, `cannot inspect the observer directory (${errnoCode(raceError) ?? "unknown error"})`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (directory === undefined)
|
|
173
|
-
throw pathUnsafe(path, "the observer directory is unavailable");
|
|
174
|
-
if (directory.isSymbolicLink())
|
|
175
|
-
throw pathUnsafe(path, "an observer parent is a symbolic link");
|
|
176
|
-
if (!directory.isDirectory())
|
|
177
|
-
throw pathUnsafe(path, "an observer parent is not a directory");
|
|
178
|
-
let real;
|
|
179
|
-
try {
|
|
180
|
-
real = realpathSync(path);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
throw pathUnsafe(path, `cannot resolve an observer parent (${errnoCode(error) ?? "unknown error"})`);
|
|
184
|
-
}
|
|
185
|
-
if (!pathIsWithin(rootReal, real))
|
|
186
|
-
throw pathUnsafe(path, "an observer parent escapes the project root");
|
|
187
|
-
return { dev: directory.dev, ino: directory.ino, birthtimeMs: directory.birthtimeMs };
|
|
188
|
-
}
|
|
189
|
-
function writablePaths(root) {
|
|
190
|
-
const paths = pathsFor(root);
|
|
191
|
-
const realRoot = rootRealPath(paths, true);
|
|
192
|
-
if (realRoot === undefined)
|
|
193
|
-
throw pathUnsafe(paths.root, "the project root is unavailable");
|
|
194
|
-
const stateIdentity = checkDirectory(paths.stateDir, realRoot, true);
|
|
195
|
-
const observerIdentity = checkDirectory(paths.observerDir, realRoot, true);
|
|
196
|
-
if (stateIdentity === undefined || observerIdentity === undefined) {
|
|
197
|
-
throw pathUnsafe(paths.observerDir, "the observer directory is unavailable");
|
|
198
|
-
}
|
|
199
|
-
return { ...paths, rootReal: realRoot, stateIdentity, observerIdentity };
|
|
200
|
-
}
|
|
201
|
-
function readablePaths(root) {
|
|
202
|
-
const paths = pathsFor(root);
|
|
203
|
-
const realRoot = rootRealPath(paths, false);
|
|
204
|
-
if (realRoot === undefined)
|
|
205
|
-
return undefined;
|
|
206
|
-
const stateIdentity = checkDirectory(paths.stateDir, realRoot, false);
|
|
207
|
-
const observerIdentity = checkDirectory(paths.observerDir, realRoot, false);
|
|
208
|
-
if (stateIdentity === undefined || observerIdentity === undefined)
|
|
209
|
-
return undefined;
|
|
210
|
-
return { ...paths, rootReal: realRoot, stateIdentity, observerIdentity };
|
|
211
|
-
}
|
|
212
|
-
function isRecord(value) {
|
|
213
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
214
|
-
}
|
|
215
|
-
function flatten(value) {
|
|
216
|
-
return value
|
|
217
|
-
.replace(/[\p{Cf}\u0080-\u009f]/gu, "")
|
|
218
|
-
.replace(/\p{Cc}/gu, " ")
|
|
219
|
-
.replace(/\s+/gu, " ")
|
|
220
|
-
.trim()
|
|
221
|
-
.slice(0, TEXT_CEILING);
|
|
222
|
-
}
|
|
223
|
-
function containsSecret(value) {
|
|
224
|
-
return redactSecrets(value) !== value || CREDENTIAL_URI_PATTERN.test(value);
|
|
225
|
-
}
|
|
226
|
-
function validateObservation(value, persisted) {
|
|
227
|
-
if (!isRecord(value)) {
|
|
228
|
-
throw new SkillObserverError("OBSERVER_SHAPE_INVALID", "an observation must be an object");
|
|
229
|
-
}
|
|
230
|
-
const fields = persisted ? PERSISTED_FIELDS : INPUT_FIELDS;
|
|
231
|
-
// The unknown key names are never echoed or persisted: a stray field name can carry secrets.
|
|
232
|
-
if (Object.keys(value).some((key) => !fields.includes(key))) {
|
|
233
|
-
throw new SkillObserverError("OBSERVER_FIELD_UNKNOWN", "an observation carries a field outside the contract");
|
|
234
|
-
}
|
|
235
|
-
if (persisted) {
|
|
236
|
-
const missing = PERSISTED_FIELDS.filter((field) => !Object.hasOwn(value, field));
|
|
237
|
-
if (missing.length > 0) {
|
|
238
|
-
throw new SkillObserverError("OBSERVER_FIELD_MISSING", `missing observation field: ${missing.join(", ")}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (!OBSERVER_SIGNALS.includes(value["signal"])) {
|
|
242
|
-
throw new SkillObserverError("OBSERVER_SIGNAL_UNKNOWN", `signal must be one of ${OBSERVER_SIGNALS.join(", ")}`);
|
|
243
|
-
}
|
|
244
|
-
if (typeof value["skillId"] !== "string" || !SKILL_ID.test(value["skillId"])) {
|
|
245
|
-
throw new SkillObserverError("OBSERVER_SKILL_ID_INVALID", "skillId must be a lowercase skill identifier");
|
|
246
|
-
}
|
|
247
|
-
for (const field of ["observed", "proposal"]) {
|
|
248
|
-
const text = value[field];
|
|
249
|
-
if (typeof text !== "string" || text.trim() === "") {
|
|
250
|
-
throw new SkillObserverError("OBSERVER_TEXT_MISSING", `${field} must be non-empty text`);
|
|
251
|
-
}
|
|
252
|
-
const spaced = text
|
|
253
|
-
.replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
254
|
-
.replace(/\s+/gu, " ")
|
|
255
|
-
.trim();
|
|
256
|
-
if (containsSecret(text) || containsSecret(flatten(text)) || containsSecret(spaced)) {
|
|
257
|
-
throw new SkillObserverError("OBSERVER_SECRET_REFUSED", `${field} looks like it carries a credential and was refused before any write`);
|
|
258
|
-
}
|
|
259
|
-
if (flatten(text).length === 0) {
|
|
260
|
-
throw new SkillObserverError("OBSERVER_TEXT_MISSING", `${field} must contain usable text`);
|
|
261
|
-
}
|
|
262
|
-
if (persisted && (text.length > TEXT_CEILING || flatten(text) !== text)) {
|
|
263
|
-
throw new SkillObserverError("OBSERVER_TEXT_NOT_CANONICAL", `${field} is not canonical bounded text`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (persisted && value["applied"] !== false) {
|
|
267
|
-
throw new SkillObserverError("OBSERVER_APPLIED_INVALID", "applied must be the boolean false");
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function logEntryState(path) {
|
|
271
|
-
let entry;
|
|
272
|
-
try {
|
|
273
|
-
entry = lstatSync(path);
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
if (errnoCode(error) === "ENOENT")
|
|
277
|
-
return "missing";
|
|
278
|
-
throw new SkillObserverError("OBSERVER_LOG_UNAVAILABLE", `cannot inspect the observation log: ${path}`);
|
|
279
|
-
}
|
|
280
|
-
if (entry === undefined) {
|
|
281
|
-
throw new SkillObserverError("OBSERVER_LOG_UNAVAILABLE", `cannot inspect the observation log: ${path}`);
|
|
282
|
-
}
|
|
283
|
-
if (entry.isSymbolicLink()) {
|
|
284
|
-
throw new SkillObserverError("OBSERVER_LOG_SYMLINK", "the observation log is a symbolic link");
|
|
285
|
-
}
|
|
286
|
-
if (!entry.isFile()) {
|
|
287
|
-
throw new SkillObserverError("OBSERVER_LOG_NOT_REGULAR", "the observation log is not a regular file");
|
|
288
|
-
}
|
|
289
|
-
if (entry.nlink !== 1)
|
|
290
|
-
throw new SkillObserverError("OBSERVER_LOG_HARD_LINK", "the observation log has hard links");
|
|
291
|
-
if ((entry.mode & 0o077) !== 0) {
|
|
292
|
-
throw new SkillObserverError("OBSERVER_LOG_PERMISSIONS", "the observation log has group or world permission bits");
|
|
293
|
-
}
|
|
294
|
-
return "present";
|
|
295
|
-
}
|
|
296
|
-
function assertRegularLogHandle(handle) {
|
|
297
|
-
const entry = fstatSync(handle);
|
|
298
|
-
if (!entry.isFile()) {
|
|
299
|
-
throw new SkillObserverError("OBSERVER_LOG_NOT_REGULAR", "the observation log is not a regular file");
|
|
300
|
-
}
|
|
301
|
-
if (entry.nlink !== 1)
|
|
302
|
-
throw new SkillObserverError("OBSERVER_LOG_HARD_LINK", "the observation log has hard links");
|
|
303
|
-
if ((entry.mode & 0o077) !== 0) {
|
|
304
|
-
throw new SkillObserverError("OBSERVER_LOG_PERMISSIONS", "the observation log has group or world permission bits");
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function removeCreatedLogAfterOpenFailure(paths, handle) {
|
|
308
|
-
let descriptor;
|
|
309
|
-
try {
|
|
310
|
-
descriptor = fstatSync(handle);
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", "the newly created observation log could not be inspected");
|
|
314
|
-
}
|
|
315
|
-
let namedPath;
|
|
316
|
-
try {
|
|
317
|
-
namedPath = lstatSync(paths.log);
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
if (errnoCode(error) === "ENOENT")
|
|
321
|
-
return;
|
|
322
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", "the newly created observation log could not be inspected");
|
|
323
|
-
}
|
|
324
|
-
if (namedPath.isSymbolicLink() ||
|
|
325
|
-
!namedPath.isFile() ||
|
|
326
|
-
namedPath.nlink !== 1 ||
|
|
327
|
-
namedPath.dev !== descriptor.dev ||
|
|
328
|
-
namedPath.ino !== descriptor.ino ||
|
|
329
|
-
namedPath.ctimeMs !== descriptor.ctimeMs ||
|
|
330
|
-
namedPath.birthtimeMs !== descriptor.birthtimeMs) {
|
|
331
|
-
throw new SkillObserverError("OBSERVER_LOG_PATH_CHANGED", "the newly created observation log path no longer identifies the opened file");
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
unlinkSync(paths.log);
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", "the newly created observation log could not be removed");
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
lstatSync(paths.log);
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
if (errnoCode(error) === "ENOENT")
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", "the newly created observation log could not be removed");
|
|
347
|
-
}
|
|
348
|
-
function assertLogPathIdentity(path, descriptor) {
|
|
349
|
-
let namedPath;
|
|
350
|
-
try {
|
|
351
|
-
namedPath = lstatSync(path);
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
throw new SkillObserverError("OBSERVER_LOG_PATH_CHANGED", "the observation log path no longer identifies the opened file");
|
|
355
|
-
}
|
|
356
|
-
if (namedPath.dev !== descriptor.dev ||
|
|
357
|
-
namedPath.ino !== descriptor.ino ||
|
|
358
|
-
namedPath.ctimeMs !== descriptor.ctimeMs ||
|
|
359
|
-
namedPath.birthtimeMs !== descriptor.birthtimeMs) {
|
|
360
|
-
throw new SkillObserverError("OBSERVER_LOG_PATH_CHANGED", "the observation log path no longer identifies the opened file");
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
function openLogForAppend(paths) {
|
|
364
|
-
assertTrustedParents(paths);
|
|
365
|
-
const state = logEntryState(paths.log);
|
|
366
|
-
assertTrustedParents(paths);
|
|
367
|
-
try {
|
|
368
|
-
const handle = openSync(paths.log, constants.O_RDWR |
|
|
369
|
-
constants.O_APPEND |
|
|
370
|
-
NO_FOLLOW |
|
|
371
|
-
(state === "missing" ? constants.O_CREAT | constants.O_EXCL : 0), 0o600);
|
|
372
|
-
try {
|
|
373
|
-
assertRegularLogHandle(handle);
|
|
374
|
-
try {
|
|
375
|
-
assertTrustedParents(paths);
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
if (state === "missing")
|
|
379
|
-
removeCreatedLogAfterOpenFailure(paths, handle);
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
382
|
-
return handle;
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
closeSync(handle);
|
|
386
|
-
throw error;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
if (error instanceof SkillObserverError)
|
|
391
|
-
throw error;
|
|
392
|
-
if (errnoCode(error) === "EEXIST" && state === "missing") {
|
|
393
|
-
try {
|
|
394
|
-
const handle = openSync(paths.log, constants.O_RDWR | constants.O_APPEND | NO_FOLLOW);
|
|
395
|
-
try {
|
|
396
|
-
assertRegularLogHandle(handle);
|
|
397
|
-
return handle;
|
|
398
|
-
}
|
|
399
|
-
catch (innerError) {
|
|
400
|
-
closeSync(handle);
|
|
401
|
-
throw innerError;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
catch (retryError) {
|
|
405
|
-
if (retryError instanceof SkillObserverError)
|
|
406
|
-
throw retryError;
|
|
407
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", `cannot open the observation log: ${paths.log}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (errnoCode(error) === "ELOOP" && state === "missing") {
|
|
411
|
-
throw new SkillObserverError("OBSERVER_LOG_SYMLINK", "the observation log became a symbolic link");
|
|
412
|
-
}
|
|
413
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", `cannot open the observation log: ${paths.log}`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
function openLogForRead(path) {
|
|
417
|
-
if (logEntryState(path) === "missing")
|
|
418
|
-
return undefined;
|
|
419
|
-
try {
|
|
420
|
-
const handle = openSync(path, constants.O_RDONLY | NO_FOLLOW);
|
|
421
|
-
try {
|
|
422
|
-
assertRegularLogHandle(handle);
|
|
423
|
-
return handle;
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
closeSync(handle);
|
|
427
|
-
throw error;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
catch (error) {
|
|
431
|
-
if (error instanceof SkillObserverError)
|
|
432
|
-
throw error;
|
|
433
|
-
if (errnoCode(error) === "ELOOP") {
|
|
434
|
-
throw new SkillObserverError("OBSERVER_LOG_SYMLINK", "the observation log became a symbolic link");
|
|
435
|
-
}
|
|
436
|
-
if (errnoCode(error) === "ENOENT")
|
|
437
|
-
return undefined;
|
|
438
|
-
throw new SkillObserverError("OBSERVER_LOG_OPEN_FAILED", `cannot open the observation log: ${path}`);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
function readLogBytes(path, handle) {
|
|
442
|
-
const before = fstatSync(handle);
|
|
443
|
-
if (before.size > MAX_LOG_BYTES) {
|
|
444
|
-
throw new SkillObserverError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log exceeds the 1 MiB limit");
|
|
445
|
-
}
|
|
446
|
-
let bytes;
|
|
447
|
-
try {
|
|
448
|
-
bytes = readFileSync(handle);
|
|
449
|
-
}
|
|
450
|
-
catch {
|
|
451
|
-
throw new SkillObserverError("OBSERVER_LOG_READ_FAILED", "the observation log could not be read");
|
|
452
|
-
}
|
|
453
|
-
if (bytes.byteLength !== before.size) {
|
|
454
|
-
throw new SkillObserverError("OBSERVER_LOG_CONCURRENT_MODIFICATION", "the observation log changed during the read");
|
|
455
|
-
}
|
|
456
|
-
let after;
|
|
457
|
-
try {
|
|
458
|
-
after = fstatSync(handle);
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
461
|
-
throw new SkillObserverError("OBSERVER_LOG_CONCURRENT_MODIFICATION", "the observation log changed during the read");
|
|
462
|
-
}
|
|
463
|
-
const changedTimestamp = (field) => {
|
|
464
|
-
const beforeValue = before[field];
|
|
465
|
-
const afterValue = after[field];
|
|
466
|
-
return typeof beforeValue === "number" && typeof afterValue === "number" && beforeValue !== afterValue;
|
|
467
|
-
};
|
|
468
|
-
if (before.dev !== after.dev ||
|
|
469
|
-
before.ino !== after.ino ||
|
|
470
|
-
before.size !== after.size ||
|
|
471
|
-
changedTimestamp("ctimeMs") ||
|
|
472
|
-
changedTimestamp("mtimeMs")) {
|
|
473
|
-
throw new SkillObserverError("OBSERVER_LOG_CONCURRENT_MODIFICATION", "the observation log changed during the read");
|
|
474
|
-
}
|
|
475
|
-
assertLogPathIdentity(path, after);
|
|
476
|
-
return bytes;
|
|
477
|
-
}
|
|
478
|
-
function parseLog(bytes) {
|
|
479
|
-
if (bytes.byteLength > MAX_LOG_BYTES) {
|
|
480
|
-
throw new SkillObserverError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log exceeds the 1 MiB limit");
|
|
481
|
-
}
|
|
482
|
-
if (bytes.byteLength === 0)
|
|
483
|
-
return [];
|
|
484
|
-
let raw;
|
|
485
|
-
try {
|
|
486
|
-
raw = UTF8_DECODER.decode(bytes);
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
throw new SkillObserverError("OBSERVER_LOG_MALFORMED", "the observation log is not valid UTF-8");
|
|
490
|
-
}
|
|
491
|
-
const content = raw.endsWith("\n") ? raw.slice(0, -1) : raw;
|
|
492
|
-
const records = [];
|
|
493
|
-
for (const [index, line] of content.split("\n").entries()) {
|
|
494
|
-
if (line.trim() === "") {
|
|
495
|
-
throw new SkillObserverError("OBSERVER_LOG_MALFORMED", `observation log record ${index + 1} is empty`);
|
|
496
|
-
}
|
|
497
|
-
let value;
|
|
498
|
-
try {
|
|
499
|
-
value = JSON.parse(line);
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
throw new SkillObserverError("OBSERVER_LOG_MALFORMED", `observation log record ${index + 1} is not JSON`);
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
validateObservation(value, true);
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
if (error instanceof SkillObserverError && error.code === "OBSERVER_SECRET_REFUSED")
|
|
509
|
-
throw error;
|
|
510
|
-
throw new SkillObserverError("OBSERVER_LOG_MALFORMED", `observation log record ${index + 1} is invalid`);
|
|
511
|
-
}
|
|
512
|
-
if (JSON.stringify(value) !== line) {
|
|
513
|
-
throw new SkillObserverError("OBSERVER_LOG_MALFORMED", `observation log record ${index + 1} is not canonical JSON`);
|
|
514
|
-
}
|
|
515
|
-
records.push(value);
|
|
516
|
-
}
|
|
517
|
-
return records;
|
|
518
|
-
}
|
|
519
|
-
function sleepForLock(milliseconds) {
|
|
520
|
-
Atomics.wait(LOCK_WAIT_BUFFER, 0, 0, milliseconds);
|
|
521
|
-
}
|
|
522
|
-
function monotonicNowMs() {
|
|
523
|
-
return performance.now();
|
|
524
|
-
}
|
|
525
|
-
function assertTrustedParents(paths) {
|
|
526
|
-
for (const [path, expected] of [
|
|
527
|
-
[paths.stateDir, paths.stateIdentity],
|
|
528
|
-
[paths.observerDir, paths.observerIdentity],
|
|
529
|
-
]) {
|
|
530
|
-
let current;
|
|
531
|
-
try {
|
|
532
|
-
current = lstatSync(path);
|
|
533
|
-
}
|
|
534
|
-
catch {
|
|
535
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "an observer parent changed during lock creation");
|
|
536
|
-
}
|
|
537
|
-
if (current.isSymbolicLink() ||
|
|
538
|
-
!current.isDirectory() ||
|
|
539
|
-
current.dev !== expected.dev ||
|
|
540
|
-
current.ino !== expected.ino ||
|
|
541
|
-
current.birthtimeMs !== expected.birthtimeMs) {
|
|
542
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "an observer parent changed during lock creation");
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
function lockDirectoryIdentity(path, rootReal) {
|
|
547
|
-
let lock;
|
|
548
|
-
try {
|
|
549
|
-
lock = lstatSync(path);
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
if (errnoCode(error) === "ENOENT")
|
|
553
|
-
return undefined;
|
|
554
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock could not be inspected");
|
|
555
|
-
}
|
|
556
|
-
if (lock.isSymbolicLink() || !lock.isDirectory()) {
|
|
557
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock is not a private directory");
|
|
558
|
-
}
|
|
559
|
-
let real;
|
|
560
|
-
try {
|
|
561
|
-
real = realpathSync(path);
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
// A contender may release the lock between lstatSync and realpathSync. Treat that
|
|
565
|
-
// disappearance as a retryable observation, not as an unsafe path.
|
|
566
|
-
if (errnoCode(error) === "ENOENT")
|
|
567
|
-
return undefined;
|
|
568
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock could not be resolved");
|
|
569
|
-
}
|
|
570
|
-
if (!pathIsWithin(rootReal, real)) {
|
|
571
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock escapes the project root");
|
|
572
|
-
}
|
|
573
|
-
return { dev: lock.dev, ino: lock.ino, birthtimeMs: lock.birthtimeMs };
|
|
574
|
-
}
|
|
575
|
-
function assertLockDirectory(path, rootReal) {
|
|
576
|
-
return lockDirectoryIdentity(path, rootReal) !== undefined;
|
|
577
|
-
}
|
|
578
|
-
function parseLockOwner(value) {
|
|
579
|
-
if (value.length === 0 || Buffer.byteLength(value, "utf8") > LOCK_OWNER_MAX_BYTES || value !== value.trim()) {
|
|
580
|
-
return undefined;
|
|
581
|
-
}
|
|
582
|
-
const legacy = /^([1-9][0-9]*):([A-Za-z0-9_-]{1,96})$/u.exec(value);
|
|
583
|
-
if (legacy !== null) {
|
|
584
|
-
const pid = Number(legacy[1]);
|
|
585
|
-
const token = legacy[2];
|
|
586
|
-
if (typeof token === "string" && Number.isSafeInteger(pid) && pid > 0 && LOCK_OWNER_TOKEN.test(token))
|
|
587
|
-
return { pid, token };
|
|
588
|
-
return undefined;
|
|
589
|
-
}
|
|
590
|
-
let parsed;
|
|
591
|
-
try {
|
|
592
|
-
parsed = JSON.parse(value);
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
return undefined;
|
|
596
|
-
}
|
|
597
|
-
if (!isRecord(parsed))
|
|
598
|
-
return undefined;
|
|
599
|
-
const pid = parsed["pid"];
|
|
600
|
-
const token = parsed["token"];
|
|
601
|
-
if (!Number.isSafeInteger(pid) ||
|
|
602
|
-
pid <= 0 ||
|
|
603
|
-
typeof token !== "string" ||
|
|
604
|
-
!LOCK_OWNER_TOKEN.test(token)) {
|
|
605
|
-
return undefined;
|
|
606
|
-
}
|
|
607
|
-
return { pid: pid, token };
|
|
608
|
-
}
|
|
609
|
-
function ownerProcessState(pid) {
|
|
610
|
-
try {
|
|
611
|
-
process.kill(pid, 0);
|
|
612
|
-
return "live";
|
|
613
|
-
}
|
|
614
|
-
catch (error) {
|
|
615
|
-
const code = errnoCode(error);
|
|
616
|
-
if (code === "ESRCH")
|
|
617
|
-
return "dead";
|
|
618
|
-
if (code === "EPERM")
|
|
619
|
-
return "live";
|
|
620
|
-
return "unknown";
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
function inspectLock(paths) {
|
|
624
|
-
const identity = lockDirectoryIdentity(paths.lock, paths.rootReal);
|
|
625
|
-
if (identity === undefined)
|
|
626
|
-
return undefined;
|
|
627
|
-
const ownerPath = join(paths.lock, LOCK_OWNER_NAME);
|
|
628
|
-
let ownerStat;
|
|
629
|
-
try {
|
|
630
|
-
ownerStat = lstatSync(ownerPath);
|
|
631
|
-
}
|
|
632
|
-
catch (error) {
|
|
633
|
-
if (errnoCode(error) === "ENOENT")
|
|
634
|
-
return { identity, owner: undefined, state: "unknown" };
|
|
635
|
-
return { identity, owner: undefined, state: "unknown" };
|
|
636
|
-
}
|
|
637
|
-
if (ownerStat.isSymbolicLink() ||
|
|
638
|
-
!ownerStat.isFile() ||
|
|
639
|
-
ownerStat.nlink !== 1 ||
|
|
640
|
-
(ownerStat.mode & 0o077) !== 0 ||
|
|
641
|
-
ownerStat.size > LOCK_OWNER_MAX_BYTES) {
|
|
642
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock owner is unsafe");
|
|
643
|
-
}
|
|
644
|
-
let raw;
|
|
645
|
-
try {
|
|
646
|
-
raw = readFileSync(ownerPath, "utf8");
|
|
647
|
-
}
|
|
648
|
-
catch {
|
|
649
|
-
return { identity, owner: undefined, state: "unknown" };
|
|
650
|
-
}
|
|
651
|
-
const owner = parseLockOwner(raw);
|
|
652
|
-
return owner === undefined
|
|
653
|
-
? { identity, owner: undefined, state: "unknown" }
|
|
654
|
-
: {
|
|
655
|
-
identity,
|
|
656
|
-
owner,
|
|
657
|
-
state: ownerProcessState(owner.pid),
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
function sameLockOwner(left, right) {
|
|
661
|
-
return left?.pid === right?.pid && left?.token === right?.token;
|
|
662
|
-
}
|
|
663
|
-
function recoverStaleLock(paths, observed) {
|
|
664
|
-
if (observed.state !== "dead" || observed.owner === undefined)
|
|
665
|
-
return false;
|
|
666
|
-
const current = inspectLock(paths);
|
|
667
|
-
if (current === undefined ||
|
|
668
|
-
current.state !== "dead" ||
|
|
669
|
-
current.identity.dev !== observed.identity.dev ||
|
|
670
|
-
current.identity.ino !== observed.identity.ino ||
|
|
671
|
-
current.identity.birthtimeMs !== observed.identity.birthtimeMs ||
|
|
672
|
-
!sameLockOwner(current.owner, observed.owner)) {
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
|
-
const displaced = `${paths.lock}.stale.${randomUUID()}`;
|
|
676
|
-
try {
|
|
677
|
-
assertTrustedParents(paths);
|
|
678
|
-
const latest = inspectLock(paths);
|
|
679
|
-
if (latest === undefined ||
|
|
680
|
-
latest.state !== "dead" ||
|
|
681
|
-
latest.identity.dev !== observed.identity.dev ||
|
|
682
|
-
latest.identity.ino !== observed.identity.ino ||
|
|
683
|
-
latest.identity.birthtimeMs !== observed.identity.birthtimeMs ||
|
|
684
|
-
!sameLockOwner(latest.owner, observed.owner)) {
|
|
685
|
-
return false;
|
|
686
|
-
}
|
|
687
|
-
renameSync(paths.lock, displaced);
|
|
688
|
-
}
|
|
689
|
-
catch (error) {
|
|
690
|
-
const code = errnoCode(error);
|
|
691
|
-
if (code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY")
|
|
692
|
-
return false;
|
|
693
|
-
if (error instanceof SkillObserverError)
|
|
694
|
-
throw error;
|
|
695
|
-
throw new SkillObserverError("OBSERVER_LOCK_RECOVERY_FAILED", "the stale observation lock could not be displaced");
|
|
696
|
-
}
|
|
697
|
-
try {
|
|
698
|
-
rmSync(displaced, { recursive: true, force: false });
|
|
699
|
-
}
|
|
700
|
-
catch {
|
|
701
|
-
throw new SkillObserverError("OBSERVER_LOCK_RECOVERY_FAILED", "the stale observation lock could not be cleaned up");
|
|
702
|
-
}
|
|
703
|
-
return true;
|
|
704
|
-
}
|
|
705
|
-
function acquireLock(paths) {
|
|
706
|
-
const lock = {
|
|
707
|
-
path: paths.lock,
|
|
708
|
-
ownerPath: join(paths.lock, LOCK_OWNER_NAME),
|
|
709
|
-
token: `${process.pid}:${randomUUID()}`,
|
|
710
|
-
paths,
|
|
711
|
-
};
|
|
712
|
-
const deadline = monotonicNowMs() + LOCK_WAIT_MS;
|
|
713
|
-
let created;
|
|
714
|
-
// Unknown owners are never reclaimed. A timeout leaves the lock for manual review.
|
|
715
|
-
for (;;) {
|
|
716
|
-
if (deadline - monotonicNowMs() <= 0) {
|
|
717
|
-
throw new SkillObserverError("OBSERVER_LOCK_TIMEOUT", "the observation log lock wait expired");
|
|
718
|
-
}
|
|
719
|
-
try {
|
|
720
|
-
assertTrustedParents(paths);
|
|
721
|
-
mkdirSync(lock.path, { mode: 0o700 });
|
|
722
|
-
const createdStat = lstatSync(lock.path);
|
|
723
|
-
if (createdStat.isSymbolicLink() || !createdStat.isDirectory()) {
|
|
724
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock is not a private directory");
|
|
725
|
-
}
|
|
726
|
-
created = {
|
|
727
|
-
dev: createdStat.dev,
|
|
728
|
-
ino: createdStat.ino,
|
|
729
|
-
birthtimeMs: createdStat.birthtimeMs,
|
|
730
|
-
};
|
|
731
|
-
assertTrustedParents(paths);
|
|
732
|
-
if (!assertLockDirectory(lock.path, paths.rootReal)) {
|
|
733
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock disappeared during creation");
|
|
734
|
-
}
|
|
735
|
-
try {
|
|
736
|
-
assertTrustedParents(paths);
|
|
737
|
-
writeFileSync(lock.ownerPath, lock.token, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
738
|
-
assertTrustedParents(paths);
|
|
739
|
-
return lock;
|
|
740
|
-
}
|
|
741
|
-
catch (error) {
|
|
742
|
-
if (error instanceof SkillObserverError)
|
|
743
|
-
throw error;
|
|
744
|
-
throw new SkillObserverError("OBSERVER_LOCK_FAILED", "the observation log lock owner could not be recorded");
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
catch (error) {
|
|
748
|
-
if (created !== undefined) {
|
|
749
|
-
try {
|
|
750
|
-
const current = lstatSync(lock.path);
|
|
751
|
-
if (current.isDirectory() &&
|
|
752
|
-
!current.isSymbolicLink() &&
|
|
753
|
-
current.dev === created.dev &&
|
|
754
|
-
current.ino === created.ino &&
|
|
755
|
-
current.birthtimeMs === created.birthtimeMs) {
|
|
756
|
-
rmSync(lock.path, { recursive: true, force: false });
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
catch {
|
|
760
|
-
// Lock cleanup cannot change the fail-closed result.
|
|
761
|
-
}
|
|
762
|
-
created = undefined;
|
|
763
|
-
}
|
|
764
|
-
if (error instanceof SkillObserverError)
|
|
765
|
-
throw error;
|
|
766
|
-
if (errnoCode(error) !== "EEXIST") {
|
|
767
|
-
assertTrustedParents(paths);
|
|
768
|
-
throw new SkillObserverError("OBSERVER_LOCK_FAILED", "the observation log lock could not be created");
|
|
769
|
-
}
|
|
770
|
-
const inspection = inspectLock(paths);
|
|
771
|
-
if (inspection === undefined) {
|
|
772
|
-
const remaining = deadline - monotonicNowMs();
|
|
773
|
-
if (remaining <= 0) {
|
|
774
|
-
throw new SkillObserverError("OBSERVER_LOCK_TIMEOUT", "the observation log lock wait expired");
|
|
775
|
-
}
|
|
776
|
-
sleepForLock(Math.min(LOCK_POLL_MS, remaining));
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
if (recoverStaleLock(paths, inspection))
|
|
780
|
-
continue;
|
|
781
|
-
assertTrustedParents(paths);
|
|
782
|
-
const remaining = deadline - monotonicNowMs();
|
|
783
|
-
if (remaining <= 0) {
|
|
784
|
-
throw new SkillObserverError("OBSERVER_LOCK_TIMEOUT", "the observation log lock wait expired");
|
|
785
|
-
}
|
|
786
|
-
sleepForLock(Math.min(LOCK_POLL_MS, remaining));
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
function assertLockOwnership(lock) {
|
|
791
|
-
try {
|
|
792
|
-
assertTrustedParents(lock.paths);
|
|
793
|
-
const lockStat = lstatSync(lock.path);
|
|
794
|
-
if (lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
795
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock changed type");
|
|
796
|
-
}
|
|
797
|
-
const ownerStat = lstatSync(lock.ownerPath);
|
|
798
|
-
if (ownerStat.isSymbolicLink() ||
|
|
799
|
-
!ownerStat.isFile() ||
|
|
800
|
-
ownerStat.nlink !== 1 ||
|
|
801
|
-
(ownerStat.mode & 0o077) !== 0) {
|
|
802
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock owner is unsafe");
|
|
803
|
-
}
|
|
804
|
-
if (readFileSync(lock.ownerPath, "utf8") !== lock.token) {
|
|
805
|
-
throw new SkillObserverError("OBSERVER_LOCK_OWNERSHIP_LOST", "the observation log lock owner changed");
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
catch (error) {
|
|
809
|
-
if (error instanceof SkillObserverError)
|
|
810
|
-
throw error;
|
|
811
|
-
throw new SkillObserverError("OBSERVER_LOCK_UNSAFE", "the observation log lock owner could not be verified");
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
function releaseLock(lock) {
|
|
815
|
-
try {
|
|
816
|
-
try {
|
|
817
|
-
lstatSync(lock.path);
|
|
818
|
-
}
|
|
819
|
-
catch (error) {
|
|
820
|
-
if (errnoCode(error) === "ENOENT")
|
|
821
|
-
return;
|
|
822
|
-
throw error;
|
|
823
|
-
}
|
|
824
|
-
assertLockOwnership(lock);
|
|
825
|
-
rmSync(lock.path, { recursive: true, force: false });
|
|
826
|
-
}
|
|
827
|
-
catch (error) {
|
|
828
|
-
if (errnoCode(error) === "ENOENT")
|
|
829
|
-
return;
|
|
830
|
-
if (error instanceof SkillObserverError)
|
|
831
|
-
throw error;
|
|
832
|
-
throw new SkillObserverError("OBSERVER_LOCK_RELEASE_FAILED", "the observation log lock could not be released");
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
function withLock(paths, action) {
|
|
836
|
-
const lock = acquireLock(paths);
|
|
837
|
-
try {
|
|
838
|
-
assertTrustedParents(paths);
|
|
839
|
-
return action();
|
|
840
|
-
}
|
|
841
|
-
finally {
|
|
842
|
-
releaseLock(lock);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
export function recordObservation(root, observation) {
|
|
846
|
-
validateObservation(observation, false);
|
|
847
|
-
const record = {
|
|
848
|
-
signal: observation["signal"],
|
|
849
|
-
skillId: observation["skillId"],
|
|
850
|
-
observed: flatten(observation["observed"]),
|
|
851
|
-
proposal: flatten(observation["proposal"]),
|
|
852
|
-
applied: false,
|
|
853
|
-
};
|
|
854
|
-
const line = `${JSON.stringify(record)}\n`;
|
|
855
|
-
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
856
|
-
if (lineBytes > MAX_LOG_BYTES) {
|
|
857
|
-
throw new SkillObserverError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation record exceeds the 1 MiB limit");
|
|
858
|
-
}
|
|
859
|
-
const paths = writablePaths(root);
|
|
860
|
-
let committed = false;
|
|
861
|
-
let recordStateUnknownError;
|
|
862
|
-
try {
|
|
863
|
-
return withLock(paths, () => {
|
|
864
|
-
const handle = openLogForAppend(paths);
|
|
865
|
-
try {
|
|
866
|
-
assertTrustedParents(paths);
|
|
867
|
-
const existing = readLogBytes(paths.log, handle);
|
|
868
|
-
parseLog(existing);
|
|
869
|
-
const separator = existing.byteLength === 0 || existing.at(-1) === 0x0a ? "" : "\n";
|
|
870
|
-
const appendLine = `${separator}${line}`;
|
|
871
|
-
const appendBytes = Buffer.byteLength(appendLine, "utf8");
|
|
872
|
-
const expectedSize = existing.byteLength + appendBytes;
|
|
873
|
-
if (expectedSize > MAX_LOG_BYTES) {
|
|
874
|
-
throw new SkillObserverError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log would exceed the 1 MiB limit");
|
|
875
|
-
}
|
|
876
|
-
const appendBuffer = Buffer.from(appendLine, "utf8");
|
|
877
|
-
assertTrustedParents(paths);
|
|
878
|
-
try {
|
|
879
|
-
let written = 0;
|
|
880
|
-
while (written < appendBuffer.byteLength) {
|
|
881
|
-
const count = writeSync(handle, appendBuffer, written, appendBuffer.byteLength - written, null);
|
|
882
|
-
if (count <= 0)
|
|
883
|
-
throw new Error("the observation log write made no progress");
|
|
884
|
-
written += count;
|
|
885
|
-
}
|
|
886
|
-
fsyncSync(handle);
|
|
887
|
-
const after = fstatSync(handle);
|
|
888
|
-
assertLogPathIdentity(paths.log, after);
|
|
889
|
-
if (after.size > MAX_LOG_BYTES) {
|
|
890
|
-
throw new SkillObserverError("OBSERVER_LOG_SIZE_EXCEEDED", "the observation log exceeds the 1 MiB limit");
|
|
891
|
-
}
|
|
892
|
-
if (after.size !== expectedSize) {
|
|
893
|
-
throw new SkillObserverError("OBSERVER_LOG_CONCURRENT_MODIFICATION", "the observation log changed during the append");
|
|
894
|
-
}
|
|
895
|
-
assertRegularLogHandle(handle);
|
|
896
|
-
assertTrustedParents(paths);
|
|
897
|
-
}
|
|
898
|
-
catch (error) {
|
|
899
|
-
try {
|
|
900
|
-
ftruncateSync(handle, existing.byteLength);
|
|
901
|
-
fsyncSync(handle);
|
|
902
|
-
}
|
|
903
|
-
catch {
|
|
904
|
-
recordStateUnknownError = new SkillObserverError(OBSERVER_RECORD_STATE_UNKNOWN, "rollback failed; the record state is unknown; read the log before retrying");
|
|
905
|
-
throw recordStateUnknownError;
|
|
906
|
-
}
|
|
907
|
-
if (error instanceof SkillObserverError)
|
|
908
|
-
throw error;
|
|
909
|
-
throw new SkillObserverError("OBSERVER_LOG_WRITE_FAILED", "the observation log could not be written");
|
|
910
|
-
}
|
|
911
|
-
committed = true;
|
|
912
|
-
return record;
|
|
913
|
-
}
|
|
914
|
-
finally {
|
|
915
|
-
closeSync(handle);
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
catch (error) {
|
|
920
|
-
if (committed) {
|
|
921
|
-
throw new SkillObserverError(OBSERVER_RECORD_COMMITTED_CLEANUP_FAILED, "the record was committed, but cleanup failed; read the log before retrying");
|
|
922
|
-
}
|
|
923
|
-
if (recordStateUnknownError !== undefined)
|
|
924
|
-
throw recordStateUnknownError;
|
|
925
|
-
throw error;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
export function readObservations(root = process.cwd()) {
|
|
929
|
-
const paths = readablePaths(root);
|
|
930
|
-
if (paths === undefined)
|
|
931
|
-
return [];
|
|
932
|
-
return withLock(paths, () => {
|
|
933
|
-
const handle = openLogForRead(paths.log);
|
|
934
|
-
if (handle === undefined) {
|
|
935
|
-
assertTrustedParents(paths);
|
|
936
|
-
return [];
|
|
937
|
-
}
|
|
938
|
-
try {
|
|
939
|
-
assertTrustedParents(paths);
|
|
940
|
-
return parseLog(readLogBytes(paths.log, handle));
|
|
941
|
-
}
|
|
942
|
-
finally {
|
|
943
|
-
closeSync(handle);
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
}
|