@awebai/oats 0.30.3 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/servers.md CHANGED
@@ -26,13 +26,59 @@ oats server remove build
26
26
  - `--path` prepends directories to the minimal remote PATH of every routed
27
27
  command (`~/.local/bin:/opt/pi/bin`), where the remote spawn looks for the
28
28
  harness binary.
29
- - `--herdr` records the remote Herdr path in the registration and in each
30
- saved route. Routed commands do not pass it on; the remote kernel finds
31
- `herdr` on its own PATH.
29
+ - `--herdr` is refused (`E_HERDR_REMOVED`): Herdr was removed in 0.31.0. A
30
+ registration or saved route that still records `herdrPath` loads; the field
31
+ is ignored, never written or printed.
32
32
  - `--label` sets a display name. `--replace` overwrites an existing id.
33
33
  - Registrations live in `~/.oats/servers.json` on this machine, never in a
34
34
  repository.
35
35
 
36
+ ## Connections
37
+
38
+ Every routed ssh call carries these options, before `--` and the host:
39
+
40
+ | Option | Why |
41
+ |---|---|
42
+ | `BatchMode=yes`, `ConnectTimeout=15` | a prompt or an unreachable host fails fast |
43
+ | `ServerAliveInterval=15`, `ServerAliveCountMax=3` | a link that died without a reset ends the call, an attached viewer included, within about 60 s |
44
+ | `ControlMaster=auto`, `ControlPath=~/.oats/ssh/%C`, `ControlPersist=60` | every call to one host (the probe, the command, a viewer, concurrent Desktop reads) shares one authenticated connection, kept 60 s after its last client |
45
+
46
+ - Options given with `-o` override `~/.ssh/config`, so these replace any
47
+ `ControlMaster`, `ControlPath` or `ControlPersist` set there for the host.
48
+ Everything else in your ssh config (users, keys, `ProxyJump`, host
49
+ verification) applies as usual; a `ProxyJump` host is reached once per
50
+ master.
51
+ - `~/.oats/ssh` (under `OATS_HOME_DIR` when set) is created mode 0700. OATS
52
+ does not use it if it is owned by another user or writable by group or
53
+ others.
54
+ - ssh binds the control socket at the path plus 17 bytes, within the 104-byte
55
+ socket path limit of macOS, so the directory path may be at most 45 bytes.
56
+ `/Users/<name>/.oats/ssh` fits for any name up to 23 characters. The path
57
+ must also be one ssh reads literally: letters, digits and `. _ / + , : @ =
58
+ -` only (a space, `%`, `$`, a quote or `#` is ssh syntax).
59
+ - When the path does not fit, has other characters, or the directory is not
60
+ private, calls run with `ControlPath=none` instead: no connection sharing,
61
+ including any your ssh config sets up; keepalives still apply. The command
62
+ warns once per server on stderr, naming the path and the fix (an
63
+ `OATS_HOME_DIR` or `HOME` that is shorter or plain).
64
+ - A master left stale by a network drop is replaced by ssh on the next call.
65
+ - The version probe (`oats version --json`) is asked once per process per
66
+ server and target. `server add --replace` to another target forgets it.
67
+ - When ssh fails under an attached viewer (a lost link, or one never made),
68
+ `session attach --server` exits 255 and says that, if the link was lost,
69
+ the instance keeps running on the server, with the command to reattach.
70
+ When ssh fails before the viewer opens (the version probe, or a name
71
+ resolved through the host's roster), it also exits 255, with `oats: ssh to
72
+ <host> failed: …` (`--json`: the `E_SSH` envelope). Every other refusal
73
+ (an incompatible host, an unknown or ambiguous name, bad arguments) exits 1,
74
+ and so does an ssh that cannot be run on this machine at all: no link can
75
+ come back.
76
+ - Every routed command reports ssh's own failure as `E_SSH` (`ssh to <host>
77
+ failed: …`). When ssh never started on this machine, the `--json` envelope
78
+ says so with `error.details: {"sshStarted": false}`; without details, ssh
79
+ ran and the link failed
80
+ ([desktop-cli-api.md](desktop-cli-api.md#ssh-failures-e_ssh)).
81
+
36
82
  ## Run there
37
83
 
38
84
  ```bash
@@ -50,14 +96,18 @@ sent as text or on stdin, never as paths.
50
96
  |---|---|---|
51
97
  | `spawn` | registered workspace | the requested harness, backend and yolo option; `launch-config` for `--launch-config`; `schedule` for a wake schedule |
52
98
  | `status` | registered workspace | |
53
- | `retire` | saved route | `retire-home` to retire by exact home |
54
- | `session inspect`, `session attach` | saved route | `session` |
55
- | `session start`, `session restart` | saved route | `session-start`; `session-restart`; `launch-config` when `--launch-config`, `--harness` or `--yolo` is given |
56
- | `session upload` | saved route | `session-upload` |
99
+ | `retire` | the instance's home | `retire-home` to retire by exact home |
100
+ | `session inspect`, `session attach` | the instance's home | `session` |
101
+ | `session start`, `session restart` | the instance's home | `session-start`; `session-restart`; `launch-config` when `--launch-config`, `--harness` or `--yolo` is given |
102
+ | `session upload` | the instance's home | `session-upload` |
57
103
  | `okf harvest --instance <name>` | the saved home | `harvest` |
58
104
  | `schedule ...` | registered workspace | `schedule` |
59
- | `launch-config list\|set\|remove\|preview` | `--dir`, a saved route, or the registered workspace | `launch-config` |
60
- | `inspect`, `operation run` | `--dir`, a saved route, or the registered workspace | `operations` |
105
+ | `launch-config list\|set\|remove\|preview` | `--dir`, the instance's home, or the registered workspace | `launch-config` |
106
+ | `inspect`, `operation run` | `--dir`, the instance's home, or the registered workspace | `operations` |
107
+ | `readiness` | the instance's home, `--dir`, or the registered workspace | `readiness` (`readinessApi: 2`) |
108
+ | `instance events` | the instance's home | `instance-events-2` (`eventsApi: 2`) |
109
+ | `instance git`, `instance diff` | the instance's home; the Git runs there | `instance-git` (`instanceGitApi: 1`) |
110
+ | `instance stop --plan\|--apply`, `retire --plan`, `retire --plan-revision … --idempotency-key …` | the instance's home | `lifecycle-plans` (`lifecycleApi: 1`) |
61
111
 
62
112
  Before every routed command except `status`, the kernel reads the server's
63
113
  `oats version --json`. A remote OATS older than 0.22.1 is refused, and so is a
@@ -66,19 +116,36 @@ checked against the harness it would use (the flag, or the soul's default as
66
116
  the remote roster reports it). A host that advertises no harness list is
67
117
  assumed to run only pi and claude, on tmux, with no launch options.
68
118
 
69
- **Addressing an instance.** Instance commands take `--instance <name>` (spawned
70
- from this machine) or `--home </remote/home>`. The home is the identity; use it
71
- when two souls on the host own same-named instances. A name and a home that
72
- disagree are refused (`E_HOME_MISMATCH`).
73
-
74
- **`--dir` with `--server`.** For `inspect`, `operation` and `launch-config`,
75
- an explicit `--dir` names a directory on the server and travels as is. Every
76
- other routed command refuses `--dir`; its scope comes from the registration.
119
+ **Addressing an instance.** Instance commands (`retire`, the `session`
120
+ commands, `inspect`, `operation`, `launch-config`) reach any instance on the
121
+ server, whoever spawned it, by `--home </remote/home>` or by name
122
+ (`--instance <name>`, or the positional name of `retire`):
123
+
124
+ - A name spawned from this machine resolves through its saved route.
125
+ - Any other name resolves through the server's roster (one `status --json`
126
+ in the registered workspace) to its one home. A name that two homes carry
127
+ there is `E_AMBIGUOUS`, listing the homes (`error.details.candidates`);
128
+ pass `--home`. A name the roster does not list is `E_SNAPSHOT_UNKNOWN`.
129
+ - A `--home` no saved route owns is sent through the registration's target;
130
+ the server's kernel decides whether it is an instance home and its refusal
131
+ is relayed as is.
132
+ - The home is the identity; use it when two souls on the host own
133
+ same-named instances. A name and a home that disagree are refused
134
+ (`E_HOME_MISMATCH`).
135
+
136
+ **`--dir` with `--server`.** For `inspect`, `operation`, `launch-config`,
137
+ `readiness`, `instance`, and a `retire --plan` or guarded retire apply, an
138
+ explicit `--dir` names a directory on the server and travels as is. Every
139
+ other routed command refuses `--dir`; its scope comes from the
140
+ registration.
77
141
 
