@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.
- package/.env.example +53 -0
- package/CHANGELOG.md +67 -3
- package/CHARTER.md +19 -7
- package/README.md +106 -21
- package/docs/AUTHORSHIP.md +3 -3
- package/docs/BOTS.md +4 -0
- package/docs/COVERAGE.md +2 -2
- package/docs/DEPLOY.md +9 -4
- package/docs/GUARDRAILS-AND-MONORAILS.md +2 -1
- package/docs/GUIDED-SETUP.md +8 -3
- package/docs/PERMISSIONS.md +6 -3
- package/docs/ROADMAP.md +25 -22
- package/docs/SETUP.md +8 -2
- package/docs/SUBKEYS.md +59 -0
- package/gems/artifact-index.json +12 -0
- package/gems/bot-team-backup/floor.json +15 -0
- package/gems/bot-team-backup/gem.json +43 -0
- package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
- package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
- package/gems/fieldnote/floor.json +14 -0
- package/gems/fieldnote/gem.json +70 -0
- package/gems/fieldnote/runes/fieldnote.js +4 -0
- package/gems/lean-six-sigma/CHANGELOG.md +10 -0
- package/gems/lean-six-sigma/README.md +92 -0
- package/gems/lean-six-sigma/floor.json +31 -0
- package/gems/lean-six-sigma/gem.json +1296 -0
- package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
- package/gems/lean-six-sigma/guides/BELTS.md +52 -0
- package/gems/lean-six-sigma/guides/DMADV.md +56 -0
- package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
- package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
- package/gems/lean-six-sigma/guides/LEAN.md +97 -0
- package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
- package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
- package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
- package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
- package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
- package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
- package/gems/neurendipity/floor.json +65 -0
- package/gems/neurendipity/gem.json +42 -0
- package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
- package/{guides → gems/neurendipity/guides}/NEURODIVERGENT_SUPPORT.md +17 -8
- package/guides/README.md +3 -0
- package/package.json +11 -5
- package/server.json +44 -4
- package/src/aleph.js +4 -4
- package/src/annotations.js +38 -3
- package/src/atlas-role.js +1 -1
- package/src/attribution.js +555 -0
- package/src/conformance.js +2 -2
- package/src/database.js +6 -5
- package/src/dispatch.js +116 -13
- package/src/env.js +54 -9
- package/src/floor.js +82 -41
- package/src/gaps.js +39 -6
- package/src/gem-check.js +1359 -0
- package/src/guides.js +31 -4
- package/src/host-reach.js +23 -0
- package/src/http-remote.js +46 -2
- package/src/index.js +9 -2
- package/src/layers.js +1 -1
- package/src/lessons-to-checks.js +3 -3
- package/src/logger.js +2 -1
- package/src/mongo-connect.js +45 -2
- package/src/oauth-provider.js +2 -2
- package/src/owner-approvals.js +281 -0
- package/src/persons.js +20 -0
- package/src/redact.js +10 -4
- package/src/request-log.js +1 -1
- package/src/rune-dispatch.js +360 -0
- package/src/rune-registry.js +71 -0
- package/src/rune-scope.js +42 -0
- package/src/scopes.js +3 -0
- package/src/session-graph.js +25 -23
- package/src/session.js +9 -9
- package/src/setup.js +297 -30
- package/src/socket.js +545 -0
- package/src/startup-pages.js +23 -6
- package/src/startup-payload.js +9 -6
- package/src/subkey-store.js +357 -0
- package/src/subkeys.js +947 -0
- package/src/tools.js +299 -62
- package/src/transcript-session.js +1 -1
- package/src/validation.js +26 -2
- package/src/version.js +1 -1
- package/templates/BOARD.md +2 -2
- package/templates/BOTHANDOFF.md +2 -2
- package/templates/CHARACTER.md +1 -1
- package/templates/HANDOFF.md +1 -1
- package/templates/IDENTITY.md +1 -1
- package/templates/INDEX.md +19 -19
- package/templates/INSTALL.md +1 -1
- package/templates/PROCEDURE.md +1 -1
- package/templates/ROSTER.md +2 -2
- package/templates/SEED.md +2 -2
- package/templates/STAGING.md +2 -2
- package/templates/STANDARD.md +5 -5
- package/templates/THREAD.md +1 -1
- package/templates/USER.md +4 -1
- package/verification/load-gate/corpus.json +1765 -0
- package/verification/load-gate/load-gate.mjs +1246 -0
package/src/socket.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMET - Enduring Memory & Epistemic Tiers
|
|
3
|
+
* Copyright (c) 2026 Dreamforge Studio LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the MIT License - see LICENSE
|
|
6
|
+
*
|
|
7
|
+
* The GEM socket. It seats bundled GEMs from the release gems/ directory
|
|
8
|
+
* and nothing else. gems/neurendipity and gems/fieldnote ship with this
|
|
9
|
+
* release and are off unless the install lists the id in EMET_ACTIVE_GEMS.
|
|
10
|
+
*
|
|
11
|
+
* gem-library/examples/neurendipity is a shape, not an installed GEM.
|
|
12
|
+
* It is not on this path and it is not loaded.
|
|
13
|
+
*
|
|
14
|
+
* The socket does not run RUNE code. While a GEM is active, each RUNE is
|
|
15
|
+
* one read-only tool that serves the code. A refused GEM is one warning.
|
|
16
|
+
* It does not throw, and it does not stop emet_initialize. An empty list
|
|
17
|
+
* logs nothing.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { checkGem, satisfiesEmetRange } from './gem-check.js';
|
|
23
|
+
import { bindRunes, runeToolName } from './rune-registry.js';
|
|
24
|
+
import { noteArtifact } from './rune-dispatch.js';
|
|
25
|
+
import { resolveModulePath } from './module-path.js';
|
|
26
|
+
import { setting } from './env.js';
|
|
27
|
+
import { EMET_VERSION } from './version.js';
|
|
28
|
+
|
|
29
|
+
export const MAX_INSTALLED = 16;
|
|
30
|
+
export const MAX_ACTIVE = 4;
|
|
31
|
+
export const MAX_ACTIVE_FLOOR_BYTES = 16384;
|
|
32
|
+
|
|
33
|
+
const ID_RE = /^[a-z][a-z0-9-]{1,63}$/;
|
|
34
|
+
|
|
35
|
+
/** The release gems/ directory, next to src/. Bundled GEMs live here. */
|
|
36
|
+
export function releaseGemsRoot() {
|
|
37
|
+
return resolveModulePath(import.meta.url, ['..', 'gems']);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inside(rootReal, candidateReal) {
|
|
41
|
+
const rel = path.relative(rootReal, candidateReal);
|
|
42
|
+
return rel === '' || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fallbackId(name, used) {
|
|
46
|
+
const cleaned = String(name).toLowerCase().replace(/[^a-z0-9-]/g, '').replace(/^-+/, '').slice(0, 60);
|
|
47
|
+
let id = cleaned.length >= 2 && /^[a-z]/.test(cleaned) ? cleaned : 'invalid';
|
|
48
|
+
if (!used.has(id)) return id;
|
|
49
|
+
let n = 2;
|
|
50
|
+
while (used.has(`${id}-${n}`)) n++;
|
|
51
|
+
const next = `${id}-${n}`;
|
|
52
|
+
return next.length <= 64 ? next : next.slice(0, 64);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cmpId(a, b) {
|
|
56
|
+
if (a.id < b.id) return -1;
|
|
57
|
+
if (a.id > b.id) return 1;
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sameDigestSet(a, b) {
|
|
62
|
+
const left = [...a].map(String).sort();
|
|
63
|
+
const right = [...b].map(String).sort();
|
|
64
|
+
if (left.length !== right.length) return false;
|
|
65
|
+
for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasConsent(consent, id, digests) {
|
|
70
|
+
if (!Array.isArray(consent)) return false;
|
|
71
|
+
return consent.some((row) => row && row.id === id && Array.isArray(row.digests) && sameDigestSet(row.digests, digests));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onAcceptedList(accepted, id, digests) {
|
|
75
|
+
if (!Array.isArray(accepted) || accepted.length === 0) return false;
|
|
76
|
+
return accepted.some((row) => row && row.id === id && Array.isArray(row.digests) && sameDigestSet(row.digests, digests));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function emit(log, level, message, context) {
|
|
80
|
+
if (log && typeof log[level] === 'function') {
|
|
81
|
+
log[level](message, context);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (level === 'warn' || level === 'error') console.error(`emet: ${message}`);
|
|
85
|
+
else if (level === 'info') console.error(`emet: ${message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function field(manifest, key, fallback) {
|
|
89
|
+
const value = manifest && manifest[key];
|
|
90
|
+
if (typeof fallback === 'string') return typeof value === 'string' ? value : fallback;
|
|
91
|
+
if (typeof fallback === 'boolean') return typeof value === 'boolean' ? value : fallback;
|
|
92
|
+
if (typeof fallback === 'number') return Number.isInteger(value) ? value : fallback;
|
|
93
|
+
return value === undefined ? fallback : value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What gems_available and emet_status show. A checker failure stays the check id. */
|
|
97
|
+
export function refusalText(result) {
|
|
98
|
+
if (!result || !result.firstFailure) return 'GC-01';
|
|
99
|
+
return result.firstFailure;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function schemaList(schema) {
|
|
103
|
+
const props = schema && schema.properties;
|
|
104
|
+
if (props && typeof props === 'object') {
|
|
105
|
+
const keys = Object.keys(props);
|
|
106
|
+
if (keys.length) return keys.join(',');
|
|
107
|
+
}
|
|
108
|
+
return 'object';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toCandidate(dirName, dirReal, result, used) {
|
|
112
|
+
const manifest = result && result.manifest;
|
|
113
|
+
const id = manifest && typeof manifest.id === 'string' && ID_RE.test(manifest.id)
|
|
114
|
+
? manifest.id
|
|
115
|
+
: fallbackId(dirName, used);
|
|
116
|
+
used.add(id);
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
dirName,
|
|
120
|
+
dirReal,
|
|
121
|
+
manifest,
|
|
122
|
+
name: field(manifest, 'name', id).slice(0, 80),
|
|
123
|
+
description: field(manifest, 'description', '').slice(0, 400),
|
|
124
|
+
version: field(manifest, 'version', '0.0.0'),
|
|
125
|
+
kind: manifest && (manifest.kind === 'gem' || manifest.kind === 'runed-gem') ? manifest.kind : 'gem',
|
|
126
|
+
spec: field(manifest, 'spec', ''),
|
|
127
|
+
priority: field(manifest, 'priority', 0),
|
|
128
|
+
distribution: manifest && manifest.distribution === 'library' ? 'library' : 'bundled',
|
|
129
|
+
defaultOn: field(manifest, 'defaultOn', false),
|
|
130
|
+
gcOk: Boolean(result && result.ok),
|
|
131
|
+
gcReason: refusalText(result),
|
|
132
|
+
floor: result && result.floor,
|
|
133
|
+
floorBytes: result && result.floorTrusted ? result.floorBytes : 0,
|
|
134
|
+
floorTrusted: Boolean(result && result.floorTrusted),
|
|
135
|
+
floorDigest: result && result.floorDigest,
|
|
136
|
+
loadable: false,
|
|
137
|
+
active: false,
|
|
138
|
+
reason: refusalText(result)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function releaseCopy(candidate, releaseReal) {
|
|
143
|
+
if (!releaseReal || !candidate.manifest || typeof candidate.manifest.id !== 'string') return false;
|
|
144
|
+
const expected = path.join(releaseReal, candidate.manifest.id);
|
|
145
|
+
let expectedReal;
|
|
146
|
+
try { expectedReal = fs.realpathSync(expected); } catch { return false; }
|
|
147
|
+
return expectedReal === candidate.dirReal;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* SR-01: a bundled GEM loads only from gems/<id>/ of this release.
|
|
152
|
+
* A library GEM loads only when its id and digests are on the accepted list
|
|
153
|
+
* shipped with the release. This release ships no accepted list.
|
|
154
|
+
* distribution: bundled in the file is not evidence.
|
|
155
|
+
*/
|
|
156
|
+
function serverRefusal(candidate, ctx) {
|
|
157
|
+
const manifest = candidate.manifest;
|
|
158
|
+
const digests = manifest && Array.isArray(manifest.files) ? manifest.files.map((f) => f.digest) : [];
|
|
159
|
+
const inRelease = releaseCopy(candidate, ctx.releaseReal);
|
|
160
|
+
if (manifest.distribution === 'library') {
|
|
161
|
+
if (!onAcceptedList(ctx.accepted, manifest.id, digests)) return 'SR-01';
|
|
162
|
+
} else if (!inRelease) {
|
|
163
|
+
return 'SR-01';
|
|
164
|
+
}
|
|
165
|
+
if (!satisfies(ctx.emetVersion, manifest.requires && manifest.requires.emet)) return 'SR-06';
|
|
166
|
+
if (Array.isArray(manifest.overrides) && manifest.overrides.length > 0 && !hasConsent(ctx.consent, manifest.id, digests)) {
|
|
167
|
+
return 'SR-05';
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function satisfies(version, range) {
|
|
173
|
+
return satisfiesEmetRange(version, range);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A missing `released` field is allowed only for these ids, at this version,
|
|
178
|
+
* with every pinned file present at its digest. A shorter file list does not
|
|
179
|
+
* match. The pin is the id, the version, and the file digests. It does not
|
|
180
|
+
* cover every other manifest field, including defaultOn, priority, requires,
|
|
181
|
+
* description, name, authors, license, rune description, schemas, the guides
|
|
182
|
+
* list, distribution, coverage, and rune metadata other than the version.
|
|
183
|
+
* overrides still needs SR-05 consent. A rune execution of server and
|
|
184
|
+
* distribution library are refused by GC-02. gem.json is not hashed: anyone
|
|
185
|
+
* who can edit it can already add released: true, so hashing it would grant
|
|
186
|
+
* nothing extra. The list is fixed in source. Nothing is read to decide
|
|
187
|
+
* whether SR-08 runs, so a missing or corrupt manifest cannot turn the check off.
|
|
188
|
+
*/
|
|
189
|
+
export const RELEASED_LEGACY = Object.freeze([
|
|
190
|
+
Object.freeze({
|
|
191
|
+
id: 'neurendipity',
|
|
192
|
+
version: '1.2.3',
|
|
193
|
+
digests: Object.freeze({
|
|
194
|
+
'floor.json': 'sha256:484dd159ccfec5d4fd85a764aea6db687b9c2355e1b7a11532f044fc350eca01',
|
|
195
|
+
'guides/NEURODIVERGENT_SUPPORT.md': 'sha256:aa64c1f125960da7822970a87926bd042fcb4f8ba6d84e392a29dfb12321c809',
|
|
196
|
+
'guides/AUTISTIC_BURNOUT.md': 'sha256:657810094cdde539648c7d06767072e71b8c712aee73a60e7e8040ac83df40e2'
|
|
197
|
+
})
|
|
198
|
+
}),
|
|
199
|
+
Object.freeze({
|
|
200
|
+
id: 'fieldnote',
|
|
201
|
+
version: '1.0.1',
|
|
202
|
+
digests: Object.freeze({
|
|
203
|
+
'floor.json': 'sha256:11af3724b165beb3e9a49a681f1f07cb742bcb4535206f714a0dc689067f32d6',
|
|
204
|
+
'runes/fieldnote.js': 'sha256:cfb62b669f67c69f53989ed2621a40bfbfa60a744f1b4040e5b8496755598a35'
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
function legacyDigests(manifest, pin) {
|
|
210
|
+
const files = Array.isArray(manifest.files) ? manifest.files : [];
|
|
211
|
+
const keys = Object.keys(pin.digests);
|
|
212
|
+
if (files.length !== keys.length) return false;
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
if (!file || pin.digests[file.path] !== file.digest) return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function legacyReleased(manifest) {
|
|
220
|
+
if (!manifest || typeof manifest !== 'object') return false;
|
|
221
|
+
const pin = RELEASED_LEGACY.find((row) => row.id === manifest.id);
|
|
222
|
+
if (!pin || manifest.version !== pin.version || !legacyDigests(manifest, pin)) return false; // legacy-pin
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Null when the manifest may seat. Otherwise the log line that says why. */
|
|
227
|
+
export function releasedReason(manifest) {
|
|
228
|
+
if (manifest && manifest.released === true) return null;
|
|
229
|
+
const missing = !manifest || typeof manifest !== 'object' || !Object.prototype.hasOwnProperty.call(manifest, 'released');
|
|
230
|
+
if (missing) {
|
|
231
|
+
if (legacyReleased(manifest)) return null;
|
|
232
|
+
const base = 'released is missing; boolean true is required';
|
|
233
|
+
// Exact and case-sensitive. neurendipity-x, xneurendipity, and Fieldnote are not pins.
|
|
234
|
+
const id = manifest && typeof manifest.id === 'string' ? manifest.id : '';
|
|
235
|
+
const pinned = RELEASED_LEGACY.some((row) => row.id === id);
|
|
236
|
+
return pinned ? `${base} (legacy pin mismatch: version/digests)` : base;
|
|
237
|
+
}
|
|
238
|
+
const value = manifest.released;
|
|
239
|
+
if (typeof value === 'string') return `released must be boolean true, got string ${JSON.stringify(value)}`;
|
|
240
|
+
if (typeof value === 'number') return `released must be boolean true, got number ${value}`;
|
|
241
|
+
if (typeof value === 'boolean') return `released must be boolean true, got boolean ${value}`;
|
|
242
|
+
return `released must be boolean true, got ${value === null ? 'null' : typeof value}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* GEM ids the install asked to turn on. EMET_ACTIVE_GEMS is a comma-separated
|
|
247
|
+
* list. Unset means only a GEM with defaultOn true is active. options.asked,
|
|
248
|
+
* when passed, replaces the setting for that call. This is not consent: a GEM
|
|
249
|
+
* with overrides still needs consent bound to its digests.
|
|
250
|
+
*/
|
|
251
|
+
export function askedGemIds(options = {}) {
|
|
252
|
+
const raw = options.asked !== undefined ? options.asked : setting('ACTIVE_GEMS', '');
|
|
253
|
+
const list = Array.isArray(raw) ? raw : String(raw ?? '').split(',');
|
|
254
|
+
const ids = new Set();
|
|
255
|
+
for (const item of list) {
|
|
256
|
+
const id = String(item).trim();
|
|
257
|
+
if (ID_RE.test(id)) ids.add(id);
|
|
258
|
+
}
|
|
259
|
+
return ids;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function publicAvailable(candidate) {
|
|
263
|
+
const row = {
|
|
264
|
+
id: candidate.id,
|
|
265
|
+
name: candidate.name,
|
|
266
|
+
description: candidate.description,
|
|
267
|
+
version: candidate.version,
|
|
268
|
+
kind: candidate.kind,
|
|
269
|
+
spec: candidate.spec,
|
|
270
|
+
priority: candidate.priority,
|
|
271
|
+
distribution: candidate.distribution,
|
|
272
|
+
defaultOn: candidate.defaultOn,
|
|
273
|
+
active: candidate.active === true,
|
|
274
|
+
loadable: candidate.loadable === true,
|
|
275
|
+
bytes: candidate.floorTrusted ? candidate.floorBytes : 0
|
|
276
|
+
};
|
|
277
|
+
if (!row.loadable) row.reason = candidate.reason;
|
|
278
|
+
if (row.active && candidate.manifest && Array.isArray(candidate.manifest.runes) && candidate.manifest.runes.length) {
|
|
279
|
+
row.runes = candidate.manifest.runes.map((rune) => ({
|
|
280
|
+
name: rune.name,
|
|
281
|
+
runtime: rune.runtime,
|
|
282
|
+
inputs: schemaList(rune.inputSchema),
|
|
283
|
+
outputs: schemaList(rune.outputSchema),
|
|
284
|
+
sha256: rune.sha256
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
return row;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function publicActive(candidate) {
|
|
291
|
+
const floor = candidate.floor && candidate.floor.sections ? candidate.floor.sections : {};
|
|
292
|
+
const reassertion = candidate.floor && Array.isArray(candidate.floor.reassertion) ? candidate.floor.reassertion : [];
|
|
293
|
+
return {
|
|
294
|
+
id: candidate.manifest.id,
|
|
295
|
+
version: candidate.manifest.version,
|
|
296
|
+
priority: candidate.manifest.priority,
|
|
297
|
+
floor_digest: candidate.floorDigest,
|
|
298
|
+
floor,
|
|
299
|
+
reassertion
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Discover and validate GEMs.
|
|
305
|
+
* root defaults to the release gems/ directory. Tests may pass another root
|
|
306
|
+
* only together with the same releaseRoot when they are exercising a bundled copy.
|
|
307
|
+
* There is no environment variable for this path.
|
|
308
|
+
* options.db is ignored if a caller passes one. The socket never reads it.
|
|
309
|
+
*/
|
|
310
|
+
export async function seatGems(options = {}) {
|
|
311
|
+
const releaseRoot = options.releaseRoot ? path.resolve(options.releaseRoot) : releaseGemsRoot();
|
|
312
|
+
const root = options.root ? path.resolve(options.root) : releaseRoot;
|
|
313
|
+
const emetVersion = options.emetVersion || EMET_VERSION;
|
|
314
|
+
const consent = Array.isArray(options.consent) ? options.consent : [];
|
|
315
|
+
const accepted = Array.isArray(options.accepted) ? options.accepted : [];
|
|
316
|
+
const check = typeof options.check === 'function' ? options.check : checkGem;
|
|
317
|
+
const log = options.log || null;
|
|
318
|
+
const asked = askedGemIds(options);
|
|
319
|
+
|
|
320
|
+
let releaseReal = null;
|
|
321
|
+
try {
|
|
322
|
+
const st = await fs.promises.stat(releaseRoot);
|
|
323
|
+
if (st.isDirectory()) releaseReal = await fs.promises.realpath(releaseRoot);
|
|
324
|
+
} catch { releaseReal = null; }
|
|
325
|
+
|
|
326
|
+
let entries;
|
|
327
|
+
try {
|
|
328
|
+
const st = await fs.promises.lstat(root);
|
|
329
|
+
if (!st.isDirectory() && !st.isSymbolicLink()) {
|
|
330
|
+
return { gems_available: [], gems_active: [], refusals: [] };
|
|
331
|
+
}
|
|
332
|
+
entries = await fs.promises.readdir(root, { withFileTypes: true });
|
|
333
|
+
} catch (e) {
|
|
334
|
+
if (e && e.code === 'ENOENT') return { gems_available: [], gems_active: [], refusals: [] };
|
|
335
|
+
emit(log, 'error', 'GEM socket failed closed');
|
|
336
|
+
return { gems_available: [], gems_active: [], refusals: [] };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let rootReal;
|
|
340
|
+
try { rootReal = await fs.promises.realpath(root); }
|
|
341
|
+
catch {
|
|
342
|
+
emit(log, 'error', 'GEM socket failed closed');
|
|
343
|
+
return { gems_available: [], gems_active: [], refusals: [] };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const used = new Set();
|
|
347
|
+
const candidates = [];
|
|
348
|
+
for (const ent of entries) {
|
|
349
|
+
if (!ent.isDirectory() && !ent.isSymbolicLink()) continue;
|
|
350
|
+
const abs = path.join(root, ent.name);
|
|
351
|
+
try {
|
|
352
|
+
const st = await fs.promises.stat(abs);
|
|
353
|
+
if (!st.isDirectory()) {
|
|
354
|
+
if (!ent.isSymbolicLink()) continue;
|
|
355
|
+
const row = toCandidate(ent.name, abs, { ok: false, firstFailure: 'GC-01', manifest: null }, used);
|
|
356
|
+
row.gcOk = false;
|
|
357
|
+
row.reason = 'GC-01';
|
|
358
|
+
candidates.push(row);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const dirReal = await fs.promises.realpath(abs);
|
|
362
|
+
if (!inside(rootReal, dirReal)) {
|
|
363
|
+
const row = toCandidate(ent.name, dirReal, { ok: false, firstFailure: 'GC-04', manifest: null }, used);
|
|
364
|
+
row.gcOk = false;
|
|
365
|
+
row.reason = 'GC-04';
|
|
366
|
+
candidates.push(row);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
// GC-20 checks grammar. The running version is SR-06, so it is not passed here.
|
|
370
|
+
const result = await check(abs, { trustStore: options.trustStore, schema: options.schema });
|
|
371
|
+
candidates.push(toCandidate(ent.name, dirReal, result, used));
|
|
372
|
+
} catch {
|
|
373
|
+
const row = toCandidate(ent.name, abs, { ok: false, firstFailure: 'GC-01', manifest: null }, used);
|
|
374
|
+
row.gcOk = false;
|
|
375
|
+
row.reason = 'GC-01';
|
|
376
|
+
candidates.push(row);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
candidates.sort(cmpId);
|
|
381
|
+
const ctx = { releaseReal, emetVersion, consent, accepted };
|
|
382
|
+
|
|
383
|
+
candidates.forEach((candidate, index) => {
|
|
384
|
+
if (index >= MAX_INSTALLED) {
|
|
385
|
+
candidate.loadable = false;
|
|
386
|
+
candidate.active = false;
|
|
387
|
+
candidate.reason = 'SR-03';
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!candidate.gcOk) {
|
|
391
|
+
candidate.loadable = false;
|
|
392
|
+
candidate.active = false;
|
|
393
|
+
candidate.reason = candidate.gcReason || 'GC-01';
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const why = releasedReason(candidate.manifest); // sr-08-always
|
|
397
|
+
if (why) {
|
|
398
|
+
candidate.loadable = false;
|
|
399
|
+
candidate.active = false;
|
|
400
|
+
candidate.reason = 'SR-08';
|
|
401
|
+
candidate.releasedWhy = why;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const sr = serverRefusal(candidate, ctx);
|
|
405
|
+
if (sr) {
|
|
406
|
+
candidate.loadable = false;
|
|
407
|
+
candidate.active = false;
|
|
408
|
+
candidate.reason = sr;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
candidate.loadable = true;
|
|
412
|
+
candidate.reason = null;
|
|
413
|
+
candidate.wantsActive = candidate.defaultOn === true || asked.has(candidate.id);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
let activeCount = 0;
|
|
417
|
+
let activeBytes = 0;
|
|
418
|
+
for (const candidate of candidates) {
|
|
419
|
+
if (!candidate.loadable || !candidate.wantsActive) {
|
|
420
|
+
if (candidate.loadable) candidate.active = false;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const size = candidate.floorTrusted ? candidate.floorBytes : 0;
|
|
424
|
+
if (activeCount + 1 > MAX_ACTIVE) {
|
|
425
|
+
candidate.loadable = false;
|
|
426
|
+
candidate.active = false;
|
|
427
|
+
candidate.reason = 'SR-02';
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (activeBytes + size > MAX_ACTIVE_FLOOR_BYTES) {
|
|
431
|
+
candidate.loadable = false;
|
|
432
|
+
candidate.active = false;
|
|
433
|
+
candidate.reason = 'SR-04';
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
candidate.active = true;
|
|
437
|
+
activeCount += 1;
|
|
438
|
+
activeBytes += size;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const gems_available = candidates.map(publicAvailable);
|
|
442
|
+
const refusals = gems_available.filter((row) => row.loadable === false);
|
|
443
|
+
const gems_active = candidates.filter((c) => c.active).map(publicActive)
|
|
444
|
+
.sort((a, b) => (b.priority - a.priority) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
445
|
+
for (const candidate of candidates) {
|
|
446
|
+
if (candidate.loadable) continue;
|
|
447
|
+
const why = candidate.reason === 'SR-08' ? candidate.releasedWhy : null;
|
|
448
|
+
const message = why
|
|
449
|
+
? `GEM refused: ${candidate.id} ${candidate.reason}: ${why}`
|
|
450
|
+
: `GEM refused: ${candidate.id} ${candidate.reason}`;
|
|
451
|
+
emit(log, 'warn', message, { id: candidate.id, reason: candidate.reason, ...(why ? { detail: why } : {}) });
|
|
452
|
+
}
|
|
453
|
+
// Empty mount stays silent. Each seated GEM is named, then the summary.
|
|
454
|
+
// A refused GEM is not a seat line.
|
|
455
|
+
if (gems_available.length > 0) {
|
|
456
|
+
for (const row of gems_available) {
|
|
457
|
+
if (!row.loadable) continue;
|
|
458
|
+
emit(log, 'info', `GEM seated: ${row.id}`, { id: row.id });
|
|
459
|
+
}
|
|
460
|
+
emit(log, 'info', `GEMs seated: ${gems_available.length} installed, ${gems_active.length} active`, {
|
|
461
|
+
installed: gems_available.length,
|
|
462
|
+
active: gems_active.length
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const production = options.root === undefined && options.releaseRoot === undefined;
|
|
466
|
+
if (production || options.bindRunes === true) {
|
|
467
|
+
const bound = [];
|
|
468
|
+
for (const candidate of candidates) {
|
|
469
|
+
if (!candidate.active || !candidate.manifest || !Array.isArray(candidate.manifest.runes)) continue;
|
|
470
|
+
for (const rune of candidate.manifest.runes) {
|
|
471
|
+
noteArtifact({ sha256: rune.sha256, gem: candidate.id, rune: rune.name, version: rune.version });
|
|
472
|
+
bound.push({
|
|
473
|
+
name: rune.name,
|
|
474
|
+
description: rune.description,
|
|
475
|
+
inputSchema: rune.inputSchema,
|
|
476
|
+
outputSchema: rune.outputSchema,
|
|
477
|
+
runtime: rune.runtime,
|
|
478
|
+
sha256: rune.sha256,
|
|
479
|
+
artifact: rune.artifact,
|
|
480
|
+
version: rune.version,
|
|
481
|
+
annotations: rune.annotations,
|
|
482
|
+
reads: rune.reads,
|
|
483
|
+
writes: rune.writes,
|
|
484
|
+
network: rune.network,
|
|
485
|
+
gemId: candidate.id,
|
|
486
|
+
gemVersion: candidate.version,
|
|
487
|
+
gemDir: candidate.dirReal,
|
|
488
|
+
tool: runeToolName(candidate.id, rune.name)
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
bindRunes(bound);
|
|
493
|
+
}
|
|
494
|
+
const result = { gems_available, gems_active, refusals };
|
|
495
|
+
if (options.floors) {
|
|
496
|
+
const floors = Object.create(null);
|
|
497
|
+
for (const candidate of candidates) {
|
|
498
|
+
if (!candidate.loadable || !candidate.floor) continue;
|
|
499
|
+
floors[candidate.id] = {
|
|
500
|
+
sections: candidate.floor.sections || {},
|
|
501
|
+
reassertion: Array.isArray(candidate.floor.reassertion) ? candidate.floor.reassertion : []
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
result.floors = floors;
|
|
505
|
+
}
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Socket PR 4 (docs/ROADMAP.md): each GEM's byte cost, for emet_status.
|
|
511
|
+
* Read-only: built from a seatGems() result, never from the store. The caps
|
|
512
|
+
* are the ones seatGems already enforces; this only makes them visible, so an
|
|
513
|
+
* install can see what a GEM costs its startup before it asks for one.
|
|
514
|
+
* Bytes are the trusted floor bytes the socket counts toward
|
|
515
|
+
* MAX_ACTIVE_FLOOR_BYTES (0 for a GEM whose floor was not trusted).
|
|
516
|
+
*/
|
|
517
|
+
export function gemCost(seated) {
|
|
518
|
+
const available = seated && Array.isArray(seated.gems_available) ? seated.gems_available : [];
|
|
519
|
+
const perGem = available.map((row) => {
|
|
520
|
+
const out = {
|
|
521
|
+
id: row.id,
|
|
522
|
+
version: row.version,
|
|
523
|
+
kind: row.kind,
|
|
524
|
+
active: row.active === true,
|
|
525
|
+
loadable: row.loadable === true,
|
|
526
|
+
bytes: Number.isFinite(row.bytes) ? row.bytes : 0
|
|
527
|
+
};
|
|
528
|
+
if (!out.loadable && row.reason) out.reason = row.reason;
|
|
529
|
+
return out;
|
|
530
|
+
});
|
|
531
|
+
const active = perGem.filter((g) => g.active);
|
|
532
|
+
const activeBytes = active.reduce((sum, g) => sum + g.bytes, 0);
|
|
533
|
+
return {
|
|
534
|
+
installed: perGem.length,
|
|
535
|
+
active: active.length,
|
|
536
|
+
active_floor_bytes: activeBytes,
|
|
537
|
+
headroom_bytes: Math.max(0, MAX_ACTIVE_FLOOR_BYTES - activeBytes),
|
|
538
|
+
caps: {
|
|
539
|
+
max_installed: MAX_INSTALLED,
|
|
540
|
+
max_active: MAX_ACTIVE,
|
|
541
|
+
max_active_floor_bytes: MAX_ACTIVE_FLOOR_BYTES
|
|
542
|
+
},
|
|
543
|
+
per_gem: perGem
|
|
544
|
+
};
|
|
545
|
+
}
|
package/src/startup-pages.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paged startup (
|
|
3
|
-
*
|
|
2
|
+
* Paged startup (ref 358 amendment, paging; Alex approved 2026-10-03, design
|
|
3
|
+
* paper question 1). The connector gateway cuts a tool result at 20,000 UTF-8
|
|
4
4
|
* bytes, and the full startup is about 64K, so on those hosts the floor,
|
|
5
5
|
* install notes, source tags, seeds_due, record gaps and lessons never
|
|
6
6
|
* arrived. Paging delivers the SAME full payload in pages, binding text first.
|
|
@@ -41,9 +41,9 @@ export function pageBudget(raw = process.env.EMET_INIT_PAGE_BYTES) {
|
|
|
41
41
|
|
|
42
42
|
/** Binding first: the order keys are paged in. Keys not listed follow, in payload order. */
|
|
43
43
|
export const PAGE_ORDER = Object.freeze([
|
|
44
|
-
'state', 'database', 'source_tag', 'person_id', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops', 'emet_line', 'emet_line_if_unanswered',
|
|
44
|
+
'state', 'database', 'source_tag', 'source_basis', 'source_login', 'source_remembered', 'approval_state', 'person_id', 'client_approvals', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops', 'emet_line', 'emet_line_if_unanswered',
|
|
45
45
|
'tools_in_force', 'instructions_short', 'session_close_protocol', 'operating_discipline', 'charter',
|
|
46
|
-
'floor', 'floor_precedence', 'floor_reassertion',
|
|
46
|
+
'floor', 'floor_precedence', 'floor_reassertion', 'gems_available', 'gems_active',
|
|
47
47
|
'seeds_due', 'seeds_evaluated_on', 'install_notes_doc_id',
|
|
48
48
|
'identity', 'character', 'character_doc_id', 'character_sha256', 'user',
|
|
49
49
|
'record_gaps', 'named_items', 'lessons_pending', 'layer_health',
|
|
@@ -51,10 +51,11 @@ export const PAGE_ORDER = Object.freeze([
|
|
|
51
51
|
'seeds', 'threads', 'recent_context', 'layer_counts', 'layer_counts_last_30d'
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
-
/** Keys that bind the session (
|
|
54
|
+
/** Keys that bind the session (ref 358): never optional, never dropped. */
|
|
55
55
|
export const BINDING_KEYS = Object.freeze([
|
|
56
56
|
'tools_in_force', 'instructions_short', 'session_close_protocol',
|
|
57
|
-
'floor', 'floor_precedence', 'floor_reassertion', '
|
|
57
|
+
'floor', 'floor_precedence', 'floor_reassertion', 'gems_available', 'gems_active',
|
|
58
|
+
'identity', 'character', 'user'
|
|
58
59
|
]);
|
|
59
60
|
|
|
60
61
|
/** Page fields that are never payload keys. */
|
|
@@ -71,6 +72,16 @@ function orderedKeys(full) {
|
|
|
71
72
|
const bytes = (s) => Buffer.byteLength(s, 'utf8');
|
|
72
73
|
const entryBytes = (key, value) => bytes(JSON.stringify({ [key]: value })) - 1; // `"key":value` plus a comma
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* GEM lists travel with the floor. They are never split into chunks and never
|
|
77
|
+
* replaced by a pointer (startup-payload.js keeps them too). A list that does
|
|
78
|
+
* not fit the bytes left on the current page moves whole onto the next page.
|
|
79
|
+
* A list that cannot fit even an empty page is still one entry: this packer
|
|
80
|
+
* does not slice it. The caps in the socket keep a real list small enough to fit.
|
|
81
|
+
*/
|
|
82
|
+
const WHOLE_KEYS = new Set(['gems_available', 'gems_active']);
|
|
83
|
+
export { WHOLE_KEYS };
|
|
84
|
+
|
|
74
85
|
/**
|
|
75
86
|
* Pack the payload into pages of entries. budgets(k) = content bytes allowed
|
|
76
87
|
* on page k (0-based). An entry is {key, value} or {key, value, chunk, of, json}.
|
|
@@ -82,6 +93,12 @@ function pack(full, budgets) {
|
|
|
82
93
|
for (const key of orderedKeys(full)) {
|
|
83
94
|
const value = full[key];
|
|
84
95
|
const size = entryBytes(key, value);
|
|
96
|
+
if (WHOLE_KEYS.has(key)) {
|
|
97
|
+
if (size > room() && pages[pages.length - 1].length) newPage();
|
|
98
|
+
pages[pages.length - 1].push({ key, value });
|
|
99
|
+
used[pages.length - 1] += size;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
85
102
|
if (size <= room()) { pages[pages.length - 1].push({ key, value }); used[pages.length - 1] += size; continue; }
|
|
86
103
|
if (size <= budgets(pages.length)) { newPage(); pages[pages.length - 1].push({ key, value }); used[pages.length - 1] += size; continue; }
|
|
87
104
|
// Too big for any one page: chunk it. Strings split as text; anything else
|
package/src/startup-payload.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The startup payload's size and its abridged form (
|
|
3
|
-
*
|
|
2
|
+
* The startup payload's size and its abridged form (ref 358 amendment, the
|
|
3
|
+
* host-aware startup; design: research paper emet-startup-amendment rev 3,
|
|
4
|
+
* sections 4a-4c, conservative variant chosen by Alex 2026-10-02, procedural
|
|
5
|
+
* ref 405 item 8 and ref 407).
|
|
4
6
|
*
|
|
5
7
|
* WHAT THIS FILE DOES AND DOES NOT DO (2026-10-03):
|
|
6
8
|
* - payloadForLayerCharter: the layer charter as the payload serves it, with
|
|
@@ -13,13 +15,13 @@
|
|
|
13
15
|
* selects it: the amendment says the server decides the mode and the model
|
|
14
16
|
* does not, and a host can never ask for less than the server allows. The
|
|
15
17
|
* server can only prove a returning host with the startup receipts of
|
|
16
|
-
* stage 2
|
|
18
|
+
* stage 2 (review note 3b, 5), which do not exist yet, so emet_initialize serves
|
|
17
19
|
* the full payload to every caller. This function is the tested builder
|
|
18
20
|
* stage 2's mode check will call, and the measure of what abridging saves.
|
|
19
21
|
* - resultSize: the size of a payload as the host receives it, in characters
|
|
20
22
|
* and UTF-8 bytes (the connector gateway cuts at 20,000 bytes).
|
|
21
23
|
*
|
|
22
|
-
* FIDELITY RULE (
|
|
24
|
+
* FIDELITY RULE (Alex, ref 358): the floor, identity, character, user, the close
|
|
23
25
|
* protocol and tools in force are never pointers and never shortened here.
|
|
24
26
|
* Only install notes, the layer charter and the long instructions become
|
|
25
27
|
* hash-pinned pointers in the abridged form, and the seed and thread indexes
|
|
@@ -54,7 +56,7 @@ export function keySizes(payload) {
|
|
|
54
56
|
.sort((a, b) => b.chars - a.chars);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
/** The keys every abridged load embeds exactly as the full load has them (
|
|
59
|
+
/** The keys every abridged load embeds exactly as the full load has them (review note 4b; never dropped). */
|
|
58
60
|
export const ABRIDGED_KEEP = Object.freeze([
|
|
59
61
|
'state', 'database', 'source_tag', 'person_id', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops',
|
|
60
62
|
'emet_line', 'emet_line_if_unanswered',
|
|
@@ -63,7 +65,8 @@ export const ABRIDGED_KEEP = Object.freeze([
|
|
|
63
65
|
'install_notes_doc_id',
|
|
64
66
|
'seeds_due', 'seeds_evaluated_on',
|
|
65
67
|
'record_gaps', 'named_items', 'lessons_pending',
|
|
66
|
-
'floor', 'floor_precedence', 'floor_reassertion'
|
|
68
|
+
'floor', 'floor_precedence', 'floor_reassertion',
|
|
69
|
+
'gems_available', 'gems_active'
|
|
67
70
|
]);
|
|
68
71
|
|
|
69
72
|
/** Keys that become hash-pinned pointers: text the host already echoed at a full load, unchanged. */
|