@awebai/oats 0.25.4 → 0.25.6
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/bin/oats.mjs +7 -3
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +156 -9
- package/capabilities/oats-aweb/injects/aweb.md +7 -0
- package/capabilities/oats-aweb/oats.json +15 -2
- package/capabilities/oats-okf/bin/oats-okf.mjs +3 -1
- package/capabilities/oats-okf/lib/config.mjs +5 -1
- package/capabilities/oats-okf/lib/io.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +24 -3
- package/capabilities/oats-okf/lib/sources.mjs +52 -16
- package/capabilities/oats-okf/lib/stores.mjs +37 -15
- package/capabilities/oats-okf/oats.json +4 -1
- package/capabilities/oats-okf/skills/okf/SKILL.md +3 -2
- package/docs/capabilities.md +19 -0
- package/docs/capability-manifest.schema.json +4 -0
- package/docs/design/2026-09-20-redesign-program-board.md +1 -1
- package/docs/design/2026-09-23-workspace-module-contracts.md +27 -0
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +180 -0
- package/docs/design/2026-09-24-phase-d-plan.md +129 -0
- package/docs/desktop-cli-api.md +35 -7
- package/docs/integrations.md +73 -7
- package/docs/rebuild-to-v2.md +11 -10
- package/docs/release-notes/v0.25.5.md +42 -0
- package/docs/release-notes/v0.25.6.md +37 -0
- package/docs/workspace-adoption.md +1 -1
- package/docs/workspaces.md +11 -8
- package/lib/core.mjs +45 -7
- package/lib/resolve.mjs +28 -5
- package/package-catalog.json +2 -2
- package/package.json +1 -1
package/docs/integrations.md
CHANGED
|
@@ -85,10 +85,11 @@ can spawn. `owns`/`reads` are responsibility/context, not ACLs. See
|
|
|
85
85
|
[knowledge](knowledge.md) for the **prepared** version scope, provisioning and
|
|
86
86
|
commands, and [migration](knowledge-migration.md) before updating v1.
|
|
87
87
|
|
|
88
|
-
**`oats.aweb`** fills `messaging`: mints an instance identity at spawn
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
**`oats.aweb`** fills `messaging`: mints an instance identity at spawn (local
|
|
89
|
+
mode) or grants an instance an expiring session as a resident global identity
|
|
90
|
+
(global mode), removes or revokes it at retire, contributes the aweb messaging
|
|
91
|
+
and team skills, wires the channel plugin so sessions are woken by mail, and
|
|
92
|
+
exposes `oats aweb roster` and `oats aweb setup`. Requires the `aw` CLI.
|
|
92
93
|
|
|
93
94
|
**`oats.jira`** fills `tasks`: the `jira-tasks` protocol and an advisory
|
|
94
95
|
spawn hook. Requires `acli`; settings commonly include `site` and `project`.
|
|
@@ -135,6 +136,26 @@ remove it on `retire`; supply the roster; teach send, reply, chat, and "read
|
|
|
135
136
|
the event first" in the inject and skill; contribute launch arguments so the
|
|
136
137
|
session is woken; enforce the soul type's `reach` on both sides; state
|
|
137
138
|
whether the address outlives the instance; and keep task coordination out.
|
|
139
|
+
Any messaging provider emits `identity: { mode, alias, team, address|null,
|
|
140
|
+
resident|null, grant?: { id, expiresAt, scopes } }` in its spawn meta (and
|
|
141
|
+
in its launch meta when it renews); `oats.aweb` is the reference
|
|
142
|
+
implementation. **This is a messaging-layer contract, not an oats.aweb
|
|
143
|
+
detail** (decision 27): from kernel 0.25.6 the kernel copies it through as
|
|
144
|
+
the principal the instance *acts as* — `oats status --json
|
|
145
|
+
instances[].identity`, the roster's `identity:` line, `oats inspect --home …
|
|
146
|
+
selected.identity` — preferring the capability whose captured layer is
|
|
147
|
+
`messaging`, adding `provider: <capability id>`, and never interpreting
|
|
148
|
+
`grant`. The kernel offers no `--identity` flag: the choice travels as
|
|
149
|
+
`--provider <cap> identity.mode=… identity.resident=…` and is bound by the
|
|
150
|
+
spawn decision's `effective.providers`.
|
|
151
|
+
|
|
152
|
+
A provider whose settings include a **host fact** — a custody directory, a
|
|
153
|
+
state root — declares that key `hostOnly: true` in its manifest. The
|
|
154
|
+
resolver then accepts it **only** from the deployment's `oats-local.yaml`
|
|
155
|
+
`settings.<cap>` and refuses it in the workspace file, `byTeam` payloads, a
|
|
156
|
+
soul's slot payload and `--provider` flags (`E_WORKSPACE_SCHEMA`, reason
|
|
157
|
+
`host-only-key`, path and key named). The provider cannot enforce this
|
|
158
|
+
itself: it receives one merged payload without provenance.
|
|
138
159
|
|
|
139
160
|
**Tasks.** Teach claim, update, block, hand off, and complete; identify the
|
|
140
161
|
instance to the tracker in a way that survives it; keep conversation out.
|
|
@@ -197,12 +218,57 @@ warning naming the fresh-purpose remedy. On an older `aw` the pre-1.36.1
|
|
|
197
218
|
report stands (`aliasReusable: false`, warning naming aweb-abim), because
|
|
198
219
|
that CLI cannot revoke the certificate.
|
|
199
220
|
|
|
200
|
-
## oats.aweb settings (1.
|
|
221
|
+
## oats.aweb settings (1.12.0)
|
|
201
222
|
|
|
202
223
|
Set in `oats-local.yaml` under `settings.oats.aweb.<key>` (host-owned), in the
|
|
203
224
|
soul's `messaging:` payload (true of every instance), or per spawn with
|
|
204
|
-
`oats spawn … --provider oats.aweb <key>=<value>`.
|
|
205
|
-
|
|
225
|
+
`oats spawn … --provider oats.aweb <key>=<value>`. The effective payload is
|
|
226
|
+
merged in order: workspace messaging, `byTeam[team]`, soul messaging,
|
|
227
|
+
`oats-local.yaml` `settings.oats.aweb`, then per-spawn `--provider` values.
|
|
228
|
+
`residents` is host-file-only: put custody paths only in `oats-local.yaml`,
|
|
229
|
+
never in a committed workspace or soul file (current kernels document this rule
|
|
230
|
+
but do not yet enforce provenance in the hook payload).
|
|
231
|
+
|
|
232
|
+
- `team: <team id>`. The payload team wins over `OATS_TEAM_ID`/
|
|
233
|
+
`OATS_TEAM_NAME`; if both are set and differ, the hook warns and uses the
|
|
234
|
+
payload. Workspace v2 spawns can have an empty `OATS_TEAM_ID`, so set this in
|
|
235
|
+
the payload for global grants.
|
|
236
|
+
- `identity.mode: local | global` (default `local`). Any other value is fatal.
|
|
237
|
+
Local mode is the historical behavior: a spawned team identity is minted for
|
|
238
|
+
the instance, or `identity.source` uses the existing retained-seat flow below.
|
|
239
|
+
Its spawn meta includes `identity: { mode: "local", alias, team, address:
|
|
240
|
+
null, resident: null }` beside the existing top-level `alias`, `team`, and
|
|
241
|
+
`delivery` keys.
|
|
242
|
+
- `identity.mode: global` makes the instance act as a resident global identity
|
|
243
|
+
through an aweb session grant; it never mints a new global identity and never
|
|
244
|
+
copies root keys into the instance home. `identity.resident` is required and
|
|
245
|
+
resolves through `residents.<name>` to an absolute custody directory whose
|
|
246
|
+
`.aw/identity.yaml` already exists. Missing or unresolved residents fail with
|
|
247
|
+
the `oats-local.yaml settings.oats.aweb.residents.<name>` key to set. Optional
|
|
248
|
+
`identity.scopes` defaults to exactly `[mail.read, mail.send, chat.read,
|
|
249
|
+
chat.send]`; optional `identity.ttl` defaults to `8h` (aw accepts `60s` to
|
|
250
|
+
`720h`). Spawn runs `aw id grant mint --scope <comma-list> --ttl <ttl>
|
|
251
|
+
--label oats:<instance> --out <home>/.aweb-identity --json` from the custody
|
|
252
|
+
directory with `AWEB_IDENTITY_HOME` removed from the child environment: in aw
|
|
253
|
+
1.36.1, grant commands are not identity-home-aware and intentionally refuse
|
|
254
|
+
both `--identity-home` and external `AWEB_IDENTITY_HOME`, so cwd selects the
|
|
255
|
+
custody identity. The hook parses the whole JSON document because aw `--json`
|
|
256
|
+
output is indented across lines, with a fallback to the first brace-prefixed
|
|
257
|
+
block when progress lines precede it; it then verifies the minted grant's
|
|
258
|
+
`team_id` and returns
|
|
259
|
+
`env.AWEB_IDENTITY_HOME=<home>/.aweb-identity`. If the minted team differs,
|
|
260
|
+
the hook revokes the grant and keeps nothing. As of aw 1.36.1, receiving,
|
|
261
|
+
wake registration and `aw whoami` work through a grant, but sending mail or
|
|
262
|
+
chat through a grant is rejected by the server with 422 (`from_did must match
|
|
263
|
+
the authenticated sender`) because the aw client signs with the grant-key DID
|
|
264
|
+
where the server expects the resident's. Retire revokes
|
|
265
|
+
`meta.identity.grant.id` through the custody directory; with no grant id it
|
|
266
|
+
reports `nothing-to-revoke`. A failed revoke exits nonzero and reports the TTL
|
|
267
|
+
expiry.
|
|
268
|
+
- `residents: { <name>: /abs/custody/dir }` is the host-owned map for global
|
|
269
|
+
mode. Each custody directory's `.aw` holds the resident identity root keys and
|
|
270
|
+
team certificate. Do not put this map in committed source; the hook cannot
|
|
271
|
+
distinguish payload provenance.
|
|
206
272
|
- `delivery: channel | session` (default `channel`). `session` hands
|
|
207
273
|
notification delivery to the host wake broker: `AWEB_DELIVERY=session` in
|
|
208
274
|
the launch environment (declared by the manifest), no Claude channel flag,
|
package/docs/rebuild-to-v2.md
CHANGED
|
@@ -48,8 +48,9 @@ capabilities plus `oats.core`), so a public soul stays usable.
|
|
|
48
48
|
Two teams that need two different messaging identities (an open-source team
|
|
49
49
|
and a hosted-operations team, say) stay in ONE workspace: `team:` is a label,
|
|
50
50
|
and the provider payload is addressed by label under `messaging.byTeam` (§2).
|
|
51
|
-
Read §8b before relying on it:
|
|
52
|
-
|
|
51
|
+
Read §8b before relying on it: oats.aweb 1.12.0 mints into the `team` the
|
|
52
|
+
payload names, but the `.aw` root it mints FROM is still found by search and
|
|
53
|
+
must hold that team's membership (1.11.2 ignored `team` altogether).
|
|
53
54
|
|
|
54
55
|
## 2. Write `oats-workspace.yaml` v2 in the host repo
|
|
55
56
|
|
|
@@ -76,8 +77,8 @@ members:
|
|
|
76
77
|
- git:github.com/acme/platform
|
|
77
78
|
packages:
|
|
78
79
|
oats.framework: v1.1.3
|
|
79
|
-
oats.okf: v2.1.
|
|
80
|
-
oats.aweb: v1.
|
|
80
|
+
oats.okf: v2.1.4
|
|
81
|
+
oats.aweb: v1.12.0
|
|
81
82
|
teams:
|
|
82
83
|
global: { description: Org-wide }
|
|
83
84
|
engineering: { description: Platform }
|
|
@@ -136,7 +137,7 @@ they enumerate `souls/*/soul.yaml`. A soul left under `agents/` is invisible to
|
|
|
136
137
|
| 0.24 | v2 |
|
|
137
138
|
|---|---|
|
|
138
139
|
| `schemaVersion: 1` | `schemaVersion: 2` |
|
|
139
|
-
| `requires.knowledge: { capability: oats.okf, source: git:…@v2.1.
|
|
140
|
+
| `requires.knowledge: { capability: oats.okf, source: git:…@v2.1.4#oats-package }` | `capabilities: { oats.okf: { from: package } }` — or nothing, if the workspace default already says so |
|
|
140
141
|
| `requires.capabilities.<cap>: { source: git:… }` | `<cap>: { from: package }` (published) or `<cap>: { from: here }` / `{ from: <repo key> }` (a member capability) |
|
|
141
142
|
| `source: repo:…` / `path:` | `{ from: here }` |
|
|
142
143
|
| `defaults.capabilities` | fold into `capabilities:`; use `off` to remove a workspace default |
|
|
@@ -217,8 +218,8 @@ messaging identity per team (§1), a soul that loses its label silently lands
|
|
|
217
218
|
outside every team-addressed payload; nothing refuses it. Label the membership
|
|
218
219
|
when a whole repo belongs to one team, and the soul when it does not.
|
|
219
220
|
|
|
220
|
-
**Per-soul memory-harvest opt-out:** not available in OKF 2.1.3
|
|
221
|
-
item. Neither `okf.json` (`version`, `owner`, `owns`, `reads`) nor the settings
|
|
221
|
+
**Per-soul memory-harvest opt-out:** not available in OKF 2.1.3 or 2.1.4 — a
|
|
222
|
+
later OKF item. Neither `okf.json` (`version`, `owner`, `owns`, `reads`) nor the settings
|
|
222
223
|
payload (`bindings-file`, `state-dir`, `harvest-runtime`, `harvest-model`) has a
|
|
223
224
|
key that keeps a soul registered for reads while excluding it from harvest. A
|
|
224
225
|
soul that must not be harvested today says `knowledge: none` (no OKF at all for
|
|
@@ -327,7 +328,7 @@ Member capabilities need no approval: membership is the trust.
|
|
|
327
328
|
**Non-interactive approval (CI, scripted rebuilds):**
|
|
328
329
|
|
|
329
330
|
```bash
|
|
330
|
-
oats sync --approve oats.okf@v2.1.
|
|
331
|
+
oats sync --approve oats.okf@v2.1.4 --approve oats.aweb@v1.12.0
|
|
331
332
|
```
|
|
332
333
|
|
|
333
334
|
`--approve <id>@<version>` is repeatable and approves **exactly** the entry the
|
|
@@ -405,11 +406,11 @@ machine-level setting would give the seat to EVERY instance of every messaging
|
|
|
405
406
|
soul on that machine, and a seat can be held once. The Desktop's
|
|
406
407
|
confirmed apply carries the same map.
|
|
407
408
|
|
|
408
|
-
## 8b. Where the team `.aw` lives now
|
|
409
|
+
## 8b. Where the team `.aw` lives now, and what `byTeam` does today
|
|
409
410
|
|
|
410
411
|
A freshly minted identity (every spawn without `identity.source`) needs an
|
|
411
412
|
**initialised aweb root**: a directory holding `.aw` with a team membership to
|
|
412
|
-
mint into. oats.aweb 1.11.2
|
|
413
|
+
mint into. oats.aweb's spawn hook (1.11.2 and 1.12.0 alike) looks for `.aw` among these, first hit
|
|
413
414
|
wins: the declared team scope (`OATS_TEAM_SCOPE`, from the removed
|
|
414
415
|
`oats-config.yaml` `team:` block — **empty under v2**), the instance home, the
|
|
415
416
|
git repo containing the home, the resolution context (the soul's work repo) and
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# OATS 0.25.5
|
|
2
|
+
|
|
3
|
+
Patch release: one lifecycle-contract defect fixed; two provider releases
|
|
4
|
+
pinned in the official catalog.
|
|
5
|
+
|
|
6
|
+
## Fixed
|
|
7
|
+
|
|
8
|
+
- **Launch-hook `meta` was collected and discarded.** A capability's `launch`
|
|
9
|
+
hook may return `meta` like the spawn hook does, but the start/restart path
|
|
10
|
+
consumed only its `launch` arguments and `env`, so `instance.json.capabilityMeta`
|
|
11
|
+
kept the spawn-time value forever. A provider that re-issues a credential at
|
|
12
|
+
every start (e.g. a renewed messaging session grant) would have left the
|
|
13
|
+
ORIGINAL id on record, and retire — which reads that record as `OATS_META` —
|
|
14
|
+
would have revoked the wrong one while the live one ran to its TTL. After a
|
|
15
|
+
successful start the kernel now merges each capability's launch `meta` over
|
|
16
|
+
its entry; a hook answering without `meta` keeps its prior entry; a failed
|
|
17
|
+
launch preparation changes nothing. Regression test in
|
|
18
|
+
`test/session-restart.test.mjs`; contract paragraph in `docs/capabilities.md`.
|
|
19
|
+
|
|
20
|
+
No new flags, fields or hook events. `oats version --json` unchanged apart from
|
|
21
|
+
the version.
|
|
22
|
+
|
|
23
|
+
## Catalog
|
|
24
|
+
|
|
25
|
+
- **oats.okf → v2.1.4** (mirror in `capabilities/oats-okf` synced, published
|
|
26
|
+
tag `2af47ad3`): sparse staging to `base.root` from a single-branch partial
|
|
27
|
+
clone with streamed object reads (large repositories and large files outside
|
|
28
|
+
the knowledge base no longer matter), `git-timeout` binding key (600 s for
|
|
29
|
+
remote operations), migration record hygiene + `oats okf migrate --forget`,
|
|
30
|
+
owners keyed by `OATS_SOUL_ID` with a one-time rewrite of same-name path pins
|
|
31
|
+
(closes the `E_OWNER`-after-member-commit regression; needs kernel ≥ 0.25.3),
|
|
32
|
+
and retire removes a drained source's `okf-<id>` scheduler job.
|
|
33
|
+
- **oats.aweb → v1.12.0** (byte-identical to `capabilities/oats-aweb`):
|
|
34
|
+
resident identity session grants — `identity.mode: local | global`, `team`
|
|
35
|
+
read from the payload (so `messaging.byTeam.<label>.team` is now the per-label
|
|
36
|
+
identity), host-only `residents` map, and the messaging-layer meta key
|
|
37
|
+
`identity` on every spawn. Known limitation: sending mail/chat through a
|
|
38
|
+
grant is rejected by the aweb server as of aw 1.36.1 (client bug; receive,
|
|
39
|
+
wake and whoami work). Guide truths in `docs/workspaces.md` and
|
|
40
|
+
`docs/rebuild-to-v2.md` updated from "1.11.2 ignores `team`" to what 1.12.0
|
|
41
|
+
does.
|
|
42
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# OATS 0.25.6
|
|
2
|
+
|
|
3
|
+
Decision 27 — per-spawn identity choice (local | global) — lands its kernel half.
|
|
4
|
+
No new CLI flags; three provider-neutral additions and one manifest attribute.
|
|
5
|
+
|
|
6
|
+
## Added
|
|
7
|
+
|
|
8
|
+
- **`decision.effective.providers`** — the spawn decision now binds the merged
|
|
9
|
+
per-module payloads the providers will receive (exactly the preview's
|
|
10
|
+
`settings.<cap>`), so a confirmed apply covers every provider fact by value.
|
|
11
|
+
- **`hostOnly` settings keys** — a capability manifest may mark a settings key
|
|
12
|
+
`hostOnly: true`; the resolver accepts it only from the deployment's
|
|
13
|
+
`oats-local.yaml settings.<cap>` and refuses it in the workspace file, `byTeam`
|
|
14
|
+
payloads, a soul's slot payload and `--provider` flags (`E_WORKSPACE_SCHEMA`,
|
|
15
|
+
reason `host-only-key`). Closes the documented hole where a committed file
|
|
16
|
+
could point a messaging provider at a custody root.
|
|
17
|
+
- **Served identity on the roster** — `oats status --json instances[].identity`,
|
|
18
|
+
the text roster's `identity:` line and `oats inspect --home … selected.identity`
|
|
19
|
+
carry the principal an instance acts as, copied from its messaging provider's
|
|
20
|
+
hook meta (`{ mode, alias, team, address, resident, grant?, provider }`).
|
|
21
|
+
- `oats version --json` `features[]` gains `served-identity`.
|
|
22
|
+
|
|
23
|
+
## Desktop (10B-0)
|
|
24
|
+
|
|
25
|
+
- **Terminal owner leases** (PR #117) — the Desktop's terminal wire is now
|
|
26
|
+
`terminalApi: 2`: every terminal is a lease (256-bit token) held by the
|
|
27
|
+
admitted renderer *document*; a navigation, reload or renderer crash revokes
|
|
28
|
+
the document and cleans its viewers; the 20-terminal cap is reserved
|
|
29
|
+
synchronously so parallel opens cannot overshoot; writes are bounded and
|
|
30
|
+
one-way. Maintainer's native gate (real Electron, tmux, PTY) passed 23/23:
|
|
31
|
+
open/ready/write/close, quota, reload revocation, source isolation, no
|
|
32
|
+
residue after quit. See `packages/desktop/docs/terminal-owner-leases.md`.
|
|
33
|
+
|
|
34
|
+
## For providers
|
|
35
|
+
|
|
36
|
+
oats.aweb 1.13.0 (next) can declare `residents` `hostOnly`. Any messaging provider
|
|
37
|
+
that emits `identity` in its spawn/launch meta appears on the roster.
|
|
@@ -29,7 +29,7 @@ two roles never collapse: `oats-okf`, `oats-aweb`, `oats-jira`, `oats-linear`,
|
|
|
29
29
|
their `oats-package/` is consumed only as a package: `from: package`, pinned
|
|
30
30
|
in the workspace's `packages:`, locked and approved per version. The framework's
|
|
31
31
|
own souls therefore say `oats.okf: { from: package }` even though `oats-okf` is
|
|
32
|
-
a member. A bare version in `packages:` (`oats.okf: v2.1.
|
|
32
|
+
a member. A bare version in `packages:` (`oats.okf: v2.1.4`) resolves through
|
|
33
33
|
the catalog; a package outside it is written `git:<repo>@<ref>`.
|
|
34
34
|
|
|
35
35
|
## Join it on your machine
|
package/docs/workspaces.md
CHANGED
|
@@ -61,7 +61,7 @@ members: # repo refs, NO @revision (E_WORKSPAC
|
|
|
61
61
|
|
|
62
62
|
packages: # the ONLY versioned things
|
|
63
63
|
oats.framework: v1.1.3 # bare version → resolves through the official catalog
|
|
64
|
-
oats.okf: v2.1.
|
|
64
|
+
oats.okf: v2.1.4
|
|
65
65
|
acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
|
|
66
66
|
|
|
67
67
|
teams: # labels, declared once so they cannot drift
|
|
@@ -383,13 +383,16 @@ a label under `byTeam` that is not declared in `teams:` is `E_WORKSPACE_SCHEMA`.
|
|
|
383
383
|
**`byTeam` is kernel-merged; whether a provider honours what arrives is the
|
|
384
384
|
provider's.** `spawn --preview` shows the merged `settings.<cap>` so the
|
|
385
385
|
delivery is verifiable, and `instance.json.providers.<cap>` records it — but
|
|
386
|
-
oats.aweb
|
|
387
|
-
team
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
386
|
+
**oats.aweb 1.12.0 reads `team` from its payload** and mints into exactly that
|
|
387
|
+
team (`--team-id`), warning when the payload disagrees with an `OATS_TEAM_*`
|
|
388
|
+
value — so `byTeam.<label>.team` IS the per-label identity. What the payload
|
|
389
|
+
does not change is **where the `.aw` root is found**: the hook still searches
|
|
390
|
+
the bounded candidates in
|
|
391
|
+
[rebuild-to-v2.md §8b](rebuild-to-v2.md#8b-where-the-team-aw-lives-now-and-what-byteam-does-today)
|
|
392
|
+
and that root must hold a membership of the named team (the deployment's `.aw`
|
|
393
|
+
joined to every team its labels name is the simple layout). On oats.aweb
|
|
394
|
+
1.11.2 `team` was ignored (the root's active team won), so `byTeam` there is
|
|
395
|
+
a recorded intent only.
|
|
393
396
|
A store (`stores: { <name>: <repo ref> }`) names a repository; where a
|
|
394
397
|
knowledge base lives inside it is the knowledge provider's own concern — for
|
|
395
398
|
OKF 2.1.3 that is the **bindings file** (`bases.<alias>.repository` + `root`,
|
package/lib/core.mjs
CHANGED
|
@@ -6097,7 +6097,7 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
6097
6097
|
const inst = instance || meta?.instance || basename(home);
|
|
6098
6098
|
const command = renderLaunchRecipe(recipe, { home, instance: inst });
|
|
6099
6099
|
const selectionSource = frozen ? (config?.frozen || (!config && !selection.launchConfig && !selection.runtime) ? "frozen" : "config") : "config";
|
|
6100
|
-
return { recipe, command, runtime, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen };
|
|
6100
|
+
return { recipe, command, runtime, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
6101
6101
|
}
|
|
6102
6102
|
/** The environment a planned launch runs under: the host's base, the
|
|
6103
6103
|
* capabilities' validated env, the configuration's literals and its
|
|
@@ -6603,7 +6603,14 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
6603
6603
|
// Workspace model: the decision binds WHAT WILL BE MATERIALIZED — the
|
|
6604
6604
|
// resolution revision (member commits, package commits, payloads). A member
|
|
6605
6605
|
// that moved between preview and apply changes it → E_DECISION_STALE.
|
|
6606
|
-
if (o.prepared)
|
|
6606
|
+
if (o.prepared) {
|
|
6607
|
+
d.resolution = o.prepared.resolution.revision;
|
|
6608
|
+
// Decision 27 (K1′): the decision also binds the merged per-module payloads the spawn will
|
|
6609
|
+
// hand each provider (exactly what reaches OATS_SETTINGS) — so a confirmed apply covers every
|
|
6610
|
+
// provider fact (an identity choice, a delivery mode) by value, not only through the
|
|
6611
|
+
// resolution revision. Kernel-agnostic: whatever keys the payloads carry.
|
|
6612
|
+
d.effective.providers = structuredClone(o.prepared.resolution.payloads ?? {});
|
|
6613
|
+
}
|
|
6607
6614
|
d.revision = createHash("sha256").update(canonicalJson(d)).digest("hex").slice(0, 24);
|
|
6608
6615
|
return d;
|
|
6609
6616
|
};
|
|
@@ -7356,6 +7363,25 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
7356
7363
|
}
|
|
7357
7364
|
}
|
|
7358
7365
|
|
|
7366
|
+
/** Decision 27 (K2): the principal an instance ACTS AS, as its messaging-layer provider reported
|
|
7367
|
+
* it — a documented layer contract `{ mode: "local"|"global", alias, team, address|null, resident|null,
|
|
7368
|
+
* grant?: { id, expiresAt, scopes } }` under `identity` in the provider's hook meta. The kernel copies it
|
|
7369
|
+
* through and never interprets `grant`. Preference: the capability whose captured layer is "messaging";
|
|
7370
|
+
* else any capability that emitted an `identity` object. Null when none did. */
|
|
7371
|
+
export function servedIdentityOf(meta) {
|
|
7372
|
+
const cm = meta?.capabilityMeta;
|
|
7373
|
+
if (!cm || typeof cm !== "object") return null;
|
|
7374
|
+
const runtime = Array.isArray(meta.capabilityRuntime) ? meta.capabilityRuntime : [];
|
|
7375
|
+
const messaging = runtime.filter((c) => c && c.layer === "messaging").map((c) => c.id);
|
|
7376
|
+
const pick = (ids) => { for (const id of ids) { const v = cm[id]?.identity; if (v && typeof v === "object" && !Array.isArray(v)) return { ...v, provider: id }; } return null; };
|
|
7377
|
+
return pick(messaging) ?? pick(Object.keys(cm));
|
|
7378
|
+
}
|
|
7379
|
+
/** One line for a roster/inspect: `acts as <address> via grant, expires <t>` or `alias <alias> on <team>`. */
|
|
7380
|
+
export function servedIdentityLine(identity) {
|
|
7381
|
+
if (!identity) return null;
|
|
7382
|
+
if (identity.mode === "global" && identity.grant) return `acts as ${identity.address || identity.alias || identity.resident || "?"} via grant${identity.grant.expiresAt ? `, expires ${identity.grant.expiresAt}` : ""}`;
|
|
7383
|
+
return `alias ${identity.alias || "?"} on ${identity.team || "?"}`;
|
|
7384
|
+
}
|
|
7359
7385
|
export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
7360
7386
|
const windows = tmuxWindows(tmuxSession);
|
|
7361
7387
|
const readInstancesOf = (agentDir) => {
|
|
@@ -7394,7 +7420,8 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
|
7394
7420
|
try { const state = inspectHerdr({ ...meta.sessionTarget, binary: "herdr" }); liveness = { running: state.present, runtimeState: state.status }; }
|
|
7395
7421
|
catch (error) { liveness = { running: null, runtimeState: "unreachable", runtimeError: error.message }; }
|
|
7396
7422
|
}
|
|
7397
|
-
|
|
7423
|
+
const identity = servedIdentityOf(meta);
|
|
7424
|
+
return { ...meta, ...(identity ? { identity } : {}), ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
|
|
7398
7425
|
|
|
7399
7426
|
});
|
|
7400
7427
|
};
|
|
@@ -8296,6 +8323,7 @@ export function prepareLaunchHooks({ frozen, runtime, resolvedCfg, home, meta, c
|
|
|
8296
8323
|
// prepares the target runtime under its CAPTURED settings.
|
|
8297
8324
|
const ctx = contextDir || meta.repo;
|
|
8298
8325
|
const withLaunchHook = [];
|
|
8326
|
+
let hookMeta;
|
|
8299
8327
|
for (const p of capturedProviders(meta, frozen)) {
|
|
8300
8328
|
const manifest = ctx ? capabilityManifest(p.id, ctx) : undefined;
|
|
8301
8329
|
if (!manifest) throw oatsError("E_LAUNCH_PREPARATION", `${p.id} was part of this home's launch at spawn but is no longer installed in the scope; reinstall it (oats install) or respawn the instance; nothing was stopped`);
|
|
@@ -8328,6 +8356,12 @@ export function prepareLaunchHooks({ frozen, runtime, resolvedCfg, home, meta, c
|
|
|
8328
8356
|
refreshed.push(c.capability);
|
|
8329
8357
|
}
|
|
8330
8358
|
Object.assign(env, res.env || {});
|
|
8359
|
+
// A launch hook's `meta` is part of its documented return (the same shape
|
|
8360
|
+
// spawn persists as capabilityMeta). It was collected and then dropped
|
|
8361
|
+
// here, so a provider that re-issues a credential at every start — a
|
|
8362
|
+
// renewed session grant, for instance — left the ORIGINAL id on record and
|
|
8363
|
+
// retire undid the wrong one. Carry it to the caller; the start records it.
|
|
8364
|
+
hookMeta = res.meta && Object.keys(res.meta).length ? res.meta : undefined;
|
|
8331
8365
|
}
|
|
8332
8366
|
const unprepared = contributions.filter((c) => c.capability !== null && !refreshed.includes(c.capability) && c.launch && c.launch[frozen.runtime] !== undefined && c.launch[runtime] === undefined).map((c) => c.capability);
|
|
8333
8367
|
const legacyArgs = contributions.filter((c) => c.capability === null && c.launch && Object.keys(c.launch).length);
|
|
@@ -8335,7 +8369,7 @@ export function prepareLaunchHooks({ frozen, runtime, resolvedCfg, home, meta, c
|
|
|
8335
8369
|
if (unprepared.length) throw oatsError("E_LAUNCH_PREPARATION", `${unprepared.join(", ")} contributed ${frozen.runtime} launch arguments at spawn and none for ${runtime}; change that capability's setting (for example its delivery mode), or the provider must declare a launch hook; nothing was stopped`);
|
|
8336
8370
|
const launch = {};
|
|
8337
8371
|
for (const c of contributions) { if (c.capability === null && refreshed.length) continue; for (const [rt, args] of Object.entries(c.launch || {})) if (args) launch[rt] = `${launch[rt] ? `${launch[rt]} ` : ""}${args}`; }
|
|
8338
|
-
return { launch, env, contributions: contributions.filter((c) => !(c.capability === null && refreshed.length)), refreshed };
|
|
8372
|
+
return { launch, env, contributions: contributions.filter((c) => !(c.capability === null && refreshed.length)), refreshed, ...(hookMeta ? { meta: hookMeta } : {}) };
|
|
8339
8373
|
}
|
|
8340
8374
|
|
|
8341
8375
|
/** Start a stopped instance again in its existing home: no new home, no
|
|
@@ -8639,7 +8673,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
8639
8673
|
// The independent receipt first (retire and session consult it), then the
|
|
8640
8674
|
// mutable metadata; both tmp+rename. A failure between them is what the
|
|
8641
8675
|
// pending receipt exists for.
|
|
8642
|
-
const record = (meta, { id, backend, target, model, command, startedAt, reused, launch, runtime: newRuntime, yolo: newYolo, stop, nativeRecordId }, clearPending = true) => {
|
|
8676
|
+
const record = (meta, { id, backend, target, model, command, startedAt, reused, launch, runtime: newRuntime, yolo: newYolo, stop, nativeRecordId, hookMeta }, clearPending = true) => {
|
|
8643
8677
|
checkRoots();
|
|
8644
8678
|
const baselinePath = retirementBaselinePath(realHome);
|
|
8645
8679
|
let baseline;
|
|
@@ -8654,7 +8688,10 @@ export function startInstanceSession(home, o = {}) {
|
|
|
8654
8688
|
const restarts = (Array.isArray(meta.restarts) ? meta.restarts : []).slice(recorded ? -20 : -19);
|
|
8655
8689
|
if (!recorded) restarts.push({ startedAt, model: model ?? null, reused });
|
|
8656
8690
|
const next = { ...meta, model, command, launched: true, startId: id, restarts, restartCount: (meta.restartCount || 0) + (recorded ? 0 : 1),
|
|
8657
|
-
...(launch ? { launch } : {}), ...(newRuntime ? { runtime: newRuntime } : {}), ...(newYolo !== undefined ? { yolo: newYolo } : {})
|
|
8691
|
+
...(launch ? { launch } : {}), ...(newRuntime ? { runtime: newRuntime } : {}), ...(newYolo !== undefined ? { yolo: newYolo } : {}),
|
|
8692
|
+
// Launch-hook meta lands per capability over the spawn's record; a hook
|
|
8693
|
+
// that answered without meta keeps its previous entry (retire reads it).
|
|
8694
|
+
...(hookMeta ? { capabilityMeta: { ...(meta.capabilityMeta || {}), ...hookMeta } } : {}) };
|
|
8658
8695
|
if (backend === "herdr") { next.sessionTarget = target; delete next.tmux; }
|
|
8659
8696
|
else { next.tmux = { session: target.session, window: target.window, socket: resolve(target.socket) }; delete next.sessionTarget; }
|
|
8660
8697
|
if (capturedStart) atomicWriteFileSync(metaPath, canonicalJson(next), { assertRoots: checkRoots });
|
|
@@ -8753,7 +8790,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
8753
8790
|
const resolvedCfg = resolveOatsConfig(context, meta.agent);
|
|
8754
8791
|
let agent; try { agent = findAgent(dirname(dirname(dirname(realHome))), meta.agent); } catch { agent = undefined; }
|
|
8755
8792
|
const plan = planLaunch({ home: realHome, instance: meta.instance, meta, contextDir: context, agentLike: agent || { runtime: meta.runtime, model: meta.model, yolo: meta.yolo }, selection: { launchConfig: o.launchConfig, runtime: o.runtime, model: o.model, yolo: o.yolo }, resolvedCfg, env: o.env || process.env, assertRoots: checkRoots });
|
|
8756
|
-
launchPlan = { recipe: plan.recipe, command: plan.command, runtime: plan.runtime, model: plan.model, yolo: plan.yolo };
|
|
8793
|
+
launchPlan = { recipe: plan.recipe, command: plan.command, runtime: plan.runtime, model: plan.model, yolo: plan.yolo, ...(plan.hookMeta ? { hookMeta: plan.hookMeta } : {}) };
|
|
8757
8794
|
command = launchPlan.command; model = launchPlan.model;
|
|
8758
8795
|
} else if (o.model !== undefined && o.model !== null && String(o.model).trim() !== "") {
|
|
8759
8796
|
const resolved = resolveModelPreference(String(o.model), runtime);
|
|
@@ -8770,6 +8807,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
8770
8807
|
const paneEnvExports = paneEnv.map((r) => `export ${r.name}=${shq(r.value)}; `).join("");
|
|
8771
8808
|
checkRoots(); // launch hooks/preparation have run; no backend has been observed
|
|
8772
8809
|
const planExtra = launchPlan ? { launch: launchPlan.recipe, runtime: launchPlan.runtime, yolo: launchPlan.yolo,
|
|
8810
|
+
...(launchPlan.hookMeta ? { hookMeta: launchPlan.hookMeta } : {}),
|
|
8773
8811
|
...(capturedStart ? { capturedIntent: capturedStart.intent } : {}) } : {};
|
|
8774
8812
|
let target = receipt.target;
|
|
8775
8813
|
let state = { present: false, state: "not-launched" };
|
package/lib/resolve.mjs
CHANGED
|
@@ -121,6 +121,23 @@ function assertNoReservedKey(value, path) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/** Settings keys a manifest marks `hostOnly: true` (decision 27, K1″). */
|
|
125
|
+
export function hostOnlyKeys(manifest) {
|
|
126
|
+
const out = new Set();
|
|
127
|
+
const decl = isObject(manifest?.settings) ? manifest.settings : {};
|
|
128
|
+
for (const [key, spec] of Object.entries(decl)) if (isObject(spec) && spec.hostOnly === true) out.add(key);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** Refuse a hostOnly key in a committed or per-spawn payload layer: only oats-local.yaml `settings.<cap>` may carry it. */
|
|
132
|
+
function assertNoHostOnlyKey(value, path, hostOnly, capability) {
|
|
133
|
+
if (!hostOnly.size || !isObject(value)) return;
|
|
134
|
+
for (const key of hostOnly) {
|
|
135
|
+
if (Object.hasOwn(value, key)) {
|
|
136
|
+
throw fail("E_WORKSPACE_SCHEMA", `${path}/${key}: ${show(key)} is a host-only setting of ${capability} (its manifest marks it hostOnly) — it may appear only in the deployment's oats-local.yaml under settings.${capability}, never in a committed workspace or soul file or a --provider flag (decision 27)`, { path: `${path}/${key}`, key, capability, reason: "host-only-key" });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
/** Canonical JSON: object keys sorted (recursively), arrays in order, no whitespace. */
|
|
125
142
|
export function canonicalJson(value) {
|
|
126
143
|
if (value === undefined) return "null";
|
|
@@ -569,15 +586,21 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
569
586
|
for (const slot of SLOTS) if (isObject(definition[slot])) assertNoReservedKey(definition[slot], `/${slot}`);
|
|
570
587
|
for (const m of modules) {
|
|
571
588
|
const layers = [];
|
|
589
|
+
// Decision 27 (K1″): a manifest may mark a settings key `hostOnly` — a host fact (a custody
|
|
590
|
+
// path, a state directory) that only the deployment's own oats-local.yaml may supply. Every
|
|
591
|
+
// committed or per-spawn layer is refused with that key present, BEFORE the merge, because the
|
|
592
|
+
// provider receives one merged payload without provenance and cannot enforce this itself.
|
|
593
|
+
const hostOnly = hostOnlyKeys(m.manifest);
|
|
594
|
+
const committed = (payload, path) => { assertNoHostOnlyKey(payload, path, hostOnly, m.name); layers.push(payload); };
|
|
572
595
|
if (m.layer === "messaging" && isObject(workspace?.messaging)) {
|
|
573
596
|
// Decision 23: base ⊕ byTeam[soul.team]; `byTeam` never reaches the provider (nor may a team's own payload nest one).
|
|
574
597
|
const { byTeam, ...base } = workspace.messaging;
|
|
575
|
-
|
|
576
|
-
if (team !== null && isObject(byTeam) && isObject(byTeam[team])) { assertNoReservedKey(byTeam[team], `/messaging/byTeam/${team}`);
|
|
598
|
+
committed(base, "/messaging");
|
|
599
|
+
if (team !== null && isObject(byTeam) && isObject(byTeam[team])) { assertNoReservedKey(byTeam[team], `/messaging/byTeam/${team}`); committed(byTeam[team], `/messaging/byTeam/${team}`); }
|
|
577
600
|
}
|
|
578
|
-
if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`);
|
|
579
|
-
if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`); layers.push(settings[m.name]); }
|
|
580
|
-
if (Object.hasOwn(providers, m.name) && isObject(providers[m.name]))
|
|
601
|
+
if (m.layer && isObject(definition[m.layer])) { assertNoReservedKey(definition[m.layer], `/${m.layer}`); committed(definition[m.layer], `/${m.layer}`); }
|
|
602
|
+
if (Object.hasOwn(settings, m.name) && isObject(settings[m.name])) { assertNoReservedKey(settings[m.name], `/settings/${m.name}`); layers.push(settings[m.name]); } // the host layer: hostOnly keys are legal here
|
|
603
|
+
if (Object.hasOwn(providers, m.name) && isObject(providers[m.name])) committed(providers[m.name], `/spawn/providers/${m.name}`);
|
|
581
604
|
payloads[m.name] = mergePayload(...layers);
|
|
582
605
|
}
|
|
583
606
|
|
package/package-catalog.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"packages": {
|
|
4
4
|
"oats.okf": {
|
|
5
5
|
"url": "https://github.com/awebai/oats-okf.git",
|
|
6
|
-
"ref": "v2.1.
|
|
6
|
+
"ref": "v2.1.4",
|
|
7
7
|
"path": "oats-package"
|
|
8
8
|
},
|
|
9
9
|
"oats.aweb": {
|
|
10
10
|
"url": "https://github.com/awebai/oats-aweb.git",
|
|
11
|
-
"ref": "v1.
|
|
11
|
+
"ref": "v1.12.0",
|
|
12
12
|
"path": "oats-package"
|
|
13
13
|
},
|
|
14
14
|
"oats.jira": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.6",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|