@anhlp/alp-core 0.0.0-beta.2 → 0.0.1-beta.3
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/dist/library-edit.d.ts +7 -4
- package/dist/library-edit.js +59 -6
- package/package.json +2 -2
- package/src/library-edit.js +60 -7
package/dist/library-edit.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Strict payload contract for opt-in project-data writes. Instructions carry prompt authority;
|
|
2
|
+
* executable configuration and references to it have no place in this policy. */
|
|
3
|
+
export function assertProjectDataContent(kind: any, content: any): void;
|
|
1
4
|
/**
|
|
2
5
|
* One entry: the version that applies (the project's, else the library's, else the
|
|
3
6
|
* built-in), or one scope's version with `scope`. `revision` goes back with a save.
|
|
@@ -52,7 +55,7 @@ export function usersOf(kind: any, name: any, { root, library, templates }?: {})
|
|
|
52
55
|
* that scope (null to create); a different current revision refuses the save.
|
|
53
56
|
* @returns {Promise<{ revision: string | null }>}
|
|
54
57
|
*/
|
|
55
|
-
export function saveEntry(kind: any, name: any, content: any, { scope, root, library, templates, revision }?: {}): Promise<{
|
|
58
|
+
export function saveEntry(kind: any, name: any, content: any, { scope, root, library, templates, revision, policy }?: {}): Promise<{
|
|
56
59
|
revision: string | null;
|
|
57
60
|
}>;
|
|
58
61
|
/**
|
|
@@ -60,16 +63,16 @@ export function saveEntry(kind: any, name: any, content: any, { scope, root, lib
|
|
|
60
63
|
* entry others refer to stays, and the error lists them.
|
|
61
64
|
* @returns {Promise<{ removed: boolean, now?: 'builtin' | 'library' }>}
|
|
62
65
|
*/
|
|
63
|
-
export function deleteEntry(kind: any, name: any, { scope, root, library, templates, revision }?: {}): Promise<{
|
|
66
|
+
export function deleteEntry(kind: any, name: any, { scope, root, library, templates, revision, policy }?: {}): Promise<{
|
|
64
67
|
removed: boolean;
|
|
65
68
|
now?: "builtin" | "library";
|
|
66
69
|
}>;
|
|
67
70
|
/** Copies the entry that applies under `from` to a new entry `to` in a scope; built-ins are copied this way. */
|
|
68
|
-
export function duplicateEntry(kind: any, from: any, to: any, { scope, root, library, templates }?: {}): Promise<{
|
|
71
|
+
export function duplicateEntry(kind: any, from: any, to: any, { scope, root, library, templates, policy }?: {}): Promise<{
|
|
69
72
|
revision: string | null;
|
|
70
73
|
}>;
|
|
71
74
|
/** Renames an entry within a scope; refused while others refer to it by its old name. */
|
|
72
|
-
export function renameEntry(kind: any, from: any, to: any, { scope, root, library, templates }?: {}): Promise<{
|
|
75
|
+
export function renameEntry(kind: any, from: any, to: any, { scope, root, library, templates, policy }?: {}): Promise<{
|
|
73
76
|
revision: string | null;
|
|
74
77
|
}>;
|
|
75
78
|
/**
|
package/dist/library-edit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// packages/core/src/library-edit.js
|
|
2
|
-
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { moveFile } from "./atomic.js";
|
|
@@ -39,6 +39,42 @@ function base(scope, { root, library }, kind) {
|
|
|
39
39
|
return fail("INVALID_SCOPE", `Scope must be library or project, not '${scope}'`);
|
|
40
40
|
}
|
|
41
41
|
var revisionOf = (content) => content === void 0 ? null : createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
|
|
42
|
+
function assertProjectDataContent(kind, content) {
|
|
43
|
+
const deny = () => fail("UNSAFE_LIBRARY_WRITE", "Project-data writes allow only instruction entries and non-executable agent configuration");
|
|
44
|
+
if (!object(content) || !["agents", "skills"].includes(kind)) deny();
|
|
45
|
+
const keys = kind === "skills" ? ["body"] : ["instructions", "config"];
|
|
46
|
+
if (Object.keys(content).some((key) => !keys.includes(key))) deny();
|
|
47
|
+
const text = kind === "skills" ? content.body : content.instructions;
|
|
48
|
+
if (typeof text !== "string" || !text.trim()) deny();
|
|
49
|
+
if (content.config !== void 0) {
|
|
50
|
+
const config = content.config;
|
|
51
|
+
if (!object(config)) deny();
|
|
52
|
+
const allowed = ["description", "model", "thinking", "context", "autocompact"];
|
|
53
|
+
if (Object.keys(config).some((key) => !allowed.includes(key))) deny();
|
|
54
|
+
for (const key of ["description", "thinking"]) if (config[key] !== void 0 && typeof config[key] !== "string") deny();
|
|
55
|
+
if (config.model !== void 0 && (typeof config.model !== "string" || !/^(codex|claude):[^\s/\\:]{1,240}$/.test(config.model))) deny();
|
|
56
|
+
for (const key of ["context", "autocompact"]) if (config[key] !== void 0 && !(typeof config[key] === "string" || typeof config[key] === "number" && Number.isFinite(config[key]))) deny();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function projectDataEntry(kind, name, { scope, root, library, templates }) {
|
|
60
|
+
if (scope !== "project" || !root || !["agents", "skills"].includes(kind)) fail("UNSAFE_LIBRARY_WRITE", "Project-data writes require project scope and agents or skills");
|
|
61
|
+
nameOf(name);
|
|
62
|
+
const directory = base(scope, { root, library }, kind);
|
|
63
|
+
await safeEntryPaths(directory, kind, name);
|
|
64
|
+
const found = await layers(kind, name, { root, library, templates });
|
|
65
|
+
if (found.library !== void 0 || found.builtin !== void 0) fail("UNSAFE_LIBRARY_WRITE", "Project-data writes cannot shadow or reveal a library or built-in entry");
|
|
66
|
+
if (found.project !== void 0) assertProjectDataContent(kind, found.project);
|
|
67
|
+
const owned = path.join(directory, kind, name);
|
|
68
|
+
const files = await readdir(owned).catch((error) => {
|
|
69
|
+
if (error.code === "ENOENT") return [];
|
|
70
|
+
throw error;
|
|
71
|
+
});
|
|
72
|
+
const allowed = new Set(FILES[kind](name).map(([file]) => path.basename(file)));
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const info = await lstat(path.join(owned, file));
|
|
75
|
+
if (!allowed.has(file) || !info.isFile() || info.isSymbolicLink() || info.nlink !== 1) fail("UNSAFE_LIBRARY_WRITE", "Project-data entries cannot contain sidecars, links or executable assets");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
42
78
|
async function safeEntryPaths(directory, kind, name) {
|
|
43
79
|
for (const [file] of FILES[kind](name)) {
|
|
44
80
|
await assertNoSymlinks(directory, path.join(kind, file));
|
|
@@ -236,9 +272,13 @@ async function writeAtomic(file, text) {
|
|
|
236
272
|
await moveFile(temporary, file);
|
|
237
273
|
}
|
|
238
274
|
var exists = (target) => stat(target).then(() => true, () => false);
|
|
239
|
-
async function saveEntry(kind, name, content, { scope, root, library, templates, revision } = {}) {
|
|
275
|
+
async function saveEntry(kind, name, content, { scope, root, library, templates, revision, policy } = {}) {
|
|
240
276
|
kindOf(kind);
|
|
241
277
|
nameOf(name);
|
|
278
|
+
if (policy === "project-data") {
|
|
279
|
+
assertProjectDataContent(kind, content);
|
|
280
|
+
await projectDataEntry(kind, name, { scope, root, library, templates });
|
|
281
|
+
}
|
|
242
282
|
const directory = base(scope, { root, library }, kind);
|
|
243
283
|
await safeEntryPaths(directory, kind, name);
|
|
244
284
|
await check(kind, name, content, { scope, root, library, templates });
|
|
@@ -260,9 +300,10 @@ async function saveEntry(kind, name, content, { scope, root, library, templates,
|
|
|
260
300
|
}
|
|
261
301
|
return { revision: revisionOf(await readLayer(kind, name, directory)) };
|
|
262
302
|
}
|
|
263
|
-
async function deleteEntry(kind, name, { scope, root, library, templates, revision } = {}) {
|
|
303
|
+
async function deleteEntry(kind, name, { scope, root, library, templates, revision, policy } = {}) {
|
|
264
304
|
kindOf(kind);
|
|
265
305
|
nameOf(name);
|
|
306
|
+
if (policy === "project-data") await projectDataEntry(kind, name, { scope, root, library, templates });
|
|
266
307
|
const directory = base(scope, { root, library }, kind);
|
|
267
308
|
const current = await readLayer(kind, name, directory);
|
|
268
309
|
if (current === void 0) fail("NOT_FOUND", `No ${kind} entry '${name}' in the ${scope}`);
|
|
@@ -278,14 +319,25 @@ async function deleteEntry(kind, name, { scope, root, library, templates, revisi
|
|
|
278
319
|
const now = scope === "project" && found.library !== void 0 ? "library" : "builtin";
|
|
279
320
|
return { removed: true, ...fallback !== void 0 ? { now } : {} };
|
|
280
321
|
}
|
|
281
|
-
async function duplicateEntry(kind, from, to, { scope, root, library, templates } = {}) {
|
|
322
|
+
async function duplicateEntry(kind, from, to, { scope, root, library, templates, policy } = {}) {
|
|
323
|
+
kindOf(kind);
|
|
324
|
+
nameOf(from);
|
|
325
|
+
nameOf(to);
|
|
326
|
+
if (policy === "project-data") {
|
|
327
|
+
await projectDataEntry(kind, from, { scope, root, library, templates });
|
|
328
|
+
await projectDataEntry(kind, to, { scope, root, library, templates });
|
|
329
|
+
}
|
|
282
330
|
const { content } = await getEntry(kind, from, { root, library, templates });
|
|
283
|
-
return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null });
|
|
331
|
+
return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null, policy });
|
|
284
332
|
}
|
|
285
|
-
async function renameEntry(kind, from, to, { scope, root, library, templates } = {}) {
|
|
333
|
+
async function renameEntry(kind, from, to, { scope, root, library, templates, policy } = {}) {
|
|
286
334
|
kindOf(kind);
|
|
287
335
|
nameOf(from);
|
|
288
336
|
nameOf(to);
|
|
337
|
+
if (policy === "project-data") {
|
|
338
|
+
await projectDataEntry(kind, from, { scope, root, library, templates });
|
|
339
|
+
await projectDataEntry(kind, to, { scope, root, library, templates });
|
|
340
|
+
}
|
|
289
341
|
const directory = base(scope, { root, library }, kind);
|
|
290
342
|
const content = await readLayer(kind, from, directory);
|
|
291
343
|
if (content === void 0) fail("NOT_FOUND", `No ${kind} entry '${from}' in the ${scope}`);
|
|
@@ -303,6 +355,7 @@ async function renameEntry(kind, from, to, { scope, root, library, templates } =
|
|
|
303
355
|
}
|
|
304
356
|
export {
|
|
305
357
|
EDIT_KINDS,
|
|
358
|
+
assertProjectDataContent,
|
|
306
359
|
deleteEntry,
|
|
307
360
|
duplicateEntry,
|
|
308
361
|
getEntry,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ALP core module",
|
|
6
6
|
"files": [
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
"node": ">=22.12"
|
|
285
285
|
},
|
|
286
286
|
"dependencies": {
|
|
287
|
-
"@anhlp/alp-protocol": "0.0.
|
|
287
|
+
"@anhlp/alp-protocol": "0.0.1-beta.3"
|
|
288
288
|
},
|
|
289
289
|
"scripts": {
|
|
290
290
|
"build": "node ../../scripts/build-workspace.mjs core",
|
package/src/library-edit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { lstat, mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { moveFile } from './atomic.js';
|
|
@@ -60,6 +60,46 @@ function base(scope, { root, library }, kind) {
|
|
|
60
60
|
}
|
|
61
61
|
const revisionOf = content => content === undefined ? null : createHash('sha256').update(JSON.stringify(content)).digest('hex').slice(0, 16);
|
|
62
62
|
|
|
63
|
+
/** Strict payload contract for opt-in project-data writes. Instructions carry prompt authority;
|
|
64
|
+
* executable configuration and references to it have no place in this policy. */
|
|
65
|
+
export function assertProjectDataContent(kind, content) {
|
|
66
|
+
const deny = () => fail('UNSAFE_LIBRARY_WRITE', 'Project-data writes allow only instruction entries and non-executable agent configuration');
|
|
67
|
+
if (!object(content) || !['agents', 'skills'].includes(kind)) deny();
|
|
68
|
+
const keys = kind === 'skills' ? ['body'] : ['instructions', 'config'];
|
|
69
|
+
if (Object.keys(content).some(key => !keys.includes(key))) deny();
|
|
70
|
+
const text = kind === 'skills' ? content.body : content.instructions;
|
|
71
|
+
if (typeof text !== 'string' || !text.trim()) deny();
|
|
72
|
+
if (content.config !== undefined) {
|
|
73
|
+
const config = content.config;
|
|
74
|
+
if (!object(config)) deny();
|
|
75
|
+
const allowed = ['description', 'model', 'thinking', 'context', 'autocompact'];
|
|
76
|
+
if (Object.keys(config).some(key => !allowed.includes(key))) deny();
|
|
77
|
+
for (const key of ['description', 'thinking']) if (config[key] !== undefined && typeof config[key] !== 'string') deny();
|
|
78
|
+
if (config.model !== undefined && (typeof config.model !== 'string' || !/^(codex|claude):[^\s/\\:]{1,240}$/.test(config.model))) deny();
|
|
79
|
+
for (const key of ['context', 'autocompact']) if (config[key] !== undefined && !(typeof config[key] === 'string' || typeof config[key] === 'number' && Number.isFinite(config[key]))) deny();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Authoritative mutation preflight. Reject sidecars and lower-layer collisions rather than
|
|
84
|
+
* moving executable assets or revealing a different host configuration. Like safeEntryPaths,
|
|
85
|
+
* this protects static repository input, not a concurrently malicious same-user process. */
|
|
86
|
+
async function projectDataEntry(kind, name, { scope, root, library, templates }) {
|
|
87
|
+
if (scope !== 'project' || !root || !['agents', 'skills'].includes(kind)) fail('UNSAFE_LIBRARY_WRITE', 'Project-data writes require project scope and agents or skills');
|
|
88
|
+
nameOf(name);
|
|
89
|
+
const directory = base(scope, { root, library }, kind);
|
|
90
|
+
await safeEntryPaths(directory, kind, name);
|
|
91
|
+
const found = await layers(kind, name, { root, library, templates });
|
|
92
|
+
if (found.library !== undefined || found.builtin !== undefined) fail('UNSAFE_LIBRARY_WRITE', 'Project-data writes cannot shadow or reveal a library or built-in entry');
|
|
93
|
+
if (found.project !== undefined) assertProjectDataContent(kind, found.project);
|
|
94
|
+
const owned = path.join(directory, kind, name);
|
|
95
|
+
const files = await readdir(owned).catch(error => { if (error.code === 'ENOENT') return []; throw error; });
|
|
96
|
+
const allowed = new Set(FILES[kind](name).map(([file]) => path.basename(file)));
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
const info = await lstat(path.join(owned, file));
|
|
99
|
+
if (!allowed.has(file) || !info.isFile() || info.isSymbolicLink() || info.nlink !== 1) fail('UNSAFE_LIBRARY_WRITE', 'Project-data entries cannot contain sidecars, links or executable assets');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
63
103
|
/** Reject repository-controlled links before any entry read or mutation. This is
|
|
64
104
|
* a preflight for static repository input, not a sandbox against same-user races. */
|
|
65
105
|
async function safeEntryPaths(directory, kind, name) {
|
|
@@ -274,8 +314,12 @@ const exists = target => stat(target).then(() => true, () => false);
|
|
|
274
314
|
* that scope (null to create); a different current revision refuses the save.
|
|
275
315
|
* @returns {Promise<{ revision: string | null }>}
|
|
276
316
|
*/
|
|
277
|
-
export async function saveEntry(kind, name, content, { scope, root, library, templates, revision } = {}) {
|
|
317
|
+
export async function saveEntry(kind, name, content, { scope, root, library, templates, revision, policy } = {}) {
|
|
278
318
|
kindOf(kind); nameOf(name);
|
|
319
|
+
if (policy === 'project-data') {
|
|
320
|
+
assertProjectDataContent(kind, content);
|
|
321
|
+
await projectDataEntry(kind, name, { scope, root, library, templates });
|
|
322
|
+
}
|
|
279
323
|
const directory = base(scope, { root, library }, kind);
|
|
280
324
|
await safeEntryPaths(directory, kind, name);
|
|
281
325
|
await check(kind, name, content, { scope, root, library, templates });
|
|
@@ -300,8 +344,9 @@ export async function saveEntry(kind, name, content, { scope, root, library, tem
|
|
|
300
344
|
* entry others refer to stays, and the error lists them.
|
|
301
345
|
* @returns {Promise<{ removed: boolean, now?: 'builtin' | 'library' }>}
|
|
302
346
|
*/
|
|
303
|
-
export async function deleteEntry(kind, name, { scope, root, library, templates, revision } = {}) {
|
|
347
|
+
export async function deleteEntry(kind, name, { scope, root, library, templates, revision, policy } = {}) {
|
|
304
348
|
kindOf(kind); nameOf(name);
|
|
349
|
+
if (policy === 'project-data') await projectDataEntry(kind, name, { scope, root, library, templates });
|
|
305
350
|
const directory = base(scope, { root, library }, kind);
|
|
306
351
|
const current = await readLayer(kind, name, directory);
|
|
307
352
|
if (current === undefined) fail('NOT_FOUND', `No ${kind} entry '${name}' in the ${scope}`);
|
|
@@ -320,14 +365,23 @@ export async function deleteEntry(kind, name, { scope, root, library, templates,
|
|
|
320
365
|
}
|
|
321
366
|
|
|
322
367
|
/** Copies the entry that applies under `from` to a new entry `to` in a scope; built-ins are copied this way. */
|
|
323
|
-
export async function duplicateEntry(kind, from, to, { scope, root, library, templates } = {}) {
|
|
368
|
+
export async function duplicateEntry(kind, from, to, { scope, root, library, templates, policy } = {}) {
|
|
369
|
+
kindOf(kind); nameOf(from); nameOf(to);
|
|
370
|
+
if (policy === 'project-data') {
|
|
371
|
+
await projectDataEntry(kind, from, { scope, root, library, templates });
|
|
372
|
+
await projectDataEntry(kind, to, { scope, root, library, templates });
|
|
373
|
+
}
|
|
324
374
|
const { content } = await getEntry(kind, from, { root, library, templates });
|
|
325
|
-
return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null });
|
|
375
|
+
return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null, policy });
|
|
326
376
|
}
|
|
327
377
|
|
|
328
378
|
/** Renames an entry within a scope; refused while others refer to it by its old name. */
|
|
329
|
-
export async function renameEntry(kind, from, to, { scope, root, library, templates } = {}) {
|
|
379
|
+
export async function renameEntry(kind, from, to, { scope, root, library, templates, policy } = {}) {
|
|
330
380
|
kindOf(kind); nameOf(from); nameOf(to);
|
|
381
|
+
if (policy === 'project-data') {
|
|
382
|
+
await projectDataEntry(kind, from, { scope, root, library, templates });
|
|
383
|
+
await projectDataEntry(kind, to, { scope, root, library, templates });
|
|
384
|
+
}
|
|
331
385
|
const directory = base(scope, { root, library }, kind);
|
|
332
386
|
const content = await readLayer(kind, from, directory);
|
|
333
387
|
if (content === undefined) fail('NOT_FOUND', `No ${kind} entry '${from}' in the ${scope}`);
|
|
@@ -343,4 +397,3 @@ export async function renameEntry(kind, from, to, { scope, root, library, templa
|
|
|
343
397
|
}
|
|
344
398
|
return { revision: revisionOf(await readLayer(kind, to, directory)) };
|
|
345
399
|
}
|
|
346
|
-
|