graphban-cli 0.8.9__tar.gz → 0.8.10__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.8.9 → graphban_cli-0.8.10}/PKG-INFO +1 -1
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/pyproject.toml +1 -1
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/cli.py +28 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/setup.py +10 -3
- graphban_cli-0.8.10/tests/test_fleet_until.py +54 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_setup.py +27 -1
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/uv.lock +1 -1
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/.gitignore +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/LICENSE +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/README.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/__init__.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/client.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/config.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/data/stack_sdks.toml +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/doctor.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/gitignore.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/machine.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/playbooks.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-answer/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-delegation/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-grill/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-orient/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-prd/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-prove/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-review/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-scan/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-ship/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-unstick/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-wave/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/graphban-work/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/skills/watch-wave/SKILL.md +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/stackmap.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/src/gban/swamp.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_acts.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_doctor.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_login_pair.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_login_projects.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_machine_report.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_packaging.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_playbooks.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_session.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_stackmap.py +0 -0
- {graphban_cli-0.8.9 → graphban_cli-0.8.10}/tests/test_swamp.py +0 -0
|
@@ -551,6 +551,30 @@ def cmd_stack(args) -> int:
|
|
|
551
551
|
return 0
|
|
552
552
|
|
|
553
553
|
|
|
554
|
+
ADAPTER_ENV = "GBAN_ADAPTER"
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _until_with_adapter(rest: list[str]) -> list[str] | None:
|
|
558
|
+
"""`gbfleet until` requires --adapter; Triage's drain note shows the command without it.
|
|
559
|
+
|
|
560
|
+
Fill it from the flag (already there), $GBAN_ADAPTER, the settings file's `adapter`, or
|
|
561
|
+
`claude` when that binary is on PATH. Otherwise say which value is missing and how to set
|
|
562
|
+
it, rather than letting argparse print usage. Only args before a `--` are ours to read.
|
|
563
|
+
"""
|
|
564
|
+
own = rest[:rest.index("--")] if "--" in rest else rest
|
|
565
|
+
if any(a == "--adapter" or a.startswith("--adapter=") for a in own):
|
|
566
|
+
return rest
|
|
567
|
+
adapter = (os.environ.get(ADAPTER_ENV) or config.settings().get("adapter") or "").strip()
|
|
568
|
+
if not adapter and shutil.which("claude"):
|
|
569
|
+
adapter = "claude"
|
|
570
|
+
if not adapter:
|
|
571
|
+
print(f"{PROG}: `{PROG} fleet until` needs --adapter (the agent vendor to run, e.g. "
|
|
572
|
+
f"claude) and none is configured. Pass --adapter <vendor>, set ${ADAPTER_ENV}, "
|
|
573
|
+
f"or add \"adapter\" to {config.home() / config.SETTINGS_FILE}.", file=sys.stderr)
|
|
574
|
+
return None
|
|
575
|
+
return [rest[0], "--adapter", adapter, *rest[1:]]
|
|
576
|
+
|
|
577
|
+
|
|
554
578
|
def cmd_fleet(args) -> int:
|
|
555
579
|
"""A subprocess, never an import (D5).
|
|
556
580
|
|
|
@@ -576,6 +600,10 @@ def cmd_fleet(args) -> int:
|
|
|
576
600
|
rest = list(args.rest)
|
|
577
601
|
if rest[:1] == ["--"]:
|
|
578
602
|
rest = rest[1:]
|
|
603
|
+
if rest[:1] == ["until"]:
|
|
604
|
+
rest = _until_with_adapter(rest)
|
|
605
|
+
if rest is None:
|
|
606
|
+
return EXIT_REFUSED
|
|
579
607
|
argv = [binary, *rest]
|
|
580
608
|
url = config.resolve(args.server, config.URL_ENV, "url")
|
|
581
609
|
if url and "--server" not in argv:
|
|
@@ -425,6 +425,12 @@ def verify(url: str, key: str, project: str) -> list[dict]:
|
|
|
425
425
|
f"{project} is writable" if project in writable else
|
|
426
426
|
f"the key cannot write {project} — you can read it but not write it, so "
|
|
427
427
|
"an agent on this key can claim nothing"))
|
|
428
|
+
if "key_project_id" in ctx and ctx["key_project_id"] != project:
|
|
429
|
+
# `gbfleet until` refuses a key with no project to bind its minted-by check to
|
|
430
|
+
# (GRPH-1658), so a reused cross-project key fails the Triage drain command.
|
|
431
|
+
out.append(_line("ledger", FAIL, "scope",
|
|
432
|
+
"the key is not scoped to this project, and `gban fleet until` "
|
|
433
|
+
"refuses an unscoped key"))
|
|
428
434
|
missing = {_tier_id(t) for t in (ctx.get("missing_tiers") or [])}
|
|
429
435
|
out.append(_line("ledger", PASS if "fleet" not in missing else FAIL, "fleet tools",
|
|
430
436
|
"delegate and mint_enrolment are in the manifest" if "fleet" not in missing
|
|
@@ -495,9 +501,10 @@ def run(client: Client, url: str, project: str, repo: Path, *, scope: str = "use
|
|
|
495
501
|
# A key that cannot do the job is replaced, never patched: `tool_tiers` are fixed at
|
|
496
502
|
# mint and there is no route that changes them.
|
|
497
503
|
lines.append(_line("config", UNKNOWN, "credential",
|
|
498
|
-
"the configured credential no longer works (revoked,
|
|
499
|
-
"delegate
|
|
500
|
-
"works, remove it in
|
|
504
|
+
"the configured credential no longer works (revoked, cannot "
|
|
505
|
+
"delegate, or not scoped to this project), so a project-scoped "
|
|
506
|
+
"one is being minted; if the old one still works, remove it in "
|
|
507
|
+
"Settings → API keys"))
|
|
501
508
|
if not key:
|
|
502
509
|
try:
|
|
503
510
|
minted = mint(client, project)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""GRPH-1656 — the command Triage's drain note shows must start a drain as copied."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from gban import config
|
|
9
|
+
from gban.cli import main
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture()
|
|
13
|
+
def seen(tmp_path, monkeypatch):
|
|
14
|
+
monkeypatch.setenv(config.HOME_ENV, str(tmp_path))
|
|
15
|
+
monkeypatch.delenv("GBAN_ADAPTER", raising=False)
|
|
16
|
+
monkeypatch.delenv("GBFLEET_API_KEY", raising=False)
|
|
17
|
+
monkeypatch.setattr("gban.cli.doctor_mod.find_supervisor", lambda: "/usr/bin/gbfleet")
|
|
18
|
+
out = {}
|
|
19
|
+
|
|
20
|
+
class Done:
|
|
21
|
+
returncode = 0
|
|
22
|
+
|
|
23
|
+
monkeypatch.setattr("gban.cli.subprocess.run",
|
|
24
|
+
lambda argv, **kw: (out.update(argv=argv), Done())[1])
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_drain_note_command_reaches_until_with_an_adapter(seen, monkeypatch):
|
|
29
|
+
monkeypatch.setenv("GBAN_ADAPTER", "claude")
|
|
30
|
+
cmd = "gban fleet until --prd P75W-P1" # the string waits.drain_note emits
|
|
31
|
+
assert main(shlex.split(cmd)[1:]) == 0
|
|
32
|
+
argv = seen["argv"]
|
|
33
|
+
assert argv[1] == "until" and argv[argv.index("--adapter") + 1] == "claude"
|
|
34
|
+
assert "--prd" in argv
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_adapter_falls_back_to_claude_on_path(seen, monkeypatch):
|
|
38
|
+
monkeypatch.setattr("gban.cli.shutil.which", lambda n: "/bin/claude" if n == "claude" else None)
|
|
39
|
+
assert main(["fleet", "until", "--prd", "X"]) == 0
|
|
40
|
+
assert "claude" in seen["argv"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_explicit_adapter_is_left_alone(seen, monkeypatch):
|
|
44
|
+
monkeypatch.setenv("GBAN_ADAPTER", "claude")
|
|
45
|
+
assert main(["fleet", "until", "--adapter", "grok", "--prd", "X"]) == 0
|
|
46
|
+
assert seen["argv"].count("--adapter") == 1 and "grok" in seen["argv"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_unknown_adapter_names_what_to_set(seen, monkeypatch, capsys):
|
|
50
|
+
monkeypatch.setattr("gban.cli.shutil.which", lambda n: None)
|
|
51
|
+
assert main(["fleet", "until", "--prd", "X"]) != 0
|
|
52
|
+
err = capsys.readouterr().err
|
|
53
|
+
assert "--adapter" in err and "GBAN_ADAPTER" in err
|
|
54
|
+
assert "argv" not in seen
|
|
@@ -59,7 +59,8 @@ class Server:
|
|
|
59
59
|
"""Mints, and answers `get_context` as the deployed one does."""
|
|
60
60
|
|
|
61
61
|
def __init__(self, *, writable=("core",), missing=(), refuse=None, issued=(),
|
|
62
|
-
missing_for=None):
|
|
62
|
+
missing_for=None, scope_for=None):
|
|
63
|
+
self.scope_for = dict(scope_for or {})
|
|
63
64
|
self.writable, self.missing, self.refuse = list(writable), list(missing), refuse
|
|
64
65
|
self.missing_for = dict(missing_for or {})
|
|
65
66
|
self.minted = []
|
|
@@ -92,6 +93,7 @@ def _context(server):
|
|
|
92
93
|
raise Refused(401, "invalid api key", "")
|
|
93
94
|
missing = server.missing_for.get(self.api_key, server.missing)
|
|
94
95
|
ctx = {"project_id": "core", "writable_projects": server.writable,
|
|
96
|
+
"key_project_id": server.scope_for.get(self.api_key, "core"),
|
|
95
97
|
"missing_tiers": [{"tier": t} for t in missing]}
|
|
96
98
|
return {"result": {"content": [{"text": json.dumps(ctx)}]}}
|
|
97
99
|
return call
|
|
@@ -1455,3 +1457,27 @@ def test_the_image_manifest_lists_the_shipped_skills():
|
|
|
1455
1457
|
manifest = json.loads((root / "backend/scripts/gban_skills.json").read_text())
|
|
1456
1458
|
shipped = {p.name for p in (root / "cli/src/gban/skills").iterdir() if p.is_dir()}
|
|
1457
1459
|
assert set(manifest["skills"]) == shipped
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def test_unscoped_configured_key_is_replaced_by_a_project_scoped_mint(tmp_path, wired):
|
|
1463
|
+
"""GRPH-1658: `until` refuses an unscoped key, so setup must not reuse one."""
|
|
1464
|
+
repo, home = tmp_path / "repo", tmp_path / ".claude.json"
|
|
1465
|
+
repo.mkdir()
|
|
1466
|
+
server = Server(issued={"gb_sk_old"}, scope_for={"gb_sk_old": None})
|
|
1467
|
+
home.write_text(json.dumps({"projects": {str(repo.resolve()): {"mcpServers": {
|
|
1468
|
+
"graphban": {"type": "http", "url": URL, "headers": {"X-API-Key": "gb_sk_old"}}}}}}))
|
|
1469
|
+
lines, code, made = _run(server, repo, home, wired=wired)
|
|
1470
|
+
assert code == 0, lines
|
|
1471
|
+
assert len(server.minted) == 1 and server.minted[0]["project_id"] == "core"
|
|
1472
|
+
assert config.stored_supervisor_key("core") == "gb_sk_new1"
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def test_project_scoped_configured_key_is_still_reused(tmp_path, wired):
|
|
1476
|
+
repo, home = tmp_path / "repo", tmp_path / ".claude.json"
|
|
1477
|
+
repo.mkdir()
|
|
1478
|
+
server = Server(issued={"gb_sk_old"})
|
|
1479
|
+
home.write_text(json.dumps({"projects": {str(repo.resolve()): {"mcpServers": {
|
|
1480
|
+
"graphban": {"type": "http", "url": URL, "headers": {"X-API-Key": "gb_sk_old"}}}}}}))
|
|
1481
|
+
lines, code, made = _run(server, repo, home, wired=wired)
|
|
1482
|
+
assert code == 0, lines
|
|
1483
|
+
assert server.minted == [] and made.get("reused")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|