@dreamforgestudiollc/emet 0.2.4 → 0.2.6

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.
Files changed (101) hide show
  1. package/.env.example +53 -0
  2. package/CHANGELOG.md +67 -3
  3. package/CHARTER.md +19 -7
  4. package/README.md +106 -21
  5. package/docs/AUTHORSHIP.md +3 -3
  6. package/docs/BOTS.md +4 -0
  7. package/docs/COVERAGE.md +2 -2
  8. package/docs/DEPLOY.md +9 -4
  9. package/docs/GUARDRAILS-AND-MONORAILS.md +2 -1
  10. package/docs/GUIDED-SETUP.md +8 -3
  11. package/docs/PERMISSIONS.md +6 -3
  12. package/docs/ROADMAP.md +25 -22
  13. package/docs/SETUP.md +8 -2
  14. package/docs/SUBKEYS.md +59 -0
  15. package/gems/artifact-index.json +12 -0
  16. package/gems/bot-team-backup/floor.json +15 -0
  17. package/gems/bot-team-backup/gem.json +43 -0
  18. package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
  19. package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
  20. package/gems/fieldnote/floor.json +14 -0
  21. package/gems/fieldnote/gem.json +70 -0
  22. package/gems/fieldnote/runes/fieldnote.js +4 -0
  23. package/gems/lean-six-sigma/CHANGELOG.md +10 -0
  24. package/gems/lean-six-sigma/README.md +92 -0
  25. package/gems/lean-six-sigma/floor.json +31 -0
  26. package/gems/lean-six-sigma/gem.json +1296 -0
  27. package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
  28. package/gems/lean-six-sigma/guides/BELTS.md +52 -0
  29. package/gems/lean-six-sigma/guides/DMADV.md +56 -0
  30. package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
  31. package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
  32. package/gems/lean-six-sigma/guides/LEAN.md +97 -0
  33. package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
  34. package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
  35. package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
  36. package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
  37. package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
  38. package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
  39. package/gems/neurendipity/floor.json +65 -0
  40. package/gems/neurendipity/gem.json +42 -0
  41. package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
  42. package/{guides → gems/neurendipity/guides}/NEURODIVERGENT_SUPPORT.md +17 -8
  43. package/guides/README.md +3 -0
  44. package/package.json +11 -5
  45. package/server.json +44 -4
  46. package/src/aleph.js +4 -4
  47. package/src/annotations.js +38 -3
  48. package/src/atlas-role.js +1 -1
  49. package/src/attribution.js +555 -0
  50. package/src/conformance.js +2 -2
  51. package/src/database.js +6 -5
  52. package/src/dispatch.js +116 -13
  53. package/src/env.js +54 -9
  54. package/src/floor.js +82 -41
  55. package/src/gaps.js +39 -6
  56. package/src/gem-check.js +1359 -0
  57. package/src/guides.js +31 -4
  58. package/src/host-reach.js +23 -0
  59. package/src/http-remote.js +46 -2
  60. package/src/index.js +9 -2
  61. package/src/layers.js +1 -1
  62. package/src/lessons-to-checks.js +3 -3
  63. package/src/logger.js +2 -1
  64. package/src/mongo-connect.js +45 -2
  65. package/src/oauth-provider.js +2 -2
  66. package/src/owner-approvals.js +281 -0
  67. package/src/persons.js +20 -0
  68. package/src/redact.js +10 -4
  69. package/src/request-log.js +1 -1
  70. package/src/rune-dispatch.js +360 -0
  71. package/src/rune-registry.js +71 -0
  72. package/src/rune-scope.js +42 -0
  73. package/src/scopes.js +3 -0
  74. package/src/session-graph.js +25 -23
  75. package/src/session.js +9 -9
  76. package/src/setup.js +297 -30
  77. package/src/socket.js +545 -0
  78. package/src/startup-pages.js +23 -6
  79. package/src/startup-payload.js +9 -6
  80. package/src/subkey-store.js +357 -0
  81. package/src/subkeys.js +947 -0
  82. package/src/tools.js +299 -62
  83. package/src/transcript-session.js +1 -1
  84. package/src/validation.js +26 -2
  85. package/src/version.js +1 -1
  86. package/templates/BOARD.md +2 -2
  87. package/templates/BOTHANDOFF.md +2 -2
  88. package/templates/CHARACTER.md +1 -1
  89. package/templates/HANDOFF.md +1 -1
  90. package/templates/IDENTITY.md +1 -1
  91. package/templates/INDEX.md +19 -19
  92. package/templates/INSTALL.md +1 -1
  93. package/templates/PROCEDURE.md +1 -1
  94. package/templates/ROSTER.md +2 -2
  95. package/templates/SEED.md +2 -2
  96. package/templates/STAGING.md +2 -2
  97. package/templates/STANDARD.md +5 -5
  98. package/templates/THREAD.md +1 -1
  99. package/templates/USER.md +4 -1
  100. package/verification/load-gate/corpus.json +1765 -0
  101. package/verification/load-gate/load-gate.mjs +1246 -0
