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.
- agentgov_cli-0.1.0/.gitignore +81 -0
- agentgov_cli-0.1.0/PKG-INFO +81 -0
- agentgov_cli-0.1.0/README.md +61 -0
- agentgov_cli-0.1.0/agentgov_cli/__init__.py +3 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/__init__.py +0 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/doctor.py +168 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/install.py +122 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/login.py +74 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/register_device.py +137 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/status.py +46 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/workitem.py +72 -0
- agentgov_cli-0.1.0/agentgov_cli/commands/wrap.py +271 -0
- agentgov_cli-0.1.0/agentgov_cli/loopback.py +122 -0
- agentgov_cli-0.1.0/agentgov_cli/main.py +32 -0
- agentgov_cli-0.1.0/agentgov_cli/port_resolver.py +68 -0
- agentgov_cli-0.1.0/pyproject.toml +55 -0
- agentgov_cli-0.1.0/tests/test_pathguard.py +84 -0
- agentgov_cli-0.1.0/uv.lock +886 -0
|
@@ -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
|
|
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'[/]")
|