78
142
  **Not routed.** `session input` runs on the execution host, where schedules
79
143
  and messaging capabilities call it. `session restart --stop-grace` is refused
80
144
  with `--server`; the remote default applies. `session attach --print` shows
81
- the ssh command without running it.
145
+ the ssh command without running it. A server without the `session` commands
146
+ (before 0.22.2) is refused with the tmux command to attach there directly,
147
+ naming the session and window its roster records for the instance (else
148
+ `pi-agents`, that kernel's default).
82
149
 
83
150
  ## The roster and harvest
84
151
 
@@ -91,8 +158,13 @@ The **roster** is what the Desktop shows: one group per server id and route
91
158
  target (host and workspace), with the registration (present or not), the
92
159
  probe result, the remote souls, the instances joined with saved routes
93
160
  (`savedRoute`, `running` or `null` when unknown, `retirePending`,
94
- `rollbackIncomplete`, `missingRemotely`), and `retireFailures` (deferred
95
- self-retirements that failed there). A removed or edited registration keeps
161
+ `rollbackIncomplete`, `missingRemotely`, `addressable`), and `retireFailures`
162
+ (deferred self-retirements that failed there). Each instance row also relays
163
+ the host's own facts from its `status --json`: `identity`,
164
+ `identityAddress`, `teams`, `startedAt`, `createdAt`, `model`,
165
+ `runtimeState`, `parentInstance`, `siblingInstance`, `relation`,
166
+ `relativeTo` and `spawnOrigin`. A fact the host does not supply is `null`
167
+ (an older host, or a saved route the host no longer lists). A removed or edited registration keeps
96
168
  its group from the saved routes. State is pulled on every call within
