@primusclaw/connector 0.0.0-stage → 0.1.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 +37 -0
- package/README.md +418 -2
- package/dist/cli.js +3972 -0
- package/dist/terminal-hook.js +206 -0
- package/package.json +33 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Advanced Micro Devices, Inc.
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Third-Party Tools and Agents
|
|
24
|
+
|
|
25
|
+
PrimusClaw can drive third-party coding agents, including Cursor Agent
|
|
26
|
+
(`cursor-agent`), Claude Code (`@anthropic-ai/claude-code`) and OpenAI Codex
|
|
27
|
+
(`@openai/codex`). Those tools are not distributed as part of this repository;
|
|
28
|
+
they are installed at runtime and are governed by their own separate license
|
|
29
|
+
terms, which the MIT license above does not cover. Users are responsible for
|
|
30
|
+
reviewing and complying with the individual license for each tool they enable.
|
|
31
|
+
|
|
32
|
+
## Bundled Third-Party Code
|
|
33
|
+
|
|
34
|
+
The `sandbox/` directory is a fork of
|
|
35
|
+
[kubernetes-sigs/agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox),
|
|
36
|
+
which is licensed under the Apache License 2.0 — not the MIT license above. See
|
|
37
|
+
`sandbox/LICENSE`, `sandbox/NOTICE` and `sandbox/UPSTREAM.md`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,419 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
Copyright Advanced Micro Devices, Inc.
|
|
3
|
+
SPDX-License-Identifier: MIT
|
|
4
|
+
-->
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# claw-connector
|
|
7
|
+
|
|
8
|
+
Lets you drive the coding agents on your own machine (Claude Code, Codex, Cursor) from Claw and the chat
|
|
9
|
+
clients built on it, for example from your phone.
|
|
10
|
+
|
|
11
|
+
The connector only makes **outbound HTTPS requests** to Claw. It opens no port, and your machine needs no public address. Claw relays
|
|
12
|
+
the conversation and keeps an audit trail; the agent itself, your files, your checkout and your model credentials
|
|
13
|
+
stay on your machine.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
phone / chat client --> Claw (remote agents) <-- connector on your machine --> agent adapter (stdio) --> your agent
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Claw's side is documented in [claw/docs/remote-agents.md](https://github.com/AMD-AGI/PrimusClaw/blob/main/claw/docs/remote-agents.md); the wire
|
|
20
|
+
contract is `claw/packages/protocol/src/remote-agents.ts`, compiled into this package.
|
|
21
|
+
|
|
22
|
+
## Quick start (about 5 minutes)
|
|
23
|
+
|
|
24
|
+
You need Node.js 22 or newer, a coding agent that already works on this machine (Claude Code, Codex or the Cursor CLI), and the
|
|
25
|
+
two addresses the people who run your Claw give you: the Claw API URL and the identity service URL. The addresses below are
|
|
26
|
+
placeholders. The Claude and Codex adapters are fetched by `npx` the first time a session starts, so the machine needs
|
|
27
|
+
access to the npm registry.
|
|
28
|
+
|
|
29
|
+
**1. Install**
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install -g @primusclaw/connector
|
|
33
|
+
claw-connector --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If npm answers `EACCES`, point its global prefix at a directory you own (`npm config set prefix ~/.npm-global`, then put
|
|
37
|
+
`~/.npm-global/bin` on your `PATH`) instead of using `sudo`. To update later, run the same `npm install -g` again.
|
|
38
|
+
|
|
39
|
+
**2. Choose what an agent may touch**
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
claw-connector init # writes agents.toml and prints where: ~/.config/claw-connector/agents.toml unless XDG_CONFIG_HOME or CLAW_CONNECTOR_HOME is set
|
|
43
|
+
claw-connector status # after you edit it: prints a "problem:" line if the file is wrong
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
By default every agent asks before it writes a file or runs a command, and your phone answers each question. To let phone sessions run
|
|
47
|
+
like your terminal instead, run `claw-connector init --approval never` (or set `approval = "never"` on an agent later); see
|
|
48
|
+
[Running without per-step approval](#running-without-per-step-approval).
|
|
49
|
+
|
|
50
|
+
Open the file `init` printed and set `roots`, the directories an agent may work in. The sample enables `claude`, working in `~/work`;
|
|
51
|
+
remove the `#` in front of the `codex` or `cursor` block to offer those too. A root that does not exist is reported as a problem, so
|
|
52
|
+
create it or change it. See [agents.toml](#agentstoml).
|
|
53
|
+
|
|
54
|
+
**3. Sign in**
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
claw-connector login --server https://claw.example.com/claw-api --identity-url https://identity.example.com
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`login` prints a link and a short code. Open the link in a browser where you are signed in to Claw, check the machine name shown
|
|
61
|
+
there, and confirm; the command then prints `signed in as <machine name>`. The connector holds a rotating sign-in token (file mode
|
|
62
|
+
0600) and refreshes its short-lived access token by itself. Put `server` and `identity_url` in `agents.toml` and the two
|
|
63
|
+
options can be left out.
|
|
64
|
+
|
|
65
|
+
If there is no browser sign-in to use (a development server), use a Claw API key instead. Pass it in `CLAW_CONNECTOR_KEY`
|
|
66
|
+
(replace the placeholder value) rather than with `--key`, which would show it in the process list:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
CLAW_CONNECTOR_KEY='replace-with-your-claw-api-key' claw-connector login --server https://claw.example.com/claw-api
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The key is stored as it is in `credentials.json` (mode 0600) and is for trying things out; the device sign-in is the normal way.
|
|
73
|
+
|
|
74
|
+
**4. Run**
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
claw-connector run
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It prints `connecting claude to https://claw.example.com/claw-api as "<machine name>"` and stays in the foreground: keep it in a
|
|
81
|
+
terminal tab, `tmux` or `screen`. Ctrl-C stops it and every agent it started. From another terminal, `claw-connector status` shows
|
|
82
|
+
whether it is running and signed in.
|
|
83
|
+
|
|
84
|
+
**5. See it in Clawy**
|
|
85
|
+
|
|
86
|
+
In Clawy (or any chat client built on Claw), send `/agents`. The agents of this machine are listed, online while `run` is up.
|
|
87
|
+
A session started there runs on this machine, inside one of your `roots`, and each permission request comes back as a card in the chat.
|
|
88
|
+
|
|
89
|
+
To disconnect: `claw-connector pause` (stay offline until `resume`), `claw-connector stop`, or `claw-connector logout` (also
|
|
90
|
+
asks the identity service to revoke the device sign-in; a saved key is deleted from this machine, not revoked at Claw).
|
|
91
|
+
|
|
92
|
+
## agents.toml
|
|
93
|
+
|
|
94
|
+
```toml
|
|
95
|
+
# Everything that limits an agent lives here, on your machine. Nothing Claw sends can widen it.
|
|
96
|
+
server = "https://claw.example.com/claw-api" # optional: --server / CLAW_URL
|
|
97
|
+
identity_url = "https://identity.example.com" # optional: --identity-url / CLAW_IDENTITY_URL
|
|
98
|
+
name = "my-laptop" # shown to you in Claw; defaults to the host name
|
|
99
|
+
|
|
100
|
+
[[agents]]
|
|
101
|
+
id = "claude"
|
|
102
|
+
preset = "claude" # claude | codex | cursor, or a custom command (below)
|
|
103
|
+
roots = ["~/work"] # required: a session's directory must be inside one of these
|
|
104
|
+
approval = "never" # optional, "ask" (the default) or "never": see "Running without per-step approval"
|
|
105
|
+
|
|
106
|
+
[[agents]]
|
|
107
|
+
id = "codex"
|
|
108
|
+
preset = "codex"
|
|
109
|
+
roots = ["~/work/repo-a", "~/work/repo-b"]
|
|
110
|
+
[agents.env] # passed to the adapter; the model settings live here, never in Claw
|
|
111
|
+
OPENAI_API_KEY = "..." # or leave your own environment / CODEX_HOME to supply it
|
|
112
|
+
|
|
113
|
+
[[agents]]
|
|
114
|
+
id = "mine"
|
|
115
|
+
kind = "my-agent" # a label chat clients pick an icon from
|
|
116
|
+
command = "my-agent-acp" # any agent that speaks the Agent Client Protocol over stdio
|
|
117
|
+
args = []
|
|
118
|
+
ask_mode = "careful" # the mode in which it asks before it writes (required for a custom agent)
|
|
119
|
+
roots = ["~/work"]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Presets and the adapter versions they pin (override `command` and `args` to move):
|
|
123
|
+
|
|
124
|
+
| Preset | Command | Mode every session is forced into | Modes a phone may select | With `approval = "never"` |
|
|
125
|
+
|---|---|---|---|---|
|
|
126
|
+
| `claude` | `npx @agentclientprotocol/claude-agent-acp@0.99.0` | `default` | `default`, `plan` | `bypassPermissions` |
|
|
127
|
+
| `claude-attach` | `npx @agentclientprotocol/claude-agent-acp@0.99.0` | `default` | `default`, `plan` | `bypassPermissions` |
|
|
128
|
+
| `codex` | `npx @agentclientprotocol/codex-acp@2.3.0` | `read-only` | `read-only` | `agent-full-access` |
|
|
129
|
+
| `cursor` | `agent acp` (the Cursor CLI) | `ask` | `ask`, `plan` | `agent`, started as `agent --force acp` |
|
|
130
|
+
|
|
131
|
+
`claude-attach` does not start new conversations: it takes over Claude Code conversations that already exist on this
|
|
132
|
+
machine (see [Taking over an existing conversation](#taking-over-an-existing-conversation)).
|
|
133
|
+
|
|
134
|
+
An agent with no `roots` is never offered. A configuration problem is reported at start-up, not skipped.
|
|
135
|
+
|
|
136
|
+
## Running without per-step approval
|
|
137
|
+
|
|
138
|
+
By default every agent asks before it writes a file or runs a command, and the phone answers each question. To let a
|
|
139
|
+
phone session run like your own terminal session (Claude `bypassPermissions`, Codex full access, Cursor `agent` with
|
|
140
|
+
`--force`), turn the questions off for that agent:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
claw-connector init --approval never # every sample agent
|
|
144
|
+
claw-connector init --approval claude=never # only Claude (repeatable: --approval never --approval cursor=ask)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
or set `approval = "never"` in an `[[agents]]` table of an existing `agents.toml`. It is per agent, so Claude can run
|
|
148
|
+
unattended while Cursor keeps asking. For that agent the connector then:
|
|
149
|
+
|
|
150
|
+
- starts every session in the preset's no-approval mode (the table above; for a custom agent, `ask_mode` names it);
|
|
151
|
+
- lets a phone select any mode the adapter offers (without the setting a phone can only select the safe modes);
|
|
152
|
+
- accepts an adapter's warning that it will not ask for permission, which happens when your own Claude settings default to
|
|
153
|
+
`bypassPermissions`, instead of refusing the session (`mode_not_enforced`). The warning is written to `audit.jsonl`
|
|
154
|
+
with `accepted: true`. Without the setting that refusal stays, except that `allow_unsafe_modes = true` in the file
|
|
155
|
+
accepts it too, for every agent that has no `approval` of its own (below);
|
|
156
|
+
- tells the phone: every agent reports `unattended` in its capabilities, and `session_started`, `session_resumed` and
|
|
157
|
+
`mode_update` carry `unattended`, so a client can always show whether a session asks. `status` and the line `run` prints
|
|
158
|
+
at start-up name the agents without per-step approval.
|
|
159
|
+
|
|
160
|
+
**The risk, in one sentence:** an agent without per-step approval acts on whatever it is told, including text it read in
|
|
161
|
+
a file or a web page, with everything your account can do inside its roots (and `--force` or full access reach outside the
|
|
162
|
+
sandbox the adapter would otherwise keep), and nobody is asked first.
|
|
163
|
+
|
|
164
|
+
**To go back:** set `approval = "ask"` on the agent and restart the connector (`claw-connector stop`, then `run`). The
|
|
165
|
+
policy is read again on start, so a session that was unattended resumes asking. Setting `"ask"` is the way back that
|
|
166
|
+
always works. Deleting the `approval = "never"` line is enough only when the file does not set `allow_unsafe_modes = true`:
|
|
167
|
+
that is the older, coarser form, which lets a phone select unsafe modes for every agent and accepts the warning for all
|
|
168
|
+
of them (but starts sessions in the asking mode), so an agent without an `approval` line of its own still resumes an
|
|
169
|
+
unattended session unattended (the phone shows it). `approval = "ask"` on an agent overrides it for that agent.
|
|
170
|
+
|
|
171
|
+
**Upgrading.** Two things change for an `agents.toml` that already exists, with no setting needed:
|
|
172
|
+
|
|
173
|
+
- A file with `allow_unsafe_modes = true` now also accepts an adapter's "will not ask for permission" warning for every
|
|
174
|
+
agent without its own `approval`. Such a session used to be closed with `mode_not_enforced`; now it opens and is
|
|
175
|
+
reported `unattended`. Set `approval = "ask"` on an agent to keep refusing it.
|
|
176
|
+
- A preset agent whose `safe_modes` or `ask_mode` names a mode the preset does not know to ask (for example `claude`
|
|
177
|
+
with `safe_modes = ["default", "plan", "acceptEdits"]`) is now a configuration error, and `claw-connector run` does not
|
|
178
|
+
start. Remove the mode, or set `approval = "never"` on that agent (or `allow_unsafe_modes = true` in the file, for an
|
|
179
|
+
agent that does not say `approval = "ask"`).
|
|
180
|
+
|
|
181
|
+
## What the connector enforces
|
|
182
|
+
|
|
183
|
+
All of these run on your machine and fail closed.
|
|
184
|
+
|
|
185
|
+
- **Directories.** A session's working directory is resolved (symlinks included) and must be inside one of the agent's
|
|
186
|
+
`roots`; otherwise no process is started and the session is closed with `cwd_outside_roots`. The agent runs in the
|
|
187
|
+
resolved path.
|
|
188
|
+
- **Ask mode, read back.** After the adapter creates a session, the connector sets the agent's ask mode and reads the
|
|
189
|
+
mode back from the adapter. Your own agent settings may start an adapter in a mode that never asks (bypass, full
|
|
190
|
+
access); an adapter that does not confirm the ask mode, or that warns it will not invoke its permission callback, is
|
|
191
|
+
not opened, unless you set `approval = "never"` for that agent (above) or `allow_unsafe_modes = true` for the file,
|
|
192
|
+
either of which accepts the warning and records it. A mode
|
|
193
|
+
change the agent makes on its own to a mode outside the allowed list is undone at once. The adapter's
|
|
194
|
+
`session/resume` can reset the mode, so it is locked again after every resume.
|
|
195
|
+
- **Unsafe modes.** Modes in which the agent does not ask before writing (`acceptEdits`, `bypassPermissions`, Codex write and full-access
|
|
196
|
+
modes, Cursor `agent`) cannot be selected from a phone. `approval = "never"` on an agent, or `allow_unsafe_modes = true`
|
|
197
|
+
for the whole file, in `agents.toml` is the only way, and it is yours alone. Flags that turn a tool's own approvals off
|
|
198
|
+
(`--force`, `--yolo`, ...) in an agent's `args` are refused unless one of those is set too. A session recorded in a mode
|
|
199
|
+
you no longer allow comes back in the ask mode. On a preset agent `safe_modes` can only narrow what the preset
|
|
200
|
+
knows to ask (naming another mode is a configuration error unless the agent has `approval = "never"` or the file has
|
|
201
|
+
`allow_unsafe_modes`), and a session in a mode the preset does not know to ask is reported `unattended`.
|
|
202
|
+
- **Cursor.** Measured on `agent acp` 2026.10.22: in `agent` mode a shell command asks (options `allow-once`, `allow-always`,
|
|
203
|
+
`reject-once`) but a file edit is applied without asking, whatever the approval setting. So the preset starts in `ask`,
|
|
204
|
+
which cannot write. `approval = "never"` selects `agent` and starts `agent --force acp` (`--force` is a root flag of the
|
|
205
|
+
Cursor CLI, "force allow commands unless explicitly denied"; it is accepted before or after `acp`). Cursor
|
|
206
|
+
has no `session/resume`; the connector uses `session/load` and marks the replayed history `replay: true`.
|
|
207
|
+
- **Permissions.** Each permission request becomes a card with the options **the adapter offered**, passed through with
|
|
208
|
+
their own ids and kinds (`allow_once`, `allow_always`, `reject_once`). An answer must be one of those ids; any other is refused
|
|
209
|
+
and the request stays open. An unanswered request is answered `cancelled` after 10 minutes, and a cancel,
|
|
210
|
+
a close or an adapter exit answers every waiting request `cancelled`. Cursor's `cursor/create_plan` and a single
|
|
211
|
+
`cursor/ask_question` are shown as cards; a question the phone cannot carry is skipped, visibly, so the agent never waits.
|
|
212
|
+
- **Automatic approval, by rule.** `auto_allow = [{ kind = "edit", path_glob = "docs/**" }]` (at file level or per agent) lets
|
|
213
|
+
a call through without asking when its kind matches and every path it touches is inside the session directory and
|
|
214
|
+
matches the glob. It uses the one-time option, never an "always" one, is decided on your machine, is written to the
|
|
215
|
+
audit log and is shown on the phone. The default is no rules.
|
|
216
|
+
- **Credentials go over HTTPS only.** The Claw URL and the identity URL must be `https://` (`http://` is accepted only for
|
|
217
|
+
localhost, for a development server); `login` and `run` refuse anything else before a token is saved or sent.
|
|
218
|
+
- **Existing conversations.** Taken over only when no other process holds them open, only from inside the roots, only in the
|
|
219
|
+
directory the conversation itself records, and in the ask mode (see above).
|
|
220
|
+
- **No remote shell.** Only these commands are carried out: `session.new`, `prompt`, `cancel`, `set_mode`, `resume`,
|
|
221
|
+
`close`, `decision`. Anything else is dropped and audited. Claw never sends a command line, an environment variable or a
|
|
222
|
+
key.
|
|
223
|
+
- **What leaves the machine.** Agent output, tool calls, diffs and command output are uploaded to Claw's event store (so
|
|
224
|
+
your phone can show them), after credential-shaped strings (API keys, tokens, bearer headers, private keys, JWTs) are
|
|
225
|
+
masked and long tool output is cut to 8 KB. Adapter stderr never leaves; it goes to `logs/` under the state directory.
|
|
226
|
+
A prompt's text is not written to the local audit log, only its hash and length.
|
|
227
|
+
|
|
228
|
+
## Taking over an existing conversation
|
|
229
|
+
|
|
230
|
+
An agent made from the `claude-attach` preset lists the Claude Code conversations on this machine whose directory is inside
|
|
231
|
+
its `roots`, and lets you continue one from your phone. It reports them to Claw as an opaque list (`capabilities.resumable`:
|
|
232
|
+
reference, title, directory, last activity, and whether anything holds it open), newest first, at most a dozen, so that a
|
|
233
|
+
chat client can offer a choice; `session.new` then names the chosen one (`resume_ref`). Claw and the chat client know no
|
|
234
|
+
product: the agent's `kind` is the label `attach`.
|
|
235
|
+
|
|
236
|
+
```toml
|
|
237
|
+
[[agents]]
|
|
238
|
+
id = "claude-attach"
|
|
239
|
+
preset = "claude-attach"
|
|
240
|
+
roots = ["~/work"]
|
|
241
|
+
# attach_cli = "claude" # the command asked which sessions are open (default: claude, from PATH)
|
|
242
|
+
# [agents.env]
|
|
243
|
+
# CLAUDE_CONFIG_DIR = "/home/me/.claude" # where the conversations live, if not the default
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A taken-over conversation asks before every write and command like any other session. `approval = "never"` in this table
|
|
247
|
+
works as it does for `claude`: the conversation is resumed in `bypassPermissions` and the phone shows it as unattended
|
|
248
|
+
(see [Running without per-step approval](#running-without-per-step-approval)).
|
|
249
|
+
|
|
250
|
+
What you do: **exit the conversation in the terminal first** (`/exit`), then pick it on the phone. When you are done,
|
|
251
|
+
close it from the phone and continue in the terminal with `claude --resume <id>`: the turns you took on the phone are in
|
|
252
|
+
its history, because both sides read and append to the same transcript.
|
|
253
|
+
|
|
254
|
+
Why the terminal must be closed: Claude Code does not stop a second process from resuming a conversation that is still
|
|
255
|
+
open, and two writers fork the transcript; the next resume follows only one branch, so a turn is lost without any error
|
|
256
|
+
(measured). The connector therefore refuses to take over a conversation that another process holds open, and it checks again
|
|
257
|
+
before it starts the agent again after a pause and before every prompt. A prompt that meets a terminal is rejected, not sent
|
|
258
|
+
(the session stays, and the prompt can be sent again once the terminal is closed), and the connector's agent for the
|
|
259
|
+
conversation is discarded: what the terminal wrote is not in its memory, so the next prompt starts a fresh one that reads
|
|
260
|
+
the transcript. The same happens when the transcript is no longer the size the connector left it, which is how a terminal
|
|
261
|
+
that came and went between two prompts is noticed.
|
|
262
|
+
|
|
263
|
+
While the connector's own agent is working on the conversation (starting, or running a turn) the size cannot say whether
|
|
264
|
+
someone else wrote, because the agent's own writes grow it too. So during that time the registry is read every second and
|
|
265
|
+
once more, in full, when the work ends. A terminal that held the conversation open for about a second or more during a turn
|
|
266
|
+
is noticed even if it has exited by the next prompt: that prompt goes to a fresh agent, after the full check above. One seen
|
|
267
|
+
while the agent is starting refuses the takeover (`session_live`) and stops the agent. A terminal that opens and closes the
|
|
268
|
+
conversation between two reads, which is shorter than anything that can take a turn, is not seen.
|
|
269
|
+
|
|
270
|
+
The first thing asked is the terminal itself. `claw-connector init` (or `claw-connector hooks install`) adds two Claude Code
|
|
271
|
+
hooks, `SessionStart` and `SessionEnd`, to the user-level `settings.json` of the Claude config directory. Each runs a tiny
|
|
272
|
+
program that keeps one row per terminal session, `<config dir>/claw-connector-terminals/<pid>.json`: the session id and the
|
|
273
|
+
process that holds it, with that process's start time. Claude Code runs the same hooks for sessions started by an ACP adapter
|
|
274
|
+
(this connector's own included) and by `claude -p`; they are told apart by `CLAUDE_CODE_ENTRYPOINT` in the hook's environment
|
|
275
|
+
(`cli` in a terminal, `sdk-cli` otherwise), so only terminal sessions are recorded. `SessionEnd` runs on `/exit`, Ctrl-C and
|
|
276
|
+
Ctrl-D twice, a closed terminal (SIGHUP) and a plain `kill`; it does not run after `kill -9` or a power cut, so a row counts
|
|
277
|
+
only while the process it names is still the same process (same start time), and the next hook sweeps dead rows away. The
|
|
278
|
+
hooks are merged into `settings.json` without touching anything else in it (a file that is not valid JSON is left alone), take
|
|
279
|
+
under two seconds at most (`SessionStart` holds up the first turn, so they have timeouts of 2 s and 1 s), print nothing and
|
|
280
|
+
always exit 0, so they cannot get in the way of a session. `claw-connector hooks uninstall` takes exactly those entries out
|
|
281
|
+
again (they carry the mark `--managed-by claw-connector`; a hook of your own is never taken for one of them). The entries name this installation's node and program by absolute path, so install from a durable one (`npm i -g
|
|
282
|
+
@primusclaw/connector`) rather than a throwaway `npx` cache; `hooks status` and `run` say when a path is gone, and installing
|
|
283
|
+
again repairs it. A conversation that was opened before the hooks were installed is not in the rows, and neither is one under a
|
|
284
|
+
different config directory or when Claude Code is configured not to run user hooks: the rows can only add a refusal, and
|
|
285
|
+
everything below stays in force as the fallback (`claw-connector hooks status` says where they stand).
|
|
286
|
+
|
|
287
|
+
Who holds the conversation open is then asked of Claude Code itself
|
|
288
|
+
(`claude agents --json`, which lists the live processes of this machine and ignores the files a killed one leaves behind)
|
|
289
|
+
and of the registry Claude Code keeps in `$CLAUDE_CONFIG_DIR/sessions/`, whose rows are each checked against the process they
|
|
290
|
+
name. The command does not report a row from another host or container that shares the directory (it asks whether the pid is
|
|
291
|
+
alive here), and such a row cannot be checked: it counts as open whatever the command says. If the command is missing or
|
|
292
|
+
fails, the registry alone is the answer; if neither can be read, the answer is "cannot tell" and the takeover is refused.
|
|
293
|
+
A transcript written within the last 10 seconds is refused too (a terminal that has only just exited may still be writing). The reasons Claw is told are
|
|
294
|
+
`session_live`, `recently_written` and `twin_check_failed`; the agent that this connector itself started for the conversation
|
|
295
|
+
is not mistaken for a twin.
|
|
296
|
+
|
|
297
|
+
The ask mode still applies: a conversation keeps the mode it was left in, often one that never asks (`bypassPermissions`),
|
|
298
|
+
and the adapter says so while it sets the session up. That warning describes the mode that is about to be replaced, so it is
|
|
299
|
+
set aside until the ask mode (`default`) is locked and read back from the adapter; a warning that comes after that, or a mode
|
|
300
|
+
that is not confirmed, closes the session. The phone can select only the safe modes, as for every agent. Closing the
|
|
301
|
+
session stops the agent process; it never deletes the conversation.
|
|
302
|
+
|
|
303
|
+
## Commands
|
|
304
|
+
|
|
305
|
+
| Command | |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `init [--approval ask\|never \| AGENT=ask\|never] [--no-hooks]` | Write a sample `agents.toml`; an existing file is never touched. `--approval never` turns per-step approval off (see above). Also installs the Claude Code session hooks (`--no-hooks` skips that). |
|
|
308
|
+
| `hooks install\|uninstall\|status` | The `SessionStart`/`SessionEnd` hooks alone; `--config-dir DIR` names the Claude config directory (default: those of the `claude-attach` agents, else `$CLAUDE_CONFIG_DIR` or `~/.claude`). |
|
|
309
|
+
| `login` | Device sign-in. `--key <api key>` (or `CLAW_CONNECTOR_KEY`) is for development only. After a revocation, run it again: it connects this machine as a new connector. |
|
|
310
|
+
| `run` | Connect and serve. Exit code 3 means the connector was revoked. |
|
|
311
|
+
| `status [--json]` | Running or not, paused, signed in, agents, adapter processes. |
|
|
312
|
+
| `pause` / `resume` | Stop every agent within a second and stay offline, or undo that. |
|
|
313
|
+
| `stop` | Stop the running connector and every agent it started. |
|
|
314
|
+
| `logout` | Stop, ask the identity service to revoke the sign-in, delete it from this machine. The stored connector id stays, so the next sign-in reuses the same connector. |
|
|
315
|
+
|
|
316
|
+
**Kill switch.** Every adapter is started in its own process group and recorded (pid and start time) in
|
|
317
|
+
`children.json`. `stop`, `pause`, a revocation and Ctrl-C signal exactly those groups: a process that is not recorded, or
|
|
318
|
+
whose pid has been reused, is never signalled, and nothing here uses a pattern kill. If an adapter's group leader (an `npx` wrapper) died but the
|
|
319
|
+
agent it started is still in that group, the group is still stopped. If you disconnect the connector in Claw,
|
|
320
|
+
the connector sees it within a heartbeat, stops every agent and exits without retrying.
|
|
321
|
+
|
|
322
|
+
## Files
|
|
323
|
+
|
|
324
|
+
`~/.config/claw-connector/` (or `$XDG_CONFIG_HOME/claw-connector/`): `agents.toml`, `credentials.json` (0600; a file other users can read is
|
|
325
|
+
refused). `~/.local/state/claw-connector/` (or `$XDG_STATE_HOME/claw-connector/`): `connector.json` (the connector's identity and the
|
|
326
|
+
commands handled), `sessions.json`, `outbox/` (events not yet acknowledged by Claw), `audit.jsonl` (spawn, directory
|
|
327
|
+
decisions, every command received, every permission request and answer, every automatic approval, mode changes),
|
|
328
|
+
`logs/`, `children.json`, `connector.pid`. `CLAW_CONNECTOR_HOME=/some/dir` keeps all of it under one directory.
|
|
329
|
+
|
|
330
|
+
## Restarts and crashes
|
|
331
|
+
|
|
332
|
+
A prompt counts as handled only when it reaches the agent (or is dropped by a cancel, a close or a stop), so a connector that
|
|
333
|
+
dies with a prompt still queued is sent that prompt again by Claw instead of losing it. A permission request waiting
|
|
334
|
+
for an answer lives in the adapter, which dies with the connector: the next run closes such a card (`cancelled`, by
|
|
335
|
+
`connector_restart`), so a phone is not left with a button that does nothing. Session state, unacknowledged events and
|
|
336
|
+
their sequence numbers are on disk; a session resumes in a fresh adapter on its next prompt.
|
|
337
|
+
|
|
338
|
+
Known limit: the working-directory check resolves the path and starts the adapter in the resolved path, but another process of
|
|
339
|
+
the same user that can rewrite a directory in that path could swap a symlink between the check and the start. The
|
|
340
|
+
whitelist protects against what Claw (or a phone) asks for, not against another local process of the same user, which could already
|
|
341
|
+
run the agent itself.
|
|
342
|
+
|
|
343
|
+
## Connection behaviour
|
|
344
|
+
|
|
345
|
+
One long-lived `text/event-stream` carries commands to the connector; events go back in batches (150 ms or 20 events,
|
|
346
|
+
one request at a time). Every event has a per-session sequence number written to disk before it is queued, and stays
|
|
347
|
+
there until Claw acknowledges it, so a dropped connection, a failed request or a restart loses nothing and, because Claw
|
|
348
|
+
stores each `(session, seq)` once, repeats nothing. The connector sends a heartbeat every 30 s, drops a stream that is silent for 75 s
|
|
349
|
+
and reconnects with a 1 s to 60 s backoff with jitter. Commands are delivered at least once and carried out once
|
|
350
|
+
(by command id and number). A card still waiting after a reconnect is sent again with the same action id.
|
|
351
|
+
|
|
352
|
+
## Development
|
|
353
|
+
|
|
354
|
+
```sh
|
|
355
|
+
cd connector
|
|
356
|
+
npm ci
|
|
357
|
+
npm run typecheck && npm test # node:test against a stand-in Claw and a stand-in ACP adapter (real processes, real sockets)
|
|
358
|
+
npm run build # dist/cli.js and dist/terminal-hook.js; the wire contract from claw/packages/protocol is compiled in
|
|
359
|
+
node scripts/check-pack.mjs # packs the tarball: it must hold only dist/cli.js, dist/terminal-hook.js, README.md, LICENSE and package.json
|
|
360
|
+
npm pack && npm install -g ./primusclaw-connector-*.tgz # the package as the registry serves it, from a checkout
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Releasing (maintainers)
|
|
364
|
+
|
|
365
|
+
A release is a GitHub Release whose tag is `connector-vX.Y.Z`, cut the way Hyperloom's are. Publishing it starts
|
|
366
|
+
[connector-release.yml](https://github.com/AMD-AGI/PrimusClaw/blob/main/.github/workflows/connector-release.yml): it builds, tests and
|
|
367
|
+
checks the tarball, signs a build-provenance attestation for it, attaches it to the release and publishes it to npm with Trusted Publishing
|
|
368
|
+
(an OIDC credential; no npm token is stored anywhere). Only stable `MAJOR.MINOR.PATCH` versions are released: a prerelease needs a dist-tag,
|
|
369
|
+
which OIDC publishing sets only with a newer npm and a separate permission on the Trusted Publisher entry.
|
|
370
|
+
|
|
371
|
+
**What stops an unreviewed publish.** A release runs the workflow file as it exists at its tag, and a tag can point at any commit, so the
|
|
372
|
+
checks inside that file (tag shape, the commit being in `main`, the environment below having a reviewer) catch honest mistakes and are not the
|
|
373
|
+
control. The control is the GitHub environment `npm-publish`: one of its required reviewers must approve the `publish` job, and the Trusted
|
|
374
|
+
Publisher entry on npmjs.com names that environment, so npm hands the publish credential to no other job. Only the `publish` job, which checks out
|
|
375
|
+
and installs nothing, runs behind it. Two more rules narrow who can get that far. The environment does not let administrators bypass its
|
|
376
|
+
protection, and a tag ruleset limits creating `refs/tags/connector-v*` to repository admins, so only an admin can start a release run at all.
|
|
377
|
+
Self-review is allowed: the maintainer who cuts a release may approve its publish, so the approval is an explicit decision by a listed reviewer
|
|
378
|
+
and not a second person's check. `scripts/release-tests/connector-release-workflow.sh`, run on every change by the Connector packaging
|
|
379
|
+
workflow, fails if the workflow file stops having that shape, or if its environment check would refuse the environment as set up below.
|
|
380
|
+
|
|
381
|
+
**One-time setup** (a repository admin, and a member of the `primusclaw` npm organisation). The order matters.
|
|
382
|
+
|
|
383
|
+
1. Create the environment `npm-publish` (repository Settings > Environments) with **Required reviewers** (the people who may publish) and
|
|
384
|
+
**Allow administrators to bypass** off. **Prevent self-review** is left off, so a reviewer who cuts a release may approve it (turning it on is
|
|
385
|
+
accepted too). The release run refuses to build unless the environment has at least one required reviewer and administrator bypass is off.
|
|
386
|
+
Then add a tag ruleset (Settings > Rules > Rulesets, target: tags) matching `refs/tags/connector-v*` that restricts creation to repository
|
|
387
|
+
admins: with self-review allowed this is what limits who can start a release run, and a deployment tag rule on the environment alone does
|
|
388
|
+
not help, since the tag of an unreviewed commit matches it too. Do not restrict the environment to "Protected branches only": a release's
|
|
389
|
+
ref is a tag, so it would be refused.
|
|
390
|
+
2. Publish 0.1.0 once by hand from a clean checkout of the merged `main` (the package must exist before it can be given a Trusted Publisher):
|
|
391
|
+
`cd connector && npm ci && npm publish --access public`, logged in to npm with 2FA.
|
|
392
|
+
3. On npmjs.com, Packages > `@primusclaw/connector` > Settings > Trusted publishing > GitHub Actions: Organization `AMD-AGI`, Repository
|
|
393
|
+
`PrimusClaw`, Workflow filename `connector-release.yml`, Environment `npm-publish` (all case-sensitive; create the entry only after step 1).
|
|
394
|
+
Allowed actions: `npm publish`, which is what the workflow runs; leave dist-tag management off. The entry must see its first successful
|
|
395
|
+
publish within 2 days or it expires.
|
|
396
|
+
4. Settings > Publishing access: require two-factor authentication and disallow tokens.
|
|
397
|
+
|
|
398
|
+
**Cutting a release.**
|
|
399
|
+
|
|
400
|
+
1. Set the new version in `package.json` and `src/version.ts` (`scripts/check-pack.mjs` fails if they disagree) and merge that to `main`.
|
|
401
|
+
2. Write the release notes from the PR descriptions, not from commit subjects: every change states in its PR what a user will now observe.
|
|
402
|
+
For the PRs that touched `connector/` since the previous release (`prev` is its tag, or for the first workflow release the commit 0.1.0 was
|
|
403
|
+
published from):
|
|
404
|
+
```sh
|
|
405
|
+
prev=connector-v0.1.0
|
|
406
|
+
git fetch origin main --tags
|
|
407
|
+
for n in $(git log --first-parent --format=%s "$prev"..origin/main -- connector/ | grep -o '(#[0-9]*)$' | tr -d '(#)'); do
|
|
408
|
+
gh pr view "$n" --repo AMD-AGI/PrimusClaw --json number,title,body --jq '"## #\(.number) \(.title)\n\n\(.body)\n"'
|
|
409
|
+
done > notes.md
|
|
410
|
+
```
|
|
411
|
+
Edit `notes.md` into the notes a user of the package wants to read.
|
|
412
|
+
3. Publish the release on the merged commit (not as a pre-release):
|
|
413
|
+
`gh release create connector-v0.1.1 --target <sha-on-main> --title "claw-connector 0.1.1" --notes-file notes.md`
|
|
414
|
+
4. The `package` job runs and writes a summary: the commit, whether the release workflow at that commit equals the one on `main`, and the
|
|
415
|
+
tarball's name, sha256 and files. A reviewer opens the run, checks that the tagged commit is the reviewed one on `main` and that the
|
|
416
|
+
workflow file is `main`'s (if the summary says it is different, read the diff first), and approves `publish`. The job then attests the
|
|
417
|
+
tarball, attaches it to the release and publishes it.
|
|
418
|
+
5. Check it: `npm view @primusclaw/connector version`, and `gh attestation verify primusclaw-connector-0.1.1.tgz --repo AMD-AGI/PrimusClaw`
|
|
419
|
+
on the tarball attached to the release.
|