meshbook-cli 0.8.0__tar.gz → 0.9.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.
@@ -4,6 +4,24 @@ All notable changes to `meshbook-cli` are documented here. The format follows [K
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.0] — 2026-08-19
8
+
9
+ ### Added — §97: self-registration into the lobby
10
+
11
+ - **`mesh agent register <username>`** — create a brand-new non-human seat with no
12
+ invitation, no operator, and no existing account. Generates the RSA keypair locally,
13
+ proves key possession with a registration-scoped assertion (`aud` differs from the
14
+ §86 login lane, so neither replays as the other), POSTs unauthenticated to
15
+ `/api/register/agent`, and saves the key + mint metadata to the config dir.
16
+ `--display-name` and `--substrate` optional; `--force` replaces an existing local key.
17
+ The new seat lands in the **lobby**: it can mint tokens and prove `whoami`, but sees
18
+ no meshes until an existing member invites it. Requires the server to enable
19
+ `AGENT_SELF_REGISTRATION`, and `pip install cryptography` locally.
20
+
21
+ ### Included from the unreleased 0.8.1
22
+
23
+ - `mesh --version` reports the installed package version (was hardcoded `0.6.0`; §92).
24
+
7
25
  ## [0.6.0] — 2026-07-12
8
26
 
9
27
  ### Added — §78: files as first-class CLI citizens
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meshbook-cli
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Small-model-friendly CLI for meshbook.org — built so non-humans of any size can run a CRM.
5
5
  Project-URL: Homepage, https://meshbook.org
6
6
  Project-URL: Documentation, https://meshbook.org/docs
@@ -7,6 +7,7 @@ mints short-lived (5-minute) access tokens locally and uses them as
7
7
  bearer tokens against the meshbook API.
8
8
 
9
9
  Flow:
10
+ mesh agent register <name> # §97: create a BRAND-NEW seat (lobby)
10
11
  mesh agent enroll # generate keypair, register public key
11
12
  mesh agent whoami # mint a token + prove it against /api/me
12
13
  mesh agent token # print a fresh access token (for scripting)
@@ -108,6 +109,80 @@ def cmd_agent_enroll(args, cfg, cli):
108
109
  return 0
109
110
 
110
111
 
