pwragent 0.0.0 → 1.3.8
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/README.md +494 -2
- package/bin/pwragent.js +10 -1
- package/dist/THIRD_PARTY_LICENSES +165 -0
- package/dist/federation-core.js +19573 -0
- package/dist/ws-LICENSE +20 -0
- package/examples/pwragent-headless.service +25 -0
- package/licenses/xterm-LICENSE +26 -0
- package/node_modules/node-addon-api/LICENSE.md +9 -0
- package/node_modules/node-addon-api/README.md +319 -0
- package/node_modules/node-addon-api/common.gypi +20 -0
- package/node_modules/node-addon-api/except.gypi +25 -0
- package/node_modules/node-addon-api/index.js +12 -0
- package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/node_modules/node-addon-api/napi-inl.h +6607 -0
- package/node_modules/node-addon-api/napi.h +3201 -0
- package/node_modules/node-addon-api/node_addon_api.gyp +32 -0
- package/node_modules/node-addon-api/node_api.gyp +9 -0
- package/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/node_modules/node-addon-api/nothing.c +0 -0
- package/node_modules/node-addon-api/package-support.json +21 -0
- package/node_modules/node-addon-api/package.json +480 -0
- package/node_modules/node-addon-api/tools/README.md +73 -0
- package/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/node_modules/node-addon-api/tools/eslint-format.js +79 -0
- package/node_modules/node-pty/LICENSE +69 -0
- package/node_modules/node-pty/README.md +164 -0
- package/node_modules/node-pty/binding.gyp +150 -0
- package/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/node_modules/node-pty/lib/index.js +52 -0
- package/node_modules/node-pty/lib/interfaces.js +7 -0
- package/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/node_modules/node-pty/lib/terminal.js +190 -0
- package/node_modules/node-pty/lib/types.js +7 -0
- package/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/node_modules/node-pty/lib/utils.js +38 -0
- package/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/node_modules/node-pty/package.json +66 -0
- package/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.pdb +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.pdb +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.pdb +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.pdb +0 -0
- package/node_modules/node-pty/scripts/post-install.js +76 -0
- package/node_modules/node-pty/scripts/prebuild.js +34 -0
- package/node_modules/node-pty/src/unix/pty.cc +876 -0
- package/node_modules/node-pty/src/unix/spawn-helper.cc +23 -0
- package/node_modules/node-pty/src/win/conpty.cc +616 -0
- package/node_modules/node-pty/src/win/conpty.h +41 -0
- package/node_modules/node-pty/src/win/conpty_console_list.cc +44 -0
- package/node_modules/node-pty/src/win/path_util.cc +95 -0
- package/node_modules/node-pty/src/win/path_util.h +26 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-arm64/OpenConsole.exe +0 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-arm64/conpty.dll +0 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-x64/OpenConsole.exe +0 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-x64/conpty.dll +0 -0
- package/node_modules/node-pty/typings/node-pty.d.ts +215 -0
- package/package.json +32 -5
- package/src/cli.js +123 -0
- package/src/client.js +27 -0
- package/src/codex.js +112 -0
- package/src/daemon.js +125 -0
- package/src/federation-backend.js +453 -0
- package/src/federation-core.ts +24 -0
- package/src/federation.js +454 -0
- package/src/handoffs.js +141 -0
- package/src/projects.js +140 -0
- package/src/runtime.js +265 -0
- package/src/store.js +80 -0
- package/src/wire.js +33 -0
package/README.md
CHANGED
|
@@ -1,4 +1,496 @@
|
|
|
1
|
-
# PwrAgent
|
|
1
|
+
# PwrAgent Headless
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Experimental Node-only daemon and CLI for a dedicated Linux outpost. The daemon
|
|
4
|
+
owns a local Codex App Server and keeps an accepted turn running after the CLI
|
|
5
|
+
or its SSH connection disconnects. Federation uses the same encrypted transport,
|
|
6
|
+
signed enrollment and capability-checked routing as the desktop. There is no
|
|
7
|
+
Electron, display server, provider SDK, or Sandboxer API dependency. Remote
|
|
8
|
+
terminals use the pinned `node-pty` native dependency, loaded when a terminal
|
|
9
|
+
opens. The packaged CLI bundles its shared JavaScript services and their notices.
|
|
4
10
|
|
|
11
|
+
It supports one active Codex turn per profile, local job commands, and remote
|
|
12
|
+
desktop navigation, project creation/cloning, repository-aware thread intake,
|
|
13
|
+
remote terminals, thread creation, continuation, transcript reads, subscribed
|
|
14
|
+
streaming events, steering, interruption, naming and model/execution settings.
|
|
15
|
+
It can consume authorized messaging services from a provider-owning peer.
|
|
16
|
+
The daemon owns its execution lifetime; an SSH forward only provides access.
|
|
17
|
+
|
|
18
|
+
## Install and update
|
|
19
|
+
|
|
20
|
+
Outpost ships with every desktop release, using the exact desktop version and
|
|
21
|
+
source commit. Suffix-free versions use npm `latest`; alpha, beta, prerelease
|
|
22
|
+
and rc versions use `next`. The existing npm `0.0.0` placeholder does not
|
|
23
|
+
provide this implementation; these commands require a successful paired release.
|
|
24
|
+
Use an exact version to match a desktop installation. See the
|
|
25
|
+
[release contract](../../docs/headless-npm-release.md) for maintenance channels
|
|
26
|
+
and publication verification.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install --global pwragent@next
|
|
30
|
+
pwragent --help
|
|
31
|
+
# Update to the newest headless prerelease:
|
|
32
|
+
npm install --global pwragent@next
|
|
33
|
+
# Or choose a reproducible version:
|
|
34
|
+
npm install --global pwragent@1.3.6
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Provide **Node >=22 and npm separately**, on Linux (macOS is also supported for
|
|
38
|
+
development). The npm package includes the JavaScript federation bundle and its
|
|
39
|
+
license notice; it does not need Electron, pnpm, a compiler or workspace packages
|
|
40
|
+
on the destination machine. It supports **Codex only**. Install the Codex CLI
|
|
41
|
+
separately and authenticate a dedicated Codex home as the eventual service user.
|
|
42
|
+
The adapter follows the Codex **0.159.2** App Server contract; compatibility with
|
|
43
|
+
other versions is not guaranteed. PwrAgent neither installs nor authenticates
|
|
44
|
+
Codex and does not bundle Node or provider credentials.
|
|
45
|
+
|
|
46
|
+
Start the installed CLI with the same `serve` options shown below, replacing
|
|
47
|
+
`node packages/pwragent/bin/pwragent.js` with `pwragent`. Upgrading package files
|
|
48
|
+
does not upgrade an already running process: finish or interrupt active work,
|
|
49
|
+
stop the daemon, install the selected version, and start it again. Do not remove
|
|
50
|
+
the PwrAgent root, Codex home or workspace during updates. There is no automatic
|
|
51
|
+
CLI updater or automatic systemd enablement.
|
|
52
|
+
|
|
53
|
+
## systemd installation example
|
|
54
|
+
|
|
55
|
+
On a Linux host with systemd, an administrator must first create the dedicated
|
|
56
|
+
unprivileged `pwragent` user/group and provision these non-overlapping directories:
|
|
57
|
+
|
|
58
|
+
| Directory | Owner / purpose |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `/srv/pwragent/workspace` | `pwragent`, existing task workspace |
|
|
61
|
+
| `/var/lib/pwragent` | `pwragent`, private PwrAgent root |
|
|
62
|
+
| `/var/lib/codex-outpost` | `pwragent`, private Codex home authenticated separately |
|
|
63
|
+
| `/opt/pwragent` | root, npm installation prefix |
|
|
64
|
+
|
|
65
|
+
Keep both state homes private (0700). Verify the service user can execute Node
|
|
66
|
+
>=22 at `/usr/bin/node` and its separately installed Codex at
|
|
67
|
+
`/usr/local/bin/codex`; edit the template if either absolute path differs. A
|
|
68
|
+
shell's Node version-manager initialization does not run under systemd.
|
|
69
|
+
|
|
70
|
+
After publication, install a pinned version into the service prefix:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
sudo npm install --prefix /opt/pwragent --omit=dev --ignore-scripts pwragent@1.3.6
|
|
74
|
+
sudo install -m 0644 /opt/pwragent/node_modules/pwragent/examples/pwragent-headless.service /etc/systemd/system/pwragent-headless.service
|
|
75
|
+
# Review the account, paths, profile and Codex configuration before enabling:
|
|
76
|
+
sudo systemctl daemon-reload
|
|
77
|
+
sudo systemctl enable --now pwragent-headless.service
|
|
78
|
+
sudo systemctl status pwragent-headless.service
|
|
79
|
+
sudo journalctl -u pwragent-headless.service
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The service runs as `pwragent`, with `PWRAGENT_HOME=/var/lib/pwragent` and
|
|
83
|
+
`PWRAGENT_PROFILE=outpost`. Local client commands must use that same account,
|
|
84
|
+
root and profile, for example:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
sudo -u pwragent env PWRAGENT_HOME=/var/lib/pwragent PWRAGENT_PROFILE=outpost \
|
|
88
|
+
/usr/bin/node /opt/pwragent/node_modules/pwragent/bin/pwragent.js status
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For a service update, finish or interrupt active work, run
|
|
92
|
+
`sudo systemctl stop pwragent-headless.service`, repeat the prefix installation
|
|
93
|
+
with the chosen new version (or `pwragent@next`), then run
|
|
94
|
+
`sudo systemctl start pwragent-headless.service`. Compare the new example unit
|
|
95
|
+
with the installed one before deliberately updating it; npm does not overwrite
|
|
96
|
+
`/etc/systemd/system`. Stopping during a turn can leave its outcome `unknown`.
|
|
97
|
+
|
|
98
|
+
## Run from a checkout
|
|
99
|
+
|
|
100
|
+
Use Node 22 or newer on Linux (macOS is also supported for development). Provide
|
|
101
|
+
an installed Codex executable, an existing workspace, and an existing dedicated
|
|
102
|
+
Codex home with authentication configured by the operator. Install this checkout's
|
|
103
|
+
dependencies with `pnpm install`, then run `pnpm --filter pwragent build`. A packed
|
|
104
|
+
tarball already includes the bundle. The protocol adapter follows the repository's
|
|
105
|
+
Codex 0.159.2 contract; compatibility with other versions is not guaranteed.
|
|
106
|
+
Validation uses a fake App Server, not real inference or live accounts.
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
node packages/pwragent/bin/pwragent.js serve \
|
|
110
|
+
--profile outpost \
|
|
111
|
+
--workspace /srv/pwragent/workspace \
|
|
112
|
+
--codex-home /var/lib/codex-outpost \
|
|
113
|
+
--codex-bin /usr/local/bin/codex
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`serve` stays in the foreground. To keep it alive while a laptop is off, an
|
|
117
|
+
outpost service manager must own it. A reviewed systemd template is in
|
|
118
|
+
[`examples/pwragent-headless.service`](examples/pwragent-headless.service).
|
|
119
|
+
The template is not installed or enabled by the CLI. Its account and absolute
|
|
120
|
+
paths are placeholders. `KillMode=control-group` is needed to reap tools that
|
|
121
|
+
start a new session or process group. Running `serve` in an SSH foreground shell
|
|
122
|
+
alone does not provide this lifetime guarantee.
|
|
123
|
+
|
|
124
|
+
Run subsequent commands on the outpost, as the same OS user and with the same
|
|
125
|
+
`PWRAGENT_HOME` and profile. Write the task text to a local file, then:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
node packages/pwragent/bin/pwragent.js submit --profile outpost \
|
|
129
|
+
--id task-001 --prompt-file /path/to/task.txt --sandbox workspace-write
|
|
130
|
+
node packages/pwragent/bin/pwragent.js status --profile outpost
|
|
131
|
+
node packages/pwragent/bin/pwragent.js show --profile outpost --id task-001
|
|
132
|
+
node packages/pwragent/bin/pwragent.js wait --profile outpost --id task-001
|
|
133
|
+
node packages/pwragent/bin/pwragent.js read --profile outpost --id task-001
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
All command results are JSON. `submit` returns **acceptance**, not completion.
|
|
137
|
+
`wait` polls metadata once per second and exits 0 only for `completed`, 2 for
|
|
138
|
+
every other terminal outcome, and 1 for a client/runtime error. Closing `wait`
|
|
139
|
+
does not cancel work. `list` returns the newest 100 jobs; `show --id` retrieves
|
|
140
|
+
older retained jobs. `interrupt --id` requests interruption of an active turn.
|
|
141
|
+
During initial dispatch it reports that the turn ID is not yet confirmed; retry
|
|
142
|
+
once `show` returns a turn ID.
|
|
143
|
+
|
|
144
|
+
To continue a conversation, submit a **new job ID** with `--thread THREAD_ID`
|
|
145
|
+
using the `threadId` returned by a prior job. The daemon only accepts thread IDs
|
|
146
|
+
already in its own registry. Continuation uses App Server `thread/resume`.
|
|
147
|
+
An explicit `--model MODEL` is optional; otherwise Codex selects its default
|
|
148
|
+
(or the resumed thread's model). Every submission defaults to `read-only`;
|
|
149
|
+
select `workspace-write` explicitly when edits are wanted.
|
|
150
|
+
|
|
151
|
+
## Execution and failure contract
|
|
152
|
+
|
|
153
|
+
- Each job ID is immutable. Retrying the same ID and input returns its existing
|
|
154
|
+
record, even after reconnect/restart. Different input with that ID fails.
|
|
155
|
+
A timeout or lost CLI response is a reason to inspect/retry the same ID, not
|
|
156
|
+
create another job. There is no queue: a new ID while busy is rejected.
|
|
157
|
+
- Both thread and turn setup specify `approvalPolicy: never`. Full access is
|
|
158
|
+
not offered. Turn sandbox policy disables tool network access; workspace-write
|
|
159
|
+
grants the thread's registered project directory and excludes ambient temporary directories.
|
|
160
|
+
Codex/provider access and operator-configured MCP integrations still follow
|
|
161
|
+
Codex configuration. This CLI is not a replacement for VM isolation.
|
|
162
|
+
- Unexpected command/file/permission approvals are denied. Questions and other
|
|
163
|
+
server requests receive an unsupported-method error. The turn is interrupted
|
|
164
|
+
and recorded as `blocked`, requiring an operator's explicit follow-up.
|
|
165
|
+
- Only a `turn/completed` notification or terminal turn response establishes
|
|
166
|
+
success/failure/interruption. Early notifications are handled before the
|
|
167
|
+
`turn/start` response. Unrelated thread/turn completions cannot finish a job.
|
|
168
|
+
- Provider loss, protocol timeout, or shutdown before a terminal outcome yields
|
|
169
|
+
`unknown`. A restart never resubmits prompts. Inspect the protocol transcript
|
|
170
|
+
and workspace before explicitly continuing an uncertain job with a new ID.
|
|
171
|
+
A microVM memory restore may preserve a live process; a cold boot cannot
|
|
172
|
+
promise that an in-flight model request or tool operation completed.
|
|
173
|
+
- SIGTERM/SIGINT stop admission, close CLI connections and terminate the owned
|
|
174
|
+
Codex process group, escalating to SIGKILL after at most two seconds. A systemd cgroup
|
|
175
|
+
provides the wider descendant-ownership boundary. This is cancellation, not
|
|
176
|
+
checkpointing. App Server request timeouts are 30 seconds; turn duration has
|
|
177
|
+
no automatic limit. There is no spend budget or VM auto-stop management.
|
|
178
|
+
|
|
179
|
+
## Local state and crash recovery
|
|
180
|
+
|
|
181
|
+
State lives at `$PWRAGENT_HOME/profiles/NAME/state/headless`; the root defaults
|
|
182
|
+
to `~/.pwragent`, and NAME defaults to `PWRAGENT_PROFILE` or `default`.
|
|
183
|
+
The dedicated directory is owned by the service user and mode 0700; files and
|
|
184
|
+
the Unix socket are private through umask 0077. Federation is disabled until
|
|
185
|
+
explicit enrollment or invite creation. Its optional listener binds only to
|
|
186
|
+
127.0.0.1. Same-user local processes are trusted. The
|
|
187
|
+
resolved workspace, Codex home and headless state must not overlap. Unix socket
|
|
188
|
+
paths must fit in 100 bytes; choose a shorter PwrAgent root if needed.
|
|
189
|
+
|
|
190
|
+
The registry is bound to one canonical workspace root and Codex home. Projects
|
|
191
|
+
are registered canonical directories inside that root. It stores job
|
|
192
|
+
IDs, input hashes, thread/turn IDs, settings, timestamps and terminal outcomes.
|
|
193
|
+
It does not persist prompts or duplicate provider transcripts. `read` obtains
|
|
194
|
+
history only through `thread/read`; no Codex-owned storage file is inspected.
|
|
195
|
+
Errors and transcripts may contain sensitive task content; local clients and
|
|
196
|
+
authenticated peers with the corresponding capabilities can read them. Raw Codex stderr is drained but not copied to the journal.
|
|
197
|
+
|
|
198
|
+
An exclusive `owner.lock` prevents two daemons from owning this registry.
|
|
199
|
+
Normal shutdown removes it. Abrupt daemon death or a cold boot can leave
|
|
200
|
+
`owner.lock` and `rpc.sock` behind. Startup deliberately refuses to guess whether
|
|
201
|
+
an old writer is gone. For manual recovery, stop the service, inspect the PID
|
|
202
|
+
and Codex PID in `owner.lock`, and establish that the daemon and its process
|
|
203
|
+
group/cgroup have exited. Only then remove those two files from this profile's
|
|
204
|
+
headless directory and restart. Do not remove `identity.json` or `jobs/`, and do
|
|
205
|
+
not remove `threads.json` or `federation.json`, or recover concurrently with another startup. PID reuse is a reason to inspect,
|
|
206
|
+
not to kill an unrelated process. Incomplete records become `unknown` at startup.
|
|
207
|
+
|
|
208
|
+
State is atomically replaced and fsynced only at lifecycle boundaries (normally
|
|
209
|
+
four small job writes per accepted turn), with no SQLite commits, heartbeat,
|
|
210
|
+
per-event writes, or transcript log. Metadata polling does not write. Records
|
|
211
|
+
are capped at 10,000; there is no automatic garbage collection. Prompt input is
|
|
212
|
+
capped at 256 KiB and protocol responses at 32 MiB; oversized history reads fail
|
|
213
|
+
explicitly. A corrupted registry fails startup rather than discarding history.
|
|
214
|
+
|
|
215
|
+
## Federation enrollment and SSH forwarding
|
|
216
|
+
|
|
217
|
+
The near-term enrollment flow uses an existing desktop gateway invite. Copy the
|
|
218
|
+
encoded invite into a private file on the outpost, then import it with
|
|
219
|
+
`federation-join`. No SaaS login is required. A future login flow can mint the
|
|
220
|
+
same enrollment material without replacing the peer identity.
|
|
221
|
+
|
|
222
|
+
For an already provisioned Sandboxer test machine, the laptop can establish a
|
|
223
|
+
reverse forward to its desktop gateway. These are manual examples, not commands
|
|
224
|
+
run by PwrAgent. Replace the example ID and paths, and use the actual gateway
|
|
225
|
+
port. The official CLI on the development Mac is `/usr/local/bin/sbx`; another
|
|
226
|
+
program named `sbx` may be on PATH.
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
# Laptop: copy an unexpired desktop gateway invite, then keep the forward open.
|
|
230
|
+
/usr/local/bin/sbx scp ./outpost-invite.txt sb_EXAMPLE:/home/user/outpost-invite.txt
|
|
231
|
+
/usr/local/bin/sbx forward sb_EXAMPLE --reverse --local 47830 --remote 47830
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
# Outpost: the daemon is already running under its service manager.
|
|
236
|
+
chmod 600 /home/user/outpost-invite.txt
|
|
237
|
+
node packages/pwragent/bin/pwragent.js federation-join --profile outpost \
|
|
238
|
+
--invite-file /home/user/outpost-invite.txt \
|
|
239
|
+
--gateway ws://127.0.0.1:47830 --label "Research outpost"
|
|
240
|
+
node packages/pwragent/bin/pwragent.js federation-status --profile outpost
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The endpoint override changes reachability only. The original invite's gateway
|
|
244
|
+
identity, Ed25519 public key and Noise key remain pinned. Plain `ws://` overrides
|
|
245
|
+
must be loopback; `wss://` is also accepted. SSH can alternatively carry the same
|
|
246
|
+
reverse forward using a `ProxyCommand` of `/usr/local/bin/sbx ssh %h --stdio`.
|
|
247
|
+
The laptop initiates the authenticated outbound HTTPS connection. The outpost
|
|
248
|
+
does not need to reach home through Cloudflare Access or Tailscale.
|
|
249
|
+
|
|
250
|
+
`federation-join` persists enrollment intent before dialing. It returns current
|
|
251
|
+
connection status; an unavailable forward appears as `unreachable`. Transient
|
|
252
|
+
failures retry with bounded backoff. Authentication failures stop retries and
|
|
253
|
+
remain visible in `federation-status`. Once joined, remove the copied invite
|
|
254
|
+
file. The daemon removes its pending token after successful authentication and
|
|
255
|
+
reconnects using its retained identity, including after a restart. A lost
|
|
256
|
+
acknowledgment is recovered by trying authenticated reconnect before reusing a
|
|
257
|
+
one-time invite.
|
|
258
|
+
|
|
259
|
+
An outpost can also accept a viewer directly: `federation-invite --port 47831`
|
|
260
|
+
starts a loopback listener and returns a 15-minute one-use encoded invite. A
|
|
261
|
+
laptop-side `/usr/local/bin/sbx forward sb_EXAMPLE --remote 47831 --local 47831`
|
|
262
|
+
then lets a separate desktop viewer profile import that invite. Issued invite
|
|
263
|
+
tokens are stored only as HMACs. The listener resumes on daemon restart.
|
|
264
|
+
`federation-revoke --peer ID` revokes an inbound peer and closes its connection;
|
|
265
|
+
`federation-leave` removes the upstream pairing while retaining the outpost's
|
|
266
|
+
identity, listener and jobs. Neither command deletes compute or thread data.
|
|
267
|
+
|
|
268
|
+
Federation grants only implemented capabilities. Unsupported operations return
|
|
269
|
+
errors rather than falling back to the viewer's machine. Read-only peers cannot
|
|
270
|
+
create or control turns. Event subscriptions select both class and thread; no
|
|
271
|
+
transcript is broadcast without demand. Navigation uses the current bounded
|
|
272
|
+
protocol-2 pages, and remote submissions carrying a stable `queueEntryId` reuse
|
|
273
|
+
the same durable job after a lost response. There is still no turn queue: a
|
|
274
|
+
second distinct submission while busy is rejected.
|
|
275
|
+
|
|
276
|
+
Directory launchpads and `create_instance_thread` can create an independent
|
|
277
|
+
thread and start its first text turn in a registered owner-side project. Supply
|
|
278
|
+
its `directoryKey`/`cwd`; omitting it selects the configured workspace root. Select
|
|
279
|
+
`workMode: local` and `executionMode: auto` when workspace edits are needed;
|
|
280
|
+
the defaults remain local and read-only. Launchpad requests for worktrees,
|
|
281
|
+
grouping, environments or provider extensions are rejected before creation.
|
|
282
|
+
|
|
283
|
+
## Identity, compute lifetime and remaining scope
|
|
284
|
+
|
|
285
|
+
The private `federation.json` stores the retained peer ID, signing/Noise keys,
|
|
286
|
+
pinned gateway, enrollments and peer records. `threads.json` stores owned thread
|
|
287
|
+
metadata. These are separate from desktop configuration and OS secret storage.
|
|
288
|
+
Provision each outpost with a dedicated root; do not clone a provisioned
|
|
289
|
+
identity into another running machine or bake it into a template image.
|
|
290
|
+
|
|
291
|
+
The peer advertises `outpost` metadata with `role: remote_agent`, `persistence: unknown`,
|
|
292
|
+
`retainUntil: null`, and `threadStorage: owner`. Unknown persistence is deliberate:
|
|
293
|
+
a process cannot infer its substrate's retention contract. There is no implicit
|
|
294
|
+
expiry, pause or destruction. A lost forward means unreachable; accepted work
|
|
295
|
+
continues. A provider stop that preserves the VM is a pause, not revocation.
|
|
296
|
+
Restoring disk preserves the peer; memory restore may also preserve processes.
|
|
297
|
+
Resize/cold boot requires restarting the service and inspecting uncertain jobs.
|
|
298
|
+
|
|
299
|
+
No provider lifecycle automation runs in this package. It does not create,
|
|
300
|
+
resume, stop, resize or delete a machine, infer idleness from a listening daemon,
|
|
301
|
+
or impose Amp's idle timers. Explicit provider deletion still destroys any
|
|
302
|
+
state stored only on that machine. Federation does **not** replicate Codex
|
|
303
|
+
transcripts to a durable gateway archive. Protect the outpost disk and identity
|
|
304
|
+
if replacement/recovery matters; gateway-side transcript backup and recovery
|
|
305
|
+
onto a new lease remain work to implement. A new empty disk cannot recover a
|
|
306
|
+
lost private identity or conversation merely by reusing a label.
|
|
307
|
+
|
|
308
|
+
General desktop Agent tools, automations/goals, Token Miser, image/blob input,
|
|
309
|
+
interactive approvals and provider installation/login remain outside this
|
|
310
|
+
execution host. General worktree creation is unavailable; Git thread intake
|
|
311
|
+
uses the shared worktree import service. Plan/collaboration and fast mode
|
|
312
|
+
requests are rejected explicitly. One active text turn is supported across all
|
|
313
|
+
projects in a profile. Transcript reads remain bounded by the transport frame
|
|
314
|
+
limit. No provider connections or provider credentials run here.
|
|
315
|
+
|
|
316
|
+
## Project and service contract
|
|
317
|
+
|
|
318
|
+
The CLI and federation project RPCs operate on outpost paths. They never resolve
|
|
319
|
+
a laptop path or borrow the viewer's Git environment. These checkout examples
|
|
320
|
+
use disposable directories under the configured `--workspace` root:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
node packages/pwragent/bin/pwragent.js project-create --name scratch
|
|
324
|
+
node packages/pwragent/bin/pwragent.js project-clone --name repo --remote https://example.invalid/org/repo.git
|
|
325
|
+
node packages/pwragent/bin/pwragent.js project-add --path /srv/pwragent/workspace/existing
|
|
326
|
+
node packages/pwragent/bin/pwragent.js project-list
|
|
327
|
+
node packages/pwragent/bin/pwragent.js submit --id project-task --cwd /srv/pwragent/workspace/repo --prompt-file /task.txt
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`project-remove --path PATH` unregisters an unused project without deleting its
|
|
331
|
+
files. Registered threads retain their actual cwd across restart. Turns,
|
|
332
|
+
launchpads and terminals resolve that owner-side directory again before use;
|
|
333
|
+
a missing directory, changed symlink or conflicting caller cwd fails explicitly.
|
|
334
|
+
|
|
335
|
+
Private Git access is destination-local and opt-in. `git-auth-configure
|
|
336
|
+
--config-file /outpost/git-auth-settings.json` accepts only `sshKeyPath`,
|
|
337
|
+
`knownHostsPath` and `credentialHelperPath`. The referenced files must already
|
|
338
|
+
exist on the outpost, belong to its service user and be private regular files;
|
|
339
|
+
a helper must also be executable. SSH requires both a key and pinned known-hosts
|
|
340
|
+
file. An empty object clears the selection. The persisted configuration contains
|
|
341
|
+
paths, not key or credential contents.
|
|
342
|
+
|
|
343
|
+
Git confines ancestor-repository discovery to the configured workspace and
|
|
344
|
+
disables ambient system/global configuration, interactive prompts,
|
|
345
|
+
credential helpers, SSH configuration, default keys and SSH agents. Only the
|
|
346
|
+
explicit helper/key is enabled. Clone URLs cannot embed HTTP credentials.
|
|
347
|
+
Authentication failure removes only the new clone directory and does not
|
|
348
|
+
register a project. Enrollment and Git intake do not import the sender's Git or SSH environment.
|
|
349
|
+
|
|
350
|
+
| Service | Required grants | Supported behavior |
|
|
351
|
+
| --- | --- | --- |
|
|
352
|
+
| `outpost.projects.list` | `thread_navigation` | Owner project inventory |
|
|
353
|
+
| `outpost.projects.create/add/remove` | `environment_actions` | Create/clone, register and unregister projects |
|
|
354
|
+
| `pty.*` | `remote_pty` | Input, resize, ack flow control, snapshots, history and close |
|
|
355
|
+
| `thread.handoff.*` | `thread_handoff`, `turn_control`, `environment_actions`, `file_push` | Explicit copy/move with receiver preflight and validated history |
|
|
356
|
+
| Messaging consumption | `messaging_route`, owner protocol support and owner authorization | Text replies, surface inspection/rename and authorized attachment of threads |
|
|
357
|
+
|
|
358
|
+
Terminals use the same Electron-free PTY service as desktop, including opener
|
|
359
|
+
ownership through relays, bounded replay, ack-window backpressure and cleanup.
|
|
360
|
+
A brief disconnect can reattach the same shell. The normal disconnect grace is
|
|
361
|
+
10 seconds; after expiry or daemon shutdown, the shell is reaped and its saved
|
|
362
|
+
history can open under a new shell. Agent execution has a separate lifetime.
|
|
363
|
+
|
|
364
|
+
Repository intake requires an existing registered destination clone with a
|
|
365
|
+
matching remote and accessible published commit. The shared handoff service
|
|
366
|
+
preflights it before transferring history or workspace bytes. A move archives
|
|
367
|
+
the source only after destination history validation and acknowledgement.
|
|
368
|
+
Git supplies the new destination worktree; App Server supplies thread export
|
|
369
|
+
bytes through its protocol and imports them through `thread/fork`. Unknown
|
|
370
|
+
intake outcomes survive restart and require inspection instead of another fork.
|
|
371
|
+
|
|
372
|
+
Messaging is an explicitly consumed service. Configure/enroll the always-on
|
|
373
|
+
provider owner separately and route/bind the chat to an outpost-owned thread.
|
|
374
|
+
That owner retains its adapters, credentials, actor permissions and original
|
|
375
|
+
conversation. It starts turns and subscribes to their events for normal replies.
|
|
376
|
+
The outpost stores the full origin and causal destination with the accepted job.
|
|
377
|
+
New outpost threads advertise the shared PwrAgent messaging tools. App Server
|
|
378
|
+
must support namespaced dynamic tools; intake additionally requires protocol
|
|
379
|
+
`fs/readFile` and `thread/fork`. Authorized
|
|
380
|
+
tool calls go to the original owner through `messaging.destination`, which
|
|
381
|
+
rechecks retained-source/provider admission, RBAC and remote-control authority.
|
|
382
|
+
Connection alone establishes neither a provider service nor a destination.
|
|
383
|
+
|
|
384
|
+
Retained-source messaging tools require an owner/provider that supports that
|
|
385
|
+
contract (currently Telegram and Slack). Other providers can still route text
|
|
386
|
+
turns and normal replies through their owning peer. Their outpost tools fail
|
|
387
|
+
closed when retained authorization is unavailable. File/PDF tools additionally
|
|
388
|
+
require the `messaging_files` protocol capability on the original owner's route.
|
|
389
|
+
|
|
390
|
+
`send_messaging_file` reads only an explicit file in the active outpost thread's
|
|
391
|
+
workspace. Shared file validation rejects private Codex/PwrAgent storage,
|
|
392
|
+
selected Git authentication files, symlink escapes, hard links, empty files and
|
|
393
|
+
provider upload-limit violations before transfer. The shared chunked file-push
|
|
394
|
+
protocol transfers the validated byte snapshot; it never reopens the original
|
|
395
|
+
path. The owner must grant `file_push` and allow incoming files. Its receiver
|
|
396
|
+
issues an authenticated-source receipt; delivery consumes that receipt once,
|
|
397
|
+
checks the received checksum, revalidates provider file limits/MIME and rechecks
|
|
398
|
+
the original actor, provider admission, RBAC and route. Outpost paths never become
|
|
399
|
+
provider-owner read requests. Transfers use the owner's configured receiving
|
|
400
|
+
folder, as other federation file pushes do.
|
|
401
|
+
|
|
402
|
+
`inspect_messaging_pdfs`, `search_messaging_pdf_text` and
|
|
403
|
+
`render_messaging_pdf_pages` execute against the original owner's turn-bound PDF
|
|
404
|
+
store. The owner checks the exact outpost identity, turn and retained human
|
|
405
|
+
request before exposing an attachment. PDF bytes stay there; bounded rendered
|
|
406
|
+
pages return as native tool image content. Both single-page and multi-page PDFs
|
|
407
|
+
use this path on supported outpost threads. Owner restart releases those
|
|
408
|
+
in-memory attachments and receipts; tools fail closed instead of recovering
|
|
409
|
+
from arbitrary paths.
|
|
410
|
+
|
|
411
|
+
Existing/forked Codex threads keep their creation-time tool catalog; this change
|
|
412
|
+
does not retrofit tools into older persisted threads.
|
|
413
|
+
|
|
414
|
+
When the owner is unavailable, the tool reports `peer_unavailable`; it never
|
|
415
|
+
moves the destination to another peer. Tool-call outcomes are recorded at call
|
|
416
|
+
boundaries. Lost acknowledgements, reconnect and restart do not replay an
|
|
417
|
+
uncertain send. No messaging queue or automatic retry runs on the outpost.
|
|
418
|
+
|
|
419
|
+
## Architecture
|
|
420
|
+
|
|
421
|
+
The desktop evaluator still starts Electron under Xvfb. Its backend registry
|
|
422
|
+
also owns settings, secrets, Git enrichment and desktop services. The headless
|
|
423
|
+
package instead owns a small App Server adapter and durable job registry.
|
|
424
|
+
`@pwragent/federation` extracts the existing host-independent Noise transport,
|
|
425
|
+
enrollment, router and RPC implementation. Desktop wrappers preserve existing
|
|
426
|
+
imports and inject desktop diagnostics; the headless host supplies private JSON
|
|
427
|
+
identity storage and its backend adapter. The same package now owns the PTY,
|
|
428
|
+
file transfer and transactional thread-intake services. `@pwragent/agent-tools`
|
|
429
|
+
owns the host-independent dynamic tool schemas/router; desktop imports wrappers
|
|
430
|
+
for both packages. Neither shared package imports desktop code. The CLI bundles
|
|
431
|
+
the JavaScript services and produces `dist/THIRD_PARTY_LICENSES` at build time.
|
|
432
|
+
|
|
433
|
+
[Amp's retention contract](https://ampcode.com/security) distinguishes paused
|
|
434
|
+
compute from deletion: paused orb disk and memory remain until thread deletion.
|
|
435
|
+
Its [current idle policy](https://ampcode.com/docs/orbs/sizes-and-costs) requires
|
|
436
|
+
both five minutes without agent work and twenty minutes without interaction.
|
|
437
|
+
Those clocks are reference research, not policies implemented here. Amp's
|
|
438
|
+
server-side thread ownership also differs from this outpost's local ownership.
|
|
439
|
+
|
|
440
|
+
[Sandboxer SSH](https://sandboxer.com/docs/ssh.md) documents authenticated
|
|
441
|
+
outbound WebSocket transport, SCP, forward/reverse-forward and `ssh --stdio`.
|
|
442
|
+
[Sandboxer long-running tasks](https://sandboxer.com/docs/long-running.md)
|
|
443
|
+
documents systemd and memory-preserving stop/resume. The repository's
|
|
444
|
+
[federation architecture](../../docs/federation-architecture.md) and
|
|
445
|
+
[connectivity reference](../../docs/federation-connectivity-reference.md) describe
|
|
446
|
+
the shared trust and routing contract. Research was checked on 2026-10-23.
|
|
447
|
+
|
|
448
|
+
## Development validation
|
|
449
|
+
|
|
450
|
+
```sh
|
|
451
|
+
pnpm --filter pwragent test
|
|
452
|
+
pnpm --filter pwragent pack:check
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
The tests spawn the actual daemon and CLI with a fake Codex executable in
|
|
456
|
+
short temporary directories under /tmp, so the daemon socket path stays
|
|
457
|
+
under its 100-byte limit wherever the checkout lives. They cover disconnect survival,
|
|
458
|
+
idempotency, continuation, single-owner admission, early terminal events,
|
|
459
|
+
failures, denied interactive requests, shutdown and process-group cleanup.
|
|
460
|
+
They also use the actual encrypted federation stack to cover one-time invite
|
|
461
|
+
consumption, capability denial, key pinning, direct and three-peer relay access,
|
|
462
|
+
subscription filtering, dropped forwards, restart and identity retention. A
|
|
463
|
+
desktop Vitest integration test runs its real `FederationRemoteBackendClient`
|
|
464
|
+
against the daemon. External providers and Codex are mocked; no live peers,
|
|
465
|
+
operator projects, credentials or compute machines are used. Ubuntu CI runs the
|
|
466
|
+
Node suite; the desktop contract test runs on POSIX lanes.
|
|
467
|
+
|
|
468
|
+
A separate live smoke test installed the packed tarball and official Codex
|
|
469
|
+
0.159.2 in a Node 24 Debian Linux arm64 Docker image. It used an unprivileged
|
|
470
|
+
user, a read-only authentication-file mount, container-owned state and workspace,
|
|
471
|
+
and an isolated federation. The desktop backend client created a thread and
|
|
472
|
+
started real model work that ran commands and wrote a marker file. Execution
|
|
473
|
+
survived viewer disconnection; reconnect, transcript reads, submission
|
|
474
|
+
deduplication, container restart, continuation and three-peer gateway relay
|
|
475
|
+
passed. This caught the first-turn requirement to use an already loaded thread
|
|
476
|
+
directly: Codex cannot resume its empty rollout before the first turn starts.
|
|
477
|
+
|
|
478
|
+
A subsequent live test enrolled the Docker outpost into an existing desktop
|
|
479
|
+
gateway using a one-use invite copied privately into the container. PwrAgent's
|
|
480
|
+
instance tools discovered its workspace and models, created a thread through
|
|
481
|
+
the directory launchpad, ran commands and wrote a marker file. A continuation
|
|
482
|
+
completed while the gateway forward was closed; reconnecting exposed its
|
|
483
|
+
transcript through the gateway. This caught missing launchpad configuration
|
|
484
|
+
and materialization contracts, now covered by the mocked integration tests.
|
|
485
|
+
|
|
486
|
+
Docker's default seccomp policy blocked the namespaces used by Codex's Linux
|
|
487
|
+
sandbox. The successful run retained dropped container capabilities and
|
|
488
|
+
`no-new-privileges`, using the Docker default profile with additional allowance
|
|
489
|
+
for `clone`, `unshare`, `setns`, `mount`, `umount2` and `pivot_root`. Codex retained
|
|
490
|
+
its workspace-write sandbox. This verifies that container configuration, not
|
|
491
|
+
every Linux host policy. systemd and microVM restore remain unverified. Windows
|
|
492
|
+
is not supported.
|
|
493
|
+
|
|
494
|
+
## License
|
|
495
|
+
|
|
496
|
+
MIT. See the repository root [LICENSE](../../LICENSE).
|
package/bin/pwragent.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { main } from "../src/cli.js";
|
|
4
|
+
|
|
5
|
+
// Socket ownership and private state creation must precede all runtime work.
|
|
6
|
+
process.umask(0o077);
|
|
7
|
+
try {
|
|
8
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(error.message);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|