agentgov-cli 0.1.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.
@@ -0,0 +1,81 @@
1
+ # ============================================================================
2
+ # AgentGov top-level .gitignore
3
+ # Per-subsystem gitignores live inside control-tower/, gateway/, cli/ if needed.
4
+ # ============================================================================
5
+
6
+ # ---- Secrets & environment ----
7
+ .env
8
+ .env.local
9
+ .env.*.local
10
+ .env.production
11
+ !.env.example
12
+ !.env.act.example
13
+ *.pem
14
+ *.key
15
+ !signing_keys.example/*.pub
16
+ # PyPI publish token (project-local so twine finds it via `twine upload --config-file`)
17
+ .pypirc
18
+
19
+ # ---- Python ----
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+ *.so
24
+ .Python
25
+ .venv/
26
+ venv/
27
+ env/
28
+ ENV/
29
+ build/
30
+ dist/
31
+ *.egg-info/
32
+ .eggs/
33
+ *.egg
34
+ .pytest_cache/
35
+ .mypy_cache/
36
+ .ruff_cache/
37
+ .coverage
38
+ .coverage.*
39
+ htmlcov/
40
+ coverage.xml
41
+ *.cover
42
+ .tox/
43
+
44
+ # ---- Node / Next.js ----
45
+ node_modules/
46
+ .next/
47
+ out/
48
+ .vercel/
49
+ .turbo/
50
+ *.tsbuildinfo
51
+ next-env.d.ts
52
+ .pnpm-debug.log*
53
+ npm-debug.log*
54
+ yarn-debug.log*
55
+ yarn-error.log*
56
+ .pnpm-store/
57
+
58
+ # ---- Supabase ----
59
+ supabase/.branches/
60
+ supabase/.temp/
61
+ supabase/functions/*/deno.lock
62
+
63
+ # ---- Gateway runtime state ----
64
+ # The gateway keeps SQLite caches + disk-backed event queues in these paths.
65
+ # They are ephemeral by design and must never be committed.
66
+ gateway/.runtime/
67
+ gateway/policy_cache.sqlite*
68
+ gateway/event_queue/
69
+ ~/.agentgov/
70
+
71
+ # ---- IDE / OS ----
72
+ .vscode/
73
+ .idea/
74
+ *.swp
75
+ *.swo
76
+ .DS_Store
77
+ Thumbs.db
78
+
79
+ # ---- Local overrides ----
80
+ *.local
81
+ CLAUDE.local.md
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentgov-cli
3
+ Version: 0.1.0
4
+ Summary: AgentGov CLI — wrap Claude Code, bind work items, check gateway health.
5
+ Author: AgentGov Contributors
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: httpx>=0.27.2
9
+ Requires-Dist: pydantic>=2.9.0
10
+ Requires-Dist: pynacl>=1.5.0
11
+ Requires-Dist: rich>=13.9.0
12
+ Requires-Dist: typer>=0.12.5
13
+ Requires-Dist: uuid7>=0.1.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy>=1.11.2; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
17
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
18
+ Requires-Dist: ruff>=0.6.9; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # agentgov
22
+
23
+ The developer-laptop CLI for AgentGov.
24
+
25
+ Wraps Claude Code to bind sessions to work items and enforce governance from a signed policy snapshot. Runs alongside `agentgov-gateway` (the local proxy) — this CLI handles login, device registration, session wrapping, work-item binding, and diagnostics.
26
+
27
+ Part of the AgentGov project — see [github.com/deepakahu/agentGov](https://github.com/deepakahu/agentGov) for the full monorepo.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install agentgov agentgov-gateway
33
+ ```
34
+
35
+ ## First-time setup
36
+
37
+ ```bash
38
+ # Attach this machine to your AgentGov control tower
39
+ agentgov login --tower https://your-agentgov.vercel.app
40
+
41
+ # Register this device (generates an X25519 keypair, gets a gtw_ token)
42
+ agentgov register-device --name "$(hostname)"
43
+
44
+ # Drop Claude Code hooks + statusline + slash command into ~/.claude/
45
+ agentgov install
46
+
47
+ # Start the gateway (background)
48
+ agentgov-gateway &
49
+ ```
50
+
51
+ ## Every day
52
+
53
+ ```bash
54
+ # Run Claude Code through the governed gateway
55
+ agentgov wrap claude
56
+
57
+ # Bind the current session to a work item
58
+ # (or use the /workitem slash command inside Claude Code)
59
+ agentgov workitem PROJ-1234
60
+ ```
61
+
62
+ ## Diagnose
63
+
64
+ ```bash
65
+ agentgov status # what's my current binding, tokens, quota
66
+ agentgov doctor # is everything wired correctly
67
+ ```
68
+
69
+ ## Commands
70
+
71
+ - `agentgov login` — attach this machine to a user in the control tower
72
+ - `agentgov register-device` — generate device keypair + register with tower
73
+ - `agentgov install` — drop client assets into `~/.claude/`
74
+ - `agentgov wrap <cmd>` — wrap `claude` (or another command) in a governed session
75
+ - `agentgov workitem <ref>` — bind the current session to a work item
76
+ - `agentgov status` — show current binding + totals
77
+ - `agentgov doctor` — diagnose the installation
78
+
79
+ ## License
80
+
81
+ Apache-2.0
@@ -0,0 +1,61 @@
1
+ # agentgov
2
+
3
+ The developer-laptop CLI for AgentGov.
4
+
5
+ Wraps Claude Code to bind sessions to work items and enforce governance from a signed policy snapshot. Runs alongside `agentgov-gateway` (the local proxy) — this CLI handles login, device registration, session wrapping, work-item binding, and diagnostics.
6
+
7
+ Part of the AgentGov project — see [github.com/deepakahu/agentGov](https://github.com/deepakahu/agentGov) for the full monorepo.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install agentgov agentgov-gateway
13
+ ```
14
+
15
+ ## First-time setup
16
+
17
+ ```bash
18
+ # Attach this machine to your AgentGov control tower
19
+ agentgov login --tower https://your-agentgov.vercel.app
20
+
21
+ # Register this device (generates an X25519 keypair, gets a gtw_ token)
22
+ agentgov register-device --name "$(hostname)"
23
+
24
+ # Drop Claude Code hooks + statusline + slash command into ~/.claude/
25
+ agentgov install
26
+
27
+ # Start the gateway (background)
28
+ agentgov-gateway &
29
+ ```
30
+
31
+ ## Every day
32
+
33
+ ```bash
34
+ # Run Claude Code through the governed gateway
35
+ agentgov wrap claude
36
+
37
+ # Bind the current session to a work item
38
+ # (or use the /workitem slash command inside Claude Code)
39
+ agentgov workitem PROJ-1234
40
+ ```
41
+
42
+ ## Diagnose
43
+
44
+ ```bash
45
+ agentgov status # what's my current binding, tokens, quota
46
+ agentgov doctor # is everything wired correctly
47
+ ```
48
+
49
+ ## Commands
50
+
51
+ - `agentgov login` — attach this machine to a user in the control tower
52
+ - `agentgov register-device` — generate device keypair + register with tower
53
+ - `agentgov install` — drop client assets into `~/.claude/`
54
+ - `agentgov wrap <cmd>` — wrap `claude` (or another command) in a governed session
55
+ - `agentgov workitem <ref>` — bind the current session to a work item
56
+ - `agentgov status` — show current binding + totals
57
+ - `agentgov doctor` — diagnose the installation
58
+
59
+ ## License
60
+
61
+ Apache-2.0
@@ -0,0 +1,3 @@
1
+ """AgentGov CLI. Developer-laptop tool: wrap · workitem · status · doctor."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,168 @@
1
+ """`agentgov doctor` — diagnose the installation (CLAUDE.md §13, §14).
2
+
3
+ The old §12.1 blanket rule ("gateway must NEVER run on a governed developer's
4
+ machine") has been REPLACED by the §14.4 CONDITIONAL rule:
5
+
6
+ The gateway MAY run locally provided
7
+ (a) upstream_mode = oauth_passthrough (no vault to steal)
8
+ OR
9
+ (b) every credential is user-scoped + spending-capped + device-encrypted.
10
+
11
+ So this doctor:
12
+ - Confirms local gateway is reachable (green: it IS local).
13
+ - Reports the machine fingerprint + the upstream_mode the gateway is
14
+ running in.
15
+ - Flags a WARNING (not an error) only if apikey_vault is on AND we couldn't
16
+ confirm device-encrypted delivery (i.e. an env-provided key overrides the
17
+ sealed cred — legacy §12 path on a dev machine, which we discourage).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import platform
24
+ import socket
25
+ import uuid
26
+ from pathlib import Path
27
+
28
+ import httpx
29
+ import typer
30
+ from rich.console import Console
31
+ from rich.table import Table
32
+
33
+ console = Console()
34
+
35
+
36
+ def run(
37
+ gateway_url: str = typer.Option(None, "--gateway", envvar="AGENTGOV_GATEWAY_URL"),
38
+ ) -> None:
39
+ ok = 0
40
+ warn = 0
41
+ fail = 0
42
+ table = Table("check", "result", "detail", show_lines=False)
43
+
44
+ # 1. Managed settings present (Linux only; on macOS/Windows we just show the path)
45
+ settings_path = _managed_settings_path()
46
+ if settings_path.exists():
47
+ table.add_row("managed-settings.json", "[green]OK[/]", str(settings_path))
48
+ ok += 1
49
+ else:
50
+ table.add_row("managed-settings.json", "[yellow]MISSING[/]", str(settings_path))
51
+ warn += 1
52
+
53
+ # 2. Hooks executable
54
+ hooks_dir = Path.home() / ".claude" / "agentgov-hooks"
55
+ if not hooks_dir.exists():
56
+ table.add_row("hooks installed", "[red]MISSING[/]", str(hooks_dir))
57
+ fail += 1
58
+ else:
59
+ problems = [str(h) for h in hooks_dir.glob("*.py") if not h.stat().st_mode & 0o100]
60
+ if problems:
61
+ table.add_row("hooks executable", "[red]FAIL[/]", ", ".join(problems))
62
+ fail += 1
63
+ else:
64
+ table.add_row("hooks executable", "[green]OK[/]", str(hooks_dir))
65
+ ok += 1
66
+
67
+ # 3. Statusline installed
68
+ sl = Path.home() / ".claude" / "agentgov-statusline" / "agentgov_statusline.sh"
69
+ if sl.exists():
70
+ table.add_row("statusline installed", "[green]OK[/]", str(sl))
71
+ ok += 1
72
+ else:
73
+ table.add_row("statusline installed", "[yellow]MISSING[/]", str(sl))
74
+ warn += 1
75
+
76
+ # 4. Identity + device registration (§13/§14)
77
+ identity = Path.home() / ".agentgov" / "identity.json"
78
+ creds = Path.home() / ".agentgov" / "gateway_creds.json"
79
+ if identity.exists():
80
+ table.add_row("identity", "[green]OK[/]", str(identity))
81
+ ok += 1
82
+ else:
83
+ table.add_row("identity", "[yellow]MISSING[/]", "run `agentgov login`")
84
+ warn += 1
85
+ if creds.exists():
86
+ table.add_row("device registered", "[green]OK[/]", str(creds))
87
+ ok += 1
88
+ else:
89
+ table.add_row(
90
+ "device registered", "[yellow]MISSING[/]", "run `agentgov register-device --name ...`"
91
+ )
92
+ warn += 1
93
+
94
+ # 5. Gateway reachable
95
+ url = gateway_url or "http://localhost:8000"
96
+ upstream_mode = None
97
+ try:
98
+ r = httpx.get(f"{url}/health", timeout=3.0)
99
+ if r.status_code == 200:
100
+ table.add_row("gateway reachable", "[green]OK[/]", url)
101
+ ok += 1
102
+ # Try to read the mode from the /admin endpoint or fall back to
103
+ # env — for now we just show what env says.
104
+ import os as _os
105
+
106
+ upstream_mode = _os.environ.get("UPSTREAM_MODE", "oauth_passthrough")
107
+ else:
108
+ table.add_row("gateway reachable", "[red]FAIL[/]", f"{url} → HTTP {r.status_code}")
109
+ fail += 1
110
+ except Exception as e:
111
+ table.add_row("gateway reachable", "[red]FAIL[/]", f"{url}: {e}")
112
+ fail += 1
113
+
114
+ # 6. §14.4 CONDITIONAL rule check
115
+ fp = _machine_fingerprint()
116
+ if upstream_mode == "oauth_passthrough":
117
+ table.add_row(
118
+ "§14.4 rule",
119
+ "[green]OK[/]",
120
+ f"oauth_passthrough — no vault · fingerprint={fp[:16]}…",
121
+ )
122
+ ok += 1
123
+ elif upstream_mode == "apikey_vault":
124
+ # We can't introspect the running gateway's config remotely (yet).
125
+ # This is a soft warning until we add /admin/mode endpoint.
126
+ import os as _os
127
+
128
+ legacy_env_key = bool(_os.environ.get("ANTHROPIC_API_KEY", ""))
129
+ if legacy_env_key:
130
+ table.add_row(
131
+ "§14.4 rule",
132
+ "[yellow]WARN[/]",
133
+ (
134
+ "apikey_vault WITH env-provided ANTHROPIC_API_KEY — legacy §12 path. "
135
+ "Prefer per-device sealed credentials."
136
+ ),
137
+ )
138
+ warn += 1
139
+ else:
140
+ table.add_row(
141
+ "§14.4 rule",
142
+ "[green]OK[/]",
143
+ "apikey_vault via per-device sealed credential (recommended)",
144
+ )
145
+ ok += 1
146
+
147
+ console.print(table)
148
+ console.print(f"\n[bold]{ok} OK · {warn} WARN · {fail} FAIL[/]")
149
+ if fail:
150
+ raise typer.Exit(2)
151
+ if warn:
152
+ raise typer.Exit(1)
153
+
154
+
155
+ def _managed_settings_path() -> Path:
156
+ sysname = platform.system()
157
+ if sysname == "Darwin":
158
+ return Path("/Library/Application Support/ClaudeCode/managed-settings.json")
159
+ if sysname == "Windows":
160
+ return Path(r"C:\ProgramData\ClaudeCode\managed-settings.json")
161
+ return Path("/etc/claude-code/managed-settings.json")
162
+
163
+
164
+ def _machine_fingerprint() -> str:
165
+ """SHA-256 of hostname + a stable MAC — same recipe as the gateway."""
166
+ hostname = socket.gethostname()
167
+ mac = uuid.getnode()
168
+ return hashlib.sha256(f"{hostname}:{mac:012x}".encode()).hexdigest()
@@ -0,0 +1,122 @@
1
+ """`agentgov install` — copies client assets to ~/.claude/, prints managed-settings.json.
2
+
3
+ The managed-settings.json is printed to stdout (with the admin-visible path per OS)
4
+ so an administrator can drop it into the OS-managed path. That path is ROOT-owned;
5
+ we do NOT try to write it — the CLI runs as the developer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import platform
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from rich.console import Console
18
+
19
+ console = Console()
20
+
21
+ # Asset source is discovered relative to the installed package (client_assets is
22
+ # a sibling directory of the cli/). In wheel installs we ship it via
23
+ # package data; in local dev we look up-tree.
24
+ ASSET_ROOTS = [
25
+ Path(__file__).resolve().parent.parent.parent.parent / "client_assets",
26
+ Path(sys.prefix) / "share" / "agentgov" / "client_assets",
27
+ ]
28
+
29
+
30
+ def run(
31
+ gateway_url: str = "http://localhost:8000",
32
+ force: bool = False,
33
+ ) -> None:
34
+ """Install hooks + statusline + slash commands into ~/.claude/."""
35
+ src = _find_assets()
36
+ dest = Path.home() / ".claude"
37
+ dest.mkdir(exist_ok=True)
38
+
39
+ hooks_src = src / "hooks"
40
+ hooks_dest = dest / "agentgov-hooks"
41
+ if hooks_dest.exists() and not force:
42
+ console.print(f"[yellow]Skipping[/] hooks — {hooks_dest} exists (use --force)")
43
+ else:
44
+ shutil.copytree(hooks_src, hooks_dest, dirs_exist_ok=True)
45
+ for h in hooks_dest.glob("*.py"):
46
+ h.chmod(0o755)
47
+ console.print(f"[green]Copied[/] hooks → {hooks_dest}")
48
+
49
+ statusline_src = src / "statusline"
50
+ statusline_dest = dest / "agentgov-statusline"
51
+ if statusline_dest.exists() and not force:
52
+ console.print(f"[yellow]Skipping[/] statusline — {statusline_dest} exists")
53
+ else:
54
+ shutil.copytree(statusline_src, statusline_dest, dirs_exist_ok=True)
55
+ for h in statusline_dest.glob("*.sh"):
56
+ h.chmod(0o755)
57
+ console.print(f"[green]Copied[/] statusline → {statusline_dest}")
58
+
59
+ commands_dest = dest / "commands"
60
+ commands_dest.mkdir(exist_ok=True)
61
+ shutil.copy2(src / "commands" / "workitem.md", commands_dest / "workitem.md")
62
+ console.print(f"[green]Copied[/] /workitem slash command → {commands_dest / 'workitem.md'}")
63
+
64
+ settings = _build_managed_settings(gateway_url, hooks_dest, statusline_dest)
65
+ console.rule("[bold]managed-settings.json[/]")
66
+ console.print(f"Ask your admin to save this file at:\n {_managed_settings_path()}\n")
67
+ console.print(json.dumps(settings, indent=2))
68
+
69
+
70
+ def _find_assets() -> Path:
71
+ for root in ASSET_ROOTS:
72
+ if root.exists():
73
+ return root
74
+ raise FileNotFoundError(f"client_assets/ not found in any of: {ASSET_ROOTS}")
75
+
76
+
77
+ def _build_managed_settings(
78
+ gateway_url: str, hooks_dest: Path, statusline_dest: Path
79
+ ) -> dict[str, object]:
80
+ return {
81
+ "env": {
82
+ "ANTHROPIC_BASE_URL": gateway_url,
83
+ "AGENTGOV_LOOPBACK_PORT": os.environ.get("AGENTGOV_LOOPBACK_PORT", "8788"),
84
+ },
85
+ # apiKeyHelper disabled — Claude Code must use the agk_ key from the wrapper env.
86
+ "apiKeyHelper": "",
87
+ "hooks": {
88
+ "PreToolUse": [
89
+ {
90
+ "matcher": "Read|Write|Edit|Glob|Grep|Bash|NotebookEdit",
91
+ "hooks": [
92
+ {"type": "command", "command": str(hooks_dest / "pretooluse_pathguard.py")}
93
+ ],
94
+ }
95
+ ],
96
+ "SessionStart": [
97
+ {
98
+ "hooks": [
99
+ {"type": "command", "command": str(hooks_dest / "sessionstart_register.py")}
100
+ ]
101
+ }
102
+ ],
103
+ },
104
+ "statusLine": {
105
+ "type": "command",
106
+ "command": str(statusline_dest / "agentgov_statusline.sh"),
107
+ },
108
+ # Permissions baseline: developers may NOT override the base URL or hooks.
109
+ "permissions": {
110
+ "allow": [],
111
+ "deny": [],
112
+ },
113
+ }
114
+
115
+
116
+ def _managed_settings_path() -> str:
117
+ sysname = platform.system()
118
+ if sysname == "Darwin":
119
+ return "/Library/Application Support/ClaudeCode/managed-settings.json"
120
+ if sysname == "Windows":
121
+ return r"C:\ProgramData\ClaudeCode\managed-settings.json"
122
+ return "/etc/claude-code/managed-settings.json"
@@ -0,0 +1,74 @@
1
+ """`agentgov login` — obtain an AgentGov session token (CLAUDE.md §13.5).
2
+
3
+ v1 implements a DEV-ONLY stub: the CLI accepts a bearer token you paste
4
+ (from the dashboard) and stores it in ~/.agentgov/identity.json (0600).
5
+ The real OAuth device-code flow lands with Auth0 in Phase 5; the file
6
+ layout + tower endpoint are already device-code-shaped so the swap is
7
+ transparent to `wrap`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+
16
+ import httpx
17
+ import typer
18
+ from rich.console import Console
19
+
20
+ console = Console()
21
+
22
+
23
+ def run(
24
+ tower_url: str = typer.Option(
25
+ None, "--tower", envvar="CONTROL_TOWER_URL", help="Control tower URL"
26
+ ),
27
+ token: str = typer.Option(
28
+ None,
29
+ "--token",
30
+ help="Paste your dev-admin token (development mode only). Prompts if omitted.",
31
+ prompt=False,
32
+ ),
33
+ ) -> None:
34
+ """Attach this machine to a user in the AgentGov control tower."""
35
+ tower = tower_url or os.environ.get("CONTROL_TOWER_URL") or "http://localhost:3000"
36
+
37
+ if not token:
38
+ console.print(
39
+ "[bold]Dev-mode login[/] — paste a dev-admin token from the dashboard, or "
40
+ "the literal string 'agentgov-dev-admin' to use the seeded ADMIN.",
41
+ )
42
+ token = typer.prompt("token", hide_input=True).strip()
43
+ else:
44
+ token = token.strip()
45
+
46
+ # Verify the token can list /api/devices (a low-risk authenticated call).
47
+ try:
48
+ r = httpx.get(
49
+ f"{tower.rstrip('/')}/api/devices",
50
+ headers={"Authorization": f"Bearer {token}"},
51
+ timeout=10.0,
52
+ )
53
+ except Exception as e:
54
+ console.print(f"[red]Cannot reach control tower at {tower}: {e}[/]")
55
+ raise typer.Exit(2) from e
56
+ if r.status_code == 401:
57
+ console.print("[red]Token rejected by control tower.[/]")
58
+ raise typer.Exit(2)
59
+ if r.status_code >= 400:
60
+ console.print(f"[red]Unexpected response from control tower: {r.status_code} {r.text}[/]")
61
+ raise typer.Exit(2)
62
+
63
+ # Persist.
64
+ identity_path = Path.home() / ".agentgov" / "identity.json"
65
+ identity_path.parent.mkdir(parents=True, exist_ok=True)
66
+ payload = {
67
+ "tower_url": tower,
68
+ "auth0_token": token,
69
+ # user_id + tenant_id populated on register-device response.
70
+ }
71
+ identity_path.write_text(json.dumps(payload, indent=2))
72
+ identity_path.chmod(0o600)
73
+ console.print(f"[green]✅ Logged in.[/] Identity saved to {identity_path}")
74
+ console.print("Next: [bold]agentgov register-device --name 'my-laptop'[/]")