@dreamforgestudiollc/emet 0.0.0-stage → 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 +189 -0
- package/CHANGELOG.md +360 -0
- package/CHARTER.md +137 -0
- package/LICENSE +22 -0
- package/README.md +868 -2
- package/docs/AUTHORSHIP.md +26 -0
- package/docs/BENCHMARKS.md +261 -0
- package/docs/BOOTSTRAP.md +127 -0
- package/docs/BOTS.md +83 -0
- package/docs/COVERAGE.md +64 -0
- package/docs/DEPLOY.md +251 -0
- package/docs/DISCIPLINES.md +29 -0
- package/docs/GUARDRAILS-AND-MONORAILS.md +108 -0
- package/docs/GUIDED-SETUP.md +301 -0
- package/docs/PERMISSIONS.md +121 -0
- package/docs/ROADMAP.md +62 -0
- package/docs/SETUP.md +265 -0
- 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/gems/neurendipity/guides/NEURODIVERGENT_SUPPORT.md +287 -0
- package/guides/README.md +3 -0
- package/package.json +76 -4
- package/server.json +79 -0
- package/src/aleph.js +395 -0
- package/src/annotations.js +170 -0
- package/src/approve-throttle.js +54 -0
- package/src/atlas-role.js +58 -0
- package/src/attribution.js +555 -0
- package/src/conformance.js +688 -0
- package/src/content_analyzer.js +450 -0
- package/src/database.js +812 -0
- package/src/disciplines.js +26 -0
- package/src/dispatch.js +382 -0
- package/src/env.js +97 -0
- package/src/floor.js +228 -0
- package/src/gaps.js +509 -0
- package/src/gem-check.js +1359 -0
- package/src/guides.js +75 -0
- package/src/host-reach.js +23 -0
- package/src/http-remote.js +313 -0
- package/src/index.js +397 -0
- package/src/invites.js +167 -0
- package/src/layers.js +449 -0
- package/src/lessons-to-checks.js +366 -0
- package/src/logger.js +300 -0
- package/src/metadata.js +61 -0
- package/src/module-path.js +32 -0
- package/src/mongo-connect.js +91 -0
- package/src/named-items.js +208 -0
- package/src/oauth-provider.js +838 -0
- package/src/owner-approvals.js +281 -0
- package/src/persons.js +136 -0
- package/src/provenance.js +257 -0
- package/src/redact.js +237 -0
- package/src/request-log.js +124 -0
- 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 +100 -0
- package/src/security-headers.js +26 -0
- package/src/session-graph.js +947 -0
- package/src/session.js +1097 -0
- package/src/setup.js +1580 -0
- package/src/socket.js +545 -0
- package/src/startup-pages.js +371 -0
- package/src/startup-payload.js +129 -0
- package/src/subkey-store.js +357 -0
- package/src/subkeys.js +947 -0
- package/src/tools.js +4508 -0
- package/src/transcript-session.js +106 -0
- package/src/validation.js +1187 -0
- package/src/version.js +2 -0
- package/templates/BOARD.md +72 -0
- package/templates/BOTHANDOFF.md +57 -0
- package/templates/CHARACTER.md +30 -0
- package/templates/HANDOFF.md +63 -0
- package/templates/IDENTITY.md +29 -0
- package/templates/INDEX.md +82 -0
- package/templates/INSTALL.md +29 -0
- package/templates/PROCEDURE.md +39 -0
- package/templates/ROSTER.md +43 -0
- package/templates/SEED.md +65 -0
- package/templates/STAGING.md +37 -0
- package/templates/STANDARD.md +99 -0
- package/templates/THREAD.md +43 -0
- package/templates/USER.md +32 -0
- 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
|
+
}
|