graphban-cli 0.2.0__tar.gz → 0.4.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.5
2
2
  Name: graphban-cli
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: gban — a Graphban client for the human at a terminal
5
5
  Project-URL: Homepage, https://github.com/asc-me/graphban
6
6
  Project-URL: Repository, https://github.com/asc-me/graphban
@@ -127,28 +127,53 @@ is named in the refusal when it exists — logging in once inside one project mu
127
127
  mint a credential for it while you are standing in another repository, and a key in the wrong
128
128
  project is not a mistake anybody notices quickly.
129
129
 
130
- `setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries,
131
- installs the supervisor if it is missing, drops the delegation skill into `.claude/skills/`,
132
- and then **verifies** rather than asserting: it asks the new credential what it can actually
133
- see. Restart the harness afterwards — MCP servers are read at startup.
130
+ `setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries
131
+ into every parent harness that would actually read them, installs the supervisor if it is
132
+ missing, drops the delegation skill into `.claude/skills/`, and then **verifies** rather than
133
+ asserting: it asks the new credential what it can actually see. Restart the session that will
134
+ call `delegate`/`spawn` afterwards — MCP servers are read at startup.
134
135
 
135
136
  Three properties worth knowing, each of which is a bug this command exists to not have:
136
137
 
137
138
  - **The credential does not expire.** `gban keys mint` produces a *wave* key, which lasts a
138
139
  day (`FLEET_KEY_DAYS`); that is right for a wave and wrong for a project. Only seats expire.
139
- - **It writes where the harness will actually read.** `~/.claude.json`'s per-project
140
- `mcpServers` outranks a repository `.mcp.json`, so writing the repository file under a
141
- stale entry leaves the agent on the old key — which surfaces as a JSON parse error, because
142
- the harness is parsing a 401 body. `--scope user` is the default for that reason, and
143
- because a credential outside the repository cannot be committed.
140
+ - **It writes where the harness will actually read.** There is more than one. Claude Code
141
+ reads `~/.claude.json`'s per-project `mcpServers` (JSON), which outranks a repository
142
+ `.mcp.json` — writing the repository file under a stale entry leaves the agent on the old
143
+ key, and surfaces as a JSON parse error because the harness is parsing a 401 body. Grok
144
+ reads `~/.grok/config.toml`'s `mcp_servers` (TOML, snake_case; `mcpServers` parses and
145
+ loads nothing). Writing only Claude's file while Grok holds a different key reports
146
+ success and leaves `delegate` unadvertised (GRPH-825). Grok's `gbfleet` MCP also
147
+ gets `--workspace ~/.grok/gbfleet-wt/<repo>`: the sibling default is a path Grok's
148
+ sandbox cannot write, and spawn then dies as `git worktree add` 128 (GRPH-826).
149
+ Claude keeps the sibling default. `--scope user` is the default because a credential
150
+ outside the repository cannot be committed. Re-running a working setup still
151
+ *repairs* a missing `gbfleet` entry and a missing `--workspace`; reuse skips a mint,
152
+ never a write.
144
153
  - **`--auto` matches, and says so.** A project carries no repository link — no remote, no
145
154
  path — so `--auto` compares your project ids and names against this directory, what is in
146
155
  it, and its siblings. One level, never a recursive walk. Two directories answering to one
147
156
  project, or one directory answering to two, are **refused rather than guessed**: a
148
157
  credential minted into the wrong repository is not a mistake anybody notices quickly.
149
158
  - **It refuses to write a key into a file git tracks.** `--scope project` on a tracked
