@awebai/oats 0.22.17 → 0.22.19
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 +1 -0
- package/bin/oats.mjs +341 -21
- package/docs/configuration.md +65 -0
- package/docs/design/2026-09-07-mobile-agent-management-proposal.md +228 -0
- package/docs/design/launch-configurations.md +164 -0
- package/docs/desktop-cli-api.md +67 -1
- package/docs/desktop-instance-start.md +39 -3
- package/docs/oats-config.schema.json +28 -0
- package/docs/release-notes/v0.22.18.md +101 -0
- package/docs/release-notes/v0.22.19.md +115 -0
- package/docs/souls-and-instances.md +1 -0
- package/lib/core.mjs +836 -137
- package/lib/servers.mjs +89 -4
- package/package.json +1 -1
- package/packages/record/bin/capture.mjs +49 -6
- package/packages/record/lib/capture-lock.mjs +68 -5
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Proposal: manage server agents from an iPhone
|
|
2
|
+
|
|
3
|
+
Status: proposed, September 7, 2026. Requested by Juan following a feasibility
|
|
4
|
+
assessment. This document authorizes no implementation, deployment, or changes
|
|
5
|
+
to running teams. Recommendations below are not shipped functionality.
|
|
6
|
+
|
|
7
|
+
## Purpose and recommendation
|
|
8
|
+
|
|
9
|
+
Let an operator manage OATS agents on a server and talk to them from an iPhone.
|
|
10
|
+
Agents keep working when the phone is locked, disconnected, or switched off.
|
|
11
|
+
Screenshots and files are part of the first useful experience.
|
|
12
|
+
|
|
13
|
+
Start with one server reached privately through Tailscale and a mobile web app
|
|
14
|
+
that can be installed on the iPhone Home Screen. Build on the existing OATS
|
|
15
|
+
control and session contracts. A native iPhone app can consume the same service
|
|
16
|
+
later if sharing, voice, notifications, or other native integration justify it.
|
|
17
|
+
|
|
18
|
+
The substantial work is making the client/server boundary explicit and making
|
|
19
|
+
conversation delivery reliable. A narrow management and terminal client is
|
|
20
|
+
feasible without rewriting the kernel. A polished conversation experience is
|
|
21
|
+
more than embedding the Desktop terminal in a small screen.
|
|
22
|
+
|
|
23
|
+
This extends the [architecture reassessment](2026-09-07-architecture-reassessment.md)
|
|
24
|
+
and its interpretation of Juan's KB direction: standard components with clear
|
|
25
|
+
contracts, replaceable service providers, and preserved identity and knowledge
|
|
26
|
+
across runtime changes. It does not supersede those contracts.
|
|
27
|
+
|
|
28
|
+
## What exists and what is missing
|
|
29
|
+
|
|
30
|
+
Assessment baseline: source main `d20f082`, with Desktop behavior inspected in
|
|
31
|
+
the lead worktree containing that main. These are reusable pieces, not a claim
|
|
32
|
+
that a remote mobile API already exists.
|
|
33
|
+
|
|
34
|
+
| Area | Existing basis | Work needed for mobile |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| Lifecycle and configuration | CLI-backed roster, spawn/start, model choices, retirement and schedule operations | Authenticated network access with exact target selection and truthful results |
|
|
37
|
+
| Terminal access | Session adapters, HTTP pane capture/input, Electron PTY attachment | Network streaming, resize, reconnect and bounded viewer lifetime outside Electron |
|
|
38
|
+
| Attachments | Desktop file/image ingestion and execution-host upload | Browser or native file selection, authenticated upload and visible transfer state |
|
|
39
|
+
| Provider operations | Scoped inspection and declared capability operations | Reuse the same discovery and invocation routes in mobile views |
|
|
40
|
+
| Conversations | aweb is the intended owner of durable identity, messages and delivery | Qualify the human/agent conversation, reply and replay contracts before promising chat |
|
|
41
|
+
| Background operation | Durable agent sessions and OATS scheduling mechanisms | Qualify service, scheduler and message wake operation with every GUI closed |
|
|
42
|
+
|
|
43
|
+
The current [Desktop HTTP backend](../../packages/desktop/server/oats-web.mjs)
|
|
44
|
+
binds to loopback and checks local Host/Origin values. It assumes a trusted
|
|
45
|
+
local client; it is not an authenticated remote service. Live terminal data,
|
|
46
|
+
resize and attachment calls currently cross
|
|
47
|
+
[Electron IPC](../../packages/desktop/main.mjs). Exposing the current port
|
|
48
|
+
unchanged or running Electron headlessly would not complete the required work.
|
|
49
|
+
|
|
50
|
+
## Architecture and ownership
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart TD
|
|
54
|
+
Phone[iPhone: mobile web or native client] -->|HTTPS over Tailscale| Service[OATS host control service]
|
|
55
|
+
Desktop[Desktop client] -->|Shared control contracts| Service
|
|
56
|
+
Service --> Kernel[Existing OATS CLI and kernel]
|
|
57
|
+
Kernel --> Sessions[Session adapters: tmux or Herdr]
|
|
58
|
+
Service --> Messaging[Selected messaging provider: aweb initially]
|
|
59
|
+
Kernel --> Providers[Selected knowledge and task capabilities]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The service is a proposed independently runnable form of the existing control
|
|
63
|
+
backend, with the necessary terminal transport moved out of Electron. It
|
|
64
|
+
delegates lifecycle mutations to the supported OATS CLI. Desktop adoption can
|
|
65
|
+
be incremental; mobile must not introduce a second lifecycle implementation.
|
|
66
|
+
|
|
67
|
+
| Component | Responsibility |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| OATS kernel | Construction, lifecycle, runtime/model configuration, host placement, schedules and capability resolution |
|
|
70
|
+
| Host control service | Authenticated client access, scoped command dispatch, bounded live observations, attachments and terminal viewers |
|
|
71
|
+
| Session backend | Durable execution, attach/detach, literal input, resize and terminal observation |
|
|
72
|
+
| Messaging provider | Durable identity, conversations, events, delivery and any missing human/agent messaging semantics |
|
|
73
|
+
| Knowledge provider | Knowledge representation, access, harvest and promotion policy |
|
|
74
|
+
| Phone and Desktop | Operator interaction and presentation of the same contracts |
|
|
75
|
+
|
|
76
|
+
Use one control service for the selected deployment on a host, not a process
|
|
77
|
+
per agent or client. Existing schedulers and message wake mechanisms remain
|
|
78
|
+
responsible for their work; do not build another scheduler or wake broker in
|
|
79
|
+
the mobile client. Share bounded subscriptions and roster refreshes where
|
|
80
|
+
possible. Open terminal viewers only when needed, apply output backpressure,
|
|
81
|
+
and detach abandoned viewers without stopping agents.
|
|
82
|
+
|
|
83
|
+
tmux and Herdr remain interchangeable session adapters. Mobile access does not
|
|
84
|
+
require a Herdr migration. Start with a direct connection to one execution
|
|
85
|
+
host; later add saved server endpoints or reuse OATS registered-host routing.
|
|
86
|
+
SSH credentials for routed hosts stay on the routing server, not the phone.
|
|
87
|
+
|
|
88
|
+
## Private access through Tailscale
|
|
89
|
+
|
|
90
|
+
Install the ordinary Tailscale app on the iPhone and connect the server to the
|
|
91
|
+
same tailnet. Use Tailscale Serve to proxy the loopback service over HTTPS,
|
|
92
|
+
accessible only through the tailnet's access rules. No embedded VPN or public
|
|
93
|
+
Funnel endpoint is needed. This is a proposed deployment choice, not a current
|
|
94
|
+
OATS installation instruction. [Tailscale Serve documentation](https://tailscale.com/docs/features/tailscale-serve)
|
|
95
|
+
|
|
96
|
+
Network membership must map to an explicit authorized operator. For an initial
|
|
97
|
+
single-owner deployment, choose a simple revocable application session or
|
|
98
|
+
validated Tailscale identity; do not add a second account platform. If trusting
|
|
99
|
+
Serve's identity headers, keep the backend reachable only through the trusted
|
|
100
|
+
local proxy path and follow its header-handling requirements. Configure the
|
|
101
|
+
actual HTTPS Host/Origin rather than removing the existing checks.
|
|
102
|
+
[Tailscale identity headers](https://tailscale.com/docs/features/tailscale-serve#identity-headers)
|
|
103
|
+
|
|
104
|
+
Authorize every action, terminal attachment and upload against a registered
|
|
105
|
+
workspace and exact instance home. Keep runtime permission settings such as
|
|
106
|
+
`yolo` separate from who may control the agent. Exclude multi-tenant hosting
|
|
107
|
+
and elaborate permissions administration from the first version.
|
|
108
|
+
|
|
109
|
+
## Phone experience
|
|
110
|
+
|
|
111
|
+
The primary screen lists workspaces and their agents, with readable names and
|
|
112
|
+
running, stopped, needs-input or unknown states. A disconnected server is
|
|
113
|
+
unreachable, not evidence that its agents stopped. Do not infer needs-input
|
|
114
|
+
or task completion solely from terminal activity.
|
|
115
|
+
|
|
116
|
+
An agent page offers conversation, start controls and session details. Starting
|
|
117
|
+
a stopped agent uses its existing home and shows runtime/model options and
|
|
118
|
+
effective permission mode. Make a launch override distinct from editing future
|
|
119
|
+
defaults. A running agent's model does not silently change because a setting
|
|
120
|
+
was edited. Interrupt and retirement are explicit actions with their existing
|
|
121
|
+
semantics; retirement is not relabeled as a harmless Stop button.
|
|
122
|
+
|
|
123
|
+
The composer supports text and selected photos/files with visible upload state.
|
|
124
|
+
Upload to the agent's execution host, preserve the destination while a transfer
|
|
125
|
+
is in flight, and submit only when attachments are ready. Failed transfers stay
|
|
126
|
+
visible and must not silently redirect to another agent. Start with a file/photo
|
|
127
|
+
picker; system dictation can supply text. A native share extension and realtime
|
|
128
|
+
spoken conversation are later options.
|
|
129
|
+
|
|
130
|
+
Terminal access is a secondary view for interactive harness prompts and tools.
|
|
131
|
+
Provide touch-accessible Escape, interrupt and other essential terminal keys.
|
|
132
|
+
Avoid exposing tmux session names, Desktop split layouts or backend chrome.
|
|
133
|
+
Pasting or uploading into a terminal does not automatically press Enter; a
|
|
134
|
+
conversation Send action is an explicit submission.
|
|
135
|
+
|
|
136
|
+
## Reliable conversations and background behavior
|
|
137
|
+
|
|
138
|
+
Terminal input is useful across harnesses, but a pane transcript is not a
|
|
139
|
+
durable conversation model. Rich chat should use the selected messaging
|
|
140
|
+
provider, initially aweb. Missing durable reply, read-state or replay semantics
|
|
141
|
+
belong in that provider's contract, not an OATS-specific message database or an
|
|
142
|
+
ANSI-output parser. An installation without those capabilities can still offer
|
|
143
|
+
management and a terminal, with chat availability described honestly.
|
|
144
|
+
|
|
145
|
+
Before qualifying chat, establish the following behavior:
|
|
146
|
+
|
|
147
|
+
- The operator has their own sender identity; the client does not impersonate
|
|
148
|
+
the destination agent. Replies remain associated with the conversation.
|
|
149
|
+
- Stable message IDs and replay cursors allow reconnect and missed-message
|
|
150
|
+
retrieval. Retry after an uncertain send does not submit the same request
|
|
151
|
+
twice. Offline text remains a draft until explicitly sent; do not queue
|
|
152
|
+
terminal keystrokes or destructive controls for automatic replay.
|
|
153
|
+
- Accepted, delivered and acted-on are different observations. An input write
|
|
154
|
+
or agent wake is not proof of a completed task. Unknown outcomes remain
|
|
155
|
+
unknown until the existing operation can be reconciled.
|
|
156
|
+
- Delivery and wake continue on the server while clients are absent. Waking a
|
|
157
|
+
running harness and starting a stopped instance are different actions;
|
|
158
|
+
follow explicit policy before a message starts an agent or consumes compute.
|
|
159
|
+
|
|
160
|
+
iOS normally suspends background apps. Do not depend on an always-connected
|
|
161
|
+
phone SSE, WebSocket or SSH session. The server retains state; a notification
|
|
162
|
+
signals new activity, and reopening the client fetches authoritative updates.
|
|
163
|
+
Notifications can be delayed or disabled and are not delivery receipts.
|
|
164
|
+
[Apple background execution documentation](https://developer.apple.com/documentation/uikit/extending-your-app-s-background-execution-time)
|
|
165
|
+
|
|
166
|
+
Home Screen web apps support Web Push on iOS/iPadOS 16.4 and later, with
|
|
167
|
+
permission requested through user interaction. Native applications can use
|
|
168
|
+
APNs. Both require a server-side notification path and device testing. Keep
|
|
169
|
+
sensitive message bodies out of lock-screen notifications by default. Private
|
|
170
|
+
application access can remain on Tailscale while the server makes outbound
|
|
171
|
+
connections to the push service; test this combination on a real phone rather
|
|
172
|
+
than assuming VPN reconnection or notification behavior.
|
|
173
|
+
[WebKit Web Push documentation](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)
|
|
174
|
+
[Apple remote notification documentation](https://developer.apple.com/documentation/usernotifications/setting-up-a-remote-notification-server)
|
|
175
|
+
|
|
176
|
+
## Proposed delivery sequence
|
|
177
|
+
|
|
178
|
+
1. **Prove the service boundary.** Extract only what an independent client
|
|
179
|
+
needs, retain existing CLI dispatch and add authenticated network access.
|
|
180
|
+
Qualify one server over Tailscale, including operation with Desktop closed.
|
|
181
|
+
2. **Deliver mobile management and terminal access.** Workspace/agent list,
|
|
182
|
+
start with model choices, explicit lifecycle actions, terminal input/output,
|
|
183
|
+
screenshots/files and reconnect. Reuse existing schedule controls and
|
|
184
|
+
provider inspection. This is useful without claiming polished chat.
|
|
185
|
+
3. **Qualify conversations and notifications.** Resolve the messaging-provider
|
|
186
|
+
contract gaps, then implement conversation presentation, replay, send
|
|
187
|
+
deduplication and opt-in notifications. Prove delivery while the app sleeps.
|
|
188
|
+
4. **Decide on native and broader hosting.** Use actual phone experience to
|
|
189
|
+
choose whether native sharing, voice and polish justify another client.
|
|
190
|
+
Expand to multiple servers through existing routes or explicit endpoints;
|
|
191
|
+
avoid adding a fleet control platform as a prerequisite.
|
|
192
|
+
|
|
193
|
+
The browser client is the recommended first delivery vehicle, not an
|
|
194
|
+
architectural dependency. If native is chosen first, the same server and
|
|
195
|
+
messaging requirements apply. No calendar estimate is justified until the
|
|
196
|
+
service extraction and conversation-contract gaps have been scoped.
|
|
197
|
+
|
|
198
|
+
## Acceptance before calling it usable
|
|
199
|
+
|
|
200
|
+
- With Desktop closed and the phone locked or disconnected, existing agents
|
|
201
|
+
keep running and an explicitly enabled test schedule and message wake work.
|
|
202
|
+
No production team or harvest schedule is changed to demonstrate this.
|
|
203
|
+
- Start the intended stopped home with the chosen runtime/model. Same-named
|
|
204
|
+
instances elsewhere cannot receive its actions. Unreachable hosts display
|
|
205
|
+
uncertainty and retrying an interrupted mutation does not blindly repeat it.
|
|
206
|
+
- Upload an actual iPhone screenshot and a file to the execution host; verify
|
|
207
|
+
complete bytes and destination, and demonstrate a visible transfer failure.
|
|
208
|
+
- Switch Wi-Fi/cellular, lock/unlock, and reconnect. Conversation history catches
|
|
209
|
+
up without duplicate sends. A terminal reconnect can show a bounded current
|
|
210
|
+
capture; it must not pretend to recover a durable conversation transcript.
|
|
211
|
+
- Close terminal views and disconnect abruptly without killing agent sessions
|
|
212
|
+
or accumulating PTYs. Repeated reconnects and slow clients leave bounded
|
|
213
|
+
memory/process use; share observations instead of polling each agent per view.
|
|
214
|
+
- Test a real notification, denied notification permission, expired client
|
|
215
|
+
access and tailnet disconnection. The UI preserves drafts and accurately
|
|
216
|
+
states what it knows, without showing an unsuccessful send as delivered.
|
|
217
|
+
- A different knowledge provider's declared operations remain usable without
|
|
218
|
+
mobile code assuming OKF, a particular file layout or a universal harvester.
|
|
219
|
+
|
|
220
|
+
Open implementation decisions are the initial operator authentication method,
|
|
221
|
+
the existing aweb contracts that need extension, notification ownership under
|
|
222
|
+
the messaging provider, and the precise terminal stream protocol. Settle them
|
|
223
|
+
with narrow proofs before expanding scope. This proposal changes neither
|
|
224
|
+
current deployment defaults nor the priority of ongoing reliability work.
|
|
225
|
+
|
|
226
|
+
Related contracts: [provider operations](operations-contract.md),
|
|
227
|
+
[Desktop CLI API](../desktop-cli-api.md), [servers](../servers.md),
|
|
228
|
+
[schedules](../schedules.md), and [Desktop](../desktop.md).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Launch configurations and launch recipes
|
|
2
|
+
|
|
3
|
+
A **launch configuration** is a named way to start a harness, declared per
|
|
4
|
+
scope under `launch-configs:` in `oats-config.yaml` (see
|
|
5
|
+
docs/configuration.md): runtime, an executable, literal arguments,
|
|
6
|
+
environment (literals or `{fromEnv}` references), model, yolo. It is
|
|
7
|
+
independent of any soul; a soul may name one as its default
|
|
8
|
+
(`launch-config:` in soul.yaml, `oats soul set --launch-config`), and a
|
|
9
|
+
spawn, start or restart selects one by name.
|
|
10
|
+
|
|
11
|
+
A **launch recipe** is what a start is made of, recorded in the instance's
|
|
12
|
+
`instance.json` under `launch` beside the rendered `command`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"version": 1,
|
|
17
|
+
"runtime": "claude",
|
|
18
|
+
"launchConfig": "personal", "launchConfigSource": "/scope",
|
|
19
|
+
"executable": "/scope/tools/claude-wrapper.sh",
|
|
20
|
+
"executableDeclared": "./tools/claude-wrapper.sh", "executableResolvedFrom": "relative to /scope",
|
|
21
|
+
"args": ["--settings", "/abs/settings.json"],
|
|
22
|
+
"env": { "KEY": { "fromEnv": "SRC" }, "LIT": "plain" },
|
|
23
|
+
"model": "claude-opus-5", "yolo": true,
|
|
24
|
+
"hooks": {
|
|
25
|
+
"launch": { "claude": "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" },
|
|
26
|
+
"env": { "AWEB_DELIVERY": "session" },
|
|
27
|
+
"contributions": [{ "capability": "oats.aweb", "layer": "messaging", "level": "/scope", "settings": { "delivery": "session" }, "trust": { "trusted": true, "integrity": "sha256-..." }, "launch": { "claude": "..." }, "env": ["AWEB_DELIVERY"] }]
|
|
28
|
+
},
|
|
29
|
+
"prompt": { "kind": "task-file", "file": "TASK.md" }
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The rendered `command` is produced by one renderer from the recipe. With no
|
|
34
|
+
configuration it is byte-identical to what spawn rendered before recipes
|
|
35
|
+
existed, so an older kernel starts such a home unchanged, and the golden
|
|
36
|
+
matrix freezes that. Configuration `args` go after the runtime's own
|
|
37
|
+
options and before capability launch arguments (for claude and codex the
|
|
38
|
+
`--` separator keeps them from consuming the task; for pi they follow the
|
|
39
|
+
task, like capability arguments). Every argument and literal value is
|
|
40
|
+
single-quoted: spaces, quotes and metacharacters are literal.
|
|
41
|
+
|
|
42
|
+
## Environment references
|
|
43
|
+
|
|
44
|
+
`{fromEnv: SRC}` renders as `NAME="$SRC"` in the command: the persisted
|
|
45
|
+
command, the pending receipt and every answer carry the reference, never a
|
|
46
|
+
value. At start the execution host checks each source variable is set
|
|
47
|
+
(`E_LAUNCH_ENV_MISSING` before anything is created or stopped) and hands the
|
|
48
|
+
source variables to the pane only (tmux `-e`; a Herdr launch exports them in
|
|
49
|
+
the launched shell). Literal values are non-secret by contract but no answer
|
|
50
|
+
shows them: `list` and `preview` redact every environment value.
|
|
51
|
+
|
|
52
|
+
## Selection rules
|
|
53
|
+
|
|
54
|
+
- A named configuration is a unit. `--launch-config NAME` with a `--runtime`
|
|
55
|
+
that disagrees with the configuration's runtime is refused
|
|
56
|
+
(`E_LAUNCH_CONFIG_MISMATCH`) before anything happens; the same runtime may
|
|
57
|
+
be repeated; `--model` and `--yolo` override the configuration's fields.
|
|
58
|
+
- Without `--launch-config`: a spawn takes the soul's `launch-config` default
|
|
59
|
+
or none; an existing home keeps its recorded configuration, except that
|
|
60
|
+
`--runtime` alone deliberately leaves it behind and renders the new
|
|
61
|
+
runtime's defaults (no old executable or args are carried).
|
|
62
|
+
- Model: explicit, else the configuration's, else on an existing home the
|
|
63
|
+
recorded model when the runtime is unchanged, else the runtime's native
|
|
64
|
+
default; a spawn without either resolves the soul's preference for the
|
|
65
|
+
runtime. A model never crosses runtimes.
|
|
66
|
+
- Executable: the configuration's (bare name on PATH; a path resolved against
|
|
67
|
+
the declaring scope when relative) or the runtime's default (claude through
|
|
68
|
+
`oats-claude-config`). It must be a regular executable file; it is never
|
|
69
|
+
run to probe it. Capability runtime-package requirements are checked with
|
|
70
|
+
the runtime's default binary, as at spawn.
|
|
71
|
+
|
|
72
|
+
## Capability boundary
|
|
73
|
+
|
|
74
|
+
Spawn hooks contribute `launch` arguments keyed by runtime and `env` values.
|
|
75
|
+
Launch arguments are runtime-specific by construction; environment is
|
|
76
|
+
runtime-neutral by contract. The recipe records both with per-capability
|
|
77
|
+
provenance (settings and trust at spawn). A later start on the same runtime
|
|
78
|
+
reuses them. A runtime switch reuses the environment and needs the new
|
|
79
|
+
runtime's launch arguments from the same capabilities: a capability that
|
|
80
|
+
answered arguments for the old runtime and none for the new one refuses the
|
|
81
|
+
switch (`E_LAUNCH_PREPARATION`) with the remedy (change that capability's
|
|
82
|
+
setting, or the provider declares a `launch` hook). Spawn hooks are never
|
|
83
|
+
re-run by a start or restart.
|
|
84
|
+
|
|
85
|
+
## `oats launch-config preview`
|
|
86
|
+
|
|
87
|
+
Read-only; nothing is locked or started. `--home ABS` describes an existing
|
|
88
|
+
home under a selection (`selection.source`: `frozen` when nothing was
|
|
89
|
+
selected, `config` when re-resolved, `frozen-command` for a home that
|
|
90
|
+
predates recipes, whose selection needs the restart conversion);
|
|
91
|
+
`--soul NAME [--dir SCOPE] [--agents-root ABS]` describes a new instance.
|
|
92
|
+
Answer: `{context, selected, selection:{source, launchConfig, runtime,
|
|
93
|
+
model, yolo}, runtime, model, modelSource, yolo, launchConfig,
|
|
94
|
+
launchConfigSource, executable:{path, declared, resolvedFrom}, argv,
|
|
95
|
+
environment:[{name, redacted|fromEnv}], command (redacted rendering),
|
|
96
|
+
prompt:{kind:"task-file", file:"TASK.md"}, hooks (redacted),
|
|
97
|
+
preflight:[{check: executable|environment|model|capabilities, ok, detail}],
|
|
98
|
+
ok}`. The TASK body is never included.
|
|
99
|
+
|
|
100
|
+
## Starting and restarting an existing home
|
|
101
|
+
|
|
102
|
+
`oats session start --home ABS` runs the recorded recipe as it is (a
|
|
103
|
+
`--model` re-renders the model in place and the recipe follows). With
|
|
104
|
+
`--launch-config`, `--runtime` or `--yolo` the recipe is re-resolved by the
|
|
105
|
+
same planner preview uses, against the home's recorded context, and every
|
|
106
|
+
check runs before anything is observed: the recipe's shape, the executable
|
|
107
|
+
(regular file, executable), the references (set on this host), the
|
|
108
|
+
capabilities' contributions (below), the runtime packages. A recorded
|
|
109
|
+
reference is re-checked on every start path, model-only starts included,
|
|
110
|
+
and the pane receives the source's value under the kernel alias.
|
|
111
|
+
|
|
112
|
+
`oats session restart --home ABS [same flags] [--stop-grace SECONDS]` stops
|
|
113
|
+
the running harness and starts again in place under the one per-home lock:
|
|
114
|
+
|
|
115
|
+
1. Every preflight above, first. A refusal leaves the harness running.
|
|
116
|
+
2. The stop: SIGTERM to every process under the pane's launcher (a wrapper
|
|
117
|
+
that does not exec, the harness, their children), then a bounded wait
|
|
118
|
+
(default 20 s) for the signalled processes to be gone and the session to
|
|
119
|
+
read as a bare shell or stopped. Nothing is escalated: a harness still
|
|
120
|
+
there when the wait ends is reported (`E_SESSION_STOP_FAILED`, with the
|
|
121
|
+
processes still running) and nothing is launched. Elapsed time is never
|
|
122
|
+
taken as exit; a turn interruption is never taken as exit.
|
|
123
|
+
3. The in-place start, with the pending receipt carrying the new recipe,
|
|
124
|
+
runtime and yolo, exactly as a start does; `.oats-restart.json` keeps the
|
|
125
|
+
stop's facts (what was signalled, when, whether exit was observed).
|
|
126
|
+
|
|
127
|
+
What the harnesses do on SIGTERM, from their installed sources and
|
|
128
|
+
documentation as read by the operating lead on 2026-09-07 (no live process
|
|
129
|
+
signalled): pi (@earendil-works/pi-coding-agent 0.84.2) registers
|
|
130
|
+
SIGTERM/SIGHUP handlers that end tracked children, dispose extensions and
|
|
131
|
+
exit; Claude Code's documentation makes Ctrl-C state-dependent (interrupt,
|
|
132
|
+
clear, double-press exit) and does not establish that an external SIGTERM
|
|
133
|
+
runs its SessionEnd hook; Codex's documentation establishes no SIGTERM
|
|
134
|
+
cleanup guarantee. So the contract is the request and the observation, not
|
|
135
|
+
a promise that a harness flushes its latest conversation: OATS preserves
|
|
136
|
+
the home, work, identity and notes; an old native conversation's unsaved
|
|
137
|
+
state is the harness's own. Wrappers should exec the harness or forward
|
|
138
|
+
signals. A longer `--stop-grace` can accommodate hook cleanup.
|
|
139
|
+
|
|
140
|
+
## Homes that predate recipes
|
|
141
|
+
|
|
142
|
+
A home with a recorded `command` and no `launch` is converted narrowly when
|
|
143
|
+
a start selects something: only the kernel's own generated shapes are
|
|
144
|
+
recognized (identity environment, the binary, the runtime's template
|
|
145
|
+
arguments, `--model`, yolo, the task prompt). Other environment is kept and
|
|
146
|
+
attributed to the capability whose recorded declaration (`environment`,
|
|
147
|
+
`environmentNamespaces` in `capabilityRuntime`) owns it, so a session-delivery
|
|
148
|
+
home switches runtime with its `AWEB_DELIVERY` intact; spawn hooks are never
|
|
149
|
+
re-run. Any other argument is unclassified: the start is refused, naming the
|
|
150
|
+
arguments, unless an active trusted capability declares a `launch` hook that
|
|
151
|
+
prepares the launch anew (then its answer replaces them). The conversion is
|
|
152
|
+
recorded (`launch.legacy`) by the start that uses it.
|
|
153
|
+
|
|
154
|
+
## The `launch` hook
|
|
155
|
+
|
|
156
|
+
A capability may declare `hooks.launch`. It runs on a start or restart of an
|
|
157
|
+
existing home (never at spawn, never spawn's identity work), side-effect-free
|
|
158
|
+
by contract, with `OATS_RUNTIME` set to the target runtime and
|
|
159
|
+
`OATS_PREVIOUS_RUNTIME` to the recorded one, and answers `{launch:{<runtime>:
|
|
160
|
+
args}, env:{...}}` for that runtime; its answer replaces the capability's
|
|
161
|
+
recorded contribution. Without it, a capability that contributed
|
|
162
|
+
runtime-specific arguments at spawn cannot follow a runtime change
|
|
163
|
+
(`E_LAUNCH_PREPARATION`), and a capability the scope no longer trusts has its
|
|
164
|
+
recorded arguments withheld the same way.
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -20,6 +20,14 @@ prints exactly one JSON object on stdout:
|
|
|
20
20
|
`version` is the installed package's exact semver (e.g. `0.20.0`).
|
|
21
21
|
Desktop 0.22 accepts `desktopApi === 1` and semver `>=0.22.0 <0.23.0`.
|
|
22
22
|
|
|
23
|
+
Optional features are negotiated from the probe's `features` array. Starting
|
|
24
|
+
an existing home requires `session-start`; named launch configurations and
|
|
25
|
+
runtime/permission overrides require `launch-config`; restarting a running
|
|
26
|
+
home also requires `session-restart`. Desktop checks the corresponding
|
|
27
|
+
`remote` entries before offering these operations for a server. The router
|
|
28
|
+
then probes the execution host before sending a mutation. An absent feature
|
|
29
|
+
means an update is needed; it is not inferred from the version number.
|
|
30
|
+
|
|
23
31
|
The band is widened one kernel minor at a time, after confirming this v1
|
|
24
32
|
surface is unchanged, and always admits the kernel published by the same
|
|
25
33
|
release — Desktop and the CLI are built from one tag, so a band excluding its
|
|
@@ -36,7 +44,65 @@ no progress prose (progress goes to stderr):
|
|
|
36
44
|
|
|
37
45
|
## Mutations exposed to Desktop v1
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
The commands below use the same envelope. Additional capability operations
|
|
48
|
+
are described in [the operations contract](design/operations-contract.md).
|
|
49
|
+
|
|
50
|
+
### Existing-home launch and restart
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
oats session start --home /absolute/home [--server id] \
|
|
54
|
+
[--launch-config name] [--runtime pi|claude|codex] \
|
|
55
|
+
[--model id] [--yolo|--no-yolo] --json
|
|
56
|
+
oats session restart --home /absolute/home [the same options] --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Desktop addresses the exact existing home from the selected workspace's
|
|
60
|
+
roster. Restart is one kernel command. The kernel owns configuration
|
|
61
|
+
validation, stop observation, the lifecycle lock, launch recovery and session
|
|
62
|
+
metadata. Desktop does not implement restart by retiring and spawning.
|
|
63
|
+
Failure or timeout requires a fresh status check before retrying: a lost
|
|
64
|
+
response does not establish that launch failed.
|
|
65
|
+
|
|
66
|
+
For a remote home, its saved route supplies the execution host even if its
|
|
67
|
+
registration has subsequently changed. The remote kernel validates the new
|
|
68
|
+
configuration before stopping the current harness. A missing feature fails
|
|
69
|
+
before any stop/start command is sent.
|
|
70
|
+
|
|
71
|
+
### Launch configurations
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
oats launch-config list [--dir /scope | --home /home | --soul name --agents-root /scope/agents] --json
|
|
75
|
+
oats launch-config set name --file /private/definition.json [--keep-env] --dir /scope --json
|
|
76
|
+
oats launch-config remove name --dir /scope --json
|
|
77
|
+
oats launch-config preview (--home /home | --soul name --agents-root /scope/agents --dir /scope) \
|
|
78
|
+
[--launch-config name] [--runtime runtime] [--model id] [--yolo|--no-yolo] --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
All accept `--server id`. Scope edits follow the registration; inspection and
|
|
82
|
+
preview of an existing home follow its saved route. A local definition file
|
|
83
|
+
is serialized to SSH stdin and read on the host with `--file -`; the local
|
|
84
|
+
filename is never passed to the server as though it existed there.
|
|
85
|
+
|
|
86
|
+
The list result supplies `context`, `selected` and `configurations`. Each
|
|
87
|
+
configuration has a name, runtime, executable, literal argument array,
|
|
88
|
+
environment, model, permission choice and declaring `source`. Environment
|
|
89
|
+
literals appear as `{ "redacted": true }`; references appear as
|
|
90
|
+
`{ "fromEnv": "VARIABLE_NAME" }`. Optional executable/model/yolo fields can be
|
|
91
|
+
null. An editor must not write redaction markers back. `--keep-env`, with
|
|
92
|
+
`env` omitted from the replacement definition, copies the effective named
|
|
93
|
+
configuration's environment once into the complete replacement.
|
|
94
|
+
|
|
95
|
+
Preview is read-only and returns a redacted invocation plus `preflight`
|
|
96
|
+
checks. A successful inspection envelope can contain `result.ok: false`:
|
|
97
|
+
the selected launch is not ready. Desktop displays the failed checks rather
|
|
98
|
+
than treating successful inspection as permission to launch. Environment
|
|
99
|
+
references resolve on the execution host at launch, including subsequent
|
|
100
|
+
starts of the saved recipe. Editing a named definition does not change a
|
|
101
|
+
running instance or silently update its frozen launch recipe. Select the
|
|
102
|
+
configuration explicitly on a later start/restart to apply the new definition.
|
|
103
|
+
|
|
104
|
+
See [launch configuration syntax](configuration.md) and
|
|
105
|
+
[the Desktop start/restart workflow](desktop-instance-start.md).
|
|
40
106
|
|
|
41
107
|
### `oats spawn <agent> … --json`
|
|
42
108
|
|
|
@@ -5,25 +5,61 @@ The Desktop roster is the place to return to it:
|
|
|
5
5
|
|
|
6
6
|
- A running row opens its terminal.
|
|
7
7
|
- A stopped row offers **Start…**. Clicking the row opens the same dialog.
|
|
8
|
+
- A running row's action menu offers **Restart with…** to change harness or launch configuration in the same home.
|
|
8
9
|
- The hierarchy's action popover offers **Start…** for a stopped instance.
|
|
9
10
|
- An unknown status is shown as unknown, not as permission to launch another process.
|
|
10
11
|
|
|
11
|
-
The Start dialog names the existing instance, runtime and host.
|
|
12
|
-
|
|
12
|
+
The Start/Restart dialog names the existing instance, runtime and host. Choose
|
|
13
|
+
a named launch configuration or keep the recorded launch. Without a selected
|
|
14
|
+
configuration, the harness can also be changed directly. A named configuration
|
|
15
|
+
fixes its harness; model and permission choices can override its defaults.
|
|
16
|
+
Enter a model or leave the field blank to use the selected launch's default.
|
|
17
|
+
An old harness's model is not carried to a different harness. Available local model
|
|
13
18
|
suggestions are advisory; a model ID can also be typed. Start uses the saved
|
|
14
19
|
briefing and state in a new harness conversation; it does not resume an old
|
|
15
20
|
harness conversation ID. After the launch appears in the roster, Desktop
|
|
16
21
|
opens the instance's terminal.
|
|
17
22
|
|
|
18
|
-
If
|
|
23
|
+
If an ordinary Start dialog finds the instance already running, its action becomes
|
|
19
24
|
**Open terminal**. A failed or timed-out start requires **Refresh status** before
|
|
20
25
|
another attempt, because the launch may have succeeded before the reply was
|
|
21
26
|
lost. Changing workspaces dismisses the dialog and prevents a delayed launch
|
|
22
27
|
reply from opening a terminal in the wrong workspace.
|
|
23
28
|
|
|
29
|
+
**Restart with…** is explicit: after validating the new configuration, the
|
|
30
|
+
kernel stops the current harness and starts the selected one. It does not
|
|
31
|
+
retire the instance, rerun identity creation or replace its worktree. A stop
|
|
32
|
+
that cannot be confirmed does not authorize another launch. Save in-progress
|
|
33
|
+
work before restarting; the old harness conversation is not transferred to
|
|
34
|
+
another harness.
|
|
35
|
+
|
|
36
|
+
Restart requests termination and waits for the current process to stop before
|
|
37
|
+
launching its replacement. If stopping times out, it leaves the instance for
|
|
38
|
+
inspection instead of forcing a kill or launching a second harness. OATS
|
|
39
|
+
preserves the home and work; it cannot guarantee that a harness or custom
|
|
40
|
+
wrapper saves all of its in-flight conversation state. Wrappers should
|
|
41
|
+
`exec` the harness or forward termination signals correctly.
|
|
42
|
+
|
|
43
|
+
**Preview invocation** asks the execution host for the resolved command and
|
|
44
|
+
displays it as text, with environment values redacted. It never launches an
|
|
45
|
+
agent. **Manage launch configurations** in the dialog creates or updates named
|
|
46
|
+
configurations at the displayed scope, including executable/wrapper, a JSON
|
|
47
|
+
argument list, environment references, model and permissions. Saving a
|
|
48
|
+
configuration changes its definition; applying it to an existing home requires
|
|
49
|
+
an explicit Start or Restart. When editing redacted environment values, keep
|
|
50
|
+
**Preserve the saved environment** selected or enter a complete replacement.
|
|
51
|
+
Use the server's workspace to manage configurations defined on that server.
|
|
52
|
+
|
|
24
53
|
Desktop sends `POST /api/start/<instance>?ws=…&home=…` (and `server=…` for a
|
|
25
54
|
remote instance). The backend resolves that exact roster identity and calls
|
|
26
55
|
`oats session start --home <absolute-home> [--server <id>] [--model <model>] --json`.
|
|
56
|
+
Launch choices add `--launch-config`, `--runtime` or `--yolo`/`--no-yolo`.
|
|
57
|
+
Restart uses `POST /api/restart/<instance>?ws=…&home=…` and the single kernel
|
|
58
|
+
command `oats session restart` with the same selectors and choices.
|
|
59
|
+
Configuration inspection and editing use `POST /api/launch-configs?ws=…`,
|
|
60
|
+
routed through `oats launch-config list/set/remove/preview`. These features
|
|
61
|
+
require the CLI's `launch-config` and `session-restart` capabilities; old
|
|
62
|
+
clients/hosts receive an update explanation instead of unsupported arguments.
|
|
27
63
|
The installed CLI must advertise `session-start`; remote starting also needs
|
|
28
64
|
the remote operation. The execution host checks the actual saved session
|
|
29
65
|
before launch. Desktop does not scaffold a home or execute a launcher itself.
|
|
@@ -84,6 +84,34 @@
|
|
|
84
84
|
"required": ["name"],
|
|
85
85
|
"additionalProperties": false
|
|
86
86
|
},
|
|
87
|
+
"launch-configs": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"description": "Named ways to start a harness, independent of any soul. The closest scope declaring a name provides the whole entry (no merging between scopes). Selected by name at spawn or session start/restart; explicit flags override its fields.",
|
|
90
|
+
"propertyNames": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
|
91
|
+
"additionalProperties": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"required": ["runtime"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"runtime": { "type": "string", "enum": ["pi", "claude", "codex"], "description": "The harness family this configuration starts; it decides the kernel's own baseline arguments (skills, instructions, task) and how model/yolo are expressed." },
|
|
97
|
+
"executable": { "type": "string", "minLength": 1, "description": "The program to run instead of the runtime's default binary: a bare name is looked up on PATH on the execution host; a path containing a slash is resolved against the declaring scope's directory when relative. Checked to exist and be executable before any start; never executed just to probe it." },
|
|
98
|
+
"args": { "type": "array", "items": { "type": "string" }, "description": "Extra arguments, each passed literally (no shell interpretation): native configuration files or profiles go here as ordinary arguments." },
|
|
99
|
+
"env": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
102
|
+
"additionalProperties": {
|
|
103
|
+
"oneOf": [
|
|
104
|
+
{ "type": "string" },
|
|
105
|
+
{ "type": "object", "additionalProperties": false, "required": ["fromEnv"], "properties": { "fromEnv": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } }
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"description": "Environment for the harness. A string is a literal (non-secret by contract; still redacted in every answer). {fromEnv: NAME} is resolved from the execution host's environment at start time; the reference, never the value, is recorded. A missing reference refuses the start before anything stops."
|
|
109
|
+
},
|
|
110
|
+
"model": { "type": "string", "minLength": 1, "description": "Model for this configuration's runtime; overrides the soul default when this configuration is selected." },
|
|
111
|
+
"yolo": { "type": "boolean", "description": "Permission bypass for this configuration; overrides scope and soul defaults when selected." }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
87
115
|
"agent-types": {
|
|
88
116
|
"type": "object",
|
|
89
117
|
"description": "Agent families declared by name; membership is `type: <name>` in each soul.yaml.",
|