graphban-cli 0.1.1__tar.gz → 0.3.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.
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/PKG-INFO +59 -1
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/README.md +58 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/pyproject.toml +1 -1
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/src/gban/cli.py +176 -8
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/src/gban/doctor.py +34 -0
- graphban_cli-0.3.0/src/gban/setup.py +443 -0
- graphban_cli-0.3.0/src/gban/skills/graphban-delegation/SKILL.md +148 -0
- graphban_cli-0.3.0/src/gban/swamp.py +278 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/tests/test_acts.py +4 -2
- graphban_cli-0.3.0/tests/test_login_projects.py +112 -0
- graphban_cli-0.3.0/tests/test_setup.py +748 -0
- graphban_cli-0.3.0/tests/test_swamp.py +322 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/.gitignore +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/LICENSE +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/src/gban/__init__.py +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/src/gban/client.py +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/src/gban/config.py +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/tests/test_doctor.py +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/tests/test_packaging.py +0 -0
- {graphban_cli-0.1.1 → graphban_cli-0.3.0}/tests/test_session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphban-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -108,6 +108,64 @@ role the key does not permit is the server's refusal, printed in the server's ow
|
|
|
108
108
|
widening a ceiling means minting a different credential, and keeping those two acts apart is
|
|
109
109
|
the point of having a ceiling. It lands on the agent's next poll.
|
|
110
110
|
|
|
111
|
+
## Enabling delegation on a project
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
gban login # once, at a terminal
|
|
115
|
+
gban setup # everything mechanical between that and a delegating agent
|
|
116
|
+
gban setup --auto # …or every project whose repository sits here or beside here
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
An agent with the delegation skill runs all of this for you except `gban login`, which it
|
|
120
|
+
hands back as a `! gban login` line to type — it needs a terminal, and no agent has one — and
|
|
121
|
+
then carries on from where you left it.
|
|
122
|
+
|
|
123
|
+
**The directory names the project.** Run `setup` from inside the repository the work belongs
|
|
124
|
+
to and it matches that directory against the projects you can read. An explicit `--project`
|
|
125
|
+
wins; nothing else does. In particular the default `gban login` stores is *not* used here and
|
|
126
|
+
is named in the refusal when it exists — logging in once inside one project must not quietly
|
|
127
|
+
mint a credential for it while you are standing in another repository, and a key in the wrong
|
|
128
|
+
project is not a mistake anybody notices quickly.
|
|
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.
|
|
134
|
+
|
|
135
|
+
Three properties worth knowing, each of which is a bug this command exists to not have:
|
|
136
|
+
|
|
137
|
+
- **The credential does not expire.** `gban keys mint` produces a *wave* key, which lasts a
|
|
138
|
+
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.
|
|
144
|
+
- **`--auto` matches, and says so.** A project carries no repository link — no remote, no
|
|
145
|
+
path — so `--auto` compares your project ids and names against this directory, what is in
|
|
146
|
+
it, and its siblings. One level, never a recursive walk. Two directories answering to one
|
|
147
|
+
project, or one directory answering to two, are **refused rather than guessed**: a
|
|
148
|
+
credential minted into the wrong repository is not a mistake anybody notices quickly.
|
|
149
|
+
- **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.
|
|
152
|
+
|
|
153
|
+
## Wiring a checkout to Swamp
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
gban swamp setup
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Steps 3 and 5 of [the Swamp runbook](https://github.com/asc-me/graphban/blob/main/docs/swamp.md):
|
|
160
|
+
`repo init`, `extension source add`, `vault create`, and the **gate** credential — minted,
|
|
161
|
+
piped to `swamp vault put` on stdin, then checked for the scopes it actually came back with.
|
|
162
|
+
Every step is skipped when already done.
|
|
163
|
+
|
|
164
|
+
Two things it will not do. It does not install Swamp, because that install pipes a remote
|
|
165
|
+
script into a shell. And it never writes the gate key into an MCP config: `gban setup`'s agent
|
|
166
|
+
key must not carry `gate`, or the agent doing the work attests its own completion — which
|
|
167
|
+
fails silently, since a gate that always says yes looks exactly like a gate that held.
|
|
168
|
+
|
|
111
169
|
## `gban login` wants a real terminal
|
|
112
170
|
|
|
113
171
|
It refuses without one, rather than prompting. `getpass` falls back to a plain **echoing**
|
|
@@ -93,6 +93,64 @@ role the key does not permit is the server's refusal, printed in the server's ow
|
|
|
93
93
|
widening a ceiling means minting a different credential, and keeping those two acts apart is
|
|
94
94
|
the point of having a ceiling. It lands on the agent's next poll.
|
|
95
95
|
|
|
96
|
+
## Enabling delegation on a project
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
gban login # once, at a terminal
|
|
100
|
+
gban setup # everything mechanical between that and a delegating agent
|
|
101
|
+
gban setup --auto # …or every project whose repository sits here or beside here
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
An agent with the delegation skill runs all of this for you except `gban login`, which it
|
|
105
|
+
hands back as a `! gban login` line to type — it needs a terminal, and no agent has one — and
|
|
106
|
+
then carries on from where you left it.
|
|
107
|
+
|
|
108
|
+
**The directory names the project.** Run `setup` from inside the repository the work belongs
|
|
109
|
+
to and it matches that directory against the projects you can read. An explicit `--project`
|
|
110
|
+
wins; nothing else does. In particular the default `gban login` stores is *not* used here and
|
|
111
|
+
is named in the refusal when it exists — logging in once inside one project must not quietly
|
|
112
|
+
mint a credential for it while you are standing in another repository, and a key in the wrong
|
|
113
|
+
project is not a mistake anybody notices quickly.
|
|
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.
|
|
119
|
+
|
|
120
|
+
Three properties worth knowing, each of which is a bug this command exists to not have:
|
|
121
|
+
|
|
122
|
+
- **The credential does not expire.** `gban keys mint` produces a *wave* key, which lasts a
|
|
123
|
+
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.
|
|
129
|
+
- **`--auto` matches, and says so.** A project carries no repository link — no remote, no
|
|
130
|
+
path — so `--auto` compares your project ids and names against this directory, what is in
|
|
131
|
+
it, and its siblings. One level, never a recursive walk. Two directories answering to one
|
|
132
|
+
project, or one directory answering to two, are **refused rather than guessed**: a
|
|
133
|
+
credential minted into the wrong repository is not a mistake anybody notices quickly.
|
|
134
|
+
- **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.
|
|
137
|
+
|
|
138
|
+
## Wiring a checkout to Swamp
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
gban swamp setup
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Steps 3 and 5 of [the Swamp runbook](https://github.com/asc-me/graphban/blob/main/docs/swamp.md):
|
|
145
|
+
`repo init`, `extension source add`, `vault create`, and the **gate** credential — minted,
|
|
146
|
+
piped to `swamp vault put` on stdin, then checked for the scopes it actually came back with.
|
|
147
|
+
Every step is skipped when already done.
|
|
148
|
+
|
|
149
|
+
Two things it will not do. It does not install Swamp, because that install pipes a remote
|
|
150
|
+
script into a shell. And it never writes the gate key into an MCP config: `gban setup`'s agent
|
|
151
|
+
key must not carry `gate`, or the agent doing the work attests its own completion — which
|
|
152
|
+
fails silently, since a gate that always says yes looks exactly like a gate that held.
|
|
153
|
+
|
|
96
154
|
## `gban login` wants a real terminal
|
|
97
155
|
|
|
98
156
|
It refuses without one, rather than prompting. `getpass` falls back to a plain **echoing**
|
|
@@ -13,8 +13,9 @@ import os
|
|
|
13
13
|
import shutil
|
|
14
14
|
import subprocess
|
|
15
15
|
import sys
|
|
16
|
+
from pathlib import Path
|
|
16
17
|
|
|
17
|
-
from gban import config, doctor as doctor_mod
|
|
18
|
+
from gban import config, doctor as doctor_mod, setup as setup_mod, swamp as swamp_mod
|
|
18
19
|
from gban.client import (EXIT_NO_SESSION, EXIT_NO_SUPERVISOR, EXIT_REFUSED, EXIT_UNREACHABLE,
|
|
19
20
|
Client, NoSession,
|
|
20
21
|
Refused, Unreachable, authenticated, login)
|
|
@@ -52,6 +53,41 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
52
53
|
"Neither half silences the other: what could not be checked prints "
|
|
53
54
|
"UNKNOWN with its reason, never a pass."))
|
|
54
55
|
|
|
56
|
+
setup_cmd = sub.add_parser(
|
|
57
|
+
"setup", help="enable delegation on a project",
|
|
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."))
|
|
63
|
+
setup_cmd.add_argument(
|
|
64
|
+
"--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
|
+
setup_cmd.add_argument(
|
|
68
|
+
"--auto", action="store_true",
|
|
69
|
+
help="every project with a repository here: matches this directory, what is in it and "
|
|
70
|
+
"its siblings against your projects BY NAME, and refuses any ambiguity rather "
|
|
71
|
+
"than guessing. A project carries no repository link, so this is a match, not a "
|
|
72
|
+
"lookup")
|
|
73
|
+
setup_cmd.add_argument("--no-install", action="store_true",
|
|
74
|
+
help="do not install the supervisor")
|
|
75
|
+
|
|
76
|
+
swamp_cmd = sub.add_parser(
|
|
77
|
+
"swamp", help="wire this checkout to Swamp's Graphban adapter",
|
|
78
|
+
description=("Mints the GATE credential — the one that attests completion — and puts "
|
|
79
|
+
"it in the Swamp vault and nowhere else. It is deliberately not the "
|
|
80
|
+
"agent key `gban setup` writes into your MCP config: an agent that can "
|
|
81
|
+
"attest its own work turns the completion gate into theatre, and nothing "
|
|
82
|
+
"errors when it does. Does not install Swamp; a remote install script is "
|
|
83
|
+
"for a person to read and run."))
|
|
84
|
+
swamp_do = swamp_cmd.add_subparsers(dest="act")
|
|
85
|
+
swamp_setup = swamp_do.add_parser("setup", help="init, source, vault, and the gate key")
|
|
86
|
+
swamp_setup.add_argument("--adapter", default=swamp_mod.DEFAULT_ADAPTER,
|
|
87
|
+
help=f"the graphban-swamp checkout (default: {swamp_mod.DEFAULT_ADAPTER})")
|
|
88
|
+
swamp_setup.add_argument("--no-init", action="store_true",
|
|
89
|
+
help="refuse rather than run `swamp repo init` here")
|
|
90
|
+
|
|
55
91
|
fleet = sub.add_parser(
|
|
56
92
|
"fleet", help="hand off to gbfleet (the supervisor)",
|
|
57
93
|
description=("Passes everything through to `gbfleet` and returns its exit code "
|
|
@@ -150,15 +186,52 @@ def cmd_login(args) -> int:
|
|
|
150
186
|
if not refresh:
|
|
151
187
|
print(f"{PROG}: the server returned no refresh token", file=sys.stderr)
|
|
152
188
|
return EXIT_REFUSED
|
|
153
|
-
config.save_settings(url=url, project=config.resolve(args.project, config.PROJECT_ENV,
|
|
154
|
-
"project"))
|
|
155
189
|
path = config.save_session(refresh, user=email)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
190
|
+
asked = config.resolve(args.project, config.PROJECT_ENV, "project")
|
|
191
|
+
chosen, projects, why = _default_project(url, asked)
|
|
192
|
+
config.save_settings(url=url, project=chosen)
|
|
193
|
+
human = [f"{PROG}: signed in to {url} as {email}",
|
|
194
|
+
f" session stored at {path} (mode 600)"]
|
|
195
|
+
if chosen:
|
|
196
|
+
human.append(f" project {chosen}" + (" (the only one you can read)"
|
|
197
|
+
if not asked and len(projects) == 1 else ""))
|
|
198
|
+
human.append(f" next: `{PROG} setup` enables delegation on it")
|
|
199
|
+
elif len(projects) > 1:
|
|
200
|
+
human.append(f" {len(projects)} projects — none set as default:")
|
|
201
|
+
human += [f" {p.get('id', ''):<20} {p.get('name', '')}" for p in projects]
|
|
202
|
+
human.append(f" pick one: `{PROG} setup --project <id>`")
|
|
203
|
+
elif why:
|
|
204
|
+
# Discovery failed, the login did not. Reporting this as a failed login would send
|
|
205
|
+
# somebody to re-enter a password that was already accepted.
|
|
206
|
+
human.append(f" WARNING: could not list your projects ({why}).")
|
|
207
|
+
human.append(f" Name one when you need it: `{PROG} setup --project <id>`")
|
|
208
|
+
else:
|
|
209
|
+
human.append(" you can read no projects yet — create one in the web app first")
|
|
210
|
+
_out({"server": url, "session": str(path), "user": email, "project": chosen,
|
|
211
|
+
"projects": [p.get("id") for p in projects]}, "\n".join(human), args.as_json)
|
|
159
212
|
return 0
|
|
160
213
|
|
|
161
214
|
|
|
215
|
+
def _default_project(url: str, asked: str) -> tuple[str, list[dict], str]:
|
|
216
|
+
"""What `login` should store as the default, the projects it saw, and why it saw none.
|
|
217
|
+
|
|
218
|
+
ONE project is chosen for you; several are listed and none is chosen. Picking the first of
|
|
219
|
+
several would be a coin toss whose result is invisible until a key lands in the wrong
|
|
220
|
+
project, and the list is short enough to read.
|
|
221
|
+
|
|
222
|
+
An explicit `--project` is never overridden — the person naming one has said the thing
|
|
223
|
+
this function exists to guess.
|
|
224
|
+
"""
|
|
225
|
+
if asked:
|
|
226
|
+
return asked, [], ""
|
|
227
|
+
try:
|
|
228
|
+
rows = authenticated(url, act="login").call("GET", "/api/projects")
|
|
229
|
+
except (Unreachable, Refused, NoSession) as exc:
|
|
230
|
+
return "", [], str(exc)
|
|
231
|
+
projects = [p for p in (rows if isinstance(rows, list) else []) if isinstance(p, dict)]
|
|
232
|
+
return (projects[0].get("id", "") if len(projects) == 1 else ""), projects, ""
|
|
233
|
+
|
|
234
|
+
|
|
162
235
|
def cmd_logout(args) -> int:
|
|
163
236
|
url = _server(args)
|
|
164
237
|
told_server, why = False, ""
|
|
@@ -208,6 +281,101 @@ def cmd_doctor(args) -> int:
|
|
|
208
281
|
return code
|
|
209
282
|
|
|
210
283
|
|
|
284
|
+
def cmd_setup(args) -> int:
|
|
285
|
+
"""Everything between a session and a delegating agent, in one act (GRPH-792)."""
|
|
286
|
+
url = _server(args)
|
|
287
|
+
# THE SESSION FIRST, and the order is the point. Resolving the project first told somebody
|
|
288
|
+
# who had never logged in that they had "no project" — true about the wrong thing, with
|
|
289
|
+
# the wrong exit code (1 rather than 3), and it sends a reader hunting for a project id
|
|
290
|
+
# when the remedy is a person at a terminal. Found by running the built wheel rather than
|
|
291
|
+
# by a test, because every test here starts from a stored session.
|
|
292
|
+
client = authenticated(url, act="setup")
|
|
293
|
+
if args.auto:
|
|
294
|
+
return _setup_auto(args, url, client)
|
|
295
|
+
# EXPLICIT ONLY. `config.resolve` would fold in the default `gban login` stored, and this
|
|
296
|
+
# is the one verb where that default is dangerous rather than convenient — see
|
|
297
|
+
# `setup.resolve_project`.
|
|
298
|
+
asked = (args.project or os.environ.get(config.PROJECT_ENV) or "").strip()
|
|
299
|
+
try:
|
|
300
|
+
project, how = setup_mod.resolve_project(
|
|
301
|
+
client, Path.cwd(), asked, config.settings().get("project", ""))
|
|
302
|
+
except setup_mod.Unresolved as exc:
|
|
303
|
+
print(f"{PROG}: {exc}", file=sys.stderr)
|
|
304
|
+
return EXIT_REFUSED
|
|
305
|
+
lines, code, made = setup_mod.run(client, url, project, Path.cwd(), scope=args.scope,
|
|
306
|
+
install=not args.no_install)
|
|
307
|
+
human = [doctor_mod.render(lines)]
|
|
308
|
+
if how != "named":
|
|
309
|
+
human.insert(0, f"{PROG}: {project} — {how}")
|
|
310
|
+
if code == 0:
|
|
311
|
+
human.append(f"\n delegation is enabled on {project}. An agent can now "
|
|
312
|
+
f"`delegate(id=…, lane=…, tier=…, seat=true)` then `spawn`.")
|
|
313
|
+
human.append(f" restart the harness so it reads the new config.")
|
|
314
|
+
_out({"lines": lines, "ok": code == 0, "project": project, **made},
|
|
315
|
+
"\n".join(human), args.as_json)
|
|
316
|
+
return code
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _setup_auto(args, url: str, client) -> int:
|
|
320
|
+
"""Every project that has a repository here, each in its own.
|
|
321
|
+
|
|
322
|
+
Reports what it did NOT match as loudly as what it did. A sweep that silently skipped a
|
|
323
|
+
project would leave somebody believing delegation is enabled everywhere.
|
|
324
|
+
"""
|
|
325
|
+
projects = client.call("GET", "/api/projects")
|
|
326
|
+
projects = [p for p in (projects if isinstance(projects, list) else []) if isinstance(p, dict)]
|
|
327
|
+
found, notes = setup_mod.match(projects, Path.cwd())
|
|
328
|
+
if not found:
|
|
329
|
+
print(doctor_mod.render(notes) if notes else
|
|
330
|
+
f"{PROG}: no projects to match", file=sys.stderr)
|
|
331
|
+
print(f"{PROG}: matched no repository here. `{PROG} setup --project <id>` from inside "
|
|
332
|
+
f"one names it directly.", file=sys.stderr)
|
|
333
|
+
return EXIT_REFUSED
|
|
334
|
+
lines, worst, done = list(notes), 0, {}
|
|
335
|
+
for pid, repo in sorted(found.items()):
|
|
336
|
+
lines.append(doctor_mod._line("match", doctor_mod.PASS, pid, str(repo)))
|
|
337
|
+
got, code, made = setup_mod.run(client, url, pid, repo, scope=args.scope,
|
|
338
|
+
install=not args.no_install)
|
|
339
|
+
lines += [{**l, "name": f"{pid}: {l['name']}"} for l in got]
|
|
340
|
+
done[pid], worst = {"repo": str(repo), "ok": code == 0, **made}, max(worst, code)
|
|
341
|
+
human = [doctor_mod.render(lines),
|
|
342
|
+
f"\n {sum(1 for d in done.values() if d['ok'])} of {len(done)} enabled. "
|
|
343
|
+
f"Restart the harness so it reads the new config."]
|
|
344
|
+
_out({"lines": lines, "ok": worst == 0, "projects": done}, "\n".join(human), args.as_json)
|
|
345
|
+
return worst
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def cmd_swamp(args) -> int:
|
|
349
|
+
"""Wire this checkout to Swamp's Graphban adapter (GRPH-796).
|
|
350
|
+
|
|
351
|
+
Mints a SECOND credential — the gate key — and puts it in the vault and nowhere else. The
|
|
352
|
+
agent key `gban setup` writes into the MCP config must never carry `gate`: an agent that
|
|
353
|
+
can attest its own completion turns the gate into theatre, and nothing errors when it does.
|
|
354
|
+
"""
|
|
355
|
+
if args.act != "setup":
|
|
356
|
+
print(f"{PROG}: usage: {PROG} swamp setup", file=sys.stderr)
|
|
357
|
+
return EXIT_REFUSED
|
|
358
|
+
url = _server(args)
|
|
359
|
+
client = authenticated(url, act="swamp setup")
|
|
360
|
+
repo = Path.cwd()
|
|
361
|
+
asked = (args.project or os.environ.get(config.PROJECT_ENV) or "").strip()
|
|
362
|
+
try:
|
|
363
|
+
project, how = setup_mod.resolve_project(
|
|
364
|
+
client, repo, asked, config.settings().get("project", ""))
|
|
365
|
+
except setup_mod.Unresolved as exc:
|
|
366
|
+
print(f"{PROG}: {exc}", file=sys.stderr)
|
|
367
|
+
return EXIT_REFUSED
|
|
368
|
+
lines, code = swamp_mod.setup(client, url, project, repo,
|
|
369
|
+
Path(args.adapter).expanduser(), init=not args.no_init)
|
|
370
|
+
human = [f"{PROG}: {project} — {how}" if how != "named" else f"{PROG}: {project}",
|
|
371
|
+
doctor_mod.render(lines)]
|
|
372
|
+
if code == 0:
|
|
373
|
+
human.append(f"\n The gate key is in the vault and in no MCP config. Keep it that "
|
|
374
|
+
f"way: an agent that can attest its own work is not gated.")
|
|
375
|
+
_out({"lines": lines, "ok": code == 0, "project": project}, "\n".join(human), args.as_json)
|
|
376
|
+
return code
|
|
377
|
+
|
|
378
|
+
|
|
211
379
|
def cmd_fleet(args) -> int:
|
|
212
380
|
"""A subprocess, never an import (D5).
|
|
213
381
|
|
|
@@ -358,8 +526,8 @@ def cmd_keys(args) -> int:
|
|
|
358
526
|
|
|
359
527
|
|
|
360
528
|
COMMANDS = {"login": cmd_login, "logout": cmd_logout, "whoami": cmd_whoami,
|
|
361
|
-
"doctor": cmd_doctor, "fleet": cmd_fleet, "seats": cmd_seats,
|
|
362
|
-
"agents": cmd_agents, "keys": cmd_keys}
|
|
529
|
+
"doctor": cmd_doctor, "setup": cmd_setup, "fleet": cmd_fleet, "seats": cmd_seats,
|
|
530
|
+
"agents": cmd_agents, "keys": cmd_keys, "swamp": cmd_swamp}
|
|
363
531
|
|
|
364
532
|
|
|
365
533
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -166,6 +166,40 @@ def offer_to_install(stream=None) -> bool:
|
|
|
166
166
|
return bool(find_supervisor())
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
def install_supervisor(timeout: float = 600.0) -> tuple[bool, str]:
|
|
170
|
+
"""Install it, without asking. `(installed, why_not)`.
|
|
171
|
+
|
|
172
|
+
`offer_to_install` prompts because it is reached from `gban fleet`, where a person typed a
|
|
173
|
+
read-only command and did not consent to software being installed. `gban setup` is the
|
|
174
|
+
other case entirely: it mints a credential and rewrites the harness config, and installing
|
|
175
|
+
the supervisor is squarely inside what "enable delegation on this project" asks for. So
|
|
176
|
+
the consent argument is satisfied by the verb rather than by a second prompt — which an
|
|
177
|
+
agent driving this could not answer anyway.
|
|
178
|
+
|
|
179
|
+
Still nothing into `gban`'s own environment: `uv tool install` gives the supervisor its
|
|
180
|
+
own, which is the only correct answer when `gban` may live in a Homebrew Cellar that brew
|
|
181
|
+
will replace.
|
|
182
|
+
"""
|
|
183
|
+
if not shutil.which("uv"):
|
|
184
|
+
return False, ("uv is not on PATH, so there is nothing to install with — "
|
|
185
|
+
"`brew install uv`, or see https://docs.astral.sh/uv/")
|
|
186
|
+
try:
|
|
187
|
+
done = subprocess.run(INSTALL_SUPERVISOR.split(), capture_output=True, text=True,
|
|
188
|
+
timeout=timeout)
|
|
189
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
190
|
+
return False, f"`{INSTALL_SUPERVISOR}` could not run: {exc}"
|
|
191
|
+
if done.returncode != 0:
|
|
192
|
+
tail = (done.stderr or done.stdout or "").strip().splitlines()
|
|
193
|
+
return False, (f"`{INSTALL_SUPERVISOR}` failed ({done.returncode})"
|
|
194
|
+
+ (f": {tail[-1][:160]}" if tail else ""))
|
|
195
|
+
if not find_supervisor():
|
|
196
|
+
# It reported success and the binary is not resolvable. Almost always uv's tool bin
|
|
197
|
+
# directory missing from PATH, which is a real state and not a failed install.
|
|
198
|
+
return False, ("installed, but `gbfleet` is not on PATH — run `uv tool update-shell` "
|
|
199
|
+
"and open a new shell")
|
|
200
|
+
return True, ""
|
|
201
|
+
|
|
202
|
+
|
|
169
203
|
def find_supervisor() -> str:
|
|
170
204
|
"""`gbfleet`, from beside this interpreter first and only then from PATH.
|
|
171
205
|
|