@awebai/oats 0.32.0 → 0.33.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/README.md +1 -1
- package/bin/oats.mjs +30 -15
- package/docs/capabilities.md +5 -0
- package/docs/configuration.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +3 -2
- package/docs/desktop-cli-api.md +50 -6
- package/docs/first-team.md +20 -2
- package/docs/implementation.md +64 -2
- package/docs/integrations.md +1 -1
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.33.0.md +174 -0
- package/docs/souls-and-instances.md +64 -0
- package/docs/workspaces.md +1 -1
- package/lib/core.mjs +89 -17
- package/lib/harness-trust.mjs +139 -0
- package/lib/instance-inspect.mjs +16 -2
- package/lib/process-group.mjs +54 -0
- package/lib/remote.mjs +594 -102
- package/package-catalog.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# OATS 0.33.0
|
|
2
|
+
|
|
3
|
+
## Added
|
|
4
|
+
|
|
5
|
+
- **`oats spawn <soul> --preview --max-age <seconds>`** (feature
|
|
6
|
+
`spawn-preview-max-age`). A preview reuses member heads this machine
|
|
7
|
+
observed at most that many seconds ago, as the read verbs do since 0.32.0,
|
|
8
|
+
so a warm preview asks no remote. With the flag the preview's JSON carries
|
|
9
|
+
the read verbs' `observation {observedAt, reused, localRevision}` block;
|
|
10
|
+
without it the preview is unchanged. The decision still covers the heads
|
|
11
|
+
the preview used: an apply with `--expect-decision` observes live and
|
|
12
|
+
refuses `E_DECISION_STALE` when a reused head has moved, and a re-preview
|
|
13
|
+
then shows the new head. The apply refuses `--max-age` (`E_BAD_ARGS`), and
|
|
14
|
+
the refusal message of every form that refuses the flag now lists
|
|
15
|
+
`spawn --preview` among the read forms.
|
|
16
|
+
|
|
17
|
+
## Changed
|
|
18
|
+
|
|
19
|
+
- **OATS fetches only what it reads from a remote** (awebai/oats#384). A
|
|
20
|
+
commit comes with its trees and its files up to 64 KiB; a larger file is
|
|
21
|
+
fetched when a read or a module needs it. The first spawn from a large
|
|
22
|
+
workspace host downloads megabytes instead of its whole tree (tsm: 13 MB in
|
|
23
|
+
3 s, instead of 2.4 GB in 6.5 minutes). A server that cannot serve partial
|
|
24
|
+
fetches (a default `git daemon`, an old self-hosted git), or a local git
|
|
25
|
+
older than 2.45, gets whole trees as before, with one `oats: warning` per
|
|
26
|
+
repository.
|
|
27
|
+
|
|
28
|
+
- **Installing a soul's modules reads their files without a process per
|
|
29
|
+
file.** `oats spawn` copies each module's files from the remote cache
|
|
30
|
+
through the command's one `git cat-file --batch` reader per repository,
|
|
31
|
+
as the reads and discovery already do, instead of one `git cat-file blob`
|
|
32
|
+
per file. A Desktop developer spawn on this deployment ran 50 to 59
|
|
33
|
+
processes instead of 81 (34 per-file reads became 3 to 7 readers).
|
|
34
|
+
Budgets, digests, partial caches and errors are unchanged.
|
|
35
|
+
|
|
36
|
+
- **oats.engineering 1.4.0** (catalog and workspace pin, and the bundled
|
|
37
|
+
mirrors): the developer's loop REQUIRES loading `/understand-the-spec` and
|
|
38
|
+
`/execution-strategy` before implementation.
|
|
39
|
+
|
|
40
|
+
- **oats.aweb 1.17.5** (catalog and workspace pin, and the bundled mirror;
|
|
41
|
+
1.17.4 and 1.17.5): a faster spawn and retire. The spawn hook mints the identity with one
|
|
42
|
+
`aw init --join-from` instead of `aw team invite` + `aw team join` +
|
|
43
|
+
`aw init`, so the invite token no longer appears in any argv. The aw floor
|
|
44
|
+
check stops at `aw version`'s version line instead of waiting on its GitHub
|
|
45
|
+
update check. Retire deletes by workspace id, with `aw wake deregister`
|
|
46
|
+
running at the same time. Measured spawn hook: 7.72 s to 4.62 s mean
|
|
47
|
+
(awebai/oats-aweb#31). Hook output, compensation and the aw floor (1.36.13)
|
|
48
|
+
are unchanged. The mint now has one 120 s budget where three calls had
|
|
49
|
+
about 210 s; raise `OATS_AWEB_JOIN_TIMEOUT_MS` on a slow network. Since the
|
|
50
|
+
hook no longer holds the invite token, 1.17.5 always records the alias it
|
|
51
|
+
requested and warns, without quoting the reply, when aw reports another
|
|
52
|
+
(awebai/oats-aweb#32).
|
|
53
|
+
|
|
54
|
+
- **Pressing Spawn (Cmd-Enter) closes the spawn dialog at once.** The spawn
|
|
55
|
+
completes in the background: a pending row ("Spawning…") appears in the
|
|
56
|
+
sidebar roster where the instance will stand, and is replaced in place by
|
|
57
|
+
the real row once the roster reports it. Outcomes arrive as notifications:
|
|
58
|
+
"<name> spawned" with Open, what didn't finish for a partial or incomplete
|
|
59
|
+
spawn (with View schedules for a wake that wasn't saved), and, for a
|
|
60
|
+
refused or failed spawn, the reason with **Reopen spawn**, which restores
|
|
61
|
+
the whole draft. An unknown outcome keeps the row, reading "Outcome
|
|
62
|
+
unknown", with Check result. Prepare reuses the dialog's fresh preview
|
|
63
|
+
instead of reading it again, and the kernel's `--expect-decision` still
|
|
64
|
+
refuses a decision that changed. Dialog previews use `--max-age 60` when the
|
|
65
|
+
CLI supports it (feature `spawn-preview-max-age`)
|
|
66
|
+
([desktop-spawn-preview.md](../../packages/desktop/docs/desktop-spawn-preview.md#background-spawn-the-dialog-closes-on-spawn)).
|
|
67
|
+
|
|
68
|
+
- **The spawn dialog never waits on the preview while you edit.** The last
|
|
69
|
+
preview stays visible while it updates, Spawn stays pressable, and answers
|
|
70
|
+
are reused for 60 s (#378).
|
|
71
|
+
|
|
72
|
+
- **Core capabilities and Capabilities read as one system** on the soul page,
|
|
73
|
+
the capability page, the instance inspector and the spawn preview. Every
|
|
74
|
+
core row says why it's there, including a slot the soul empties or that has
|
|
75
|
+
no default. The capability page opened from a soul gains a "Why" row.
|
|
76
|
+
Workspace › Capabilities lists Repo owned before Packages (#377).
|
|
77
|
+
|
|
78
|
+
- **The Desktop accepts OATS CLIs `>=0.25.8 <0.34.0`**, so it runs against
|
|
79
|
+
this release's kernel. Install the CLI and the Desktop 0.33.0 together: the
|
|
80
|
+
Desktop 0.32.x refuses a 0.33 CLI.
|
|
81
|
+
|
|
82
|
+
- **Unattended claude and codex launches** (awebai/oats#341,
|
|
83
|
+
[souls-and-instances.md](../souls-and-instances.md#unattended-launches-folder-trust)).
|
|
84
|
+
- Every codex launch passes `-c check_for_update_on_startup=false`, so
|
|
85
|
+
Codex's update prompt no longer blocks it.
|
|
86
|
+
- Codex does not apply a trusted parent to the folders below it. When
|
|
87
|
+
`~/.codex/config.toml` trusts the deployment or an ancestor, a codex
|
|
88
|
+
launch now trusts its new home for that session; the file is never
|
|
89
|
+
written.
|
|
90
|
+
- A claude or codex spawn whose home is not covered by the operator's
|
|
91
|
+
one-time trust of the deployment warns `the <harness> session will stop
|
|
92
|
+
at its folder-trust prompt: trust <deployment> once (…)`. `oats readiness`
|
|
93
|
+
reports the same as a `harness-trust` item in `checks.configured` (not
|
|
94
|
+
required).
|
|
95
|
+
|
|
96
|
+
- **The Desktop names an OATS cache problem, and keeps the roster through an
|
|
97
|
+
unreadable remote.** When the kernel can't read a remote
|
|
98
|
+
(`E_REMOTE_UNREADABLE`), the sidebar roster keeps the instances it last
|
|
99
|
+
observed instead of going empty. A cache problem (`details.reason: "cache"`)
|
|
100
|
+
reads "Couldn't refresh instances · OATS cache problem · observed <age>",
|
|
101
|
+
with the kernel's message in full (it names the lock file or the process
|
|
102
|
+
holding it, and the remedy) and Retry. With nothing observed yet, the failed
|
|
103
|
+
state shows that message. A network failure or a timeout reads "Couldn't
|
|
104
|
+
reach <host> · showing what was read <age>", with the raw code behind
|
|
105
|
+
Details. Of the kernel's structured `details`, only the reason and the
|
|
106
|
+
host of the remote cross to the window; the kernel's message is shown as
|
|
107
|
+
given, with the file or process it names
|
|
108
|
+
([desktop-cli-api.md](../desktop-cli-api.md#workspace)).
|
|
109
|
+
|
|
110
|
+
## Fixed
|
|
111
|
+
|
|
112
|
+
- **A background spawn never fails silently, and a soul can't be spawned
|
|
113
|
+
twice at once** (awebai/oats#383). While a spawn of a soul is in flight,
|
|
114
|
+
opening Spawn for it shows the press disabled with "A spawn of <soul> is in
|
|
115
|
+
progress" and a link to its pending row; it re-enables when the spawn
|
|
116
|
+
settles. After a window reload, a spawn whose outcome wasn't known yet comes
|
|
117
|
+
back as a pending row and its outcome is reported, a failure included, even
|
|
118
|
+
when it settled while another workspace was on screen; Reopen spawn then
|
|
119
|
+
restores the soul and the name. Quitting the Desktop mid-spawn
|
|
120
|
+
still loses an unreported failure
|
|
121
|
+
([desktop-spawn-preview.md](../../packages/desktop/docs/desktop-spawn-preview.md#background-spawn-the-dialog-closes-on-spawn)).
|
|
122
|
+
|
|
123
|
+
- **A git that the system kills is no longer reported as a timeout**
|
|
124
|
+
(awebai/oats#387). A remote read reports `timeout` only when OATS's own
|
|
125
|
+
timer stopped git. When something else kills git (an out-of-memory kill
|
|
126
|
+
during a large fetch, for example), the error now reads `cannot read remote
|
|
127
|
+
<url> (killed): git was killed (signal SIGKILL)`, with `reason: killed` and
|
|
128
|
+
`details.signal`. Before, it said `(timeout)`.
|
|
129
|
+
|
|
130
|
+
- **Codex sessions see their instance** (awebai/oats#342). Codex can run tool
|
|
131
|
+
commands under its shared app-server daemon, without the session's
|
|
132
|
+
environment, so `oats aweb roster` inside Codex asked for `--soul`.
|
|
133
|
+
- A codex launch now sets the instance environment for tool commands with
|
|
134
|
+
`shell_environment_policy.set`: `OATS_INSTANCE`/`OATS_INSTANCE_HOME`, the
|
|
135
|
+
capabilities' launch environment (`AWEB_IDENTITY_HOME`, `AWEB_DELIVERY`)
|
|
136
|
+
and the launch configuration's literals.
|
|
137
|
+
- It also sets `PATH` with the home's `.oats/bin` first. The user's login
|
|
138
|
+
shell may put its profile's entries ahead of it.
|
|
139
|
+
- A capability command with no home in its environment finds the home from
|
|
140
|
+
its working directory.
|
|
141
|
+
|
|
142
|
+
- The partial-fetch tests now pass on a host whose git is older than 2.45
|
|
143
|
+
(awebai/oats#389). The kernel already fell back to whole trees there; the
|
|
144
|
+
tests now read the kernel's own git probe:
|
|
145
|
+
- on an older git they assert that fallback (whole trees, one warning per
|
|
146
|
+
repository);
|
|
147
|
+
- they skip the partial-cache mechanics that cannot happen there, and name
|
|
148
|
+
why;
|
|
149
|
+
- under CI, an older git fails those tests instead.
|
|
150
|
+
|
|
151
|
+
- **A killed fetch no longer leaves a remote cache unusable** (awebai/oats#386).
|
|
152
|
+
OATS ends git with SIGTERM first, so git removes its own lock files, and
|
|
153
|
+
SIGKILL only after a grace. A lock left by a git killed earlier is removed
|
|
154
|
+
once it is older than the longest fetch, with an `oats: warning` naming it.
|
|
155
|
+
A younger one is never removed: the error names the file and says it is
|
|
156
|
+
safe to remove once no oats or git process is running.
|
|
157
|
+
|
|
158
|
+
- **Ending git leaves no survivor in its process group** (awebai/oats#386). After
|
|
159
|
+
git's own output closes, OATS keeps checking git's process group until the grace
|
|
160
|
+
ends: a member that ignores SIGTERM and holds no pipe is still killed, and a
|
|
161
|
+
group seen empty is never signalled again (its id may already belong to another
|
|
162
|
+
process).
|
|
163
|
+
|
|
164
|
+
- **Processes making the first fetch from one remote at once all succeed**
|
|
165
|
+
(two spawns, two Desktop previews). Every write to a cache takes a
|
|
166
|
+
per-cache lock: a live holder is waited for, a dead one's lock is reclaimed,
|
|
167
|
+
and the cache repo appears whole. A failure to write the local cache is now
|
|
168
|
+
reported as `E_REMOTE_UNREADABLE` with the new reason `cache`, never as
|
|
169
|
+
`network`.
|
|
170
|
+
|
|
171
|
+
- Fetching a commit into the remote cache may take 10 minutes instead of 30 s, so
|
|
172
|
+
the first spawn from a large workspace host no longer fails with `cannot read
|
|
173
|
+
remote … (timeout)`; the timeout error names the fetch and the elapsed time
|
|
174
|
+
(awebai/oats#362).
|
|
@@ -204,6 +204,70 @@ the harness's own precedence. The `CLAUDE.md → AGENTS.md` and
|
|
|
204
204
|
`.claude/skills → ../.agents/skills` aliases are kept. OATS composes
|
|
205
205
|
instructions and pins model/provider settings; it excludes nothing.
|
|
206
206
|
|
|
207
|
+
### Unattended launches: folder trust
|
|
208
|
+
|
|
209
|
+
Claude Code and Codex ask before they work in a folder they have not seen, and
|
|
210
|
+
every instance home is new. A launch stopped at that prompt waits for a human,
|
|
211
|
+
so the operator trusts the **deployment directory** (where `oats-local.yaml`
|
|
212
|
+
is) once per harness. OATS only reads the harnesses' configuration; it never
|
|
213
|
+
writes it.
|
|
214
|
+
|
|
215
|
+
- **Claude Code** looks for an accepted entry for its folder or an ancestor, up
|
|
216
|
+
to a git root. Instance homes are not inside a git repository, so one entry
|
|
217
|
+
for the deployment covers every home under it. To add it, run `claude` in the
|
|
218
|
+
deployment once and accept the prompt. That records
|
|
219
|
+
`projects["<deployment>"].hasTrustDialogAccepted` in `~/.claude.json`
|
|
220
|
+
(`$CLAUDE_CONFIG_DIR/.claude.json` when that is set).
|
|
221
|
+
- **Codex** applies only an exact entry: a trusted parent does not cover the
|
|
222
|
+
folders below it. To give the operator's consent, run `codex` in the
|
|
223
|
+
deployment once and choose "Trust and continue". That records
|
|
224
|
+
`[projects."<deployment>"] trust_level = "trusted"` in
|
|
225
|
+
`~/.codex/config.toml` (`$CODEX_HOME/config.toml`). With that entry, or one
|
|
226
|
+
for an ancestor of the deployment, each codex launch trusts its own new home
|
|
227
|
+
for that session (`-c 'projects={"<home>"={trust_level="trusted"}}'`) and
|
|
228
|
+
leaves the config file unchanged. A yolo launch always does this. The plan
|
|
229
|
+
re-reads the entry at every start.
|
|
230
|
+
- Every codex launch also passes `-c check_for_update_on_startup=false`, so
|
|
231
|
+
Codex's "Update available" choice cannot block it.
|
|
232
|
+
|
|
233
|
+
When a claude or codex home is not covered, the spawn says so (text and
|
|
234
|
+
`--json` `warnings`): `the <harness> session will stop at its folder-trust
|
|
235
|
+
prompt: trust <deployment> once (<the step>)`. `oats readiness` reports the
|
|
236
|
+
same in `checks.configured` (code `harness-trust`, not required).
|
|
237
|
+
|
|
238
|
+
### Codex tool commands and the instance environment
|
|
239
|
+
|
|
240
|
+
Codex can run tool commands under its shared app-server daemon rather than as
|
|
241
|
+
children of the session OATS launched, and then they do not inherit the
|
|
242
|
+
session's environment. Codex (0.157.1) runs a session that has `-c` overrides
|
|
243
|
+
embedded, without the daemon, and every kernel codex launch has them, so an
|
|
244
|
+
OATS codex session does not appear in `codex agents`. So that the environment
|
|
245
|
+
does not depend on this, a codex launch also sets it for tool
|
|
246
|
+
commands explicitly with `-c shell_environment_policy.set.<NAME>="<value>"`:
|
|
247
|
+
|
|
248
|
+
- the instance: `OATS_INSTANCE`, `OATS_INSTANCE_HOME`, `PI_AGENT_INSTANCE`,
|
|
249
|
+
`PI_AGENT_HOME`;
|
|
250
|
+
- every capability's launch environment (for example the messaging
|
|
251
|
+
provider's identity home and delivery mode);
|
|
252
|
+
- the launch configuration's literal values. A reference's value never goes
|
|
253
|
+
on a command line.
|
|
254
|
+
|
|
255
|
+
`PATH` comes from the launching shell, with the home's `.oats/bin` first, so
|
|
256
|
+
only the execution passes it; the persisted command does not carry it. Codex
|
|
257
|
+
runs tool commands through the user's login shell, and a profile that prepends
|
|
258
|
+
directories puts those entries ahead of `.oats/bin`. A second `oats` in such a
|
|
259
|
+
directory is found first.
|
|
260
|
+
|
|
261
|
+
A capability command (`oats <namespace> …`) run with none of
|
|
262
|
+
`OATS_INSTANCE_HOME`, `PI_AGENT_HOME` or `OATS_HOME` set finds its instance
|
|
263
|
+
from the working directory. It uses the nearest enclosing directory laid out as
|
|
264
|
+
`<agents-root>/<soul>/instances/<name>` whose `instance.json` records that
|
|
265
|
+
name, and validates it like a home named by the environment. The walk uses the
|
|
266
|
+
directory as the shell names it (`$PWD`). That matters for an attached
|
|
267
|
+
instance, whose `work/` links into its owner's tree: below it, the physical
|
|
268
|
+
path is the owner's. A process that has no `$PWD` there would act as the
|
|
269
|
+
owner, so an attached instance runs capability commands from its home.
|
|
270
|
+
|
|
207
271
|
## Lifecycle
|
|
208
272
|
|
|
209
273
|
### Spawn
|
package/docs/workspaces.md
CHANGED
|
@@ -183,7 +183,7 @@ as `confirmed` or the reason it is not:
|
|
|
183
183
|
| `not-listed` | the workspace does not list the repo |
|
|
184
184
|
| `no-backlink` | no (or invalid) `oats-membership.yaml` at the member's default branch |
|
|
185
185
|
| `backlink-elsewhere` | the member names a different workspace (a case-only difference is flagged: repo paths are case-sensitive identities) |
|
|
186
|
-
| `cannot-read` | the operator cannot read the member (auth / not-found / network / timeout) |
|
|
186
|
+
| `cannot-read` | the operator cannot read the member (auth / not-found / network / timeout / killed: the system killed git, e.g. out of memory), or this machine's remote cache could not be written (cache) |
|
|
187
187
|
|
|
188
188
|
An unconfirmed member contributes nothing but its row: its souls are invisible,
|
|
189
189
|
its capabilities unresolvable (`E_NOT_A_MEMBER` / `E_MEMBERSHIP_UNCONFIRMED`).
|
package/lib/core.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import { killGroup } from "./process-group.mjs";
|
|
|
46
46
|
import { oatsError, herdrInstanceBusy, herdrInstanceRemoved, herdrSettingRemoved, HERDR_REMOVED } from "./errors.mjs";
|
|
47
47
|
import { envRows, recordedTeams, teamsEnv } from "./teams.mjs";
|
|
48
48
|
import { harnessUnavailable, launchConfigUnknown, launchLayers, launchReport, selectionFrom } from "./launch-preference.mjs";
|
|
49
|
+
import { claudeTrusts, codexTrustsRoot, harnessTrustWarning } from "./harness-trust.mjs";
|
|
49
50
|
async function materializePreparedDefault(prepared, home) { const m = await import("./instance-resolution.mjs"); return m.materializePrepared(prepared, home); }
|
|
50
51
|
// Capability rows for a PREPARED spawn (workspace model): the one function that
|
|
51
52
|
// turns a Resolution's modules into the row shape hooks/environment/requirements/
|
|
@@ -2287,11 +2288,43 @@ function shimPathPrefix(tokens, binary, home) {
|
|
|
2287
2288
|
if (!prefix.some((t) => t.name === "PATH")) texts.push(`PATH=${dir}:"$PATH"`);
|
|
2288
2289
|
return texts;
|
|
2289
2290
|
}
|
|
2291
|
+
/** codex runs tool commands with the PATH it is given here, the one the launching shell has (the
|
|
2292
|
+
* shim first): its value is this host's, so only the execution carries it. The user's login shell,
|
|
2293
|
+
* which Codex runs tool commands through, may still put its own profile entries ahead of it. */
|
|
2294
|
+
const CODEX_TOOL_PATH = `-c "shell_environment_policy.set.PATH=\\"$PATH\\""`;
|
|
2295
|
+
/** The binary and its arguments as the execution runs them. */
|
|
2296
|
+
function executionArgv(tokens, binary, harness) {
|
|
2297
|
+
const texts = tokens.slice(binary).map((t) => t.text);
|
|
2298
|
+
if (harness === "codex") texts.splice(1, 0, CODEX_TOOL_PATH);
|
|
2299
|
+
return texts;
|
|
2300
|
+
}
|
|
2290
2301
|
/** The persisted launch command as a shell runs it: the same command, with the home's kernel shim
|
|
2291
2302
|
* first on PATH. The persisted bytes never carry it (they stay a shape parseLaunchCommand reads). */
|
|
2292
|
-
export function launchShellCommand(command, home) {
|
|
2303
|
+
export function launchShellCommand(command, home, harness) {
|
|
2293
2304
|
const { tokens, binary } = parseLaunchCommand(command);
|
|
2294
|
-
return [...shimPathPrefix(tokens, binary, home), ...tokens
|
|
2305
|
+
return [...shimPathPrefix(tokens, binary, home), ...executionArgv(tokens, binary, harness)].join(" ");
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/** The deployment directory a home belongs to: the one its spawn recorded, else the nearest
|
|
2309
|
+
* ancestor holding oats-local.yaml, else the agents root's parent. */
|
|
2310
|
+
function deploymentOfHome(home, meta) {
|
|
2311
|
+
if (typeof meta?.workspace?.deployment === "string" && meta.workspace.deployment) return realPathOrNearest(meta.workspace.deployment);
|
|
2312
|
+
for (let d = dirname(resolve(home)); ; d = dirname(d)) {
|
|
2313
|
+
if (existsSync(join(d, "oats-local.yaml"))) return realPathOrNearest(d);
|
|
2314
|
+
if (dirname(d) === d) return realPathOrNearest(dirname(dirname(dirname(dirname(resolve(home))))));
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
/** Folder trust for a launch in `home` (lib/harness-trust.mjs, read-only): `trustHome` — the
|
|
2318
|
+
* codex launch trusts the home, because the operator trusts its deployment root or an
|
|
2319
|
+
* ancestor — and the warning when the session will stop at its harness's folder-trust prompt. */
|
|
2320
|
+
export function launchFolderTrust({ harness, home, meta, yolo, env = process.env }) {
|
|
2321
|
+
const root = deploymentOfHome(home, meta);
|
|
2322
|
+
if (harness === "codex") {
|
|
2323
|
+
const trustHome = codexTrustsRoot(root, { env });
|
|
2324
|
+
return { trustHome, warning: harnessTrustWarning({ harness, root, covered: trustHome || yolo === true }) };
|
|
2325
|
+
}
|
|
2326
|
+
if (harness === "claude") return { trustHome: false, warning: harnessTrustWarning({ harness, root, covered: claudeTrusts(home, { env }) }) };
|
|
2327
|
+
return { trustHome: false, warning: null };
|
|
2295
2328
|
}
|
|
2296
2329
|
|
|
2297
2330
|
/** The harness command line of a recipe. With no configuration the bytes
|
|
@@ -2306,19 +2339,38 @@ export function launchShellCommand(command, home) {
|
|
|
2306
2339
|
* under <home>/.agents/skills are found from cwd=home like any repo's — and OATS
|
|
2307
2340
|
* contributes only the composed AGENTS.md (--append-system-prompt). The task
|
|
2308
2341
|
* positional goes ahead of contributed options because pi has no `--`. claude gets `--` before the prompt so a
|
|
2309
|
-
* greedy contributed flag cannot eat it. codex keeps its native policy;
|
|
2310
|
-
*
|
|
2311
|
-
|
|
2342
|
+
* greedy contributed flag cannot eat it. codex keeps its native policy; it
|
|
2343
|
+
* never stops at Codex's update prompt, and the generated home is trusted for
|
|
2344
|
+
* the launch (projects=...) under yolo or `trustHome`: the operator trusts the
|
|
2345
|
+
* deployment root or an ancestor in Codex's config (lib/harness-trust.mjs),
|
|
2346
|
+
* which Codex itself does not apply to the homes below it. Codex's own form
|
|
2347
|
+
* of the override is the inline table: a dotted projects."<home>" key is not
|
|
2348
|
+
* honoured (codex-cli 0.157.1). */
|
|
2349
|
+
export function renderLaunchRecipe(recipe, { home, instance, redact = false, trustHome = false }) {
|
|
2312
2350
|
const { harness, executable, model, yolo } = recipe;
|
|
2313
2351
|
const cfgArgs = (recipe.args || []).map(shq).join(" ");
|
|
2314
2352
|
const hookArgs = recipe.hooks?.launch?.[harness] || "";
|
|
2315
2353
|
const tail = `${cfgArgs ? ` ${cfgArgs}` : ""}${hookArgs ? ` ${hookArgs}` : ""}`;
|
|
2354
|
+
// The launch's environment, in prefix order: the instance, the capabilities' env, the
|
|
2355
|
+
// configuration's env (a reference by reference, never by value).
|
|
2356
|
+
const hookEnv = recipe.hooks?.env || {};
|
|
2357
|
+
const env = [["OATS_INSTANCE", instance], ["OATS_INSTANCE_HOME", home], ["PI_AGENT_INSTANCE", instance], ["PI_AGENT_HOME", home]].map(([name, value]) => ({ name, value }));
|
|
2358
|
+
for (const name of Object.keys(hookEnv).sort()) env.push({ name, value: redact ? "<redacted>" : hookEnv[name] });
|
|
2359
|
+
for (const name of Object.keys(recipe.env || {}).sort()) {
|
|
2360
|
+
const v = recipe.env[name];
|
|
2361
|
+
env.push(typeof v === "string" ? { name, value: redact ? "<redacted>" : v } : { name, reference: true });
|
|
2362
|
+
}
|
|
2316
2363
|
let cmdline;
|
|
2317
2364
|
if (harness === "claude") {
|
|
2318
2365
|
cmdline = `${shq(executable)}${yolo ? " --dangerously-skip-permissions" : ""}${model ? ` --model ${shq(model)}` : ""}${tail} -- "$(cat TASK.md)"`;
|
|
2319
2366
|
} else if (harness === "codex") {
|
|
2320
2367
|
const codexTrust = `projects={${JSON.stringify(realPathOrNearest(home))}={trust_level="trusted"}}`;
|
|
2321
|
-
|
|
2368
|
+
// Codex may run tool commands outside the session's process (its shared app-server daemon),
|
|
2369
|
+
// so the env the prefix below gives the session is also set for them. A reference's value
|
|
2370
|
+
// never goes on argv, and PATH is the execution's (CODEX_TOOL_PATH: the shim first).
|
|
2371
|
+
const toolEnvArgs = env.filter((e) => !e.reference && e.name !== "PATH")
|
|
2372
|
+
.map((e) => ` -c ${shq(`shell_environment_policy.set.${e.name}=${JSON.stringify(e.value)}`)}`).join("");
|
|
2373
|
+
cmdline = `${shq(executable)} --cd ${shq(home)} -c check_for_update_on_startup=false${yolo ? " --yolo" : ""}${yolo || trustHome ? ` -c ${shq(codexTrust)}` : ""}${toolEnvArgs}${model ? ` --model ${shq(model)}` : ""}${tail} -- "$(cat TASK.md)"`;
|
|
2322
2374
|
} else {
|
|
2323
2375
|
// Decision 13: pi starts NORMALLY — its own skill discovery (~/.pi/agent/skills,
|
|
2324
2376
|
// .agents/skills up the tree, so the instance's copied capability skills are
|
|
@@ -2326,13 +2378,7 @@ export function renderLaunchRecipe(recipe, { home, instance, redact = false }) {
|
|
|
2326
2378
|
// the composed instructions.
|
|
2327
2379
|
cmdline = `${shq(executable)} --append-system-prompt ${shq(join(home, "AGENTS.md"))} --approve --name ${shq(instance)}${model ? ` --model ${shq(model)}` : ""} ${shq("@TASK.md")}${tail}`;
|
|
2328
2380
|
}
|
|
2329
|
-
|
|
2330
|
-
const envTokens = Object.keys(hookEnv).sort().map((name) => `${name}=${shq(redact ? "<redacted>" : hookEnv[name])}`);
|
|
2331
|
-
for (const name of Object.keys(recipe.env || {}).sort()) {
|
|
2332
|
-
const v = recipe.env[name];
|
|
2333
|
-
envTokens.push(typeof v === "string" ? `${name}=${shq(redact ? "<redacted>" : v)}` : `${name}="$${LAUNCH_REF_PREFIX}${name}"`);
|
|
2334
|
-
}
|
|
2335
|
-
return `OATS_INSTANCE=${shq(instance)} OATS_INSTANCE_HOME=${shq(home)} PI_AGENT_INSTANCE=${shq(instance)} PI_AGENT_HOME=${shq(home)}${envTokens.length ? ` ${envTokens.join(" ")}` : ""} ${cmdline}`;
|
|
2381
|
+
return `${env.map((e) => e.reference ? `${e.name}="$${LAUNCH_REF_PREFIX}${e.name}"` : `${e.name}=${shq(e.value)}`).join(" ")} ${cmdline}`;
|
|
2336
2382
|
}
|
|
2337
2383
|
|
|
2338
2384
|
/** Execution, not preview: mark pending before dispatch, then resolve native
|
|
@@ -2344,7 +2390,7 @@ function nativeRecordCommand(command, home, harness) {
|
|
|
2344
2390
|
const args = tokens.slice(binary + 1).filter(t => t.kind !== "prompt").map(t => t.value ?? t.text);
|
|
2345
2391
|
const id = prepareNativeStart(home, harness);
|
|
2346
2392
|
const recorder = join(PKG_ROOT, "packages", "record", "bin", "record-native-start.mjs");
|
|
2347
|
-
const inner = `${shq(process.execPath)} ${shq(recorder)} ${shq(home)} ${shq(id)} ${shq(harness)} ${shq(JSON.stringify(args))} && exec ${tokens
|
|
2393
|
+
const inner = `${shq(process.execPath)} ${shq(recorder)} ${shq(home)} ${shq(id)} ${shq(harness)} ${shq(JSON.stringify(args))} && exec ${executionArgv(tokens, binary, harness).join(" ")}`;
|
|
2348
2394
|
return `${shimPathPrefix(tokens, binary, home).join(" ")} /bin/sh -c ${shq(inner)}`;
|
|
2349
2395
|
}
|
|
2350
2396
|
|
|
@@ -2485,9 +2531,10 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
2485
2531
|
...(frozen?.legacy ? { legacy: { ...frozen.legacy, ...(hooks.refreshed?.length ? { replacedBy: hooks.refreshed } : {}) } } : {}),
|
|
2486
2532
|
};
|
|
2487
2533
|
const inst = instance || meta?.instance || basename(home);
|
|
2488
|
-
const
|
|
2534
|
+
const { trustHome } = launchFolderTrust({ harness, home, meta, yolo: recipe.yolo, env });
|
|
2535
|
+
const command = renderLaunchRecipe(recipe, { home, instance: inst, trustHome });
|
|
2489
2536
|
const selectionSource = frozen ? (config?.frozen || (!config && !selection.launchConfig && !selection.harness) ? "frozen" : "config") : "config";
|
|
2490
|
-
return { recipe, command, harness, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, launchChoice, warnings: hooks.warnings || [], ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
2537
|
+
return { recipe, command, trustHome, harness, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, launchChoice, warnings: hooks.warnings || [], ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
2491
2538
|
}
|
|
2492
2539
|
/** The environment a planned launch runs under: the host's base, the
|
|
2493
2540
|
* capabilities' validated env, the configuration's literals and its
|
|
@@ -3577,7 +3624,9 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3577
3624
|
prompt: LAUNCH_PROMPT, kernelBin: shimTarget,
|
|
3578
3625
|
};
|
|
3579
3626
|
if (triggerEventFile) recipe.env.OATS_TRIGGER_EVENT_FILE = triggerEventFile;
|
|
3580
|
-
const
|
|
3627
|
+
const folderTrust = launchFolderTrust({ harness, home, meta: { workspace: { deployment: o.prepared?.deployment } }, yolo });
|
|
3628
|
+
if (folderTrust.warning) warnings.push(folderTrust.warning);
|
|
3629
|
+
const cmdline = renderLaunchRecipe(recipe, { home, instance, trustHome: folderTrust.trustHome });
|
|
3581
3630
|
|
|
3582
3631
|
// Module skills as materialize landed them (flat, .agents/skills/<skill>/),
|
|
3583
3632
|
// beside the soul's own: per-skill provenance `module:<cap>` (lead decision c3),
|
|
@@ -3884,6 +3933,29 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
|
3884
3933
|
return out;
|
|
3885
3934
|
}
|
|
3886
3935
|
|
|
3936
|
+
/** The working directory as the shell names it: $PWD when it is the process's own directory, else
|
|
3937
|
+
* the physical cwd. A path through a symlink (an attached instance's work/, which links into its
|
|
3938
|
+
* owner's tree) then still belongs to the instance it was entered from. */
|
|
3939
|
+
export function logicalCwd() {
|
|
3940
|
+
const cwd = process.cwd(), pwd = process.env.PWD;
|
|
3941
|
+
try { if (pwd && isAbsolute(pwd) && realpathSync(pwd) === realpathSync(cwd)) return pwd; } catch { /* a stale PWD */ }
|
|
3942
|
+
return cwd;
|
|
3943
|
+
}
|
|
3944
|
+
/** The instance home enclosing `dir` (itself or its nearest such ancestor): a directory laid out
|
|
3945
|
+
* as <agents-root>/<agent>/instances/<name> whose instance.json records `instance: <name>`.
|
|
3946
|
+
* For a process whose harness strips the session env (Codex's shared app-server daemon runs
|
|
3947
|
+
* tool commands outside the session); the caller validates the home as it does one named by
|
|
3948
|
+
* OATS_INSTANCE_HOME. undefined when no such directory encloses `dir`. */
|
|
3949
|
+
export function enclosingInstanceHome(dir) {
|
|
3950
|
+
for (let d = resolve(dir); ; d = dirname(d)) {
|
|
3951
|
+
if (basename(dirname(d)) === "instances" && INSTANCE_NAME_RE.test(basename(d))) {
|
|
3952
|
+
try { if (JSON.parse(readFileSync(join(d, "instance.json"), "utf8"))?.instance === basename(d)) return d; }
|
|
3953
|
+
catch { /* no readable instance.json: not a home */ }
|
|
3954
|
+
}
|
|
3955
|
+
if (dirname(d) === d) return undefined;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3887
3959
|
// Locate an instance home under an agents root, including capability-defined
|
|
3888
3960
|
// agents homing under <root>/<name>/ WITHOUT a soul there (listAgents cannot
|
|
3889
3961
|
// see those). Shared by retireInstance and `oats spawn --parent`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** Folder trust for the claude and codex harnesses (#341): whether a new instance home is
|
|
2
|
+
* covered by the operator's one-time trust of the deployment root, so the launched session
|
|
3
|
+
* does not stop at the harness's folder-trust prompt.
|
|
4
|
+
*
|
|
5
|
+
* The operator's harness configuration is only read, never written: trusting the deployment
|
|
6
|
+
* root is the operator's act. A configuration that is missing, unreadable or in a form this
|
|
7
|
+
* reader does not know counts as not trusted, so a launch is warned about rather than
|
|
8
|
+
* silently stalled.
|
|
9
|
+
*
|
|
10
|
+
* - Claude walks up from its working directory, stopping at a git root, and honours
|
|
11
|
+
* `projects["<dir>"].hasTrustDialogAccepted` in ~/.claude.json
|
|
12
|
+
* ($CLAUDE_CONFIG_DIR/.claude.json when that is set). Instance homes are not inside a git
|
|
13
|
+
* repository, so one entry for the deployment root covers every home under it.
|
|
14
|
+
* - Codex honours only an exact `[projects."<dir>"] trust_level = "trusted"` entry in
|
|
15
|
+
* ~/.codex/config.toml ($CODEX_HOME/config.toml), never a parent's. A per-invocation
|
|
16
|
+
* override does trust a home, so the kernel's codex launch adds one for the home when the
|
|
17
|
+
* operator trusts the deployment root or an ancestor of it (renderLaunchRecipe). */
|
|
18
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
|
|
22
|
+
const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
23
|
+
const userHome = (env) => env.HOME || homedir();
|
|
24
|
+
|
|
25
|
+
/** `dir` and its ancestors, nearest first. */
|
|
26
|
+
function* upwards(dir) {
|
|
27
|
+
for (let d = real(dir); ; d = dirname(d)) { yield d; if (dirname(d) === d) return; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether Claude Code trusts `home` without asking: an accepted trust entry for the home or
|
|
31
|
+
* an ancestor, up to and including the nearest git root. */
|
|
32
|
+
export function claudeTrusts(home, { env = process.env } = {}) {
|
|
33
|
+
const file = env.CLAUDE_CONFIG_DIR ? join(env.CLAUDE_CONFIG_DIR, ".claude.json") : join(userHome(env), ".claude.json");
|
|
34
|
+
let projects;
|
|
35
|
+
try { projects = JSON.parse(readFileSync(file, "utf8"))?.projects; } catch { return false; }
|
|
36
|
+
if (!projects || typeof projects !== "object") return false;
|
|
37
|
+
for (const d of upwards(home)) {
|
|
38
|
+
if (Object.hasOwn(projects, d) && projects[d]?.hasTrustDialogAccepted === true) return true;
|
|
39
|
+
if (existsSync(join(d, ".git"))) return false;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A TOML key: bare, "basic" (JSON-compatible escapes) or 'literal'. null when not a key. */
|
|
45
|
+
function tomlKey(text) {
|
|
46
|
+
const t = text.trim();
|
|
47
|
+
if (/^[A-Za-z0-9_-]+$/.test(t)) return t;
|
|
48
|
+
if (/^'[^']*'$/.test(t)) return t.slice(1, -1);
|
|
49
|
+
if (/^"(?:[^"\\]|\\.)*"$/.test(t)) { try { return JSON.parse(t); } catch { return null; } }
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** A dotted TOML key path, split on the dots outside quotes. null when any part is not a key. */
|
|
53
|
+
function tomlPath(text) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
let cur = "", quote = null;
|
|
56
|
+
for (let i = 0; i < text.length; i++) {
|
|
57
|
+
const c = text[i];
|
|
58
|
+
if (quote) { cur += c; if (c === "\\" && quote === '"') cur += text[++i] ?? ""; else if (c === quote) quote = null; }
|
|
59
|
+
else if (c === '"' || c === "'") { quote = c; cur += c; }
|
|
60
|
+
else if (c === ".") { parts.push(cur); cur = ""; }
|
|
61
|
+
else cur += c;
|
|
62
|
+
}
|
|
63
|
+
if (quote) return null;
|
|
64
|
+
parts.push(cur);
|
|
65
|
+
const keys = parts.map(tomlKey);
|
|
66
|
+
return keys.includes(null) ? null : keys;
|
|
67
|
+
}
|
|
68
|
+
const tomlString = (text) => { const t = text.trim(); return /^'[^']*'$/.test(t) || /^"(?:[^"\\]|\\.)*"$/.test(t) ? tomlKey(t) : null; };
|
|
69
|
+
/** The text of a line before a `#` comment that is outside quotes. */
|
|
70
|
+
function uncommented(line) {
|
|
71
|
+
let quote = null;
|
|
72
|
+
for (let i = 0; i < line.length; i++) {
|
|
73
|
+
const c = line[i];
|
|
74
|
+
if (quote) { if (c === "\\" && quote === '"') i++; else if (c === quote) quote = null; }
|
|
75
|
+
else if (c === '"' || c === "'") quote = c;
|
|
76
|
+
else if (c === "#") return line.slice(0, i);
|
|
77
|
+
}
|
|
78
|
+
return line;
|
|
79
|
+
}
|
|
80
|
+
/** `key = value` split at the first `=` outside quotes. */
|
|
81
|
+
function assignment(line) {
|
|
82
|
+
let quote = null;
|
|
83
|
+
for (let i = 0; i < line.length; i++) {
|
|
84
|
+
const c = line[i];
|
|
85
|
+
if (quote) { if (c === "\\" && quote === '"') i++; else if (c === quote) quote = null; }
|
|
86
|
+
else if (c === '"' || c === "'") quote = c;
|
|
87
|
+
else if (c === "=") return [line.slice(0, i), line.slice(i + 1)];
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The directories config.toml marks `trust_level = "trusted"`, in the forms Codex writes and
|
|
93
|
+
* their plain TOML equivalents: `[projects."<dir>"]` tables, `"<dir>" = { trust_level = … }`
|
|
94
|
+
* under `[projects]`, and dotted `projects."<dir>".trust_level` keys. */
|
|
95
|
+
function codexTrustedDirs(text) {
|
|
96
|
+
const trusted = new Set();
|
|
97
|
+
let table = [];
|
|
98
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
99
|
+
const line = uncommented(raw).trim();
|
|
100
|
+
if (!line) continue;
|
|
101
|
+
if (line.startsWith("[")) {
|
|
102
|
+
const m = /^\[\s*([^[\]]+?)\s*\]$/.exec(line);
|
|
103
|
+
table = m && !line.startsWith("[[") ? tomlPath(m[1]) ?? [null] : [null];
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const kv = assignment(line);
|
|
107
|
+
if (!kv) continue;
|
|
108
|
+
const key = tomlPath(kv[0].trim());
|
|
109
|
+
if (!key) continue;
|
|
110
|
+
const path = [...table, ...key], value = kv[1].trim();
|
|
111
|
+
if (path.length === 3 && path[0] === "projects" && path[2] === "trust_level" && tomlString(value) === "trusted") trusted.add(path[1]);
|
|
112
|
+
const inline = /^\{\s*trust_level\s*=\s*("[^"]*"|'[^']*')\s*\}$/.exec(value);
|
|
113
|
+
if (path.length === 2 && path[0] === "projects" && inline && tomlString(inline[1]) === "trusted") trusted.add(path[1]);
|
|
114
|
+
}
|
|
115
|
+
return trusted;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether Codex's config trusts the deployment `root` or one of its ancestors: the operator's
|
|
119
|
+
* one-time consent for the kernel to trust each new home under it at launch. */
|
|
120
|
+
export function codexTrustsRoot(root, { env = process.env } = {}) {
|
|
121
|
+
const file = join(env.CODEX_HOME || join(userHome(env), ".codex"), "config.toml");
|
|
122
|
+
let trusted;
|
|
123
|
+
try { trusted = codexTrustedDirs(readFileSync(file, "utf8")); } catch { return false; }
|
|
124
|
+
for (const d of upwards(root)) if (trusted.has(d)) return true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The step that trusts the deployment root, per harness. */
|
|
129
|
+
const TRUST_STEP = {
|
|
130
|
+
claude: (root) => `run \`claude\` in ${root} and accept its folder-trust prompt; one entry covers every instance home under it`,
|
|
131
|
+
codex: (root) => `run \`codex\` in ${root} and choose "Trust and continue"; OATS then trusts each new home under it at launch`,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** The spawn and readiness warning for a home its harness will not trust without asking;
|
|
135
|
+
* null when `covered`, or for a harness without a folder-trust prompt. */
|
|
136
|
+
export function harnessTrustWarning({ harness, root, covered }) {
|
|
137
|
+
if (covered || !Object.hasOwn(TRUST_STEP, harness)) return null;
|
|
138
|
+
return `the ${harness} session will stop at its folder-trust prompt: trust ${root} once (${TRUST_STEP[harness](root)})`;
|
|
139
|
+
}
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
15
15
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { capabilityManifests, sessionDefaults, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
17
|
+
import { capabilityManifests, sessionDefaults, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchFolderTrust, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
18
|
import { agentDirOf, discoverOrStandalone, ensureWorkspaceSoul, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
19
19
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
20
20
|
import { kernelCompatibility } from "./resolve.mjs";
|
|
@@ -308,6 +308,20 @@ function launchItems(t) {
|
|
|
308
308
|
reason: `this instance runs ${show(a)}; its launch preference now says ${show(b)} (${t.launchCurrent.at ?? t.launchCurrent.from})`,
|
|
309
309
|
remedy: "`oats session restart --reselect-launch`, or respawn", recorded: a, current: b, from: t.launchCurrent.from, at: t.launchCurrent.at })];
|
|
310
310
|
}
|
|
311
|
+
/** `harness-trust` (#341; a warning): the claude or codex session this subject launches will stop at
|
|
312
|
+
* its folder-trust prompt, because the operator has not trusted the deployment. An instance's recorded
|
|
313
|
+
* harness and home; a soul's launch here, for a home under the agents root. Reads, never writes, the
|
|
314
|
+
* harness's configuration (lib/harness-trust.mjs). */
|
|
315
|
+
function folderTrustItems(t) {
|
|
316
|
+
const harness = t.home ? t.meta?.harness : t.launch?.effective?.harness;
|
|
317
|
+
if (harness !== "claude" && harness !== "codex") return [];
|
|
318
|
+
const meta = t.home ? t.meta : { workspace: { deployment: t.deployment } };
|
|
319
|
+
// A soul's yolo is its selected launch configuration's (a host fact, never the soul's).
|
|
320
|
+
const config = () => { try { return launchConfigsAt(t.deployment)[t.launch?.effective?.launchConfig]; } catch { return undefined; } };
|
|
321
|
+
const yolo = t.home ? (t.meta?.yolo ?? t.meta?.launch?.yolo) : config()?.yolo;
|
|
322
|
+
const { warning } = launchFolderTrust({ harness, home: t.home ?? t.agentsRoot, meta, yolo });
|
|
323
|
+
return warning ? [item("launch", "fail", { required: false, producer: "harness folder trust", code: "harness-trust", reason: warning, harness, deployment: t.deployment })] : [];
|
|
324
|
+
}
|
|
311
325
|
function roll(items) {
|
|
312
326
|
const req = items.filter((i) => i.required !== false);
|
|
313
327
|
if (!items.length) return "not-applicable";
|
|
@@ -411,7 +425,7 @@ export async function readinessDocument(t, { selector = null, remoteOptions, cat
|
|
|
411
425
|
evidence: { command: req.command }, remedy: found ? null : req.install, ...cap(mod.name) }));
|
|
412
426
|
}
|
|
413
427
|
}
|
|
414
|
-
configured.push(...teamItems(t), ...launchItems(t));
|
|
428
|
+
configured.push(...teamItems(t), ...launchItems(t), ...folderTrustItems(t));
|
|
415
429
|
// member — the soul's member repository is a confirmed member of the workspace
|
|
416
430
|
// (oats-membership.yaml backlink observed over the remotes).
|
|
417
431
|
const d = t.discovery;
|