@nfinitmonkeys/clan-engine 0.3.0 → 0.4.1
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/brain-build.d.ts +41 -8
- package/dist/brain-build.js +137 -16
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +12 -1
- package/dist/types.js +1 -1
- package/package.json +5 -2
package/dist/brain-build.d.ts
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
* an opt-in HOOK (opts.curate): the caller brings the LLM, the engine stays
|
|
14
14
|
* zero-LLM and re-gates everything the curator returns.
|
|
15
15
|
*/
|
|
16
|
-
import type { ClanIdentity, GeneratedFile, BrainCurator } from './types.js';
|
|
16
|
+
import type { ClanIdentity, GeneratedFile, BrainCurator, BrainOwner } from './types.js';
|
|
17
|
+
/** Fence tag that holds a serialized EncryptedPage inside a private page's body.
|
|
18
|
+
* A private page keeps its Markdown frontmatter (with `private: true`) so the
|
|
19
|
+
* vault stays browsable/round-trippable, but the body is replaced by this fenced
|
|
20
|
+
* JSON block — the plaintext body never touches disk. */
|
|
21
|
+
export declare const ENCRYPTED_FENCE = "clan-encrypted";
|
|
17
22
|
export interface HeldBack {
|
|
18
23
|
from: string;
|
|
19
24
|
reasons: string[];
|
|
@@ -34,16 +39,44 @@ export interface BrainBuildOpts {
|
|
|
34
39
|
* scanSensitive + create-only; a throwing curator falls back to the
|
|
35
40
|
* mechanical pages with a note in the report. */
|
|
36
41
|
curate?: BrainCurator;
|
|
42
|
+
/** The clan itself as an encrypted-brain recipient (alphaId + X25519 public
|
|
43
|
+
* key). Required to WRITE a page whose source frontmatter carries
|
|
44
|
+
* `private: true`: such a page is sanitizer-EXEMPT (that is the whole point)
|
|
45
|
+
* but its body is encrypted to this owner BEFORE any write. Without an owner,
|
|
46
|
+
* a private page is HELD BACK ("run clan keys init first") and its plaintext
|
|
47
|
+
* is NEVER written. The private key never reaches the engine. */
|
|
48
|
+
owner?: BrainOwner;
|
|
37
49
|
}
|
|
50
|
+
/** True when a rendered page's content carries an encrypted (private) body —
|
|
51
|
+
* i.e. it holds a fenced `clan-encrypted` block. Used by scanners/tests to
|
|
52
|
+
* assert a private page never persisted plaintext. */
|
|
53
|
+
export declare function isPrivatePageContent(content: string): boolean;
|
|
54
|
+
/** Extract the EncryptedPage JSON from a private page's rendered content (the
|
|
55
|
+
* fenced `clan-encrypted` block). Returns null when the page is not private /
|
|
56
|
+
* the fence is absent or unparseable. The inverse of the write-time encode. */
|
|
57
|
+
export declare function decodePrivatePage(content: string): unknown | null;
|
|
38
58
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
59
|
+
* Encrypt a private page's plaintext body to the owner and render the full page
|
|
60
|
+
* content (frontmatter preserved, body replaced by a fenced `clan-encrypted`
|
|
61
|
+
* block). clan-crypto is imported lazily so the engine core loads without it;
|
|
62
|
+
* the owner's private key is NEVER involved — only its public identity. Throws
|
|
63
|
+
* if clan-crypto is unavailable so the caller can hold the page back rather than
|
|
64
|
+
* write plaintext.
|
|
65
|
+
*/
|
|
66
|
+
export declare function encryptPrivatePage(frontmatterBlock: string, title: string, plaintextBody: string, owner: BrainOwner): Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Mine existing files into brain pages. Every NON-PRIVATE candidate is scanned;
|
|
69
|
+
* flagged ones are held back. A candidate whose source frontmatter declares
|
|
70
|
+
* `private: true` is sanitizer-EXEMPT but is ENCRYPTED to opts.owner before its
|
|
71
|
+
* page is emitted (its plaintext body never appears on disk); without an owner
|
|
72
|
+
* it is HELD BACK ("run clan keys init first") — plaintext is never written.
|
|
73
|
+
*
|
|
74
|
+
* The call stays synchronous ONLY when there is neither a curator nor a private
|
|
75
|
+
* candidate (existing callers unchanged); a curator OR any private page (which
|
|
76
|
+
* needs async encryption) makes it return a Promise of the (re-gated) result.
|
|
43
77
|
*/
|
|
44
78
|
export declare function brainBuild(repo: string, identity: ClanIdentity, opts?: BrainBuildOpts & {
|
|
45
79
|
curate?: undefined;
|
|
80
|
+
owner?: undefined;
|
|
46
81
|
}): BrainBuildResult;
|
|
47
|
-
export declare function brainBuild(repo: string, identity: ClanIdentity, opts: BrainBuildOpts
|
|
48
|
-
curate: BrainCurator;
|
|
49
|
-
}): Promise<BrainBuildResult>;
|
|
82
|
+
export declare function brainBuild(repo: string, identity: ClanIdentity, opts: BrainBuildOpts): BrainBuildResult | Promise<BrainBuildResult>;
|
package/dist/brain-build.js
CHANGED
|
@@ -16,8 +16,14 @@
|
|
|
16
16
|
import { readFileSync, existsSync, readdirSync, lstatSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { homedir } from 'os';
|
|
19
|
+
import YAML from 'yaml';
|
|
19
20
|
import { brainRoot, markerLines } from './templates.js';
|
|
20
21
|
import { scanSensitive } from './sanitize.js';
|
|
22
|
+
/** Fence tag that holds a serialized EncryptedPage inside a private page's body.
|
|
23
|
+
* A private page keeps its Markdown frontmatter (with `private: true`) so the
|
|
24
|
+
* vault stays browsable/round-trippable, but the body is replaced by this fenced
|
|
25
|
+
* JSON block — the plaintext body never touches disk. */
|
|
26
|
+
export const ENCRYPTED_FENCE = 'clan-encrypted';
|
|
21
27
|
function slugify(s) {
|
|
22
28
|
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'untitled';
|
|
23
29
|
}
|
|
@@ -31,6 +37,66 @@ function read(p) { try {
|
|
|
31
37
|
catch {
|
|
32
38
|
return null;
|
|
33
39
|
} }
|
|
40
|
+
/** Read the YAML frontmatter of a source file (between the first two `---`).
|
|
41
|
+
* Returns null when there is no leading frontmatter block or it won't parse. */
|
|
42
|
+
function frontmatter(content) {
|
|
43
|
+
if (!content.startsWith('---'))
|
|
44
|
+
return null;
|
|
45
|
+
const end = content.indexOf('\n---', 3);
|
|
46
|
+
if (end === -1)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
const doc = YAML.parse(content.slice(3, end));
|
|
50
|
+
return doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** True when a source file's frontmatter declares `private: true`. A private
|
|
57
|
+
* page is exempt from sanitizer hold-back but MUST be encrypted before write. */
|
|
58
|
+
function declaredPrivate(content) {
|
|
59
|
+
return frontmatter(content)?.private === true;
|
|
60
|
+
}
|
|
61
|
+
/** Serialize an EncryptedPage into a fenced `clan-encrypted` block. This is the
|
|
62
|
+
* on-disk body of a private page: the plaintext body is replaced by this fence
|
|
63
|
+
* so no plaintext ever touches the vault. Round-trippable via decodePrivatePage. */
|
|
64
|
+
function fenceEncrypted(page) {
|
|
65
|
+
return '```' + ENCRYPTED_FENCE + '\n' + JSON.stringify(page) + '\n```';
|
|
66
|
+
}
|
|
67
|
+
/** True when a rendered page's content carries an encrypted (private) body —
|
|
68
|
+
* i.e. it holds a fenced `clan-encrypted` block. Used by scanners/tests to
|
|
69
|
+
* assert a private page never persisted plaintext. */
|
|
70
|
+
export function isPrivatePageContent(content) {
|
|
71
|
+
return new RegExp('```' + ENCRYPTED_FENCE + '\\b').test(content);
|
|
72
|
+
}
|
|
73
|
+
/** Extract the EncryptedPage JSON from a private page's rendered content (the
|
|
74
|
+
* fenced `clan-encrypted` block). Returns null when the page is not private /
|
|
75
|
+
* the fence is absent or unparseable. The inverse of the write-time encode. */
|
|
76
|
+
export function decodePrivatePage(content) {
|
|
77
|
+
const m = new RegExp('```' + ENCRYPTED_FENCE + '\\s*\\n([\\s\\S]*?)\\n```').exec(content);
|
|
78
|
+
if (!m)
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(m[1]);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Encrypt a private page's plaintext body to the owner and render the full page
|
|
89
|
+
* content (frontmatter preserved, body replaced by a fenced `clan-encrypted`
|
|
90
|
+
* block). clan-crypto is imported lazily so the engine core loads without it;
|
|
91
|
+
* the owner's private key is NEVER involved — only its public identity. Throws
|
|
92
|
+
* if clan-crypto is unavailable so the caller can hold the page back rather than
|
|
93
|
+
* write plaintext.
|
|
94
|
+
*/
|
|
95
|
+
export async function encryptPrivatePage(frontmatterBlock, title, plaintextBody, owner) {
|
|
96
|
+
const crypto = await import('@nfinitmonkeys/clan-crypto');
|
|
97
|
+
const page = crypto.encryptPage(plaintextBody, [{ alphaId: owner.alphaId, spkiB64: owner.spkiB64 }]);
|
|
98
|
+
return `---\n${frontmatterBlock}\n---\n\n# ${title}\n\n${fenceEncrypted(page)}\n`;
|
|
99
|
+
}
|
|
34
100
|
function listMd(dir, depth = 3) {
|
|
35
101
|
if (depth < 0 || !existsSync(dir))
|
|
36
102
|
return [];
|
|
@@ -89,7 +155,7 @@ function mineDocs(repo) {
|
|
|
89
155
|
continue;
|
|
90
156
|
const rel = f.slice(repo.length + 1);
|
|
91
157
|
const title = (/^#\s+(.+)$/m.exec(body)?.[1] ?? rel).trim();
|
|
92
|
-
out.push({ category: 'sources', slug: slugify(rel.replace(/\.md$/, '')), title, type: 'source', from: rel, body });
|
|
158
|
+
out.push({ category: 'sources', slug: slugify(rel.replace(/\.md$/, '')), title, type: 'source', from: rel, body, private: declaredPrivate(body) });
|
|
93
159
|
}
|
|
94
160
|
return out;
|
|
95
161
|
}
|
|
@@ -185,10 +251,24 @@ function mineMemory(repo, home) {
|
|
|
185
251
|
if (!body || body.length < 40)
|
|
186
252
|
continue;
|
|
187
253
|
const title = (/^name:\s*(.+)$/m.exec(body)?.[1] ?? base.replace(/\.md$/, '')).trim();
|
|
188
|
-
out.push({ category: 'sources', slug: 'memory--' + slugify(base.replace(/\.md$/, '')), title, type: 'source', from: `session-memory/${base}`, body });
|
|
254
|
+
out.push({ category: 'sources', slug: 'memory--' + slugify(base.replace(/\.md$/, '')), title, type: 'source', from: `session-memory/${base}`, body, private: declaredPrivate(body) });
|
|
189
255
|
}
|
|
190
256
|
return out;
|
|
191
257
|
}
|
|
258
|
+
/** Frontmatter block for a mined page. Private pages carry `private: true` so a
|
|
259
|
+
* later query knows to decrypt; the body is JSON-quoted scalars only. */
|
|
260
|
+
function pageFrontmatter(c, title) {
|
|
261
|
+
const lines = [
|
|
262
|
+
`name: ${JSON.stringify(title)}`,
|
|
263
|
+
`description: ${JSON.stringify('mined from ' + c.from)}`,
|
|
264
|
+
`type: ${JSON.stringify(c.type)}`,
|
|
265
|
+
`source: ${JSON.stringify(c.from)}`,
|
|
266
|
+
];
|
|
267
|
+
if (c.private)
|
|
268
|
+
lines.push('private: true');
|
|
269
|
+
lines.push(markerLines());
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|
|
192
272
|
export function brainBuild(repo, identity, opts = {}) {
|
|
193
273
|
const home = opts.home ?? homedir();
|
|
194
274
|
const candidates = [
|
|
@@ -203,15 +283,22 @@ export function brainBuild(repo, identity, opts = {}) {
|
|
|
203
283
|
const root = brainRoot(identity);
|
|
204
284
|
const pages = [];
|
|
205
285
|
const heldBack = [];
|
|
286
|
+
const notes = [];
|
|
206
287
|
const sourceNotes = [];
|
|
207
288
|
const seen = new Set();
|
|
289
|
+
// Private candidates that survived path/create-only gating, deferred for async
|
|
290
|
+
// encryption. { path, title, frontmatter, body } — body encrypted before write.
|
|
291
|
+
const privateJobs = [];
|
|
208
292
|
for (const c of candidates) {
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
293
|
+
// A private page is EXEMPT from the sanitizer hold-back (that is the point —
|
|
294
|
+
// it will be encrypted, never written in the clear). Non-private pages are
|
|
295
|
+
// scanned on body + title + source path exactly as before.
|
|
296
|
+
if (!c.private) {
|
|
297
|
+
const scan = scanSensitive(`${c.body}\n${c.title}\n${c.from}`, { hipaa: opts.hipaa });
|
|
298
|
+
if (scan.flagged) {
|
|
299
|
+
heldBack.push({ from: c.from, reasons: scan.reasons });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
215
302
|
}
|
|
216
303
|
// Unique slug within its category (suffix on collision — never silently drop).
|
|
217
304
|
let path = `${root}/wiki/${c.category}/${c.slug}.md`;
|
|
@@ -222,29 +309,63 @@ export function brainBuild(repo, identity, opts = {}) {
|
|
|
222
309
|
// existing brain page, so a programmatic caller can't overwrite memory.
|
|
223
310
|
if (existsSync(join(repo, path)))
|
|
224
311
|
continue;
|
|
312
|
+
const title = oneLine(c.title);
|
|
313
|
+
const fm = pageFrontmatter(c, title);
|
|
314
|
+
if (c.private) {
|
|
315
|
+
if (!opts.owner) {
|
|
316
|
+
// No clan keypair → refuse to write plaintext. Hold it back with a clear
|
|
317
|
+
// remediation, exactly like a sanitizer trip.
|
|
318
|
+
heldBack.push({ from: c.from, reasons: ['private-page-needs-key: run `clan keys init` first'] });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
privateJobs.push({ path, title, fm, body: c.body, from: c.from });
|
|
322
|
+
sourceNotes.push(`${path} ← ${c.from} (private)`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
225
325
|
// Safe frontmatter: JSON-quote scalars so a title/path containing ':' '#'
|
|
226
326
|
// a newline, or a leading '-' can't break the YAML.
|
|
227
|
-
const title = oneLine(c.title);
|
|
228
327
|
pages.push({
|
|
229
328
|
path,
|
|
230
329
|
managed: true,
|
|
231
|
-
content: `---\
|
|
330
|
+
content: `---\n${fm}\n---\n\n# ${title}\n\n${c.body}\n`,
|
|
232
331
|
});
|
|
233
332
|
sourceNotes.push(`${path} ← ${c.from}`);
|
|
234
333
|
}
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
334
|
+
const scanned = candidates.length;
|
|
335
|
+
// Assemble: encrypt any private jobs (async), then optionally curate.
|
|
336
|
+
const needsAsync = privateJobs.length > 0 || !!opts.curate;
|
|
337
|
+
if (!needsAsync)
|
|
338
|
+
return { pages, heldBack, scanned, notes };
|
|
339
|
+
return (async () => {
|
|
340
|
+
for (const job of privateJobs) {
|
|
341
|
+
try {
|
|
342
|
+
const content = await encryptPrivatePage(job.fm, job.title, job.body, opts.owner);
|
|
343
|
+
pages.push({ path: job.path, managed: true, content });
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
// clan-crypto unavailable → hold the page back rather than write plaintext.
|
|
347
|
+
heldBack.push({ from: job.from, reasons: [`private-page-encrypt-failed: ${e.message}`] });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const mech = { pages, heldBack, scanned, notes };
|
|
351
|
+
if (!opts.curate)
|
|
352
|
+
return mech;
|
|
353
|
+
return runCurate(repo, identity, opts, mech, sourceNotes);
|
|
354
|
+
})();
|
|
239
355
|
}
|
|
240
356
|
/** Apply the caller's curation hook, then re-gate its output. The curated set
|
|
241
357
|
* REPLACES the mechanical pages, but the invariants survive curation:
|
|
242
358
|
* scanSensitive on every body+path, create-only, and vault-root confinement
|
|
243
359
|
* (an LLM cannot aim a write at .claude/, .mcp.json, or outside the brain). */
|
|
244
360
|
async function runCurate(repo, identity, opts, mech, sourceNotes) {
|
|
361
|
+
// Encrypted (private) pages are NEVER handed to the curator — the LLM can't
|
|
362
|
+
// (and must not) rewrite ciphertext. They pass through verbatim; only the
|
|
363
|
+
// plaintext mechanical pages are offered for curation.
|
|
364
|
+
const encrypted = mech.pages.filter(p => isPrivatePageContent(p.content));
|
|
365
|
+
const plain = mech.pages.filter(p => !isPrivatePageContent(p.content));
|
|
245
366
|
let curated;
|
|
246
367
|
try {
|
|
247
|
-
curated = await opts.curate({ pages:
|
|
368
|
+
curated = await opts.curate({ pages: plain, identity, sourceNotes });
|
|
248
369
|
if (!Array.isArray(curated))
|
|
249
370
|
throw new Error('curator returned non-array');
|
|
250
371
|
}
|
|
@@ -252,7 +373,7 @@ async function runCurate(repo, identity, opts, mech, sourceNotes) {
|
|
|
252
373
|
return { ...mech, notes: [...mech.notes, `curation failed: ${e.message}; mechanical pages used`] };
|
|
253
374
|
}
|
|
254
375
|
const root = brainRoot(identity);
|
|
255
|
-
const pages = [];
|
|
376
|
+
const pages = [...encrypted];
|
|
256
377
|
const heldBack = [...mech.heldBack];
|
|
257
378
|
const notes = [...mech.notes];
|
|
258
379
|
let outside = 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* from here, so their output can never drift again. The conversational wizard
|
|
7
7
|
* drives these same verbs; the LLM improvises the conversation, never the writes.
|
|
8
8
|
*/
|
|
9
|
-
export { ENGINE_VERSION, LAYOUT_VERSION, GENERATED_BY, type Tier, type Mode, type ClanIdentity, type GeneratedFile, type PlanAction, type Plan, type InspectResult, type InventoryItem, type NodeState, type McpServerInfo, type Journal, type JournalOp, type ApplyResult, type DoctorCheck, type BrainCurateInput, type BrainCurator, } from './types.js';
|
|
9
|
+
export { ENGINE_VERSION, LAYOUT_VERSION, GENERATED_BY, type Tier, type Mode, type ClanIdentity, type GeneratedFile, type PlanAction, type Plan, type InspectResult, type InventoryItem, type NodeState, type McpServerInfo, type Journal, type JournalOp, type ApplyResult, type DoctorCheck, type BrainCurateInput, type BrainCurator, type BrainOwner, } from './types.js';
|
|
10
10
|
export { inspect } from './inspect.js';
|
|
11
11
|
export { reconcile, type ReconcileOptions } from './reconcile.js';
|
|
12
12
|
export { apply, type ApplyOptions } from './apply.js';
|
|
13
13
|
export { undo, listJournals, type UndoResult } from './undo.js';
|
|
14
14
|
export { doctor } from './doctor.js';
|
|
15
15
|
export { scanInventory, writeInventoryManifest } from './inventory.js';
|
|
16
|
-
export { brainBuild, type BrainBuildResult, type BrainBuildOpts, type HeldBack } from './brain-build.js';
|
|
16
|
+
export { brainBuild, encryptPrivatePage, decodePrivatePage, isPrivatePageContent, ENCRYPTED_FENCE, type BrainBuildResult, type BrainBuildOpts, type HeldBack } from './brain-build.js';
|
|
17
17
|
export { scanSensitive, type ScanResult } from './sanitize.js';
|
|
18
18
|
export { planMcpJson, planDistrictMcpJson, jungleLocalServer, districtBoxCreds, type JungleCreds } from './mcp.js';
|
|
19
19
|
export { planBrainHook } from './hooks.js';
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { apply } from './apply.js';
|
|
|
13
13
|
export { undo, listJournals } from './undo.js';
|
|
14
14
|
export { doctor } from './doctor.js';
|
|
15
15
|
export { scanInventory, writeInventoryManifest } from './inventory.js';
|
|
16
|
-
export { brainBuild } from './brain-build.js';
|
|
16
|
+
export { brainBuild, encryptPrivatePage, decodePrivatePage, isPrivatePageContent, ENCRYPTED_FENCE } from './brain-build.js';
|
|
17
17
|
export { scanSensitive } from './sanitize.js';
|
|
18
18
|
export { planMcpJson, planDistrictMcpJson, jungleLocalServer, districtBoxCreds } from './mcp.js';
|
|
19
19
|
export { planBrainHook } from './hooks.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* file's marker so a future engine can tell "my old output" from "my current
|
|
7
7
|
* output" and run migrations. Bump LAYOUT_VERSION when the template SHAPE
|
|
8
8
|
* changes (a path moves, a format changes); bump ENGINE_VERSION freely. */
|
|
9
|
-
export declare const ENGINE_VERSION = "0.
|
|
9
|
+
export declare const ENGINE_VERSION = "0.4.1";
|
|
10
10
|
export declare const LAYOUT_VERSION = 1;
|
|
11
11
|
/** The marker key written into generated-file frontmatter / headers. */
|
|
12
12
|
export declare const GENERATED_BY = "clan-engine";
|
|
@@ -59,6 +59,17 @@ export interface BrainCurateInput {
|
|
|
59
59
|
* re-gates EVERY curated body through scanSensitive + create-only regardless —
|
|
60
60
|
* an LLM echoing a secret from context is held back like a mined one. */
|
|
61
61
|
export type BrainCurator = (input: BrainCurateInput) => Promise<GeneratedFile[]>;
|
|
62
|
+
/**
|
|
63
|
+
* The clan itself as an encrypted-brain recipient — its alphaId plus its X25519
|
|
64
|
+
* ENCRYPTION public key (SPKI DER base64, the same convention as
|
|
65
|
+
* step_up_devices.public_key_spki). Handed to brain-build so a page marked
|
|
66
|
+
* `private: true` can be encrypted to the owner BEFORE any write. The private
|
|
67
|
+
* half NEVER reaches the engine; only this public identity does. Shape matches
|
|
68
|
+
* @nfinitmonkeys/clan-crypto's Recipient. */
|
|
69
|
+
export interface BrainOwner {
|
|
70
|
+
alphaId: string;
|
|
71
|
+
spkiB64: string;
|
|
72
|
+
}
|
|
62
73
|
export type ActionKind = 'create' | 'update' | 'backup-replace' | 'merge' | 'append' | 'remove' | 'skip';
|
|
63
74
|
export interface PlanAction {
|
|
64
75
|
kind: ActionKind;
|
package/dist/types.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* file's marker so a future engine can tell "my old output" from "my current
|
|
7
7
|
* output" and run migrations. Bump LAYOUT_VERSION when the template SHAPE
|
|
8
8
|
* changes (a path moves, a format changes); bump ENGINE_VERSION freely. */
|
|
9
|
-
export const ENGINE_VERSION = '0.
|
|
9
|
+
export const ENGINE_VERSION = '0.4.1';
|
|
10
10
|
export const LAYOUT_VERSION = 1;
|
|
11
11
|
/** The marker key written into generated-file frontmatter / headers. */
|
|
12
12
|
export const GENERATED_BY = 'clan-engine';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nfinitmonkeys/clan-engine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Deterministic setup engine for Nfinit Monkeys clans
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Deterministic setup engine for Nfinit Monkeys clans — the single template + reconciliation authority behind the published CLIs and the internal fleet tooling. Idempotent, dry-run-by-default, non-destructive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clan-engine": "./dist/cli.js"
|
|
@@ -37,5 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"yaml": "^2.9.0"
|
|
40
|
+
},
|
|
41
|
+
"optionalDependencies": {
|
|
42
|
+
"@nfinitmonkeys/clan-crypto": "^0.2.0"
|
|
40
43
|
}
|
|
41
44
|
}
|