@cello-protocol/cli 0.0.35 → 0.0.36

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,QA2kBjC,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,37 @@ 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 unverified 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. An unverified name is marked
163
+ as a claim exactly as the Claude Code shim marks it, and the marker cannot be forged because
164
+ MONIKER_RE excludes quotes and parentheses.
165
+
166
+ Re-validated here rather than trusted: Hermes has no metadata layer, so this prose IS the frame
167
+ (spec §11) and a name-shaped token is the only thing that may ever enter it.
168
+ """
169
+ # fullmatch, never match - see _safe_scalar. re.match would admit "CELLO_Support\n", putting a
170
+ # newline into prose that IS the frame. Rejected whole; never stripped (§3: no mutation oracle).
171
+ who = data.get("who") if isinstance(data, dict) else None
172
+ if not isinstance(who, str) or not _MONIKER_RE.fullmatch(who):
173
+ return None
174
+ if data.get("whoKnown") is True:
175
+ return who
176
+ return '"' + who + '" (unverified)'
177
+
178
+
148
179
  class CelloAdapter(BasePlatformAdapter):
149
180
  """Wake-only adapter: daemon IPC notifications in, nothing out.
150
181
 
@@ -422,34 +453,68 @@ class CelloAdapter(BasePlatformAdapter):
422
453
  # Unlike Raft's hardcoded generic hint, surface the (content-free) metadata the
423
454
  # daemon pushes - session id, counterparty pubkey, state - so the agent can act
424
455
  # without an extra discovery round-trip. Message CONTENT is never present here.
456
+ #
457
+ # OBSERVED 2026-07-09: a woken agent handed "reply [SILENT] if no action is needed"
458
+ # takes that exit every time. Six wakes, six [SILENT]s, ZERO tool calls - the Hermes
459
+ # transcript shows a "CELLO wake..." user turn followed immediately by an assistant
460
+ # turn of "[SILENT]", with nothing in between.
461
+ # A MESSAGE-ARRIVAL wake is never a no-action event: a peer is blocked waiting on a
462
+ # reply. So silence is offered ONLY on state-change wakes; the message path spells out
463
+ # the mandatory steps and forbids silence.
464
+ #
465
+ # cello_use_agent must come FIRST. The cello MCP server holds its own daemon connection,
466
+ # separate from this adapter's, and a freshly started one has no current agent. With more
467
+ # than one agent online the daemon's sole-online fallback cannot resolve, so every other
468
+ # cello_* call would fail with no_current_agent.
425
469
  session_id = _safe_scalar(data.get("session_id") or data.get("sessionId"))
470
+ agent = self._agent_name
426
471
  if kind == "cello_message":
427
472
  sender = _safe_scalar(data.get("from"))
428
- headline = (
473
+ # DOD-HERMES-3 AC1/AC2: the name LEADS, the pubkey rides beside it (spec §11).
474
+ who = _render_who(data)
475
+ origin = (
476
+ "from " + who + " (counterparty pubkey " + sender + ")"
477
+ if who is not None
478
+ else "from counterparty pubkey " + sender
479
+ )
480
+ return (
429
481
  "CELLO wake: a new message arrived on session "
430
482
  + 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 "
483
+ + " "
484
+ + origin
485
+ + ". A peer is waiting on you. Message content is never pushed, so you must fetch"
486
+ " it. Use the MCP tools named cello_* (from the 'cello' MCP server) - they are"
487
+ " already available to you. Do NOT run the 'cello' command-line program, and do"
488
+ " NOT restart anything. Do this now, in order: (1) call the cello_use_agent tool"
489
+ " with name='"
490
+ + agent
491
+ + "'; (2) call the cello_receive tool with session_id='"
440
492
  + session_id
441
- + " changed state to '"
442
- + state
443
- + "' (counterparty pubkey "
444
- + counterparty
445
- + ")."
493
+ + "' to read the message; (3) reply with the cello_send tool on that same session"
494
+ " unless the message genuinely needs no answer. cello_receive must precede"
495
+ " cello_send or the daemon rejects the send with session_not_current."
496
+ " Do NOT answer [SILENT] on a message wake - reading the message is not optional."
446
497
  )
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]."
498
+
499
+ state = _safe_scalar(data.get("state"))
500
+ counterparty = _safe_scalar(data.get("counterpartyPubkey"))
501
+ who = _render_who(data)
502
+ subject = (
503
+ who + " (counterparty pubkey " + counterparty + ")"
504
+ if who is not None
505
+ else "counterparty pubkey " + counterparty
506
+ )
507
+ return (
508
+ "CELLO wake: session "
509
+ + session_id
510
+ + " with "
511
+ + subject
512
+ + " changed state to '"
513
+ + state
514
+ + "'. This is a state notice, not a message. If it needs no action, reply with exactly"
515
+ " [SILENT]. If you do need to act, call cello_use_agent with name='"
516
+ + agent
517
+ + "' first, then whichever cello_* tool you need."
453
518
  )
454
519
 
455
520
  # ------------------------------------------------------------------ outbound (no-op)
@@ -540,13 +605,32 @@ def register(ctx) -> None:
540
605
  emoji="\U0001F3BB",
541
606
  platform_hint=(
542
607
  "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."
608
+ "agent-to-agent communication.\n"
609
+ "\n"
610
+ "HOW TO USE CELLO: exclusively through the MCP tools named cello_* , served by "
611
+ "the MCP server called 'cello'. They are already available to you. Their names "
612
+ "are cello_use_agent, cello_receive, cello_send, cello_check_notifications, "
613
+ "cello_list_sessions, cello_close_session, cello_status, and others.\n"
614
+ "\n"
615
+ "NEVER run the 'cello' command-line program. It is not the way in, and running "
616
+ "it from inside a turn spawns a daemon bound to your process that dies when your "
617
+ "turn ends. NEVER run 'cello login', 'cello logout', or anything that restarts "
618
+ "the CELLO daemon: a single daemon serves EVERY agent on this machine, so "
619
+ "restarting it takes all of them offline and orphans every connected MCP server. "
620
+ "Restarting is never the fix. If a cello_* tool returns an error, read the error, "
621
+ "fix the cause, and retry the tool.\n"
622
+ "\n"
623
+ "Before any other cello_* call, select your agent with cello_use_agent "
624
+ "(name='" + os.environ.get("CELLO_AGENT_NAME", "your-agent") + "'). The cello MCP "
625
+ "server holds its own daemon connection with no agent selected, so other calls "
626
+ "fail with no_current_agent until you do.\n"
627
+ "\n"
628
+ "Always read before you send: cello_receive on a session before cello_send, or "
629
+ "the daemon rejects the send with session_not_current. Wake notices are "
630
+ "content-free by design - fetch the actual message with cello_receive. A wake "
631
+ "saying a MESSAGE ARRIVED is never a no-action event: a peer is waiting; read it "
632
+ "and reply. Answer [SILENT] only for a state-change notice that genuinely needs "
633
+ "nothing from you, never on a message wake."
550
634
  ),
551
635
  )
552
636
  `;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2kB9C,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.36",
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.39",
29
29
  "@cello-protocol/protocol-types": "0.0.19"
30
30
  },
31
31
  "devDependencies": {