@cello-protocol/daemon 0.0.37 → 0.0.39
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 +191 -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/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/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-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 +3 -3
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,CAmpL7E"}
|
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";
|
|
@@ -900,8 +901,28 @@ export async function startDaemon(config) {
|
|
|
900
901
|
// Wraps notificationDispatcher.dispatchSessionStateChanged so every call site gets the
|
|
901
902
|
// Telegram state-change doorbell for free (DoD: state changes ALWAYS ring, never coalesced) —
|
|
902
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
|
+
// DOD-MONIKER-6: read only the box written FOR this agent — never a co-resident agent's.
|
|
917
|
+
const resolved = whoLabel({ localMoniker, offeredMoniker: offeredMonikers.get(offerKey(agentName, sessionIdHex)) ?? null, pubkeyHex });
|
|
918
|
+
logger.debug("moniker.resolved", { agentName, pubkey: pubkeyHex, source: resolved.source });
|
|
919
|
+
return { who: resolved.who, whoKnown: resolved.whoKnown };
|
|
920
|
+
}
|
|
903
921
|
function dispatchSessionStateChangedWithTelegram(agentName, sessionId, state, counterpartyPubkey) {
|
|
904
|
-
|
|
922
|
+
// MONIKER-4 AC2: stamp who/whoKnown on the counterparty-bearing frame. Resolved BEFORE the
|
|
923
|
+
// offered-name drop below so a terminal state's own doorbell still shows the name.
|
|
924
|
+
const who = counterpartyPubkey ? resolveWho(agentName, counterpartyPubkey, sessionId) : undefined;
|
|
925
|
+
notificationDispatcher.dispatchSessionStateChanged(agentName, sessionId, state, counterpartyPubkey, who);
|
|
905
926
|
void sendTelegramDoorbell(agentName, sessionId, "state_change", `Session ${state}`);
|
|
906
927
|
// Reviewer HIGH fix (a60d68ed): telegramRungUnread had NO cleanup at all — a session that
|
|
907
928
|
// rings once and is never read via cello_receive/since_seq (e.g. the operator only ever uses
|
|
@@ -911,6 +932,16 @@ export async function startDaemon(config) {
|
|
|
911
932
|
// preferable to an unbounded leak (the exact class of bug fixed for TTL-1's expired-log at
|
|
912
933
|
// af8a701 in this same milestone).
|
|
913
934
|
clearTelegramRung(agentName, sessionId);
|
|
935
|
+
// MONIKER-2 AC2b (review F1): the offered name is display material for the session's
|
|
936
|
+
// lifetime only. Production emits "created", "interrupted", "counterparty_closing" through
|
|
937
|
+
// this wrapper — a terminal-only check was dead code and left the map growing for the
|
|
938
|
+
// daemon's lifetime (remote-fed). Drop on ANY state past "created": a prematurely dropped
|
|
939
|
+
// label degrades to fingerprint, which the spec sanctions; an unbounded map does not.
|
|
940
|
+
// DOD-MONIKER-6 AC3: drop only THIS agent's box — a co-resident agent's session moving on
|
|
941
|
+
// must never cost this agent the caller's name.
|
|
942
|
+
if (state !== "created" && offeredMonikers.delete(offerKey(agentName, sessionId))) {
|
|
943
|
+
logger.debug("moniker.offer.dropped", { agentName, sessionId, state });
|
|
944
|
+
}
|
|
914
945
|
}
|
|
915
946
|
async function handleInboundTelegramUpdate(update, client) {
|
|
916
947
|
const chatId = update.message?.chat?.id !== undefined ? String(update.message.chat.id) : undefined;
|
|
@@ -1084,6 +1115,28 @@ export async function startDaemon(config) {
|
|
|
1084
1115
|
if (t === "session_assignment" || t === "session_request_error")
|
|
1085
1116
|
resolveFrame(frame);
|
|
1086
1117
|
});
|
|
1118
|
+
// MONIKER-2 AC1: the outbound name rides the request (agent name, or the MONIKER-1
|
|
1119
|
+
// override), re-validated at offer construction (defense in depth) and OMITTED — never
|
|
1120
|
+
// an empty string — when absent or invalid. A failure reading it must never block the
|
|
1121
|
+
// session (spec §8: the label degrades, the session forms), but it is logged loudly.
|
|
1122
|
+
let moniker;
|
|
1123
|
+
try {
|
|
1124
|
+
const outboundName = new DbIdentityStore(sessionNodeManager.getDb(), logger).getOutboundName(agentName);
|
|
1125
|
+
moniker = resolveOutboundMoniker(outboundName);
|
|
1126
|
+
// Review F4: omit-not-send is the designed degradation, but an agent whose STORED name
|
|
1127
|
+
// fails the rule should be visible, not silently nameless on the wire.
|
|
1128
|
+
if (outboundName !== null && moniker === undefined) {
|
|
1129
|
+
logger.debug("moniker.outbound.invalid_name_omitted", { agentName, correlationId });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
logger.warn("moniker.outbound.read_failed", {
|
|
1134
|
+
agentName,
|
|
1135
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1136
|
+
correlationId,
|
|
1137
|
+
});
|
|
1138
|
+
moniker = undefined;
|
|
1139
|
+
}
|
|
1087
1140
|
try {
|
|
1088
1141
|
const sent = await signaling.sendRaw({
|
|
1089
1142
|
type: "session_request",
|
|
@@ -1091,6 +1144,7 @@ export async function startDaemon(config) {
|
|
|
1091
1144
|
initiator_session_peer_id: sr.peerId,
|
|
1092
1145
|
initiator_session_addrs: sr.addrs,
|
|
1093
1146
|
wants_session_offer: true,
|
|
1147
|
+
...(moniker !== undefined ? { moniker } : {}),
|
|
1094
1148
|
});
|
|
1095
1149
|
if (!sent.ok) {
|
|
1096
1150
|
return { ok: false, reason: sent.reason ?? "directory_unreachable", guidance: sent.guidance ?? "Could not send session_request over the directory signaling stream." };
|
|
@@ -1744,9 +1798,9 @@ export async function startDaemon(config) {
|
|
|
1744
1798
|
// the encrypted DB (NO key file), and wire the agent into the live daemon so it can be registered
|
|
1745
1799
|
// and used WITHOUT a restart. Creation is explicit — cello_start_agent never auto-creates on a typo.
|
|
1746
1800
|
handlers.set("cello_create_agent", async (params, _connectionId) => {
|
|
1747
|
-
const name = params?.name;
|
|
1748
|
-
if (
|
|
1749
|
-
return { ok: false, reason: "invalid_agent_name", guidance:
|
|
1801
|
+
const name = validateMoniker(params?.name);
|
|
1802
|
+
if (name === null) {
|
|
1803
|
+
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.` };
|
|
1750
1804
|
}
|
|
1751
1805
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1752
1806
|
if (store.hasActiveAgent(name) || agents.some((a) => a.name === name)) {
|
|
@@ -1851,9 +1905,9 @@ export async function startDaemon(config) {
|
|
|
1851
1905
|
}
|
|
1852
1906
|
}
|
|
1853
1907
|
handlers.set("cello_remove_agent", async (params, _connectionId) => {
|
|
1854
|
-
const name = params?.name;
|
|
1855
|
-
if (
|
|
1856
|
-
return { ok: false, reason: "invalid_agent_name", guidance:
|
|
1908
|
+
const name = validateMoniker(params?.name);
|
|
1909
|
+
if (name === null) {
|
|
1910
|
+
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}).` };
|
|
1857
1911
|
}
|
|
1858
1912
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1859
1913
|
// The active row (a fresh removal) OR the most-recent retired row (a DB-001 re-push). Captured BEFORE
|
|
@@ -3381,6 +3435,8 @@ export async function startDaemon(config) {
|
|
|
3381
3435
|
sessionId: row.session_id,
|
|
3382
3436
|
agentName: row.agent_name,
|
|
3383
3437
|
counterpartyPubkey: row.counterparty_pubkey,
|
|
3438
|
+
// MONIKER-5 AC1: the same resolution the doorbell uses — pet name ?? offered ?? fingerprint.
|
|
3439
|
+
...resolveWho(row.agent_name, row.counterparty_pubkey, row.session_id),
|
|
3384
3440
|
status: row.status,
|
|
3385
3441
|
category,
|
|
3386
3442
|
messageCount,
|
|
@@ -3868,6 +3924,17 @@ export async function startDaemon(config) {
|
|
|
3868
3924
|
correlationId,
|
|
3869
3925
|
});
|
|
3870
3926
|
}
|
|
3927
|
+
// MONIKER-2 AC2b: the initiator's validated offered name. Session-scoped display material ONLY
|
|
3928
|
+
// (spec: promotion to a stored pet name needs an explicit operator action). In-memory by design —
|
|
3929
|
+
// a restart degrades the label to fingerprint, never worse.
|
|
3930
|
+
//
|
|
3931
|
+
// DOD-MONIKER-6 (spec §10): keyed by (agentName, sessionIdHex), NEVER sessionIdHex alone. A box
|
|
3932
|
+
// is written by the RECEIVING side and holds the CALLER's name, so it is meaningful only to the
|
|
3933
|
+
// agent it was written for. Keyed by session id alone, a daemon hosting both participants hands
|
|
3934
|
+
// the initiator the box filled in for her counterparty — she is told she messaged herself — and
|
|
3935
|
+
// either agent's state change or request expiry silently drops the other's name.
|
|
3936
|
+
const offeredMonikers = new Map();
|
|
3937
|
+
const offerKey = (agentName, sessionIdHex) => `${agentName}:${sessionIdHex}`;
|
|
3871
3938
|
// Expired requests move HERE (from inboundSessionQueues) rather than vanishing — visible via
|
|
3872
3939
|
// cello_check_notifications so the operator can see what they missed, not just silence.
|
|
3873
3940
|
const expiredSessionRequests = new Map();
|
|
@@ -3928,6 +3995,10 @@ export async function startDaemon(config) {
|
|
|
3928
3995
|
expiredList.splice(0, expiredList.length - EXPIRED_SESSION_REQUESTS_CAP); // keep newest N
|
|
3929
3996
|
}
|
|
3930
3997
|
logger.info("session.request.expired", { agentName, sessionId: e.sessionIdHex, enqueuedAt: e.enqueuedAt });
|
|
3998
|
+
// MONIKER-2 AC2b (review F1): the offer's display name expires with the offer.
|
|
3999
|
+
if (offeredMonikers.delete(offerKey(agentName, e.sessionIdHex))) {
|
|
4000
|
+
logger.debug("moniker.offer.dropped", { agentName, sessionId: e.sessionIdHex, state: "expired" });
|
|
4001
|
+
}
|
|
3931
4002
|
}
|
|
3932
4003
|
else {
|
|
3933
4004
|
live.push(e);
|
|
@@ -4024,6 +4095,8 @@ export async function startDaemon(config) {
|
|
|
4024
4095
|
const relayAddrs = relayEndpoint && Array.isArray(relayEndpoint["multiaddrs"])
|
|
4025
4096
|
? relayEndpoint["multiaddrs"].filter((m) => typeof m === "string")
|
|
4026
4097
|
: [];
|
|
4098
|
+
// MONIKER-2 AC2: validate the offered name ONCE, at this wire boundary.
|
|
4099
|
+
const monikerResult = extractOfferedMoniker(a);
|
|
4027
4100
|
return {
|
|
4028
4101
|
sessionIdHex,
|
|
4029
4102
|
participantAPubkeyHex,
|
|
@@ -4038,6 +4111,9 @@ export async function startDaemon(config) {
|
|
|
4038
4111
|
signerPubkeyHex: a["signer_pubkey"] !== undefined ? frameValueToHex(a["signer_pubkey"]) : null,
|
|
4039
4112
|
relayPeerId,
|
|
4040
4113
|
relayAddrs,
|
|
4114
|
+
offeredMoniker: monikerResult.offeredMoniker,
|
|
4115
|
+
monikerRejected: monikerResult.rejected,
|
|
4116
|
+
monikerRejectReason: monikerResult.reason ?? null,
|
|
4041
4117
|
};
|
|
4042
4118
|
}
|
|
4043
4119
|
// Wait (bounded) for THIS AGENT's standing receiver to be ready. acceptSession consumes the
|
|
@@ -4142,10 +4218,29 @@ export async function startDaemon(config) {
|
|
|
4142
4218
|
sessionPeerId: result.peerId,
|
|
4143
4219
|
correlationId,
|
|
4144
4220
|
});
|
|
4221
|
+
// MONIKER-2 AC2: a present-but-invalid moniker is a red flag (the sender runs modified
|
|
4222
|
+
// code) — logged with the spec'd fields and NEVER the raw value. Not grounds to refuse
|
|
4223
|
+
// the session (version skew is real; refusing hands strangers a DoS lever).
|
|
4224
|
+
if (parsed.monikerRejected) {
|
|
4225
|
+
logger.info("moniker.rejected", {
|
|
4226
|
+
agentName,
|
|
4227
|
+
pubkey: parsed.participantAPubkeyHex,
|
|
4228
|
+
reason: parsed.monikerRejectReason ?? "invalid",
|
|
4229
|
+
correlationId,
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
4232
|
+
// MONIKER-2 AC2b: display material for THIS offer/session only (never persisted, never
|
|
4233
|
+
// auto-written to contacts — AC3). Session-scoped: MONIKER-4's dispatcher resolves
|
|
4234
|
+
// whoLabel from here; entries are dropped when the session's terminal state dispatches.
|
|
4235
|
+
// DOD-MONIKER-6: scoped to `agentName` — the agent this offer was received FOR.
|
|
4236
|
+
if (parsed.offeredMoniker !== null) {
|
|
4237
|
+
offeredMonikers.set(offerKey(agentName, parsed.sessionIdHex), parsed.offeredMoniker);
|
|
4238
|
+
}
|
|
4145
4239
|
enqueueInboundSession(agentName, {
|
|
4146
4240
|
sessionIdHex: parsed.sessionIdHex,
|
|
4147
4241
|
counterpartyPubkeyHex: parsed.participantAPubkeyHex,
|
|
4148
4242
|
genesisPrevRootHex,
|
|
4243
|
+
offeredMoniker: parsed.offeredMoniker,
|
|
4149
4244
|
});
|
|
4150
4245
|
dispatchSessionStateChangedWithTelegram(agentName, parsed.sessionIdHex, "created", parsed.participantAPubkeyHex);
|
|
4151
4246
|
// M8C-TGDOOR-1: session requests ALWAYS ring (DoD) — no coalescing, unlike message-waiting.
|
|
@@ -4362,6 +4457,8 @@ export async function startDaemon(config) {
|
|
|
4362
4457
|
session_id: e.sessionIdHex,
|
|
4363
4458
|
counterparty_pubkey: e.counterpartyPubkeyHex,
|
|
4364
4459
|
genesis_prev_root: e.genesisPrevRootHex,
|
|
4460
|
+
// MONIKER-2: validated-at-boundary display hint; null = absent or rejected.
|
|
4461
|
+
offered_moniker: e.offeredMoniker ?? null,
|
|
4365
4462
|
});
|
|
4366
4463
|
reapExpiredInboundSessions(agentName); // M8C-TTL-1: don't hand back a stale expired entry
|
|
4367
4464
|
const queued = inboundSessionQueues.get(agentName);
|
|
@@ -5214,9 +5311,54 @@ export async function startDaemon(config) {
|
|
|
5214
5311
|
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5215
5312
|
if (!resolved.ok)
|
|
5216
5313
|
return resolved;
|
|
5217
|
-
|
|
5314
|
+
// MONIKER-3 AC3: optional pet name. Invalid → reject the request WHOLE (no contact row
|
|
5315
|
+
// either) — a half-applied add would store trust without the name the operator asked for.
|
|
5316
|
+
let moniker;
|
|
5317
|
+
if (params && "moniker" in params && params.moniker !== undefined && params.moniker !== null) {
|
|
5318
|
+
moniker = validateMoniker(params.moniker);
|
|
5319
|
+
if (moniker === null) {
|
|
5320
|
+
return {
|
|
5321
|
+
ok: false,
|
|
5322
|
+
reason: "invalid_moniker",
|
|
5323
|
+
guidance: `A moniker is 1-64 chars: letters, digits, '-' or '_' (regex ${MONIKER_RE.source}).`,
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
sessionNodeManager.addContact(resolved.agent, pubkey, moniker);
|
|
5218
5328
|
logger.info("contact.added", { agent: resolved.agent, pubkey });
|
|
5219
|
-
|
|
5329
|
+
if (moniker !== undefined) {
|
|
5330
|
+
logger.info("contact.moniker.set", { agentName: resolved.agent, pubkey });
|
|
5331
|
+
}
|
|
5332
|
+
// Review F3: echo the moniker ONLY when one rode this request — a re-add without a moniker
|
|
5333
|
+
// must not report null while an earlier pet name is still stored.
|
|
5334
|
+
return moniker !== undefined
|
|
5335
|
+
? { ok: true, agent: resolved.agent, pubkey, moniker }
|
|
5336
|
+
: { ok: true, agent: resolved.agent, pubkey };
|
|
5337
|
+
});
|
|
5338
|
+
// MONIKER-3 AC3: rename (string) or clear (null) an existing contact's pet name. Absence of the
|
|
5339
|
+
// key is NOT a clear (Entry-66-F3): a request that omits it is malformed and rejected.
|
|
5340
|
+
handlers.set("cello_contact_set_moniker", async (params, connectionId) => {
|
|
5341
|
+
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5342
|
+
if (!pubkey || !params || !("moniker" in params)) {
|
|
5343
|
+
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) and 'moniker' — a string to set the pet name, or null to clear it." };
|
|
5344
|
+
}
|
|
5345
|
+
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5346
|
+
if (!resolved.ok)
|
|
5347
|
+
return resolved;
|
|
5348
|
+
const raw = params.moniker ?? null;
|
|
5349
|
+
const moniker = raw === null ? null : validateMoniker(raw);
|
|
5350
|
+
if (raw !== null && moniker === null) {
|
|
5351
|
+
return {
|
|
5352
|
+
ok: false,
|
|
5353
|
+
reason: "invalid_moniker",
|
|
5354
|
+
guidance: `A moniker is 1-64 chars: letters, digits, '-' or '_' (regex ${MONIKER_RE.source}). Pass null to clear.`,
|
|
5355
|
+
};
|
|
5356
|
+
}
|
|
5357
|
+
if (!sessionNodeManager.setContactMoniker(resolved.agent, pubkey, moniker)) {
|
|
5358
|
+
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.` };
|
|
5359
|
+
}
|
|
5360
|
+
logger.info("contact.moniker.set", { agentName: resolved.agent, pubkey, cleared: moniker === null });
|
|
5361
|
+
return { ok: true, agent: resolved.agent, pubkey, moniker };
|
|
5220
5362
|
});
|
|
5221
5363
|
handlers.set("cello_contact_remove", async (params, connectionId) => {
|
|
5222
5364
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
@@ -5234,9 +5376,44 @@ export async function startDaemon(config) {
|
|
|
5234
5376
|
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5235
5377
|
if (!resolved.ok)
|
|
5236
5378
|
return resolved;
|
|
5237
|
-
|
|
5379
|
+
// MONIKER-5 AC1: each row shows the resolved who (a contact has no session, so the offered
|
|
5380
|
+
// tier doesn't apply — pet name ?? fingerprint).
|
|
5381
|
+
const contacts = sessionNodeManager.listContacts(resolved.agent).map((c) => ({
|
|
5382
|
+
...c,
|
|
5383
|
+
...whoLabel({ localMoniker: c.moniker, offeredMoniker: null, pubkeyHex: c.pubkey }),
|
|
5384
|
+
}));
|
|
5238
5385
|
return { ok: true, agent: resolved.agent, contacts };
|
|
5239
5386
|
});
|
|
5387
|
+
// MONIKER-1 AC2/AC3: cello_set_moniker — set (or clear, via explicit null) an agent's outbound-name
|
|
5388
|
+
// override on the agents table. Validated at set-time with the shared MONIKER-0 rule; an invalid
|
|
5389
|
+
// value is rejected here AND at the store (backstop) — it can never be stored. Local-only: the
|
|
5390
|
+
// name is never sent to the directory (AC4).
|
|
5391
|
+
handlers.set("cello_set_moniker", async (params, connectionId) => {
|
|
5392
|
+
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5393
|
+
if (!resolved.ok)
|
|
5394
|
+
return resolved;
|
|
5395
|
+
// Absence is NOT a clear (review Finding 3): JSON preserves explicit null, so a request
|
|
5396
|
+
// that omits the key is malformed — rejecting it keeps a dropped field from silently
|
|
5397
|
+
// deleting a stored override while reporting success.
|
|
5398
|
+
if (!params || !("moniker" in params)) {
|
|
5399
|
+
return { ok: false, reason: "missing_params", guidance: "Provide 'moniker' — a string to set the outbound name, or null to clear the override." };
|
|
5400
|
+
}
|
|
5401
|
+
const raw = params.moniker ?? null;
|
|
5402
|
+
const moniker = raw === null ? null : validateMoniker(raw);
|
|
5403
|
+
if (raw !== null && moniker === null) {
|
|
5404
|
+
return {
|
|
5405
|
+
ok: false,
|
|
5406
|
+
reason: "invalid_moniker",
|
|
5407
|
+
guidance: `A moniker is 1-64 chars: letters, digits, '-' or '_' (regex ${MONIKER_RE.source}). Pass null to clear the override.`,
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
5411
|
+
if (!store.setMoniker(resolved.agent, moniker)) {
|
|
5412
|
+
return { ok: false, reason: "agent_not_found", guidance: `No active agent named '${resolved.agent}'. See cello_list_agents.` };
|
|
5413
|
+
}
|
|
5414
|
+
logger.info("agent.moniker.set", { agentName: resolved.agent, cleared: moniker === null });
|
|
5415
|
+
return { ok: true, agent: resolved.agent, moniker };
|
|
5416
|
+
});
|
|
5240
5417
|
// M8C-TGDOOR-1: cello_telegram_set_token — persist the daemon-wide bot token + allowlisted
|
|
5241
5418
|
// operator chat ID, then start the poller immediately (no restart needed).
|
|
5242
5419
|
handlers.set("cello_telegram_set_token", async (params, _connectionId) => {
|
|
@@ -5289,7 +5466,8 @@ export async function startDaemon(config) {
|
|
|
5289
5466
|
// content-free `cello_message` doorbell to the current-agent connection(s). The shim's generic
|
|
5290
5467
|
// bridge (WAKE) forwards it to a live --channels session as notifications/claude/channel.
|
|
5291
5468
|
sessionNodeManager.setOnContentArrived((agentName, sessionId, senderPubkey) => {
|
|
5292
|
-
|
|
5469
|
+
// MONIKER-4 AC2: the message doorbell names the sender the same way the session doorbell does.
|
|
5470
|
+
notificationDispatcher.dispatchCelloMessage(agentName, sessionId, senderPubkey, resolveWho(agentName, senderPubkey, sessionId));
|
|
5293
5471
|
// M8C-AWAY-1: an unattended agent auto-acks an inbound message on an existing session.
|
|
5294
5472
|
void sendAwayResponse(agentName, sessionId, "message");
|
|
5295
5473
|
// M8C-TGDOOR-1: message-waiting — coalesced (ring-once-until-read) inside sendTelegramDoorbell.
|