@cello-protocol/daemon 0.0.37 → 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 +180 -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 +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";
|
|
@@ -900,8 +901,27 @@ 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
|
+
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
|
+
}
|
|
903
920
|
function dispatchSessionStateChangedWithTelegram(agentName, sessionId, state, counterpartyPubkey) {
|
|
904
|
-
|
|
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);
|
|
905
925
|
void sendTelegramDoorbell(agentName, sessionId, "state_change", `Session ${state}`);
|
|
906
926
|
// Reviewer HIGH fix (a60d68ed): telegramRungUnread had NO cleanup at all — a session that
|
|
907
927
|
// rings once and is never read via cello_receive/since_seq (e.g. the operator only ever uses
|
|
@@ -911,6 +931,14 @@ export async function startDaemon(config) {
|
|
|
911
931
|
// preferable to an unbounded leak (the exact class of bug fixed for TTL-1's expired-log at
|
|
912
932
|
// af8a701 in this same milestone).
|
|
913
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
|
+
}
|
|
914
942
|
}
|
|
915
943
|
async function handleInboundTelegramUpdate(update, client) {
|
|
916
944
|
const chatId = update.message?.chat?.id !== undefined ? String(update.message.chat.id) : undefined;
|
|
@@ -1084,6 +1112,28 @@ export async function startDaemon(config) {
|
|
|
1084
1112
|
if (t === "session_assignment" || t === "session_request_error")
|
|
1085
1113
|
resolveFrame(frame);
|
|
1086
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
|
+
}
|
|
1087
1137
|
try {
|
|
1088
1138
|
const sent = await signaling.sendRaw({
|
|
1089
1139
|
type: "session_request",
|
|
@@ -1091,6 +1141,7 @@ export async function startDaemon(config) {
|
|
|
1091
1141
|
initiator_session_peer_id: sr.peerId,
|
|
1092
1142
|
initiator_session_addrs: sr.addrs,
|
|
1093
1143
|
wants_session_offer: true,
|
|
1144
|
+
...(moniker !== undefined ? { moniker } : {}),
|
|
1094
1145
|
});
|
|
1095
1146
|
if (!sent.ok) {
|
|
1096
1147
|
return { ok: false, reason: sent.reason ?? "directory_unreachable", guidance: sent.guidance ?? "Could not send session_request over the directory signaling stream." };
|
|
@@ -1744,9 +1795,9 @@ export async function startDaemon(config) {
|
|
|
1744
1795
|
// the encrypted DB (NO key file), and wire the agent into the live daemon so it can be registered
|
|
1745
1796
|
// and used WITHOUT a restart. Creation is explicit — cello_start_agent never auto-creates on a typo.
|
|
1746
1797
|
handlers.set("cello_create_agent", async (params, _connectionId) => {
|
|
1747
|
-
const name = params?.name;
|
|
1748
|
-
if (
|
|
1749
|
-
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.` };
|
|
1750
1801
|
}
|
|
1751
1802
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1752
1803
|
if (store.hasActiveAgent(name) || agents.some((a) => a.name === name)) {
|
|
@@ -1851,9 +1902,9 @@ export async function startDaemon(config) {
|
|
|
1851
1902
|
}
|
|
1852
1903
|
}
|
|
1853
1904
|
handlers.set("cello_remove_agent", async (params, _connectionId) => {
|
|
1854
|
-
const name = params?.name;
|
|
1855
|
-
if (
|
|
1856
|
-
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}).` };
|
|
1857
1908
|
}
|
|
1858
1909
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1859
1910
|
// The active row (a fresh removal) OR the most-recent retired row (a DB-001 re-push). Captured BEFORE
|
|
@@ -3381,6 +3432,8 @@ export async function startDaemon(config) {
|
|
|
3381
3432
|
sessionId: row.session_id,
|
|
3382
3433
|
agentName: row.agent_name,
|
|
3383
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),
|
|
3384
3437
|
status: row.status,
|
|
3385
3438
|
category,
|
|
3386
3439
|
messageCount,
|
|
@@ -3868,6 +3921,10 @@ export async function startDaemon(config) {
|
|
|
3868
3921
|
correlationId,
|
|
3869
3922
|
});
|
|
3870
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();
|
|
3871
3928
|
// Expired requests move HERE (from inboundSessionQueues) rather than vanishing — visible via
|
|
3872
3929
|
// cello_check_notifications so the operator can see what they missed, not just silence.
|
|
3873
3930
|
const expiredSessionRequests = new Map();
|
|
@@ -3928,6 +3985,10 @@ export async function startDaemon(config) {
|
|
|
3928
3985
|
expiredList.splice(0, expiredList.length - EXPIRED_SESSION_REQUESTS_CAP); // keep newest N
|
|
3929
3986
|
}
|
|
3930
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
|
+
}
|
|
3931
3992
|
}
|
|
3932
3993
|
else {
|
|
3933
3994
|
live.push(e);
|
|
@@ -4024,6 +4085,8 @@ export async function startDaemon(config) {
|
|
|
4024
4085
|
const relayAddrs = relayEndpoint && Array.isArray(relayEndpoint["multiaddrs"])
|
|
4025
4086
|
? relayEndpoint["multiaddrs"].filter((m) => typeof m === "string")
|
|
4026
4087
|
: [];
|
|
4088
|
+
// MONIKER-2 AC2: validate the offered name ONCE, at this wire boundary.
|
|
4089
|
+
const monikerResult = extractOfferedMoniker(a);
|
|
4027
4090
|
return {
|
|
4028
4091
|
sessionIdHex,
|
|
4029
4092
|
participantAPubkeyHex,
|
|
@@ -4038,6 +4101,9 @@ export async function startDaemon(config) {
|
|
|
4038
4101
|
signerPubkeyHex: a["signer_pubkey"] !== undefined ? frameValueToHex(a["signer_pubkey"]) : null,
|
|
4039
4102
|
relayPeerId,
|
|
4040
4103
|
relayAddrs,
|
|
4104
|
+
offeredMoniker: monikerResult.offeredMoniker,
|
|
4105
|
+
monikerRejected: monikerResult.rejected,
|
|
4106
|
+
monikerRejectReason: monikerResult.reason ?? null,
|
|
4041
4107
|
};
|
|
4042
4108
|
}
|
|
4043
4109
|
// Wait (bounded) for THIS AGENT's standing receiver to be ready. acceptSession consumes the
|
|
@@ -4142,10 +4208,28 @@ export async function startDaemon(config) {
|
|
|
4142
4208
|
sessionPeerId: result.peerId,
|
|
4143
4209
|
correlationId,
|
|
4144
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
|
+
}
|
|
4145
4228
|
enqueueInboundSession(agentName, {
|
|
4146
4229
|
sessionIdHex: parsed.sessionIdHex,
|
|
4147
4230
|
counterpartyPubkeyHex: parsed.participantAPubkeyHex,
|
|
4148
4231
|
genesisPrevRootHex,
|
|
4232
|
+
offeredMoniker: parsed.offeredMoniker,
|
|
4149
4233
|
});
|
|
4150
4234
|
dispatchSessionStateChangedWithTelegram(agentName, parsed.sessionIdHex, "created", parsed.participantAPubkeyHex);
|
|
4151
4235
|
// M8C-TGDOOR-1: session requests ALWAYS ring (DoD) — no coalescing, unlike message-waiting.
|
|
@@ -4362,6 +4446,8 @@ export async function startDaemon(config) {
|
|
|
4362
4446
|
session_id: e.sessionIdHex,
|
|
4363
4447
|
counterparty_pubkey: e.counterpartyPubkeyHex,
|
|
4364
4448
|
genesis_prev_root: e.genesisPrevRootHex,
|
|
4449
|
+
// MONIKER-2: validated-at-boundary display hint; null = absent or rejected.
|
|
4450
|
+
offered_moniker: e.offeredMoniker ?? null,
|
|
4365
4451
|
});
|
|
4366
4452
|
reapExpiredInboundSessions(agentName); // M8C-TTL-1: don't hand back a stale expired entry
|
|
4367
4453
|
const queued = inboundSessionQueues.get(agentName);
|
|
@@ -5214,9 +5300,54 @@ export async function startDaemon(config) {
|
|
|
5214
5300
|
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5215
5301
|
if (!resolved.ok)
|
|
5216
5302
|
return resolved;
|
|
5217
|
-
|
|
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);
|
|
5218
5317
|
logger.info("contact.added", { agent: resolved.agent, pubkey });
|
|
5219
|
-
|
|
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 };
|
|
5220
5351
|
});
|
|
5221
5352
|
handlers.set("cello_contact_remove", async (params, connectionId) => {
|
|
5222
5353
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
@@ -5234,9 +5365,44 @@ export async function startDaemon(config) {
|
|
|
5234
5365
|
const resolved = resolveContactAgent(perConnectionState.get(connectionId), params);
|
|
5235
5366
|
if (!resolved.ok)
|
|
5236
5367
|
return resolved;
|
|
5237
|
-
|
|
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
|
+
}));
|
|
5238
5374
|
return { ok: true, agent: resolved.agent, contacts };
|
|
5239
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
|
+
});
|
|
5240
5406
|
// M8C-TGDOOR-1: cello_telegram_set_token — persist the daemon-wide bot token + allowlisted
|
|
5241
5407
|
// operator chat ID, then start the poller immediately (no restart needed).
|
|
5242
5408
|
handlers.set("cello_telegram_set_token", async (params, _connectionId) => {
|
|
@@ -5289,7 +5455,8 @@ export async function startDaemon(config) {
|
|
|
5289
5455
|
// content-free `cello_message` doorbell to the current-agent connection(s). The shim's generic
|
|
5290
5456
|
// bridge (WAKE) forwards it to a live --channels session as notifications/claude/channel.
|
|
5291
5457
|
sessionNodeManager.setOnContentArrived((agentName, sessionId, senderPubkey) => {
|
|
5292
|
-
|
|
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));
|
|
5293
5460
|
// M8C-AWAY-1: an unattended agent auto-acks an inbound message on an existing session.
|
|
5294
5461
|
void sendAwayResponse(agentName, sessionId, "message");
|
|
5295
5462
|
// M8C-TGDOOR-1: message-waiting — coalesced (ring-once-until-read) inside sendTelegramDoorbell.
|