agent-lock-cli 0.1.1__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.
Files changed (37) hide show
  1. agent_lock_cli-0.1.1/LICENSE +21 -0
  2. agent_lock_cli-0.1.1/PKG-INFO +139 -0
  3. agent_lock_cli-0.1.1/README.md +110 -0
  4. agent_lock_cli-0.1.1/pyproject.toml +43 -0
  5. agent_lock_cli-0.1.1/setup.cfg +4 -0
  6. agent_lock_cli-0.1.1/src/agent_lock/__init__.py +3 -0
  7. agent_lock_cli-0.1.1/src/agent_lock/__main__.py +5 -0
  8. agent_lock_cli-0.1.1/src/agent_lock/allow.py +40 -0
  9. agent_lock_cli-0.1.1/src/agent_lock/cli.py +181 -0
  10. agent_lock_cli-0.1.1/src/agent_lock/completion.py +414 -0
  11. agent_lock_cli-0.1.1/src/agent_lock/darwin.py +276 -0
  12. agent_lock_cli-0.1.1/src/agent_lock/discover.py +542 -0
  13. agent_lock_cli-0.1.1/src/agent_lock/doctor.py +1347 -0
  14. agent_lock_cli-0.1.1/src/agent_lock/errors.py +22 -0
  15. agent_lock_cli-0.1.1/src/agent_lock/install.py +1024 -0
  16. agent_lock_cli-0.1.1/src/agent_lock/linux.py +336 -0
  17. agent_lock_cli-0.1.1/src/agent_lock/state.py +275 -0
  18. agent_lock_cli-0.1.1/src/agent_lock/windows.py +706 -0
  19. agent_lock_cli-0.1.1/src/agent_lock/wrapper.py +259 -0
  20. agent_lock_cli-0.1.1/src/agent_lock_cli.egg-info/PKG-INFO +139 -0
  21. agent_lock_cli-0.1.1/src/agent_lock_cli.egg-info/SOURCES.txt +35 -0
  22. agent_lock_cli-0.1.1/src/agent_lock_cli.egg-info/dependency_links.txt +1 -0
  23. agent_lock_cli-0.1.1/src/agent_lock_cli.egg-info/entry_points.txt +2 -0
  24. agent_lock_cli-0.1.1/src/agent_lock_cli.egg-info/top_level.txt +1 -0
  25. agent_lock_cli-0.1.1/tests/test_allow.py +40 -0
  26. agent_lock_cli-0.1.1/tests/test_bind_bin.py +40 -0
  27. agent_lock_cli-0.1.1/tests/test_completion.py +104 -0
  28. agent_lock_cli-0.1.1/tests/test_discover.py +74 -0
  29. agent_lock_cli-0.1.1/tests/test_doctor_parse.py +209 -0
  30. agent_lock_cli-0.1.1/tests/test_help.py +89 -0
  31. agent_lock_cli-0.1.1/tests/test_install.py +341 -0
  32. agent_lock_cli-0.1.1/tests/test_nft.py +136 -0
  33. agent_lock_cli-0.1.1/tests/test_packaging.py +84 -0
  34. agent_lock_cli-0.1.1/tests/test_pf.py +63 -0
  35. agent_lock_cli-0.1.1/tests/test_review_fixes.py +1370 -0
  36. agent_lock_cli-0.1.1/tests/test_windows_rules.py +88 -0
  37. agent_lock_cli-0.1.1/tests/test_wrapper.py +62 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agent-lock contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-lock-cli
