Repository navigation
sym 0.14.0 Core Secure - #53
Merged
Merged
Conversation
…els expire
Every peer re-sends its whole wake-channel list (peer-info) on every connect, and the
receiver logged a line and rewrote wake-channels.json per entry per frame, even for
entries it already held. On the daemon, xmesh reconnects thousands of times a day, so
stdout.log reached 1 GB of "learned wake channel for unknown".
- WakeManager.learnWakeChannel(nodeId, channel, {source, lastSeen}) is the one way a
channel is recorded, from the peer itself (direct), the relay's list (relay) or
another peer's peer-info (gossip). A weaker source never replaces a stronger one's
token, so unauthenticated gossip cannot repoint a phone that gave us its token.
- lastSeen is the last first-hand sighting. Gossip forwards the lastSeen it holds
instead of the time of sending, and a channel unseen for 30 days expires, in memory
and on disk. A channel saved before lastSeen existed gets one TTL of grace, once.
- peer-info and the relay list log one line per frame, only when something was
learned, and write the file only when something changed. The relay list handling
moves to RelayConnection._handleRelayPeers so it can be tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…ay by re-spelling it The directed de-duplication mark was the carried assertionId (which no signature covers) or the raw base64url signature string. Base64url decoding ignores padding and the unused bits of the last character, so `sig + '='`, a twin final character, or an injected assertionId all verified as the same signed record under a new mark, and a relay could make one signed directed record surface again and again. The mark is now sha256 of the decoded signature bytes. Ed25519 is deterministic, so one assertion has one signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
§18.3.1: every Core Secure CMB MUST be signed. A signing failure was logged and the record was then stored and broadcast unsigned, so a node with a broken key kept emitting records every verifying peer refuses. remember() now throws ESIGN (with a cmb-signing-failed metric) before anything is stored or dispatched, on first signing and on the re-sign after a collapse. The daemon's IPC handler reports a failed command as that command's error, instead of logging every handler error as a parse error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
buildTranscript bound the room and both party names, and aeadAADv2 the room, as raw code points. The published reference (conformance lib.mjs) NFC-normalizes all three, and so does sym's own record signature. The same visible text arrives composed or decomposed depending on where it was typed (macOS tends to NFD), so two honest peers in the room "café" hashed different transcripts and rejected each other's proofs, and a correctly sealed frame failed authentication. Tests: a decomposed room/name gives the same transcript and AAD bytes as the composed one, and a frame sealed under one form opens under the other. They fail on the previous code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…by an hour The relay's list is first-hand evidence, so each repeat moved lastSeen by a few milliseconds and asked for a save: a file write per relay reconnect. Against a 30-day TTL that precision is worthless. The record is still updated in memory; a save is asked for only when lastSeen moves by an hour or the source gets stronger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…ceipts counted
- B-R10 (§7): createCMB names the room 'default' when the author names none, instead of
null, which signed the string "null" and read as "every room" on receipt. checkAudience
now places a record without a room in 'default', so it is not admitted in every room it
is replayed into, and the v2.0 preimage refuses a record with no room. A fixture that
set a top-level `room` (read by nothing) passed only through that hole.
- B-R11: mood valence and arousal are kept only when measured, and must be numbers in
[-1, 1]. They were invented as 0 when absent, and a mood given only as numbers became
{text:'neutral'}, dropping them.
- B-R13: a category is at most 256 KiB, the seven at most 960 KiB, an agent id at most
64 bytes (§3.1.2), so every record fits a 1 MiB frame. createCMB throws ECMBSIZE, and
a receiver refuses an oversized record before rendering, verifying or storing it
(metric cmb-oversize-rejected).
- B-R3 (interim): an unsigned record is still accepted as unverified, but each one is
counted (metric cmb-unsigned-received) and the peer is named once in the log, so the
emitters a signed-only default would cut off can be found first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…lication-v2 The SDK's record-signature and handshake vectors were not the published files: they lacked the 2026-09-14 errata fields (`usage`, `expectedSignatureIsOneOfMany`), and the tests asserted the opposite of what those fields say, comparing our Ed25519 signatures and handshake proofs byte for byte. A hedged signer (WebKit, CryptoKit) is conforming and fails that comparison. There was also no SDK copy of application-v2 and no test of the mmp-app-v1 commitment. - copy all four published conformance/v2 files verbatim (record-signature, handshake, e2e, application) - signing: reproduce the preimage, assertion id and category commitment exactly; verify expectedSignature, never reproduce it - handshake: reproduce both proof payloads; verify the published proofs and two of our own signings, no byte comparison - new application-v2 test: both commitments, payload integrity, refusal of a changed byte, a wrong length and padded base64url - new digest pin: each local vector must hash to the published manifest's sha256, so a hand edit or a stale copy fails instead of drifting The digest pins and the errata assertions fail on the previous copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…art time
"the lease refuses a second holder" wrote lock.pid as `1` plus
{"startTime":0}. Windows has no PID 1, so the test failed there; and on
every OS the reader's metadata key is `start`, not `startTime`, so the
metadata was ignored and the test only exercised the legacy
no-start-time branch, never the v2 start-time check it was meant to cover.
The holder is now a sleeping child process, recorded exactly as a running
holder writes its lock ({"start": processStartTime(pid)}), and killed in
cleanup. The test also asserts the reader sees that start time and that
the refusal names the holder's pid.
Mutation check: making the start-time comparison always report a mismatch
fails the new test; the old one still passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
… everywhere
On win32 child.kill('SIGTERM') is TerminateProcess: no signal handler and
no exit hook run, so "releases the lock on SIGTERM when the host has no
handler" can only fail there (lock.pid stays). It is now skipped on win32,
with that reason in the skip message.
What Windows actually needs is the hard-kill path, so a new test runs on
every platform: a child acquires the lock, is killed with SIGKILL
(TerminateProcess on Windows), its lockfile is shown to be left behind
with its pid and start time, and the next acquire must reclaim it.
The new test passes on the current lib, because reclaim already works; a
mutation that stops treating a dead pid as stale makes it fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
§15.8 anchors a remix on the oldest root reachable by recursively VERIFYING parent records. resolveTetherAnchor stepped onto any stored entry, including a peer block admitted without its author's signature being verified, and followed whatever parents that block claimed. The author of one unsigned block could pick the anchor of every remix that cites it, and the integrator then signed a tether naming it. A step now has to verify (storedRecordVerifies): its categories recompute to the key it is held under, and this node either wrote it or recorded a verified signature verdict when admitting it. An unverifiable parent ends that branch. A remix with no verified ancestor in reach now reports an unverifiable root (key and categories null) instead of anchoring on itself, which measured the remix against its own text and attested it as tethered. `complete` reports whether the walk met an unavailable or unverifiable edge (§15.2 incomplete proof). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
A verified record's signed `to` could only veto directed treatment, never grant it, so a relay that stripped the frame's `directed` flag turned a signed directed CMB into a broadcast that SVAF could reject and that never surfaced (§9.2.2). When the author signed an addressee, it now alone decides whether the CMB is directed to this node (§8.8.5 step 7). The frame flags still decide for unverified records and records that sign no addressee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
- B-R4 (§8.8.4, §18.3.1): a published v2.0 record (mmp-sig-v2.0) signs its author's node id, but it was verified against the key of whichever peer delivered the frame. A peer could vouch for a record by signing it itself under another node's id, and a genuine relay failed. It is now verified against the key held for createdByNodeId. A missing key is unverified, not refused: that is key-distribution latency, as for v1. - B-R6 (§8.8.5 step 5): a v2.0 record's carried assertionId must equal the one its preimage yields, or the record is refused. On other suites no signature covers an assertionId, so a carried one is dropped instead of kept beside verified fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…e lookup On Windows every start-time read is a synchronous PowerShell process (~0.45 s), run per pid per lock check and remembered for only 10 s, so a daemon start paid it once per node directory whose lock it checked. - createStartTimeCache: a successful read is kept until the pid is seen dead (or a fresh read returns a different start); a failed read is kept 10 s as before, so lookup failure still means "held", warned once - one PowerShell call reads every pid asked for (Get-Process -Id a,b,c; `exit 0` so one vanished pid does not discard the others), using the same per-process expression as before, so strings match existing locks - locksHeldByLiveProcess / primeProcessStartTimes: a pass over many locks reads all their holders in one lookup; migrateStores now checks only the nodes that still need the rename, all in one batch - the one hazard of remembering: a pid that dies and is reused between two checks is never seen dead. A mismatch, which hands the lock to someone else, is therefore always confirmed by a fresh read, so a remembered value can only err toward "held" POSIX keeps reading `ps` directly (cheap, nothing remembered). Tests run the cache through an injected lookup on every platform: one read per live pid however often it is checked, forgotten when seen dead, failures retried after 10 s, one lookup per prime and per migrateStores pass, a mismatch re-read before reclaiming, a failure held and warned once. They fail on the previous code, which had no batched or injectable path. The PowerShell batch itself runs only in the win32-only config test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
§15.8 makes the tether content-only so any holder of the root and the record re-encodes the two texts and recomputes what the integrator signed. The gate measured the remix side on its fused vectors, which are this receiver's attention readout over its own recent anchors: two nodes holding the same root and record signed different drifts (0.134757 vs 0.183918 in the new test), and no holder could reproduce either. The gate now runs evaluateLineageTetherFromText on the record it stores, the same computation the retroactive audit uses. That function is made content-only throughout (a vector on a category with no text was taken as-is) and single-kernel: the semantic encoder replaces the lexical one at runtime, and an evaluation whose encodes straddled the switch compared unrelated spaces, so it now re-encodes both sides when the kernel identity changes underneath it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
The cold-start exit of the heuristic gate stored a remix and returned without a tether, so the frame handler had nothing to sever on: a remix drifted past its root's reject floor kept its lineage whenever the receiver's anchor window was empty or uncovered. An empty window says nothing about the chain; §15.8 compares the record with its lineage root, not with memory. The cold-start exit now runs the same tether computation as the warm path before returning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
auditLineageTethers falls back to fetchCMB when the verified walk finds no anchor. fetchCMB answers from the local store before asking any peer, so for a parent held locally but unverifiable it handed back the very record the walk had just refused, and the audit attested a tether to it (the B-L4 defect, reached through the opt-in fetch path). Keys the store already holds are skipped there; only records this node does not hold are fetched, and those self-verify against their address. One guard line in lib/node.js, kept in its own commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…d pipe on Windows On Windows the daemon listened on SYM_SOCKET as given. A filesystem path there cannot be a Unix socket (listen EACCES on ...\d.sock), so the daemon exited and daemon-relay-only failed. And the clients did not resolve the endpoint the way the daemon did: - SymDaemonClient defaulted to /tmp/sym.sock, an address no daemon has listened on since the socket moved to <state root>/daemon.sock, on any OS - bin/sym.js read SYM_SOCKET itself before getSocketPath, bypassing any mapping the daemon applied lib/platform now has ipcEndpoint(address, platform): unchanged on POSIX; on Windows a pipe name is used as given and a filesystem path becomes \\.\pipe\sym-<basename>-<sha256 of the resolved, lower-cased path>, so two spellings of one path reach one pipe and two paths never share one. getSocketPath resolves SYM_SOCKET through it, and the daemon, the CLI and SymDaemonClient all use getSocketPath / ipcEndpoint. The unrooted Windows default stays \\.\pipe\sym-daemon; a rooted deployment (SYM_STATE_DIR) gets a pipe of its own root, as its socket is under its root elsewhere. Tests: the mapping and resolution for win32 and POSIX as pure functions (fail on the previous code, which had none); SymDaemonClient's default and an explicit path resolve to the daemon's endpoint (fails before on macOS too: /tmp/sym.sock); daemon-relay-only now asserts relay-auth on its own, that the relay leg starts before the IPC server, the endpoint the daemon listens on, and that `sym status` reaches it through that endpoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…till verifies _handleCmbFetch rebuilt metadata from a fixed list of fields and dropped the v2.0 ones (signatureSuite, addressScheme, createdByNodeId, application, assertionId), so a v2.0 record fetched from this node failed verification at the requester. The stored metadata is now served whole, as a copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…d marks kept apart - K1: a key is marked seen only once its CMB has surfaced, which can be after an async SVAF pass, so a second copy arriving meanwhile (the same record via two peers, or a reconnect replay) passed the check and surfaced twice. A key is now held in flight from the check until its frame is finished with, and a copy arriving meanwhile is skipped. The SVAF chain returns its promise so the release happens exactly then. - K3: directed assertion marks (key#sig:…) shared the broadcast map and its 10,000 cap, so a broadcast flood could evict them and let a replayed directed record surface again. They now have their own map and cap, persisted in the same file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…ESTHOME) Sandboxing HOME was a per-file convention (require tests/_isolate-home first). The file that forgets it is exactly the one that leaks: it writes identities, private keys, stores and locks into the real ~/.sym, where they surface as phantom agents, and can read the real relay.env and its API key. Nothing said so. Node's test runner sets NODE_TEST_CONTEXT in every test process (children inherit it). assertTestSandbox in lib/core/state-root.js, the module that decides where state lives, now throws ETESTHOME when it is set and any of these resolves outside fs.realpathSync(os.tmpdir()): - os.homedir() (HOME; USERPROFILE on Windows), since several paths still derive from it directly - the state root in effect, captured at load, so HOME sandboxed too late is caught - SYM_IDENTITY_DIR when set, where the keypair goes The message names the directories and says to sandbox HOME and USERPROFILE; SYM_TEST_REAL_HOME=1 opts out. SymNode's constructor calls it before anything is read or written, and the daemon before it reads relay.env, migrates stores or builds its node. Every existing test file passes under the guard: the files that construct a node or start the daemon already sandbox (the 38 that do not load _isolate-home were run under it, 393/393). New tests: in-process and child process refusals for HOME, SYM_STATE_DIR and SYM_IDENTITY_DIR, nothing written to the refused home, symlinked temp dirs accepted, the opt-out, no check outside the runner, and the daemon refusing to start. 8 of 10 fail on the previous code (the two that pass describe a sandboxed run and the opt-out, which behave the same before and after). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…bered The confirming re-read before a mismatch reclaims a lock exists because a remembered start time can outlive an unobserved pid reuse. `ps` reads are never remembered, so on POSIX the second read repeated the first for nothing; it now happens only on the remembered (Windows) path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…, timestamps follow the clock - K4: inbox()'s limit counted acked items, so a drain could return fewer unread deliveries than asked. It now counts unread; acked items still come back, marked. - K5: the inbox `from` was the author's label whether or not anything proved it. It is now the author when the signature proved who that is (a v2.0 record verified against its signed node id, even when relayed, or a verified record its author delivered), and otherwise the peer that delivered it. The claim stays in author.name. A v2.0 record verified that way now carries author.nodeId. - K6: the createdTimestamp ratchet restarted from zero in every process, and after a backward clock step it ran ahead of the clock indefinitely. It now starts from this node's newest stored record, and after a step of more than a minute it follows the clock again, with a log line and a clock-stepped-back metric. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…y; no sends on dead sockets - bin/sym-daemon.js built its own directory from os.homedir(), so a daemon rooted with SYM_STATE_DIR read and wrote its room, tasks and relay.env in the user's real ~/.sym. It now uses the same state root as every node. - A relay-only daemon (SYM_RELAY_ONLY) has LAN discovery off, but it still published its room beacon on mDNS. It no longer does, and says so in the log. - sendFrame counted a frame written to a destroyed or non-writable socket as sent, until 'close' arrived (B-D6 residual). It returns false for those now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…pec PR #17 sym integrates an admitted CMB as the identity, so every admission is the "collapsed integration" case PR #17 adds to §15.5: the author's record is kept exactly as signed, the receiver mints nothing, nothing is attributed to the receiver and no K→K self-edge is written. The test pins that, so a future integration that changes the text (which §15.5 would then require to mint a receiver remix) fails loudly here instead of silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…ws test debt, the daemon's log flood Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
The heuristic gate attached each category's receiver-local vector to the
records it read, in place. Its anchors are the store's own cached objects
(MemoryStore.anchors hands out entry.cmb), so every anchor the gate read kept
a vector on each category until the next persist of that entry, which is
routine (a peer remix citing it advances its lifecycle), and the vectors were
written to disk. A record carries no vector (§7.1). The incoming record was
mutated the same way.
localisedCategories now returns a new {text, vector} map built from text
alone, and anchors are measured through views holding only what
computeCategoryDrifts reads. A wire vector still never reaches the
measurement: a category with no text has no entry in the map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
The gate chose its encoder once, at its start (isSemanticReady), but named each embedding-cache entry by kernelId() read at lookup time. The semantic encoder loads asynchronously while a node runs, so a gate that started lexical and was still encoding when it finished filed lexical vectors under the semantic kernel's name, and every later gate was served them as semantic vectors for as long as they stayed cached. context-encoder gains svafKernel(): the kernel in force now, pinned, as one value carrying its §15.8 id and an encoder bound to that same kernel. kernelId() and encodeForSVAF() are defined through it. The gate pins one kernel for its whole run and _localVector keys and encodes with it, so the id in the key is the producer by construction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…ntry
write() and receiveFromPeer() stored `{ ...cmb, lineage }`, copying the
store's expanded lineage (parents plus a locally computed ancestor closure)
onto the record as a top-level member. A two-section record has exactly
`categories` and `metadata` (§8.8.1) and its lineage is metadata.lineage, so
the extra member read as the pre-boundary shape: classifyAddress took its
parents as the record's, tried only the remix derivation, and reported every
stored v2 remix as a mismatch though the record was untouched.
The store now keeps the record as given; the entry already carries the same
lineage, which is where the closure belongs. Files written by earlier
versions hold the stapled member, so every read of a store file (first-touch
index build, load(), _load) drops it from two-section records. A pre-boundary
record keeps its top-level lineage, which on that shape is its own section.
classifyAddress was right for the shape it was given and is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y3qC1m9acDVaerca56dvV
…iew R1, R2) SECURITY.md, the CHANGELOG and WIRE §3: at most 200,000 statements held, and beyond that only in-force revokes and in-force anchor-level statements, which the quotas bound; a dead or over-quota revoke is an ordinary candidate; the file is compacted. Resolving is not charged to a lane but paced: at settles, a pull's end decided at the next settle, one resolve per eviction pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
… protected by its kind The mutation check of the R1 fix found one guard no test held: tier 2 protects in-force anchor-level statements, not every live one. A removed anchor-level grant stays live while it is the chain of a statement an endorse rescued; once a pass has dropped that statement (deeper, so first), the grant goes too, while the anchor's in-force revoke and endorse stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…per arrival; the slack said The diff check of f26fc92 (SHIP), its four lows: 1. _writeFile writes the temp file, fsyncs it, renames it over the file and fsyncs the directory: a power loss leaves the old file or the new one, never an empty one (the lines kept from other pins cannot be fetched from a peer). 2. A crash's statements.jsonl.<pid>.tmp (of a process no longer alive) is removed when the store opens; a live process's is left alone. 3. A pass that compacts the file during an arrival has written that statement already: it is not appended again. 4. SECURITY.md, the CHANGELOG and WIRE §3 say that up to EVICT_SLACK (3,125 at the default bound) more can be held between passes, and that the protected floor is judged again at the next pass. Tests (authority-attacks, both fail on f26fc92): no statement appears twice in the file after 600 arrivals across compactions; a rewrite fsyncs the temp file and the directory, and a stale temp file is removed at open while a live process's stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…eir schema, the sequence before the parse, Close codes close (S1, S2, S4; #23) MMP 2.0 update 1, from the reviews of #26, #31 and #23 against 390b4af: - S1: _sealRecord refuses a record whose signed room is not the session's room (AudienceError, 'not-addressed'), as it refuses one signed to another node. The store is per node, not per room, so after a room change a fetch answer or an anchor replay could otherwise disclose an old room's record before the receiver refused it. - S2: verifyRoomGrant checks the grant against room-join.schema.json before anything else: a closed object, lowercase UUID ids, integer timestamps. The payload signs decimal text, so string timestamps verified and the expiry timer (which needs a number) never armed: the grantee kept its session past expiry. signRoomGrant mints the schema's object only and refuses one that could never verify; the node sends grantForWire(grant), the schema's members only. - S4: a sealed frame that opens advances the receive sequence; its inner frame or record is parsed only after the position is taken (openControlSealed / parseControlPlain, openEncryptedSealed / parseEncryptedPlain). Refusing what it carries changes nothing else; before, the counter stayed behind and the next frame closed the session as a gap. - #23/§7.2: every sealed error whose action is Close (1001, 1003-1010) ends the session at the receiver; others are information. sendError takes only 1002 and 2xxx (never 1009, 1010, 1011, the other Close codes or 4xxx): EBADERRORCODE. - Stale comments: sealed-control.js (error is sealed), room-grant.js (the session closes at expiry; the proving handshake is the connection path). Tests: spec-update-session (each fails on 390b4af): a record for another room is never sealed or served; string timestamps and extra members are refused, mint and wire carry the schema's members; a refused inner frame and a refused record plaintext leave the session taking the next frame; a sealed 1008 closes, a sealed 2001 does not; sendError refuses the protocol's codes. The grant tests use schema-valid grants (lowercase UUID ids, a bound key). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…ire grammar; equivocation relayed once; no root over a suffix (S3, D1, #27) MMP 2.0 update 1 (#27, sym-attest-v1) and the review of it against 390b4af: - S3: no attestation is signed about a directed record (a signed `to`) or a Legacy Import record, so none enters this node's chain or is gossiped: the room would otherwise learn a sealed one-to-one record's cognition key, assertion and verdict (a confirmation oracle on private content), and every receiver refused the Legacy Import ones anyway (no assertion id). A peer's attestation about a directed record this node holds is kept as evidence and never relayed. - Grammar: the attestation object is closed (a member the schema does not define refuses it), with exactly seven verdicts; by and attester are lowercase UUIDs; a role is up to 64 characters (any role a §6.6.2 grant confers), a method up to 32. - Equivocation: the conflicting checkpoint is relayed once, as evidence; a witness already signed stands; a third root is dropped before any work, as before. - _attesterRole judges a scoped role on the record its assertionId names: a cognition key is shared by every record with the same categories, so one held under it but asserted by another judges no scope. - D1 (until #27's chained construction lands): a checkpoint root is never signed over a suffix of this node's chain. Once its own early attestations are evicted, checkpoints pause, and the node says so once. - The attestation-received doc comment no longer calls `method` unsigned. Tests: spec-update-attest (each fails on 390b4af). gossip-lanes and admission-attestation use UUID signers, and expect the conflicting copy relayed once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…ndored (D2, #34, #37) MMP 2.0 update 1 (meshcognition-website PR #43, spec/mmp-2.0-update-1 at 2660ab9), §8.8.5 step 1: a relayer could reorder parents, respell a signed string in NFD or send lineage {parents: []} for null, and the record still verified while nodes stored different bytes for one assertion (records/probe-malleability.js). Now: - a received record whose category text, createdBy or room is not NFC is refused, never normalised; createCMB mints all three NFC; - lineage.parents and each meta.parents are held sorted bytewise, as they are signed, and minted as a sorted set (each reference once); - a lineage with no parents is held, and minted, as null; - the 256-character caps on createdBy, room and a parent reference count code points, as JSON Schema's maxLength does (a 200-emoji label was refused at 400 UTF-16 units); - MAX_RECORD_BYTES is measured on the two-section record only: remember()'s decoded payload kept beside its own record was counted a second time at mint. HELD, by the coordinator's word: the update also projects an absent application as null; that rule is under review, so a null application is still dropped. The vector test pins exactly that one member as the difference. Vendored from 2660ab9 with their manifest digests in SOURCES.json: record-projection-v2, record-size-v2, control-encrypted-v2, the cmb, control-frame, control-encrypted, cmb-fetch, cmb-fetch-result, room-join, relay-frame, sym-attest-frame and encrypted-cmb-frame schemas, the frame registry, and scripts/mmp/lib.mjs, the reference construction, which tests/spec-update-vectors.test.js runs beside sym's code (projection, size measures, control AAD, the never-inner list). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…ealed ping; fetched records verified (D4, #26, #30, #23) MMP 2.0 update 1 (meshcognition-website PR #43 at 2660ab9) and the founder's mood ruling: - mood is { type, mood, context, timestamp }: mood 1 to 1,024 characters, context at most 4,096 or null, counted in code points (lib/core/mood-frame). broadcastMood sends no from/fromName and throws EMOODFRAME rather than send an out-of-bounds frame. On receipt a frame carrying any other member (from, fromName, valence) is refused, and the event is labelled with the session's proven name, never a payload name. - cmb-fetch carries no from and names one key (a keys array, or any member the schema does not define, is refused and counted); cmb-fetch-result carries no timestamp, and both are checked closed on receipt. A record is served only into a session of its own signed room. fetchCMB asks only for a cognition key. - D4: a fetched record is put through the whole of §8.8.5 (author key by createdByNodeId, signature, assertion identity, audience). fetchCMB returns { verified: true, cmb, authorNodeId } only when it passes, and otherwise { verified: false, reason, categories } with no metadata, which the §15.8 audit may use for the tether. A record held under Legacy Import answers the same way. A non-v2.0 record answers nothing; an altered category is reported as a mismatch, apart from a malformed record. - cmb-anchors: only a non-empty list starts the once-a-minute limit (an admission with nothing to replay no longer held back the next one's context); replay is limited to records this node authored in the session's room; a frame that is not the schema's (more than 50 keys, a non-key, an extra member) is refused whole. - A clear 1011 prompts a re-handshake only on the client side, while the peer is present, when its detail names a session this node holds with that peer or a probe ping is outstanding, while no newer session is waiting to supersede, and no faster than the retry backoff (1 s doubling to 30 s). - A sealed ping is answered with a pong, and a pong (clear or sealed) ends an outstanding probe. - control-encrypted is checked as its schema bounds it: the members exactly, a 32-hex sessionId, a sequence of at most 29 digits, a sealed value at least as long as the tag. Already as update 1 states, with no change: the server supersedes at admission (after room-join in a gated room); a sealed error never completes a client's supersession (it never reaches the frame path); 1009 is sent sealed and not retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
… application.schema NFC From the PR #43 verification (coordinator, 2026-10-08; the hold on #34's application rule lifted): - An absent application projects to null (§8.8.4: both sign as lp("0")). Every published record spells it so, and encrypted-cmb-frame.schema.json requires the member: sym's sealed records with no application were schema-invalid (the reviewer reproduced it with buildEncryptedFrame). The record-projection-v2 vector now matches with no exception, byte for byte, as does lib.mjs's recordProjectionV2. - One room grammar, §5.8's [a-z0-9._-] of 1 to 64 characters (lib/core/room-id): a node's own room (SymNode throws EBADROOM), the handshake hello, a record's signed room (received and minted), an attestation's room (was any text up to 256), and rooms.isValidRoom, which refused §5.8-valid dotted and underscored rooms (the spec's recommended acme.prod among them). `sym` stays refused and unownable while per-room service types exist (§5.1 migration). Being ASCII, a room is its own NFC form: the NFC tests for rooms become refusals. - application.schema is signed as NFC(schema): minted NFC, and refused on receipt when it is not. - legacyServiceType(room): the per-room DNS-SD type earlier releases used, only where it is a valid RFC 6335 service name (for the §5.1 migration browse). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…mmp list, RFC 6763 TXT keys (#17, #22) MMP 2.0 update 1, §5.1 (PR #43 at 2660ab9), and the relay review's ALIGN-SYM items: - The daemon advertises every room on _sym._tcp with its name in TXT room, as the library already did; it advertised and browsed only _<room>._tcp, so a daemon node and a library node in one room never saw each other. It browses the per-room type earlier releases advertised as well (discoveryBrowseTypes), where that is a valid RFC 6335 service name, and never advertises it. - A node dials only an advertisement whose TXT room is its own (absent reads as default), and whose mmp, a comma-separated version list, lists 2.0 (it compared mmp === '2.0'). - TXT keys are read from the raw strings as RFC 6763 §6.4 says: case-insensitively, the first occurrence winning, a string beginning with '=' ignored (bonjour-service's own object lets the last one win and keeps case). - A discovery the host built advertises the node's own room (Discovery.setRoom): a caller's BonjourDiscovery advertised 'default' whatever room its node was in, which the TXT room filter would have turned into a partition. - The loopback registry takes a registration under any type this node browses. - The dns-sd code path (_startDnsSd, _resolvePeer) was unreachable since 0.3.72 and held the unanchored /mmp=(\S+)/ match the review found: removed. - CLI and daemon messages say the §5.8 grammar and the one service type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…socket; relay-auth lowercase (#25, #20 deferred) MMP 2.0 update 1, §4.4.9 (PR #43 at 2660ab9), and the relay review: - 4007 is reconnected with backoff like any close but 4004 and 4006. It belongs to relay-auth key proof (draft #20), deferred past 0.14.0 and not in MMP; its review found that "MUST NOT reconnect after 4007" makes a lockout of up to a week. It was a hard stop here. - What a relay listed in relay-peers.features (fan-out) is forgotten when its socket closes: the next relay says its own. It was kept across connections. - relay-auth carries the nodeId in its lowercase form (§3.1.1; a relay refuses another with 4002). Already as update 1 states, now pinned by tests: 1001, 1011, 1013, 4005 and 4008 reconnect with backoff (1009 is handled the same way: every code outside HARD_STOPS is). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…n; only gated remixes count (D3, #35, #17) MMP 2.0 update 1 (PR #43 at 2660ab9): - D3 (#35): a receiver MUST NOT skip or refuse a record because its lineage cites the receiver's own records. The ingest echo skip is gone: a reply, an outcome or a grounding citing this node is verified, gated, stored and delivered like any record. The anti-echo rule sits at the remix trigger (MeshAgent: a record citing this node's own records is not remixed) and the remix gate. - §15.5 (#17): the §15.8 tether does not apply to a collapsed integration. sym's gates keep the incoming text, so every integration collapses onto the author's record as signed: no tether is evaluated, the record is never stored as a severed root, and no tether attestation is signed about it. The retroactive audit applies the same rule: it judges only records this node minted, never a peer's record it holds. The §15.8 suites that pinned receiver-side severance now pin the exemption. - #35 counters: remixProduced counts remixes minted through the gated remix path (remix()), not every record with parents; the decision log says whether a refused record cites parents (`cites`), never that it was a remix, which a receiver cannot know. - Layer 6 is given no valence or arousal: unsigned (§8.8.4), dropped from Core Secure records by the projection and unverified on a Legacy Import record. - xmesh-insight-v1 is not offered by default (it is registered nowhere; the registry lists xmesh-insight as a legacy type); a host adds it with opts.extraExtensions. Reported, not changed: with the exemption, §15.8 runs nowhere in sym's receive path, and the remixing side (remix()) has never evaluated the tether; see the round's report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…ment ships; interop with 0.13.17 - CHANGELOG 0.14.0: a section for MMP 2.0 update 1 (PR #43 at 2660ab9), item by item, and "Interop with 0.13.17": LAN meshing with 0.13.17 stops except through Legacy Import routes, by design. The passages the round made stale are corrected in place (the mood frame, rooms in NFC, discovery, the counter rule, cmb-fetch's keys, xmesh-insight-v1, 4007, the B-L5/B-L6 tethers). The released 0.13.17 (2026-10-07, a relay fix) gets its own entry; the earlier, unreleased 0.13.17 security candidate keeps its record under that name. - SECURITY.md: the counter advances when a frame opens; sealed Close-action errors; the clear-1011 conditions; the seal point's room check; the canonical projection and §5.8 rooms; fetched records attributed only after §8.8.5; the mood frame; the attestation rules; grant schema checks; 4007 an ordinary close; TXT room as a hint; and, under what it does not solve, that §15.8 applies to nothing sym receives (collapsed integrations) while remix() does not evaluate it, and the 0.13.17 LAN break. - docs/WIRE-0.14.0.md: rewritten against update 1. What the spec now defines is named by section; what remains is what sym chooses (anchor replay, 1011 pacing, the probe, sendError, the relay pace against the §19.1 floors). - reference.md and SKILL.md: the §5.8 room grammar and the one service type. - application.schema.json vendored (the record schema references it) with its manifest digest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…ion by range or prev (D1, #27); re-vendor 5417176 MMP 2.0 update 1 at 5417176 (PR #43 with its verification fixes; the chained checkpoint pending one reviewer check): - A checkpoint covers the attester's attestations since its previous one, seq fromSeq..uptoSeq, chained to that checkpoint's root (`genesis` for the first): root = SHA-256("mmp-attest-chain-v1\n" || lp(prev) || lp(fromSeq) || lp(uptoSeq) || lp(segment root)); it signs fromSeq and prev. So this node holds only its last segment. A node that lost its segment (or, with no checkpoint held, seq 1) signs nothing over fewer: its chain ends, visibly (`checkpoint-chain-ended`), and no further checkpoint is signed under it. The segment and the checkpoint are appended to the logs before anything that depends on them is gossiped. - A witness carries and signs fromSeq (after lp(NFC(room))). A witness naming another range or root than the checkpoint held at its position is refused and said (`disagrees`), never taken as the attester's equivocation: it is only the witness's word, and would let any witness frame an honest attester. - Two checkpoints from one attester that are not the same checkpoint conflict when their ranges overlap or they name the same prev (the rule catches a fork cut at other boundaries). Duplicates are judged by content (range, prev, root). The link checks run before any signature: a reversed range, prev not genesis exactly at fromSeq 1, or a checkpoint that does not start right after the one its prev names, is malformed, never evidence. After a conflict this node never witnesses that attester again and drops its further checkpoints unverified (`attester-equivocated`), which bounds the evidence to one copy. The stale drop stays "older than every position held". - The wire forms are the schema's closed objects (node-stats included); time and position are safe integers. Logs written before the update hold unchained checkpoints; they are read and kept as before, one per position, and reconcileChain checks either form. - cmb-encrypted is bounded as encrypted-cmb-frame.schema.json now bounds it (sequence of at most 29 digits, sealed 22 to 983,062 characters, a 32-hex sessionId). - The relay pacer sends relay-auth and relay-pong ahead of every other frame but inside the bucket (the update's floors are 25/300; sym stays at 20/200): they went out at once whatever the bucket held. Re-vendored from 5417176 with the manifest digests in SOURCES.json: sym-attest-v1.json (new), record-projection-v2 (six new cases: an application-bearing record, 256 against 257 astral characters, an NFD application.schema, a non-§5.8 room, an uppercase to), record-size-v2 (a new CJK case), control-encrypted-v2, the cmb, encrypted-cmb-frame, control-frame, control-encrypted, room-join, relay-frame and sym-attest-frame schemas, the registry, and lib.mjs. sym passes every case, and lib.mjs's sym-attest constructions, conflict and link rules agree with sym's on all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…the changelog, SECURITY and WIRE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…guards removed A mutation pass over the round's changes (36 mutants, then 7) killed every mutant but five: - Removing the signature check of a fetched record survived: no test served a record whose key and assertion matched under a swapped signature. Now one does (D4: `bad-signature`, categories only). - Four guards duplicated a rule that holds elsewhere, so no test could tell them apart from nothing, and they are gone: the remix trigger's own cites-this-node check (the trigger already keeps every record that cites a parent out of the remix cycle); the witness's equivocation check (only a checkpoint the store took is witnessed, and it takes none of an equivocating attester's); the tether's collapsed check (no gate evaluates a tether for a collapsed integration); and the fetch server's room check (the session's one seal point refuses a record of another room, and the answer lists it missing). The comments say where each rule now holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…e range check at step 1 (#27 ruling) The coordinator's ruling for #27, which the spec author is applying to PR #43: - Witnesses are not equivocation evidence: they are not attester-signed. Only two conflicting attester-signed checkpoints are (as sym already held). A verified witness that names another range or root than the attester-signed checkpoint held at its position counts against the witness: it is refused (`disagrees`), said once (`witness-contradicts-checkpoint`), and that witness's further witnesses are dropped unverified for 10 minutes (`witness-muted`; the ruling's MAY). - The link checks run in §6's order: uptoSeq >= fromSeq at step 1 (fromWireCheckpoint refuses a reversed range, as it already refused prev other than genesis at fromSeq 1), and the prev link to a held checkpoint after step 4 (after the duplicate drop, before any signature work), as before. The hello's room already has the §5.8 grammar here (c0e615f). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…tates them; re-vendor 0ab7b76 meshcognition-website PR #43 at 0ab7b76 ("Verify #43, round 2: witness evidence, link-check placement, and the handshake room"): - A witness is never equivocation evidence against the attester. One that names the range of an attester-signed checkpoint held, with another root, is evidence against the witness (`disagrees`: refused, said once, the witness muted for 10 minutes). One that overlaps a held checkpoint without matching it is a lead (`witness-lead`: refused and said, no mute). sym muted the witness for any mismatch at the held position, a different range included; a lead is not evidence against anyone. AttestationStore.witnessAssessment gives the three verdicts; the vector's witnessLeads and lib.mjs's attestWitnessAssessment agree with it. - §6's order (the range at step 1, the prev link at step 5, after the duplicate drop): as sym does, except that sym checks the signature's spelling (step 3) after steps 4 and 5; all three discard with no state changed, so only the refusal's reason differs (said in WIRE-0.14.0.md). - The hello's room has the §5.8 pattern in handshake.schema.json now, as sym already refuses. Re-vendored from 0ab7b76: sym-attest-v1.json and lib.mjs (nothing else changed); SOURCES.json records the head and the digests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…stations are about room-bound records The full suite and the integration suite at f3c1472 (under the heavy lock) found four tests that still pinned what update 1 changed: - node-stats: the stats object is closed (sym-attest-frame.schema.json), so a frame carrying a self-asserted name and nodeId is now refused, not read around; the test sends both. - e2e-admission, e2e-attestation-gossip, e2e-checkpoint-witness (integration): they gated directed records, about which no attestation is signed since S3 (018206d; sym-attest-v1 §5.1). They gate room-bound records now; e2e-admission also pins that a directed record is attested by nobody, and e2e-checkpoint-witness that the first checkpoint is chained to genesis over 1..2. handshake.schema.json is vendored from 0ab7b76 too (PR #43's final head, per the coordinator), and one test pins that the hello, a record, a sealed record, a grant, relay-auth and sym-attest all name a room by the §5.8 pattern sym uses, and the hello its nodeId in the lowercase spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…root is minted as a fresh root By the founder's ruling (0.14.0 is the breaking release): §15.8 runs where the spec puts it, on the remixing node. node.remix() now resolves the remix's nearest verified lineage root from the parents it cites and evaluates the remix's own text against it (content-only, one kernel, α-weighted against Tguarded) before minting: - faithful: minted with its lineage, a `tethered` mmp-tether-v1 attestation on the entry; - drifted past the floor: minted as a fresh root, no parent from the severed chain, the departed source in provenance.tether.departedFrom, a `severed` attestation; - no verified root in reach (or nothing evaluable): minted with its lineage, unverified, unsigned. The §15.7 flag is consumed when the call begins (a concurrent remix() is refused) and given back when nothing is minted (a duplicate, a collapse); remember() inside remix() no longer sets it. remix() refuses opts.cmb. MeshAgent's remix cycle and the XMesh synthesis loop await it; createCMB and the tether share one category-text rule (mintCategoryText). The receive path's collapsed integrations stay exempt (§15.5), unchanged. CHANGELOG: under breaking, with before and after. Test: tests/spec-update-remix-tether.test.js. Not done (coordinator: wrap up): the mutation check on the new guard, and the downstream survey of node.remix() callers in mesh-channel and xmesh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRVMpaJkAA2fFbvuHCEzqH
…, §15.8) The full suite at 15849b0 had one failure: "an ESIGN from the synthesis loop is said as the node failing to sign" asserted synchronously, but remix() is asynchronous since 15849b0 and the loop says the failure when it settles. The handler already contains it (a rejection is logged, never left unhandled), so this is the test, not the lib. The test is async and awaits the settle; withNode cleans up after an async callback settles instead of when it returns. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
…two redundant lines removed A 28-mutant pass over remix()'s tether guard (skip the evaluation, always faithful, always severed, unverifiable as faithful, no fresh root, a parent kept, no or wrong attestation, the verdict reversed, the §15.7 flag not consumed or not given back, opts.cmb allowed, and others) left 11 survivors. Seven were real gaps and now have tests (tests/spec-update-remix-tether.test.js, 12 tests): - the anchor and attestation are about the lineage ROOT, not the parent cited (a hop above the root); - a severed remix citing several parents keeps none, and no category descends from anything; - the signed drift is what a third party recomputes from the minted record and the root (a remix given only a focus is evaluated on what it was minted with); - the verdict is on disk (a store rebuilt from the node's directory holds it); - nothing evaluable (every category weighted out): lineage kept, unverified, nothing signed; - lineageTether:false: no evaluation, no severance; - opts.cmb is refused and the flag is not spent; a remix that fails to mint gives the flag back. Two lines were redundant and are removed: `categoryParents: undefined` on a severed remix (remember() never reads categoryParents; the several-parents test would catch it if it ever does), and the copy of the verdict onto the returned entry (remember() returns the store's own entry, so it already carries it; the same test reads it from the returned entry). One mutant stays: dropping the `parentKeys.length > 0` early-out changes nothing observable (a record with no parents resolves to no root), so it is an equivalent mutant, kept as a cheap shortcut. A test that read the anchor through two records stored in one millisecond was flaky (the walk takes the earliest-stored ancestor): it waits 5 ms between them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
…ted kernel; tests for the fetched-record and 1011 guards; three hardenings
F1 (Medium). remix()'s tether measured in whatever kernel happened to be loaded. The §9.2.1 reject
floor is calibrated on the semantic encoder (all-MiniLM-L6-v2); the lexical n-gram encoder that runs
until it loads, or in its place where the optional @huggingface/transformers dependency is missing,
measures drift on another scale, so a `tethered` or `severed` verdict, an attestation or a severance
made in it means nothing. remix() now awaits the calibrated kernel, at most `tetherKernelWaitMs`
(default 10 s). If it is unavailable by then the remix keeps its lineage, unverified: nothing is
evaluated, severed or signed, and a line is logged once (a `tether-kernel-unavailable` metric). A
verdict from the lexical kernel is never signed. auditLineageTethers follows the same rule and
leaves a chain unchecked.
F4. Tests that fail when each guard is removed: the clear-1011 server-role guard (the server side
never prompts a handshake), and the fetched record's assertion-identity, audience and signature
checks (one record each, the reviewer's f-fetched-verify.js as the model).
F7. Legacy Import's trySend refuses a record signed for another room, as the seal point does. The
relay client's own-frame queue (auth, pong) is bounded: a pong already waiting answers the next, and
at most 8 wait. A remix() that began before stop() mints nothing after it (`{ refused: 'node-stopped'
}`, the flag given back). Stale comments refreshed (lib/rooms.js, bin/sym-daemon.js), and the
"vouched" wording in the unresolvable-author message and comment now says what §6.6.9 does (an
in-force grant names the key).
Mutation check of the new guards (13 mutants: the kernel check, its bound, its said-once line, the
audit check, the stop guard, the 1011 server-role guard, the three fetched-record checks, the
legacy room check, the queue cap and the pong dedupe): all caught.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
… say what the code does; the tarball ships the docs it points to F2. Every spec pin cites c07ff20, the merge of PR #43 into meshcognition-website main (the artifacts are byte-identical to the 0ab7b76 ones vendored before): SOURCES.json, WIRE-0.14.0.md, the CHANGELOG and the headers of the spec-update tests. "Pending merge" wording is gone. F3. e2e-v2.json re-vendored from c07ff20 (80f6183e…, differing from the earlier copy only by a `note`); the pin in tests/mmp-v2-vectors-published.test.js moved. The two tests that read it keep their path, so the one copy is replaced in place. F5. WIRE-0.14.0.md: a gap closes the session with a sealed 1010 on a relay session; on the LAN the transport closes and nothing is sent. F6. SECURITY.md: losing the attestation log while keeping the identity makes an honest attester look like an equivocator to peers that kept its checkpoints; use a new identity after a wipe. And the §15.8 paragraph (and the CHANGELOG's remix entry) now say that remix() and the audit measure in the calibrated kernel only, and that without the optional dependency §15.8 is not enforced. Release prep, short of the version: package.json "files" adds docs/AUTHORITY-OPERATOR.md, docs/WIRE-0.14.0.md and CHANGELOG.md (the shipped SECURITY.md points to the two docs); npm pack --dry-run lists 84 files, no tests. The EBADROOM note for a room like "My Team Room" moves from the update-1 list to "Core Secure — breaking". "version" stays 0.13.17 and the heading "(unreleased)". Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
Main-only: 2cf4dea and 3188621 (0.13.17: a re-announced relay peer keeps a live transport; a LAN transport added to a relay-first peer is used), c10b45e, and the XMesh-in-prose naming commits (977f32d and merges). Five files conflicted; each main hunk was read and decided: - lib/node.js (4 hunks): taken from 0.14.0. All were the 0.13 peer table: the peer-with-no-transport skip in the two send loops (trySend(null, ...) already returns "send failed", counted as not reached, never thrown on), stop() closing every transport a peer holds (stop() closes every session, and every transport belongs to one), and _addPeer's detach-before-close and bestTransport recompute (_createPeer and _addPeer are gone; _admitSession and _supersede recompute the active transport before the old session is closed, and a session's close removes only itself). - lib/frame-handler.js, bin/sym-daemon.js, lib/rooms.js: taken from 0.14.0; main's hunks were the XMesh spelling in comments the rewrite replaced (the spelling holds on the whole tree). - CHANGELOG.md: the 0.14.0 entry kept; main's 0.13.17 text replaces the summary in the history section, with a note why 0.14.0 does not carry that code; the XMesh spelling applied to the Tenant-suffixed bullet; no duplicated headings. Ported from main: its three 0.13.17 tests drove the 0.13 peer table (_createPeer, _addPeer), which 0.14.0 does not have. The first two were already run under Core Secure sessions in tests/relay-reannounce-scenes.test.js (d2099e7); the third (stop() survives a peer without a transport) is added there, with a room send past such a peer. tests/product-name.test.js (the naming rule) is kept and passes. package.json and package-lock.json merged without conflict. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
The hello's implementation, node.version, status().version and the module's version are read from package.json; a release cannot leave a literal behind. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
The ADMIT clause asserts the Admission Attestation on the stored entry. Since #27 (018206d) no attestation is signed about a directed record, so the driver's { to: nodeB.nodeId } record failed the clause on a correct runtime. The record is now a room broadcast, which is attested. Dev tooling only: scripts/ is not in the tarball, so the published artifact is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
sym 0.14.0 "Core Secure": MMP 2.0 update 1 (published 8 October 2026, sym-bot/meshcognition-website c07ff20) implemented end to end, with the 0.13.17 hotfix merged in.
What it is (full list in CHANGELOG.md):
_sym._tcpwith TXT room, the relay client for relay 0.6.1;remix()is asynchronous and §15.8-tethered (a breaking change, in the CHANGELOG with before and after).Breaking: LAN meshing with 0.13.17 stops except through Legacy Import routes, by design; room names must follow the §5.8 grammar (
EBADROOM); all 0.13 role grants confer nothing until the anchor re-issues them (docs/AUTHORITY-OPERATOR.md).Reviewed independently: the authority store (release review, narrow re-review, diff check: SHIP); PR #43's folds by three layer reviewers; the alignment round by a final combined review (SHIP-WITH-FIXES; every finding fixed); the §15.8 guard by a mutation check.
Verified at 6519392: full suite 1417 tests, 1414 pass, 0 fail, 3 skipped; integration 8/8;
npm run gate:stock-nodespassed;npm audit0 vulnerabilities; npm pack lists 84 files with no tests.Release: the founder approved publishing 0.14.0 under the npm tag
next;lateststays 0.13.17 until mesh-channel and XMesh are verified on it.🤖 Generated with Claude Code
https://claude.ai/code/session_01VMk5HbMaz4oD7zoakvPyA6