openvibers/openvibe.network Commits

Refresh the T2 design docs to fbd6312 and decide app on_behalf_of confirmation (#51)

OP OpenVibers committed 5 Oct 2026 · 0c02e01888 · 6 files, +54 −27

**Files changed**

- `docs/t2-projects-and-grants.md:8` — slice-6–7 pin v0.90.0 → v0.97.0.
- `docs/t2-projects-and-grants.md:10` — slice-9 owner notification stated as built (`confirmations.notifyOwner` → `openvibe-shared` `TYPES.CONFIRMATION_REQUESTED`, PR #47 / `fbd6312`), dropping "still waits".
- `docs/t2-projects-and-grants.md:14-18` — pinned version v0.90.0 → v0.97.0, `origin/main` `fb34f33` → `fbd6312`, "latest tag v0.94.0 additive" wording removed, "v0.86–v0.90 additive" → "v0.86–v0.97"; pin noted as moving in a separate PR.
- `docs/t2-projects-and-grants.md:62` — gap-list header reworded to past tense (the v0.85.0 snapshot no longer reads as currently blocked).
- `docs/t2-projects-and-grants.md:520-521` — §6 "Sensitive app grants" now points at the §9 decision instead of "listed as unresolved".
- `docs/t2-projects-and-grants.md:608` — slice-7 note reworded so no stale v0.90.0 literal remains.
- `docs/t2-projects-and-grants.md:617-624` — slice 9 retitled "Decision events and the owner notification" (built) with `notifyOwner` / `server/index.js:621` / Shared v2.7.0; trailing note updated.
- `docs/t2-projects-and-grants.md:626-646` — §9 restructured to "Decided and unresolved" with the app-`on_behalf_of` decision under **Decided** (rationale: presence, per-capability consent, revocation, threat model, consent-screen control; no contract delta).
- `docs/t2-cells-and-node-principal.md:3` — `011355a` → `fbd6312`.
- `docs/t2-cells-and-node-principal.md:7-8` — v0.90.0 → v0.97.0, `fb34f33` → `fbd6312`, stale "latest tag v0.94.0" wording replaced.
- `docs/t2-resource-registry.md:6` — v0.90.0 → v0.97.0, `fb34f33` → `fbd6312`, same pin wording.
- `docs/contracts-proposal/identity.service-token-claims.v1.json:4` — added `"_note": "shipped in openvibe-contracts v0.89.0"` (JSON valid).
- `docs/capabilities-proposal/network.confirmation.manage.json:4` — added the same `_note` key (JSON valid).
- `docs/developer-projects.md:310` — consent-screen gap note expanded to require listing capability ids and marking `sensitive` ones, naming the consent screen as the app path's compensation for having no `cap_confirm` (decided 2026-10-04).
- `docs/developer-projects.md:46` — stale "Network pins v0.90.0" → "Network's pin moves to v0.97.0".

**Decision (two sentences)**

Apps acting `on_behalf_of` a person with authorization-code tokens do **not** use the confirmation path: the token exists because a person chose the app at the consent screen and is normally driving it when the action happens, whereas an agent is minted by its host and used unattended, so `capability@1`'s `sensitive` rule stays agent-only; what an app may do is already agreed per capability (scope at `/oauth/authorize`, re-intersected at `issueCode` and narrowed again at `mint`), sensitive capabilities are almost all non-grantable, and only `media.object.delete` and `space.post.write` are app-grantable — not carved out as a named subset, since consent plus revocation, the 300 s token and sandbox refusal bound the risk. No contract delta: `cap_confirm` stays agent-only, and the compensating control is the consent screen listing (and marking sensitive) the capability ids, with a request lacking `scope` not consenting to unnamed capabilities.

**Old heads not resolved:** the two historical cutover records still say `v0.90.0` — `docs/cutover-t14-user-owned-trust.md:198` and `docs/cutover-t2-confirmations-budgets.md:193`; I left them untouched because they describe the pin at those past cutove

Changes

ddiff --git a/docs/capabilities-proposal/network.confirmation.manage.json b/docs/capabilities-proposal/network.confirmation.manage.json
iindex 55ee4c18..0a6e1eda 100644
---- a/docs/capabilities-proposal/network.confirmation.manage.json
++++ b/docs/capabilities-proposal/network.confirmation.manage.json
@@@ -1,6 +1,7 @@
{
"id": "network.confirmation.manage",
"version": "1.0",
++ "_note": "shipped in openvibe-contracts v0.89.0",
"owner": "network",
"status": "planned",
"visibility": "first-party",
ddiff --git a/docs/contracts-proposal/identity.service-token-claims.v1.json b/docs/contracts-proposal/identity.service-token-claims.v1.json
iindex 01b15f99..e6dc7ae9 100644
---- a/docs/contracts-proposal/identity.service-token-claims.v1.json
++++ b/docs/contracts-proposal/identity.service-token-claims.v1.json
@@@ -1,6 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
++ "_note": "shipped in openvibe-contracts v0.89.0",
"title": "ServiceTokenClaims",
"description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod, node or agent principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in. Agent tokens (actor_type agent; roadmap WS-Z2, docs/t2-projects-and-grants.md) are minted for an agent by its host and also carry project_id, env and on_behalf_of (the owner), and act (the host).",
"type": "object",
ddiff --git a/docs/developer-projects.md b/docs/developer-projects.md
iindex b70fbcc6..d20c07c5 100644
---- a/docs/developer-projects.md
++++ b/docs/developer-projects.md
@@@ -43,7 +43,7 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which
## Grants, the allowance and the capability catalog
- The only grants that exist are capabilities in the openvibe-contracts catalog, at the version Network has installed.
--- **The grantability rule.** The contracts `visibility` enum is `public | partner | first-party | internal` (`partner` since the v0.83.0 release; Network pins v0.90.0). Only `active` capabilities with visibility `public` or `partner` may be granted to apps. Staff may put a `partner` capability in one project's allowance by hand, but it never comes from the default or the sandbox allowance. `first-party` and `internal` capabilities are never grantable: staff cannot add them to an allowance, members cannot request them, and token issuance filters them out again.
++- **The grantability rule.** The contracts `visibility` enum is `public | partner | first-party | internal` (`partner` since the v0.83.0 release; Network's pin moves to v0.97.0). Only `active` capabilities with visibility `public` or `partner` may be granted to apps. Staff may put a `partner` capability in one project's allowance by hand, but it never comes from the default or the sandbox allowance. `first-party` and `internal` capabilities are never grantable: staff cannot add them to an allowance, members cannot request them, and token issuance filters them out again.
- **The allowance** is the set of grantable capabilities one project's apps may hold. Only staff set it. `DEV_DEFAULT_ALLOWANCE` seeds new projects, with public capabilities only. It is empty by default, so staff decide each project's **production** apps.
- **The sandbox allowance** is added to the allowance for **sandbox apps only**, in every project, without a staff decision. It comes from `DEV_SANDBOX_ALLOWANCE`. When that is unset, the code default is `media.object.upload`, `media.object.read`, `media.object.list`, `media.object.delete`, `events.app.publish`, `events.app.read`, `events.app.subscribe`, `tools.job.create`, `tools.job.read` and `tools.job.cancel`. Only `public` + `active` capabilities of the installed openvibe-contracts catalog count. `partner`, `first-party`, `internal` and unknown ids are dropped. The three `events.app.*` ids are defined from openvibe-contracts v0.28.0 (Network pins v0.30.1); with an older release they would be left out. `media.object.list` and `media.object.delete` (Media's list and delete verbs, WS-G task 2) are left out the same way until the pinned release defines them; until then a sandbox app's upload and read grants cover them at Media. The project view and `GET /catalog` show it as `sandbox_allowance`. Production apps never use it.
- **A grant never exceeds the allowance** (for a sandbox app: allowance ∪ sandbox allowance). It is checked in three places. Approval refuses with `403 grant.beyond_allowance`. Shrinking the allowance revokes approved grants, and denies requested ones, that fall outside it. A sandbox app's grant that is still inside the sandbox allowance is kept. Token issuance intersects approved grants with the current allowance and with the current grantability.
@@@ -307,7 +307,7 @@ Until the relay is turned on in an environment, nobody else receives these event
## Known gaps (next steps)
--- **Consent screen.** The account chooser shows the app's name, not the capabilities it asks for. Codes and Network need a consent step before third-party production apps go live.
++- **Consent screen.** The account chooser shows the app's name, not the capabilities it asks for. Codes and Network need a consent step before third-party production apps go live, and it must list the capability ids the app asks for, marking the ones `capability@1` marks `sensitive`; a request without a `scope` must not consent to capabilities the screen did not name. This screen is the app path's compensation for not using the agent confirmation flow — an authorization-code app acts for a person who is present and authorizes it capability by capability, so it carries no `cap_confirm` (decided 2026-10-04, `docs/t2-projects-and-grants.md` section 9).
- **Service-side project reads.** The route is built, but no service calls it yet. Host holds the grant. Contracts still has to make `network.project.read` `active`.
- **PowerChat as the first project** (ADR-014 migration). This is not done. PowerChat's existing OAuth client is unchanged.
- **Receivers.** Every default sandbox audience must refuse `env: sandbox` except on routes that keep sandbox data apart, and must key tenancy by `project_id`. Media and Events do this in Wave 20. Tools does not yet (see [Sandbox tokens](#sandbox-tokens)).
ddiff --git a/docs/t2-cells-and-node-principal.md b/docs/t2-cells-and-node-principal.md
iindex 0429650e..15973d6b 100644
---- a/docs/t2-cells-and-node-principal.md
++++ b/docs/t2-cells-and-node-principal.md
@@@ -1,11 +1,11 @@
# T2 lane B — cells, the node principal and registry slice 5 (design)
--Status: **N1–N7 merged** on `origin/main` `011355a` (registry slice 5, the node principal, node tokens and the registry
++Status: **N1–N7 merged** on `origin/main` `fbd6312` (registry slice 5, the node principal, node tokens and the registry
operator writers; each slice's state is in §8). The cells layer of `docs/t2-resource-registry.md` §9 was already on
`origin/main` (`d6ed8c4`, PR #5: `migrations/0008_cells_and_node_principals.sql`, `server/registry/cells.js`,
`test/cells-registry.test.js`); this document designed the rest of lane B on top of it and re-planned registry slice 5.
--Pinned: Network `openvibe-contracts` **v0.90.0** (`package.json:33`, `origin/main` `fb34f33`; the latest Contracts tag,
--v0.94.0, is additive and not pinned yet). v0.85.0 closed every contract gap in §2 and the §9.1 T1 brief (the doc was
++Pinned: Network `openvibe-contracts` **v0.97.0** (`package.json:33`, `origin/main` `fbd6312`; the latest Contracts
++release, to which Network's pin moves in a separate PR). v0.85.0 closed every contract gap in §2 and the §9.1 T1 brief (the doc was
written against v0.83.0, checked then against v0.84.0). OpenVibe.Bot
`origin/main` `17ce069` (pins Contracts v0.79.0, `package.json:29`), OpenVibe.Node `origin/main` `4d2b8e1`. Bot is not
deployed (`ov access run openvibe-ovh releases bot` → `unknown service "bot"`), so no production device exists.
ddiff --git a/docs/t2-projects-and-grants.md b/docs/t2-projects-and-grants.md
iindex 1cab608c..4be3c61a 100644
---- a/docs/t2-projects-and-grants.md
++++ b/docs/t2-projects-and-grants.md
@@@ -5,16 +5,17 @@ merge `4ec36e1`) and **lane B step 3 built** (`GET /internal/projects/:project_i
only: `server/internal/routes.js:490`; PR #28, merge `e422342`) and **slice 3 built** (delegated grants and modes:
`migrations/0017_agent_grants.sql`, the ceiling in `server/developer/agents.js`), **slices 4–5 built** (confirmations,
owner side, and budgets: `0018`, `0019`) and **slices 6–7 built** (`GET /internal/agents/:agent` and the four
--`/internal/confirmations` routes, `server/internal/routes.js`, with the pin at `openvibe-contracts` v0.90.0) and **slices
++`/internal/confirmations` routes, `server/internal/routes.js`, with the pin at `openvibe-contracts` v0.97.0) and **slices
8–9 built** (agent tokens, `server/developer/agent-tokens.js`; `network.confirmation.changed@1` decision events from
--`server/developer/confirmations.js`); the owner notification of slice 9 still waits on an openvibe-shared type (§8).
++`server/developer/confirmations.js`; and the owner notification, `confirmations.notifyOwner` sending `openvibe-shared`
++`TYPES.CONFIRMATION_REQUESTED`, PR #47, merge `fbd6312`).
Plan T2, "Projects and grants: projects as the ownership boundary for every resource;
`agt_` principals, delegated grants with modes, sensitive capabilities, confirmation requests, budgets (WS-Z2)".
--Pinned version: `openvibe-contracts` **v0.90.0** (`package.json:33`, since slice 7; still the pin at `origin/main` `fb34f33`,
--the latest Contracts tag v0.94.0 is additive and not pinned yet); every contract claim below was
++Pinned version: `openvibe-contracts` **v0.97.0** (`package.json:33`; the latest Contracts release, to which Network's pin
++moves in a separate PR; `origin/main` `fbd6312`); every contract claim below was
re-checked against the `v0.85.0` tag of OpenVibe.Contracts when slices 1–2 were built (it was written against v0.83.0).
v0.89.0 published `network.confirmation.manage` (`planned`, `implementedBy` the four `/internal/confirmations` routes),
--the `agent` actor in `identity.service-token-claims@1` and `network.confirmation.changed@1`; v0.86–v0.90 are additive.
++the `agent` actor in `identity.service-token-claims@1` and `network.confirmation.changed@1`; v0.86–v0.97 are additive.
Scope: an **agent** is a principal (`agt_<ULID>`) that acts for one person, inside one developer project, run by one
host (a developer app or a first-party service such as OpenVibe.Actor). Its owner **delegates** part of the host's
@@@ -58,7 +59,7 @@ allowances changes.
("`partner` is not in the enum yet") and the comment at `policy.js:8` predate that; slice 1 corrects both. The rule
itself does not change.
--**Gaps — not in v0.85.0, so the slices that need them are blocked (section 8):**
++**Gaps — not in v0.85.0, so the slices that needed them were blocked at that tag (section 8):**
1. `identity.service-token-claims@1`: `sub` allows only `svc:` / `app:` / `mod:` / `node:` and `actor_type` only
`service | app | mod | node`. An agent token cannot validate. (v0.85.0 closed the same gap for the node actor of
@@@ -516,7 +517,8 @@ the capability's audience. Owning services read budgets from `GET /internal/agen
- **The grantability rule and the allowances.** Not reimplemented: the ceiling *is* `effectiveGrants` for app hosts. The
one new rule (service-hosted agents never get `internal`) only narrows.
- **Sensitive app grants.** `media.object.delete` and `space.post.write` keep working for apps exactly as today:
-- `capability@1` defines confirmation for **agents**. Apps acting with `on_behalf_of` are listed as unresolved.
++ `capability@1` defines confirmation for **agents**, and apps acting with `on_behalf_of` are decided not to use it
++ (section 9, Decided): per-capability consent, the consent screen and revocation cover the app path.
- **Mod principals, node principals, export tokens.** Untouched.
- **Account deletion (`account-data.js:319` `erase`)** revokes the subject's agents (`revoked_by = 'account_deleted'`,
then `'project_archived'` for the rest of a project it archives; built, slice 2), cancels their pending and approved-unused
@@@ -603,7 +605,7 @@ and agree).
6. **Internal agent read** (built). `GET /internal/agents/:agent` under `network.project.read` (already in the catalog),
`internalAgentView` in `server/developer/agents.js`, the route in `server/internal/routes.js`;
`test/internal-agents.test.js`. Checks: `npm test`, `npm run test:pg`.
--7. **Internal confirmations** (built). The pin moved to `openvibe-contracts` v0.90.0 (v0.89.0 published
++7. **Internal confirmations** (built). The pin moved to the Contracts release then current (v0.89.0 published
`network.confirmation.manage`), the four `/internal/confirmations` routes (`internalRouter`, `read` and `cancel` in
`server/developer/confirmations.js`, mounted under the guard in `server/internal/routes.js`), no `DEFAULT_GRANTS`
row yet (none until a receiver ships), internal cases in `test/confirmations.test.js`. Checks: `npm test`,
@@@ -612,20 +614,43 @@ and agree).
`server/developer/tokens.js` and `principals.issueToken`, `cap`/`cap_confirm` split, `test/agent-tokens.test.js`.
Network's own guard refuses agent tokens on its routes until a route needs them. No migration. Checks: `npm test`,
`npm run test:pg`.
--9. **Decision events** (built) **and the owner notification** (*still blocked on an openvibe-shared notification
-- type*). Audit rows carry `network.confirmation.changed@1` (`store.audit` takes the envelope's `actor` and
-- `on_behalf_of`), `test/confirmation-events.test.js`. Still to do: `notification-service.js` notifies the owner on
-- create (payload rules of `docs/notification-digest.md`: never the summary). No migration. Checks: `npm test`,
-- `npm run test:pg`.
--
--Slices 2–9 are built, in this order (2–5 each added the next migration number; 6–9 need none); only the slice-9 owner
--notification waits on a release outside Network.
--
--## 9. Unresolved
++9. **Decision events and the owner notification** (built). Audit rows carry `network.confirmation.changed@1`
++ (`store.audit` takes the envelope's `actor` and `on_behalf_of`), `test/confirmation-events.test.js`; on create
++ `confirmations.notifyOwner` notifies the owner through `notification-service.js` with `openvibe-shared`
++ `TYPES.CONFIRMATION_REQUESTED` (payload rules of `docs/notification-digest.md`: never the summary), wired in
++ `server/index.js:621`; PR #47, merge `fbd6312`. No migration. Checks: `npm test`, `npm run test:pg`.
++
++Slices 2–9 are built, in this order (2–5 each added the next migration number; 6–9 need none); the slice-9 owner
++notification shipped with `openvibe-shared` v2.7.0 (`TYPES.CONFIRMATION_REQUESTED`, PR #47).
++
++## 9. Decided and unresolved
++
++### Decided
++
++- **Apps acting `on_behalf_of` a person do not use the confirmation path** (decided 2026-10-04 by the build plan,
++ revisable by the owner). An authorization-code token is issued to a person who is at the consent screen and, in the
++ interactive flow, driving the app when the action happens; an agent token is minted by its host (section 4, "Agent
++ tokens") and used unattended, which is why `capability@1`'s `sensitive` rule — "an agent needs its owner's
++ confirmation unless a standing rule covers it" — stays specific to agents. What an app may do is already agreed
++ capability by capability: `/oauth/authorize` takes `scope` (capability ids), `tokens.issueCode` keeps the grantable
++ subset, and `tokens.mint` intersects it with the app's grants again and can only narrow it, never widen it. Sensitive
++ capabilities are almost all `first-party` or `internal` and never grantable to apps; today exactly two are
++ app-grantable (public, active), `media.object.delete` and `space.post.write`, and they are not carved out as a named
++ subset either: the person authorizes each at consent, and the residual risk is bounded by revocation (grant revoke,
++ app revoke, `setAllowance` shrink, project archive), the 300-second token, and a receiver's refusal of `env: sandbox`.
++ The agent path defends an autonomous, prompt-injectable principal whose future actions the owner cannot enumerate when
++ delegating; an app runs the developer's own code and is authorized per capability. The compensating control on the app
++ side is therefore the consent screen, not an inbox: it must list the capability ids the app asks for and mark the
++ sensitive ones, and a request without a `scope` must not consent to capabilities the screen did not name
++ (`docs/developer-projects.md`, "Consent screen"). An app holding a refresh token (`/oauth/token`
++ `grant_type=refresh_token`) can still act after the person has left it; that is the case the per-capability consent
++ and revocation exist for, and if it proves too loose for the two app-grantable sensitive capabilities the narrower fix
++ is to leave sensitive capabilities out of refreshed tokens (the person consents again), not the agent inbox. No
++ contract delta: `cap_confirm` and the confirmation flow stay
++ agent-only in `identity.service-token-claims@1`, and Network builds nothing for apps here.
++
++### Unresolved
--- **Apps acting `on_behalf_of` a person** (authorization-code tokens) are not agents, and `capability@1` asks confirmation
-- of agents. Whether a third-party app's sensitive actions should also be confirmed is a separate decision (the consent
-- screen gap in `docs/developer-projects.md`).
- **Per-agent usage display.** Owners cannot see an agent's `used` until `common.usage-recorded@1` gains an agent
dimension; whether an agent id may appear in a rollup is a privacy decision for Contracts.
- **Money budgets across services** belong to Billing (T5); Network records nothing for them yet.
ddiff --git a/docs/t2-resource-registry.md b/docs/t2-resource-registry.md
iindex bb2cf467..1441fe09 100644
---- a/docs/t2-resource-registry.md
++++ b/docs/t2-resource-registry.md
@@@ -3,7 +3,7 @@
Status: **resource registry and N6 built** (`migrations/0007_resource_registry.sql`,
`migrations/0009_resource_offer_kinds.sql`, `server/registry/offers.js`); **cells and node principals built**
(section 9, `migrations/0008`). The original slice plan in §7 is historical.
--Current pin: `openvibe-contracts` **v0.90.0** (`package.json` at `origin/main` `fb34f33`; the latest tag, v0.94.0, is additive and not pinned yet; the kinds below are as of v0.85.0); the v0.83.0 observations below describe the original design.
++Current pin: `openvibe-contracts` **v0.97.0** (`package.json` at `origin/main` `fbd6312`; the latest release, to which Network's pin moves in a separate PR; the kinds below are as of v0.85.0); the v0.83.0 observations below describe the original design.
Scope: one table and one module that store **what the network can sell or place on** — node, provider, storage, delivery,
runtime, agent and harness offers — reported by the components that own them, listed publicly so that `openvibe-sdk/placement` can plan against