150
- `.mcp.json` is refused rather than warned about, because a warning attached to committing a
151
- credential still commits it.
159
+ `.mcp.json` or `.grok/config.toml` is refused rather than warned about, because a warning
160
+ attached to committing a credential still commits it.
161
+
162
+ ## Wiring a checkout to Swamp
163
+
164
+ ```bash
165
+ gban swamp setup
166
+ ```
167
+
168
+ Steps 3 and 5 of [the Swamp runbook](https://github.com/asc-me/graphban/blob/main/docs/swamp.md):
169
+ `repo init`, `extension source add`, `vault create`, and the **gate** credential — minted,
170
+ piped to `swamp vault put` on stdin, then checked for the scopes it actually came back with.
171
+ Every step is skipped when already done.
172
+
173
+ Two things it will not do. It does not install Swamp, because that install pipes a remote
174
+ script into a shell. And it never writes the gate key into an MCP config: `gban setup`'s agent
175
+ key must not carry `gate`, or the agent doing the work attests its own completion — which
176
+ fails silently, since a gate that always says yes looks exactly like a gate that held.
152
177
 
153
178
  ## `gban login` wants a real terminal
154
179
 
@@ -112,28 +112,53 @@ is named in the refusal when it exists — logging in once inside one project mu
112
112
  mint a credential for it while you are standing in another repository, and a key in the wrong
113
113
  project is not a mistake anybody notices quickly.
114
114
 
115
- `setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries,
116
- installs the supervisor if it is missing, drops the delegation skill into `.claude/skills/`,
117
- and then **verifies** rather than asserting: it asks the new credential what it can actually
118
- see. Restart the harness afterwards — MCP servers are read at startup.
115
+ `setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries
116
+ into every parent harness that would actually read them, installs the supervisor if it is
117
+ missing, drops the delegation skill into `.claude/skills/`, and then **verifies** rather than
118
+ asserting: it asks the new credential what it can actually see. Restart the session that will
119
+ call `delegate`/`spawn` afterwards — MCP servers are read at startup.
119
120
 
120
121
  Three properties worth knowing, each of which is a bug this command exists to not have:
121
122
 
122
123
  - **The credential does not expire.** `gban keys mint` produces a *wave* key, which lasts a
123
124
  day (`FLEET_KEY_DAYS`); that is right for a wave and wrong for a project. Only seats expire.
124
- - **It writes where the harness will actually read.** `~/.claude.json`'s per-project
125
- `mcpServers` outranks a repository `.mcp.json`, so writing the repository file under a
126
- stale entry leaves the agent on the old key — which surfaces as a JSON parse error, because
127
- the harness is parsing a 401 body. `--scope user` is the default for that reason, and
128
- because a credential outside the repository cannot be committed.
125
+ - **It writes where the harness will actually read.** There is more than one. Claude Code
126
+ reads `~/.claude.json`'s per-project `mcpServers` (JSON), which outranks a repository
127
+ `.mcp.json` — writing the repository file under a stale entry leaves the agent on the old
128
+ key, and surfaces as a JSON parse error because the harness is parsing a 401 body. Grok
129
+ reads `~/.grok/config.toml`'s `mcp_servers` (TOML, snake_case; `mcpServers` parses and
130
+ loads nothing). Writing only Claude's file while Grok holds a different key reports
131
+ success and leaves `delegate` unadvertised (GRPH-825). Grok's `gbfleet` MCP also
132
+ gets `--workspace ~/.grok/gbfleet-wt/<repo>`: the sibling default is a path Grok's
133
+ sandbox cannot write, and spawn then dies as `git worktree add` 128 (GRPH-826).
134
+ Claude keeps the sibling default. `--scope user` is the default because a credential
135
+ outside the repository cannot be committed. Re-running a working setup still
136
+ *repairs* a missing `gbfleet` entry and a missing `--workspace`; reuse skips a mint,
137
+ never a write.
129
138
  - **`--auto` matches, and says so.** A project carries no repository link — no remote, no
130
139
  path — so `--auto` compares your project ids and names against this directory, what is in
131
140
  it, and its siblings. One level, never a recursive walk. Two directories answering to one
132
141
  project, or one directory answering to two, are **refused rather than guessed**: a
133
142
  credential minted into the wrong repository is not a mistake anybody notices quickly.
134
143
  - **It refuses to write a key into a file git tracks.** `--scope project` on a tracked