package/src/redact.js CHANGED
@@ -11,14 +11,14 @@
11
11
  * carry it verbatim was minutes away from being written to the store. A rule
12
12
  * that says "do not paste secrets" is an instruction; a scrubber the content
13
13
  * passes through before it lands anywhere is a control. This module is the
14
- * control. It sits inside the single write path (THE DOOR, a decision record):
14
+ * control. It sits inside the single write path (THE DOOR, procedural ref 280):
15
15
  * memory saves, transcript saves, document writes, and log output all pass
16
16
  * through it, so an instance cannot opt out by forgetting.
17
17
  *
18
18
  * What it masks, in order:
19
19
  * 1. Literal values of secret-bearing environment variables (anything whose
20
20
  * name contains PASSPHRASE, PASSWORD, SECRET, TOKEN, API_KEY, PRIVATE_KEY,
21
- * or MONGODB_URI), plus the user:password pair inside any URI among them.
21
+ * PEPPER, or MONGODB_URI), plus the user:password pair inside any URI among them.
22
22
  * These are exact-string matches and cannot false-positive.
23
23
  * 2. Credentials embedded in URIs: scheme://user:password@host.
24
24
  * 3. Bearer tokens and JWTs.
@@ -38,12 +38,12 @@
38
38
  *
39
39
  * The hit count reports CHANGES. Text that already carries the mask is matched
40
40
  * and left as it is, uncounted - otherwise every read-modify-write of a
41
- * document re-reports the masks it already held (observed 2026-09-07).
41
+ * document re-reports the masks it already held (observed 2026-09-07, meta ref 83).
42
42
  */
43
43
 
44
44
  export const MASK = '*****';
45
45
 
46
- const SECRET_ENV_NAME = /(PASSPHRASE|PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|PRIVATE[_-]?KEY|MONGODB_URI)/i;
46
+ const SECRET_ENV_NAME = /(PASSPHRASE|PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|PRIVATE[_-]?KEY|PEPPER|MONGODB_URI)/i;
47
47
  const MIN_LITERAL_LENGTH = 6;
48
48
 
49
49
  // Registered literal secrets. A Set so re-registering is a no-op.
