@awebai/oats 0.25.5 → 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/docs/capabilities.md +8 -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 +15 -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 +19 -2
- package/docs/release-notes/v0.25.6.md +37 -0
- package/lib/core.mjs +29 -2
- package/lib/resolve.mjs +28 -5
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir,
|
|
32
32
|
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, stripInternalAnnotations, withConfigFile, teamAgentRoots,
|
|
33
33
|
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions,
|
|
34
|
-
ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
|
|
34
|
+
ensureRoot, findRoot, findAgent, listAgents, listInstances, servedIdentityLine, servedIdentityOf, listAgentDefs, createAgent as coreCreateAgent,
|
|
35
35
|
spawnInstance, spawnInstanceAsync, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
36
36
|
} from "../lib/core.mjs";
|
|
37
37
|
import {
|
|
@@ -935,7 +935,9 @@ function computeInspect({ onFail } = {}) {
|
|
|
935
935
|
const result = {
|
|
936
936
|
operationsApi: 1, kernel: OATS_VERSION,
|
|
937
937
|
scope: { context: ctx, requestedContext: requestedContext === ctx ? null : requestedContext, workspace: roots.length ? workspaceOf(roots[0]) : ctx, team: r.team || null, chain: chain.map((c) => ({ file: c._file, level: c._level, levelKind: levelOf(c._level) })), agentsRoots: roots },
|
|
938
|
-
selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config"
|
|
938
|
+
selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config",
|
|
939
|
+
// Decision 27 (K2): the principal this home acts as, from its messaging provider's hook meta.
|
|
940
|
+
...(meta ? { identity: servedIdentityOf(meta) } : {}) },
|
|
939
941
|
souls, sources, layers, capabilities, knowledge, snapshot, currentConfig,
|
|
940
942
|
problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
|
|
941
943
|
...(meta ? snapshotCaps.filter((c) => !mans[c.id]).map((c) => ({ code: "captured-capability-missing", message: `${c.id} was active when this home was composed but no manifest for it is acquired now`, capability: c.id })) : [])],
|
|
@@ -947,6 +949,7 @@ function printInspect(result) {
|
|
|
947
949
|
const { ctx, selectedSoul, home } = result._print; delete result._print;
|
|
948
950
|
const { souls, layers, capabilities } = result;
|
|
949
951
|
console.log(`oats inspect — ${shortPath(ctx)}${selectedSoul ? ` soul ${selectedSoul.name}` : ""}${home ? ` home ${shortPath(home)}` : ""}`);
|
|
952
|
+
if (result.selected?.identity) console.log(` identity: ${servedIdentityLine(result.selected.identity)}`);
|
|
950
953
|
for (const s of souls) console.log(` soul ${s.name} [${s.kind}${s.capability ? ` ${s.capability}` : ""}] runtime ${s.runtime}${s.model ? ` model ${s.model}` : ""} work ${s.work}${s.editable.fields.length ? "" : " (read-only)"}`);
|
|
951
954
|
for (const l of LAYERS) console.log(` layer ${l}: ${layers[l].id || (layers[l].disabled ? "disabled" : "none")}${layers[l].provenance ? ` (${layers[l].provenance})` : ""}`);
|
|
952
955
|
for (const c of capabilities) console.log(` ${c.id}@${c.version || "?"} ${c.health.status}${c.activation.enabled ? ` active:${c.activation.target}` : " inactive"}${c.operations.length ? ` ops: ${c.operations.map((o) => `${o.name}${o.available ? "" : "(unavailable)"}`).join(", ")}` : ""}`);
|
|
@@ -2343,6 +2346,7 @@ async function status() {
|
|
|
2343
2346
|
for (const i of a.instances) {
|
|
2344
2347
|
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
|
|
2345
2348
|
const key = i.home ?? `${a.name}/${i.instance}`;
|
|
2349
|
+
if (i.identity) console.log(` identity: ${servedIdentityLine(i.identity)}`);
|
|
2346
2350
|
const s = ws?.soul.get(key);
|
|
2347
2351
|
if (s && (verbose || s.status !== "current")) console.log(` ${soulDriftLine(s, a.name)}`);
|
|
2348
2352
|
const rows = ws?.drift.get(key) || [];
|
|
@@ -3366,7 +3370,7 @@ function versionCmd() {
|
|
|
3366
3370
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3367
3371
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3368
3372
|
// never listed.
|
|
3369
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "session-recompose", "readiness-verify", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 1, lifecycleApi: 1, readinessApi: 1, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
|
|
3373
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "session-recompose", "readiness-verify", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 1, lifecycleApi: 1, readinessApi: 1, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
|
|
3370
3374
|
return;
|
|
3371
3375
|
}
|
|
3372
3376
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
package/docs/capabilities.md
CHANGED
|
@@ -282,6 +282,14 @@ for this contract. Hyphenated vendors are also excluded because translating a
|
|
|
282
282
|
hyphen to `_` would let `aweb-evil.*` collide with names already inside
|
|
283
283
|
`aweb.*`'s `AWEB_*` namespace.
|
|
284
284
|
|
|
285
|
+
A manifest's `settings.<key>` may carry `hostOnly: true` (decision 27). Such a
|
|
286
|
+
key is a fact about the machine — a custody directory, a state root — and the
|
|
287
|
+
resolver accepts it only from the deployment's own `oats-local.yaml`
|
|
288
|
+
`settings.<capability>`; a committed workspace or soul file or a `--provider`
|
|
289
|
+
flag carrying it is refused (`E_WORKSPACE_SCHEMA`, reason `host-only-key`).
|
|
290
|
+
Declare it for any key whose value points at something a committed file must
|
|
291
|
+
never be able to choose.
|
|
292
|
+
|
|
285
293
|
A hook may return only names in its manifest's exact `environment` declaration.
|
|
286
294
|
For package capabilities that declaration is part of the integrity-locked tree
|
|
287
295
|
and of what the per-version approval showed; for member capabilities it is
|
|
@@ -263,6 +263,10 @@
|
|
|
263
263
|
},
|
|
264
264
|
"description": {
|
|
265
265
|
"type": "string"
|
|
266
|
+
},
|
|
267
|
+
"hostOnly": {
|
|
268
|
+
"type": "boolean",
|
|
269
|
+
"description": "When true, this key is a host fact (a custody path, a state directory): the resolver accepts it only from the deployment's oats-local.yaml settings.<capability> and refuses it in the workspace file, byTeam payloads, a soul's slot payload and --provider flags (E_WORKSPACE_SCHEMA reason host-only-key). Decision 27."
|
|
266
270
|
}
|
|
267
271
|
},
|
|
268
272
|
"additionalProperties": false
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
|
|
4
4
|
|
|
5
|
-
**Last update:** 2026-09-23 20:00Z · **0.25.0–0.25.
|
|
5
|
+
**Last update:** 2026-09-23 20:00Z · **0.25.0–0.25.5 PUBLISHED** (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID; quarantine-retry fix; launch meta + catalog pins) · **OKF v2.1.4 + oats.aweb v1.12.0 TAGGED and pinned** · **decision 27 accepted; K1′/K1″/K2 kernel PR next** · **oats.aweb 1.13.0 (#110) queued** · **Desktop 10B-0 resumed** · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
|
|
6
6
|
|
|
7
7
|
Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
|
|
8
8
|
|
|
@@ -565,6 +565,21 @@ is a member; a soul that lives in it may need a work clone). Under an explicit
|
|
|
565
565
|
`oats-local.yaml` `standalone:` header the next steps say the view is standalone
|
|
566
566
|
and list only that repo.
|
|
567
567
|
|
|
568
|
+
### 0.25.6 — decision 27: served identity is a messaging-layer fact (K1′, K1″, K2)
|
|
569
|
+
|
|
570
|
+
- **K1′** `decision.effective.providers` = the resolution's merged per-module payloads
|
|
571
|
+
(exactly what reaches `OATS_SETTINGS`), bound by the decision revision. No new flag.
|
|
572
|
+
- **K1″** manifest `settings.<key>.hostOnly: true` → the resolver refuses that key in
|
|
573
|
+
the workspace base, `byTeam[*]`, the soul's slot and `--provider` layers with
|
|
574
|
+
`E_WORKSPACE_SCHEMA { reason: "host-only-key", path, key, capability }`; only
|
|
575
|
+
`oats-local.yaml settings.<cap>` may carry it. Generalises decision 23's reserved
|
|
576
|
+
`byTeam` into a capability-declared attribute. Schema: `docs/capability-manifest.schema.json`.
|
|
577
|
+
- **K2** `oats status --json instances[].identity` and `oats inspect … selected.identity`
|
|
578
|
+
copy `capabilityMeta[<cap>].identity` (messaging-layer capability preferred; `provider`
|
|
579
|
+
added); text `identity: acts as <address> via grant, expires <t>` / `alias <a> on <team>`.
|
|
580
|
+
Layer contract shape in `docs/integrations.md`.
|
|
581
|
+
- `features[]` gains `served-identity`.
|
|
582
|
+
|
|
568
583
|
### 0.25.5 — launch-hook `meta` is persisted
|
|
569
584
|
|
|
570
585
|
`runLifecycleHooks("launch")` collected each capability's `meta` and the
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Desktop Phase F — the Desktop is built FOR workspace model v2
|
|
2
|
+
|
|
3
|
+
**Status**: boundary for the Desktop engineer, issued 2026-09-24 by the lead under
|
|
4
|
+
the human's direction: *"the desktop should not just adapt to the new version,
|
|
5
|
+
it should be natively built for it."* Supersedes the Phase 3 parity plan's
|
|
6
|
+
assumptions about what the Desktop reads; keeps its visual deliverable (the
|
|
7
|
+
redesign frames, excl. 05/06).
|
|
8
|
+
|
|
9
|
+
**Human's second directive, verbatim intent**: make ultra sure the Desktop is set
|
|
10
|
+
up to work in the new setup — new versions, new CLI, new deployment shape.
|
|
11
|
+
|
|
12
|
+
## 0. Why this is a rebuild of the model, not a patch
|
|
13
|
+
|
|
14
|
+
The Desktop today is a 0.24 product that *tolerates* 0.25 kernels: its
|
|
15
|
+
`ACCEPT_RANGE` admits `0.25.x`, so it launches, and the few CLI verbs it drives
|
|
16
|
+
(`version`, `session *`, `spawn`, `retire`, `schedule`, `catalog`) still answer.
|
|
17
|
+
But its **model of a deployment is 0.24's**, reimplemented in
|
|
18
|
+
`packages/desktop/server/deployment.mjs`: it reads `oats-config.yaml`,
|
|
19
|
+
`agents/<name>/soul`, `local-agents/`, and capability manifests from
|
|
20
|
+
`.agents/capabilities/installed/`, and derives the roster itself. None of these
|
|
21
|
+
is how a v2 deployment is shaped:
|
|
22
|
+
|
|
23
|
+
| 0.24 (what the Desktop reads) | v2 (what a deployment IS) |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `oats-config.yaml` at the repo root | `oats-local.yaml` at the deployment directory → `workspace:` URL |
|
|
26
|
+
| souls at `agents/<name>/soul` | souls at `souls/<name>` **in member repos**, materialised per commit into `agents/<name>/souls/<commit>` |
|
|
27
|
+
| capabilities installed into `.agents/capabilities/installed/` | packages resolved through `oats-workspace.yaml` + the official catalog, locked in `oats-lock.json`, **copied whole into each instance home** (`.oats/modules/<cap>`) |
|
|
28
|
+
| `team:` block | `oats-membership.yaml` `team:` label per member; `messaging.byTeam.<label>` payloads |
|
|
29
|
+
| `oats catalog` DTO | removed; the catalog is `package-catalog.json` resolved by `oats sync` |
|
|
30
|
+
| roster derived by the Desktop | `oats status --json` is the roster (agents, instances, `modules[]` drift, `soul` source drift, `identity`) |
|
|
31
|
+
|
|
32
|
+
A Desktop that keeps the left column and adds a few right-column fields is the
|
|
33
|
+
"adapt" outcome the human rejected. Phase F replaces the left column.
|
|
34
|
+
|
|
35
|
+
## 1. The principle: the kernel is the model; the Desktop renders and drives it
|
|
36
|
+
|
|
37
|
+
- **Read model**: every fact the Desktop shows about a deployment comes from
|
|
38
|
+
the kernel's JSON surfaces — `oats status --json`, `oats inspect --json`,
|
|
39
|
+
`oats workspace status --json`, `oats spawn --preview --json`, `oats
|
|
40
|
+
readiness --json`, `oats version --json`, `oats sync --json`. The Desktop
|
|
41
|
+
does not parse `oats-config.yaml`, `oats-local.yaml`, `soul.yaml`,
|
|
42
|
+
manifests or lock files itself. Where a fact is missing from a kernel
|
|
43
|
+
surface, the fix is a kernel PR (lead's lane), not a Desktop-side parser.
|
|
44
|
+
- **Write model**: every mutation is a kernel verb with `--json`: `sync`,
|
|
45
|
+
`sync --approve`, `spawn` (preview → `--expect-decision` apply), `retire`,
|
|
46
|
+
`session start|restart|recompose`, `schedule *`, `onboard`. The Desktop
|
|
47
|
+
never writes a deployment file.
|
|
48
|
+
- **Version contract**: `oats version --json` `features[]` is the capability
|
|
49
|
+
probe. The Desktop's `ACCEPT_RANGE` moves to `>=0.25.6 <0.27.0` (the first
|
|
50
|
+
kernel with `served-identity`), and each feature the UI depends on is gated
|
|
51
|
+
on its `features[]` name, not on a version number.
|
|
52
|
+
|
|
53
|
+
## 2. Deliverables (slices; each is one PR against main, each reviewed by the lead)
|
|
54
|
+
|
|
55
|
+
**F1 — Deployment model on kernel JSON.** Replace
|
|
56
|
+
`server/deployment.mjs`'s own readers with `oats status --json` (+ `oats
|
|
57
|
+
workspace status --json` for the workspace header: name, key, members, packages,
|
|
58
|
+
lock state, `approvalNeeded`). Roster rows carry `modules[]` drift, `soul`
|
|
59
|
+
source (`repo: <member> @ <c7>`, "member moved since"), `identity`. Legacy
|
|
60
|
+
`local-agents/`/`tmp-agents/` paths are dropped. Remove `server/catalog.mjs`'s
|
|
61
|
+
`oats catalog` DTO validation (the verb no longer exists).
|
|
62
|
+
|
|
63
|
+
**F2 — Workspace onboarding and sync.** A "Open deployment" flow that: detects a
|
|
64
|
+
directory with `oats-local.yaml` (v2), or offers `oats onboard` for one without
|
|
65
|
+
(the kernel asks for the deployment directory and the workspace URL — the
|
|
66
|
+
Desktop collects both, never invents a folder name; decision 9). A "Sync"
|
|
67
|
+
action runs `oats sync --json`; exit 2 with `approvalNeeded[]` renders an
|
|
68
|
+
approval sheet showing each package's `executables` digest and applies with
|
|
69
|
+
`oats sync --approve <id>@<version>` (the version string is what
|
|
70
|
+
`approvalNeeded[].version` reports — for a git-pinned package, the full OID).
|
|
71
|
+
Lock drift and `E_PACKAGE_INTEGRITY` are surfaced verbatim.
|
|
72
|
+
|
|
73
|
+
**F3 — Spawn dialog on the v2 preview.** The preview already carries
|
|
74
|
+
`modules`, `providers`, `settings.<cap>`, `team`, `resolution`, `decision`.
|
|
75
|
+
Render: which modules the instance will get and from where (package vs member,
|
|
76
|
+
commit); the merged `settings.<cap>` per provider (read-only); **Identity**
|
|
77
|
+
select (local | global) with a Resident field for global, prefilled from
|
|
78
|
+
`settings.<messaging cap>.identity`, forwarded as `--provider <cap>
|
|
79
|
+
identity.mode=… identity.resident=…` (decision 27 — there is no kernel flag);
|
|
80
|
+
`decision.effective.providers` is what the confirm binds. `cli-adapter.mjs`
|
|
81
|
+
`SPAWN_ARG_RULES` gains one `provider` rule (capability id, dotted key, value
|
|
82
|
+
grammar); no identity-named rules. Work mode select includes `workspace`.
|
|
83
|
+
|
|
84
|
+
**F4 — Instance card and roster on v2 facts.** The served-identity line (`acts
|
|
85
|
+
as <address> via grant, expires <t>` / `alias <a> on <team>`); module rows with
|
|
86
|
+
"moved since" markers and a **re-spawn** action (preview → apply, then retire
|
|
87
|
+
the old instance — module homes answer `E_UNSUPPORTED_MODE` to
|
|
88
|
+
`session recompose` by design; an instance never changes under itself); the
|
|
89
|
+
soul-source row; quarantine state from `rollbackIncomplete` with the retry
|
|
90
|
+
action (`oats retire`) and `--force` behind a confirm.
|
|
91
|
+
|
|
92
|
+
**F5 — Redesign frames** (the original Phase 3 deliverable, excl. 05/06),
|
|
93
|
+
implemented on top of F1–F4 rather than on the 0.24 model.
|
|
94
|
+
|
|
95
|
+
**F6 — Version and doctor surface.** `oats version --json` and `oats doctor
|
|
96
|
+
--json` in an About/Health pane; `ACCEPT_RANGE` and the three pins move to
|
|
97
|
+
`>=0.25.6`; a kernel below the floor is refused with the upgrade command shown.
|
|
98
|
+
|
|
99
|
+
Order: F1 → F2 → F3 → F4 → F5 → F6, or F1 then F3/F4 in parallel if the engineer
|
|
100
|
+
spawns children (its call; the lead reviews each PR).
|
|
101
|
+
|
|
102
|
+
## 3. What the engineer must LEARN first (before F1)
|
|
103
|
+
|
|
104
|
+
Read, in this order, in the checked-out main:
|
|
105
|
+
1. `docs/workspaces.md` — the v2 model end to end (deployment vs workspace,
|
|
106
|
+
members, packages, payloads, `byTeam`, hosting).
|
|
107
|
+
2. `docs/rebuild-to-v2.md` — how an operator builds a deployment (this is the
|
|
108
|
+
flow F2 wraps).
|
|
109
|
+
3. `docs/desktop-cli-api.md` — every JSON surface, with examples; note
|
|
110
|
+
`features[]`, `decision.effective.providers`, `instances[].identity`.
|
|
111
|
+
4. `docs/design/2026-09-23-workspace-module-contracts.md` §0.25.x — what
|
|
112
|
+
changed per release and why.
|
|
113
|
+
5. `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md` and
|
|
114
|
+
`…/served-identity-is-a-messaging-layer-fact.md` — the decisions.
|
|
115
|
+
6. `test/fixtures/northwind/build.mjs` — the two-team fixture workspace; run
|
|
116
|
+
`node --test test/spawn-workspace.test.mjs` once and read what a v2 spawn
|
|
117
|
+
produces on disk (`.oats/modules/`, `instance.json` `modules`/`providers`/
|
|
118
|
+
`workspace.soul.id`).
|
|
119
|
+
|
|
120
|
+
Then build a scratch deployment by hand with the CLI (`oats onboard`, `oats
|
|
121
|
+
sync`, `oats sync --approve`, `oats spawn --preview`, `oats spawn --no-launch`,
|
|
122
|
+
`oats status`, `oats inspect`, `oats retire`) against the Northwind fixture
|
|
123
|
+
remotes, and keep the transcript: F1's tests are written against exactly those
|
|
124
|
+
JSON shapes.
|
|
125
|
+
|
|
126
|
+
## 3b. Native rework, not a compatibility layer (human, 2026-09-24)
|
|
127
|
+
|
|
128
|
+
The human's rule, verbatim intent: *"do a native rework — do not keep v1-specific
|
|
129
|
+
things, and no v1 modules calling v2 modules."* Concretely:
|
|
130
|
+
|
|
131
|
+
- **Remove, do not wrap.** A 0.24 reader (`server/deployment.mjs`'s
|
|
132
|
+
`oats-config.yaml`/`soul.yaml`/manifest parsing, `local-agents/`,
|
|
133
|
+
`.agents/capabilities/installed/`, the `oats catalog` DTO) is deleted in the
|
|
134
|
+
slice that replaces it — never kept behind a flag, a fallback branch, or an
|
|
135
|
+
"if the kernel is old" path. The Desktop supports one kernel line
|
|
136
|
+
(`ACCEPT_RANGE` from 0.25.6) and refuses older ones with the upgrade command.
|
|
137
|
+
- **No adapters between generations.** No module whose job is to translate a
|
|
138
|
+
v1-shaped object into a v2-shaped one or vice versa (no `legacyRosterToV2()`,
|
|
139
|
+
no `toOldCard()`); the v2 kernel JSON is consumed where it is read and shaped
|
|
140
|
+
once for rendering. If a v1 module still needs a v2 fact, the v1 module is
|
|
141
|
+
the thing being replaced — replace it, do not feed it.
|
|
142
|
+
- **Names and types follow v2.** Types, fields and UI labels use the kernel's
|
|
143
|
+
vocabulary (workspace, member, package, module, deployment, soul source,
|
|
144
|
+
served identity); 0.24 vocabulary (installed capability, config chain, team
|
|
145
|
+
block, agents root as identity) leaves the codebase with the code that used
|
|
146
|
+
it. `git grep` for the old terms is part of each slice's exit check.
|
|
147
|
+
- **Tests follow the same rule.** Fixtures shaped like 0.24 deployments are
|
|
148
|
+
deleted with the readers; new fixtures are v2 deployments produced by the
|
|
149
|
+
kernel (Northwind or a hand-built scratch deployment), not hand-written
|
|
150
|
+
JSON imitating old shapes.
|
|
151
|
+
- **One exception, stated per case.** Where a 0.24 concept has a genuine v2
|
|
152
|
+
successor with the same meaning and the Desktop code is already correct for
|
|
153
|
+
it (a terminal broker, a tmux target admission), it stays — the PR names it
|
|
154
|
+
as "unchanged, v2-agnostic", not as "kept for compatibility".
|
|
155
|
+
|
|
156
|
+
Exit check for Phase F as a whole: no file under `packages/desktop/` reads a
|
|
157
|
+
deployment file, names a 0.24 concept, or contains a code path that exists
|
|
158
|
+
only for a kernel below the floor.
|
|
159
|
+
|
|
160
|
+
## 4. Rules that do not change
|
|
161
|
+
|
|
162
|
+
- `packages/desktop/**` only; kernel gaps go to the lead as a written ask
|
|
163
|
+
(they become kernel PRs; the engineer never adds a Desktop-side parser to
|
|
164
|
+
work around one).
|
|
165
|
+
- No native tmux/PTY/Electron execution by the agent; the lead's native gate
|
|
166
|
+
at review time is the acceptance.
|
|
167
|
+
- Every slice: focused tests + the intended mutants on the new code; the
|
|
168
|
+
Desktop suite green; one PR per slice against main; lead's pr-review.
|
|
169
|
+
- The design frames are the visual authority; the kernel JSON is the data
|
|
170
|
+
authority; where a frame shows a 0.24 concept (an "installed capability"
|
|
171
|
+
list, a `team:` block), the frame is adapted to the v2 concept and the
|
|
172
|
+
adaptation noted in the PR.
|
|
173
|
+
|
|
174
|
+
## 5. Acceptance for Phase F as a whole
|
|
175
|
+
|
|
176
|
+
The lead builds a fresh v2 deployment from the Northwind fixture using ONLY the
|
|
177
|
+
Desktop (open → onboard → sync → approve → spawn with a global identity →
|
|
178
|
+
inspect → retire) on kernel 0.25.6+, and every fact shown matches `oats status
|
|
179
|
+
--json` / `oats inspect --json` byte for byte. Nothing in
|
|
180
|
+
`packages/desktop/server` reads a deployment file.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Phase D — the OATS project runs on the architecture it offers (plan)
|
|
2
|
+
|
|
3
|
+
**Status**: plan, 2026-09-24, lead. Decisions 18–22 of the workspace model, the
|
|
4
|
+
five-soul roster and its 2026-09-24 amendment, the human's sequencing
|
|
5
|
+
("knowledge centralisation first"; "do not retire live souls until their
|
|
6
|
+
instances retire"). Mailed to the human and the OSS coordinator before the
|
|
7
|
+
first swarm. Method per standing instruction: write → swarm build →
|
|
8
|
+
adversarial-review swarm → PR → main; releases under delegated authority.
|
|
9
|
+
|
|
10
|
+
## Slices, in order
|
|
11
|
+
|
|
12
|
+
### D1 — Knowledge centralisation (IN PROGRESS)
|
|
13
|
+
|
|
14
|
+
Goal: every roster soul's knowledge lives in the central base
|
|
15
|
+
`awebai/oats-knowledge` (OKF 2.1.4), one owned node each; the in-repo
|
|
16
|
+
`agents/*/soul/knowledge` bundles stop receiving writes and are decommissioned
|
|
17
|
+
when no live instance links them.
|
|
18
|
+
|
|
19
|
+
Done: nodes `oats-operator-expert` and `integrations-expert` chartered
|
|
20
|
+
(oats-knowledge PR #3); eight attributed seeds landed in
|
|
21
|
+
`agents/oats-expert/soul/knowledge/inbox` (oats PR #121); the roster amendment
|
|
22
|
+
accepted; the rule "no per-soul knowledge merges" in force.
|
|
23
|
+
|
|
24
|
+
Work:
|
|
25
|
+
1. **Copy-migrate by judgement** (the 2026-09-21 method: two-part test plus the
|
|
26
|
+
2026-09-24 recipe refinement; not copying): `agents/oats-expert/soul/knowledge`
|
|
27
|
+
(~130 files) → node `oats-expert`; `agents/oats-desktop-engineer/soul/knowledge`
|
|
28
|
+
(~120) → `oats-desktop-expert`; `agents/cli-dev/soul/knowledge` (~155) →
|
|
29
|
+
`oats-kernel-expert`; `agents/integrations-expert/soul/knowledge` (13) →
|
|
30
|
+
`integrations-expert`. Others (`dev-coordinator`, `docs-expert`, `ux-designer`,
|
|
31
|
+
`lead`, `oats-coordinator`) are assessed for the few universal concepts they
|
|
32
|
+
hold and otherwise not carried. One PR per node on oats-knowledge, reviewed
|
|
33
|
+
by the lead; the OSS coordinator reviews the operator and integrations PRs.
|
|
34
|
+
2. **Seed the operator node** — attributed to `oats-expert-antares`:
|
|
35
|
+
MOVE (not copy) from the oats-expert bundle: `lessons/the-aweb-team-root-must-sit-where-the-spawn-hook-looks`,
|
|
36
|
+
`lessons/okf-state-directory-must-sit-outside-every-work-tree`,
|
|
37
|
+
`lessons/capability-trust-hash-covers-the-installation-record`,
|
|
38
|
+
`lessons/a-locally-minted-oats-identity-has-no-cross-team-first-contact-address`,
|
|
39
|
+
`lessons/stale-checkout-serves-stale-soul`,
|
|
40
|
+
`playbooks/rebuild-a-deployment-in-scratch-against-local-bare-remotes`;
|
|
41
|
+
generalise the R1–R10 rebuild findings and the published-combination
|
|
42
|
+
verifications from `stewardship/delivery-log` (the log keeps the record).
|
|
43
|
+
From the inbox: `check-the-record-before-redesigning-identity`,
|
|
44
|
+
`grant-custody-service-and-renewal-belong-on-the-custody-host` (operator
|
|
45
|
+
half), `the-wake-broker-accepts-a-grant-home`.
|
|
46
|
+
3. **Seed the integrations node** — from the inbox:
|
|
47
|
+
`a-merged-provider-payload-cannot-enforce-host-only-keys`,
|
|
48
|
+
`aw-grant-commands-resolve-the-identity-from-cwd-only` (discipline half),
|
|
49
|
+
`oats-runtime-requirements-for-grant-backed-resident-operation`; from the
|
|
50
|
+
integrations-expert bundle: `fake-aw-must-model-real-refusals` and the rest
|
|
51
|
+
of this week's harvested lessons.
|
|
52
|
+
4. **Route the remainder of the inbox**: aweb package expert (D3) gets
|
|
53
|
+
`aweb-grants-are-team-bound-to-the-custody-identitys-active-team`,
|
|
54
|
+
`a-grant-signed-send-must-name-the-subject-as-sender` and the aw halves;
|
|
55
|
+
kernel expert gets `path-keyed-owner-registry-breaks-under-per-commit-soul-copies`.
|
|
56
|
+
5. **Rebind the souls** in `souls/<name>/`: the `knowledge:` grammar there is
|
|
57
|
+
still 0.24's (`capability` + `source: git:…@v2.1.2#oats-package`); v2 is
|
|
58
|
+
`capabilities: { oats.okf: { from: package } }` plus `okf.json`
|
|
59
|
+
(`{ version: 1, owner: <node owner uuid>, owns: ["oats/<node>"], reads: [...] }`)
|
|
60
|
+
and the store `oats` naming `awebai/oats-knowledge` / `knowledge` / `main`.
|
|
61
|
+
Add `souls/oats-operator-expert` (rename of `oats-setup-expert`, keeps the
|
|
62
|
+
`oats.setup` charter) and `souls/integrations-expert`.
|
|
63
|
+
6. **Do NOT delete** `agents/<n>/soul/knowledge` or the legacy souls while a
|
|
64
|
+
live instance links them (human rule). Record which are live; decommission
|
|
65
|
+
as they retire; new spawns use `souls/<n>`.
|
|
66
|
+
7. **Release playbook** (`oats-expert` node, stewardship area): landing order
|
|
67
|
+
for provider PRs (tag → pin on the branch → squash; a bundled provider never
|
|
68
|
+
lands ahead of its tag), the version literals to bump on a catalog bump,
|
|
69
|
+
the mirror checker, the bump-PR step. First entries: today's two lessons.
|
|
70
|
+
|
|
71
|
+
### D2 — The OATS workspace as a v2 workspace (decisions 18, 19, 21)
|
|
72
|
+
|
|
73
|
+
`oats-workspace.yaml` at the `oats` repo (name `oats`; members = the seven
|
|
74
|
+
framework repos incl. `oats` itself; `packages:` = oats.framework / okf / aweb /
|
|
75
|
+
jira / linear / authoring / dev pinned from the catalog; `defaults.capabilities`
|
|
76
|
+
= `oats.core` from package + knowledge `oats.okf`); `oats-membership.yaml` ×7
|
|
77
|
+
(each package repo is a member AND a package publisher — non-collapse:
|
|
78
|
+
`packages:` resolves it as a package, membership only grants trust and a soul).
|
|
79
|
+
`package-catalog.json` stays the official marketplace (decision 21); the `oats`
|
|
80
|
+
repo keeps `oats-dev` as dev capabilities. A fresh deployment directory (asked
|
|
81
|
+
for, never named by convention — decision 9) is the acceptance: `oats onboard`
|
|
82
|
+
→ `sync` → `approve` → spawn every roster soul `--no-launch`.
|
|
83
|
+
|
|
84
|
+
### D3 — Souls to `souls/<name>` and the six package-expert souls (decision 20)
|
|
85
|
+
|
|
86
|
+
Each package repo carries `souls/<pkg>-expert` (okf-expert, aweb-expert,
|
|
87
|
+
jira-expert, linear-expert, authoring-expert, dev-expert), the expert in that
|
|
88
|
+
package, with a node in the central base from day one. **Seams named in the
|
|
89
|
+
charters** (roster amendment): `aweb-expert` READS
|
|
90
|
+
`aweb-protocol-expert` in base `aweb-oss-knowledge` (repo
|
|
91
|
+
`github.com/awebai/aweb`, root `knowledge/`, branch `main`, OKF 2.1.x
|
|
92
|
+
descriptor at `knowledge/okf-base.json`; owner `1913b77b-…`) through a
|
|
93
|
+
read-only store reference; `okf-expert` names its seam to the knowledge-theory
|
|
94
|
+
material in `oats-expert`. Whether the aweb bookshelf decisions the program
|
|
95
|
+
rests on are published into that node is the aweb side's call (asked).
|
|
96
|
+
|
|
97
|
+
### D4 — `oats.core` and `oats.setup` rewritten (decision 22, W9b)
|
|
98
|
+
|
|
99
|
+
Not patched: written for the v2 world. `oats.core`: home layout, `oats status`
|
|
100
|
+
with modules/soul/identity rows, spawn preview → apply, `sync --approve`, the
|
|
101
|
+
two-directory boundary, what a module is. `oats.setup` (held by
|
|
102
|
+
`oats-operator-expert`): onboarding that ASKS for the deployment directory and
|
|
103
|
+
the workspace URL, the hosting rule (decision 26), the public-member executable
|
|
104
|
+
rule, the rebuild guide as procedure with the operator node as rationale.
|
|
105
|
+
Developer souls in `oats.dev` gain `promotesTo: <node>` (roster amendment);
|
|
106
|
+
the harvester delivers to that node as a PR the owning expert reviews.
|
|
107
|
+
|
|
108
|
+
### D5 — Catalog update and 0.26.0
|
|
109
|
+
|
|
110
|
+
Catalog pins for the new package versions; **widen Desktop `ACCEPT_RANGE` and
|
|
111
|
+
the three pins FIRST** (minor bump rule); release notes; tag; the fresh
|
|
112
|
+
deployment from D2 rebuilt on the published artefacts by the OSS coordinator
|
|
113
|
+
(outsider verification) before the program board marks Phase D done.
|
|
114
|
+
|
|
115
|
+
## Adversarial review (per slice)
|
|
116
|
+
|
|
117
|
+
Each slice's swarm is followed by a review swarm with the standing lenses
|
|
118
|
+
(direction against the decisions; correctness by reproduction; security —
|
|
119
|
+
trust at acquisition, hoisted paths, hook approval, host-only keys; docs as
|
|
120
|
+
contract — every guide claim has a test), plus two Phase-D-specific ones: **the
|
|
121
|
+
outsider** (can a reader who was not in the room set OATS up from `oats.setup`
|
|
122
|
+
alone?) and **the seam** (does every cross-project read resolve to a real node
|
|
123
|
+
with a real owner?).
|
|
124
|
+
|
|
125
|
+
## Out of scope
|
|
126
|
+
|
|
127
|
+
Desktop (Phase F, its own boundary); oats.aweb 1.13.0 re-land (held on the
|
|
128
|
+
aweb release); legacy `~/OATS` deployment cutover (the operator's, on the
|
|
129
|
+
published 0.26.0).
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -376,12 +376,32 @@ the pre-fix marker and is never accepted for dispatch.
|
|
|
376
376
|
import; `--instructions-file`/`--def-file` refused with `E_BAD_ARGS`). Test:
|
|
377
377
|
the deployment tree is byte-identical after a success, a refusal and an
|
|
378
378
|
unknown-soul preview.
|
|
379
|
+
**Workspace deployments (0.25.1+) — one stated exception**: the first preview
|
|
380
|
+
of a workspace soul may populate the deployment's per-commit soul cache
|
|
381
|
+
(`agents/<soul>/souls/<commit>/`, the swappable `agents/<soul>/soul` pointer,
|
|
382
|
+
`soulFetched: true` in the result). That cache is derived, content-addressed
|
|
383
|
+
and idempotent — the same member commit yields the same bytes, a later
|
|
384
|
+
preview of the same commit writes nothing — and nothing else moves: no lock,
|
|
385
|
+
no event, no home, no instance. A Desktop treats a preview as
|
|
386
|
+
side-effect-free for everything it shows; it must not assume the deployment
|
|
387
|
+
directory's byte-identity across the FIRST preview of a soul or commit.
|
|
379
388
|
- **Exact root**: `spawn <soul> --agents-root <abs>` binds the soul to that root
|
|
380
389
|
(as inspect/readiness take it) — no team-soul / capability-agent / importable-
|
|
381
390
|
def fallback; mismatch → `E_SOUL_UNKNOWN`. The preview echoes
|
|
382
391
|
`subject {soul, agentsRoot|null, dir|null}` **as given, byte-exact**.
|
|
383
392
|
- **Decision binding**: `decision {instance, home, branch, base{ref,oid},
|
|
384
|
-
revision}` (24-hex).
|
|
393
|
+
effective{…, providers}, resolution, revision}` (24-hex). From 0.25.6
|
|
394
|
+
(`features: served-identity`) `effective.providers` is the merged
|
|
395
|
+
per-module payload each provider will receive (`{ "<cap>": {…} }`, exactly
|
|
396
|
+
`settings.<cap>` of the preview) — so a confirmed apply binds every
|
|
397
|
+
provider fact (an identity choice, a delivery mode) **by value**; a Desktop
|
|
398
|
+
that changes a provider field re-previews. `instances[].identity` (status)
|
|
399
|
+
and `selected.identity` (inspect) carry the served principal a messaging
|
|
400
|
+
provider reported: `{ mode: "local"|"global", alias, team, address|null,
|
|
401
|
+
resident|null, grant?: { id, expiresAt, scopes }, provider }`; absent when
|
|
402
|
+
no provider emitted one. A Desktop offers the identity choice as
|
|
403
|
+
`--provider <cap> identity.mode=… identity.resident=…` — there is no
|
|
404
|
+
kernel flag for it. Apply with `spawn … --expect-decision <revision>`: the
|
|
385
405
|
kernel recomputes name/home/branch/base under the same placement path and
|
|
386
406
|
refuses **`E_DECISION_STALE`** with `details.decision` (the fresh one) on ANY
|
|
387
407
|
drift — no auto-suffix, no silent re-base, nothing created. A GUI re-previews
|
|
@@ -774,9 +794,12 @@ written. Captured selectors are refused (`E_BAD_ARGS`).
|
|
|
774
794
|
|
|
775
795
|
- `sync` is the `syncApi: 1` report of the first sync (members, packages,
|
|
776
796
|
changes, `approvalNeeded`, `problems`); `lock` is the lock it wrote.
|
|
777
|
-
- **Exit `2` with `ok: true`** when `sync.approvalNeeded` is non-empty
|
|
778
|
-
|
|
779
|
-
|
|
797
|
+
- **Exit `2` with `ok: true`** when `sync.approvalNeeded` is non-empty. Approve
|
|
798
|
+
non-interactively with `oats sync --dir <dir> --approve <id>@<version> …`
|
|
799
|
+
(0.25.2+; `<version>` is `approvalNeeded[].version` verbatim — a catalog
|
|
800
|
+
version, or the full commit OID for a git-pinned package); a Desktop renders
|
|
801
|
+
`approvalNeeded[].executables` (the digest of the executable set it is
|
|
802
|
+
approving) and `targets`, then runs that command. Exit `0` otherwise.
|
|
780
803
|
- `hosting` states decision 26 (the kernel cannot see forge visibility, so it
|
|
781
804
|
reports `hostIsMember` and the rule rather than judging).
|
|
782
805
|
- `next.clone[]` is one row per **confirmed** member (`url` = what the
|
|
@@ -801,9 +824,10 @@ written. Captured selectors are refused (`E_BAD_ARGS`).
|
|
|
801
824
|
|
|
802
825
|
Discovers, confirms membership, resolves `packages:` to commits, writes
|
|
803
826
|
`oats-lock.json` (lockfileVersion 3), reports. **Exit `2` with `ok: true`** when
|
|
804
|
-
the lock was written but approvals are pending (`approvalNeeded` non-empty)
|
|
805
|
-
|
|
806
|
-
|
|
827
|
+
the lock was written but approvals are pending (`approvalNeeded` non-empty).
|
|
828
|
+
Approve with repeatable `--approve <id>@<version>` (0.25.2+, non-interactive;
|
|
829
|
+
`<version>` = `approvalNeeded[].version` verbatim); the interactive prompt is
|
|
830
|
+
the TTY fallback, not the contract. Exit `0` otherwise.
|
|
807
831
|
|
|
808
832
|
```json
|
|
809
833
|
{"syncApi":1,
|
|
@@ -1052,6 +1076,10 @@ refreshes the home in place:
|
|
|
1052
1076
|
materialized modules is not supported yet; re-spawn"): its `AGENTS.md` was
|
|
1053
1077
|
composed from the soul at a recorded member commit plus the materialized
|
|
1054
1078
|
modules' injects, and the instance never changes under itself (decision 7).
|
|
1079
|
+
**Desktop contract (Phase F, F4)**: for a module home, show the drift rows
|
|
1080
|
+
and offer *re-spawn* (preview → apply of the same soul/purpose, then retire
|
|
1081
|
+
the old instance); do not offer "recompose". A kernel recompose for module
|
|
1082
|
+
homes is not planned — an instance never changes under itself.
|
|
1055
1083
|
The refresh path for such a home is a new spawn (the soul is re-fetched at
|
|
1056
1084
|
the member's current commit). `session-recompose` **stays advertised** in
|
|
1057
1085
|
`features[]` because the verb still works for classic homes; gate the UI
|
package/docs/integrations.md
CHANGED
|
@@ -137,8 +137,25 @@ the event first" in the inject and skill; contribute launch arguments so the
|
|
|
137
137
|
session is woken; enforce the soul type's `reach` on both sides; state
|
|
138
138
|
whether the address outlives the instance; and keep task coordination out.
|
|
139
139
|
Any messaging provider emits `identity: { mode, alias, team, address|null,
|
|
140
|
-
resident|null, grant?: { id, expiresAt, scopes } }` in its spawn meta
|
|
141
|
-
`oats.aweb` is the reference
|
|
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.
|
|
142
159
|
|
|
143
160
|
**Tasks.** Teach claim, update, block, hand off, and complete; identify the
|
|
144
161
|
instance to the tracker in a way that survives it; keep conversation out.
|
|
@@ -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.
|
package/lib/core.mjs
CHANGED
|
@@ -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
|
};
|
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.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",
|