agentinbox-cli 0.0.0 → 0.1.0-nightly.1
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/README.md +312 -0
- package/dist/audit-CQe-_dPQ.js +2 -0
- package/dist/audit-DETqygzG.js +65 -0
- package/dist/audit-DETqygzG.js.map +1 -0
- package/dist/auth-BniddcRD.js +447 -0
- package/dist/auth-BniddcRD.js.map +1 -0
- package/dist/auth-C2v7bEmQ.js +2 -0
- package/dist/bus-Cz0flS37.js +12 -0
- package/dist/bus-Cz0flS37.js.map +1 -0
- package/dist/bus-OKIGut72.js +2 -0
- package/dist/cli.js +141 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-CIvgMqLx.js +2 -0
- package/dist/config-v975gxv4.js +44 -0
- package/dist/config-v975gxv4.js.map +1 -0
- package/dist/crypto-DYZ4R7c2.js +74 -0
- package/dist/crypto-DYZ4R7c2.js.map +1 -0
- package/dist/db-CfoMO5zW.js +2 -0
- package/dist/db-DdagFmUY.js +247 -0
- package/dist/db-DdagFmUY.js.map +1 -0
- package/dist/errors-CJLUPllS.js +38 -0
- package/dist/errors-CJLUPllS.js.map +1 -0
- package/dist/instance-DZMV78rI.js +2 -0
- package/dist/instance-Dy2jeyZc.js +174 -0
- package/dist/instance-Dy2jeyZc.js.map +1 -0
- package/dist/server-CWqKeCGn.js +981 -0
- package/dist/server-CWqKeCGn.js.map +1 -0
- package/dist/service-63pdSmhO.js +332 -0
- package/dist/service-63pdSmhO.js.map +1 -0
- package/dist/settings-Dq2ETFsd.js +98 -0
- package/dist/settings-Dq2ETFsd.js.map +1 -0
- package/dist/settings-qtn5WTBM.js +2 -0
- package/dist/setup-token-Bkwk84l7.js +47 -0
- package/dist/setup-token-Bkwk84l7.js.map +1 -0
- package/dist/skill/SKILL.md +226 -0
- package/dist/skill-BWWtinsE.js +2 -0
- package/dist/skill-WKbeyNct.js +43 -0
- package/dist/skill-WKbeyNct.js.map +1 -0
- package/dist/urls-CNYhbeUQ.js +2 -0
- package/dist/urls-DmnonkJo.js +22 -0
- package/dist/urls-DmnonkJo.js.map +1 -0
- package/dist/web/assets/_agentId-il4y7OCG.js +1 -0
- package/dist/web/assets/_app-BIO8WIYB.js +1 -0
- package/dist/web/assets/_app-wlLL4MrD.js +63 -0
- package/dist/web/assets/_webhookId-CiwDrkY4.js +1 -0
- package/dist/web/assets/account-status-at9Bv3_T.js +1 -0
- package/dist/web/assets/accounts-B4rfPwCV.js +1 -0
- package/dist/web/assets/accounts-list-e--JWxHH.js +1 -0
- package/dist/web/assets/activity-zrckFhPk.js +1 -0
- package/dist/web/assets/agent-create-form-DaWPR6kx.js +1 -0
- package/dist/web/assets/agents-D4gPq4dt.js +1 -0
- package/dist/web/assets/approvals-DaoZ6wuf.js +2 -0
- package/dist/web/assets/arrow-left-DVJV4Zx7.js +1 -0
- package/dist/web/assets/arrow-right-4PFX6MSS.js +1 -0
- package/dist/web/assets/audit-bAdLe-px.js +1 -0
- package/dist/web/assets/audit-bits-DQHo74i6.js +1 -0
- package/dist/web/assets/bot-DVw15lBn.js +1 -0
- package/dist/web/assets/card-EQTpqntn.js +1 -0
- package/dist/web/assets/checkbox-DDZZurgK.js +1 -0
- package/dist/web/assets/chevron-down-892ylB4R.js +1 -0
- package/dist/web/assets/chevron-right-CbBKS6-T.js +1 -0
- package/dist/web/assets/chips-input-Btdc_f7b.js +1 -0
- package/dist/web/assets/circle-x-Bi4IGwky.js +1 -0
- package/dist/web/assets/common-h6596gEC.js +1 -0
- package/dist/web/assets/connect-host-Co31qIYr.js +1 -0
- package/dist/web/assets/data-table-DsKrM62X.js +1 -0
- package/dist/web/assets/dialog-CgZr27yw.js +1 -0
- package/dist/web/assets/dist-B0e3Ju3r.js +1 -0
- package/dist/web/assets/empty-CE_WZGEC.js +1 -0
- package/dist/web/assets/es2015-DPaldoxG.js +41 -0
- package/dist/web/assets/event-row-C0qRfWuV.js +1 -0
- package/dist/web/assets/events-Svyij7mp.js +1 -0
- package/dist/web/assets/eye-off-DjpaihHf.js +1 -0
- package/dist/web/assets/field-CcyMn3ey.js +1 -0
- package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/web/assets/get-started-Cxfw97sJ.js +1 -0
- package/dist/web/assets/grants-BilXCDps.js +1 -0
- package/dist/web/assets/index-DgIgw66p.css +2 -0
- package/dist/web/assets/index-TbxC5ZgP.js +10 -0
- package/dist/web/assets/input-CMhmmHiH.js +1 -0
- package/dist/web/assets/input-group-DRs3TNaT.js +1 -0
- package/dist/web/assets/key-round-CVIZRCLM.js +1 -0
- package/dist/web/assets/labels-C_Pwium9.js +1 -0
- package/dist/web/assets/login-DUJia7lg.js +1 -0
- package/dist/web/assets/mail-CAVmuESo.js +1 -0
- package/dist/web/assets/mutations-D_GluJ0j.js +1 -0
- package/dist/web/assets/paperclip-QFjM4y6n.js +1 -0
- package/dist/web/assets/password-input-_m9Ycphv.js +1 -0
- package/dist/web/assets/pencil-Bf1JhsG0.js +1 -0
- package/dist/web/assets/play-DiPk49JH.js +1 -0
- package/dist/web/assets/plus-CETmK0SB.js +1 -0
- package/dist/web/assets/preload-helper-BFuRUfdt.js +1 -0
- package/dist/web/assets/progress-3ZOsbLSK.js +6 -0
- package/dist/web/assets/queries-a3M5_loa.js +2 -0
- package/dist/web/assets/refresh-cw-DOn9kD7g.js +1 -0
- package/dist/web/assets/search-gcYjitzV.js +1 -0
- package/dist/web/assets/select-_tULk-vs.js +1 -0
- package/dist/web/assets/send-BhQwhuJo.js +1 -0
- package/dist/web/assets/settings-Dl4wb4f0.js +1 -0
- package/dist/web/assets/settings-form-D21eKBh-.js +1 -0
- package/dist/web/assets/setup-DYnNhnfH.js +1 -0
- package/dist/web/assets/sheet--2lKU0jN.js +1 -0
- package/dist/web/assets/shield-alert-CbFgd06X.js +1 -0
- package/dist/web/assets/stamp-CH2OvjH2.js +1 -0
- package/dist/web/assets/stream-6OwrgVZw.js +1 -0
- package/dist/web/assets/tabs-CiAUEj_y.js +1 -0
- package/dist/web/assets/tag-CP72Kk6Y.js +1 -0
- package/dist/web/assets/test-connection-F2tMowbv.js +1 -0
- package/dist/web/assets/textarea-CCokBZIw.js +1 -0
- package/dist/web/assets/toggle-group-D1pC7sJr.js +1 -0
- package/dist/web/assets/trash-DxdMNg8B.js +1 -0
- package/dist/web/assets/use-sign-out-BuzlTb0Q.js +1 -0
- package/dist/web/assets/useInfiniteQuery-DE6WChlb.js +1 -0
- package/dist/web/assets/useMatch-BKUnm54_.js +1 -0
- package/dist/web/assets/useMutation-DGl4PaZ1.js +1 -0
- package/dist/web/assets/useQuery-DAhf1YDH.js +1 -0
- package/dist/web/assets/useSelector-D70eKNYs.js +1 -0
- package/dist/web/assets/webhook-BWT75M78.js +1 -0
- package/dist/web/assets/webhook-sheet-4U_6Rw8I.js +34 -0
- package/dist/web/assets/webhooks-C1JvTI88.js +1 -0
- package/dist/web/assets/with-selector-CkNyXYMW.js +1 -0
- package/dist/web/assets/x-BHjDLXNO.js +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +31 -0
- package/package.json +94 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unlabored Labs
|
|
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/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# agentinbox
|
|
2
|
+
|
|
3
|
+
[](https://github.com/unlaboredlabs/agentinbox/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/agentinbox-cli)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A self-hosted Gmail broker for AI agents. One small daemon holds your Gmail credentials; every agent gets its own
|
|
8
|
+
scoped API key and reaches mail through **MCP tools** (read, search, draft, send, organize) and **signed webhooks** or an
|
|
9
|
+
MCP event feed for new mail. Guardrails decide what each agent may do per mailbox, every action lands in an audit log,
|
|
10
|
+
and a web console makes setup and management painless. It runs anywhere Node does, against your own Google Cloud
|
|
11
|
+
project, with no cloud service in between.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **One place for credentials.** Connect Gmail and Google Workspace accounts once. Refresh tokens are encrypted at rest.
|
|
16
|
+
Agents only ever see an `aib_…` key.
|
|
17
|
+
- **MCP for every agent.** Streamable HTTP at `/mcp`: `search_threads`, `get_thread`, `reply`, `send_message`,
|
|
18
|
+
`modify_labels`, `wait_for_events` and more. Each agent sees only the tools its grants allow.
|
|
19
|
+
- **Real-time events.** New mail, replies to threads you sent into, and label changes arrive within seconds through
|
|
20
|
+
Gmail push via a Pub/Sub *pull* subscription (no public URL needed), or by polling with zero extra setup.
|
|
21
|
+
- **Webhooks.** Filter by account, label, sender, recipient, subject or attachments. Payloads are signed per
|
|
22
|
+
[Standard Webhooks](https://www.standardwebhooks.com), retried with backoff, logged, and replayable.
|
|
23
|
+
- **Guardrails that don't degrade content.** Per agent and mailbox: capabilities, label scope, send mode (direct,
|
|
24
|
+
draft-only or human approval), recipient allowlists, reply-only and send caps. Email content is cleaned, not censored.
|
|
25
|
+
- **A console you'll actually enjoy.** Guided Google setup, connect snippets for Claude Code, Codex and Cursor, an
|
|
26
|
+
approvals inbox, live events, delivery logs and the audit trail. Dark mode included.
|
|
27
|
+
- **A ready-made agent skill.** `SKILL.md` teaches agents to use the tools well and to treat email as data.
|
|
28
|
+
|
|
29
|
+
<!--
|
|
30
|
+
## Preview
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
<table>
|
|
35
|
+
<tr>
|
|
36
|
+
<td width="50%"><img src="docs/screenshots/setup.png" alt="Setup wizard"><br>Guided Google Cloud setup.</td>
|
|
37
|
+
<td width="50%"><img src="docs/screenshots/agents.png" alt="Agents"><br>Agents, grants and connect snippets.</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<td><img src="docs/screenshots/approvals.png" alt="Approvals"><br>Approve or reject sends.</td>
|
|
41
|
+
<td><img src="docs/screenshots/webhooks.png" alt="Webhooks"><br>Webhooks with delivery log and replay.</td>
|
|
42
|
+
</tr>
|
|
43
|
+
</table>
|
|
44
|
+
-->
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
Requires Node.js 22.13 or newer.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx agentinbox-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The npm package is `agentinbox-cli`; the command it provides is `agentinbox`. Use `npx agentinbox-cli …` for one-off
|
|
55
|
+
runs, or `npm i -g agentinbox-cli` and then `agentinbox …`.
|
|
56
|
+
|
|
57
|
+
agentinbox starts on port 8425 and prints a one-time setup link (and opens it if it can):
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
agentinbox v0.1.0 · data in /home/you/.agentinbox
|
|
61
|
+
|
|
62
|
+
First run: open this link to claim the instance and finish setup:
|
|
63
|
+
|
|
64
|
+
http://localhost:8425/setup?token=…
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Open it, choose an admin password, and the setup wizard walks you through the rest in about ten minutes.
|
|
68
|
+
|
|
69
|
+
## Setting up Google
|
|
70
|
+
|
|
71
|
+
agentinbox uses **your own** Google Cloud project and OAuth client. Gmail's read scopes are restricted, so a shared
|
|
72
|
+
client isn't an option, and this way your mail never passes through anyone else's app. The wizard shows each step with
|
|
73
|
+
the exact values to paste; in short:
|
|
74
|
+
|
|
75
|
+
1. **Create a Google Cloud project** and enable the **Gmail API** (and the **Cloud Pub/Sub API** if you want push).
|
|
76
|
+
2. **Configure the OAuth consent screen** (Google Auth Platform → Audience):
|
|
77
|
+
- **Internal** if every mailbox is in your Google Workspace organization. No review, no expiry, nothing else to do.
|
|
78
|
+
- **External** for personal `@gmail.com` accounts (or mailboxes outside your org). Then **publish the app to
|
|
79
|
+
"In production"**. Left in *Testing*, Google expires refresh tokens after **7 days** and your accounts silently
|
|
80
|
+
disconnect. You don't need Google's verification for your own use: on consent, click *Advanced → Go to
|
|
81
|
+
(app name)* past the "unverified app" warning.
|
|
82
|
+
3. **Create an OAuth client** of type *Web application*, add the redirect URI the console shows
|
|
83
|
+
(`<public URL>/oauth/callback`), and paste the client ID, client secret and project ID into the console.
|
|
84
|
+
4. **Connect accounts.** Each one goes through Google's consent screen. If the console is on plain HTTP at a non-localhost
|
|
85
|
+
address, Google can't redirect back to it; the console then asks you to paste the URL you land on instead.
|
|
86
|
+
5. **Choose how new mail arrives:**
|
|
87
|
+
- **Pub/Sub (recommended):** one click creates the topic, grants Gmail's push service
|
|
88
|
+
(`gmail-api-push@system.gserviceaccount.com`) permission to publish, and creates a pull subscription, using a
|
|
89
|
+
connected account that can manage Pub/Sub in the project. Events arrive in seconds, with no inbound connections.
|
|
90
|
+
If a Workspace organization policy (domain-restricted sharing) blocks that grant, the console says so; use polling.
|
|
91
|
+
- **Polling:** no extra setup; checks each mailbox every 30 seconds by default.
|
|
92
|
+
|
|
93
|
+
Then create an agent and give it a grant on a mailbox.
|
|
94
|
+
|
|
95
|
+
## Connecting agents
|
|
96
|
+
|
|
97
|
+
Create an agent in the console (**Agents → New agent**) to get its API key; the key is shown once. The console has
|
|
98
|
+
copy-paste snippets with your URL and key filled in, and a **Test connection** button. By hand:
|
|
99
|
+
|
|
100
|
+
**Claude Code**
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
claude mcp add --transport http agentinbox http://your-host:8425/mcp --header "Authorization: Bearer aib_…"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Codex** (`~/.codex/config.toml`)
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
[mcp_servers.agentinbox]
|
|
110
|
+
url = "http://your-host:8425/mcp"
|
|
111
|
+
bearer_token_env_var = "AGENTINBOX_KEY"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Cursor** (`~/.cursor/mcp.json`) and most other MCP clients
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"agentinbox": {
|
|
120
|
+
"url": "http://your-host:8425/mcp",
|
|
121
|
+
"headers": { "Authorization": "Bearer aib_…" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Any MCP client that speaks streamable HTTP works: endpoint `/mcp`, header `Authorization: Bearer <key>`.
|
|
128
|
+
|
|
129
|
+
### The agent skill
|
|
130
|
+
|
|
131
|
+
`SKILL.md` teaches an agent the tools, Gmail search syntax, reply etiquette, how sends and denials work, the event loop,
|
|
132
|
+
and that email content is data, never instructions. Install it on the machine where the agent runs (no running
|
|
133
|
+
agentinbox needed there):
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
npx agentinbox-cli skill install --url https://mail.example.com # ~/.claude/skills/agentinbox/SKILL.md
|
|
137
|
+
npx agentinbox-cli skill install --target codex --url https://mail.example.com
|
|
138
|
+
npx agentinbox-cli skill install --target ./skills --url https://mail.example.com
|
|
139
|
+
npx agentinbox-cli skill print --url https://mail.example.com # to stdout
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Every instance also serves it at `/skill.md` with its public URL filled in.
|
|
143
|
+
|
|
144
|
+
## Events and webhooks
|
|
145
|
+
|
|
146
|
+
Event types: `message.received`, `message.sent`, `thread.reply_received` (a reply in a thread the account sent into),
|
|
147
|
+
`label.added`, `label.removed`, `account.auth_failed` and `sync.gap`. Agents only receive events for mailboxes and
|
|
148
|
+
labels their grants let them read.
|
|
149
|
+
|
|
150
|
+
- **Agents without an HTTP endpoint** (most coding agents) use the MCP tools `wait_for_events` (long poll),
|
|
151
|
+
`list_events` and `ack_events`. The server keeps each agent's position.
|
|
152
|
+
- **Agents with an endpoint** get webhooks: create one under **Webhooks**, pick event types and filters, and optionally
|
|
153
|
+
include the cleaned message body. Failed deliveries retry with exponential backoff before they're marked dead, and
|
|
154
|
+
any delivery can be replayed from the console.
|
|
155
|
+
|
|
156
|
+
Each webhook request carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Verify them with any
|
|
157
|
+
[Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) and the `whsec_…` secret shown when
|
|
158
|
+
you create the webhook:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import { Webhook } from "standardwebhooks";
|
|
162
|
+
|
|
163
|
+
const wh = new Webhook(process.env.AGENTINBOX_WEBHOOK_SECRET);
|
|
164
|
+
const payload = wh.verify(rawBody, request.headers); // throws if the signature or timestamp is bad
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A dependency-free Node version is in the [skill](server/skill/SKILL.md#webhooks).
|
|
168
|
+
|
|
169
|
+
By default webhooks may only target public addresses and loopback; other private addresses (for example a LAN host or
|
|
170
|
+
`n8n.internal`) must be added to the webhook allowlist in **Settings**.
|
|
171
|
+
|
|
172
|
+
## Security model
|
|
173
|
+
|
|
174
|
+
agentinbox assumes agents can be manipulated by the mail they read. So it keeps content intact and puts the hard limits
|
|
175
|
+
on actions.
|
|
176
|
+
|
|
177
|
+
**Content is cleaned, not censored.** Agents get full-fidelity mail converted to tidy Markdown, with links and tables.
|
|
178
|
+
Only what a human reader couldn't see is removed: hidden CSS (`display:none`, zero-size, transparent or
|
|
179
|
+
same-colour text), HTML comments, scripts and styles, zero-width characters, Unicode tag characters and bidi overrides.
|
|
180
|
+
Each body is wrapped in one `<email_content … untrusted>` delimiter and comes with signals: SPF/DKIM/DMARC results, an
|
|
181
|
+
external-sender flag, whether hidden content was removed, and phrases typical of prompt injection. Signals are
|
|
182
|
+
informational; nothing is withheld because of them.
|
|
183
|
+
|
|
184
|
+
**Actions are strictly controlled.** A new agent can do nothing. Each grant (agent × mailbox) sets:
|
|
185
|
+
|
|
186
|
+
| Control | |
|
|
187
|
+
|---|---|
|
|
188
|
+
| Capabilities | `read` (search, read, attachments, events), `draft`, `send`, `organize` (labels, archive, read state), `trash` |
|
|
189
|
+
| Label scope | Confine the agent to threads carrying certain labels. Searches are rewritten and fetches by id are checked. |
|
|
190
|
+
| Send mode | `draft_only` (default: a Gmail draft for a human to send), `approval` (queued in the console; approving sends it) or `direct` |
|
|
191
|
+
| Allowed recipients | Exact addresses or `*@domain.com` globs |
|
|
192
|
+
| Reply only | Sending only inside threads the agent can already see |
|
|
193
|
+
| Send caps | Per hour and per day |
|
|
194
|
+
|
|
195
|
+
**Everything is audited.** Every tool call and every console change is recorded with its outcome, and denials name the
|
|
196
|
+
guardrail that blocked them.
|
|
197
|
+
|
|
198
|
+
**The console** is protected by an admin password (set through the one-time setup link), HttpOnly session cookies
|
|
199
|
+
that expire after 30 days of inactivity (90 at most), a CSRF header on every change, request size limits, a strict
|
|
200
|
+
Content Security Policy, and a 15-minute lockout after five wrong passwords (or 30 attempts) from one address. When
|
|
201
|
+
failures pile up across many addresses, every sign-in attempt is slowed down instead. It never allows being framed.
|
|
202
|
+
The data directory and database are readable only by their owner.
|
|
203
|
+
|
|
204
|
+
Recommendations: split agents that read untrusted mail from agents that can send; prefer `draft_only` or `approval`;
|
|
205
|
+
use allowlists for anything that sends directly.
|
|
206
|
+
|
|
207
|
+
## Running it
|
|
208
|
+
|
|
209
|
+
### As a service
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
npx agentinbox-cli service install
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Installs a background service that starts at boot: systemd on Linux (a user service with lingering enabled, or a system
|
|
216
|
+
service when run as root) and launchd on macOS. The service runs its own copy of agentinbox from the data directory,
|
|
217
|
+
so it never depends on the npx cache; a failed install leaves the running service as it was. Re-run `service install`
|
|
218
|
+
to update; `service status` and `service uninstall` do what you'd expect, and uninstalling keeps your data. Pass the same
|
|
219
|
+
`--port`, `--host` and `--data` options each time. `AGENTINBOX_MASTER_KEY`, `TRUST_PROXY`, `TRUST_PROXY_HOPS` and
|
|
220
|
+
`AGENTINBOX_LOG_LEVEL` are taken from your shell and remembered in `<data>/service.env`, so later installs keep them
|
|
221
|
+
(set one to an empty value to remove it).
|
|
222
|
+
|
|
223
|
+
Only one agentinbox runs per data directory; a second one refuses to start.
|
|
224
|
+
|
|
225
|
+
When a newer version is published, the console shows **Update available** with the exact command to run.
|
|
226
|
+
|
|
227
|
+
### Behind a reverse proxy
|
|
228
|
+
|
|
229
|
+
agentinbox serves plain HTTP. To expose it beyond your network, put it behind a TLS-terminating proxy (Caddy, nginx,
|
|
230
|
+
Tailscale Funnel) and:
|
|
231
|
+
|
|
232
|
+
- bind it to localhost: `--host 127.0.0.1`
|
|
233
|
+
- set `TRUST_PROXY=1`, so lockouts apply per client and cookies are marked `Secure`. The client address is taken from
|
|
234
|
+
the right end of `X-Forwarded-For`; with more than one proxy in front, set `TRUST_PROXY_HOPS` to their number
|
|
235
|
+
- set **Settings → Public URL** to the https address, so MCP snippets, the skill and the OAuth redirect use it
|
|
236
|
+
|
|
237
|
+
`GET /health` (or `/api/health`) needs no auth and suits uptime monitors: `200` when healthy, `503` when degraded, for
|
|
238
|
+
example when an account needs to be reconnected or ingest is failing. It reports only a status, the version and
|
|
239
|
+
whether setup is complete.
|
|
240
|
+
|
|
241
|
+
### Options
|
|
242
|
+
|
|
243
|
+
With a global install (`npm i -g agentinbox-cli`); otherwise prefix `npx agentinbox-cli` instead of `agentinbox`.
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
agentinbox [start] [--port 8425] [--host 0.0.0.0] [--data ~/.agentinbox] [--no-open] [--dev]
|
|
247
|
+
agentinbox service install|uninstall|status [--port …] [--host …] [--data …]
|
|
248
|
+
agentinbox skill install [--target claude|codex|agents|<dir>] [--url <public url>]
|
|
249
|
+
agentinbox skill print [--url <public url>]
|
|
250
|
+
agentinbox reset-password [--data …]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| Flag | Env | Default | |
|
|
254
|
+
|---|---|---|---|
|
|
255
|
+
| `--port` | `PORT` | `8425` | Port to listen on |
|
|
256
|
+
| `--host` | `HOST` | `0.0.0.0` | Bind address. Use `127.0.0.1` behind a reverse proxy or SSH tunnel |
|
|
257
|
+
| `--data` | `AGENTINBOX_DATA` | `~/.agentinbox` | Data directory |
|
|
258
|
+
| | `AGENTINBOX_MASTER_KEY` | | Base64 32-byte key for encrypting secrets, instead of `<data>/master.key` |
|
|
259
|
+
| | `TRUST_PROXY` | | `1` behind a reverse proxy: client IPs from `X-Forwarded-For`, https from `X-Forwarded-Proto` |
|
|
260
|
+
| | `TRUST_PROXY_HOPS` | `1` | Number of proxies in front of agentinbox (which `X-Forwarded-For` entry, from the right, is the client) |
|
|
261
|
+
| | `AGENTINBOX_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
|
|
262
|
+
|
|
263
|
+
Forgot the admin password? Run `npx agentinbox-cli reset-password` on the server, as the user agentinbox runs as (`sudo` for a
|
|
264
|
+
system service) and with the same `--data`. It signs out all sessions and prints a new setup link; a running instance
|
|
265
|
+
picks it up without a restart.
|
|
266
|
+
|
|
267
|
+
## Data and backups
|
|
268
|
+
|
|
269
|
+
Everything lives in the data directory (`~/.agentinbox` by default):
|
|
270
|
+
|
|
271
|
+
| File | |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `agentinbox.db` | SQLite database: accounts, encrypted tokens, agents, grants, events, deliveries, approvals, audit log |
|
|
274
|
+
| `master.key` | Encrypts refresh tokens, webhook secrets and the OAuth client secret (mode 0600) |
|
|
275
|
+
| `setup-token` | The one-time setup token, until the instance is claimed |
|
|
276
|
+
| `agentinbox.lock` | Marks the running instance (pid and port) |
|
|
277
|
+
| `service.env` | Environment the service was installed with, possibly including `AGENTINBOX_MASTER_KEY` (mode 0600; `service install` only) |
|
|
278
|
+
| `agentinbox.log` | Service output on macOS (launchd); on Linux use `journalctl --user -u agentinbox` (or without `--user` for a system service) |
|
|
279
|
+
| `app/` | The service's own install (`service install` only) |
|
|
280
|
+
|
|
281
|
+
Back up `agentinbox.db` and `master.key` together: without the key, stored secrets can't be decrypted, and agentinbox
|
|
282
|
+
refuses to start rather than create a new one. Keep the key somewhere safer than the database backup, or supply it via
|
|
283
|
+
`AGENTINBOX_MASTER_KEY`. The database runs in WAL mode, so don't just copy the file while agentinbox is running: stop
|
|
284
|
+
it first (`systemctl --user stop agentinbox`), or take a consistent copy with
|
|
285
|
+
`sqlite3 ~/.agentinbox/agentinbox.db ".backup /path/to/backup.db"`. Mail
|
|
286
|
+
bodies are never stored; agentinbox keeps metadata (ids, headers, labels, snippets) and fetches content from Gmail on
|
|
287
|
+
demand. Events, deliveries and audit entries are pruned after the retention period set in **Settings** (30 days by
|
|
288
|
+
default).
|
|
289
|
+
|
|
290
|
+
## Development
|
|
291
|
+
|
|
292
|
+
```sh
|
|
293
|
+
pnpm install
|
|
294
|
+
AGENTINBOX_DATA=/tmp/agentinbox-dev pnpm dev
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`pnpm dev` runs the server on `:8425` in watch mode (API, MCP and OAuth only) and the console on Vite at `:5173`, which
|
|
298
|
+
proxies to it. Open the Vite URL; the printed setup link already points there.
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
pnpm typecheck && pnpm lint && pnpm test
|
|
302
|
+
pnpm build # console → dist/web, server → dist/cli.js
|
|
303
|
+
node dist/cli.js # run the build
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Stack: TypeScript on Node 22+, Hono on `@hono/node-server`, `node:sqlite`, the MCP TypeScript SDK, and thin fetch-based
|
|
307
|
+
Google clients (no `googleapis`, so `npx` stays fast). The console is Vite, React 19, TanStack Router / Query / Table /
|
|
308
|
+
Form and shadcn/ui on Tailwind v4.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as json, r as now } from "./db-DdagFmUY.js";
|
|
3
|
+
//#region server/audit.ts
|
|
4
|
+
const SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name
|
|
5
|
+
FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;
|
|
6
|
+
const toEntry = (r) => ({
|
|
7
|
+
id: r.id,
|
|
8
|
+
ts: r.ts,
|
|
9
|
+
actorType: r.actor_type,
|
|
10
|
+
actorId: r.actor_id,
|
|
11
|
+
actorName: r.actor_name,
|
|
12
|
+
action: r.action,
|
|
13
|
+
accountId: r.account_id,
|
|
14
|
+
target: r.target,
|
|
15
|
+
outcome: r.outcome,
|
|
16
|
+
reason: r.reason,
|
|
17
|
+
durationMs: r.duration_ms,
|
|
18
|
+
detail: json.parse(r.detail, null)
|
|
19
|
+
});
|
|
20
|
+
/** Append-only audit log. Every agent tool call and every admin mutation is recorded. */
|
|
21
|
+
var AuditLog = class {
|
|
22
|
+
db;
|
|
23
|
+
bus;
|
|
24
|
+
constructor(db, bus) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.bus = bus;
|
|
27
|
+
}
|
|
28
|
+
record(input) {
|
|
29
|
+
const { lastInsertRowid } = this.db.run(`INSERT INTO audit_log (ts, actor_type, actor_id, action, account_id, target, outcome, reason, duration_ms, detail)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, now(), input.actorType, input.actorId ?? null, input.action, input.accountId ?? null, input.target ?? null, input.outcome, input.reason ?? null, input.durationMs ?? null, input.detail ? json.str(input.detail) : null);
|
|
31
|
+
const entry = toEntry(this.db.get(`${SELECT} WHERE a.id = ?`, lastInsertRowid));
|
|
32
|
+
this.bus.emit("audit", entry);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
/** Newest first. */
|
|
36
|
+
list(q = {}) {
|
|
37
|
+
const where = [];
|
|
38
|
+
const params = [];
|
|
39
|
+
const add = (clause, value) => {
|
|
40
|
+
where.push(clause);
|
|
41
|
+
params.push(value);
|
|
42
|
+
};
|
|
43
|
+
if (q.agentId) add("a.actor_id = ?", q.agentId);
|
|
44
|
+
if (q.accountId) add("a.account_id = ?", q.accountId);
|
|
45
|
+
if (q.outcome) add("a.outcome = ?", q.outcome);
|
|
46
|
+
if (q.action) add("a.action = ?", q.action);
|
|
47
|
+
const before = Number(q.cursor);
|
|
48
|
+
if (q.cursor && Number.isInteger(before)) add("a.id < ?", before);
|
|
49
|
+
const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);
|
|
50
|
+
const rows = this.db.all(`${SELECT} ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY a.id DESC LIMIT ?`, ...params, limit + 1);
|
|
51
|
+
const items = rows.slice(0, limit).map(toEntry);
|
|
52
|
+
return {
|
|
53
|
+
items,
|
|
54
|
+
nextCursor: rows.length > limit ? String(items.at(-1).id) : null
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Deletes entries older than `beforeIso`. Returns the number removed. */
|
|
58
|
+
prune(beforeIso) {
|
|
59
|
+
return this.db.run("DELETE FROM audit_log WHERE ts < ?", beforeIso).changes;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { AuditLog as t };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=audit-DETqygzG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-DETqygzG.js","names":[],"sources":["../server/audit.ts"],"sourcesContent":["import type { ActorType, AuditEntry, AuditOutcome, Page } from \"../shared/types.ts\";\nimport type { Bus } from \"./bus.ts\";\nimport { type Db, json, now } from \"./db/index.ts\";\n\nexport interface AuditInput {\n actorType: ActorType;\n actorId?: string | null;\n /** MCP tool name or console action, e.g. \"send_message\", \"agent.create\", \"account.connect\". */\n action: string;\n accountId?: string | null;\n target?: string | null;\n outcome: AuditOutcome;\n /** Required for denials: the guardrail that blocked it (\"allow_to\", \"label_scope\", \"rate_limit\", …). */\n reason?: string | null;\n durationMs?: number | null;\n /** Small, non-sensitive context. Never message bodies, keys or tokens. */\n detail?: Record<string, unknown> | null;\n}\n\nexport interface AuditQuery {\n agentId?: string;\n accountId?: string;\n outcome?: AuditOutcome;\n action?: string;\n /** Opaque cursor from a previous page. */\n cursor?: string | null;\n limit?: number;\n}\n\ninterface AuditRow {\n id: number;\n ts: string;\n actor_type: ActorType;\n actor_id: string | null;\n actor_name: string | null;\n action: string;\n account_id: string | null;\n target: string | null;\n outcome: AuditOutcome;\n reason: string | null;\n duration_ms: number | null;\n detail: string | null;\n}\n\nconst SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name\n FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;\n\nconst toEntry = (r: AuditRow): AuditEntry => ({\n id: r.id,\n ts: r.ts,\n actorType: r.actor_type,\n actorId: r.actor_id,\n actorName: r.actor_name,\n action: r.action,\n accountId: r.account_id,\n target: r.target,\n outcome: r.outcome,\n reason: r.reason,\n durationMs: r.duration_ms,\n detail: json.parse<Record<string, unknown> | null>(r.detail, null),\n});\n\n/** Append-only audit log. Every agent tool call and every admin mutation is recorded. */\nexport class AuditLog {\n constructor(\n private readonly db: Db,\n private readonly bus: Bus,\n ) {}\n\n record(input: AuditInput): AuditEntry {\n const { lastInsertRowid } = this.db.run(\n `INSERT INTO audit_log (ts, actor_type, actor_id, action, account_id, target, outcome, reason, duration_ms, detail)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n now(),\n input.actorType,\n input.actorId ?? null,\n input.action,\n input.accountId ?? null,\n input.target ?? null,\n input.outcome,\n input.reason ?? null,\n input.durationMs ?? null,\n input.detail ? json.str(input.detail) : null,\n );\n const entry = toEntry(this.db.get<AuditRow>(`${SELECT} WHERE a.id = ?`, lastInsertRowid)!);\n this.bus.emit(\"audit\", entry);\n return entry;\n }\n\n /** Newest first. */\n list(q: AuditQuery = {}): Page<AuditEntry> {\n const where: string[] = [];\n const params: (string | number)[] = [];\n const add = (clause: string, value: string | number) => {\n where.push(clause);\n params.push(value);\n };\n if (q.agentId) add(\"a.actor_id = ?\", q.agentId);\n if (q.accountId) add(\"a.account_id = ?\", q.accountId);\n if (q.outcome) add(\"a.outcome = ?\", q.outcome);\n if (q.action) add(\"a.action = ?\", q.action);\n const before = Number(q.cursor);\n if (q.cursor && Number.isInteger(before)) add(\"a.id < ?\", before);\n const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);\n const rows = this.db.all<AuditRow>(\n `${SELECT} ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"} ORDER BY a.id DESC LIMIT ?`,\n ...params,\n limit + 1,\n );\n const items = rows.slice(0, limit).map(toEntry);\n return { items, nextCursor: rows.length > limit ? String(items.at(-1)!.id) : null };\n }\n\n /** Deletes entries older than `beforeIso`. Returns the number removed. */\n prune(beforeIso: string): number {\n return this.db.run(\"DELETE FROM audit_log WHERE ts < ?\", beforeIso).changes;\n }\n}\n"],"mappings":";;;AA4CA,MAAM,SAAS;;AAGf,MAAM,WAAW,OAA6B;CAC5C,IAAI,EAAE;CACN,IAAI,EAAE;CACN,WAAW,EAAE;CACb,SAAS,EAAE;CACX,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,SAAS,EAAE;CACX,QAAQ,EAAE;CACV,YAAY,EAAE;CACd,QAAQ,KAAK,MAAsC,EAAE,QAAQ,IAAI;AACnE;;AAGA,IAAa,WAAb,MAAsB;CAED;CACA;CAFnB,YACE,IACA,KACA;EAFiB,KAAA,KAAA;EACA,KAAA,MAAA;CAChB;CAEH,OAAO,OAA+B;EACpC,MAAM,EAAE,oBAAoB,KAAK,GAAG,IAClC;+CAEA,IAAI,GACJ,MAAM,WACN,MAAM,WAAW,MACjB,MAAM,QACN,MAAM,aAAa,MACnB,MAAM,UAAU,MAChB,MAAM,SACN,MAAM,UAAU,MAChB,MAAM,cAAc,MACpB,MAAM,SAAS,KAAK,IAAI,MAAM,MAAM,IAAI,IAC1C;EACA,MAAM,QAAQ,QAAQ,KAAK,GAAG,IAAc,GAAG,OAAO,kBAAkB,eAAe,CAAE;EACzF,KAAK,IAAI,KAAK,SAAS,KAAK;EAC5B,OAAO;CACT;;CAGA,KAAK,IAAgB,CAAC,GAAqB;EACzC,MAAM,QAAkB,CAAC;EACzB,MAAM,SAA8B,CAAC;EACrC,MAAM,OAAO,QAAgB,UAA2B;GACtD,MAAM,KAAK,MAAM;GACjB,OAAO,KAAK,KAAK;EACnB;EACA,IAAI,EAAE,SAAS,IAAI,kBAAkB,EAAE,OAAO;EAC9C,IAAI,EAAE,WAAW,IAAI,oBAAoB,EAAE,SAAS;EACpD,IAAI,EAAE,SAAS,IAAI,iBAAiB,EAAE,OAAO;EAC7C,IAAI,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM;EAC1C,MAAM,SAAS,OAAO,EAAE,MAAM;EAC9B,IAAI,EAAE,UAAU,OAAO,UAAU,MAAM,GAAG,IAAI,YAAY,MAAM;EAChE,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,EAAE,SAAS,IAAI,CAAC,GAAG,GAAG;EACtD,MAAM,OAAO,KAAK,GAAG,IACnB,GAAG,OAAO,GAAG,MAAM,SAAS,WAAW,MAAM,KAAK,OAAO,IAAI,GAAG,8BAChE,GAAG,QACH,QAAQ,CACV;EACA,MAAM,QAAQ,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,OAAO;EAC9C,OAAO;GAAE;GAAO,YAAY,KAAK,SAAS,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC,CAAE,EAAE,IAAI;EAAK;CACpF;;CAGA,MAAM,WAA2B;EAC/B,OAAO,KAAK,GAAG,IAAI,sCAAsC,SAAS,CAAC,CAAC;CACtE;AACF"}
|