@@ -121,6 +121,12 @@ const RULES = [
121
121
  // JWT
122
122
  { name: 'jwt', pre: (t) => t.includes('eyJ'),
123
123
  re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, sub: () => MASK },
124
+ // Delegated client sub-keys (EMET-0001). The full key, the session handle
125
+ // and the claim nonce. A bare key_id (32 hex, no prefix) is an index and
126
+ // stays visible. Lengths are exact so a shorter hex id is not masked.
127
+ { name: 'subkey', pre: (t) => t.includes('emk_') || t.includes('emh_') || t.includes('emn_'),
128
+ re: /(?<![A-Za-z0-9_])(?:emk_[0-9a-f]{32}_[A-Za-z0-9_-]{43}|emh_[A-Za-z0-9_-]{43}|emn_[A-Za-z0-9_-]{43})(?![A-Za-z0-9_-])/g,
129
+ sub: () => MASK },
124
130
  // key: value / key = value / "key": "value" (key survives, value is masked)
125
131
  { name: 'key-value',
126
132
  // Every alternative in KEY_NAMES contains one of these substrings, and the
@@ -68,7 +68,7 @@ export function requestLogEntry({ method, path, statusCode, ms, ip, query = {},
68
68
  entry.rpc = clip(b.method);
69
69
  if (b.method === 'tools/call' && b.params && b.params.name) {
70
70
  entry.tool = clip(b.params.name);
71
- // Parameter NAMES only (the owner, 2026-09-27, code audit 1.6) - values never.
71
+ // Parameter NAMES only (Alex, 2026-09-27, code audit 1.6) - values never.
72
72
  const a = b.params.arguments;
73
73
  if (a && typeof a === 'object' && !Array.isArray(a)) entry.params = Object.keys(a).sort().map(clip);
74
74
  }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ * Licensed under the MIT License - see LICENSE
5
+ *
6
+ * Serve one active RUNE. The call returns the code and does not run it.
7
+ * A kept result is sealed on the existing write path: the principal comes
8
+ * from authentication, the served hash is the pin, and the attestation
9
+ * fields do not change on a later write. The file keeps SERVE_CAP rows per
10
+ * principal. This release creates SERVE_COLLECTION (`<prefix>gem_serves`).
11
+ * Alex approved it as an append-only write site on the write-path allow-list.
12
+ * The collection keeps every row. Nothing is deleted.
13
+ */
14
+ import crypto from 'node:crypto';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { ValidationError } from './validation.js';
18
+ import { activeRune, activeRunes, metadataGemClause, metadataGemValue } from './rune-registry.js';
19
+ import { writerStamp, declaredAttribution } from './persons.js';
20
+ import { resolveModulePath } from './module-path.js';
21
+ import { COLLECTION_PREFIX } from './database.js';
22
+
23
+ const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
24
+ const LAYERS = ['identity', 'semantic', 'episodic', 'procedural', 'meta', 'working'];
25
+ /** File cap. The Mongo collection is not capped. */
26
+ export const SERVE_CAP = 200;
27
+ /** Created by this release. One document per serve, append-only. */
28
+ export const SERVE_COLLECTION = `${COLLECTION_PREFIX}gem_serves`;
29
+ const SEAL_KEYS = ['gem', 'rune', 'served_sha256'];
30
+ const STRIP_KEYS = ['gem', 'rune', 'served_sha256', 'client_sha256', 'input_sha256', 'input_ref', 'principal', 'rune_attested'];
31
+ const FREEZE_KEYS = ['gem', 'version', 'rune', 'served_sha256', 'client_sha256', 'input_sha256', 'input_ref', 'principal', 'rune_attested'];
32
+ const INSTRUCTIONS = 'Run this code in your own sandbox. It is a pure function of the inputs you pass in. EMET does not run it. If you have no sandbox, you have no result and must say so.';
33
+
34
+ const artifacts = new Map();
35
+ let artifactsLoaded = false;
36
+ let serveLogOverride = null;
37
+ let serveTurn = Promise.resolve();
38
+ const serveIndexReady = new WeakSet();
39
+
40
+ /** Tests point the file at a temp path. Production leaves this unset. */
41
+ export function setServeLogPath(file) {
42
+ serveLogOverride = file || null;
43
+ }
44
+
45
+ function serveLogPath() {
46
+ return serveLogOverride || resolveModulePath(import.meta.url, ['..', 'logs', 'gem-serves.json']);
47
+ }
48
+
49
+ function principalKey(principal) {
50
+ return typeof principal === 'string' ? principal : '';
51
+ }
52
+
53
+ /** Latest SERVE_CAP file rows for each principal, in the order they were recorded. */
54
+ function capServeRows(rows) {
55
+ const counts = new Map();
56
+ const kept = [];
57
+ const list = Array.isArray(rows) ? rows : [];
58
+ for (let i = list.length - 1; i >= 0; i--) {
59
+ const row = list[i];
60
+ if (!row || typeof row !== 'object') continue;
61
+ const key = principalKey(row.principal);
62
+ const n = counts.get(key) || 0;
63
+ if (n >= SERVE_CAP) continue;
64
+ counts.set(key, n + 1);
65
+ kept.push(row);
66
+ }
67
+ kept.reverse();
68
+ return kept;
69
+ }
70
+
71
+ function readServeLog() {
72
+ try {
73
+ const body = JSON.parse(fs.readFileSync(serveLogPath(), 'utf8'));
74
+ if (!Array.isArray(body)) return [];
75
+ return capServeRows(body);
76
+ } catch { return []; }
77
+ }
78
+
79
+ function writeServeLog(rows) {
80
+ const file = serveLogPath();
81
+ fs.mkdirSync(path.dirname(file), { recursive: true });
82
+ const tmp = path.join(
83
+ path.dirname(file),
84
+ `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`
85
+ );
86
+ try {
87
+ fs.writeFileSync(tmp, JSON.stringify(capServeRows(rows)), { mode: 0o600 });
88
+ fs.renameSync(tmp, file);
89
+ } finally {
90
+ try { fs.rmSync(tmp, { force: true }); } catch { /* renamed into place */ }
91
+ }
92
+ }
93
+
94
+ function afterServe(fn) {
95
+ const run = serveTurn.then(fn, fn);
96
+ serveTurn = run.then(() => {}, () => {});
97
+ return run;
98
+ }
99
+
100
+ async function ensureServeIndex(db) {
101
+ if (!db || serveIndexReady.has(db)) return;
102
+ const col = db.collection(SERVE_COLLECTION);
103
+ if (!col || typeof col.createIndex !== 'function') return;
104
+ serveIndexReady.add(db);
105
+ try {
106
+ await col.createIndex({ principal: 1, session_id: 1 });
107
+ } catch {
108
+ // An index miss never blocks the serve. The next serve tries again.
109
+ serveIndexReady.delete(db);
110
+ }
111
+ }
112
+
113
+ export async function appendServeStore(db, event) {
114
+ // A new document per serve. Two processes cannot overwrite each other's rows,
115
+ // and nothing already stored is updated or removed.
116
+ await ensureServeIndex(db);
117
+ await db.collection(SERVE_COLLECTION).insertOne({
118
+ _id: crypto.randomBytes(16).toString('hex'),
119
+ principal: event.principal,
120
+ session_id: event.session_id,
121
+ gem: event.gem,
122
+ rune: event.rune,
123
+ version: event.version,
124
+ sha256: event.sha256,
125
+ time: event.time
126
+ });
127
+ }
128
+
129
+ async function openServeStore(ctx) {
130
+ if (!ctx) return null;
131
+ if (ctx.db && typeof ctx.db.collection === 'function') return ctx.db;
132
+ const mgr = ctx.dbManager;
133
+ if (!mgr) return null;
134
+ if ((!mgr.db || typeof mgr.db.collection !== 'function') && typeof mgr.ensureClient === 'function') {
135
+ try { await mgr.ensureClient(); } catch { return null; }
136
+ }
137
+ return mgr.db && typeof mgr.db.collection === 'function' ? mgr.db : null;
138
+ }
139
+
140
+ async function recordServe(event, db) {
141
+ await afterServe(async () => {
142
+ if (db) await appendServeStore(db, event);
143
+ writeServeLog(readServeLog().concat(event));
144
+ });
145
+ }
146
+
147
+ async function storeHasServe(db, query) {
148
+ await ensureServeIndex(db);
149
+ const row = await db.collection(SERVE_COLLECTION).findOne({
150
+ principal: query.principal,
151
+ session_id: query.session_id || null,
152
+ gem: query.gem,
153
+ rune: query.rune,
154
+ sha256: query.sha256
155
+ });
156
+ return Boolean(row);
157
+ }
158
+
159
+ export function servePrincipal() {
160
+ const attr = declaredAttribution();
161
+ if (attr && attr.keyId) return attr.keyId;
162
+ if (attr && attr.login) return attr.login;
163
+ const stamp = writerStamp();
164
+ return stamp.person_id || null;
165
+ }
166
+
167
+ function sessionOf(value) {
168
+ if (value === undefined || value === null || value === '') return null;
169
+ const text = typeof value === 'string' ? value.trim() : '';
170
+ return text && text.length <= 200 ? text : null;
171
+ }
172
+
173
+ function digestOf(buf) {
174
+ return 'sha256:' + crypto.createHash('sha256').update(buf).digest('hex');
175
+ }
176
+
177
+ function digestEqual(declared, actual) {
178
+ const a = Buffer.from(String(declared));
179
+ const b = Buffer.from(String(actual));
180
+ if (a.length !== b.length) return false;
181
+ return crypto.timingSafeEqual(a, b);
182
+ }
183
+
184
+ function indexPath() {
185
+ return resolveModulePath(import.meta.url, ['..', 'gems', 'artifact-index.json']);
186
+ }
187
+
188
+ function loadArtifacts() {
189
+ if (artifactsLoaded) return;
190
+ artifactsLoaded = true;
191
+ try {
192
+ const body = JSON.parse(fs.readFileSync(indexPath(), 'utf8'));
193
+ for (const [hash, row] of Object.entries(body)) {
194
+ if (SHA256_RE.test(hash) && row && typeof row === 'object') artifacts.set(hash, row);
195
+ }
196
+ } catch { /* a release with no index still serves the files it has */ }
197
+ }
198
+
199
+ /** Remember a shipped artifact by hash. Entries are not removed. */
200
+ export function noteArtifact(row) {
201
+ loadArtifacts();
202
+ if (!row || !SHA256_RE.test(row.sha256 || '')) return;
203
+ if (artifacts.has(row.sha256)) return;
204
+ artifacts.set(row.sha256, { gem: row.gem, rune: row.rune, version: row.version });
205
+ }
206
+
207
+ export function artifactRecord(hash) {
208
+ loadArtifacts();
209
+ return artifacts.get(hash) || null;
210
+ }
211
+
212
+ function pinRune(rune) {
213
+ if (!rune || typeof rune.gemDir !== 'string' || typeof rune.artifact !== 'string') return null;
214
+ let buf;
215
+ try { buf = fs.readFileSync(path.join(rune.gemDir, rune.artifact)); }
216
+ catch { return null; }
217
+ const actual = digestOf(buf);
218
+ if (!digestEqual(rune.sha256, actual)) return null;
219
+ noteArtifact({ sha256: actual, gem: rune.gemId, rune: rune.name, version: rune.version });
220
+ return { code: buf.toString('utf8'), sha256: actual };
221
+ }
222
+
223
+ function activeByGem(gem, runeName) {
224
+ if (typeof gem !== 'string' || typeof runeName !== 'string') return null;
225
+ for (const rune of activeRunes()) {
226
+ if (metadataGemValue(rune) === gem && rune.name === runeName) return rune;
227
+ }
228
+ return null;
229
+ }
230
+
231
+ export function matchServe(query) {
232
+ return readServeLog().find((row) =>
233
+ row.principal === query.principal &&
234
+ row.session_id === (query.session_id || null) &&
235
+ row.gem === query.gem &&
236
+ row.rune === query.rune &&
237
+ row.sha256 === query.sha256
238
+ ) || null;
239
+ }
240
+
241
+ /**
242
+ * Parent seal copied onto a revision. A different value is a refusal.
243
+ * Returns null when the parent was not sealed by the server.
244
+ */
245
+ export function frozenAttestation(parent, incoming) {
246
+ if (!parent || parent.rune_attested !== true) return null;
247
+ const prior = {};
248
+ for (const key of FREEZE_KEYS) {
249
+ if (key === 'rune_attested') prior[key] = true;
250
+ else if (typeof parent[key] === 'string' && parent[key]) prior[key] = parent[key];
251
+ }
252
+ const sent = incoming && typeof incoming === 'object' ? incoming : {};
253
+ for (const key of FREEZE_KEYS) {
254
+ if (!Object.prototype.hasOwnProperty.call(sent, key)) continue;
255
+ if (sent[key] !== prior[key]) throw new ValidationError(key, 'attestation is immutable');
256
+ }
257
+ return prior;
258
+ }
259
+
260
+ function stripUnsealed(metadata) {
261
+ for (const key of STRIP_KEYS) delete metadata[key];
262
+ }
263
+
264
+ /** Stamp a kept result from the active RUNE and a serve event. No-op otherwise. */
265
+ export async function sealAttestation(metadata, opts = {}) {
266
+ if (!metadata || typeof metadata !== 'object') return;
267
+ if (opts.inherited) return;
268
+ const full = SEAL_KEYS.every((key) => typeof metadata[key] === 'string' && metadata[key]);
269
+ if (!full) { stripUnsealed(metadata); return; }
270
+ const rune = activeByGem(metadata.gem, metadata.rune);
271
+ if (!rune) throw new ValidationError('gem', "metadata.gem must be '<gem>@<version>' and match an active directive");
272
+ const pinned = pinRune(rune);
273
+ if (!pinned) throw new ValidationError('served_sha256', 'hash pin');
274
+ const principal = servePrincipal();
275
+ if (!principal) throw new ValidationError('principal', 'no authenticated principal');
276
+ const session_id = sessionOf(opts.session_id);
277
+ const query = { principal, session_id, gem: rune.gemId, rune: rune.name, sha256: pinned.sha256 };
278
+ let db = opts.db && typeof opts.db.collection === 'function' ? opts.db : null;
279
+ let served = Boolean(matchServe(query));
280
+ if (!served && !db && opts.dbManager && typeof opts.dbManager.ensureClient === 'function') {
281
+ try { await opts.dbManager.ensureClient(); } catch { /* the file was the only copy */ }
282
+ const opened = opts.dbManager.db;
283
+ if (opened && typeof opened.collection === 'function') db = opened;
284
+ }
285
+ if (!served && db) {
286
+ try { served = await storeHasServe(db, query); }
287
+ catch (e) {
288
+ if (e instanceof ValidationError) throw e;
289
+ throw new ValidationError('gem', 'serve log');
290
+ }
291
+ }
292
+ if (!served) throw new ValidationError('gem', 'no serve event');
293
+ if (!SHA256_RE.test(metadata.client_sha256 || '')) {
294
+ throw new ValidationError('client_sha256', 'client-reported sha256');
295
+ }
296
+ const inputHash = SHA256_RE.test(metadata.input_sha256 || '');
297
+ const inputRef = typeof metadata.input_ref === 'string' && metadata.input_ref.trim().length > 0;
298
+ if (!inputHash && !inputRef) throw new ValidationError('input_sha256', 'input hash or reference');
299
+ metadata.gem = metadataGemValue(rune);
300
+ metadata.version = rune.version;
301
+ metadata.rune = rune.name;
302
+ metadata.served_sha256 = pinned.sha256;
303
+ metadata.principal = principal;
304
+ metadata.rune_attested = true;
305
+ if (!inputHash) delete metadata.input_sha256;
306
+ if (!inputRef) delete metadata.input_ref;
307
+ }
308
+
309
+ /** Results whose served hash is this one, plus the index row when the release kept it. */
310
+ export async function resultsForArtifact(dbManager, sha256) {
311
+ loadArtifacts();
312
+ const artifact = artifacts.get(sha256) || null;
313
+ const results = [];
314
+ if (!dbManager || typeof dbManager.collectionFor !== 'function') return { artifact, results };
315
+ for (const layer of LAYERS) {
316
+ let rows = [];
317
+ try { rows = await dbManager.collectionFor(layer).find({ 'metadata.served_sha256': sha256 }).toArray(); }
318
+ catch { rows = []; }
319
+ for (const row of rows || []) results.push({ layer, id: row.id, served_sha256: sha256 });
320
+ }
321
+ return { artifact, results };
322
+ }
323
+
324
+ /** Serve an active RUNE. Returns null when the name is not one. Runs nothing. */
325
+ export async function serveRune(name, args, ctx) {
326
+ const rune = activeRune(name);
327
+ if (!rune) return null;
328
+ const { handleError, createSuccessResponse } = await import('./tools.js');
329
+ const pinned = pinRune(rune);
330
+ if (!pinned) return handleError(new ValidationError('rune', 'hash pin'), name, ctx && ctx.logger);
331
+ const principal = servePrincipal();
332
+ const session_id = sessionOf(args && args.session_id);
333
+ const event = {
334
+ principal,
335
+ session_id,
336
+ gem: rune.gemId,
337
+ rune: rune.name,
338
+ version: rune.version,
339
+ sha256: pinned.sha256,
340
+ time: new Date().toISOString()
341
+ };
342
+ const db = await openServeStore(ctx);
343
+ try { await recordServe(event, db); }
344
+ catch (e) {
345
+ const err = e instanceof ValidationError ? e : new ValidationError('gem', 'serve log');
346
+ return handleError(err, name, ctx && ctx.logger);
347
+ }
348
+ const op = ctx && ctx.AuditOperation && ctx.AuditOperation.GEM_SERVE ? ctx.AuditOperation.GEM_SERVE : 'GEM_SERVE';
349
+ if (ctx && ctx.logger && typeof ctx.logger.audit === 'function') ctx.logger.audit(op, event);
350
+ return createSuccessResponse({
351
+ code: pinned.code,
352
+ sha256: pinned.sha256,
353
+ version: rune.version,
354
+ runtime: rune.runtime,
355
+ instructions: INSTRUCTIONS,
356
+ inputSchema: rune.inputSchema,
357
+ outputSchema: rune.outputSchema,
358
+ description: `${rune.description} ${metadataGemClause(rune)}`
359
+ }, name);
360
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ * Licensed under the MIT License - see LICENSE
5
+ *
6
+ * Active RUNED directives for this process. A GEM that is off is not in
7
+ * this map, so its tools are not registered. The tool name is
8
+ * gem_<gem>__<rune>. A call serves the code. It does not run it.
9
+ */
10
+
11
+ const active = new Map();
12
+ const reserved = new Set();
13
+
14
+ export function runeToolName(gemId, runeName) {
15
+ return `gem_${gemId}__${runeName}`;
16
+ }
17
+
18
+ export function reserveToolNames(names) {
19
+ for (const name of names) reserved.add(name);
20
+ }
21
+
22
+ export function toolNameReserved(name) {
23
+ return reserved.has(name);
24
+ }
25
+
26
+ /** Replace the active set. Called when the release socket seats GEMs. */
27
+ export function bindRunes(list) {
28
+ active.clear();
29
+ for (const rune of list || []) {
30
+ if (!rune || typeof rune.gemId !== 'string' || typeof rune.name !== 'string') continue;
31
+ const tool = runeToolName(rune.gemId, rune.name);
32
+ if (reserved.has(tool)) continue;
33
+ active.set(tool, { ...rune, tool });
34
+ }
35
+ }
36
+
37
+ export function activeRune(name) {
38
+ return active.get(name) || null;
39
+ }
40
+
41
+ export function activeRunes() {
42
+ return [...active.values()];
43
+ }
44
+
45
+ /** The metadata.gem a client sends to seal a result: `<gem>@<version>`. */
46
+ export function metadataGemValue(rune) {
47
+ return `${rune.gemId}@${rune.gemVersion}`;
48
+ }
49
+
50
+ /** Sentence the tool list and the served description both state. The value is in backticks so the period is not part of it. */
51
+ export function metadataGemClause(rune) {
52
+ return `To seal a result, set metadata.gem to \`${metadataGemValue(rune)}\`.`;
53
+ }
54
+
55
+ /** Tool list entries for directives that are on. Empty when none are. */
56
+ export function runeToolList() {
57
+ return activeRunes().map((rune) => ({
58
+ name: rune.tool,
59
+ description: `${rune.description} A call returns the code and does not run it. runtime ${rune.runtime}. served ${rune.sha256}. ${metadataGemClause(rune)}`,
60
+ inputSchema: rune.inputSchema || { type: 'object' },
61
+ outputSchema: rune.outputSchema || { type: 'object' },
62
+ runtime: rune.runtime,
63
+ sha256: rune.sha256,
64
+ annotations: {
65
+ readOnlyHint: true,
66
+ destructiveHint: false,
67
+ idempotentHint: !(rune.annotations && rune.annotations.idempotentHint === false),
68
+ openWorldHint: false
69
+ }
70
+ }));
71
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ * Licensed under the MIT License - see LICENSE
5
+ *
6
+ * Scope helpers kept with the socket. A client RUNE declares empty reads
7
+ * and writes. Prefix matching is on whole path segments: doc:foo matches
8
+ * doc:foo and doc:foo/bar, and does not match doc:foobar.
9
+ */
10
+
11
+ const LAYERS = new Set(['identity', 'semantic', 'episodic', 'procedural', 'meta', 'working']);
12
+
13
+ export function scopePermits(allow, scope) {
14
+ if (!Array.isArray(allow) || typeof scope !== 'string' || scope.length === 0) return false;
15
+ for (const entry of allow) {
16
+ if (typeof entry !== 'string') continue;
17
+ if (entry === scope) return true;
18
+ if (entry.startsWith('doc:') && scope.startsWith(entry + '/')) return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ export function layerOf(scope) {
24
+ return LAYERS.has(scope) ? scope : null;
25
+ }
26
+
27
+ export function docIdOf(scope) {
28
+ if (typeof scope !== 'string' || !scope.startsWith('doc:')) return null;
29
+ const id = scope.slice(4);
30
+ if (!id || id.includes('..')) return null;
31
+ return id;
32
+ }
33
+
34
+ /** The four hints are required. A missing one refuses the directive. */
35
+ export function hintsComplete(annotations) {
36
+ if (!annotations || typeof annotations !== 'object') return false;
37
+ for (const key of ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint']) {
38
+ if (typeof annotations[key] !== 'boolean') return false;
39
+ }
40
+ if (annotations.destructiveHint !== false || annotations.openWorldHint !== false) return false;
41
+ return true;
42
+ }
package/src/scopes.js CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { TOOL_ANNOTATIONS } from './annotations.js';
14
+ import { activeRune } from './rune-registry.js';
14
15
 
15
16
  // A tool whose scope deliberately differs from its annotation.
16
17
  // emet_status without probe is a read, so a guest can see that the store is up.
@@ -33,6 +34,8 @@ export function scopeForTool(name, args = null) {
33
34
  if (SCOPE_OVERRIDES[name]) return SCOPE_OVERRIDES[name];
34
35
  const a = TOOL_ANNOTATIONS[name];
35
36
  if (a && a.readOnlyHint === true) return 'emet.read';
37
+ const rune = activeRune(name);
38
+ if (rune && rune.annotations && rune.annotations.readOnlyHint === true) return 'emet.read';
36
39
  return 'emet.write';
37
40
  }
38
41