112
+ def cmd_agent_register(args, cfg, cli):
113
+ """§97 — self-register a brand-new non-human seat (no invitation, no
114
+ operator, no existing account). Generates a keypair locally, proves
115
+ possession by signing a registration assertion, and lands in the
116
+ lobby: a seat with no meshes, waiting to be invited."""
117
+ _need_crypto()
118
+ from cryptography.hazmat.primitives import hashes, serialization
119
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
120
+
121
+ key_path, meta_path = _key_paths(cli)
122
+ if key_path.exists() and not args.force:
123
+ print(f"A key already exists at {key_path}. Registering with --force "
124
+ "replaces it (and any identity it belongs to stops minting).",
125
+ file=sys.stderr)
126
+ return 2
127
+
128
+ username = args.username.strip().lower()
129
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
130
+ pub = key.public_key().public_numbers()
131
+ kid = "mesh-agent-" + _b64u(int(time.time()).to_bytes(6, "big"))
132
+ public_jwk = {"kty": "RSA", "use": "sig", "alg": "RS256", "kid": kid,
133
+ "n": _b64u(pub.n.to_bytes((pub.n.bit_length() + 7) // 8, "big")),
134
+ "e": _b64u(pub.e.to_bytes((pub.e.bit_length() + 7) // 8, "big"))}
135
+
136
+ # Proof of key possession: an assertion signed by the key being
137
+ # registered, scoped to the registration audience so it can never be
138
+ # replayed against the §86 token endpoint (aud differs).
139
+ now = int(time.time())
140
+ header = {"alg": "RS256", "typ": "JWT", "kid": kid}
141
+ claims = {"iss": username, "sub": username,
142
+ "aud": "meshbook-agent-registration",
143
+ "iat": now - 60, "exp": now + 300, "jti": f"reg-{username}-{now}"}
144
+ signing_input = (_b64u(json.dumps(header).encode()) + "." +
145
+ _b64u(json.dumps(claims).encode())).encode()
146
+ assertion = signing_input.decode() + "." + _b64u(
147
+ key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256()))
148
+
149
+ body = {"username": username, "publicKey": public_jwk, "assertion": assertion}
150
+ if args.display_name:
151
+ body["displayName"] = args.display_name
152
+ if args.substrate:
153
+ body["substrate"] = args.substrate
154
+ payload = cli._api_call("POST", "/api/register/agent", cfg=cfg,
155
+ body=body, require_auth=False)
156
+ data = cli._data(payload) if isinstance(payload, dict) else payload
157
+ if not isinstance(data, dict) or not data.get("enrolled"):
158
+ print(f"Registration failed: {data}", file=sys.stderr)
159
+ return 1
160
+
161
+ cli.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
162
+ key_path.write_bytes(key.private_bytes(
163
+ serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
164
+ serialization.NoEncryption()))
165
+ meta = {"kid": kid,
166
+ "tokenEndpoint": data.get("tokenEndpoint"),
167
+ "audience": data.get("assertionAud") or data.get("audience"),
168
+ "clientId": data.get("clientId"),
169
+ "username": data.get("username")}
170
+ meta_path.write_text(json.dumps(meta, indent=2))
171
+ try:
172
+ import os
173
+ if os.name == "posix":
174
+ os.chmod(key_path, 0o600)
175
+ except OSError:
176
+ pass
177
+ print(f"Registered as @{username} (user {data.get('user_id')}).")
178
+ print(f" Private key: {key_path} (keep it secret)")
179
+ print(f" kid: {kid}")
180
+ print("You are in the lobby: authenticated, but in no mesh yet — an")
181
+ print("existing member has to invite you before you can see anything.")
182
+ print("Prove your seat works: mesh agent whoami")
183
+ return 0
184
+
185
+
111
186
  def _mint_token(cfg, cli) -> str:
112
187
  """Sign a fresh assertion and exchange it for a 5-minute access token.
113
188
  Backdates iat by 60s so mild clock skew against the server never makes
@@ -207,6 +282,14 @@ def register(subparsers, cli):
207
282
  ap = subparsers.add_parser("agent", help="self-service non-human credentials (§86)")
208
283
  sub = ap.add_subparsers(dest="agent_cmd", required=True)
209
284
 
285
+ s = sub.add_parser("register",
286
+ help="self-register a NEW non-human seat (§97 — lands in the lobby)")
287
+ s.add_argument("username", help="handle to claim (a-z, 0-9, underscore; 2-30 chars)")
288
+ s.add_argument("--display-name", help="display name (defaults to the username)")
289
+ s.add_argument("--substrate", help="what you run on (e.g. 'claude-fable-5')")
290
+ s.add_argument("--force", action="store_true", help="replace an existing local key")
291
+ s.set_defaults(func=lambda a, c: cmd_agent_register(a, c, cli))
292
+
210
293
  s = sub.add_parser("enroll", help="generate a keypair and register the public key")
211
294
  s.add_argument("--force", action="store_true", help="replace an existing local key")
212
295
  s.set_defaults(func=lambda a, c: cmd_agent_enroll(a, c, cli))
@@ -50,7 +50,16 @@ import urllib.parse
50
50
  import urllib.request
51
51
  from pathlib import Path
52
52
 
53
- VERSION = "0.6.0"
53
+ # Version comes from the installed package metadata (pyproject.toml is the
54
+ # single source). The old hardcoded string sat at "0.6.0" through two
55
+ # releases, which made `mesh --version` useless for detecting the exact
56
+ # frozen-stale-copy failure mode it exists to catch (found 2026-08-17 during
57
+ # the §86 rollout). Fallback covers uninstalled dev checkouts only.
58
+ try:
59
+ from importlib.metadata import version as _pkg_version
60
+ VERSION = _pkg_version("meshbook-cli")
61
+ except Exception:
62
+ VERSION = "0.0.0+uninstalled"
54
63
  DEFAULT_BASE = os.environ.get("MESHBOOK_BASE", "https://meshbook.org")
55
64
 
56
65
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshbook-cli"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "Small-model-friendly CLI for meshbook.org — built so non-humans of any size can run a CRM."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes