agentgov-cli 0.1.8__tar.gz → 0.1.11__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.8 → agentgov_cli-0.1.11}/PKG-INFO +2 -2
- agentgov_cli-0.1.11/agentgov_cli/asset_manifest.py +205 -0
- agentgov_cli-0.1.11/agentgov_cli/client_assets/commands/workitem.md +58 -0
- agentgov_cli-0.1.11/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +759 -0
- agentgov_cli-0.1.11/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +194 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/doctor.py +71 -2
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/install.py +137 -50
- agentgov_cli-0.1.11/agentgov_cli/commands/status.py +72 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/uninstall.py +56 -2
- agentgov_cli-0.1.11/agentgov_cli/commands/workitem.py +377 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/pyproject.toml +4 -4
- agentgov_cli-0.1.8/agentgov_cli/client_assets/commands/workitem.md +0 -18
- agentgov_cli-0.1.8/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +0 -199
- agentgov_cli-0.1.8/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +0 -74
- agentgov_cli-0.1.8/agentgov_cli/commands/status.py +0 -46
- agentgov_cli-0.1.8/agentgov_cli/commands/workitem.py +0 -72
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/.gitignore +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/README.md +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/__init__.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/client_assets/hooks/pretooluse_pathguard.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/client_assets/hooks/sessionstart_register.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/client_assets/managed-settings.json.tmpl +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/__init__.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/gateway.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/login.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/register_device.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/wrap.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/gateway_process.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/loopback.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/main.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/port_resolver.py +0 -0
- {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/hatch_build.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentgov-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: AgentGov CLI — wrap Claude Code, bind work items, check gateway health.
|
|
5
5
|
Author: AgentGov Contributors
|
|
6
6
|
License: Apache-2.0
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
8
|
Requires-Dist: httpx>=0.27.2
|
|
9
9
|
Requires-Dist: pydantic>=2.9.0
|
|
10
10
|
Requires-Dist: pynacl>=1.5.0
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Provenance for the client assets AgentGov installs into ~/.claude/.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS
|
|
4
|
+
---------------
|
|
5
|
+
Nothing in the product could tell "installed" from "current", and five separate
|
|
6
|
+
layers all needed that one fact:
|
|
7
|
+
|
|
8
|
+
* `agentgov install` skipped the hooks directory whenever it already existed,
|
|
9
|
+
so the documented upgrade (`pip install -U` + `agentgov install`) refreshed
|
|
10
|
+
the CLI and left the OLD hooks in place. Every hook fix we shipped stopped
|
|
11
|
+
at the machines that already had AgentGov.
|
|
12
|
+
* The setup page's Step 0 preflight printed `READY -> nothing to do` on the
|
|
13
|
+
strength of three hook REGISTRATIONS, never asking whether the registered
|
|
14
|
+
files were current — so it actively confirmed a broken machine as healthy.
|
|
15
|
+
* `agentgov doctor` checked presence, executability and registration. Never
|
|
16
|
+
content.
|
|
17
|
+
* `smoke-package` only ever installed into a pristine HOME, so the upgrade
|
|
18
|
+
path that every existing user takes was untested.
|
|
19
|
+
|
|
20
|
+
That combination shipped a `UserPromptSubmit` hook whose block message went
|
|
21
|
+
only to stderr — which the VS Code extension does not surface — so a governed
|
|
22
|
+
session with no work item bound erased the prompt and displayed nothing at all.
|
|
23
|
+
The fix existed in the repo for a while and could not reach anyone.
|
|
24
|
+
|
|
25
|
+
A recorded hash per installed file answers the question all five were asking.
|
|
26
|
+
|
|
27
|
+
WHY THE HASHES AND NOT JUST A VERSION STRING
|
|
28
|
+
--------------------------------------------
|
|
29
|
+
A version alone cannot separate the two reasons an installed file differs from
|
|
30
|
+
the shipped one:
|
|
31
|
+
|
|
32
|
+
STALE we wrote it, and a newer version has since shipped -> overwrite,
|
|
33
|
+
silently, because it is ours and it is out of date.
|
|
34
|
+
MODIFIED it matches neither what we shipped nor what we last wrote -> the
|
|
35
|
+
developer edited it. Back it up before overwriting.
|
|
36
|
+
|
|
37
|
+
Getting that distinction wrong in either direction is bad: silently eating a
|
|
38
|
+
local edit, or burying every upgrade in `.bak` files and scary warnings.
|
|
39
|
+
|
|
40
|
+
WHERE IT LIVES
|
|
41
|
+
--------------
|
|
42
|
+
Inside `~/.claude/agentgov-hooks/`, alongside the assets it describes, so that
|
|
43
|
+
`agentgov uninstall` (which rmtrees that directory) disposes of it without
|
|
44
|
+
needing to know it exists. The leading dot keeps it out of the `*.py` globs
|
|
45
|
+
that install and doctor use to chmod and enumerate hooks.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import hashlib
|
|
51
|
+
import json
|
|
52
|
+
import sys
|
|
53
|
+
from datetime import UTC, datetime
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
MANIFEST_NAME = ".agentgov-manifest.json"
|
|
57
|
+
|
|
58
|
+
# Asset source is discovered relative to the installed package (client_assets is
|
|
59
|
+
# a sibling of the CLI package). In wheel installs we ship it as package data;
|
|
60
|
+
# in a source checkout we look up-tree.
|
|
61
|
+
ASSET_ROOTS = [
|
|
62
|
+
# Vendored into the package at build time by cli/hatch_build.py. This is the
|
|
63
|
+
# path that works for pip/pipx installs: it is relative to __file__, so it
|
|
64
|
+
# survives relocated venvs and `pip install --user`, unlike sys.prefix.
|
|
65
|
+
Path(__file__).resolve().parent / "client_assets",
|
|
66
|
+
# Source checkout: client_assets/ sits at the repo root (CLAUDE.md §4).
|
|
67
|
+
Path(__file__).resolve().parent.parent.parent / "client_assets",
|
|
68
|
+
# Legacy shared-data location, kept so older installs keep working.
|
|
69
|
+
Path(sys.prefix) / "share" / "agentgov" / "client_assets",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Which source subdirectory lands in which directory under ~/.claude, and the
|
|
73
|
+
# glob that selects the files install is responsible for. `managed-settings.json.tmpl`
|
|
74
|
+
# sits at the source root and is deliberately absent: it is rendered into
|
|
75
|
+
# ~/.agentgov/ for an admin to place, never copied into ~/.claude.
|
|
76
|
+
_ASSET_DIRS: tuple[tuple[str, str, str], ...] = (
|
|
77
|
+
("hooks", "agentgov-hooks", "*.py"),
|
|
78
|
+
("statusline", "agentgov-statusline", "*.sh"),
|
|
79
|
+
("commands", "commands", "*.md"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def managed_assets(src: Path) -> list[tuple[str, str]]:
|
|
84
|
+
"""(source-relative path, destination dir) for everything install owns.
|
|
85
|
+
|
|
86
|
+
Enumerated from the source tree rather than hardcoded so that adding an
|
|
87
|
+
asset makes it managed automatically. A hardcoded list is precisely what
|
|
88
|
+
let the statusline drift out of date without anything noticing — it was
|
|
89
|
+
never in whatever each layer happened to believe the asset set was.
|
|
90
|
+
"""
|
|
91
|
+
out: list[tuple[str, str]] = []
|
|
92
|
+
for sub, dest_dir, pattern in _ASSET_DIRS:
|
|
93
|
+
for path in sorted((src / sub).glob(pattern)):
|
|
94
|
+
out.append((f"{sub}/{path.name}", dest_dir))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find_assets() -> Path:
|
|
99
|
+
for root in ASSET_ROOTS:
|
|
100
|
+
if (root / "hooks").is_dir():
|
|
101
|
+
return root
|
|
102
|
+
raise FileNotFoundError(f"client_assets/ not found in any of: {ASSET_ROOTS}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cli_version() -> str:
|
|
106
|
+
try:
|
|
107
|
+
from importlib.metadata import version
|
|
108
|
+
|
|
109
|
+
return version("agentgov-cli")
|
|
110
|
+
except Exception:
|
|
111
|
+
return "unknown"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sha256_file(path: Path) -> str:
|
|
115
|
+
h = hashlib.sha256()
|
|
116
|
+
with path.open("rb") as fh:
|
|
117
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
118
|
+
h.update(chunk)
|
|
119
|
+
return h.hexdigest()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def hooks_dir(home: Path | None = None) -> Path:
|
|
123
|
+
return (home or Path.home()) / ".claude" / "agentgov-hooks"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def manifest_path(home: Path | None = None) -> Path:
|
|
127
|
+
return hooks_dir(home) / MANIFEST_NAME
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def installed_path(rel: str, dest_dir: str, home: Path | None = None) -> Path:
|
|
131
|
+
"""Where a shipped asset lands on disk."""
|
|
132
|
+
return (home or Path.home()) / ".claude" / dest_dir / Path(rel).name
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load(home: Path | None = None) -> dict[str, str]:
|
|
136
|
+
"""Recorded hash per asset from the last install. Empty when absent.
|
|
137
|
+
|
|
138
|
+
Absent is the normal state for every install predating this file, so it
|
|
139
|
+
must degrade quietly rather than raise.
|
|
140
|
+
"""
|
|
141
|
+
path = manifest_path(home)
|
|
142
|
+
try:
|
|
143
|
+
data = json.loads(path.read_text())
|
|
144
|
+
except Exception:
|
|
145
|
+
return {}
|
|
146
|
+
assets = data.get("assets")
|
|
147
|
+
return assets if isinstance(assets, dict) else {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_version(home: Path | None = None) -> str:
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(manifest_path(home).read_text())
|
|
153
|
+
except Exception:
|
|
154
|
+
return ""
|
|
155
|
+
return str(data.get("version") or "")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def save(assets: dict[str, str], home: Path | None = None) -> None:
|
|
159
|
+
path = manifest_path(home)
|
|
160
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
path.write_text(
|
|
162
|
+
json.dumps(
|
|
163
|
+
{
|
|
164
|
+
"version": cli_version(),
|
|
165
|
+
"installed_at": datetime.now(UTC).isoformat(),
|
|
166
|
+
"assets": assets,
|
|
167
|
+
},
|
|
168
|
+
indent=2,
|
|
169
|
+
)
|
|
170
|
+
+ "\n"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def classify(rel: str, dest_dir: str, src: Path, home: Path | None = None) -> str:
|
|
175
|
+
"""One of MISSING | OK | STALE | MODIFIED for a single installed asset."""
|
|
176
|
+
shipped = src / rel
|
|
177
|
+
target = installed_path(rel, dest_dir, home)
|
|
178
|
+
if not target.exists():
|
|
179
|
+
return "MISSING"
|
|
180
|
+
if not shipped.exists():
|
|
181
|
+
# Nothing to compare against — treat as fine rather than invent a fault.
|
|
182
|
+
return "OK"
|
|
183
|
+
current = sha256_file(target)
|
|
184
|
+
if current == sha256_file(shipped):
|
|
185
|
+
return "OK"
|
|
186
|
+
recorded = load(home).get(rel)
|
|
187
|
+
# No manifest (pre-manifest install) means we cannot prove we wrote it, so
|
|
188
|
+
# it is reported as MODIFIED and install will back it up. That is the safe
|
|
189
|
+
# direction: it happens at most once per machine, and it never destroys an
|
|
190
|
+
# edit we cannot account for.
|
|
191
|
+
return "STALE" if recorded and recorded == current else "MODIFIED"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def drift(home: Path | None = None) -> list[tuple[str, str]]:
|
|
195
|
+
"""(relative path, state) for every managed asset that is not OK."""
|
|
196
|
+
try:
|
|
197
|
+
src = find_assets()
|
|
198
|
+
except FileNotFoundError:
|
|
199
|
+
return []
|
|
200
|
+
out: list[tuple[str, str]] = []
|
|
201
|
+
for rel, dest_dir in managed_assets(src):
|
|
202
|
+
state = classify(rel, dest_dir, src, home)
|
|
203
|
+
if state != "OK":
|
|
204
|
+
out.append((rel, state))
|
|
205
|
+
return out
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Bind this session to a work item — ticket id, URL, or any short label
|
|
3
|
+
allowed-tools: Bash(agentgov workitem:*)
|
|
4
|
+
argument-hint: <PROJ-1234 | https://tracker/issue/42 | any short label>
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
!`agentgov workitem --session "${CLAUDE_SESSION_ID}" -- '$ARGUMENTS'`
|
|
8
|
+
|
|
9
|
+
The command above has already run and its output is shown. Relay that output to
|
|
10
|
+
the user verbatim — do not restate, reformat, or interpret it — and then stop.
|
|
11
|
+
|
|
12
|
+
If it reported a successful bind, simply acknowledge it and carry on with
|
|
13
|
+
whatever the user was doing. If it reported a failure, show the message as-is;
|
|
14
|
+
it already contains the remediation step.
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
WHY THIS FILE LOOKS THE WAY IT DOES
|
|
18
|
+
===================================
|
|
19
|
+
|
|
20
|
+
1. WHY A `!` SHELL BLOCK AND NOT A MODEL-DRIVEN TOOL CALL
|
|
21
|
+
An unbound session is DENIED by the gateway (WorkItemMiddleware →
|
|
22
|
+
NO_WORK_ITEM), so no model turn is possible until the bind succeeds. Asking
|
|
23
|
+
the model to run the bind would deadlock: the request that would fix the
|
|
24
|
+
problem is the request the problem blocks. The bind must therefore complete
|
|
25
|
+
entirely in the shell, before anything is sent upstream.
|
|
26
|
+
|
|
27
|
+
2. WHY `${CLAUDE_SESSION_ID}` IS PASSED EXPLICITLY
|
|
28
|
+
Claude Code substitutes it into slash-command shell lines, and it is the same
|
|
29
|
+
id sent as `x-claude-code-session-id` on every API request — i.e. exactly the
|
|
30
|
+
key the gateway stores sessions under. Passing it is what lets this work in
|
|
31
|
+
the VS Code extension, with no `agentgov wrap` and no loopback listener on
|
|
32
|
+
:8788. Requiring that listener is what made `/workitem` fail for every editor
|
|
33
|
+
user.
|
|
34
|
+
|
|
35
|
+
3. WHY `agentgov workitem` NEVER EXITS NON-ZERO
|
|
36
|
+
Claude Code aborts the entire invocation when a `!` command exits non-zero and
|
|
37
|
+
reports `Shell command failed for pattern "..."`. That message is Claude Code
|
|
38
|
+
quoting the shell line it tried to run — it is NOT AgentGov matching the
|
|
39
|
+
user's text against a pattern, and nothing here inspects the format of what
|
|
40
|
+
the user typed. Our real error text was being swallowed by it, which is why
|
|
41
|
+
the failure was unreadable. The CLI now reports every failure as ordinary
|
|
42
|
+
output with exit code 0.
|
|
43
|
+
|
|
44
|
+
4. WHY `'$ARGUMENTS'` IS SINGLE-QUOTED
|
|
45
|
+
`$ARGUMENTS` is substituted as text into a shell line, so unquoted user input
|
|
46
|
+
would let `$(...)`, backticks, `;`, `|`, `&` and `>` be interpreted by the
|
|
47
|
+
shell. Single quotes make every one of those literal.
|
|
48
|
+
|
|
49
|
+
KNOWN GAP: a literal apostrophe in the user's text (e.g. `don't`) closes the
|
|
50
|
+
quote early and breaks the line. Anthropic's documentation does not state
|
|
51
|
+
whether Claude Code escapes `$ARGUMENTS` before substitution, so this could
|
|
52
|
+
not be settled from the docs and is deliberately not guessed at. The CLI
|
|
53
|
+
defends on its side by stripping a stray surrounding quote pair and by
|
|
54
|
+
joining all argv entries into one reference. The clean fix is a
|
|
55
|
+
`UserPromptExpansion` hook, which receives the typed text as structured JSON
|
|
56
|
+
and never involves a shell — revisit once that hook's schema and exit-code
|
|
57
|
+
semantics are documented.
|
|
58
|
+
-->
|