pwragent 0.0.0 → 1.3.7

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.
Files changed (88) hide show
  1. package/README.md +494 -2
  2. package/bin/pwragent.js +10 -1
  3. package/dist/THIRD_PARTY_LICENSES +165 -0
  4. package/dist/federation-core.js +19569 -0
  5. package/dist/ws-LICENSE +20 -0
  6. package/examples/pwragent-headless.service +25 -0
  7. package/licenses/xterm-LICENSE +26 -0
  8. package/node_modules/node-addon-api/LICENSE.md +9 -0
  9. package/node_modules/node-addon-api/README.md +319 -0
  10. package/node_modules/node-addon-api/common.gypi +20 -0
  11. package/node_modules/node-addon-api/except.gypi +25 -0
  12. package/node_modules/node-addon-api/index.js +12 -0
  13. package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  14. package/node_modules/node-addon-api/napi-inl.h +6607 -0
  15. package/node_modules/node-addon-api/napi.h +3201 -0
  16. package/node_modules/node-addon-api/node_addon_api.gyp +32 -0
  17. package/node_modules/node-addon-api/node_api.gyp +9 -0
  18. package/node_modules/node-addon-api/noexcept.gypi +26 -0
  19. package/node_modules/node-addon-api/nothing.c +0 -0
  20. package/node_modules/node-addon-api/package-support.json +21 -0
  21. package/node_modules/node-addon-api/package.json +480 -0
  22. package/node_modules/node-addon-api/tools/README.md +73 -0
  23. package/node_modules/node-addon-api/tools/check-napi.js +99 -0
  24. package/node_modules/node-addon-api/tools/clang-format.js +71 -0
  25. package/node_modules/node-addon-api/tools/conversion.js +301 -0
  26. package/node_modules/node-addon-api/tools/eslint-format.js +79 -0
  27. package/node_modules/node-pty/LICENSE +69 -0
  28. package/node_modules/node-pty/README.md +164 -0
  29. package/node_modules/node-pty/binding.gyp +150 -0
  30. package/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  31. package/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  32. package/node_modules/node-pty/lib/index.js +52 -0
  33. package/node_modules/node-pty/lib/interfaces.js +7 -0
  34. package/node_modules/node-pty/lib/shared/conout.js +10 -0
  35. package/node_modules/node-pty/lib/terminal.js +190 -0
  36. package/node_modules/node-pty/lib/types.js +7 -0
  37. package/node_modules/node-pty/lib/unixTerminal.js +351 -0
  38. package/node_modules/node-pty/lib/utils.js +38 -0
  39. package/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  40. package/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  41. package/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  42. package/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  43. package/node_modules/node-pty/package.json +66 -0
  44. package/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  45. package/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  46. package/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  47. package/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  48. package/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  49. package/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  50. package/node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  51. package/node_modules/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  52. package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  53. package/node_modules/node-pty/prebuilds/win32-arm64/conpty.pdb +0 -0
  54. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  55. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.pdb +0 -0
  56. package/node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  57. package/node_modules/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  58. package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  59. package/node_modules/node-pty/prebuilds/win32-x64/conpty.pdb +0 -0
  60. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  61. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.pdb +0 -0
  62. package/node_modules/node-pty/scripts/post-install.js +76 -0
  63. package/node_modules/node-pty/scripts/prebuild.js +34 -0
  64. package/node_modules/node-pty/src/unix/pty.cc +876 -0
  65. package/node_modules/node-pty/src/unix/spawn-helper.cc +23 -0
  66. package/node_modules/node-pty/src/win/conpty.cc +616 -0
  67. package/node_modules/node-pty/src/win/conpty.h +41 -0
  68. package/node_modules/node-pty/src/win/conpty_console_list.cc +44 -0
  69. package/node_modules/node-pty/src/win/path_util.cc +95 -0
  70. package/node_modules/node-pty/src/win/path_util.h +26 -0
  71. package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-arm64/OpenConsole.exe +0 -0
  72. package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-arm64/conpty.dll +0 -0
  73. package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-x64/OpenConsole.exe +0 -0
  74. package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-x64/conpty.dll +0 -0
  75. package/node_modules/node-pty/typings/node-pty.d.ts +215 -0
  76. package/package.json +32 -5
  77. package/src/cli.js +123 -0
  78. package/src/client.js +27 -0
  79. package/src/codex.js +112 -0
  80. package/src/daemon.js +125 -0
  81. package/src/federation-backend.js +453 -0
  82. package/src/federation-core.ts +24 -0
  83. package/src/federation.js +454 -0
  84. package/src/handoffs.js +141 -0
  85. package/src/projects.js +140 -0
  86. package/src/runtime.js +265 -0
  87. package/src/store.js +80 -0
  88. package/src/wire.js +33 -0
package/README.md CHANGED
@@ -1,4 +1,496 @@
1
- # PwrAgent
1
+ # PwrAgent Headless
2
2
 
3
- Coming soon.
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
- console.log("PwrAgent is coming soon.");
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
+ }