@openafw/openafw 0.5.2
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.
- package/LICENSE +21 -0
- package/PRIVACY.md +377 -0
- package/README.md +152 -0
- package/dist/backends-Byh5VYtT.js +418 -0
- package/dist/bin/afw.js +45270 -0
- package/dist/bin/tap.js +83 -0
- package/dist/bin/tools.js +432 -0
- package/dist/secrets-9JqUBHyw.js +3 -0
- package/dist/secrets-Bj-gyv53.js +169 -0
- package/package.json +62 -0
- package/ui-dist/assets/index-BY6COSYk.css +1 -0
- package/ui-dist/assets/index-C9yCeZlD.js +20 -0
- package/ui-dist/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 afw 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.
|
package/PRIVACY.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Privacy & data handling
|
|
2
|
+
|
|
3
|
+
afw is local-first. Everything you capture stays on your own
|
|
4
|
+
machine under `~/.afw/`. The published package does **not** collect
|
|
5
|
+
telemetry, does **not** report crashes, and does **not** send any data
|
|
6
|
+
**about you or your usage** anywhere. There is no `openafw.com` /
|
|
7
|
+
afw / Anthropic host wired into the package.
|
|
8
|
+
|
|
9
|
+
From v0.4 the daemon makes one outbound request of its own: a daily
|
|
10
|
+
**version check** against the public npm registry — see the v0.4
|
|
11
|
+
section. It carries no user data, contacts only the registry npm
|
|
12
|
+
itself uses, and can be switched off. From v0.5 an **opt-in** daily
|
|
13
|
+
**pricing-catalog refresh** (off by default) can fetch a public price
|
|
14
|
+
list from models.dev — see the v0.5 section.
|
|
15
|
+
|
|
16
|
+
From v1, when you route an agent onto a managed **subscription**
|
|
17
|
+
provider, afw may refresh that subscription's OAuth token against
|
|
18
|
+
the agent's own identity provider — see the v1 section. It is
|
|
19
|
+
request-driven, stays within the agent's existing upstream, sends only
|
|
20
|
+
the token the agent already holds, and never contacts us.
|
|
21
|
+
|
|
22
|
+
This file is updated **every release** with the exact behaviour of
|
|
23
|
+
the version it ships with. If a future release sends any data **about
|
|
24
|
+
you or your usage** anywhere, it will be:
|
|
25
|
+
1. Strictly opt-in.
|
|
26
|
+
2. Documented here under that version's section with what is sent,
|
|
27
|
+
to where, and why.
|
|
28
|
+
3. Documented in the release notes.
|
|
29
|
+
|
|
30
|
+
(A version check against the public package registry is not in that
|
|
31
|
+
category — it reveals nothing about you — but it is documented in full
|
|
32
|
+
below and is disableable all the same.)
|
|
33
|
+
|
|
34
|
+
Audit the claims yourself by reading the source — `git grep https://`
|
|
35
|
+
finds every URL the package can talk to.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## v1
|
|
40
|
+
|
|
41
|
+
v1 adds **model routing & combos** (`afw route` and the Control ·
|
|
42
|
+
Routing pages) — you can reconfigure which model(s) a wired agent's
|
|
43
|
+
traffic reaches. This changes where your agent traffic goes, so here is
|
|
44
|
+
exactly what it does.
|
|
45
|
+
|
|
46
|
+
### Where your traffic goes
|
|
47
|
+
|
|
48
|
+
By default every route is plain **passthrough**: traffic reaches the
|
|
49
|
+
same upstream your agent already chose, unchanged. Nothing about routing
|
|
50
|
+
takes effect until you explicitly opt a route in.
|
|
51
|
+
|
|
52
|
+
When you do route an agent to a different model or a combo, its requests
|
|
53
|
+
go to the upstream **you configured for that model's provider** — a URL
|
|
54
|
+
you typed in yourself, via the UI or `afw route provider add`. afw
|
|
55
|
+
still opens no outbound surface of its own: it only ever talks to
|
|
56
|
+
upstreams you named. There is no afw/Anthropic host in this path.
|
|
57
|
+
|
|
58
|
+
### `secrets.json` — your API keys
|
|
59
|
+
|
|
60
|
+
Cross-agent routing and the vision companion may need API keys for
|
|
61
|
+
providers your agent never authenticates to itself. Those keys are
|
|
62
|
+
stored **locally** in `~/.afw/secrets.json`, written with file mode
|
|
63
|
+
`0600` (readable only by your user account).
|
|
64
|
+
|
|
65
|
+
* The keys are sent **only** to the upstream URLs you configured for
|
|
66
|
+
their providers — nowhere else, never to us.
|
|
67
|
+
* The secret store is **write-only** across the CLI and the daemon API:
|
|
68
|
+
you can set and remove keys, but a value can never be read back out.
|
|
69
|
+
The UI shows only which key *refs* exist, never their contents.
|
|
70
|
+
* Removing a provider removes its stored key.
|
|
71
|
+
|
|
72
|
+
### Captured credentials & subscription token refresh
|
|
73
|
+
|
|
74
|
+
So one agent can use another agent's model, afw manages every
|
|
75
|
+
agent's credentials itself. `afw wire` reads each agent's existing
|
|
76
|
+
credential from its **own config** and handles it one of two ways.
|
|
77
|
+
|
|
78
|
+
**Static API keys** — Claude Code's `ANTHROPIC_API_KEY`, Codex's
|
|
79
|
+
`OPENAI_API_KEY`, Hermes / OpenClaw provider keys — are copied into
|
|
80
|
+
`~/.afw/secrets.json` (mode `0600`, see above) under a
|
|
81
|
+
`provider:<agent>/<provider>` ref. They are read from files already on
|
|
82
|
+
your machine and only ever sent to the same upstream that agent
|
|
83
|
+
already calls.
|
|
84
|
+
|
|
85
|
+
**Subscription logins** — Claude Code's Claude.ai login, Codex's
|
|
86
|
+
ChatGPT login — use OAuth tokens that expire. afw does **not** copy
|
|
87
|
+
these into `secrets.json`. It reads them, only when a managed route
|
|
88
|
+
needs them, from the agent's own credential store: the macOS Keychain
|
|
89
|
+
item `Claude Code-credentials` (fallback `~/.claude/.credentials.json`)
|
|
90
|
+
and `~/.codex/auth.json`.
|
|
91
|
+
|
|
92
|
+
#### The refresh call
|
|
93
|
+
|
|
94
|
+
A subscription access token expires. When a managed subscription route
|
|
95
|
+
is used and the token is within five minutes of expiry, afw
|
|
96
|
+
refreshes it:
|
|
97
|
+
|
|
98
|
+
* **Request:** an HTTPS `POST` to the agent's **own** OAuth provider —
|
|
99
|
+
`https://platform.claude.com/v1/oauth/token` for Claude Code,
|
|
100
|
+
`https://auth.openai.com/oauth/token` for Codex. These are the exact
|
|
101
|
+
endpoints the agent itself calls to refresh the exact same token.
|
|
102
|
+
* **What it sends:** your `refresh_token` and the agent's public
|
|
103
|
+
`client_id`. Nothing else — no machine ID, no usage data, nothing
|
|
104
|
+
identifying you beyond the token the agent already holds.
|
|
105
|
+
* **Where it does NOT go:** not `openafw.com`, not any afw server.
|
|
106
|
+
We never see this request.
|
|
107
|
+
* **When:** only when you have opted a route onto a managed
|
|
108
|
+
subscription provider *and* its token is near expiry. A passthrough
|
|
109
|
+
route never triggers it.
|
|
110
|
+
|
|
111
|
+
#### Rotated tokens are written back
|
|
112
|
+
|
|
113
|
+
OAuth refresh tokens are single-use — a refresh returns a new one.
|
|
114
|
+
afw writes the rotated token back to the agent's own store
|
|
115
|
+
(Keychain / `auth.json`, atomically) so the agent picks it up on its
|
|
116
|
+
next read instead of failing. afw is a **cooperative co-refresher**
|
|
117
|
+
of the same credential, not a competing one.
|
|
118
|
+
|
|
119
|
+
This stays within afw's contract — data about you goes only to the
|
|
120
|
+
upstream your agent already talks to, and an agent's OAuth provider is
|
|
121
|
+
part of that upstream — but it is called out here, and in the release
|
|
122
|
+
notes, all the same.
|
|
123
|
+
|
|
124
|
+
#### Request shape on managed subscription routes
|
|
125
|
+
|
|
126
|
+
Anthropic gates a Claude.ai (subscription) OAuth token behind specific
|
|
127
|
+
request-shape requirements — the token is accepted only when the
|
|
128
|
+
request claims **Claude Code shape**. When afw routes a call onto a
|
|
129
|
+
managed Claude.ai subscription provider, it adjusts the outgoing
|
|
130
|
+
request to satisfy those gates:
|
|
131
|
+
|
|
132
|
+
* Two `anthropic-beta` flags on the request:
|
|
133
|
+
`claude-code-20250219` and `oauth-2025-04-20`.
|
|
134
|
+
* A fixed Claude-Code identity prepended to the request's `system`
|
|
135
|
+
block: *"You are Claude Code, Anthropic's official CLI for Claude."*
|
|
136
|
+
Your agent's own system prompt is preserved after it.
|
|
137
|
+
|
|
138
|
+
This is a **protocol-level concession** to the upstream — the same
|
|
139
|
+
shape Claude Code itself sends on every call. No new data about you
|
|
140
|
+
leaves the machine, and no new outbound surface is opened. Without
|
|
141
|
+
these adjustments, Anthropic rejects subscription tokens used outside
|
|
142
|
+
Claude Code with `4xx` errors or aggressive throttling. Disclosed here
|
|
143
|
+
for the same reason the refresh call is: afw is shaping a request
|
|
144
|
+
on your behalf, and you should know what shape it takes.
|
|
145
|
+
|
|
146
|
+
### New files on disk
|
|
147
|
+
|
|
148
|
+
| Path | What |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `~/.afw/models.json` | The catalog of providers and models afw can route to. Provider base URLs, wire formats, model metadata. No secrets — keys live in `secrets.json`. Seeded from your wire and extended by you. |
|
|
151
|
+
| `~/.afw/routing.json` | Per-agent routing decisions and combo definitions. No secrets. |
|
|
152
|
+
| `~/.afw/secrets.json` | Provider API keys — typed in by you or captured from an agent's own config by `afw wire`. File mode `0600`. Local-only; see above. |
|
|
153
|
+
| `~/.afw/oauth-<agent>.lock` | Transient lock held only while afw refreshes a subscription OAuth token, deleted immediately after. No contents. |
|
|
154
|
+
|
|
155
|
+
These are local files. Routing uploads nothing; the only outbound call
|
|
156
|
+
this version adds is the subscription token refresh described above.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## v0.5
|
|
161
|
+
|
|
162
|
+
v0.5 adds an **opt-in pricing-catalog refresh**. By default afw prices
|
|
163
|
+
calls from the catalog bundled inside the package (and your own
|
|
164
|
+
`~/.afw/pricing.json` overrides) — entirely offline. If you want prices
|
|
165
|
+
to stay fresh without upgrading afw, you can turn on a daily refresh.
|
|
166
|
+
|
|
167
|
+
### The pricing refresh
|
|
168
|
+
|
|
169
|
+
* **Off by default.** Enable with `autoRefreshPricing: true` in
|
|
170
|
+
`~/.afw/config.json`. With it off (the default) the daemon makes no
|
|
171
|
+
pricing requests at all.
|
|
172
|
+
* **Request:** when on, one plain HTTPS `GET` per day to
|
|
173
|
+
`https://models.dev/api.json` — a public, community-maintained price
|
|
174
|
+
list. The same source the bundled catalog is built from.
|
|
175
|
+
* **What it sends:** nothing about you. No user data, no machine ID, no
|
|
176
|
+
model ids you've used, no usage data — just an anonymous GET for the
|
|
177
|
+
public catalog.
|
|
178
|
+
* **Where it does NOT go:** not `openafw.com`, not any afw or
|
|
179
|
+
Anthropic server, never your captured traffic. We never see this request.
|
|
180
|
+
* The result is cached at `~/.afw/pricing-catalog.json` and read locally;
|
|
181
|
+
nothing leaves your machine.
|
|
182
|
+
|
|
183
|
+
This is the second sanctioned outbound call (after the v0.4 version check),
|
|
184
|
+
and like that one it is opt-in here, hits only a public third-party
|
|
185
|
+
endpoint, and is fully disableable.
|
|
186
|
+
|
|
187
|
+
### New files on disk
|
|
188
|
+
|
|
189
|
+
| Path | What |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `~/.afw/pricing-catalog.json` | Cached price catalog from the last refresh (only when `autoRefreshPricing` is on). |
|
|
192
|
+
|
|
193
|
+
### Session correlation (opt-in, local-only)
|
|
194
|
+
|
|
195
|
+
To attribute captured Claude Code calls to the specific window (instance) and
|
|
196
|
+
parallel sub-agent that made them — fleet management — afw can correlate
|
|
197
|
+
captures with Claude Code's own local transcripts.
|
|
198
|
+
|
|
199
|
+
* **Off by default.** Enable with `correlateSessions: true` in
|
|
200
|
+
`~/.afw/config.json`.
|
|
201
|
+
* **What it reads:** `~/.claude/projects/**/*.jsonl` — Claude Code's session
|
|
202
|
+
transcripts, already on your disk. It joins each captured call to its session
|
|
203
|
+
on the upstream `message.id` (which afw already has in the stored
|
|
204
|
+
response) and records the session id + sub-agent id locally.
|
|
205
|
+
* **What it sends:** nothing. No network calls at all. This is a local file
|
|
206
|
+
read; results are written only to your local trace DB. It never contacts
|
|
207
|
+
`openafw.com`, Anthropic, or any host.
|
|
208
|
+
* Reads stop working gracefully when the files are absent (e.g. the daemon and
|
|
209
|
+
agent run on different machines) — calls simply stay unattributed.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## v0.4
|
|
214
|
+
|
|
215
|
+
v0.4 adds the **update system** — `afw update`, `afw rollback`,
|
|
216
|
+
the dashboard update banner, and optional auto-update. This is the
|
|
217
|
+
first afw behaviour that contacts a host other than your agent's
|
|
218
|
+
own upstream. Here is exactly what it does.
|
|
219
|
+
|
|
220
|
+
### The version check
|
|
221
|
+
|
|
222
|
+
The daemon checks once a day whether a newer afw has been
|
|
223
|
+
published:
|
|
224
|
+
|
|
225
|
+
* **Request:** a plain HTTPS `GET` to
|
|
226
|
+
`https://registry.npmjs.org/@openafw%2Fopenafw/latest` — the public
|
|
227
|
+
npm registry, the same one `npm install` used to put afw on your
|
|
228
|
+
machine.
|
|
229
|
+
* **What it sends:** the package name, in the URL. Nothing else. No
|
|
230
|
+
user data, no machine ID, no install ID, no usage data, no header
|
|
231
|
+
that identifies you. The registry sees an anonymous GET, exactly
|
|
232
|
+
like `npm view @openafw/openafw version`.
|
|
233
|
+
* **Where it does NOT go:** not `openafw.com`, not any afw or
|
|
234
|
+
Anthropic server. We never see this request.
|
|
235
|
+
* **On by default**, because it carries nothing about you. Turn it
|
|
236
|
+
off completely with `updateCheck: false` in `~/.afw/config.json`
|
|
237
|
+
— the daemon then makes no version checks at all.
|
|
238
|
+
* The result is cached in `~/.afw/update.json` and never leaves
|
|
239
|
+
your machine.
|
|
240
|
+
|
|
241
|
+
### Installing an update
|
|
242
|
+
|
|
243
|
+
`afw update`, or "Update now" in the dashboard, runs
|
|
244
|
+
`npm install -g @openafw/openafw@<version>` — exactly what you would
|
|
245
|
+
run by hand. Before installing, afw snapshots your trace database
|
|
246
|
+
into `~/.afw/backups/`. After installing it restarts the daemon
|
|
247
|
+
(waiting for a quiet moment so no in-flight agent call is dropped) and
|
|
248
|
+
health-checks the new version; if the new version does not come up
|
|
249
|
+
healthy it automatically reinstalls the previous one and restores the
|
|
250
|
+
database snapshot. None of this uploads anything.
|
|
251
|
+
|
|
252
|
+
### Auto-update
|
|
253
|
+
|
|
254
|
+
Off until you turn it on. After your first manual update, afw asks
|
|
255
|
+
once whether to auto-update future releases. If you say yes
|
|
256
|
+
(`autoUpdate: true` in `~/.afw/config.json`), the daily check will
|
|
257
|
+
install a new version on its own — still backed up, still
|
|
258
|
+
health-gated, still auto-rolled-back on failure. Say no and updates
|
|
259
|
+
stay manual.
|
|
260
|
+
|
|
261
|
+
### New files on disk
|
|
262
|
+
|
|
263
|
+
| Path | What |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `~/.afw/config.json` | Your preferences: `updateCheck`, `autoUpdate`, `autoUpdateAsked`. |
|
|
266
|
+
| `~/.afw/update.json` | Cached result of the last version check. |
|
|
267
|
+
| `~/.afw/update-progress.json` | State of the current / last update, for the CLI and dashboard. |
|
|
268
|
+
| `~/.afw/backups/db-pre-<version>-<ts>.sqlite` | Database snapshots taken before each update so a rollback can restore them. The most recent 3 are kept. |
|
|
269
|
+
| `~/.afw/pending-db-restore` | Transient marker written during a rollback; consumed and deleted at the next daemon boot. |
|
|
270
|
+
|
|
271
|
+
All of these are local files. Nothing in the update system uploads
|
|
272
|
+
anything.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## v0.1
|
|
277
|
+
|
|
278
|
+
### What afw writes to your disk
|
|
279
|
+
|
|
280
|
+
| Path | What |
|
|
281
|
+
|---|---|
|
|
282
|
+
| `~/.afw/wire/routes.json` | Mapping from `<agent>/<provider>` route keys to upstream URLs, written by `afw wire`. |
|
|
283
|
+
| `~/.afw/wire/traces/traces.db` | SQLite database of every captured action — model calls (prompts, responses, tool_use blocks, tokens, cost), MCP frames (server, method, params/result), durations. |
|
|
284
|
+
| `~/.afw/backups/manifest.json` + `~/.afw/backups/<timestamp>/<agent>/<file>` | Byte-exact copies of each agent config afw rewrote, plus a manifest with `sha256` checksums of before and after. `afw unwire` uses these to restore. |
|
|
285
|
+
| `~/.afw/logs/daemon.log`, `daemon.err` | Daemon stdout / stderr — info lines for each captured request, errors for parse failures. Plain text. |
|
|
286
|
+
| `~/.afw/wire/daemon.sock` (planned) | Unix-domain socket for CLI ↔ daemon. Not yet used in v0.1. |
|
|
287
|
+
|
|
288
|
+
### What afw sends over the network
|
|
289
|
+
|
|
290
|
+
**One pattern only:** the HTTP proxy at `http://localhost:9877/wire/...`
|
|
291
|
+
forwards your agent's outbound request to whatever upstream is
|
|
292
|
+
registered in `routes.json`. Concretely:
|
|
293
|
+
|
|
294
|
+
* If your agent calls `api.anthropic.com`, afw relays the same
|
|
295
|
+
bytes to `api.anthropic.com`.
|
|
296
|
+
* If your agent calls `api.openai.com`, afw relays to `api.openai.com`.
|
|
297
|
+
* If your agent calls some internal/private endpoint, afw relays
|
|
298
|
+
there.
|
|
299
|
+
|
|
300
|
+
The bytes are unchanged — same body, same auth headers. afw
|
|
301
|
+
**also** keeps a normalized copy of the request and response on your
|
|
302
|
+
disk for `afw list / show / report`.
|
|
303
|
+
|
|
304
|
+
**Where afw does NOT send data:**
|
|
305
|
+
|
|
306
|
+
* No `openafw.com` host. No `*.openafw.com` calls anywhere in
|
|
307
|
+
the source. Grep `packages/afw/src/` to confirm.
|
|
308
|
+
* No telemetry endpoint, anonymous or otherwise.
|
|
309
|
+
* No auto-update check (`npm update -g @openafw/openafw` is something
|
|
310
|
+
you run manually).
|
|
311
|
+
* No error / crash reporting.
|
|
312
|
+
* No license-check ping (no license system exists in v0.1).
|
|
313
|
+
|
|
314
|
+
### What other tools can read
|
|
315
|
+
|
|
316
|
+
* `afw report <run-id>` produces markdown or JSON to stdout or a
|
|
317
|
+
file you choose. Nothing is uploaded; you decide where the output
|
|
318
|
+
goes. Pass `--redact` to mask common credential shapes (API keys,
|
|
319
|
+
bearer tokens, GitHub PATs, AWS keys, Google keys, Slack tokens)
|
|
320
|
+
before sharing.
|
|
321
|
+
* `afw list --json` and the REST API at `/api/runs` are served on
|
|
322
|
+
`localhost:9877` only. Not bound to external interfaces. Anyone
|
|
323
|
+
with access to your user account on this machine can read them.
|
|
324
|
+
|
|
325
|
+
### What's intentionally not in v0.1
|
|
326
|
+
|
|
327
|
+
* Any form of telemetry, opt-in or otherwise.
|
|
328
|
+
* Crash reporting.
|
|
329
|
+
* Auto-update checks.
|
|
330
|
+
* License validation.
|
|
331
|
+
* Cloud sync.
|
|
332
|
+
* Any cryptographic identifiers (machine IDs, install IDs, etc.).
|
|
333
|
+
|
|
334
|
+
### Source-code receipts
|
|
335
|
+
|
|
336
|
+
Confirm the above by reading the package source. Two greps cover
|
|
337
|
+
nearly the entire surface:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# Every URL constant or template literal that starts with http
|
|
341
|
+
git grep -E "https?://" packages/afw/src/ | grep -v "node:" | grep -v "//---"
|
|
342
|
+
|
|
343
|
+
# Every call to fetch(), which is how outbound HTTP happens in Node
|
|
344
|
+
git grep -n "fetch(" packages/afw/src/
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
What you should find:
|
|
348
|
+
* The proxy forwards to upstream URLs read from `routes.json`.
|
|
349
|
+
* `afw-tap` POSTs frames to `http://localhost:9877/api/tap/frame`
|
|
350
|
+
(the local daemon).
|
|
351
|
+
* `afw status` and `afw ui` GET `http://localhost:9877/health`
|
|
352
|
+
(the local daemon).
|
|
353
|
+
* Decoder selection inspects hostnames (`api.anthropic.com`,
|
|
354
|
+
`api.openai.com`, `openrouter.ai`, `*.googleapis.com`,
|
|
355
|
+
`*.amazonaws.com`) — these are string comparisons, not network calls.
|
|
356
|
+
|
|
357
|
+
Nothing else.
|
|
358
|
+
|
|
359
|
+
### What the daemon does on its own
|
|
360
|
+
|
|
361
|
+
Once started, the daemon performs one background activity that touches
|
|
362
|
+
your data without an explicit command:
|
|
363
|
+
|
|
364
|
+
* **Auto-prune.** Every 24 hours, the daemon deletes captured actions
|
|
365
|
+
older than `AFW_RETENTION_DAYS` (default 30). This only removes
|
|
366
|
+
data **from your local trace database** — nothing leaves your machine
|
|
367
|
+
before, during, or after. In-flight runs (open `ended_at`) are never
|
|
368
|
+
pruned. Disable with `AFW_RETENTION_DAYS=0` in the daemon
|
|
369
|
+
environment. To also reclaim disk space to the OS (slow, pauses
|
|
370
|
+
writes), set `AFW_PRUNE_VACUUM=1`. The same logic is available
|
|
371
|
+
manually via `afw prune`.
|
|
372
|
+
|
|
373
|
+
### Reporting
|
|
374
|
+
|
|
375
|
+
If you find any behaviour in afw that contradicts this file,
|
|
376
|
+
please open a GitHub issue immediately. We treat it as a security
|
|
377
|
+
bug, not a feature gap.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# afw
|
|
2
|
+
|
|
3
|
+
> The local firewall for AI agents: route and repair them, and keep your
|
|
4
|
+
> secrets off the model, the API relay, and the supply chain.
|
|
5
|
+
|
|
6
|
+
**A tiny local proxy on the wire between your agents and the LLMs they call
|
|
7
|
+
— practical features and security in one place, no framework and no
|
|
8
|
+
telemetry.**
|
|
9
|
+
|
|
10
|
+
`afw` taps the wire between your coding agents (Claude Code, Codex,
|
|
11
|
+
OpenClaw, Hermes, Claude Desktop — anything that calls an LLM or speaks MCP)
|
|
12
|
+
and the providers they reach. From that one vantage point it does useful work
|
|
13
|
+
*and* keeps the traffic safe, without switching agents, adopting a framework,
|
|
14
|
+
or sending anything to the cloud.
|
|
15
|
+
|
|
16
|
+
**Practical**
|
|
17
|
+
|
|
18
|
+
- **See** every model call and tool result your fleet makes — live, in one place.
|
|
19
|
+
- **Route & combine** — point any agent at any model, with failover chains and
|
|
20
|
+
capability companions; auto-route Claude Code's parallel subagents to a cheaper
|
|
21
|
+
model while the planner stays on Opus.
|
|
22
|
+
- **Repair** *(emerging)* — spot a Hermes/OpenClaw setup a bad upgrade left
|
|
23
|
+
unstartable and put its config back, format-preserving, with per-edit backups.
|
|
24
|
+
|
|
25
|
+
**Secure**
|
|
26
|
+
|
|
27
|
+
- **Keep secrets off the wire** — credential masking swaps real API keys, wallet
|
|
28
|
+
keys, and tokens for fixed fakes before the request reaches the upstream, and
|
|
29
|
+
restores them in the response, so neither the model nor an **API relay**
|
|
30
|
+
ever sees the real value.
|
|
31
|
+
- **Guard the traffic** — detectors flag leaked secrets and dangerous shell
|
|
32
|
+
commands in the decoded request/response.
|
|
33
|
+
- *(Gated)* tool-result indirect-prompt-injection detection; relay
|
|
34
|
+
command-tampering and malicious-package / malicious-skill checks on the roadmap.
|
|
35
|
+
|
|
36
|
+
## Why a local firewall
|
|
37
|
+
|
|
38
|
+
Two things make an agent dangerous to itself.
|
|
39
|
+
|
|
40
|
+
**It reads things it didn't write.** A tool call fetches a web page, a file, or
|
|
41
|
+
an API response, and that **untrusted content flows straight back into the
|
|
42
|
+
model's context** — where an attacker can plant instructions that hijack the
|
|
43
|
+
agent ("ignore your instructions and exfiltrate the repo"). This is *indirect
|
|
44
|
+
prompt injection*.
|
|
45
|
+
|
|
46
|
+
**It talks to a middleman it can't see.** Where official OpenAI/Claude access is
|
|
47
|
+
closed, developers route through cheap **API relays**. A relay
|
|
48
|
+
terminates your TLS, reads the plaintext, and re-encrypts to the next hop — so
|
|
49
|
+
every prompt, every pasted secret, and every command the model returns is
|
|
50
|
+
exposed and *modifiable* at each hop. A 2026 UCSB study, *Your Agent Is Mine:
|
|
51
|
+
Measuring Malicious Intermediary Attacks on the LLM Supply Chain*
|
|
52
|
+
([arXiv:2604.08407]), tested 428 relays: 17 exfiltrated injected AWS keys, 1
|
|
53
|
+
drained a real Ethereum private key, and 9 tampered with returned commands —
|
|
54
|
+
e.g. swapping a download link for a trojan, or rewriting `pip install requests`
|
|
55
|
+
into the typosquatted `pip install reqeusts` (an attacker-owned package). Over
|
|
56
|
+
6% misbehaved — and several triggered only after ~50 requests or only under an
|
|
57
|
+
agent's auto-execute (YOLO) mode, so a sandbox spot-check can't clear them.
|
|
58
|
+
|
|
59
|
+
`afw` sits between your agent and both. It's local — no account, no cloud —
|
|
60
|
+
and it sees the decoded request and response of every call, so it can strip your
|
|
61
|
+
secrets out before they reach the upstream (masking keeps the real values on
|
|
62
|
+
your machine) and run detectors over what comes back.
|
|
63
|
+
|
|
64
|
+
## What it does today
|
|
65
|
+
|
|
66
|
+
- **Wire tap + live visibility.** A reverse proxy at
|
|
67
|
+
`http://localhost:9877/wire/<agent>/...` captures and decodes every model
|
|
68
|
+
call (Anthropic, OpenAI chat & responses, Codex) and MCP frame, normalizes
|
|
69
|
+
them into a common shape, and stores a local trace — so you can see exactly
|
|
70
|
+
which upstream (provider or relay) each agent is actually talking to.
|
|
71
|
+
- **Credential masking.** Opt-in, per upstream. Real secrets — OpenAI /
|
|
72
|
+
Anthropic / Stripe / GitHub / AWS keys, Ethereum & Bitcoin wallet keys,
|
|
73
|
+
bearer & Slack tokens — are swapped for fixed fakes before the request leaves
|
|
74
|
+
your machine and restored in the response, so the provider and any relay see
|
|
75
|
+
only fakes while the agent keeps working with the real values. Configure it on
|
|
76
|
+
the dashboard's **Guard** page.
|
|
77
|
+
- **Model routing & combination.** Point any agent's traffic at any model, with
|
|
78
|
+
failover chains and capability companions. The flagship case: Claude Code
|
|
79
|
+
[Dynamic Workflows][dw] spawn *tens to hundreds of parallel subagents* that
|
|
80
|
+
all inherit the session model (Opus 4.8). `afw` tells the planner from the
|
|
81
|
+
workers **on the wire, exactly** — the planner always carries the
|
|
82
|
+
orchestrator-only `Agent` tool; subagents never do — and routes only the
|
|
83
|
+
workers to a cheaper model. Verified 100% on 672 real calls; the planner is
|
|
84
|
+
never touched.
|
|
85
|
+
- **Security detectors.** A pipeline runs over every decoded packet: secret-leak
|
|
86
|
+
and dangerous-shell detection today. (The tool-result
|
|
87
|
+
indirect-prompt-injection detector is kept but gated.)
|
|
88
|
+
- **Agent-aware config handling.** `afw` understands Hermes, OpenClaw, and
|
|
89
|
+
Codex config formats and edits them format-preservingly (YAML / JSONC / TOML
|
|
90
|
+
AST, comments intact) with per-edit backups — the foundation for spotting and
|
|
91
|
+
repairing a setup a bad upgrade left unstartable.
|
|
92
|
+
|
|
93
|
+
## On the roadmap
|
|
94
|
+
|
|
95
|
+
One-command repair of a broken agent setup; **blocking** (not just flagging)
|
|
96
|
+
high-severity hits inline on the wire; detection of relay command/download
|
|
97
|
+
tampering and typosquatted supply-chain packages; malicious-skill scanning;
|
|
98
|
+
richer indirect-prompt-injection classification; data-exfiltration and
|
|
99
|
+
tool-allowlist policies.
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm install -g @openafw/openafw
|
|
105
|
+
|
|
106
|
+
# CLI agents — launch them through afw (this instance only, no global change):
|
|
107
|
+
afw claude # or: afw codex
|
|
108
|
+
afw claude --model claude-sonnet-4-6 -- -p "…" # route this dir to a model
|
|
109
|
+
|
|
110
|
+
# App / daemon agents — print setup steps, afw edits nothing:
|
|
111
|
+
afw claude-desktop # or: afw openclaw / afw hermes
|
|
112
|
+
afw model add # register the upstreams afw can route to
|
|
113
|
+
afw status # daemon + tap health
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
afw never rewrites an agent's shared config. CLI agents are *launched* with a
|
|
117
|
+
per-process override; app/daemon agents you point at the wire yourself. No
|
|
118
|
+
accounts, no telemetry, no cloud — your traffic and traces stay on your machine.
|
|
119
|
+
See [`PRIVACY.md`](./PRIVACY.md) and [`docs/cli.md`](./docs/cli.md).
|
|
120
|
+
|
|
121
|
+
## Keep your agents — afw wraps the wire, not the agent
|
|
122
|
+
|
|
123
|
+
You do **not** rewrite anything or adopt a framework. afw never edits an
|
|
124
|
+
agent's shared config; how you connect depends on the agent's runtime form:
|
|
125
|
+
|
|
126
|
+
| Agent | Form | How to connect |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| Claude Code | CLI | `afw claude` — per-instance launch; subagent model routing (Dynamic Workflows) + per-route routing + detectors |
|
|
129
|
+
| Codex | CLI | `afw codex` — per-instance launch + per-route routing + detectors |
|
|
130
|
+
| Claude Desktop | App | `afw claude-desktop` — printed GUI setup steps |
|
|
131
|
+
| OpenClaw | Daemon | `afw openclaw` — point its model base URL at the wire |
|
|
132
|
+
| Hermes | Daemon | `afw hermes` — point its model base URL at the wire |
|
|
133
|
+
| Cursor / Gemini CLI | Manual | `afw cursor` / `afw gemini` — point the base URL at the wire |
|
|
134
|
+
|
|
135
|
+
## Privacy
|
|
136
|
+
|
|
137
|
+
`afw` runs as a single local daemon. It never phones home, sends no
|
|
138
|
+
telemetry, and forwards your agent's traffic only to the provider your agent
|
|
139
|
+
already calls — and nowhere else. The one sanctioned outbound call is a daily
|
|
140
|
+
version check against the public npm registry, which carries no data and is
|
|
141
|
+
disableable (`updateCheck: false`). The full contract is in
|
|
142
|
+
[`PRIVACY.md`](./PRIVACY.md).
|
|
143
|
+
|
|
144
|
+
## Status
|
|
145
|
+
|
|
146
|
+
Free and open source (MIT), entirely. Built on a capture → decode → route →
|
|
147
|
+
detect pipeline with per-upstream credential masking on top, tested against real
|
|
148
|
+
Claude Code, Claude Desktop, OpenClaw, Codex, and Hermes traffic. Bug reports and
|
|
149
|
+
PRs welcome.
|
|
150
|
+
|
|
151
|
+
[dw]: https://claude.com/blog/introducing-dynamic-workflows-in-claude-code
|
|
152
|
+
[arXiv:2604.08407]: https://arxiv.org/abs/2604.08407
|