97
169
  `--per-target` (default 20 s) of a total `--budget` (default 45 s); a group
98
170
  not reached is reported with `E_ROSTER_BUDGET`. `--server <id>` narrows it.
@@ -103,8 +175,8 @@ instance's saved home on the server and relays its envelope.
103
175
  ## What this machine keeps
104
176
 
105
177
  A **saved route** per remote instance, under `~/.oats/remote/<server>/`,
106
- written at spawn: the ssh host, workspace, oats path, optional Herdr path and
107
- PATH prefix, and the remote home. Retire, harvest and session commands use
178
+ written at spawn: the ssh host, workspace, oats path and PATH prefix, and the
179
+ remote home. Retire, harvest and session commands use
108
180
  the saved route, not the current registration, so editing or removing a
109
181
  registration never orphans a remote home. The route is removed when the
110
182
  remote kernel reports the home gone.
@@ -123,8 +195,5 @@ remote kernel reports the home gone.
123
195
 
124
196
  ## Limits
125
197
 
126
- - Lifecycle actions need a saved route from this machine. An instance the
127
- remote reports but that was spawned elsewhere appears in the roster with
128
- `savedRoute: false` and is read-only here.
129
198
  - No Git runs over SSH: repository operations always run on the server, by
130
199
  its kernel, in its deployment.
@@ -85,8 +85,8 @@ launch: { harness: claude, model: claude-opus-5-5 } # optional (0.30): what th
85
85
 
86
86
  Schema: [`soul.schema.json`](soul.schema.json). Which teams a soul joins is the
87
87
  deployment's choice (`oats-local.yaml`, [workspaces.md](workspaces.md#teams)),
88
- and the backend, yolo and launch configuration are spawn-time host choices
89
- (`--backend`, `--yolo`, `--launch-config`, or a launch configuration in
88
+ and yolo and the launch configuration are spawn-time host choices
89
+ (`--yolo`, `--launch-config`, or a launch configuration in
90
90
  `oats-local.yaml`), not soul identity. The harness and model are a soul's
91
91
  *preference* at most (`launch:`), which each machine overrides and spawn flags
92
92
  (`--harness`, `--model`) win over.
@@ -147,7 +147,8 @@ export function uploadAttachment({ file, home, server, instance }, io = {}) {
147
147
  if (!home) throw err("E_BAD_ARGS", "session upload needs --home </absolute/instance> or --server <id> with --instance <name> or --home");
148
148
  return { ...receiveAttachment(home, name, bytes, { maxBytes }), name, home: realpathSync(home), source: abs };
149
149
  }
150
- const route = resolveRoute(server, { instance, home }, "session upload");
150
+ io = { ...io, serverId: server };
151
+ const route = resolveRoute(server, { instance, home }, "session upload", io);
151
152
  const remote = checkRemote(route.target, io);
152
153
  // Both lists must carry the token; a probe without the arrays is an old kernel.
153
154
  const advertises = (list) => Array.isArray(list) && list.includes("session-upload");
@@ -34,6 +34,7 @@
34
34
  import { spawnSync } from "node:child_process";
35
35
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
36
36
  import { dirname, join } from "node:path";
37
+ import { recordLocalInput } from "./local-inputs.mjs";
37
38
  import YAML from "yaml";
38
39
  import { parseConfigData } from "./config-data.mjs";
39
40
 
@@ -192,8 +193,11 @@ export function soulOriginOf(index, soul) {
192
193
 
193
194
  export const snapshotPath = (dep) => join(dep, ".agents", "automations", "snapshot.json");
194
195
  export function readSnapshot(dep) {
196
+ let text;
197
+ try { text = readFileSync(snapshotPath(dep), "utf8"); } catch { recordLocalInput(snapshotPath(dep), null); return null; }
198
+ recordLocalInput(snapshotPath(dep), text);
195
199
  try {
196
- const doc = JSON.parse(readFileSync(snapshotPath(dep), "utf8"));
200
+ const doc = JSON.parse(text);
197
201
  return isObject(doc) && KIND_NAMES.every((k) => Array.isArray(doc[`${k}s`])) ? doc : null;
198
202
  } catch { return null; }
199
203
  }