agent-fileview 0.1.0__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.
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-fileview
3
+ Version: 0.1.0
4
+ Summary: Read-only file viewer that turns a coding agent's file paths into links you can open on your phone over Tailscale
5
+ Project-URL: Repository, https://github.com/tbb300/agent-fileview
6
+ Project-URL: Issues, https://github.com/tbb300/agent-fileview/issues
7
+ Project-URL: Changelog, https://github.com/tbb300/agent-fileview/blob/main/CHANGELOG.md
8
+ Author: Tom Barrett
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,claude code,codex,file viewer,tailscale
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: markdown>=3.5
23
+ Requires-Dist: pygments>=2.17
24
+ Description-Content-Type: text/markdown
25
+
26
+ # agent-fileview
27
+
28
+ Coding agents like Claude Code and Codex mention files all the time:
29
+ `src/app.py:42`, `reports/summary.md`, `logs/run.log`. When the agent runs
30
+ on a remote Linux box and you are reading its output on your phone, those
31
+ paths are dead text.
32
+
33
+ agent-fileview turns them into links. It is a small read-only web server
34
+ on the box, reachable only over your Tailscale tailnet. A file's URL is the
35
+ tailnet name plus the file's absolute path:
36
+
37
+ ```
38
+ https://box.your-tailnet.ts.net:8780/home/you/project/src/app.py#L42
39
+ ```
40
+
41
+ Tap it and the file opens on your phone. Markdown is rendered, code is
42
+ highlighted with numbered, linkable lines, and notebooks, CSV and logs get
43
+ their own views. You then add a short rule to `CLAUDE.md` or `AGENTS.md`,
44
+ and the agents start writing these links themselves.
45
+
46
+ ## Who this is for
47
+
48
+ You run coding agents such as Claude Code, Codex, OpenCode or Aider on a
49
+ Linux box: a home server, a cloud VM or a GPU workstation. They work
50
+ unattended or half-attended, often several at once. You watch and steer them
51
+ from somewhere else. That might be Claude Code's remote control in the
52
+ Claude mobile or web app, a tmux or screen session over SSH or mosh from a
53
+ phone client such as Termius, Blink or JuiceSSH, or an orchestrator that
54
+ runs agents in tmux panes.
55
+
56
+ Those views show the agent's text but not the files it names. Its reports,
57
+ plans, diffs, logs and notebooks stay on the box, and the paths in the
58
+ transcript are dead text. The Claude app even turns a `file:line` path into
59
+ a claude.ai link that does not open the file. agent-fileview serves those
60
+ files read-only over your tailnet, so each path can become a link that opens
61
+ on your phone. One rule in `CLAUDE.md` or `AGENTS.md` makes agents write
62
+ plain markdown links and never wrap them in backticks, so the mobile apps
63
+ make them tappable.
64
+
65
+ ## Quickstart
66
+
67
+ ```bash
68
+ uv tool install agent-fileview # or: pipx install agent-fileview
69
+ agent-fileview install # user service + tailscale serve
70
+ agent-fileview rule >> ~/.claude/CLAUDE.md
71
+ ```
72
+
73
+ To try it without installing, `uvx agent-fileview link .` prints a URL. The
74
+ service needs a lasting install, because `uvx` environments live in a cache
75
+ that can be cleaned.
76
+
77
+ The package installs two commands: `agent-fileview`, and `fileview` as a
78
+ short alias. The examples below use the alias.
79
+
80
+ `agent-fileview install` does the following:
81
+
82
+ 1. writes `~/.config/fileview/config.toml` (mode 0600) if it is missing;
83
+ 2. writes (mode 0644), enables and restarts the systemd user unit
84
+ `agent-fileview.service`. An existing `fileview.service` from an earlier
85
+ install keeps its name;
86
+ 3. runs `tailscale serve --bg --https=8780 unix:<socket>`. If tailscale
87
+ refuses on permissions, it asks before retrying with `sudo`. If the port
88
+ already serves something else, it stops unless you pass `--force`;
89
+ 4. prints the tailnet logins it can see and suggests `allowed_logins`;
90
+ 5. prints the agent rule.
91
+
92
+ If lingering is off, it prints the `loginctl enable-linger` command that
93
+ keeps the service running after you log out. To let `tailscale serve` run
94
+ without sudo, run `sudo tailscale set --operator=$USER` once.
95
+
96
+ To link a file yourself, run `fileview link path/to/file --line 42`.
97
+ `fileview uninstall` reverses `install` and keeps your config.
98
+
99
+ ## Commands
100
+
101
+ | Command | What it does |
102
+ |---|---|
103
+ | `fileview serve` | Runs the server on the unix socket. The unit runs this. `--tcp PORT` listens on 127.0.0.1 instead, for development. |
104
+ | `fileview install` | Installs, enables and restarts the user unit, checks linger and exposes the socket with `tailscale serve`. `--yes` uses sudo without asking. `--no-tailscale` skips tailscale. `--force` replaces another route on the port. |
105
+ | `fileview uninstall` | Removes the unit, and the `tailscale serve` route if it points at fileview (`--force` removes any route on the port). |
106
+ | `fileview link PATH...` | Prints each path's URL. `--line N` adds `#LN` (files only). It warns when the server would refuse a path, and exits 1 if any path is missing or would be refused. |
107
+ | `fileview rule` | Prints the CLAUDE.md / AGENTS.md snippet for your config, listing only roots that pass the checks. |
108
+
109
+ ## Configuration
110
+
111
+ `~/.config/fileview/config.toml`:
112
+
113
+ ```toml
114
+ host = "box.your-tailnet.ts.net" # default: Self.DNSName from `tailscale status --json`
115
+ port = 8780 # HTTPS port tailscale serves on
116
+ roots = ["~"] # directories or globs to serve
117
+ allow = [ # dot-paths exempt from the dot rule
118
+ "~/.claude/CLAUDE.md",
119
+ "~/.claude/projects/*/memory/",
120
+ "~/.codex/AGENTS.md",
121
+ ]
122
+ # socket = "/run/user/1000/fileview/fileview.sock"
123
+ # timezone = "Europe/London" # listing times; default local
124
+ # allowed_logins = ["you@example.com"] # see "Who can reach it"
125
+ ```
126
+
127
+ Environment variables override the file: `FILEVIEW_HOST`,
128
+ `FILEVIEW_PORT`, `FILEVIEW_ROOTS` and `FILEVIEW_SOCKET` replace a value,
129
+ `FILEVIEW_ALLOW` appends to the allowlist, and `FILEVIEW_TZ` sets the
130
+ timezone. List values are colon-separated. `FILEVIEW_CONFIG` points at a
131
+ different file. Restart the service after editing:
132
+ `systemctl --user restart fileview`.
133
+
134
+ The config file must be owned by you and not group- or other-writable.
135
+ Roots, allow globs and the socket must be absolute after `~` and `$VAR`
136
+ expansion, and an unset variable is an error.
137
+
138
+ Allow entries exist only to exempt specific dot-paths, so each entry must
139
+ follow these rules:
140
+
141
+ - It names at least one dot-directory literally, such as `.claude`.
142
+ - Every component down to the last literal dot-directory is literal.
143
+ - Below that, a wildcard component is either a bare `*` or starts with a
144
+ literal non-dot character, such as `p*`.
145
+ - Something literal follows the last dot-directory, so the entry cannot
146
+ cover a whole dot-directory.
147
+
148
+ `~/.claude/CLAUDE.md`, `~/.claude/projects/*/memory/` and
149
+ `~/.config/systemd/user/` pass. `~/.claude/`, `~/.claude/*`, `~/*/`,
150
+ `~/?ssh/k/` and `~/[.]ssh/k/` are refused. The server also never lets a
151
+ wildcard match a dot name, so `*` in `projects/*/memory/` cannot reach
152
+ `projects/.hidden/memory/`.
153
+
154
+ A root may be a glob such as `/srv/data/*`, and each match is vetted on its
155
+ own. A glob helps when a shared parent directory fails the ownership rule
156
+ below but your directories inside it pass.
157
+
158
+ ## Security model
159
+
160
+ agent-fileview shows files from a box that holds source code and
161
+ credentials, so it refuses by default. Read the first two points before you
162
+ install it.
163
+
164
+ - **Who can reach it.** `tailscale serve` does no per-user authentication.
165
+ Every device that your tailnet ACLs let reach this machine on the port can
166
+ read everything under the roots. Restrict it with tailnet ACLs, or set
167
+ `allowed_logins`. With that list set, each request must carry a
168
+ `Tailscale-User-Login` header naming a listed login, or it gets 403.
169
+ Tailscale sets that header for devices owned by a user. Tagged devices
170
+ and local clients send none, so they are refused once the list is set.
171
+ Logins compare case-insensitively.
172
+ - **Name rules are a heuristic.** fileview denies dot-paths and names that
173
+ look like credentials, but a secret in `notes.md` or `config.yaml` under a
174
+ root is readable. Choose roots that hold only what you would show on a
175
+ phone.
176
+ - **Socket.** The server listens only on a unix socket at
177
+ `$XDG_RUNTIME_DIR/fileview/fileview.sock`, mode 0600, in a 0700
178
+ directory. Other local users cannot connect. `tailscaled` runs as root
179
+ and proxies HTTPS to it. A process running as you can open the socket and
180
+ send any `Tailscale-User-Login` header it likes. That grants nothing new,
181
+ because such a process can already read your files.
182
+ - **Read-only.** GET and HEAD only, and every other method gets 405. The
183
+ unit runs with `NoNewPrivileges`, `ProtectSystem=strict` and a read-only
184
+ view of `/`.
185
+ - **Checked opens.** Every file is opened with `O_NOFOLLOW | O_NONBLOCK`,
186
+ and its real path is read back from `/proc/self/fd`. The requested path
187
+ and the real path must both pass every rule, and bytes come only from that
188
+ checked fd. Swapping in a symlink after the check does not redirect the
189
+ read. Only regular files and directories are served.
190
+ - **Roots.** Every request re-checks each root. A root must be a real
191
+ directory (not a symlink), owned by you, with no group or other write
192
+ bit, and with the inode it had when first accepted. A failing root is
193
+ skipped with one log line, and the others keep working.
194
+ - **No JavaScript.** Every response carries
195
+ `Content-Security-Policy: default-src 'none'; img-src 'self' data:;
196
+ style-src 'unsafe-inline'; form-action 'none'; base-uri 'self';
197
+ frame-ancestors 'none'`, plus `X-Content-Type-Options: nosniff` and
198
+ `Referrer-Policy: no-referrer`. `.html` files are served as text. SVG is
199
+ served raw under the same CSP, so scripts inside it do not run.
200
+ - **Escaping.** File names, paths and contents are escaped everywhere.
201
+ Raw HTML in markdown is escaped. Links in markdown keep only http,
202
+ https, mailto and relative URLs, checked after decoding HTML entities.
203
+ - **Bounded work.** Markdown renders in a child process limited to 2 s of
204
+ CPU, 1 GiB of memory and 3 s of wall time, and the child is killed if the
205
+ client disconnects. Idle connections close after 30 s.
206
+
207
+ ### What it refuses
208
+
209
+ Every refusal is a plain 404, so a probe cannot tell "denied" from
210
+ "missing".
211
+
212
+ - Paths outside the roots, and any `.` or `..` segment.
213
+ - Any path component starting with `.` (`.ssh`, `.aws`, `.env`, `.git`,
214
+ ...), except paths in the allowlist. Dot-components below an allowlisted
215
+ directory are still refused.
216
+ - These files, even inside the allowlist: `auth.json`, `.credentials.json`,
217
+ `credentials*`, `hosts.yml`, `.netrc`, `.pgpass`, `history.jsonl`,
218
+ `.docker/config.json`, `.kube/config`, `settings.json` and
219
+ `settings.local.json` under `.claude/`, and `*.jsonl` session transcripts
220
+ under `.claude/projects/`.
221
+ - Directories named exactly `secret(s)`, `token(s)`, `credential(s)` or
222
+ `password(s)`, case-insensitively. `tokenizer/` passes.
223
+ - Files matching `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_*`,
224
+ `*credential*`, `*secret*`, `*token*`, `*password*` or `*.kdbx`,
225
+ case-insensitively.
226
+ - Extensions outside: md txt log json jsonl csv tsv py pyx js ts c h cpp
227
+ toml yaml yml sql sh html service lock ipynb cfg ini example svg png jpg
228
+ jpeg gif webp pdf. Databases, parquet, pickles and other binary data are
229
+ never served.
230
+ - Extensionless files unless they start with `#!` and are valid UTF-8.
231
+ - Text over 20 MB and binaries over 50 MB.
232
+
233
+ ## What you see
234
+
235
+ | Type | View |
236
+ |---|---|
237
+ | `.md` | Rendered, up to 1 MiB. Relative links and images work. |
238
+ | Code and text | Highlighted with numbered lines. Each line has an `#L<n>` anchor, and the gutter is not copied with the code. `.py` files get an outline. Files over 2 MiB are not highlighted. |
239
+ | `.ipynb` | Cells in order. Markdown is rendered, code highlighted, text outputs shown and image outputs omitted. |
240
+ | `.csv`, `.tsv` | A table of the first 2,000 rows. |
241
+ | `.log` | The last 5,000 lines, numbered as in the file. |
242
+ | Images, SVG, PDF | Served raw. |
243
+ | Directories | Newest first, with sizes and times. |
244
+
245
+ `?raw=1` returns a text file as `text/plain`. Pages are dark, and
246
+ `?theme=light` switches one page to light. Range anchors such as `#L10-L20`
247
+ are not supported, because highlighting a range needs JavaScript.
248
+
249
+ ## Screenshots
250
+
251
+ <!-- Placeholders: add images under docs/screenshots/ before publishing. -->
252
+
253
+ | Listing | Markdown | Code | Notebook |
254
+ |---|---|---|---|
255
+ | *(screenshot)* | *(screenshot)* | *(screenshot)* | *(screenshot)* |
256
+
257
+ ## Limits
258
+
259
+ - Linux only. It needs `/proc/self/fd`, systemd user units and Tailscale
260
+ with `tailscale serve`.
261
+ - Python 3.11 or newer.
262
+ - One user per service. Each user who wants links runs their own instance
263
+ on their own port.
264
+ - No search, no editing and no uploads, by design.
265
+
266
+ ## Development
267
+
268
+ ```bash
269
+ uv sync
270
+ uv run ruff check
271
+ uv run pytest -q
272
+ uv run fileview serve --tcp 8141 # then open http://127.0.0.1:8141/
273
+ ```
274
+
275
+ ## Releasing
276
+
277
+ Releases go to PyPI as `agent-fileview` through Trusted Publishing, so no
278
+ API token exists anywhere.
279
+
280
+ One-time setup:
281
+
282
+ 1. On PyPI, add a pending trusted publisher for project `agent-fileview`
283
+ with owner `tbb300`, repository `agent-fileview`, workflow
284
+ `release.yml` and environment `pypi`.
285
+ 2. On GitHub, create the environment `pypi` under Settings → Environments.
286
+ Add yourself as a required reviewer, so each release waits for approval.
287
+
288
+ For each release:
289
+
290
+ 1. Set `version` in `pyproject.toml`, the only version source, and add a
291
+ CHANGELOG entry.
292
+ 2. Check locally with `uv build` and `uvx twine check --strict dist/*`.
293
+ 3. Commit, then tag and push: `git tag v0.1.1 && git push origin v0.1.1`.
294
+
295
+ The `Release` workflow runs ruff and the tests, checks that the tag equals
296
+ `v` plus the pyproject version, builds, and runs `twine check`. A separate
297
+ `publish` job then uploads through OIDC. Only that job holds
298
+ `id-token: write`.
299
+
300
+ ## License
301
+
302
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ fileview/__init__.py,sha256=50rboCPRrYdla8v3cd8CrxSJ4lSk14hKJjdi1WU_gyo,317
2
+ fileview/__main__.py,sha256=x_P85HydUuzwgXFjW_d7zIkzgfz1jBGzyrdIrqaU5Jo,56
3
+ fileview/cli.py,sha256=2vj_aHMcu6mRsodGuPybftRA7uNUOPdQrDNyoNBjy54,15137
4
+ fileview/config.py,sha256=hs3TWIuDScS6lHyIXeKfqXGqDyC4brHAkaZIOs8edv0,10660
5
+ fileview/server.py,sha256=NzzOxlq3bHx495fgl5AnTcar4qtzuuIFfXpTLByJfag,43473
6
+ agent_fileview-0.1.0.dist-info/METADATA,sha256=O7fknPhor9Dv3OoL7ROUhxserbHOUH0uitbo3FW9_F4,14122
7
+ agent_fileview-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ agent_fileview-0.1.0.dist-info/entry_points.txt,sha256=czrwWg2N7d3MwVlJ7gU4ZqVjKiI8sRo36ccB4wyufVY,82
9
+ agent_fileview-0.1.0.dist-info/licenses/LICENSE,sha256=8R5sjHvSIabREa9W5Zuskddwbx0l0UUW3IsRIg1OO0I,1068
10
+ agent_fileview-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ agent-fileview = fileview.cli:main
3
+ fileview = fileview.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Barrett
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.
fileview/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Read-only file viewer for agents' links, served over Tailscale."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ # pyproject.toml is the one version source; installs record it in metadata.
6
+ try:
7
+ __version__ = version("agent-fileview")
8
+ except PackageNotFoundError:
9
+ __version__ = "0+unknown"
fileview/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from fileview.cli import main
2
+
3
+ raise SystemExit(main())