agent-lock-cli 0.1.1__py3-none-any.whl
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.
- agent_lock/__init__.py +3 -0
- agent_lock/__main__.py +5 -0
- agent_lock/allow.py +40 -0
- agent_lock/cli.py +181 -0
- agent_lock/completion.py +414 -0
- agent_lock/darwin.py +276 -0
- agent_lock/discover.py +542 -0
- agent_lock/doctor.py +1347 -0
- agent_lock/errors.py +22 -0
- agent_lock/install.py +1024 -0
- agent_lock/linux.py +336 -0
- agent_lock/state.py +275 -0
- agent_lock/windows.py +706 -0
- agent_lock/wrapper.py +259 -0
- agent_lock_cli-0.1.1.dist-info/METADATA +139 -0
- agent_lock_cli-0.1.1.dist-info/RECORD +20 -0
- agent_lock_cli-0.1.1.dist-info/WHEEL +5 -0
- agent_lock_cli-0.1.1.dist-info/entry_points.txt +2 -0
- agent_lock_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
- agent_lock_cli-0.1.1.dist-info/top_level.txt +1 -0
agent_lock/wrapper.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Render the switcher, the Claude wrapper, the Windows cmd, and sudoers.
|
|
2
|
+
|
|
3
|
+
The scripts bake in the port. They do not read state.json, so agentnet never
|
|
4
|
+
sees the controller secret. The Claude wrapper has no `unset` of any name.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
|
|
10
|
+
from .errors import Unsupported, UsageError
|
|
11
|
+
from .linux import accepted_system_tool
|
|
12
|
+
from .state import AGENT_USER, CLAUDE_WRAPPER_PATH, SWITCHER_PATH, proxy_url
|
|
13
|
+
|
|
14
|
+
# sudoers has no quoting for the user name. Reject anything passwd would not.
|
|
15
|
+
_OWNER_NAME = re.compile(r"^[A-Za-z_][-A-Za-z0-9._]*[$]?$")
|
|
16
|
+
|
|
17
|
+
# Literal names sudo deletes before it execs /bin/sh. No wildcard.
|
|
18
|
+
SUDOERS_ENV_DELETE = (
|
|
19
|
+
"LD_PRELOAD",
|
|
20
|
+
"LD_LIBRARY_PATH",
|
|
21
|
+
"LD_AUDIT",
|
|
22
|
+
"LD_DEBUG",
|
|
23
|
+
"LD_DEBUG_OUTPUT",
|
|
24
|
+
"LD_PROFILE",
|
|
25
|
+
"LD_ORIGIN_PATH",
|
|
26
|
+
"DYLD_INSERT_LIBRARIES",
|
|
27
|
+
"DYLD_LIBRARY_PATH",
|
|
28
|
+
"DYLD_FRAMEWORK_PATH",
|
|
29
|
+
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
30
|
+
"DYLD_FALLBACK_FRAMEWORK_PATH",
|
|
31
|
+
"DYLD_IMAGE_SUFFIX",
|
|
32
|
+
"ENV",
|
|
33
|
+
"BASH_ENV",
|
|
34
|
+
"PYTHONPATH",
|
|
35
|
+
"PYTHONHOME",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _no_proxy_block(name):
|
|
40
|
+
"""Append localhost, 127.0.0.1, and ::1 only when missing. No helper variables."""
|
|
41
|
+
lines = []
|
|
42
|
+
for host in ("localhost", "127.0.0.1", "::1"):
|
|
43
|
+
lines.append('case ",${%s-}," in' % name)
|
|
44
|
+
lines.append(" *,%s,*) ;;" % host)
|
|
45
|
+
lines.append(' *) %s="${%s:+$%s,}%s" ;;' % (name, name, name, host))
|
|
46
|
+
lines.append("esac")
|
|
47
|
+
lines.append("export %s" % name)
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _git_block():
|
|
52
|
+
return """if [ -z "${GIT_CONFIG_COUNT+x}" ]; then
|
|
53
|
+
GIT_CONFIG_COUNT=1
|
|
54
|
+
GIT_CONFIG_KEY_0=safe.directory
|
|
55
|
+
GIT_CONFIG_VALUE_0='*'
|
|
56
|
+
export GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
|
|
57
|
+
fi"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _proxy_block(port):
|
|
61
|
+
url = proxy_url(port)
|
|
62
|
+
return """HTTP_PROXY=%s
|
|
63
|
+
HTTPS_PROXY=$HTTP_PROXY
|
|
64
|
+
http_proxy=$HTTP_PROXY
|
|
65
|
+
https_proxy=$HTTP_PROXY
|
|
66
|
+
export HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
|
|
67
|
+
%s
|
|
68
|
+
%s""" % (url, _no_proxy_block("NO_PROXY"), _no_proxy_block("no_proxy"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _root_exec(runuser_path, darwin):
|
|
72
|
+
switcher = shlex.quote(SWITCHER_PATH)
|
|
73
|
+
if darwin:
|
|
74
|
+
return 'exec /usr/bin/sudo --preserve-env -u %s -- %s "$@"' % (AGENT_USER, switcher)
|
|
75
|
+
return "exec %s --preserve-environment -u %s -- %s \"$@\"" % (
|
|
76
|
+
shlex.quote(runuser_path), AGENT_USER, switcher)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def render_switcher(port, runuser_path, darwin=False):
|
|
80
|
+
"""POSIX switcher. `runuser_path` is the path found at install time."""
|
|
81
|
+
port = int(port)
|
|
82
|
+
url = proxy_url(port)
|
|
83
|
+
if not darwin:
|
|
84
|
+
accepted = accepted_system_tool(runuser_path)
|
|
85
|
+
if not accepted:
|
|
86
|
+
raise Unsupported("runuser was not found")
|
|
87
|
+
runuser_path = accepted
|
|
88
|
+
runuser_comment = "" if darwin else runuser_path
|
|
89
|
+
root_exec = _root_exec(runuser_path, darwin)
|
|
90
|
+
text = """#!/bin/sh
|
|
91
|
+
# Generated by agent-lock. Do not edit.
|
|
92
|
+
# wrapper_port=%(port)s
|
|
93
|
+
# proxy_url=%(url)s
|
|
94
|
+
# claude_wrapper=%(claude)s
|
|
95
|
+
# runuser=%(runuser)s
|
|
96
|
+
# Keep the caller's PATH across exec. Record it only on the first entry so the
|
|
97
|
+
# root hop's safe PATH does not overwrite it.
|
|
98
|
+
if [ -z "${AGENT_LOCK_SAVED_PATH+x}" ]; then
|
|
99
|
+
AGENT_LOCK_SAVED_PATH=${PATH-}
|
|
100
|
+
export AGENT_LOCK_SAVED_PATH
|
|
101
|
+
fi
|
|
102
|
+
PATH=/usr/bin:/bin:/usr/sbin:/sbin
|
|
103
|
+
export PATH
|
|
104
|
+
|
|
105
|
+
if [ "$(/usr/bin/id -un)" = "%(user)s" ]; then
|
|
106
|
+
# An empty snapshot means the owner had the variable unset. Restoring it is
|
|
107
|
+
# not dropping a Claude token. Do not unset the snapshot names.
|
|
108
|
+
if [ -n "${AGENT_LOCK_OWNER_USER+x}" ]; then
|
|
109
|
+
if [ -n "$AGENT_LOCK_OWNER_USER" ]; then
|
|
110
|
+
USER=$AGENT_LOCK_OWNER_USER
|
|
111
|
+
export USER
|
|
112
|
+
else
|
|
113
|
+
unset USER
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
if [ -n "${AGENT_LOCK_OWNER_LOGNAME+x}" ]; then
|
|
117
|
+
if [ -n "$AGENT_LOCK_OWNER_LOGNAME" ]; then
|
|
118
|
+
LOGNAME=$AGENT_LOCK_OWNER_LOGNAME
|
|
119
|
+
export LOGNAME
|
|
120
|
+
else
|
|
121
|
+
unset LOGNAME
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
if [ -n "${AGENT_LOCK_OWNER_HOME+x}" ]; then
|
|
125
|
+
if [ -n "$AGENT_LOCK_OWNER_HOME" ]; then
|
|
126
|
+
HOME=$AGENT_LOCK_OWNER_HOME
|
|
127
|
+
export HOME
|
|
128
|
+
else
|
|
129
|
+
unset HOME
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
PATH=$AGENT_LOCK_SAVED_PATH
|
|
133
|
+
export PATH
|
|
134
|
+
%(proxy)s
|
|
135
|
+
%(git)s
|
|
136
|
+
# Leave ALL_PROXY alone when entering the Claude wrapper. Other commands
|
|
137
|
+
# drop an inherited ALL_PROXY. This tool never sets ALL_PROXY.
|
|
138
|
+
if [ "${1-}" != "%(claude)s" ]; then
|
|
139
|
+
unset ALL_PROXY all_proxy
|
|
140
|
+
fi
|
|
141
|
+
[ "$#" -ge 1 ] || { echo "agent-lock: missing command" >&2; exit 2; }
|
|
142
|
+
exec "$@"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [ "$(/usr/bin/id -u)" -eq 0 ]; then
|
|
146
|
+
# Command substitution eats the trailing newline. A trailing dot, then
|
|
147
|
+
# stripped, leaves IFS containing space, tab, and newline. Do not export it.
|
|
148
|
+
# sudo's own env_delete already removed the caller's IFS.
|
|
149
|
+
IFS=$(/usr/bin/printf ' \\t\\n.')
|
|
150
|
+
IFS=${IFS%%.}
|
|
151
|
+
unset ENV BASH_ENV PYTHONPATH PYTHONHOME
|
|
152
|
+
AGENT_LOCK_LD_KEYS=$(export -p | /bin/sed -n \\
|
|
153
|
+
's/.*\\(LD_[A-Za-z0-9_][A-Za-z0-9_]*\\|DYLD_[A-Za-z0-9_][A-Za-z0-9_]*\\)=.*/\\1/p')
|
|
154
|
+
# shellcheck disable=SC2086
|
|
155
|
+
for AGENT_LOCK_LD_NAME in $AGENT_LOCK_LD_KEYS; do
|
|
156
|
+
unset "$AGENT_LOCK_LD_NAME"
|
|
157
|
+
done
|
|
158
|
+
unset IFS
|
|
159
|
+
# Do not unset AGENT_LOCK_LD_KEYS or AGENT_LOCK_LD_NAME: they were not
|
|
160
|
+
# exported, so exec drops them. Never exec the user command as root.
|
|
161
|
+
%(root_exec)s
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Not root and not agentnet. Snapshot before the first sudo. After this hop
|
|
165
|
+
# USER may be root, so the snapshot has to happen here.
|
|
166
|
+
if [ -z "${AGENT_LOCK_OWNER_USER+x}" ]; then
|
|
167
|
+
AGENT_LOCK_OWNER_USER=${USER-}
|
|
168
|
+
AGENT_LOCK_OWNER_LOGNAME=${LOGNAME-}
|
|
169
|
+
AGENT_LOCK_OWNER_HOME=${HOME-}
|
|
170
|
+
export AGENT_LOCK_OWNER_USER AGENT_LOCK_OWNER_LOGNAME AGENT_LOCK_OWNER_HOME
|
|
171
|
+
fi
|
|
172
|
+
# -l is per command. A passworded ALL rule would make -v prompt every time.
|
|
173
|
+
if ! /usr/bin/sudo -n -l %(switcher)s >/dev/null 2>&1; then
|
|
174
|
+
echo "agent-lock: cannot switch to agentnet (sudo -n -l refused)" >&2
|
|
175
|
+
exit 126
|
|
176
|
+
fi
|
|
177
|
+
exec /usr/bin/sudo -n -E -- %(switcher)s "$@"
|
|
178
|
+
""" % {
|
|
179
|
+
"port": port,
|
|
180
|
+
"url": url,
|
|
181
|
+
"claude": CLAUDE_WRAPPER_PATH,
|
|
182
|
+
"runuser": runuser_comment,
|
|
183
|
+
"user": AGENT_USER,
|
|
184
|
+
"proxy": _proxy_block(port),
|
|
185
|
+
"git": _git_block(),
|
|
186
|
+
"root_exec": root_exec,
|
|
187
|
+
"switcher": SWITCHER_PATH,
|
|
188
|
+
}
|
|
189
|
+
return text
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def render_claude(port):
|
|
193
|
+
"""Claude process wrapper. The body must not contain an unset of any name."""
|
|
194
|
+
port = int(port)
|
|
195
|
+
url = proxy_url(port)
|
|
196
|
+
return """#!/bin/sh
|
|
197
|
+
# Generated by agent-lock. Do not edit.
|
|
198
|
+
# wrapper_port=%(port)s
|
|
199
|
+
# proxy_url=%(url)s
|
|
200
|
+
if [ "$(/usr/bin/id -un)" != "%(user)s" ]; then
|
|
201
|
+
exec %(switcher)s %(claude)s "$@"
|
|
202
|
+
fi
|
|
203
|
+
%(proxy)s
|
|
204
|
+
%(git)s
|
|
205
|
+
[ "$#" -ge 1 ] || { echo "agent-lock-claude: missing command" >&2; exit 2; }
|
|
206
|
+
exec "$@"
|
|
207
|
+
""" % {
|
|
208
|
+
"port": port,
|
|
209
|
+
"url": url,
|
|
210
|
+
"user": AGENT_USER,
|
|
211
|
+
"switcher": SWITCHER_PATH,
|
|
212
|
+
"claude": CLAUDE_WRAPPER_PATH,
|
|
213
|
+
"proxy": _proxy_block(port),
|
|
214
|
+
"git": _git_block(),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def render_sudoers(owner):
|
|
219
|
+
if not _OWNER_NAME.match(owner or ""):
|
|
220
|
+
raise UsageError("owner name is not safe for sudoers")
|
|
221
|
+
lines = [
|
|
222
|
+
"# agent-lock: passwordless switcher only. The script drops to agentnet.",
|
|
223
|
+
"# !set_logname: otherwise USER and LOGNAME become the target user even when env_reset is off.",
|
|
224
|
+
"# env_delete+= is one literal name per line. No wildcard.",
|
|
225
|
+
"Defaults!%s !env_reset, !set_logname, !set_home, !always_set_home" % SWITCHER_PATH,
|
|
226
|
+
]
|
|
227
|
+
for name in SUDOERS_ENV_DELETE:
|
|
228
|
+
lines.append('Defaults!%s env_delete+="%s"' % (SWITCHER_PATH, name))
|
|
229
|
+
lines.append("%s ALL=(root) NOPASSWD:SETENV: %s" % (owner, SWITCHER_PATH))
|
|
230
|
+
lines.append("")
|
|
231
|
+
return "\n".join(lines)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def render_windows_cmd(port, sid):
|
|
235
|
+
"""SID check and four proxy variables. Does not assign NO_PROXY or store a password."""
|
|
236
|
+
port = int(port)
|
|
237
|
+
url = proxy_url(port)
|
|
238
|
+
return """@echo off
|
|
239
|
+
setlocal EnableExtensions
|
|
240
|
+
rem Generated by agent-lock. Do not edit.
|
|
241
|
+
rem wrapper_port=%(port)s
|
|
242
|
+
rem agent_sid=%(sid)s
|
|
243
|
+
if "%%~1"=="" (
|
|
244
|
+
echo agent-lock: missing command 1>&2
|
|
245
|
+
exit /b 2
|
|
246
|
+
)
|
|
247
|
+
set "SID="
|
|
248
|
+
for /f "tokens=2 delims=," %%%%S in ('whoami /user /fo csv /nh') do set "SID=%%%%~S"
|
|
249
|
+
if /I not "%%SID%%"=="%(sid)s" (
|
|
250
|
+
echo agent-lock: current user is not agentnet. Open a window with: runas /user:agentnet cmd 1>&2
|
|
251
|
+
exit /b 126
|
|
252
|
+
)
|
|
253
|
+
set "HTTP_PROXY=%(url)s"
|
|
254
|
+
set "HTTPS_PROXY=%(url)s"
|
|
255
|
+
set "http_proxy=%(url)s"
|
|
256
|
+
set "https_proxy=%(url)s"
|
|
257
|
+
%%*
|
|
258
|
+
exit /b %%ERRORLEVEL%%
|
|
259
|
+
""" % {"port": port, "sid": sid, "url": url}
|
|
@@ -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
|
+
[](https://github.com/jakoes-wu/agent-lock/releases)
|
|
35
|
+
[](https://github.com/jakoes-wu/agent-lock/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/agent-lock-cli/)
|
|
37
|
+

|
|
38
|
+
[](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,20 @@
|
|
|
1
|
+
agent_lock/__init__.py,sha256=MG_j59vB9-3xK_23gc81Ir3XOFLtogwxhyNqnmI0Lug,102
|
|
2
|
+
agent_lock/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
+
agent_lock/allow.py,sha256=sKx3MunNTKg5ZAnWYs21xSKG66jvQRrfJ2YL13UOjWE,1238
|
|
4
|
+
agent_lock/cli.py,sha256=2uiuJaw6Ji1wkSYDAbhn-Fk9rIJOKQWUcA-mf8zTvUs,7554
|
|
5
|
+
agent_lock/completion.py,sha256=PD30HrYZJdLcAyZ5NAIYOWQpjjftyatkWvhxF_XVE4c,15116
|
|
6
|
+
agent_lock/darwin.py,sha256=veEGjAZdnXGfckgFLPlrRukwksVarWe5Qk344piAZkc,8609
|
|
7
|
+
agent_lock/discover.py,sha256=TtxGas8R1hFgXogexSsqrUG1Zrmrm-sk1YB9iV0BaLg,19003
|
|
8
|
+
agent_lock/doctor.py,sha256=lvaBAvhQS3U74FzOxYn530Y47D-0TzrxuB2HUQXsepk,52465
|
|
9
|
+
agent_lock/errors.py,sha256=8Fj_3LV0qNdb3MTX3AlVyyPJSPCpCLL6axt3flEFW4E,649
|
|
10
|
+
agent_lock/install.py,sha256=hF6EkbHklNjtIQV18-XnOULZfxx7BPz1RgWZlNrrXjY,37150
|
|
11
|
+
agent_lock/linux.py,sha256=o2Kc12uw6rutkGXfFE5o-C05Or1-stiMWqPPag9_1Js,10964
|
|
12
|
+
agent_lock/state.py,sha256=3TKSP1emhAZEcyTrLrBfEiNLFs5khLGdfMXB26bex6A,7851
|
|
13
|
+
agent_lock/windows.py,sha256=Rqqh_q6rbTZxVuO4MrFGjGErPUBXKsqP62YvlkAVomw,25485
|
|
14
|
+
agent_lock/wrapper.py,sha256=XyGAlsfKc5_QHdVfOuVkauJgq_f612Zepapyp3QBlQ8,8270
|
|
15
|
+
agent_lock_cli-0.1.1.dist-info/licenses/LICENSE,sha256=qTSf5bSmo-7B1POxCbehzkbFTze-IFLwKteIDZQ7vVM,1080
|
|
16
|
+
agent_lock_cli-0.1.1.dist-info/METADATA,sha256=N4tLJPkwGxPQeWD-q_LQnZsZ8aBJliLEf4VHbkIfMZw,8263
|
|
17
|
+
agent_lock_cli-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
18
|
+
agent_lock_cli-0.1.1.dist-info/entry_points.txt,sha256=q0bJiChn1A-8zARlWfVCyrctivy8EBhDx7izMvViqfg,51
|
|
19
|
+
agent_lock_cli-0.1.1.dist-info/top_level.txt,sha256=Icc5ca8PsYcMs_izcR6TPtPeFNb-4NpP0KYv1OYhQJk,11
|
|
20
|
+
agent_lock_cli-0.1.1.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
agent_lock
|