135
- `.mcp.json` is refused rather than warned about, because a warning attached to committing a
136
- credential still commits it.
144
+ `.mcp.json` or `.grok/config.toml` is refused rather than warned about, because a warning
145
+ attached to committing a credential still commits it.
146
+
147
+ ## Wiring a checkout to Swamp
148
+
149
+ ```bash
150
+ gban swamp setup
151
+ ```
152
+
153
+ Steps 3 and 5 of [the Swamp runbook](https://github.com/asc-me/graphban/blob/main/docs/swamp.md):
154
+ `repo init`, `extension source add`, `vault create`, and the **gate** credential — minted,
155
+ piped to `swamp vault put` on stdin, then checked for the scopes it actually came back with.
156
+ Every step is skipped when already done.
157
+
158
+ Two things it will not do. It does not install Swamp, because that install pipes a remote
159
+ script into a shell. And it never writes the gate key into an MCP config: `gban setup`'s agent
160
+ key must not carry `gate`, or the agent doing the work attests its own completion — which
161
+ fails silently, since a gate that always says yes looks exactly like a gate that held.
137
162
 
138
163
  ## `gban login` wants a real terminal
139
164
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "graphban-cli"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "gban — a Graphban client for the human at a terminal"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -15,7 +15,7 @@ import subprocess
15
15
  import sys
16
16
  from pathlib import Path
17
17
 
18
- from gban import config, doctor as doctor_mod, setup as setup_mod
18
+ from gban import config, doctor as doctor_mod, setup as setup_mod, swamp as swamp_mod
19
19
  from gban.client import (EXIT_NO_SESSION, EXIT_NO_SUPERVISOR, EXIT_REFUSED, EXIT_UNREACHABLE,
20
20
  Client, NoSession,
21
21
  Refused, Unreachable, authenticated, login)
@@ -56,14 +56,17 @@ def _parser() -> argparse.ArgumentParser:
56
56
  setup_cmd = sub.add_parser(
57
57
  "setup", help="enable delegation on a project",
58
58
  description=("Mints a project-scoped credential that does NOT expire, writes the "
59
- "graphban and gbfleet MCP entries where the harness will actually read "
60
- "them, and installs the supervisor. Only seats expire; a credential that "
61
- "died overnight would make 'delegation is set up' quietly stop being "
62
- "true. Re-running is safe: a working configuration is left alone."))
59
+ "graphban and gbfleet MCP entries into Claude Code (~/.claude.json) and "
60
+ "Grok (~/.grok/config.toml, TOML mcp_servers) — every file a parent "
61
+ "harness will actually read — and installs the supervisor. Only seats "
62
+ "expire; a credential that died overnight would make 'delegation is set "
63
+ "up' quietly stop being true. Re-running is safe: a working key is "
64
+ "reused, a missing gbfleet entry is still repaired."))
63
65
  setup_cmd.add_argument(
64
66
  "--scope", choices=["user", "project"], default="user",
65
- help="user (default) writes ~/.claude.json, which OUTRANKS a repo .mcp.json and "
66
- "cannot be committed; project writes .mcp.json beside the repo")
67
+ help="user (default) writes ~/.claude.json and, when it exists, ~/.grok/config.toml; "
68
+ "project writes .mcp.json and .grok/config.toml beside the repo. A user file "
69
+ "outranks the repo copy and cannot be committed")
67
70
  setup_cmd.add_argument(
68
71
  "--auto", action="store_true",
69
72
  help="every project with a repository here: matches this directory, what is in it and "
@@ -73,6 +76,21 @@ def _parser() -> argparse.ArgumentParser:
73
76
  setup_cmd.add_argument("--no-install", action="store_true",
74
77
  help="do not install the supervisor")
75
78
 
79
+ swamp_cmd = sub.add_parser(
80
+ "swamp", help="wire this checkout to Swamp's Graphban adapter",
81
+ description=("Mints the GATE credential — the one that attests completion — and puts "
82
+ "it in the Swamp vault and nowhere else. It is deliberately not the "
83
+ "agent key `gban setup` writes into your MCP config: an agent that can "
84
+ "attest its own work turns the completion gate into theatre, and nothing "
85
+ "errors when it does. Does not install Swamp; a remote install script is "
86
+ "for a person to read and run."))
87
+ swamp_do = swamp_cmd.add_subparsers(dest="act")
88
+ swamp_setup = swamp_do.add_parser("setup", help="init, source, vault, and the gate key")
89
+ swamp_setup.add_argument("--adapter", default=swamp_mod.DEFAULT_ADAPTER,
90
+ help=f"the graphban-swamp checkout (default: {swamp_mod.DEFAULT_ADAPTER})")
91
+ swamp_setup.add_argument("--no-init", action="store_true",
92
+ help="refuse rather than run `swamp repo init` here")
93
+
76
94
  fleet = sub.add_parser(
77
95
  "fleet", help="hand off to gbfleet (the supervisor)",
78
96
  description=("Passes everything through to `gbfleet` and returns its exit code "
@@ -295,7 +313,8 @@ def cmd_setup(args) -> int:
295
313
  if code == 0:
296
314
  human.append(f"\n delegation is enabled on {project}. An agent can now "
297
315
  f"`delegate(id=…, lane=…, tier=…, seat=true)` then `spawn`.")
298
- human.append(f" restart the harness so it reads the new config.")
316
+ human.append(" restart Claude Code and/or Grok — whichever session will call "
317
+ "`delegate`/`spawn` — so it reads the new config.")
299
318
  _out({"lines": lines, "ok": code == 0, "project": project, **made},
300
319
  "\n".join(human), args.as_json)
301
320
  return code
@@ -325,11 +344,43 @@ def _setup_auto(args, url: str, client) -> int:
325
344
  done[pid], worst = {"repo": str(repo), "ok": code == 0, **made}, max(worst, code)
326
345
  human = [doctor_mod.render(lines),
327
346
  f"\n {sum(1 for d in done.values() if d['ok'])} of {len(done)} enabled. "
328
- f"Restart the harness so it reads the new config."]
347
+ "Restart Claude Code and/or Grok — whichever session will call "
348
+ "`delegate`/`spawn` — so it reads the new config."]
329
349
  _out({"lines": lines, "ok": worst == 0, "projects": done}, "\n".join(human), args.as_json)
330
350
  return worst
331
351
 
332
352
 
353
+ def cmd_swamp(args) -> int:
354
+ """Wire this checkout to Swamp's Graphban adapter (GRPH-796).
355
+
356
+ Mints a SECOND credential — the gate key — and puts it in the vault and nowhere else. The
357
+ agent key `gban setup` writes into the MCP config must never carry `gate`: an agent that
358
+ can attest its own completion turns the gate into theatre, and nothing errors when it does.
359
+ """
360
+ if args.act != "setup":
361
+ print(f"{PROG}: usage: {PROG} swamp setup", file=sys.stderr)
362
+ return EXIT_REFUSED
363
+ url = _server(args)
364
+ client = authenticated(url, act="swamp setup")
365
+ repo = Path.cwd()
366
+ asked = (args.project or os.environ.get(config.PROJECT_ENV) or "").strip()
367
+ try:
368
+ project, how = setup_mod.resolve_project(
369
+ client, repo, asked, config.settings().get("project", ""))
370
+ except setup_mod.Unresolved as exc:
371
+ print(f"{PROG}: {exc}", file=sys.stderr)
372
+ return EXIT_REFUSED
373
+ lines, code = swamp_mod.setup(client, url, project, repo,
374
+ Path(args.adapter).expanduser(), init=not args.no_init)
375
+ human = [f"{PROG}: {project} — {how}" if how != "named" else f"{PROG}: {project}",
376
+ doctor_mod.render(lines)]
377
+ if code == 0:
378
+ human.append(f"\n The gate key is in the vault and in no MCP config. Keep it that "
379
+ f"way: an agent that can attest its own work is not gated.")
380
+ _out({"lines": lines, "ok": code == 0, "project": project}, "\n".join(human), args.as_json)
381
+ return code
382
+
383
+
333
384
  def cmd_fleet(args) -> int:
334
385
  """A subprocess, never an import (D5).
335
386
 
@@ -481,7 +532,7 @@ def cmd_keys(args) -> int:
481
532
 
482
533
  COMMANDS = {"login": cmd_login, "logout": cmd_logout, "whoami": cmd_whoami,
483
534
  "doctor": cmd_doctor, "setup": cmd_setup, "fleet": cmd_fleet, "seats": cmd_seats,
484
- "agents": cmd_agents, "keys": cmd_keys}
535
+ "agents": cmd_agents, "keys": cmd_keys, "swamp": cmd_swamp}
485
536
 
486
537
 
487
538
  def main(argv: list[str] | None = None) -> int: