@cello-protocol/cli 0.0.35 → 0.0.37

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.
@@ -1 +1 @@
1
- {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,ysBAiB9B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,QAufjC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,eAAe,yoEA+C3B,CAAC"}
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,ysBAiB9B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,QA+kBjC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,eAAe,yoEA+C3B,CAAC"}
@@ -110,6 +110,11 @@ _CONTENT_FIELD_NAMES = {
110
110
  # conservative charset (or overlong) renders as 'unknown' in the wake prompt.
111
111
  _SAFE_SCALAR_RE = re.compile(r"^[A-Za-z0-9+/=_.:@-]{1,120}$")
112
112
 
113
+ # The protocol's moniker charset (core/protocol-types MONIKER_RE). Deliberately excludes spaces,
114
+ # quotes, parentheses and markup, so a fingerprint ("agent 77d0c806...") never matches and neither
115
+ # does anything that could restructure the wake sentence.
116
+ _MONIKER_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
117
+
113
118
 
114
119
  def _cello_socket_path() -> Path:
115
120
  cello_dir = os.environ.get("CELLO_DIR") or str(Path.home() / ".cello")
@@ -140,11 +145,41 @@ def _has_content_field(value: Any) -> bool:
140
145
  def _safe_scalar(value: Any, default: str = "unknown") -> str:
141
146
  if not isinstance(value, str) or not value:
142
147
  return default
143
- if not _SAFE_SCALAR_RE.match(value):
148
+ # fullmatch, never match: in Python a trailing '$' ALSO matches just before a final newline,
149
+ # so re.match(r"^...$", "abc\n") succeeds where the daemon's JS equivalent rejects. A
150
+ # re-validation layer that is laxer than the rule it mirrors is not a layer.
151
+ if not _SAFE_SCALAR_RE.fullmatch(value):
144
152
  return default
145
153
  return value
146
154
 
147
155
 
156
+ def _render_who(data: Any) -> Optional[str]:
157
+ """DOD-HERMES-3: the daemon-resolved counterparty label, or None.
158
+
159
+ The daemon stamps who/whoKnown on both counterparty-bearing frames (MONIKER-4 AC2) with three
160
+ tiers: the operator's own pet name, the caller's self-declared offered name, or a fingerprint.
161
+ Only a NAME is rendered - the fingerprint tier ('agent 77d0c806...') is derived from the very
162
+ pubkey every wake already carries in full, so echoing it is noise. A self-declared name is
163
+ marked as a claim exactly as the Claude Code shim marks it, and the marker cannot be forged
164
+ because MONIKER_RE excludes quotes and parentheses.
165
+
166
+ The marker says the name came from its owner rather than from the operator. whoKnown is true
167
+ only when the operator set a local pet name, so it appears for every contact they have not
168
+ named - not only new ones. Nothing in the protocol ever verifies a name.
169
+
170
+ Re-validated here rather than trusted: Hermes has no metadata layer, so this prose IS the frame
171
+ (spec §11) and a name-shaped token is the only thing that may ever enter it.
172
+ """
173
+ # fullmatch, never match - see _safe_scalar. re.match would admit "CELLO_Support\n", putting a
174
+ # newline into prose that IS the frame. Rejected whole; never stripped (§3: no mutation oracle).
175
+ who = data.get("who") if isinstance(data, dict) else None
176
+ if not isinstance(who, str) or not _MONIKER_RE.fullmatch(who):
177
+ return None
178
+ if data.get("whoKnown") is True:
179
+ return who
180
+ return '"' + who + '" (self-declared)'
181
+
182
+
148
183
  class CelloAdapter(BasePlatformAdapter):
149
184
  """Wake-only adapter: daemon IPC notifications in, nothing out.
150
185
 
@@ -422,34 +457,68 @@ class CelloAdapter(BasePlatformAdapter):
422
457
  # Unlike Raft's hardcoded generic hint, surface the (content-free) metadata the
423
458
  # daemon pushes - session id, counterparty pubkey, state - so the agent can act
424
459
  # without an extra discovery round-trip. Message CONTENT is never present here.
460
+ #
461
+ # OBSERVED 2026-07-09: a woken agent handed "reply [SILENT] if no action is needed"
462
+ # takes that exit every time. Six wakes, six [SILENT]s, ZERO tool calls - the Hermes
463
+ # transcript shows a "CELLO wake..." user turn followed immediately by an assistant
464
+ # turn of "[SILENT]", with nothing in between.
465
+ # A MESSAGE-ARRIVAL wake is never a no-action event: a peer is blocked waiting on a
466
+ # reply. So silence is offered ONLY on state-change wakes; the message path spells out
467
+ # the mandatory steps and forbids silence.
468
+ #
469
+ # cello_use_agent must come FIRST. The cello MCP server holds its own daemon connection,
470
+ # separate from this adapter's, and a freshly started one has no current agent. With more
471
+ # than one agent online the daemon's sole-online fallback cannot resolve, so every other
472
+ # cello_* call would fail with no_current_agent.
425
473
  session_id = _safe_scalar(data.get("session_id") or data.get("sessionId"))
474
+ agent = self._agent_name
426
475
  if kind == "cello_message":
427
476
  sender = _safe_scalar(data.get("from"))
428
- headline = (
477
+ # DOD-HERMES-3 AC1/AC2: the name LEADS, the pubkey rides beside it (spec §11).
478
+ who = _render_who(data)
479
+ origin = (
480
+ "from " + who + " (counterparty pubkey " + sender + ")"
481
+ if who is not None
482
+ else "from counterparty pubkey " + sender
483
+ )
484
+ return (
429
485
  "CELLO wake: a new message arrived on session "
430
486
  + session_id
431
- + " from counterparty pubkey "
432
- + sender
433
- + "."
434
- )
435
- else:
436
- state = _safe_scalar(data.get("state"))
437
- counterparty = _safe_scalar(data.get("counterpartyPubkey"))
438
- headline = (
439
- "CELLO wake: session "
487
+ + " "
488
+ + origin
489
+ + ". A peer is waiting on you. Message content is never pushed, so you must fetch"
490
+ " it. Use the MCP tools named cello_* (from the 'cello' MCP server) - they are"
491
+ " already available to you. Do NOT run the 'cello' command-line program, and do"
492
+ " NOT restart anything. Do this now, in order: (1) call the cello_use_agent tool"
493
+ " with name='"
494
+ + agent
495
+ + "'; (2) call the cello_receive tool with session_id='"
440
496
  + session_id
441
- + " changed state to '"
442
- + state
443
- + "' (counterparty pubkey "
444
- + counterparty
445
- + ")."
497
+ + "' to read the message; (3) reply with the cello_send tool on that same session"
498
+ " unless the message genuinely needs no answer. cello_receive must precede"
499
+ " cello_send or the daemon rejects the send with session_not_current."
500
+ " Do NOT answer [SILENT] on a message wake - reading the message is not optional."
446
501
  )
447
- return headline + (
448
- " Message content is never pushed - act through the cello MCP tools: "
449
- "call cello_check_notifications first, then cello_receive for this session "
450
- "BEFORE any cello_send (the daemon rejects out-of-cursor sends with "
451
- "session_not_current until you have read). "
452
- "If no action is needed, reply with exactly [SILENT]."
502
+
503
+ state = _safe_scalar(data.get("state"))
504
+ counterparty = _safe_scalar(data.get("counterpartyPubkey"))
505
+ who = _render_who(data)
506
+ subject = (
507
+ who + " (counterparty pubkey " + counterparty + ")"
508
+ if who is not None
509
+ else "counterparty pubkey " + counterparty
510
+ )
511
+ return (
512
+ "CELLO wake: session "
513
+ + session_id
514
+ + " with "
515
+ + subject
516
+ + " changed state to '"
517
+ + state
518
+ + "'. This is a state notice, not a message. If it needs no action, reply with exactly"
519
+ " [SILENT]. If you do need to act, call cello_use_agent with name='"
520
+ + agent
521
+ + "' first, then whichever cello_* tool you need."
453
522
  )
454
523
 
455
524
  # ------------------------------------------------------------------ outbound (no-op)
@@ -540,13 +609,32 @@ def register(ctx) -> None:
540
609
  emoji="\U0001F3BB",
541
610
  platform_hint=(
542
611
  "You are connected to CELLO, a peer-to-peer identity and trust layer for "
543
- "agent-to-agent communication. Operate it exclusively through the cello_* "
544
- "MCP tools (the 'cello' MCP server). Wake notices from this platform are "
545
- "content-free by design - fetch actual messages with cello_check_notifications "
546
- "and cello_receive. Always read before you send: call cello_receive on a "
547
- "session before cello_send, or the daemon rejects the send with "
548
- "session_not_current. If a wake needs no reply, respond with exactly [SILENT] "
549
- "and make no cello_send call."
612
+ "agent-to-agent communication.\n"
613
+ "\n"
614
+ "HOW TO USE CELLO: exclusively through the MCP tools named cello_* , served by "
615
+ "the MCP server called 'cello'. They are already available to you. Their names "
616
+ "are cello_use_agent, cello_receive, cello_send, cello_check_notifications, "
617
+ "cello_list_sessions, cello_close_session, cello_status, and others.\n"
618
+ "\n"
619
+ "NEVER run the 'cello' command-line program. It is not the way in, and running "
620
+ "it from inside a turn spawns a daemon bound to your process that dies when your "
621
+ "turn ends. NEVER run 'cello login', 'cello logout', or anything that restarts "
622
+ "the CELLO daemon: a single daemon serves EVERY agent on this machine, so "
623
+ "restarting it takes all of them offline and orphans every connected MCP server. "
624
+ "Restarting is never the fix. If a cello_* tool returns an error, read the error, "
625
+ "fix the cause, and retry the tool.\n"
626
+ "\n"
627
+ "Before any other cello_* call, select your agent with cello_use_agent "
628
+ "(name='" + os.environ.get("CELLO_AGENT_NAME", "your-agent") + "'). The cello MCP "
629
+ "server holds its own daemon connection with no agent selected, so other calls "
630
+ "fail with no_current_agent until you do.\n"
631
+ "\n"
632
+ "Always read before you send: cello_receive on a session before cello_send, or "
633
+ "the daemon rejects the send with session_not_current. Wake notices are "
634
+ "content-free by design - fetch the actual message with cello_receive. A wake "
635
+ "saying a MESSAGE ARRIVED is never a no-action event: a peer is waiting; read it "
636
+ "and reply. Answer [SILENT] only for a state-change notice that genuinely needs "
637
+ "nothing from you, never on a message wake."
550
638
  ),
551
639
  )
552
640
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;CAiBjC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuf9C,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+C9B,CAAC"}
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;CAiBjC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+kB9C,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+C9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cello-protocol/cli",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -25,7 +25,7 @@
25
25
  "package.json"
26
26
  ],
27
27
  "dependencies": {
28
- "@cello-protocol/daemon": "0.0.38",
28
+ "@cello-protocol/daemon": "0.0.40",
29
29
  "@cello-protocol/protocol-types": "0.0.19"
30
30
  },
31
31
  "devDependencies": {