3
+ Version: 0.1.1
4
+ Summary: Lock agent commands to the local HTTP proxy by running them as the user agentnet.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/jakoes-wu/agent-lock
7
+ Project-URL: Changelog, https://github.com/jakoes-wu/agent-lock/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/jakoes-wu/agent-lock/issues
9
+ Keywords: claude,codex,proxy,firewall,agent,mihomo,clash
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Utilities
24
+ Classifier: Topic :: System :: Networking :: Firewalls
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # agent-lock
31
+
32
+ **English** | [简体中文](README.zh-CN.md)
33
+
34
+ [![Release](https://img.shields.io/github/v/release/jakoes-wu/agent-lock)](https://github.com/jakoes-wu/agent-lock/releases)
35
+ [![CI](https://github.com/jakoes-wu/agent-lock/actions/workflows/ci.yml/badge.svg)](https://github.com/jakoes-wu/agent-lock/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/agent-lock-cli)](https://pypi.org/project/agent-lock-cli/)
37
+ ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
39
+
40
+ Lock agent commands to the local HTTP proxy by running them as the user `agentnet`.
41
+
42
+ Codex and Claude, and the programs they start, run as an unprivileged account. The firewall of that account accepts TCP only to the local Clash / mihomo mixed port (`http://127.0.0.1:<mixed-port>`) and to loopback ports you name with `--allow-tcp`. Other TCP, UDP, and ICMP from that account is dropped. The firewall matches the account, not a process path.
43
+
44
+ `agent-lock` does not start your proxy and it does not edit `multi-codex` or `multi-claude`. netcheck is not this tool. The checks in this repository generate firewall text and classify command output. They do not load nftables, pf, or Windows Firewall on the machine that runs them.
45
+
46
+ ## Install the command
47
+
48
+ From a clone:
49
+
50
+ ```sh
51
+ git clone https://github.com/jakoes-wu/agent-lock.git
52
+ cd agent-lock
53
+ ./install.sh
54
+ ```
55
+
56
+ Or directly:
57
+
58
+ ```sh
59
+ curl -fsSL https://raw.githubusercontent.com/jakoes-wu/agent-lock/main/install.sh | sh
60
+ ```
61
+
62
+ The command is installed under `~/.local` (`--prefix` changes that). The installer prints a `PATH` hint and does not edit your shell profile.
63
+
64
+ From PyPI: `pipx install agent-lock-cli`. Use either pipx or `install.sh`, not both: they both provide an `agent-lock` command. The PyPI project is `agent-lock-cli` because `agentlock` is already published there.
65
+
66
+ The Homebrew formula is added after the first release asset exists. Until then, use the installer or pipx.
67
+
68
+ **Verified downloads.** A release publishes `agent-lock-<tag>.tar.gz` and `SHA256SUMS`. The remote installer checks the SHA-256 before it installs. A mismatch stops the install. Branches, and releases that have no such asset, are installed unverified (the installer says so). Set `AGENT_LOCK_REQUIRE_CHECKSUM=1` to refuse those. The checksum detects a corrupted download. It does not prove the GitHub account was not compromised.
69
+
70
+ `./install.sh` only installs the Python command. It does not create `agentnet` and it does not load a firewall. That is `agent-lock install`, below.
71
+
72
+ ## Lock the account
73
+
74
+ Point Clash / mihomo at a loopback mixed port, then:
75
+
76
+ ```sh
77
+ sudo agent-lock install
78
+ agent-lock doctor
79
+ agent-lock exec -- true
80
+ ```
81
+
82
+ `install` discovers `mixed-port` from the controller (`GET /configs`). If discovery fails, pass `--port`. A later install without `--allow-tcp` clears the extra loopback list. A loopback service that then dials the network is a second exit beside the proxy.
83
+
84
+ On Linux and macOS, `install` writes `CLAUDE_CODE_PROCESS_WRAPPER` to the Claude settings it records, after `claude daemon stop --any`. That variable does not apply on Windows. This repository does not write Windows Claude settings, does not change scheduled tasks, and does not store a password. Windows Claude must be started from `runas /user:agentnet`. There is no passwordless `runas`.
85
+
86
+ `doctor` checks the account, the firewall text, the wrappers, and (where the platform allows it) that a direct connection from `agentnet` fails while the proxy and the owner's own direct TCP still work. `doctor --json` prints schema version 1 and does not print the controller secret. `doctor --repair` reapplies credential ACLs only. It does not change the port and it does not chown launchers.
87
+
88
+ `uninstall` removes the lock this tool installed. It does not follow or delete the directories you shared, and it does not remove a home directory.
89
+
90
+ ## What you are accepting
91
+
92
+ - `agentnet` can read the credential files you share (`~/.codex`, `~/.cx`, `~/.claude`, `~/.cc`, and `--share-dir`). That is the cost of running the agent as that user. The macOS Claude keychain is not shared.
93
+ - On macOS, Desktop, Documents, and Downloads (including iCloud Mobile Documents) are TCC-protected. Keep repositories outside those folders. `--share-dir` refuses them.
94
+ - With umask 002, new files in a bind-bin directory become 0664 and are writable by agentnet, and new subdirectories inherit setgid. A new subdirectory created with umask 002 is 2775, so run `bind-bin` on it again. `bind-bin` is Linux only. Launchers themselves are not written by this repository.
95
+ - Unix domain sockets are not filtered. If `agentnet` can connect to the mihomo unix socket, `doctor` fails that check. It does not chmod the socket.
96
+ - A human with the sudo password can undo the firewall.
97
+ - `agentnet` is not an administrator and is not added to `sudo` or Administrators. An existing disabled account is left alone and install fails. An existing enabled account keeps its password.
98
+
99
+ ## Shell completion
100
+
101
+ Completion is generated from the same argparse parser as the help text. `agent-lock completion <shell>` prints the script even when the lock is not installed.
102
+
103
+ ```sh
104
+ # bash, in ~/.bashrc (macOS bash 3.2, no bash-completion package)
105
+ eval "$(agent-lock completion bash)"
106
+
107
+ # zsh, after compinit
108
+ eval "$(agent-lock completion zsh)"
109
+
110
+ # fish
111
+ agent-lock completion fish | source
112
+
113
+ # PowerShell
114
+ agent-lock completion powershell | Out-String | Add-Content $PROFILE
115
+ ```
116
+
117
+ Filename completion is only offered for `--claude-dir`, `--share-dir`, `--trust-exec`, and the `bind-bin` directory. `--port`, `--allow-tcp`, `--controller`, and the command after `exec --` do not complete filenames.
118
+
119
+ ## Platforms
120
+
121
+ | | Firewall | Account |
122
+ | --- | --- | --- |
123
+ | Linux | nftables `inet` table, one transaction, output hook on the uid | `useradd` when missing, `passwd -l`, home `/var/lib/agentnet` |
124
+ | macOS | pf `user` rules in an anchor inserted before `anchor "com.apple/*"` | `sysadminctl` when missing |
125
+ | Windows | block rules for literal non-loopback ranges and for loopback ports other than the mixed port | `New-LocalUser`, not an Administrator |
126
+
127
+ firewalld, a pf `pass quick` already sitting before the Apple anchor, or a Windows policy that does not merge local firewall rules is unsupported (`exit 4`). Install does not run `pfctl -d`. nftables that rejects `destroy table` is also unsupported and install exits 4. nftables 1.0.2 on Ubuntu 22.04 is one of those builds.
128
+
129
+ ## Development
130
+
131
+ Python 3.8 or newer, standard library only.
132
+
133
+ ```sh
134
+ python3 -m pyflakes src tests
135
+ python3 -m unittest discover -s tests -t tests -v
136
+ shellcheck -s sh install.sh
137
+ ```
138
+
139
+ Tests must pass without root, without nft, without pfctl, and without Windows. Do not run `install.sh` against your real home while developing; the installer tests pass `--prefix` to a temporary directory. Do not run `agent-lock install` on a machine unless you intend to create `agentnet` and load a firewall.
@@ -0,0 +1,110 @@
1
+ # agent-lock
2
+
3
+ **English** | [简体中文](README.zh-CN.md)
4
+
5
+ [![Release](https://img.shields.io/github/v/release/jakoes-wu/agent-lock)](https://github.com/jakoes-wu/agent-lock/releases)
6
+ [![CI](https://github.com/jakoes-wu/agent-lock/actions/workflows/ci.yml/badge.svg)](https://github.com/jakoes-wu/agent-lock/actions/workflows/ci.yml)
7
+ [![PyPI](https://img.shields.io/pypi/v/agent-lock-cli)](https://pypi.org/project/agent-lock-cli/)
8
+ ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
10
+
11
+ Lock agent commands to the local HTTP proxy by running them as the user `agentnet`.
12
+
13
+ Codex and Claude, and the programs they start, run as an unprivileged account. The firewall of that account accepts TCP only to the local Clash / mihomo mixed port (`http://127.0.0.1:<mixed-port>`) and to loopback ports you name with `--allow-tcp`. Other TCP, UDP, and ICMP from that account is dropped. The firewall matches the account, not a process path.
14
+
15
+ `agent-lock` does not start your proxy and it does not edit `multi-codex` or `multi-claude`. netcheck is not this tool. The checks in this repository generate firewall text and classify command output. They do not load nftables, pf, or Windows Firewall on the machine that runs them.
16
+
17
+ ## Install the command
18
+
19
+ From a clone:
20
+
21
+ ```sh
22
+ git clone https://github.com/jakoes-wu/agent-lock.git
23
+ cd agent-lock
24
+ ./install.sh
25
+ ```
26
+
27
+ Or directly:
28
+
29
+ ```sh
30
+ curl -fsSL https://raw.githubusercontent.com/jakoes-wu/agent-lock/main/install.sh | sh
31
+ ```
32
+
33
+ The command is installed under `~/.local` (`--prefix` changes that). The installer prints a `PATH` hint and does not edit your shell profile.
34
+
35
+ From PyPI: `pipx install agent-lock-cli`. Use either pipx or `install.sh`, not both: they both provide an `agent-lock` command. The PyPI project is `agent-lock-cli` because `agentlock` is already published there.
36
+
37
+ The Homebrew formula is added after the first release asset exists. Until then, use the installer or pipx.
38
+
39
+ **Verified downloads.** A release publishes `agent-lock-<tag>.tar.gz` and `SHA256SUMS`. The remote installer checks the SHA-256 before it installs. A mismatch stops the install. Branches, and releases that have no such asset, are installed unverified (the installer says so). Set `AGENT_LOCK_REQUIRE_CHECKSUM=1` to refuse those. The checksum detects a corrupted download. It does not prove the GitHub account was not compromised.
40
+
41
+ `./install.sh` only installs the Python command. It does not create `agentnet` and it does not load a firewall. That is `agent-lock install`, below.
42
+
43
+ ## Lock the account
44
+
45
+ Point Clash / mihomo at a loopback mixed port, then:
46
+
47
+ ```sh
48
+ sudo agent-lock install
49
+ agent-lock doctor
50
+ agent-lock exec -- true
51
+ ```
52
+
53
+ `install` discovers `mixed-port` from the controller (`GET /configs`). If discovery fails, pass `--port`. A later install without `--allow-tcp` clears the extra loopback list. A loopback service that then dials the network is a second exit beside the proxy.
54
+
55
+ On Linux and macOS, `install` writes `CLAUDE_CODE_PROCESS_WRAPPER` to the Claude settings it records, after `claude daemon stop --any`. That variable does not apply on Windows. This repository does not write Windows Claude settings, does not change scheduled tasks, and does not store a password. Windows Claude must be started from `runas /user:agentnet`. There is no passwordless `runas`.
56
+
57
+ `doctor` checks the account, the firewall text, the wrappers, and (where the platform allows it) that a direct connection from `agentnet` fails while the proxy and the owner's own direct TCP still work. `doctor --json` prints schema version 1 and does not print the controller secret. `doctor --repair` reapplies credential ACLs only. It does not change the port and it does not chown launchers.
58
+
59
+ `uninstall` removes the lock this tool installed. It does not follow or delete the directories you shared, and it does not remove a home directory.
60
+
61
+ ## What you are accepting
62
+
63
+ - `agentnet` can read the credential files you share (`~/.codex`, `~/.cx`, `~/.claude`, `~/.cc`, and `--share-dir`). That is the cost of running the agent as that user. The macOS Claude keychain is not shared.
64
+ - On macOS, Desktop, Documents, and Downloads (including iCloud Mobile Documents) are TCC-protected. Keep repositories outside those folders. `--share-dir` refuses them.
65
+ - With umask 002, new files in a bind-bin directory become 0664 and are writable by agentnet, and new subdirectories inherit setgid. A new subdirectory created with umask 002 is 2775, so run `bind-bin` on it again. `bind-bin` is Linux only. Launchers themselves are not written by this repository.
66
+ - Unix domain sockets are not filtered. If `agentnet` can connect to the mihomo unix socket, `doctor` fails that check. It does not chmod the socket.
67
+ - A human with the sudo password can undo the firewall.
68
+ - `agentnet` is not an administrator and is not added to `sudo` or Administrators. An existing disabled account is left alone and install fails. An existing enabled account keeps its password.
69
+
70
+ ## Shell completion
71
+
72
+ Completion is generated from the same argparse parser as the help text. `agent-lock completion <shell>` prints the script even when the lock is not installed.
73
+
74
+ ```sh
75
+ # bash, in ~/.bashrc (macOS bash 3.2, no bash-completion package)
76
+ eval "$(agent-lock completion bash)"
77
+
78
+ # zsh, after compinit
79
+ eval "$(agent-lock completion zsh)"
80
+
81
+ # fish
82
+ agent-lock completion fish | source
83
+
84
+ # PowerShell
85
+ agent-lock completion powershell | Out-String | Add-Content $PROFILE
86
+ ```
87
+
88
+ Filename completion is only offered for `--claude-dir`, `--share-dir`, `--trust-exec`, and the `bind-bin` directory. `--port`, `--allow-tcp`, `--controller`, and the command after `exec --` do not complete filenames.
89
+
90
+ ## Platforms
91
+
92
+ | | Firewall | Account |
93
+ | --- | --- | --- |
94
+ | Linux | nftables `inet` table, one transaction, output hook on the uid | `useradd` when missing, `passwd -l`, home `/var/lib/agentnet` |
95
+ | macOS | pf `user` rules in an anchor inserted before `anchor "com.apple/*"` | `sysadminctl` when missing |
96
+ | Windows | block rules for literal non-loopback ranges and for loopback ports other than the mixed port | `New-LocalUser`, not an Administrator |
97
+
98
+ firewalld, a pf `pass quick` already sitting before the Apple anchor, or a Windows policy that does not merge local firewall rules is unsupported (`exit 4`). Install does not run `pfctl -d`. nftables that rejects `destroy table` is also unsupported and install exits 4. nftables 1.0.2 on Ubuntu 22.04 is one of those builds.
99
+
100
+ ## Development
101
+
102
+ Python 3.8 or newer, standard library only.
103
+
104
+ ```sh
105
+ python3 -m pyflakes src tests
106
+ python3 -m unittest discover -s tests -t tests -v
107
+ shellcheck -s sh install.sh
108
+ ```
109
+
110
+ Tests must pass without root, without nft, without pfctl, and without Windows. Do not run `install.sh` against your real home while developing; the installer tests pass `--prefix` to a temporary directory. Do not run `agent-lock install` on a machine unless you intend to create `agentnet` and load a firewall.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agent-lock-cli"
7
+ dynamic = ["version"]
8
+ description = "Lock agent commands to the local HTTP proxy by running them as the user agentnet."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ keywords = ["claude", "codex", "proxy", "firewall", "agent", "mihomo", "clash"]
13
+ classifiers = [
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: MacOS",
18
+ "Operating System :: Microsoft :: Windows",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Utilities",
28
+ "Topic :: System :: Networking :: Firewalls",
29
+ ]
30
+
31
+ [project.scripts]
32
+ agent-lock = "agent_lock.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/jakoes-wu/agent-lock"
36
+ Changelog = "https://github.com/jakoes-wu/agent-lock/blob/main/CHANGELOG.md"
37
+ Issues = "https://github.com/jakoes-wu/agent-lock/issues"
38
+
39
+ [tool.setuptools.dynamic]
40
+ version = {attr = "agent_lock.__version__"}
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Lock agent commands to the local HTTP proxy by running them as agentnet."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,40 @@
1
+ """--allow-tcp values. Only 127.0.0.1:PORT and ::1:PORT."""
2
+
3
+ import argparse
4
+ import re
5
+
6
+ _VALUE = re.compile(r"^(127\.0\.0\.1|::1):([0-9]+)$")
7
+
8
+
9
+ def parse_allow_tcp_value(value):
10
+ """Return (host, port) or raise ValueError. Used by firewall renderers."""
11
+ match = _VALUE.match(value or "")
12
+ if not match:
13
+ raise ValueError(value)
14
+ port = int(match.group(2))
15
+ if match.group(2) != str(port):
16
+ # Reject leading zeros so the stored form is canonical.
17
+ raise ValueError(value)
18
+ if port < 1 or port > 65535:
19
+ raise ValueError(value)
20
+ return match.group(1), port
21
+
22
+
23
+ def allow_tcp_arg(value):
24
+ """argparse type. Illegal values are exit 2."""
25
+ try:
26
+ host, port = parse_allow_tcp_value(value)
27
+ except ValueError:
28
+ raise argparse.ArgumentTypeError(
29
+ "must be 127.0.0.1:PORT or ::1:PORT (port 1-65535), not {!r}".format(value))
30
+ return "{}:{}".format(host, port)
31
+
32
+
33
+ def port_arg(value):
34
+ try:
35
+ port = int(value)
36
+ except ValueError:
37
+ raise argparse.ArgumentTypeError("port must be an integer")
38
+ if str(port) != str(value).lstrip("+") or port < 1 or port > 65535:
39
+ raise argparse.ArgumentTypeError("port must be 1-65535")
40
+ return port
@@ -0,0 +1,181 @@
1
+ """agent-lock command line. Help text and examples come from this module."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from . import __version__
7
+ from .allow import allow_tcp_arg, port_arg
8
+ from .completion import SHELLS, script
9
+ from .errors import AgentLockError
10
+ from .install import cmd_bind_bin, cmd_doctor, cmd_exec, cmd_install, cmd_uninstall
11
+
12
+ COMMAND_GROUPS = (
13
+ ("Setup", (
14
+ ("install", "install the account, firewall, and wrappers"),
15
+ ("uninstall", "remove the lock and keep linked directories"),
16
+ ("bind-bin", "mark a directory setgid for new launchers"),
17
+ ("completion", "print a shell completion script"),
18
+ )),
19
+ ("Everyday", (
20
+ ("doctor", "check the lock and repair cleared ACLs"),
21
+ ("exec", "run one command as agentnet"),
22
+ )),
23
+ )
24
+ COMMAND_SUMMARY = {name: summary for _title, commands in COMMAND_GROUPS for name, summary in commands}
25
+
26
+ TOP_EXAMPLES = (
27
+ ("agent-lock install", "install the lock"),
28
+ ("agent-lock doctor", "check the lock"),
29
+ ("agent-lock exec -- true", "run one command as agentnet"),
30
+ ("agent-lock completion bash", "print a bash script"),
31
+ )
32
+
33
+ COMMAND_EXAMPLES = {
34
+ "install": (
35
+ ("agent-lock install", "install lock"),
36
+ ("agent-lock install --port 7897 --allow-tcp 127.0.0.1:11434", "pin the port"),
37
+ ("agent-lock install --controller 127.0.0.1:9097 --secret-stdin", "read secret"),
38
+ ("agent-lock install --share-dir ~/.cx --trust-exec ~/bin", "share paths"),
39
+ ),
40
+ "uninstall": (
41
+ ("agent-lock uninstall", "remove the lock"),
42
+ ("agent-lock uninstall", "safe to repeat"),
43
+ ),
44
+ "doctor": (
45
+ ("agent-lock doctor", "run every check"),
46
+ ("agent-lock doctor --json", "print version 1 JSON"),
47
+ ("agent-lock doctor --repair", "restore cleared ACLs"),
48
+ ),
49
+ "exec": (
50
+ ("agent-lock exec -- true", "run true as agentnet"),
51
+ ("agent-lock exec -- curl -I https://example.com", "only via the proxy"),
52
+ ("agent-lock exec -- git status", "run git as agentnet"),
53
+ ),
54
+ "bind-bin": (
55
+ ("sudo agent-lock bind-bin ~/.local/bin", "setgid this directory"),
56
+ ("sudo agent-lock bind-bin /tmp/bin", "clear group write again"),
57
+ ),
58
+ "completion": (
59
+ ("eval \"$(agent-lock completion bash)\"", "add this to ~/.bashrc"),
60
+ ("eval \"$(agent-lock completion zsh)\"", "run this after compinit"),
61
+ ("agent-lock completion fish | source", "load the fish script"),
62
+ ("agent-lock completion powershell", "print the PowerShell script"),
63
+ ),
64
+ }
65
+
66
+ ALLOW_TCP_HELP = (
67
+ "loopback TCP port to allow (127.0.0.1:PORT or ::1:PORT). "
68
+ "A loopback service that then dials the network is a second exit beside the proxy."
69
+ )
70
+
71
+
72
+ def _examples_text(examples):
73
+ """examples: epilog. Command lines are padded to the longest plus two."""
74
+ width = max(len(command) for command, _summary in examples) + 2
75
+ lines = ["examples:"]
76
+ lines.extend(" {}{}".format(command.ljust(width), summary) for command, summary in examples)
77
+ return "\n".join(lines) + "\n"
78
+
79
+
80
+ def _grouped_commands_text():
81
+ width = max(len(name) for name in COMMAND_SUMMARY) + 2
82
+ lines = []
83
+ for title, commands in COMMAND_GROUPS:
84
+ lines.append("{}:".format(title))
85
+ lines.extend(" {}{}".format(name.ljust(width), summary) for name, summary in commands)
86
+ lines.append("")
87
+ return "\n".join(lines).rstrip("\n")
88
+
89
+
90
+ HELP_EXAMPLES = _examples_text(TOP_EXAMPLES) + "\nRun agent-lock COMMAND -h for more examples.\n"
91
+
92
+
93
+ def build_parser():
94
+ parser = argparse.ArgumentParser(
95
+ prog="agent-lock",
96
+ allow_abbrev=False,
97
+ formatter_class=argparse.RawDescriptionHelpFormatter,
98
+ description="Lock agent commands to the local HTTP proxy by running them as the user agentnet.\n\n"
99
+ + _grouped_commands_text(),
100
+ epilog=HELP_EXAMPLES,
101
+ )
102
+ parser.add_argument("--version", action="version", version="%(prog)s " + __version__)
103
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND")
104
+ sub.required = True
105
+
106
+ install = sub.add_parser("install", description=COMMAND_SUMMARY["install"], allow_abbrev=False)
107
+ install.add_argument("--port", type=port_arg, help="mixed port; required when discovery fails")
108
+ install.add_argument("--controller", help="loopback controller (host:port or unix:PATH)")
109
+ install.add_argument("--secret-stdin", action="store_true", help="read one secret line from stdin")
110
+ install.add_argument("--claude-dir", action="append", default=None, help="extra Claude settings directory")
111
+ install.add_argument("--share-dir", action="append", default=None, help="extra directory to share with agentnet")
112
+ install.add_argument("--trust-exec", action="append", default=None, help="program or directory agentnet may execute")
113
+ install.add_argument("--allow-tcp", action="append", default=None, type=allow_tcp_arg, help=ALLOW_TCP_HELP)
114
+
115
+ sub.add_parser("uninstall", description=COMMAND_SUMMARY["uninstall"], allow_abbrev=False)
116
+
117
+ doctor = sub.add_parser("doctor", description=COMMAND_SUMMARY["doctor"], allow_abbrev=False)
118
+ doctor.add_argument("--json", action="store_true", help="print version 1 JSON")
119
+ doctor.add_argument("--repair", action="store_true", help="reapply credential ACLs; does not change the port")
120
+
121
+ sub.add_parser("exec", description=COMMAND_SUMMARY["exec"], allow_abbrev=False)
122
+
123
+ bind = sub.add_parser("bind-bin", description=COMMAND_SUMMARY["bind-bin"], allow_abbrev=False)
124
+ bind.add_argument("path", metavar="PATH", help="directory to mark setgid for the agentnet group")
125
+
126
+ completion = sub.add_parser("completion", description=COMMAND_SUMMARY["completion"], allow_abbrev=False)
127
+ completion.add_argument("shell", choices=SHELLS, help="bash, zsh, fish, or powershell")
128
+
129
+ for name, subparser in sub.choices.items():
130
+ subparser.formatter_class = argparse.RawDescriptionHelpFormatter
131
+ subparser.epilog = _examples_text(COMMAND_EXAMPLES[name])
132
+ subparser.allow_abbrev = False
133
+ return parser
134
+
135
+
136
+ def _split_exec(argv):
137
+ """Drop the command tail at the first --. Flags such as -h stay on the parser side."""
138
+ if not argv or argv[0] != "exec":
139
+ return argv, None
140
+ if "--" not in argv:
141
+ return argv, None
142
+ index = argv.index("--")
143
+ return argv[:index] or ["exec"], argv[index + 1:]
144
+
145
+
146
+ def main(argv=None):
147
+ if argv is None:
148
+ argv = sys.argv[1:]
149
+ argv = list(argv)
150
+ argv, tail = _split_exec(argv)
151
+ try:
152
+ parser = build_parser()
153
+ if argv and argv[0] == "completion":
154
+ # Print the script even when agent-lock is not installed.
155
+ args = parser.parse_args(argv)
156
+ sys.stdout.write(script(args.shell, parser))
157
+ return 0
158
+ args = parser.parse_args(argv)
159
+ except SystemExit as exc:
160
+ code = exc.code
161
+ if code is None:
162
+ return 0
163
+ return int(code)
164
+ try:
165
+ if args.command == "install":
166
+ return cmd_install(args)
167
+ if args.command == "uninstall":
168
+ return cmd_uninstall(args)
169
+ if args.command == "doctor":
170
+ return cmd_doctor(args)
171
+ if args.command == "exec":
172
+ return cmd_exec(tail)
173
+ if args.command == "bind-bin":
174
+ return cmd_bind_bin(args)
175
+ if args.command == "completion":
176
+ sys.stdout.write(script(args.shell, parser))
177
+ return 0
178
+ except AgentLockError as exc:
179
+ sys.stderr.write("agent-lock: {}\n".format(exc))
180
+ return int(exc.code)
181
+ return 2