@cello-protocol/daemon 0.0.36 → 0.0.38
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/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +187 -13
- package/dist/daemon.js.map +1 -1
- package/dist/db-identity-store.d.ts +15 -0
- package/dist/db-identity-store.d.ts.map +1 -1
- package/dist/db-identity-store.js +51 -4
- package/dist/db-identity-store.js.map +1 -1
- package/dist/network-directory-node.d.ts +6 -1
- package/dist/network-directory-node.d.ts.map +1 -1
- package/dist/network-directory-node.js +40 -5
- package/dist/network-directory-node.js.map +1 -1
- package/dist/notification-dispatcher.d.ts +8 -2
- package/dist/notification-dispatcher.d.ts.map +1 -1
- package/dist/notification-dispatcher.js +9 -2
- package/dist/notification-dispatcher.js.map +1 -1
- package/dist/registration-manager.d.ts.map +1 -1
- package/dist/registration-manager.js +1 -0
- package/dist/registration-manager.js.map +1 -1
- package/dist/session-assignment-parser.d.ts +21 -0
- package/dist/session-assignment-parser.d.ts.map +1 -1
- package/dist/session-assignment-parser.js +37 -0
- package/dist/session-assignment-parser.js.map +1 -1
- package/dist/session-ceremony.d.ts +6 -0
- package/dist/session-ceremony.d.ts.map +1 -1
- package/dist/session-ceremony.js +2 -1
- package/dist/session-ceremony.js.map +1 -1
- package/dist/session-node-manager.d.ts +15 -3
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +59 -6
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/who-label.d.ts +28 -0
- package/dist/who-label.d.ts.map +1 -0
- package/dist/who-label.js +31 -0
- package/dist/who-label.js.map +1 -0
- package/package.json +4 -4
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwoL7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -50,10 +50,11 @@ import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCer
|
|
|
50
50
|
import { upgradeAbsentToRecovered, hasAbsentParticipant } from "./seal-receipt-upgrade.js";
|
|
51
51
|
// CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
|
|
52
52
|
// at the send point here (the receive point lives in the transport content decode).
|
|
53
|
-
import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs } from "@cello-protocol/protocol-types";
|
|
53
|
+
import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs, MONIKER_RE, validateMoniker } from "@cello-protocol/protocol-types";
|
|
54
54
|
import { resolveCelloEnv, createTransportSelector, isProductionVariant, } from "./transport-composition.js";
|
|
55
55
|
import { selectAdvertisedAddress } from "./transport-selector.js";
|
|
56
|
-
import { parseSessionAssignment, sessionRequestErrorReason, parseDiscoveryLookupResult, discoveryLookupErrorReason } from "./session-assignment-parser.js";
|
|
56
|
+
import { parseSessionAssignment, sessionRequestErrorReason, parseDiscoveryLookupResult, discoveryLookupErrorReason, extractOfferedMoniker, resolveOutboundMoniker } from "./session-assignment-parser.js";
|
|
57
|
+
import { whoLabel } from "./who-label.js";
|
|
57
58
|
import { classifyOnlineResult } from "./cross-node-negotiation.js";
|
|
58
59
|
import { wireSessionCeremonyHandler, wireSessionOfferHandler, wireSealCeremonyHandler, verifyUnilateralCertificate, verifyBilateralSealCertificate, runAgentRefresh } from "./session-ceremony.js";
|
|
59
60
|
import { reDeriveFrontiers, findInflatedFrontier, checkUnilateralFrontier } from "./seal-frontier-verify.js";
|
|
@@ -559,6 +560,7 @@ export async function startDaemon(config) {
|
|
|
559
560
|
agentName,
|
|
560
561
|
persistence: getPersistence(agentName),
|
|
561
562
|
agentPubkeyHex,
|
|
563
|
+
keyProvider: agentKeyProvider,
|
|
562
564
|
getNode: entry.getNode,
|
|
563
565
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
564
566
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -570,6 +572,7 @@ export async function startDaemon(config) {
|
|
|
570
572
|
agentName,
|
|
571
573
|
persistence: getPersistence(agentName),
|
|
572
574
|
agentPubkeyHex,
|
|
575
|
+
keyProvider: agentKeyProvider,
|
|
573
576
|
getNode: entry.getNode,
|
|
574
577
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
575
578
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -637,6 +640,7 @@ export async function startDaemon(config) {
|
|
|
637
640
|
agentName: agent.name,
|
|
638
641
|
persistence: getPersistence(agent.name),
|
|
639
642
|
agentPubkeyHex: agent.pubkey,
|
|
643
|
+
keyProvider: agent.keyProvider,
|
|
640
644
|
getNode: getDirectoryNode,
|
|
641
645
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
642
646
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -647,6 +651,7 @@ export async function startDaemon(config) {
|
|
|
647
651
|
agentName: agent.name,
|
|
648
652
|
persistence: getPersistence(agent.name),
|
|
649
653
|
agentPubkeyHex: agent.pubkey,
|
|
654
|
+
keyProvider: agent.keyProvider,
|
|
650
655
|
getNode: getDirectoryNode,
|
|
651
656
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
652
657
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -896,8 +901,27 @@ export async function startDaemon(config) {
|
|
|
896
901
|
// Wraps notificationDispatcher.dispatchSessionStateChanged so every call site gets the
|
|
897
902
|
// Telegram state-change doorbell for free (DoD: state changes ALWAYS ring, never coalesced) —
|
|
898
903
|
// one wrapper rather than hooking each of the several existing call sites individually.
|
|
904
|
+
// MONIKER-4 AC2: resolve the counterparty's display label — local pet name (MONIKER-3) ??
|
|
905
|
+
// offered name for this session (MONIKER-2) ?? fingerprint. Total: any failure inside
|
|
906
|
+
// resolution degrades to fingerprint via whoLabel's own tiers; a label can never block a
|
|
907
|
+
// doorbell (spec §8).
|
|
908
|
+
function resolveWho(agentName, pubkeyHex, sessionIdHex) {
|
|
909
|
+
let localMoniker = null;
|
|
910
|
+
try {
|
|
911
|
+
localMoniker = sessionNodeManager.getContactMoniker(agentName, pubkeyHex);
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
logger.warn("moniker.local.read_failed", { agentName, pubkey: pubkeyHex, reason: err instanceof Error ? err.message : String(err) });
|
|
915
|
+
}
|
|
916
|
+
const resolved = whoLabel({ localMoniker, offeredMoniker: offeredMonikers.get(sessionIdHex) ?? null, pubkeyHex });
|
|
917
|
+
logger.debug("moniker.resolved", { agentName, pubkey: pubkeyHex, source: resolved.source });
|
|
918
|
+
return { who: resolved.who, whoKnown: resolved.whoKnown };
|
|
919
|
+
}
|
|
899
920
|
function dispatchSessionStateChangedWithTelegram(agentName, sessionId, state, counterpartyPubkey) {
|
|
900
|
-
|
|
921
|
+
// MONIKER-4 AC2: stamp who/whoKnown on the counterparty-bearing frame. Resolved BEFORE the
|
|
922
|
+
// offered-name drop below so a terminal state's own doorbell still shows the name.
|
|
923
|
+
const who = counterpartyPubkey ? resolveWho(agentName, counterpartyPubkey, sessionId) : undefined;
|
|
924
|
+
notificationDispatcher.dispatchSessionStateChanged(agentName, sessionId, state, counterpartyPubkey, who);
|
|
901
925
|
void sendTelegramDoorbell(agentName, sessionId, "state_change", `Session ${state}`);
|
|
902
926
|
// Reviewer HIGH fix (a60d68ed): telegramRungUnread had NO cleanup at all — a session that
|
|
903
927
|
// rings once and is never read via cello_receive/since_seq (e.g. the operator only ever uses
|
|
@@ -907,6 +931,14 @@ export async function startDaemon(config) {
|
|
|
907
931
|
// preferable to an unbounded leak (the exact class of bug fixed for TTL-1's expired-log at
|
|
908
932
|
// af8a701 in this same milestone).
|
|
909
933
|
clearTelegramRung(agentName, sessionId);
|
|
934
|
+
// MONIKER-2 AC2b (review F1): the offered name is display material for the session's
|
|
935
|
+
// lifetime only. Production emits "created", "interrupted", "counterparty_closing" through
|
|
936
|
+
// this wrapper — a terminal-only check was dead code and left the map growing for the
|
|
937
|
+
// daemon's lifetime (remote-fed). Drop on ANY state past "created": a prematurely dropped
|
|
938
|
+
// label degrades to fingerprint, which the spec sanctions; an unbounded map does not.
|
|
939
|
+
if (state !== "created" && offeredMonikers.delete(sessionId)) {
|
|
940
|
+
logger.debug("moniker.offer.dropped", { sessionId, state });
|
|
941
|
+
}
|
|
910
942
|
}
|
|
911
943
|
async function handleInboundTelegramUpdate(update, client) {
|
|
912
944
|
const chatId = update.message?.chat?.id !== undefined ? String(update.message.chat.id) : undefined;
|
|
@@ -1080,6 +1112,28 @@ export async function startDaemon(config) {
|
|
|
1080
1112
|
if (t === "session_assignment" || t === "session_request_error")
|
|
1081
1113
|
resolveFrame(frame);
|
|
1082
1114
|
});
|
|
1115
|
+
// MONIKER-2 AC1: the outbound name rides the request (agent name, or the MONIKER-1
|
|
1116
|
+
// override), re-validated at offer construction (defense in depth) and OMITTED — never
|
|
1117
|
+
// an empty string — when absent or invalid. A failure reading it must never block the
|
|
1118
|
+
// session (spec §8: the label degrades, the session forms), but it is logged loudly.
|
|
1119
|
+
let moniker;
|
|
1120
|
+
try {
|
|
1121
|
+
const outboundName = new DbIdentityStore(sessionNodeManager.getDb(), logger).getOutboundName(agentName);
|
|
1122
|
+
moniker = resolveOutboundMoniker(outboundName);
|
|
1123
|
+
// Review F4: omit-not-send is the designed degradation, but an agent whose STORED name
|
|
1124
|
+
// fails the rule should be visible, not silently nameless on the wire.
|
|
1125
|
+
if (outboundName !== null && moniker === undefined) {
|
|
1126
|
+
logger.debug("moniker.outbound.invalid_name_omitted", { agentName, correlationId });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
catch (err) {
|
|
1130
|
+
logger.warn("moniker.outbound.read_failed", {
|
|
1131
|
+
agentName,
|
|
1132
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1133
|
+
correlationId,
|
|
1134
|
+
});
|
|
1135
|
+
moniker = undefined;
|
|
1136
|
+
}
|
|
1083
1137
|
try {
|
|
1084
1138
|
const sent = await signaling.sendRaw({
|
|
1085
1139
|
type: "session_request",
|
|
@@ -1087,6 +1141,7 @@ export async function startDaemon(config) {
|
|
|
1087
1141
|
initiator_session_peer_id: sr.peerId,
|
|
1088
1142
|
initiator_session_addrs: sr.addrs,
|
|
1089
1143
|
wants_session_offer: true,
|
|
1144
|
+
...(moniker !== undefined ? { moniker } : {}),
|
|
1090
1145
|
});
|
|
1091
1146
|
if (!sent.ok) {
|
|
1092
1147
|
return { ok: false, reason: sent.reason ?? "directory_unreachable", guidance: sent.guidance ?? "Could not send session_request over the directory signaling stream." };
|
|
@@ -1143,6 +1198,7 @@ export async function startDaemon(config) {
|
|
|
1143
1198
|
agentName,
|
|
1144
1199
|
persistence: getPersistence(agentName),
|
|
1145
1200
|
agentPubkeyHex,
|
|
1201
|
+
keyProvider: agentKeyProvider,
|
|
1146
1202
|
getNode: () => nodeRef,
|
|
1147
1203
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
1148
1204
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -1159,6 +1215,7 @@ export async function startDaemon(config) {
|
|
|
1159
1215
|
agentName,
|
|
1160
1216
|
persistence: getPersistence(agentName),
|
|
1161
1217
|
agentPubkeyHex,
|
|
1218
|
+
keyProvider: agentKeyProvider,
|
|
1162
1219
|
getNode: () => nodeRef,
|
|
1163
1220
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
1164
1221
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -1738,9 +1795,9 @@ export async function startDaemon(config) {
|
|
|
1738
1795
|
// the encrypted DB (NO key file), and wire the agent into the live daemon so it can be registered
|
|
1739
1796
|
// and used WITHOUT a restart. Creation is explicit — cello_start_agent never auto-creates on a typo.
|
|
1740
1797
|
handlers.set("cello_create_agent", async (params, _connectionId) => {
|
|
1741
|
-
const name = params?.name;
|
|
1742
|
-
if (
|
|
1743
|
-
return { ok: false, reason: "invalid_agent_name", guidance:
|
|
1798
|
+
const name = validateMoniker(params?.name);
|
|
1799
|
+
if (name === null) {
|
|
1800
|
+
return { ok: false, reason: "invalid_agent_name", guidance: `Provide a 'name' (1-64 chars: letters, digits, '-' or '_'; regex ${MONIKER_RE.source}) for the new agent.` };
|
|
1744
1801
|
}
|
|
1745
1802
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1746
1803
|
if (store.hasActiveAgent(name) || agents.some((a) => a.name === name)) {
|
|
@@ -1845,9 +1902,9 @@ export async function startDaemon(config) {
|
|
|
1845
1902
|
}
|
|
1846
1903
|
}
|
|
1847
1904
|
handlers.set("cello_remove_agent", async (params, _connectionId) => {
|
|
1848
|
-
const name = params?.name;
|
|
1849
|
-
if (
|
|
1850
|
-
return { ok: false, reason: "invalid_agent_name", guidance:
|
|
1905
|
+
const name = validateMoniker(params?.name);
|
|
1906
|
+
if (name === null) {
|
|
1907
|
+
return { ok: false, reason: "invalid_agent_name", guidance: `Provide the 'name' of the agent to remove (1-64 chars: letters, digits, '-' or '_'; regex ${MONIKER_RE.source}).` };
|
|
1851
1908
|
}
|
|
1852
1909
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1853
1910
|
// The active row (a fresh removal) OR the most-recent retired row (a DB-001 re-push). Captured BEFORE
|
|
@@ -2720,6 +2777,7 @@ export async function startDaemon(config) {
|
|
|
2720
2777
|
agentName,
|
|
2721
2778
|
persistence: getPersistence(agentName),
|
|
2722
2779
|
agentPubkeyHex: loaded.pubkey,
|
|
2780
|
+
keyProvider: loaded.keyProvider,
|
|
2723
2781
|
getNode: entry.getNode,
|
|
2724
2782
|
getDirectoryEndpoint: getFailoverEndpoint,
|
|
2725
2783
|
getConsortiumEndpoints: resolveConsortiumRoster,
|
|
@@ -3374,6 +3432,8 @@ export async function startDaemon(config) {
|
|
|
3374
3432
|
sessionId: row.session_id,
|
|
3375
3433
|
agentName: row.agent_name,
|
|
3376
3434
|
counterpartyPubkey: row.counterparty_pubkey,
|
|
3435
|
+
// MONIKER-5 AC1: the same resolution the doorbell uses — pet name ?? offered ?? fingerprint.
|
|
3436
|
+
...resolveWho(row.agent_name, row.counterparty_pubkey, row.session_id),
|
|
3377
3437
|
status: row.status,
|
|
3378
3438
|
category,
|
|
3379
3439
|
messageCount,
|
|
@@ -3861,6 +3921,10 @@ export async function startDaemon(config) {
|
|
|
3861
3921
|
correlationId,
|
|
3862
3922
|
});
|
|
3863
3923
|
}
|
|
3924
|
+
// MONIKER-2 AC2b: sessionIdHex → the initiator's validated offered name. Session-scoped
|
|
3925
|
+
// display material ONLY (spec: promotion to a stored pet name needs an explicit operator
|
|
3926
|
+
// action). In-memory by design — a restart degrades the label to fingerprint, never worse.
|
|
3927
|
+
const offeredMonikers = new Map();
|
|
3864
3928
|
// Expired requests move HERE (from inboundSessionQueues) rather than vanishing — visible via
|
|
3865
3929
|
// cello_check_notifications so the operator can see what they missed, not just silence.
|
|
3866
3930
|
const expiredSessionRequests = new Map();
|
|
@@ -3921,6 +3985,10 @@ export async function startDaemon(config) {
|
|
|
3921
3985
|
expiredList.splice(0, expiredList.length - EXPIRED_SESSION_REQUESTS_CAP); // keep newest N
|
|
3922
3986
|
}
|
|
3923
3987
|
logger.info("session.request.expired", { agentName, sessionId: e.sessionIdHex, enqueuedAt: e.enqueuedAt });
|
|
3988
|
+
// MONIKER-2 AC2b (review F1): the offer's display name expires with the offer.
|
|
3989
|
+
if (offeredMonikers.delete(e.sessionIdHex)) {
|
|
3990
|
+
logger.debug("moniker.offer.dropped", { sessionId: e.sessionIdHex, state: "expired" });
|
|
3991
|
+
}
|
|
3924
3992
|
}
|
|
3925
3993
|
else {
|
|
3926
3994
|
live.push(e);
|
|
@@ -4017,6 +4085,8 @@ export async function startDaemon(config) {
|
|
|
4017
4085
|
const relayAddrs = relayEndpoint && Array.isArray(relayEndpoint["multiaddrs"])
|
|
4018
4086
|
? relayEndpoint["multiaddrs"].filter((m) => typeof m === "string")
|
|
4019
4087
|
: [];
|
|
4088
|
+
// MONIKER-2 AC2: validate the offered name ONCE, at this wire boundary.
|
|
4089
|
+
const monikerResult = extractOfferedMoniker(a);
|
|
4020
4090
|
return {
|
|
4021
4091
|
sessionIdHex,
|
|
4022
4092
|
participantAPubkeyHex,
|
|
@@ -4031,6 +4101,9 @@ export async function startDaemon(config) {
|
|
|
4031
4101
|
signerPubkeyHex: a["signer_pubkey"] !== undefined ? frameValueToHex(a["signer_pubkey"]) : null,
|
|
4032
4102
|
relayPeerId,
|
|
4033
4103
|
relayAddrs,
|
|
4104
|
+
offeredMoniker: monikerResult.offeredMoniker,
|
|
4105
|
+
monikerRejected: monikerResult.rejected,
|
|
4106
|
+
monikerRejectReason: monikerResult.reason ?? null,
|
|
4034
4107
|
};
|
|
4035
4108
|
}
|
|
4036
4109
|
// Wait (bounded) for THIS AGENT's standing receiver to be ready. acceptSession consumes the
|
|
@@ -4135,10 +4208,28 @@ export async function startDaemon(config) {
|
|
|
4135
4208
|
sessionPeerId: result.peerId,
|
|
4136
4209
|
correlationId,
|
|
4137
4210
|
});
|
|
4211
|
+
// MONIKER-2 AC2: a present-but-invalid moniker is a red flag (the sender runs modified
|
|
4212
|
+
// code) — logged with the spec'd fields and NEVER the raw value. Not grounds to refuse
|
|
4213
|
+
// the session (version skew is real; refusing hands strangers a DoS lever).
|
|
4214
|
+
if (parsed.monikerRejected) {
|
|
4215
|
+
logger.info("moniker.rejected", {
|
|
4216
|
+
agentName,
|
|
4217
|
+
pubkey: parsed.participantAPubkeyHex,
|
|
4218
|
+
reason: parsed.monikerRejectReason ?? "invalid",
|
|
4219
|
+
correlationId,
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
// MONIKER-2 AC2b: display material for THIS offer/session only (never persisted, never
|
|
4223
|
+
// auto-written to contacts — AC3). Session-scoped: MONIKER-4's dispatcher resolves
|
|
4224
|
+
// whoLabel from here; entries are dropped when the session's terminal state dispatches.
|
|
4225
|
+
if (parsed.offeredMoniker !== null) {
|
|
4226
|
+
offeredMonikers.set(parsed.sessionIdHex, parsed.offeredMoniker);
|
|
4227
|
+
}
|
|
4138
4228
|
enqueueInboundSession(agentName, {
|
|
4139
4229
|
sessionIdHex: parsed.sessionIdHex,
|
|
4140
4230
|
counterpartyPubkeyHex: parsed.participantAPubkeyHex,
|
|
4141
4231
|
genesisPrevRootHex,
|
|
4232
|
+
offeredMoniker: parsed.offeredMoniker,
|
|
4142
4233
|
});
|
|
4143
4234
|
dispatchSessionStateChangedWithTelegram(agentName, parsed.sessionIdHex, "created", parsed.participantAPubkeyHex);
|
|
4144
4235
|
// M8C-TGDOOR-1: session requests ALWAYS ring (DoD) — no coalescing, unlike message-waiting.
|
|
@@ -4355,6 +4446,8 @@ export async function startDaemon(config) {
|
|
|
4355
4446
|
session_id: e.sessionIdHex,
|
|
4356
4447
|
counterparty_pubkey: e.counterpartyPubkeyHex,
|
|
4357
4448
|
genesis_prev_root: e.genesisPrevRootHex,
|
|
4449
|
+
// MONIKER-2: validated-at-boundary display hint; null = absent or rejected.
|
|
4450
|
+
offered_moniker: e.offeredMoniker ?? null,
|
|
4358
4451
|
});
|
|
4359
4452
|
reapExpiredInboundSessions(agentName); // M8C-TTL-1: don't hand back a stale expired entry
|
|
4360
4453
|
const queued = inboundSessionQueues.get(agentName);
|
|
@@ -5207,9 +5300,54 @@ export async function startDaemon(config) {
|
|
|
5207
5300
|
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5208
5301
|
if (!resolved.ok)
|
|
5209
5302
|
return resolved;
|
|
5210
|
-
|
|
5303
|
+
// MONIKER-3 AC3: optional pet name. Invalid → reject the request WHOLE (no contact row
|
|
5304
|
+
// either) — a half-applied add would store trust without the name the operator asked for.
|
|
5305
|
+
let moniker;
|
|
5306
|
+
if (params && "moniker" in params && params.moniker !== undefined && params.moniker !== null) {
|
|
5307
|
+
moniker = validateMoniker(params.moniker);
|
|
5308
|
+
if (moniker === null) {
|
|
5309
|
+
return {
|
|
5310
|
+
ok: false,
|
|
5311
|
+
reason: "invalid_moniker",
|
|
5312
|
+
guidance: `A moniker is 1-64 chars: letters, digits, '-' or '_' (regex ${MONIKER_RE.source}).`,
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
sessionNodeManager.addContact(resolved.agent, pubkey, moniker);
|
|
5211
5317
|
logger.info("contact.added", { agent: resolved.agent, pubkey });
|
|
5212
|
-
|
|
5318
|
+
if (moniker !== undefined) {
|
|
5319
|
+
logger.info("contact.moniker.set", { agentName: resolved.agent, pubkey });
|
|
5320
|
+
}
|
|
5321
|
+
// Review F3: echo the moniker ONLY when one rode this request — a re-add without a moniker
|
|
5322
|
+
// must not report null while an earlier pet name is still stored.
|
|
5323
|
+
return moniker !== undefined
|
|
5324
|
+
? { ok: true, agent: resolved.agent, pubkey, moniker }
|
|
5325
|
+
: { ok: true, agent: resolved.agent, pubkey };
|
|
5326
|
+
});
|
|
5327
|
+
// MONIKER-3 AC3: rename (string) or clear (null) an existing contact's pet name. Absence of the
|
|
5328
|
+
// key is NOT a clear (Entry-66-F3): a request that omits it is malformed and rejected.
|
|
5329
|
+
handlers.set("cello_contact_set_moniker", async (params, connectionId) => {
|
|
5330
|
+
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5331
|
+
if (!pubkey || !params || !("moniker" in params)) {
|
|
5332
|
+
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) and 'moniker' — a string to set the pet name, or null to clear it." };
|
|
5333
|
+
}
|
|
5334
|
+
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5335
|
+
if (!resolved.ok)
|
|
5336
|
+
return resolved;
|
|
5337
|
+
const raw = params.moniker ?? null;
|
|
5338
|
+
const moniker = raw === null ? null : validateMoniker(raw);
|
|
5339
|
+
if (raw !== null && moniker === null) {
|
|
5340
|
+
return {
|
|
5341
|
+
ok: false,
|
|
5342
|
+
reason: "invalid_moniker",
|
|
5343
|
+
guidance: `A moniker is 1-64 chars: letters, digits, '-' or '_' (regex ${MONIKER_RE.source}). Pass null to clear.`,
|
|
5344
|
+
};
|
|
5345
|
+
}
|
|
5346
|
+
if (!sessionNodeManager.setContactMoniker(resolved.agent, pubkey, moniker)) {
|
|
5347
|
+
return { ok: false, reason: "contact_not_found", guidance: `No contact ${pubkey.slice(0, 16)}… for agent '${resolved.agent}'. Add it first with cello_contact_add.` };
|
|
5348
|
+
}
|
|
5349
|
+
logger.info("contact.moniker.set", { agentName: resolved.agent, pubkey, cleared: moniker === null });
|
|
5350
|
+
return { ok: true, agent: resolved.agent, pubkey, moniker };
|
|
5213
5351
|
});
|
|
5214
5352
|
handlers.set("cello_contact_remove", async (params, connectionId) => {
|
|
5215
5353
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
@@ -5227,9 +5365,44 @@ export async function startDaemon(config) {
|
|
|
5227
5365
|
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5228
5366
|
if (!resolved.ok)
|
|
5229
5367
|
return resolved;
|
|
5230
|
-
|
|
5368
|
+
// MONIKER-5 AC1: each row shows the resolved who (a contact has no session, so the offered
|
|
5369
|
+
// tier doesn't apply — pet name ?? fingerprint).
|
|
5370
|
+
const contacts = sessionNodeManager.listContacts(resolved.agent).map((c) => ({
|
|
5371
|
+
...c,
|
|
5372
|
+
...whoLabel({ localMoniker: c.moniker, offeredMoniker: null, pubkeyHex: c.pubkey }),
|
|
5373
|
+
}));
|
|
5231
5374
|
return { ok: true, agent: resolved.agent, contacts };
|
|
5232
5375
|
});
|
|
5376
|
+
// MONIKER-1 AC2/AC3: cello_set_moniker — set (or clear, via explicit null) an agent's outbound-name
|
|
5377
|
+
// override on the agents table. Validated at set-time with the shared MONIKER-0 rule; an invalid
|
|
5378
|
+
// value is rejected here AND at the store (backstop) — it can never be stored. Local-only: the
|
|
5379
|
+
// name is never sent to the directory (AC4).
|
|
5380
|
+
handlers.set("cello_set_moniker", async (params, connectionId) => {
|
|
5381
|
+
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5382
|
+
if (!resolved.ok)
|
|
5383
|
+
return resolved;
|
|
5384
|
+
// Absence is NOT a clear (review Finding 3): JSON preserves explicit null, so a request
|
|
5385
|
+
// that omits the key is malformed — rejecting it keeps a dropped field from silently
|
|
5386
|
+
// deleting a stored override while reporting success.
|
|
5387
|
+
if (!params || !("moniker" in params)) {
|
|
5388
|
+
return { ok: false, reason: "missing_params", guidance: "Provide 'moniker' — a string to set the outbound name, or null to clear the override." };
|
|
5389
|
+
}
|
|
5390
|
+
const raw = params.moniker ?? null;
|
|
5391
|
+
const moniker = raw === null ? null : validateMoniker(raw);
|
|
5392
|
+
if (raw !== null && moniker === null) {
|
|
5393
|
+
return {
|
|
5394
|
+
ok: false,
|
|
5395
|
+
reason: "invalid_moniker",
|
|
5396
|
+
guidance: `A moniker is 1-64 chars: letters, digits, '-' or '_' (regex ${MONIKER_RE.source}). Pass null to clear the override.`,
|
|
5397
|
+
};
|
|
5398
|
+
}
|
|
5399
|
+
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
5400
|
+
if (!store.setMoniker(resolved.agent, moniker)) {
|
|
5401
|
+
return { ok: false, reason: "agent_not_found", guidance: `No active agent named '${resolved.agent}'. See cello_list_agents.` };
|
|
5402
|
+
}
|
|
5403
|
+
logger.info("agent.moniker.set", { agentName: resolved.agent, cleared: moniker === null });
|
|
5404
|
+
return { ok: true, agent: resolved.agent, moniker };
|
|
5405
|
+
});
|
|
5233
5406
|
// M8C-TGDOOR-1: cello_telegram_set_token — persist the daemon-wide bot token + allowlisted
|
|
5234
5407
|
// operator chat ID, then start the poller immediately (no restart needed).
|
|
5235
5408
|
handlers.set("cello_telegram_set_token", async (params, _connectionId) => {
|
|
@@ -5282,7 +5455,8 @@ export async function startDaemon(config) {
|
|
|
5282
5455
|
// content-free `cello_message` doorbell to the current-agent connection(s). The shim's generic
|
|
5283
5456
|
// bridge (WAKE) forwards it to a live --channels session as notifications/claude/channel.
|
|
5284
5457
|
sessionNodeManager.setOnContentArrived((agentName, sessionId, senderPubkey) => {
|
|
5285
|
-
|
|
5458
|
+
// MONIKER-4 AC2: the message doorbell names the sender the same way the session doorbell does.
|
|
5459
|
+
notificationDispatcher.dispatchCelloMessage(agentName, sessionId, senderPubkey, resolveWho(agentName, senderPubkey, sessionId));
|
|
5286
5460
|
// M8C-AWAY-1: an unattended agent auto-acks an inbound message on an existing session.
|
|
5287
5461
|
void sendAwayResponse(agentName, sessionId, "message");
|
|
5288
5462
|
// M8C-TGDOOR-1: message-waiting — coalesced (ring-once-until-read) inside sendTelegramDoorbell.
|