@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/tools.js
CHANGED
|
@@ -14,7 +14,20 @@
|
|
|
14
14
|
import { connectMongo } from './mongo-connect.js';
|
|
15
15
|
import { resolveModulePath } from './module-path.js';
|
|
16
16
|
import { replacedByOf } from './setup.js';
|
|
17
|
-
import { currentActor, writerStamp, boundSourceTag, LOCAL_SUBJECT } from './persons.js';
|
|
17
|
+
import { currentActor, rawActor, writerStamp, boundSourceTag, declaredAttribution, LOCAL_SUBJECT } from './persons.js';
|
|
18
|
+
import { approvedTags, APPROVALS_DOC } from './attribution.js';
|
|
19
|
+
import { sealAttestation, frozenAttestation } from './rune-dispatch.js';
|
|
20
|
+
|
|
21
|
+
/** Approvals record io for attribution.js (server-kept doc, written as emet-server). */
|
|
22
|
+
export const approvalsDeps = {
|
|
23
|
+
readDoc: (id) => readDoc(id),
|
|
24
|
+
// Compare-and-set on the stored version (the reviewer 3). mutateApprovals serializes
|
|
25
|
+
// writers in this process; this check catches a writer elsewhere.
|
|
26
|
+
writeDoc: async (id, content, expectedVersion) => {
|
|
27
|
+
// Atomic across processes: writeDoc filters the update on { doc_id, version: expected }.
|
|
28
|
+
return writeDoc(id, content, 'emet-server', null, null, { serverProvision: true, approvalsRecord: true, expectedVersion: expectedVersion ?? null });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
18
31
|
import {
|
|
19
32
|
ValidationError,
|
|
20
33
|
VALID_LAYERS,
|
|
@@ -449,7 +462,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
449
462
|
// The tier-1 -> tier-2 pointer is pulled out BEFORE metadata validation and
|
|
450
463
|
// stored as top-level columns, not inside metadata. When this was written
|
|
451
464
|
// metadata was a JSON string the store could not filter into; it is a
|
|
452
|
-
// subdocument now, but the pointer stays top-level because it is
|
|
465
|
+
// subdocument now (EMET ref 6), but the pointer stays top-level because it is
|
|
453
466
|
// indexed there and the reverse lookup depends on that index.
|
|
454
467
|
const { derived_from: rawProvenance, ...restMetadata } = metadata || {};
|
|
455
468
|
let provenance = validateProvenance(rawProvenance);
|
|
@@ -495,7 +508,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
495
508
|
const keywordLayer = determineLayer(validatedContent, validatedMetadata);
|
|
496
509
|
const targetLayer = validatedLayer || FALLBACK_LAYER;
|
|
497
510
|
// Lesson fields are refused at the door when the reader would never count
|
|
498
|
-
// them (2026-09-29,
|
|
511
|
+
// them (2026-09-29, procedural ref 397): a malformed key, a key off the meta
|
|
499
512
|
// layer, or a disposition that settles nothing. Checked on the caller's
|
|
500
513
|
// metadata as sent, before anything is written.
|
|
501
514
|
{
|
|
@@ -530,6 +543,11 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
530
543
|
}
|
|
531
544
|
} else {
|
|
532
545
|
validatedMetadata.source = sourceDecision.tag;
|
|
546
|
+
if (sourceDecision.from === 'declared' || sourceDecision.from === 'auto' || sourceDecision.from === 'subkey') {
|
|
547
|
+
validatedMetadata.source_basis = sourceDecision.from;
|
|
548
|
+
if (sourceDecision.login) validatedMetadata.source_login = sourceDecision.login;
|
|
549
|
+
if (sourceDecision.from === 'subkey' && sourceDecision.key_id) validatedMetadata.key_id = sourceDecision.key_id;
|
|
550
|
+
}
|
|
533
551
|
delete validatedMetadata.source_defaulted;
|
|
534
552
|
if (sourceDecision.tag) delete validatedMetadata.source_missing;
|
|
535
553
|
else validatedMetadata.source_missing = true;
|
|
@@ -555,6 +573,12 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
555
573
|
}
|
|
556
574
|
validatedMetadata.client_id = stamp.client_id;
|
|
557
575
|
}
|
|
576
|
+
await sealAttestation(validatedMetadata, {
|
|
577
|
+
session_id: options && options.session_id,
|
|
578
|
+
inherited: options && options.inheritedAttestation === true,
|
|
579
|
+
db: dbManager && dbManager.db,
|
|
580
|
+
dbManager
|
|
581
|
+
});
|
|
558
582
|
|
|
559
583
|
// A4: importance is a named bucket. The float is the bucket midpoint
|
|
560
584
|
// unless the caller named a degree (low/mid/high) or still sent a number.
|
|
@@ -572,7 +596,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
572
596
|
}
|
|
573
597
|
|
|
574
598
|
// A5: the charter says whether this layer wants a provenance pointer.
|
|
575
|
-
// 2026-09-29 (
|
|
599
|
+
// 2026-09-29 (procedural ref 398): the pointer is STAMPED when the caller names
|
|
576
600
|
// its transcript session, and a layer whose charter REQUIRES provenance
|
|
577
601
|
// (semantic, procedural) REFUSES a write that has no source at all. The
|
|
578
602
|
// transport is stateless, so the server never guesses which host's session
|
|
@@ -677,7 +701,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
677
701
|
const importance = validatedMetadata.importance;
|
|
678
702
|
|
|
679
703
|
const columns = provenanceColumns(provenance);
|
|
680
|
-
// 2026-09-05: the digest is computed BEFORE the write and stored
|
|
704
|
+
// EMET ref 12 (2026-09-05): the digest is computed BEFORE the write and stored
|
|
681
705
|
// as a column, not only compared in the read-back receipt. Without it the
|
|
682
706
|
// aleph claim held for exactly one instant - verified at write, never again.
|
|
683
707
|
const contentSha256 = digest(validatedContent);
|
|
@@ -705,7 +729,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
705
729
|
context: validatedMetadata.context || '',
|
|
706
730
|
emotional_intensity: emotionalIntensity,
|
|
707
731
|
importance,
|
|
708
|
-
// 2026-09-05: stored as a real subdocument, not a JSON string.
|
|
732
|
+
// EMET ref 6 (2026-09-05): stored as a real subdocument, not a JSON string.
|
|
709
733
|
// The promoted columns below stay because they are indexed and the vector
|
|
710
734
|
// filter reads them.
|
|
711
735
|
metadata: validatedMetadata,
|
|
@@ -730,7 +754,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
730
754
|
chain_sha256: chainSha256
|
|
731
755
|
};
|
|
732
756
|
|
|
733
|
-
// Option A correction back-pointer (
|
|
757
|
+
// Option A correction back-pointer (Alex, 2026-10-02, procedural ref 405 item 4): a new entry may name the
|
|
734
758
|
// entry it partly corrects. The parent must exist in the same layer, and is checked BEFORE the insert so a
|
|
735
759
|
// refused correction writes nothing.
|
|
736
760
|
if (validatedMetadata.corrects !== undefined && validatedMetadata.corrects !== null) {
|
|
@@ -753,7 +777,7 @@ export async function saveMemory(dbManager, content, layer = null, metadata = {}
|
|
|
753
777
|
const correction = (validatedMetadata.corrects !== undefined && validatedMetadata.corrects !== null)
|
|
754
778
|
? await markCorrectedBy(dbManager, targetLayer, validatedMetadata.corrects, memoryId)
|
|
755
779
|
: null;
|
|
756
|
-
// the
|
|
780
|
+
// the reviewer N4: a correction whose parent could not be marked is still saved,
|
|
757
781
|
// but the caller is told in words, not only by a boolean.
|
|
758
782
|
if (correction && correction.marked !== true) {
|
|
759
783
|
warnings.push(`corrects: ${correction.parent_id} - the parent entry was not marked corrected_by (missing in ${targetLayer}, or the mark did not hold); the correction is saved, the link is one-sided until the parent is marked`);
|
|
@@ -946,8 +970,8 @@ export async function resolveSupersessionChain(read, entry, maxHops = 50) {
|
|
|
946
970
|
}
|
|
947
971
|
|
|
948
972
|
/**
|
|
949
|
-
* Option A correction back-pointer (
|
|
950
|
-
* second read of
|
|
973
|
+
* Option A correction back-pointer (Alex, 2026-10-02, procedural ref 405 item 4; the tester's sketch in his
|
|
974
|
+
* second read of episodic ref 604). Episodic entries are immutable events, so a PARTIAL correction cannot
|
|
951
975
|
* supersede its parent: supersession would put an end date on the whole parent and present every item
|
|
952
976
|
* in it as replaced. Instead the new entry carries metadata.corrects = <parent id>, and the parent is MARKED:
|
|
953
977
|
* its corrected_by array gains the new id. The parent's content, digest and is_live are untouched; its
|
|
@@ -960,7 +984,7 @@ export async function resolveSupersessionChain(read, entry, maxHops = 50) {
|
|
|
960
984
|
* corrected_by mark): the new metadata digest and, when a signing key is set
|
|
961
985
|
* and the row's existing signature verifies against its metadata before the
|
|
962
986
|
* marks (aleph.js signatureCoversPriorMetadata), a fresh signature over the
|
|
963
|
-
* same content digest and the new metadata digest (2026-10-02, the
|
|
987
|
+
* same content digest and the new metadata digest (2026-10-02, the tester's
|
|
964
988
|
* finding: the marks left signed:false behind). With no key, or a signature
|
|
965
989
|
* that does not verify, the signature is left exactly as it is: never re-signed
|
|
966
990
|
* blindly, so a row changed outside the sanctioned path keeps reading
|
|
@@ -980,7 +1004,7 @@ export async function markCorrectedBy(dbManager, layer, parentId, childId) {
|
|
|
980
1004
|
const parent = await col.findOne({ id: parentId });
|
|
981
1005
|
if (!parent) return { parent_id: parentId, marked: false };
|
|
982
1006
|
const cid = Number(childId);
|
|
983
|
-
// Race-safe (the
|
|
1007
|
+
// Race-safe (the reviewer N3 / the tester, 2026-10-02): the id is ADDED atomically
|
|
984
1008
|
// ($addToSet on the column and on the metadata copy), never written back
|
|
985
1009
|
// from a stale read, so two simultaneous corrections of one parent both
|
|
986
1010
|
// land. A legacy string-metadata row falls back to read-modify-write.
|
|
@@ -996,7 +1020,7 @@ export async function markCorrectedBy(dbManager, layer, parentId, childId) {
|
|
|
996
1020
|
// Re-attest from what is actually stored. The digest is set only if the
|
|
997
1021
|
// stored metadata is still exactly the metadata it was computed from: the
|
|
998
1022
|
// compare-and-set filter is the WHOLE metadata value, not only corrected_by
|
|
999
|
-
// (the
|
|
1023
|
+
// (the tester, 2026-10-02), so a concurrent change to any metadata field in
|
|
1000
1024
|
// between (another mark, a superseded_by, anything) makes the filter miss,
|
|
1001
1025
|
// and the loop re-reads and re-digests. The last re-attest therefore always
|
|
1002
1026
|
// covers what is stored. The value compared is the one just read back, so
|
|
@@ -1043,15 +1067,6 @@ export async function reviseMemory(dbManager, layer, target_id, content, metadat
|
|
|
1043
1067
|
if (metadata !== null && metadata !== undefined && !isPlainObject(metadata)) {
|
|
1044
1068
|
throw new ValidationError('metadata', 'Metadata must be an object', typeof metadata);
|
|
1045
1069
|
}
|
|
1046
|
-
// 2026-09-29 (a decision record): a revision with no source of its own cites
|
|
1047
|
-
// the entry it revises, so revising a semantic or procedural entry is never
|
|
1048
|
-
// refused for want of a pointer. A caller-supplied derived_from wins.
|
|
1049
|
-
const revisionMetadata = {
|
|
1050
|
-
...(metadata || {}),
|
|
1051
|
-
supersedes: targetId,
|
|
1052
|
-
...((metadata && metadata.derived_from) ? {} : { derived_from: { entries: [targetId] } })
|
|
1053
|
-
};
|
|
1054
|
-
|
|
1055
1070
|
await dbManager.getConnection(validatedLayer);
|
|
1056
1071
|
const col = dbManager.collectionFor(validatedLayer);
|
|
1057
1072
|
|
|
@@ -1063,15 +1078,29 @@ export async function reviseMemory(dbManager, layer, target_id, content, metadat
|
|
|
1063
1078
|
if (isMemorySuperseded(targetMd)) {
|
|
1064
1079
|
throw new ValidationError('target_id', `Entry ${targetId} is already superseded by ${targetMd.superseded_by} - revise the CURRENT entry, not the archived one`);
|
|
1065
1080
|
}
|
|
1081
|
+
// 2026-09-29 (procedural ref 398): a revision with no source of its own cites
|
|
1082
|
+
// the entry it revises, so revising a semantic or procedural entry is never
|
|
1083
|
+
// refused for want of a pointer. A caller-supplied derived_from wins.
|
|
1084
|
+
// Attestation copied from the parent wins over a caller value. A different
|
|
1085
|
+
// value was already refused.
|
|
1086
|
+
const priorAttestation = frozenAttestation(targetMd, metadata);
|
|
1087
|
+
const revisionMetadata = {
|
|
1088
|
+
...(metadata || {}),
|
|
1089
|
+
...(priorAttestation || {}),
|
|
1090
|
+
supersedes: targetId,
|
|
1091
|
+
...((metadata && metadata.derived_from) ? {} : { derived_from: { entries: [targetId] } })
|
|
1092
|
+
};
|
|
1066
1093
|
|
|
1067
|
-
const saved = await saveMemory(dbManager, validatedContent, validatedLayer, revisionMetadata, logger, null
|
|
1094
|
+
const saved = await saveMemory(dbManager, validatedContent, validatedLayer, revisionMetadata, logger, null, {
|
|
1095
|
+
inheritedAttestation: Boolean(priorAttestation)
|
|
1096
|
+
});
|
|
1068
1097
|
|
|
1069
1098
|
targetMd.superseded_by = saved.id;
|
|
1070
1099
|
targetMd.superseded_at = saved.timestamp;
|
|
1071
1100
|
// Write BOTH the metadata copy and the promoted top-level column. The
|
|
1072
1101
|
// top-level one is what a reader follows (indexed); the metadata copy
|
|
1073
1102
|
// stays for audit. Always written as a subdocument - a parent still
|
|
1074
|
-
// carrying the legacy string shape is converted by this touch.
|
|
1103
|
+
// carrying the legacy string shape is converted by this touch (EMET ref 6).
|
|
1075
1104
|
// is_live is left as it was. Supersession is the end date and the link
|
|
1076
1105
|
// to the new entry, not a flag that hides the old one.
|
|
1077
1106
|
await col.updateOne({ id: targetId }, { $set: {
|
|
@@ -1626,7 +1655,7 @@ export async function queryLayer(dbManager, layer, options = {}, logger = null,
|
|
|
1626
1655
|
// there is not even one to read - that transport's logger discards warn().
|
|
1627
1656
|
// So the ignored keys ride back in the response envelope, where the person
|
|
1628
1657
|
// who wrote the bad filter will actually see them. This is the surface that
|
|
1629
|
-
// would have caught the 2026-09-08 phantom defect as it happened.
|
|
1658
|
+
// would have caught the 2026-09-08 phantom defect as it happened (ref 259).
|
|
1630
1659
|
if (validatedOptions._unknown_filter_keys) {
|
|
1631
1660
|
attachWarnings(results, [
|
|
1632
1661
|
`Ignored unrecognised filter key(s): ${validatedOptions._unknown_filter_keys.join(', ')}. `
|
|
@@ -2053,6 +2082,7 @@ export async function sessionOpen({ slug, load_id } = {}, logger = null, dbManag
|
|
|
2053
2082
|
: 'Nothing was merged into another session.')
|
|
2054
2083
|
: null;
|
|
2055
2084
|
logger?.info?.('transcript session open', { session_id, ...(suffixed ? { requested } : {}) });
|
|
2085
|
+
const session_handle = await subkeyHandleForOpen(rawActor(), base, now);
|
|
2056
2086
|
// Startup page completion (2026-10-03). Through dispatch, an open with no
|
|
2057
2087
|
// load_id or with pages missing is refused before this runs (the
|
|
2058
2088
|
// startup_pages gate); a direct call is told what was checked.
|
|
@@ -2077,6 +2107,7 @@ export async function sessionOpen({ slug, load_id } = {}, logger = null, dbManag
|
|
|
2077
2107
|
return {
|
|
2078
2108
|
session_id,
|
|
2079
2109
|
base,
|
|
2110
|
+
...(session_handle ? { session_handle } : {}),
|
|
2080
2111
|
drops_seen,
|
|
2081
2112
|
...(drops_unverified ? { drops_unverified: true } : {}),
|
|
2082
2113
|
next_exchange_index: 1,
|
|
@@ -2130,6 +2161,19 @@ export function startupPagesCheck(loadId, logger = null, sessionId = null, now =
|
|
|
2130
2161
|
return { load_id: c.load_id, checked: true, complete: false, of: c.of, served: c.served, missing: c.missing, notice };
|
|
2131
2162
|
}
|
|
2132
2163
|
|
|
2164
|
+
/** The one-time session handle for a sub-key open. Null when this open did not present a key. Never logged. */
|
|
2165
|
+
async function subkeyHandleForOpen(actor, base, now) {
|
|
2166
|
+
if (!actor || setting('SUBKEYS') !== '1') return null;
|
|
2167
|
+
const { takeOpenToken, commitLiveSession } = await import('./subkeys.js');
|
|
2168
|
+
if (!takeOpenToken(actor)) return null;
|
|
2169
|
+
try {
|
|
2170
|
+
return await commitLiveSession(approvalsDeps, actor, base, now);
|
|
2171
|
+
} catch (e) {
|
|
2172
|
+
if (e && e.attributionRefusal) throw new ValidationError('subkey', e.message);
|
|
2173
|
+
throw e;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2133
2177
|
export async function transcriptAppend(args = {}, logger = null) {
|
|
2134
2178
|
const session_id = args.session_id;
|
|
2135
2179
|
const user_message = args.user_message;
|
|
@@ -2181,6 +2225,11 @@ export async function transcriptAppend(args = {}, logger = null) {
|
|
|
2181
2225
|
person_id: stamp.person_id,
|
|
2182
2226
|
client_id: stamp.client_id
|
|
2183
2227
|
};
|
|
2228
|
+
const attr = declaredAttribution();
|
|
2229
|
+
if (attr && attr.basis === 'subkey' && attr.keyId) {
|
|
2230
|
+
row.key_id = attr.keyId;
|
|
2231
|
+
row.source_basis = 'subkey';
|
|
2232
|
+
}
|
|
2184
2233
|
|
|
2185
2234
|
// Build 1 (2026-09-27): the first append of a session must carry the
|
|
2186
2235
|
// measured machine line, or say there is no local shell. The rule lived in
|
|
@@ -2497,6 +2546,8 @@ export async function readDoc(doc_id, logger = null, opts = {}) {
|
|
|
2497
2546
|
return { doc_id: doc.doc_id, content, sha256: doc.sha256, version: doc.version,
|
|
2498
2547
|
updated_at: doc.updated_at, updated_by: doc.updated_by || null,
|
|
2499
2548
|
client_id: doc.client_id || null, session_id: doc.session_id || null,
|
|
2549
|
+
...(doc.key_id ? { key_id: doc.key_id } : {}),
|
|
2550
|
+
...(doc.source_basis ? { source_basis: doc.source_basis } : {}),
|
|
2500
2551
|
path_hint: doc.path_hint || null, size: doc.content.length,
|
|
2501
2552
|
size_unit: 'characters', size_bytes: Buffer.byteLength(doc.content, 'utf8'),
|
|
2502
2553
|
...(page ? { page } : {}),
|
|
@@ -2539,6 +2590,8 @@ export async function readDocHistory(doc_id, version = null, logger = null) {
|
|
|
2539
2590
|
const retained = await hist.find({ doc_id }, { projection: { content: 0 } }).sort({ version: 1 }).toArray();
|
|
2540
2591
|
const rows = retained.map(r => ({ version: r.version, sha256: r.sha256 || null, size: r.size ?? null,
|
|
2541
2592
|
updated_at: r.updated_at || null, updated_by: r.updated_by || null, client_id: r.client_id || null, session_id: r.session_id || null, archived_at: r.archived_at || null,
|
|
2593
|
+
...(r.key_id ? { key_id: r.key_id } : {}),
|
|
2594
|
+
...(r.source_basis ? { source_basis: r.source_basis } : {}),
|
|
2542
2595
|
conformance: r.conformance ? r.conformance.verdict : null }));
|
|
2543
2596
|
return { doc_id, current_version: current ? current.version : null, retained_count: rows.length, retained: rows,
|
|
2544
2597
|
note: current ? null : 'No current document by this id; only retained revisions, if any.' };
|
|
@@ -2547,6 +2600,8 @@ export async function readDocHistory(doc_id, version = null, logger = null) {
|
|
|
2547
2600
|
return { doc_id, version, is_current: true, content: current.content, sha256: current.sha256, size: current.content.length,
|
|
2548
2601
|
updated_at: current.updated_at, updated_by: current.updated_by || null,
|
|
2549
2602
|
client_id: current.client_id || null, session_id: current.session_id || null,
|
|
2603
|
+
...(current.key_id ? { key_id: current.key_id } : {}),
|
|
2604
|
+
...(current.source_basis ? { source_basis: current.source_basis } : {}),
|
|
2550
2605
|
conformance: current.conformance || null };
|
|
2551
2606
|
}
|
|
2552
2607
|
// More than one archived row can carry a version when a write was retried;
|
|
@@ -2557,6 +2612,8 @@ export async function readDocHistory(doc_id, version = null, logger = null) {
|
|
|
2557
2612
|
return { doc_id, version, is_current: false, content: row.content, sha256: row.sha256 || null, size: row.content.length,
|
|
2558
2613
|
updated_at: row.updated_at || null, updated_by: row.updated_by || null, archived_at: row.archived_at || null,
|
|
2559
2614
|
client_id: row.client_id || null, session_id: row.session_id || null,
|
|
2615
|
+
...(row.key_id ? { key_id: row.key_id } : {}),
|
|
2616
|
+
...(row.source_basis ? { source_basis: row.source_basis } : {}),
|
|
2560
2617
|
conformance: row.conformance || null,
|
|
2561
2618
|
// Re-hashed on read, as a layer row is: a retained revision that
|
|
2562
2619
|
// does not match its own digest is returned and flagged, never hidden.
|
|
@@ -2569,7 +2626,7 @@ export async function readDocHistory(doc_id, version = null, logger = null) {
|
|
|
2569
2626
|
}
|
|
2570
2627
|
|
|
2571
2628
|
/**
|
|
2572
|
-
* Records are superseded, never amended (2026-09-26,
|
|
2629
|
+
* Records are superseded, never amended (2026-09-26, Alex: "enforce
|
|
2573
2630
|
* supersession ISO practices"). ISO 9001 7.5.3 / ISO 13485 4.2.5 separate
|
|
2574
2631
|
* DOCUMENTS, which are revised under control, from RECORDS, which are evidence
|
|
2575
2632
|
* of what happened and are not altered. The handoff is a record: it attests to
|
|
@@ -2606,17 +2663,18 @@ function _refuseRecordAmendment(doc_id, tool) {
|
|
|
2606
2663
|
}
|
|
2607
2664
|
|
|
2608
2665
|
/**
|
|
2609
|
-
* The main records are the hub's (C1, 2026-10-01
|
|
2666
|
+
* The main records are the hub's (C1, 2026-10-01; review note section
|
|
2667
|
+
* 3.5). write_doc and patch_doc refuse the main HANDOFF.md or STATE.md when the
|
|
2610
2668
|
* write's source is a bot tag (bots/<tag>/HANDOFF.md listed in
|
|
2611
2669
|
* EMET_RECORD_DOCS), and nothing is written. So do the other doors that can
|
|
2612
|
-
* change a main record (the
|
|
2670
|
+
* change a main record (the tester's F1 on PR 2): retire_doc, rename_doc (from
|
|
2613
2671
|
* or onto one) and accept_nonconformance - retire and rename set retired_by,
|
|
2614
2672
|
* not updated_by, so the close alone could not see them. The source is the one the door
|
|
2615
2673
|
* stamps as updated_by: the caller's, else EMET_SOURCE_TAG. A records rule like
|
|
2616
2674
|
* the record-amendment refusal above, so it applies in every graph mode.
|
|
2617
2675
|
*
|
|
2618
|
-
* Two further guards on the same doors (2026-10-01, the owner's approval to
|
|
2619
|
-
* make the bot tags fully functional; the
|
|
2676
|
+
* Two further guards on the same doors (2026-10-01, the owner's 4:17 PM approval to
|
|
2677
|
+
* make the bot tags fully functional; the tester's review of PR 3):
|
|
2620
2678
|
* - REGISTERED HOSTS ONLY on the main records. With a source registry
|
|
2621
2679
|
* configured (EMET_SOURCE_TAGS, parsed as save_to_layer parses it), the
|
|
2622
2680
|
* main handoff and board take writes only from a registered tag. An
|
|
@@ -2630,7 +2688,21 @@ function _refuseRecordAmendment(doc_id, tool) {
|
|
|
2630
2688
|
* Hosts (the hub) keep writing anywhere under bots/.
|
|
2631
2689
|
*/
|
|
2632
2690
|
export function sourceRegistry() {
|
|
2633
|
-
|
|
2691
|
+
const env = String(setting('SOURCE_TAGS') ?? '').split(',').map((s) => s.trim()).filter((s) => s !== '');
|
|
2692
|
+
// Attribution v2: tags approved in emet/CLIENT_APPROVALS.md count as
|
|
2693
|
+
// registered once a registry exists (a fresh install with no registry stays open).
|
|
2694
|
+
return env.length ? [...new Set([...env, ...approvedTags()])] : env;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* Attribution v2 (the reviewer A3): a declared tag never gains rights beyond the login's
|
|
2699
|
+
* tag. A guard is run for every tag returned here and the write must pass all.
|
|
2700
|
+
*/
|
|
2701
|
+
export function rightsTags(tag) {
|
|
2702
|
+
const d = declaredAttribution();
|
|
2703
|
+
if (d && d.basis === 'subkey' && d.effective && tag === d.effective && d.login && d.login !== tag) return [tag, d.login];
|
|
2704
|
+
if (d && d.declared && tag === d.declared && d.login && d.login !== tag) return [tag, d.login];
|
|
2705
|
+
return [tag];
|
|
2634
2706
|
}
|
|
2635
2707
|
|
|
2636
2708
|
/**
|
|
@@ -2645,20 +2717,27 @@ export function sourceRegistry() {
|
|
|
2645
2717
|
export function resolvedSourceTag(caller) {
|
|
2646
2718
|
const bound = boundSourceTag();
|
|
2647
2719
|
const tag = typeof caller === 'string' ? caller.trim() : '';
|
|
2648
|
-
if (typeof bound === 'string' && bound) return { tag: bound, from: 'bound' };
|
|
2649
2720
|
if (bound === undefined) return { tag: tag || null, from: 'caller' };
|
|
2721
|
+
// Attribution v2: the per-request tag decided by attribution.preflight is the
|
|
2722
|
+
// stamp (declared, or generated for a login with none). Rights stay the
|
|
2723
|
+
// login's: see rightsTags().
|
|
2724
|
+
const d = declaredAttribution();
|
|
2725
|
+
if (d && d.basis === 'subkey' && d.effective) return { tag: d.effective, from: 'subkey', login: bound || null, key_id: d.keyId || null };
|
|
2726
|
+
if (d && d.declared && d.basis === 'declared' && tag === d.declared) return { tag, from: 'declared', login: bound || null };
|
|
2727
|
+
if (typeof bound === 'string' && bound) return { tag: bound, from: 'bound' };
|
|
2650
2728
|
if (tag && sourceRegistry().includes(tag)) return { tag, from: 'legacy' };
|
|
2729
|
+
if (d && d.basis === 'auto' && d.declared) return { tag: d.declared, from: 'auto' };
|
|
2651
2730
|
return { tag: null, from: 'legacy' };
|
|
2652
2731
|
}
|
|
2653
2732
|
|
|
2654
|
-
// Look-alike doc ids (the
|
|
2733
|
+
// Look-alike doc ids (the reviewer's recheck d1-d5, 2026-10-02): a `.` or `..` segment,
|
|
2655
2734
|
// a leading `/`, or a `bots/` prefix in the wrong case would let a write pass
|
|
2656
2735
|
// the prefix checks below while naming a record they are meant to fence (d1:
|
|
2657
|
-
// bots/
|
|
2736
|
+
// bots/reviewer-bot/../intake-bot/HANDOFF.md passed the own-dir check). Doc
|
|
2658
2737
|
// ids are flat strings, never resolved as paths, so these shapes are refused
|
|
2659
2738
|
// outright, before any guard, on every door that calls this.
|
|
2660
2739
|
//
|
|
2661
|
-
// Hardening (the
|
|
2740
|
+
// Hardening (the tester's scope on the doc-id fix, 2026-10-02): also refused are
|
|
2662
2741
|
// control and invisible-format characters (\p{Cc}, \p{Cf}) anywhere; leading or
|
|
2663
2742
|
// trailing whitespace on the id or on any segment; a backslash; the `/`, `.`/`..`
|
|
2664
2743
|
// and `bots/` checks run on the NFKC form too (so full-width dots and slashes,
|
|
@@ -2705,7 +2784,7 @@ export function refuseLookalikeDocId(doc_id, tool) {
|
|
|
2705
2784
|
}
|
|
2706
2785
|
}
|
|
2707
2786
|
|
|
2708
|
-
// Cleanup path (the
|
|
2787
|
+
// Cleanup path (the tester's scope): a look-alike id may already exist in a store
|
|
2709
2788
|
// written before these checks. retire_doc, and rename_doc FROM such an id, are let
|
|
2710
2789
|
// through for a registered host tag only: EMET_SOURCE_TAGS set, the tag listed,
|
|
2711
2790
|
// and not a bot tag. Never for a write, a patch, a rename onto it, or a bot.
|
|
@@ -2715,7 +2794,27 @@ function mayCleanUpLookalike(tag) {
|
|
|
2715
2794
|
}
|
|
2716
2795
|
|
|
2717
2796
|
export function refuseBotMainRecord(doc_id, updated_by, tool, options = {}) {
|
|
2797
|
+
for (const t of rightsTags(updated_by)) refuseBotMainRecordOne(doc_id, t, tool, options);
|
|
2798
|
+
}
|
|
2799
|
+
function refuseBotMainRecordOne(doc_id, updated_by, tool, options = {}) {
|
|
2718
2800
|
const tag = updated_by || setting('SOURCE_TAG') || null;
|
|
2801
|
+
// The client-approvals record is server-only (the reviewer 4): no host writes,
|
|
2802
|
+
// patches, retires, renames, restores or accepts it.
|
|
2803
|
+
if (typeof doc_id === 'string' && FOLD(doc_id.normalize('NFKC')) === FOLD(APPROVALS_DOC) && !(options && options.approvalsRecord === true)) {
|
|
2804
|
+
throw new ValidationError(`${tool}: ${doc_id} is kept by the server (client approvals). Decide clients with emet_client_approve. Nothing was written.`);
|
|
2805
|
+
}
|
|
2806
|
+
// A3 without a registry (the reviewer re-review): a declared or generated tag writes
|
|
2807
|
+
// under bots/ only inside bots/<its own tag>/ and only once approved.
|
|
2808
|
+
{
|
|
2809
|
+
const d = declaredAttribution();
|
|
2810
|
+
if (d && d.declared && tag === d.declared && (d.basis === 'declared' || d.basis === 'auto') &&
|
|
2811
|
+
typeof doc_id === 'string' && doc_id.startsWith('bots/')) {
|
|
2812
|
+
const approved = approvedTags().includes(tag) || sourceRegistry().includes(tag);
|
|
2813
|
+
if (!doc_id.startsWith(botRecordDir(tag)) || !approved) {
|
|
2814
|
+
throw new ValidationError(`${tool}: ${doc_id} is a bot record and "${tag}" is a ${approved ? 'declared tag outside its own folder' : 'declared tag not yet approved'} - a declared tag writes only bots/${tag}/ once approved. Nothing was written.`);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2719
2818
|
if (!(options.cleanup === true && mayCleanUpLookalike(tag))) refuseLookalikeDocId(doc_id, tool);
|
|
2720
2819
|
const bot = !!tag && isBotTag(tag);
|
|
2721
2820
|
if (bot && typeof doc_id === 'string' && doc_id.startsWith('bots/') && !doc_id.startsWith(botRecordDir(tag))) {
|
|
@@ -2725,7 +2824,7 @@ export function refuseBotMainRecord(doc_id, updated_by, tool, options = {}) {
|
|
|
2725
2824
|
`Hand it to the hub instead. Nothing was written.`
|
|
2726
2825
|
);
|
|
2727
2826
|
}
|
|
2728
|
-
// Registry guard on bots/ too (the
|
|
2827
|
+
// Registry guard on bots/ too (the tester's PR 5 review; the reviewer's fix (a), 2026-10-02): with EMET_SOURCE_TAGS
|
|
2729
2828
|
// set, a non-bot write under bots/ is refused unless its tag is registered; a missing source counts as
|
|
2730
2829
|
// unregistered. Without this, a misspelled or unregistered tag is not a bot tag, so the cross-bot guard
|
|
2731
2830
|
// above never fires and it can write any bot's records or the roster. A listed bot is already fenced to
|
|
@@ -2756,28 +2855,31 @@ export function refuseBotMainRecord(doc_id, updated_by, tool, options = {}) {
|
|
|
2756
2855
|
}
|
|
2757
2856
|
|
|
2758
2857
|
/**
|
|
2759
|
-
* retire_doc and rename_doc are not bot tools at all (
|
|
2760
|
-
* decision 1, as relayed: "retire_doc isn't needed for bots: keep it
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2858
|
+
* retire_doc and rename_doc are not bot tools at all (Alex, 2026-10-01 4:27 PM
|
|
2859
|
+
* ET, decision 1, as relayed: "retire_doc isn't needed for bots: keep it
|
|
2860
|
+
* owner/hub only and out of every bot profile"; procedural ref 407; stage 1 of the
|
|
2861
|
+
* ref 358 startup amendment, admitted by the tester 2026-10-02). A profile is not a
|
|
2763
2862
|
* security boundary, so the door refuses them under ANY bot tag, for every
|
|
2764
|
-
* doc_id - not only the main records refuseBotMainRecord covers.
|
|
2765
|
-
* covers retire_doc only; rename_doc stays owner-only under
|
|
2766
|
-
* (owner-confirmed 2026-10-02: every other owner-only tool is
|
|
2863
|
+
* doc_id - not only the main records refuseBotMainRecord covers. ref 407(1)
|
|
2864
|
+
* covers retire_doc only; rename_doc stays owner-only under procedural ref 409
|
|
2865
|
+
* (owner-confirmed 2026-10-02 the recorded time: every other owner-only tool is
|
|
2767
2866
|
* unchanged), and it retires its old id internally, so it is refused too. The tag is the
|
|
2768
2867
|
* one the door would stamp: the caller's, else EMET_SOURCE_TAG. Hosts and the
|
|
2769
2868
|
* hub have no bots/<tag>/HANDOFF.md record and are unaffected. Nothing is
|
|
2770
2869
|
* written when refused.
|
|
2771
2870
|
*/
|
|
2772
2871
|
export function refuseBotRetireRename(tag, tool) {
|
|
2872
|
+
for (const t of rightsTags(tag)) refuseBotRetireRenameOne(t, tool);
|
|
2873
|
+
}
|
|
2874
|
+
function refuseBotRetireRenameOne(tag, tool) {
|
|
2773
2875
|
const t = tag || setting('SOURCE_TAG') || null;
|
|
2774
2876
|
if (!t || !isBotTag(t)) return;
|
|
2775
2877
|
throw new ValidationError(
|
|
2776
2878
|
'source',
|
|
2777
2879
|
`${tool} is not a bot tool: ${t} is a bot tag, and ` +
|
|
2778
2880
|
(tool === 'retire_doc'
|
|
2779
|
-
? `retiring documents is the owner's and the hub's (decision 1,
|
|
2780
|
-
: `renaming documents stays owner-only (
|
|
2881
|
+
? `retiring documents is the owner's and the hub's (decision 1, procedural ref 407 item 1)`
|
|
2882
|
+
: `renaming documents stays owner-only (procedural ref 409)`) +
|
|
2781
2883
|
`. Ask the hub. Nothing was changed.`
|
|
2782
2884
|
);
|
|
2783
2885
|
}
|
|
@@ -2786,7 +2888,7 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2786
2888
|
try {
|
|
2787
2889
|
if (!doc_id || typeof doc_id !== 'string') throw new ValidationError('doc_id must be a non-empty string');
|
|
2788
2890
|
// write_doc, patch_doc (which lands here) and rename_doc's copy (reissued_from).
|
|
2789
|
-
refuseBotMainRecord(doc_id, updated_by, options && options.reissued_from ? 'rename_doc' : 'write_doc', options && options.serverProvision === true ? { serverProvision: true } : {});
|
|
2891
|
+
refuseBotMainRecord(doc_id, updated_by, options && options.reissued_from ? 'rename_doc' : 'write_doc', options && options.serverProvision === true ? { serverProvision: true, ...(options.approvalsRecord === true ? { approvalsRecord: true } : {}) } : {});
|
|
2790
2892
|
if (typeof rawContent !== 'string' || rawContent.length === 0) throw new ValidationError('content must be a non-empty string');
|
|
2791
2893
|
if (rawContent.length > 2000000) throw new ValidationError('content exceeds 2MB limit');
|
|
2792
2894
|
// patch_doc lands here too, so one
|
|
@@ -2800,6 +2902,11 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2800
2902
|
try {
|
|
2801
2903
|
const prev = await col.findOne({ doc_id });
|
|
2802
2904
|
const version = (prev && prev.version ? prev.version : 0) + 1;
|
|
2905
|
+
// Compare-and-set for the client-approvals record (the reviewer round 3): checked
|
|
2906
|
+
// here before anything is archived, and again atomically in the update filter.
|
|
2907
|
+
const casOn = options && options.approvalsRecord === true && Object.prototype.hasOwnProperty.call(options, 'expectedVersion');
|
|
2908
|
+
const casConflict = () => { const e = new ValidationError(`${doc_id} changed under this write (compare-and-set). Nothing was written.`); e.versionConflict = true; return e; };
|
|
2909
|
+
if (casOn && ((prev ? prev.version : null) ?? null) !== (options.expectedVersion ?? null)) throw casConflict();
|
|
2803
2910
|
// A record in force is superseded only by a revision that names it.
|
|
2804
2911
|
// Checked before anything is archived, so a refused write leaves no trace.
|
|
2805
2912
|
if (prev && isRecordDoc(doc_id) && options && options.restore_of && (options.supersedes === undefined || options.supersedes === null)) {
|
|
@@ -2827,11 +2934,10 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2827
2934
|
}
|
|
2828
2935
|
}
|
|
2829
2936
|
const registry = sourceRegistry();
|
|
2830
|
-
const tag = updated_by || '';
|
|
2831
|
-
const registered = registry.length === 0 || registry.includes(tag);
|
|
2832
|
-
const ownBotFolder = isBotTag(updated_by) && doc_id.startsWith(`bots/${updated_by}/`);
|
|
2833
2937
|
const serverProvision = options && options.serverProvision === true;
|
|
2834
|
-
|
|
2938
|
+
// Attribution v2 (A3): the declared tag and the login's tag must both pass.
|
|
2939
|
+
const passes = (t) => registry.length === 0 || registry.includes(t || '') || (isBotTag(t) && doc_id.startsWith(`bots/${t}/`));
|
|
2940
|
+
if (!rightsTags(updated_by).every(passes) && !serverProvision && !(isDropDoc(doc_id) && !prev)) {
|
|
2835
2941
|
throw new ValidationError(`write_doc: ${updated_by || 'no source'} is not a registered source. This session may read, and may write a new drop only. Nothing was written.`);
|
|
2836
2942
|
}
|
|
2837
2943
|
if (isDropDoc(doc_id) && !prev) {
|
|
@@ -2843,7 +2949,7 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2843
2949
|
options = { ...options, drop_bucket: bucket };
|
|
2844
2950
|
}
|
|
2845
2951
|
const floorExempt = options && options.provision;
|
|
2846
|
-
const internal = options && (options.provision || options.edited || options.setupReissue || options.restore_of);
|
|
2952
|
+
const internal = options && (options.provision || options.edited || options.setupReissue || options.restore_of || (options.approvalsRecord === true && options.serverProvision === true && doc_id === APPROVALS_DOC));
|
|
2847
2953
|
if (options && options.provision && prev && doc_id === stateDoc()) {
|
|
2848
2954
|
throw new ValidationError('The board already exists. Provision does not replace it. Nothing was written.');
|
|
2849
2955
|
}
|
|
@@ -2916,7 +3022,7 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2916
3022
|
const now = new Date();
|
|
2917
3023
|
// Validation runs HERE: after redaction, on the exact text that will be
|
|
2918
3024
|
// stored, with the previous revision already in hand for the
|
|
2919
|
-
// carry-forward comparison. It marks and never rejects, so its
|
|
3025
|
+
// carry-forward comparison. It marks and never rejects (ref 304), so its
|
|
2920
3026
|
// result is a field on the row rather than a branch in this function.
|
|
2921
3027
|
// Writing the field whole also enforces L6 - the fresh object carries no
|
|
2922
3028
|
// acceptance, so an acceptance covers the one revision it was given to.
|
|
@@ -2957,11 +3063,31 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2957
3063
|
retired: false,
|
|
2958
3064
|
size_note: 'baseline is the size when this session first touched the document'
|
|
2959
3065
|
};
|
|
3066
|
+
const attr = declaredAttribution();
|
|
3067
|
+
if (attr && attr.basis === 'subkey' && attr.keyId) {
|
|
3068
|
+
writtenRow.key_id = attr.keyId;
|
|
3069
|
+
writtenRow.source_basis = 'subkey';
|
|
3070
|
+
}
|
|
2960
3071
|
if (options && options.provision && doc_id === stateDoc()) {
|
|
2961
3072
|
const ins = await col.updateOne({ doc_id }, { $setOnInsert: writtenRow }, { upsert: true });
|
|
2962
3073
|
if (ins && ins.upsertedCount === 0) {
|
|
2963
3074
|
throw new ValidationError('The board already exists. Provision does not replace it. Nothing was written.');
|
|
2964
3075
|
}
|
|
3076
|
+
} else if (casOn) {
|
|
3077
|
+
let res;
|
|
3078
|
+
try {
|
|
3079
|
+
res = options.expectedVersion == null
|
|
3080
|
+
// the reviewer delta M1: documents.doc_id has no unique index, so the first
|
|
3081
|
+
// create keys on a deterministic _id; _id is always unique, so a
|
|
3082
|
+
// second racing insert matches it (or hits a duplicate key) and loses.
|
|
3083
|
+
? await col.updateOne({ _id: `${APPROVALS_DOC}#current` }, { $setOnInsert: writtenRow }, { upsert: true })
|
|
3084
|
+
: await col.updateOne({ doc_id, version: options.expectedVersion }, { $set: writtenRow }, { upsert: false });
|
|
3085
|
+
} catch (e) {
|
|
3086
|
+
if (e && (e.code === 11000 || /duplicate key/i.test(String(e.message)))) throw casConflict();
|
|
3087
|
+
throw e;
|
|
3088
|
+
}
|
|
3089
|
+
const won = options.expectedVersion == null ? (res && res.upsertedCount === 1) : (res && res.matchedCount === 1);
|
|
3090
|
+
if (!won) throw casConflict();
|
|
2965
3091
|
} else {
|
|
2966
3092
|
await col.updateOne({ doc_id }, { $set: writtenRow }, { upsert: true });
|
|
2967
3093
|
}
|
|
@@ -2974,6 +3100,7 @@ export async function writeDoc(doc_id, rawContent, updated_by = null, path_hint
|
|
|
2974
3100
|
redacted: redaction.hits, conformance };
|
|
2975
3101
|
} finally { await client.close(); }
|
|
2976
3102
|
} catch (error) {
|
|
3103
|
+
if (error && error.versionConflict) throw error;
|
|
2977
3104
|
if (error instanceof ValidationError || error instanceof EmetError) throw error;
|
|
2978
3105
|
throw new DatabaseError(`Failed to write document: ${error.message}`, 'write_doc');
|
|
2979
3106
|
}
|
|
@@ -3421,7 +3548,7 @@ export async function patchDoc(doc_id, old_str, new_str, updated_by = null, expe
|
|
|
3421
3548
|
}
|
|
3422
3549
|
}
|
|
3423
3550
|
|
|
3424
|
-
// Restored 2026-09-19: both constants were dropped in
|
|
3551
|
+
// Restored 2026-09-19: both constants were dropped in commit-685d822 while the function
|
|
3425
3552
|
// kept referencing them, which made every semantic_recall call throw
|
|
3426
3553
|
// "SEMANTIC_RECALL_LAYERS is not defined".
|
|
3427
3554
|
const SEMANTIC_RECALL_LAYERS = ['episodic', 'semantic', 'procedural', 'meta', 'identity', 'working'];
|
|
@@ -3682,7 +3809,8 @@ const TOOL_DEFINITIONS = [
|
|
|
3682
3809
|
properties: {
|
|
3683
3810
|
recent_limit: { type: "number", description: "How many recent context entries to return (default 5)." },
|
|
3684
3811
|
page: { description: "The startup is served in pages when it is larger than one result (each page fits under 20,000 bytes). Omit for page 1; every page but the last says which page to ask for next - call again with that page, before replying, until a page says it is the last. \"all\" returns the whole startup in one result, for a host with no size limit." },
|
|
3685
|
-
load_id: { type: "string", description: "The load_id your startup returned in `startup_page.load_id`: every emet_initialize result carries one (each page of a paged startup, the whole startup with page \"all\" or when it fits one result, and a store that is not set up yet). Pass it with every later page so all pages come from the same startup, and pass the same load_id to emet_session_open so its startup page check runs on this startup." }
|
|
3812
|
+
load_id: { type: "string", description: "The load_id your startup returned in `startup_page.load_id`: every emet_initialize result carries one (each page of a paged startup, the whole startup with page \"all\" or when it fits one result, and a store that is not set up yet). Pass it with every later page so all pages come from the same startup, and pass the same load_id to emet_session_open so its startup page check runs on this startup." },
|
|
3813
|
+
source: { type: "string", description: "Your own source tag (the bot or host you are). The startup then reports the tag your writes will carry and its approval state." }
|
|
3686
3814
|
}
|
|
3687
3815
|
}
|
|
3688
3816
|
},
|
|
@@ -3692,13 +3820,14 @@ const TOOL_DEFINITIONS = [
|
|
|
3692
3820
|
inputSchema: {
|
|
3693
3821
|
type: "object",
|
|
3694
3822
|
properties: {
|
|
3695
|
-
probe: { type: "boolean", description: "Also write and read back a probe document (default false). Use after a deploy or a configuration change." }
|
|
3823
|
+
probe: { type: "boolean", description: "Also write and read back a probe document (default false). Use after a deploy or a configuration change." },
|
|
3824
|
+
source: { type: "string", description: "Your own source tag. The status then reports the tag your writes will carry and its approval state." }
|
|
3696
3825
|
}
|
|
3697
3826
|
}
|
|
3698
3827
|
},
|
|
3699
3828
|
{
|
|
3700
3829
|
name: "emet_setup_complete",
|
|
3701
|
-
description: "First-time setup, the owner's only: record the user's interview answers as bootstrap/IDENTITY.md, bootstrap/CHARACTER.md and bootstrap/USER.md and as an identity-layer entry, and create the owner person. Every write is read-back verified. Refused under a bot tag. On a store that is already set up it is refused unless reissue is true (the owner's explicit word): a re-run writes new versions of the user's identity documents; prior versions are archived, never destroyed.",
|
|
3830
|
+
description: "First-time setup, the owner's only: record the user's interview answers as bootstrap/IDENTITY.md, bootstrap/CHARACTER.md and bootstrap/USER.md and as an identity-layer entry, and create the owner person. Every write is read-back verified. Refused under a bot tag. On a store that is already set up it is refused unless reissue is true (the owner's explicit word): a re-run writes new versions of the user's identity documents; prior versions are archived, never destroyed. answers.more_than_one_bot is optional. Absent or no leaves sub-keys off. yes writes EMET_SUBKEYS=1, a generated pepper and EMET_SUBKEY_PEPPER_VERSION=1 into .env only when that file is the config source, after the store writes succeed, and does not turn the flag on in this process. A hosted server refuses yes and returns the three variables to set, then redeploy. See docs/SUBKEYS.md. answers.bot_team_backup is optional. Yes or no, in the user's words, the same way as more_than_one_bot. Yes and Yes. record the flag. yes is the recommended choice. Absent or no leaves bot-team backup off. Anything else, including a non-string, is refused before a write. Setup does not copy prompts or settings.",
|
|
3702
3831
|
inputSchema: {
|
|
3703
3832
|
type: "object",
|
|
3704
3833
|
properties: {
|
|
@@ -3711,7 +3840,9 @@ const TOOL_DEFINITIONS = [
|
|
|
3711
3840
|
user_name: { type: "string" },
|
|
3712
3841
|
user_context: { type: "string" },
|
|
3713
3842
|
user_preferences: { type: "string" },
|
|
3714
|
-
source_tag: { type: "string" }
|
|
3843
|
+
source_tag: { type: "string" },
|
|
3844
|
+
more_than_one_bot: { type: "string", description: "Optional. yes or no, in the user's words. Absent or no leaves sub-keys off. yes turns them on only when this server reads .env. Do not invent this answer." },
|
|
3845
|
+
bot_team_backup: { type: "string", description: "Optional. Yes or no, in the user's words. Yes and Yes. count as yes. yes is the recommended choice and records bot-team backup as on. Absent or no leaves it off. A non-string is refused. Do not invent this answer." }
|
|
3715
3846
|
},
|
|
3716
3847
|
required: ["assistant_name", "assistant_character", "user_name", "user_context"]
|
|
3717
3848
|
},
|
|
@@ -4146,15 +4277,31 @@ const TOOL_DEFINITIONS = [
|
|
|
4146
4277
|
},
|
|
4147
4278
|
{
|
|
4148
4279
|
name: "emet_floor",
|
|
4149
|
-
description: "Read the floor: the standing description of how this assistant engages with the person using it. It ships in the server code, so setup cannot overwrite it and no install can lose it - the character document holds the user's modifiers on top of it. emet_initialize already returns the whole floor at session start; call this to read it again without re-running initialize. Use `compact: true` for the short form partway through a long session, when the start of the conversation is far behind. Read-only, and re-reading is not the same as being bound by it.",
|
|
4280
|
+
description: "Read the floor: the standing description of how this assistant engages with the person using it. It ships in the server code, so setup cannot overwrite it and no install can lose it - the character document holds the user's modifiers on top of it. emet_initialize already returns the whole floor at session start; call this to read it again without re-running initialize. Use `compact: true` for the short form partway through a long session, when the start of the conversation is far behind. Pass `gem` to read one installed GEM's floor; reading does not turn that GEM on. Read-only, and re-reading is not the same as being bound by it.",
|
|
4150
4281
|
inputSchema: {
|
|
4151
4282
|
type: "object",
|
|
4152
4283
|
properties: {
|
|
4153
4284
|
compact: { type: "boolean", description: "Return the short re-assertion form instead of the whole floor." },
|
|
4154
|
-
section: { type: "string", description: "Return one section only, e.g. '
|
|
4285
|
+
section: { type: "string", description: "Return one section only, e.g. 'voice'. Call with no arguments to see the section names." },
|
|
4286
|
+
gem: { type: "string", description: "Read one installed GEM's floor by id, for example 'neurendipity'. Reading does not turn the GEM on. Omit it to read the core floor." }
|
|
4155
4287
|
}
|
|
4156
4288
|
}
|
|
4157
4289
|
},
|
|
4290
|
+
{
|
|
4291
|
+
name: "emet_client_approve",
|
|
4292
|
+
description: "Approve or deny a new client (a bot, or a single-instance AI such as Claude or ChatGPT) that EMET flagged on its first write. A new client is read-limited until approved. Only the owner, or the chief-of-staff bot named by EMET_COS_TAG, may decide. Pending clients are listed in emet_initialize and emet_status as client_approvals.",
|
|
4293
|
+
inputSchema: {
|
|
4294
|
+
type: "object",
|
|
4295
|
+
properties: {
|
|
4296
|
+
tag: { type: "string", description: "The pending client's tag, as listed in client_approvals.pending" },
|
|
4297
|
+
decision: { type: "string", enum: ["approve", "deny"] },
|
|
4298
|
+
reason: { type: "string", description: "Optional note kept with the decision" },
|
|
4299
|
+
as: { type: "string", enum: ["rename", "add"], description: "Required when the client_id already has approved tags: add keeps them all (several bots on one shared client); rename retires one" },
|
|
4300
|
+
from: { type: "string", description: "With as=rename on a client that has several approved tags: the tag being replaced" }
|
|
4301
|
+
},
|
|
4302
|
+
required: ["tag", "decision"]
|
|
4303
|
+
}
|
|
4304
|
+
},
|
|
4158
4305
|
{
|
|
4159
4306
|
name: "emet_invite_create",
|
|
4160
4307
|
description: "EMET Guest Access: create a one-time code for a guest. They paste it on the connect screen instead of the passphrase. Default access is read-only. Only the owner can create a guest code. EMET Member Access is a different door.",
|
|
@@ -4212,6 +4359,43 @@ const TOOL_DEFINITIONS = [
|
|
|
4212
4359
|
}
|
|
4213
4360
|
];
|
|
4214
4361
|
|
|
4362
|
+
/** Registered only when EMET_SUBKEYS=1. Flag off, a host's tool list does not include them. */
|
|
4363
|
+
const SUBKEY_TOOL_DEFINITIONS = [
|
|
4364
|
+
{
|
|
4365
|
+
name: "emet_subkey_request",
|
|
4366
|
+
description: "Request a delegated client sub-key for one registered source tag. Only an authenticated HTTP login of the owner may call it, and only when EMET_SUBKEYS=1. The result carries a one-time claim nonce. The server stores only its HMAC. Approve or deny the request with emet_client_approve from the owner's own login.",
|
|
4367
|
+
inputSchema: { type: "object", properties: {
|
|
4368
|
+
tag: { type: "string", description: "The source tag this key will stamp. An exact member of EMET_SOURCE_TAGS when a registry is set." }
|
|
4369
|
+
}, required: ["tag"] }
|
|
4370
|
+
},
|
|
4371
|
+
{
|
|
4372
|
+
name: "emet_subkey_claim",
|
|
4373
|
+
description: "Claim an approved sub-key with the key_id and the one-time nonce from emet_subkey_request. The result carries the emk_ key once. A second claim, a wrong nonce, or a missing pepper is refused. The bot keeps the key as a local secret; it is never written into EMET content.",
|
|
4374
|
+
inputSchema: { type: "object", properties: {
|
|
4375
|
+
key_id: { type: "string", description: "The key_id from emet_subkey_request." },
|
|
4376
|
+
nonce: { type: "string", description: "The one-time emn_ nonce from emet_subkey_request." }
|
|
4377
|
+
}, required: ["key_id", "nonce"] }
|
|
4378
|
+
},
|
|
4379
|
+
{
|
|
4380
|
+
name: "emet_subkey_revoke",
|
|
4381
|
+
description: "Revoke a client sub-key by tag or key_id. The owner's own login can always revoke. The key record is kept. A later write with its session handle is refused, and earlier writes stay as written.",
|
|
4382
|
+
inputSchema: { type: "object", properties: {
|
|
4383
|
+
tag: { type: "string", description: "Revoke the live key for this tag. Pass tag or key_id." },
|
|
4384
|
+
key_id: { type: "string", description: "Revoke this key. Pass tag or key_id." },
|
|
4385
|
+
reason: { type: "string", description: "Why it was revoked. Kept on the record." }
|
|
4386
|
+
} }
|
|
4387
|
+
},
|
|
4388
|
+
{
|
|
4389
|
+
name: "emet_subkey_end_session",
|
|
4390
|
+
description: "End a sub-key's live session without revoking the key. Owner only. Use it when the single live session is stuck and the key should stay usable. The old session handle stops working; the bot opens a new session with the same key.",
|
|
4391
|
+
inputSchema: { type: "object", properties: {
|
|
4392
|
+
tag: { type: "string", description: "The tag whose live session to end. Pass tag or key_id." },
|
|
4393
|
+
key_id: { type: "string", description: "The key whose live session to end. Pass tag or key_id." },
|
|
4394
|
+
reason: { type: "string", description: "Why the session was ended. Kept on the record." }
|
|
4395
|
+
} }
|
|
4396
|
+
}
|
|
4397
|
+
];
|
|
4398
|
+
|
|
4215
4399
|
/**
|
|
4216
4400
|
* The tool list as hosts receive it, with MCP annotations attached.
|
|
4217
4401
|
*
|
|
@@ -4220,7 +4404,7 @@ const TOOL_DEFINITIONS = [
|
|
|
4220
4404
|
* acting, exactly as risky as an arbitrary shell command.
|
|
4221
4405
|
*/
|
|
4222
4406
|
/**
|
|
4223
|
-
* The session graph's two inputs on every write tool (2026-09-29):
|
|
4407
|
+
* The session graph's two inputs on every write tool (2026-09-29, board 120):
|
|
4224
4408
|
* `session_id` names the session the write belongs to, `exchange` the turn.
|
|
4225
4409
|
* Added here, once, rather than hand-copied into each schema; a schema that
|
|
4226
4410
|
* already declares one keeps its own description.
|
|
@@ -4236,8 +4420,61 @@ for (const t of TOOL_DEFINITIONS) {
|
|
|
4236
4420
|
t.inputSchema.properties = t.inputSchema.properties || {};
|
|
4237
4421
|
for (const [k, v] of Object.entries(GRAPH_INPUTS)) if (!t.inputSchema.properties[k]) t.inputSchema.properties[k] = v;
|
|
4238
4422
|
}
|
|
4423
|
+
const SESSION_HANDLE_INPUT = {
|
|
4424
|
+
type: 'string',
|
|
4425
|
+
description: 'Required on every write in a session opened with a sub-key. The session_handle emet_session_open returned once. It is not repeated in receipts, logs, or session_graph.'
|
|
4426
|
+
};
|
|
4427
|
+
const SUBKEY_OPEN_INPUT = {
|
|
4428
|
+
type: 'string',
|
|
4429
|
+
description: 'The bot\'s emk_ sub-key. Presented once here. The result\'s session_handle is what later writes in this session pass.'
|
|
4430
|
+
};
|
|
4431
|
+
const SUBKEY_CLOSE_INPUT = {
|
|
4432
|
+
type: 'string',
|
|
4433
|
+
description: 'The emk_ sub-key that opened this session. A close releases that sub-key session only when this and session_handle are the ones that opened it. The handle is checked against receipts.transcript_session_id.'
|
|
4434
|
+
};
|
|
4435
|
+
|
|
4436
|
+
function cloneTool(t) {
|
|
4437
|
+
const schema = t.inputSchema ? {
|
|
4438
|
+
...t.inputSchema,
|
|
4439
|
+
properties: { ...(t.inputSchema.properties || {}) },
|
|
4440
|
+
...(Array.isArray(t.inputSchema.required) ? { required: [...t.inputSchema.required] } : {})
|
|
4441
|
+
} : t.inputSchema;
|
|
4442
|
+
return { ...t, inputSchema: schema };
|
|
4443
|
+
}
|
|
4239
4444
|
|
|
4240
|
-
|
|
4445
|
+
let toolCache = { flag: null, tools: null };
|
|
4446
|
+
|
|
4447
|
+
/** The tool list a host receives. The four sub-key tools and their schema fields exist only when EMET_SUBKEYS=1. */
|
|
4448
|
+
export function hostTools() {
|
|
4449
|
+
const on = setting('SUBKEYS') === '1';
|
|
4450
|
+
if (toolCache.tools && toolCache.flag === on) return toolCache.tools;
|
|
4451
|
+
const defs = TOOL_DEFINITIONS.map(cloneTool);
|
|
4452
|
+
if (on) {
|
|
4453
|
+
const open = defs.find((t) => t.name === 'emet_session_open');
|
|
4454
|
+
if (open && open.inputSchema) open.inputSchema.properties.subkey = SUBKEY_OPEN_INPUT;
|
|
4455
|
+
const close = defs.find((t) => t.name === 'emet_session_close');
|
|
4456
|
+
if (close && close.inputSchema) close.inputSchema.properties.subkey = SUBKEY_CLOSE_INPUT;
|
|
4457
|
+
for (const t of defs) {
|
|
4458
|
+
const props = t.inputSchema && t.inputSchema.properties;
|
|
4459
|
+
if (!props || !props.session_id || props.session_handle) continue;
|
|
4460
|
+
props.session_handle = SESSION_HANDLE_INPUT;
|
|
4461
|
+
}
|
|
4462
|
+
defs.push(...SUBKEY_TOOL_DEFINITIONS.map(cloneTool));
|
|
4463
|
+
}
|
|
4464
|
+
const tools = annotate(defs);
|
|
4465
|
+
toolCache = { flag: on, tools };
|
|
4466
|
+
return tools;
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
/** Live view of hostTools(), so a test that flips EMET_SUBKEYS sees the list for that flag. */
|
|
4470
|
+
export const TOOLS = new Proxy([], {
|
|
4471
|
+
get(_target, prop) {
|
|
4472
|
+
const list = hostTools();
|
|
4473
|
+
if (prop === Symbol.iterator) return list[Symbol.iterator].bind(list);
|
|
4474
|
+
const value = list[prop];
|
|
4475
|
+
return typeof value === 'function' ? value.bind(list) : value;
|
|
4476
|
+
}
|
|
4477
|
+
});
|
|
4241
4478
|
|
|
4242
4479
|
|
|
4243
4480
|
// ============================================
|