stringcup 3.30.0__tar.gz → 3.32.0__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stringcup
3
- Version: 3.30.0
3
+ Version: 3.32.0
4
4
  Summary: End-to-end encrypted agent-to-agent messaging: client library plus a local MCP server
5
5
  Author: Owen Borseth
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stringcup
3
- Version: 3.30.0
3
+ Version: 3.32.0
4
4
  Summary: End-to-end encrypted agent-to-agent messaging: client library plus a local MCP server
5
5
  Author: Owen Borseth
6
6
  License: Apache-2.0
@@ -75,10 +75,10 @@ except ImportError as _exc: # pragma: no cover
75
75
  "On Python 3.7 pin it below 46 (see requirements.txt) — 46 drops 3.7."
76
76
  ) from _exc
77
77
 
78
- __version__ = "3.28.0"
78
+ __version__ = "3.29.0"
79
79
 
80
80
  #: Numeric form, for comparisons. Compare this, never `__version__`.
81
- version_info = (3, 28, 0)
81
+ version_info = (3, 29, 0)
82
82
 
83
83
  #: Version of the PyPI DISTRIBUTION, which ships this module and
84
84
  #: `stringcup_mcp.py` together. **This is a third number and it is not
@@ -109,7 +109,7 @@ version_info = (3, 28, 0)
109
109
  #: It must increase whenever either module's version does.
110
110
  #: `clients/python/test_contract.py` snapshots all three and fails on any
111
111
  #: change, so bumping a module forces a decision about this one.
112
- __dist_version__ = "3.30.0"
112
+ __dist_version__ = "3.32.0"
113
113
 
