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.
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/CHANGELOG.md +18 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/PKG-INFO +1 -1
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/mesh/agent.py +83 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/mesh/cli.py +10 -1
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/pyproject.toml +1 -1
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/.gitignore +0 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/LICENSE +0 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/README.md +0 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/docs/onboarding/task-template-non-human.md +0 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/mesh/__init__.py +0 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/tests/__init__.py +0 -0
- {meshbook_cli-0.8.0 → meshbook_cli-0.9.0}/tests/test_cli_smoke.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|