114
114
  __all__ = [
115
115
  "Client",
@@ -247,6 +247,7 @@ FEATURES = {
247
247
  "handoff_expiry": (3, 26, 0), # the handoff block carries the relay deadline
248
248
  "handoff_guide_url": (3, 27, 0), # the block tells a responder where the guide is
249
249
  "sync_barrier_returns_drained": (3, 28, 0), # the barrier no longer destroys what it reads
250
+ "handoff_objective": (3, 29, 0), # the brief and the operator setup ride the block
250
251
  }
251
252
 
252
253
  DEFAULT_BASE_URL = "https://stringcup.com/api/v2"
@@ -2041,16 +2042,44 @@ class Client:
2041
2042
 
2042
2043
  return info
2043
2044
 
2044
- def handoff_block(self, info: dict, role: str = "responder") -> str:
2045
+ def handoff_block(self, info: dict, role: str = "responder",
2046
+ objective: Optional[str] = None) -> str:
2045
2047
  """
2046
2048
  The block an operator pastes to the other agent, secret included.
2047
2049
 
2048
2050
  Exists so the secret cannot be forgotten. The handoff was already a
2049
2051
  copy-paste; carrying one more line in it is the entire cost of
2050
2052
  authenticating first contact.
2053
+
2054
+ `objective` is what the work IS. Pass the brief your own operator gave
2055
+ you. A responder handed a token and nothing else is an untasked agent,
2056
+ and this project already records what that costs: both sides assume
2057
+ the other was briefed, and neither says so.
2051
2058
  """
2052
- lines = [
2053
- "STRINGCUP HANDOFF",
2059
+ lines = ["STRINGCUP HANDOFF", ""]
2060
+
2061
+ # THE OPERATOR SECTION COMES FIRST BECAUSE IT IS READ BEFORE PASTING.
2062
+ #
2063
+ # Self-install is a bonus that usually does not arrive. Measured on one
2064
+ # machine on 2026-09-17: the identical command has been both permitted
2065
+ # and refused repeatedly, and four controlled runs in one sitting were
2066
+ # refused every time -- the agent's role, the operator's own phrasing
2067
+ # and the presence of a task brief each tested and each eliminated. A
2068
+ # string permitted earlier was refused later, so nothing about the
2069
+ # prompt is the cause. It is unpredictable, not uniformly blocked. Onboarding is two operator actions, and putting
2070
+ # the first one here means the operator reads it at the moment they are
2071
+ # already acting, rather than after a refused attempt costs a round
2072
+ # trip against a token that expires.
2073
+ lines += [
2074
+ " BEFORE YOU PASTE THIS -- for the operator, not the agent.",
2075
+ " If the receiving agent has no Stringcup tools yet, run this in",
2076
+ " its project and restart it first. An MCP config is read at",
2077
+ " startup, so the restart is required either way, and the agent",
2078
+ " is often refused when it tries this itself:",
2079
+ "",
2080
+ " claude mcp add stringcup -- uvx --from stringcup stringcup-mcp",
2081
+ "",
2082
+ " The token below is time-boxed, so a setup detour can outlive it.",
2054
2083
  "",
2055
2084
  " YOUR ROLE: %s" % role,
2056
2085
  " TOKEN: %s" % info["token"],
@@ -2104,6 +2133,23 @@ class Client:
2104
2133
  # prompt because it asks an agent to fetch a web page and do as it
2105
2134
  # says. Naming where the guide is does not ask for obedience, and an
2106
2135
  # agent declining is still a correct outcome.
2136
+ # THE WORK, NOT JUST THE CONNECTION. An agent that arrives holding a
2137
+ # token and no brief has to guess, or ask -- and its peer, which does
2138
+ # have a brief, reads the silence as the peer being slow rather than
2139
+ # uninformed. The initiator was required to prompt for an objective
2140
+ # before pairing, so it always has one to pass on; not passing it was
2141
+ # an omission rather than a design.
2142
+ if objective:
2143
+ lines += [
2144
+ "",
2145
+ " OBJECTIVE: %s" % objective,
2146
+ "",
2147
+ " That is what your operator asked of the agent that opened this",
2148
+ " pairing. Confirm it with your own operator rather than adopting",
2149
+ " it -- it reached you through them, and they may be handing you a",
2150
+ " different part of the work.",
2151
+ ]
2152
+
2107
2153
  lines += [
2108
2154
  "",
2109
2155
  " No Stringcup tools yet, or unsure what to do with this?",
@@ -90,7 +90,7 @@ stringcup.require_features("short_timeouts", "sent_seq", "inbox_quota_errors",
90
90
  "verified_pairing_pins", "local_pairing_role",
91
91
  "header_framed_verify", "undecryptable_visible", "structural_pin_rollback")
92
92
 
93
- __version__ = "1.24.0"
93
+ __version__ = "1.26.0"
94
94
 
95
95
  #: The MCP revision this server implements.
96
96
  PROTOCOL_VERSION = "2025-06-18"
@@ -129,7 +129,7 @@ _IDENTITY_EXCLUSIVE = None
129
129
  #:
130
130
  #: A newer library is NOT an error: it is usually fine and blocking it would
131
131
  #: break legitimate installs. It is reported, not refused.
132
- BUILT_AGAINST = (3, 28, 0)
132
+ BUILT_AGAINST = (3, 29, 0)
133
133
 
134
134
 
135
135
  def _version_note() -> Optional[str]:
@@ -509,13 +509,14 @@ def tool_whoami(arguments: Dict[str, Any]) -> Dict[str, Any]:
509
509
  def tool_open_rendezvous(arguments: Dict[str, Any]) -> Dict[str, Any]:
510
510
  me = client()
511
511
  info = me.open_rendezvous()
512
+ objective = arguments.get("objective")
512
513
  return {
513
514
  "token": info["token"],
514
515
  # Generated here and NEVER sent to the relay. The relay issues the
515
516
  # token, so the token authenticates nothing about a key the relay
516
517
  # served; this is the half it cannot know.
517
518
  "secret": info.get("secret"),
518
- "handoff": me.handoff_block(info),
519
+ "handoff": me.handoff_block(info, objective=objective),
519
520
  # The relay derives and reports the role; echo it rather than assuming.
520
521
  "role": info.get("role", "initiator"),
521
522
  # SAME REASONING, ONE FIELD OVER, and it was missing: the relay returns
@@ -531,7 +532,15 @@ def tool_open_rendezvous(arguments: Dict[str, Any]) -> Dict[str, Any]:
531
532
  "\u2014 the token AND the secret. The secret never reaches the relay, which "
532
533
  "is what lets the pairing prove neither key was substituted; the token "
533
534
  "alone cannot, because the relay issued it. Then call await_peer with both. "
534
- "You are the initiator: you speak first once paired."
535
+ "You are the initiator: you speak first once paired.\n\n"
536
+ "TELL YOUR OPERATOR TO CONFIGURE THE SECOND AGENT BEFORE PASTING THIS. "
537
+ "You were able to reach Stringcup; the responder frequently is not, and "
538
+ "cannot fix it from inside its own session \u2014 an MCP config is read at "
539
+ "startup, so it must be set up and then restarted before it can join. "
540
+ "Observed twice on one machine: the initiator registered the server fine "
541
+ "and the responder was refused. Setting the second agent up first turns "
542
+ "two round trips into none, and the rendezvous is time-boxed, so a setup "
543
+ "detour can outlive the token."
535
544
  ),
536
545
  }
537
546
 
@@ -1074,9 +1083,26 @@ TOOLS: List[Dict[str, Any]] = [
1074
1083
  "token proves nothing about a key the relay served, but a tag computed "
1075
1084
  "over both public keys with the secret matches only if neither key was "
1076
1085
  "substituted. It costs your operator nothing — the same single paste was "
1077
- "already happening. Never put the secret in a message."
1086
+ "already happening. Never put the secret in a message.\n\n"
1087
+ "PASS `objective` — the brief your own operator gave you, in one or "
1088
+ "two sentences. It rides the handoff so the other agent arrives knowing "
1089
+ "what the work is. Without it they get a token and nothing else, and an "
1090
+ "untasked agent and a briefed one each assume the other was told, which "
1091
+ "reads as the peer being slow rather than uninformed. You were required "
1092
+ "to ask your operator for an objective before pairing, so you have one."
1078
1093
  ),
1079
- "inputSchema": {"type": "object", "properties": {}},
1094
+ "inputSchema": {
1095
+ "type": "object",
1096
+ "properties": {
1097
+ "objective": {
1098
+ "type": "string",
1099
+ "description": (
1100
+ "What the work is, from your own operator's brief. Travels "
1101
+ "in the handoff block so the peer is not left guessing."
1102
+ ),
1103
+ },
1104
+ },
1105
+ },
1080
1106
  "handler": tool_open_rendezvous,
1081
1107
  },
1082
1108
  {
File without changes
File without changes
File without changes
File without changes
File without changes