openvibers/openvibe.contracts Commits

Contracts 0.176.0: person-owned resources can be controlled (common.owned-resource-control-request@1; bot, chat, openre, community resource.control) (#134)

OP OpenVibers committed 29 days ago · 6b8eea4047 · 59 files, +1398 −88

**Person-owned resources can be controlled from OpenVibe.Services** (ADR-048 amendment 2026-10-15, step 8 phase 2, the control half). Robots, chat rooms, restream definitions and pastes are in the index as a person's own resources, but they had no actions: the control request is project-scoped and names resources by OVRN, which person-owned resources don't have.

**Changes:**
- **`common.owned-resource-control-request@1`** (new, in the catalog):
  - `owned { kind, id }`: the index summary's kind and id (a prefixed ULID or the service's slug);
  - `owner`: a `usr_` subject;
  - `action`: delete, stop, suspend, resume or archive;
  - `idempotency_key`, plus optional `on_behalf_of`, `params`, `confirmation_id`, `dry_run` and `trace_id`;
  - no `project_id`.

  It goes to the same `POST /api/v1/resources/control`, told apart by `owned`.
- **`contracts.resources.checkControlRequest`** takes either form and routes on `owned`. A person acting for someone else is refused. `checkOwnedControlRequest` is exported.
- **Capabilities** (active, first-party, sensitive), each listed in its service's manifest:
  - `bot.resource.control`: stop (e-stop) and resume (clear);
  - `chat.resource.control`: archive;
  - `openre.resource.control`: suspend and resume (definition disabled or active);
  - `community.resource.control`: delete a paste.
- **ADR-048:** a "Person-owned resources" section covering naming, who decides (the authority checks the owner it holds), gates, answers and actions.
- **Fixtures:** valid `bot-robot-stop` and `community-paste-delete-dry-run`; invalid `with-project`, `agent-owner` and `create`.
- **`test/bot-manifest.test.js`** mirrors Bot's STATUS.json with `bot.resource.control`; the Bot PR adds it there.

**Checks:** `npm test` passes all 20,043 checks. `compat.js v0.175.0` finds no breaking changes.

**Next:** each authority implements it (Bot, Chat, OpenRestream, Community); Network grants Services the four capabilities; Services offers the actions on "Your resources".

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 306a05af..1d804849 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,23 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.176.0 — 2026-10-15
++
++- **Person-owned resources can be controlled** (ADR-048 amendment, step 8 phase 2, the control half).
++ `common.owned-resource-control-request@1` names a robot, room, restream definition or paste by its index kind and id
++ and its owner (a person). It goes to the same `POST /api/v1/resources/control`.
++ `contracts.resources.checkControlRequest` takes either form (`checkOwnedControlRequest` for this one) and refuses a
++ person acting on someone else's resource.
++- **Four control capabilities, active:**
++ - `bot.resource.control`: stop and resume a robot's e-stop;
++ - `chat.resource.control`: archive a room;
++ - `openre.resource.control`: suspend and resume a restream definition;
++ - `community.resource.control`: delete a paste.
++
++ Each is listed in its service's manifest.
++- **Fixtures:** valid `bot-robot-stop` and `community-paste-delete-dry-run`; invalid `with-project`, `agent-owner` and
++ `create`.
++
## 0.175.0 — 2026-10-15
- **`media.usage.recorded`** (ADR-031 section 8): Media's per-zone hourly rollups (`common.usage-recorded@1`). Each one
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 6713d1f8..eae7a28e 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -6445,6 +6445,16 @@
"status": "active",
"adr": "ADR-048"
},
++ {
++ "id": "common.owned-resource-control-request",
++ "version": "1.0.0",
++ "owner": "contracts",
++ "schema": "common/owned-resource-control-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-048"
++ },
{
"id": "common.resource-control-result",
"version": "1.0.0",
ddiff --git a/contracts/common/owned-resource-control-request.v1.json b/contracts/common/owned-resource-control-request.v1.json
nnew file mode 100644
iindex 00000000..88579046
---- /dev/null
++++ b/contracts/common/owned-resource-control-request.v1.json
@@@ -0,0 +1,93 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/common/owned-resource-control-request.v1.json",
++ "title": "OwnedResourceControlRequest",
++ "description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "owned",
++ "owner",
++ "idempotency_key"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "delete",
++ "stop",
++ "suspend",
++ "resume",
++ "archive"
++ ]
++ },
++ "owned": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "id"
++ ],
++ "description": "The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).",
++ "properties": {
++ "kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
++ "description": "The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room)."
++ }
++ }
++ },
++ "owner": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "id"
++ ],
++ "description": "The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.",
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "Who asks: the owner, or their agent (whose sensitive actions wait for the owner's approval)."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++}
ddiff --git a/docs/adr/ADR-048-services-control-plane.md b/docs/adr/ADR-048-services-control-plane.md
iindex fb62ad34..10fd61f6 100644
---- a/docs/adr/ADR-048-services-control-plane.md
++++ b/docs/adr/ADR-048-services-control-plane.md
@@@ -6,7 +6,8 @@
id prefixes Events and Codes still owe, plus the prefixes not yet in `lib/ids.js`. Amended 2026-10-07 (step 8, the
Contracts half): only Network lists projects, Media lists objects alone, Events waits for a stored queue row, Codes
hosts no repositories, the person-owned kinds are step 8 phase 2, and the chosen prefixes joined `lib/ids.js`. No
--contract schema changes.
++contract schema changes. Amended 2026-10-15 (step 8 phase 2, the control half): person-owned resources are controlled
++through `common.owned-resource-control-request@1` (see "Person-owned resources" below).
Builds on ADR-034 §2 (one resource name) and §5 (control plane
and data plane) and on ADR-046 §6 (the data plane keeps running without the control plane). Contracts:
`common.resource-name@1`, `common.resource-control-request@1`, `common.resource-control-result@1`; helpers in
@@@ -131,6 +132,27 @@ every missing prefix, resolves the `run_` collision and adds them to `lib/ids.js
`confirmation_required` appears only on `refused`, and `done` and `pending` carry no problem.
`contracts.resources.checkControlResult` enforces these rules.
++## Person-owned resources (amendment 2026-10-15)
++
++Robots (Bot), chat rooms (Chat), restream definitions (OpenRestream) and pastes (Community) belong to a person. They
++are in no project and have no OVRN, so the project-scoped request cannot name them. They are controlled with
++`common.owned-resource-control-request@1`, sent to the same `POST /api/v1/resources/control` route.
++
++- **Naming:** a request names the resource by its index summary's `kind` and `id` (`owned`) and its `owner` (a
++ `usr_` subject). It carries no `project_id`. The authority tells the two forms apart by `owned`.
++- **Who decides:** the authority refuses unless its own record has the same owner. Services offers the actions only
++ on the viewer's own resources ("Your resources"), and sends `owner` and `on_behalf_of` as the viewer.
++ `contracts.resources.checkControlRequest` checks either form and refuses a person acting on someone else's resource.
++- **Gates:** an agent's sensitive actions (deleting, physical control such as a robot's stop) wait for the owner's
++ approval, as with project resources. Idempotency is per (caller, key).
++- **Answers:** `common.resource-control-result@1` without `resource`.
++- **Actions per authority**, as each `<service>.resource.control` capability lists them; any other is refused
++ `unsupported`:
++ - `bot.robot`: stop (the remote e-stop) and resume (clear it);
++ - `chat.room`: archive;
++ - `openre.stream`: suspend and resume (the definition disabled or active);
++ - `community.paste`: delete.
++
## Out of scope
- The console surfaces, the Services repository and its deploy (T13 steps 3, 9, 15).
ddiff --git a/fixtures/common.owned-resource-control-request/invalid/agent-owner.json b/fixtures/common.owned-resource-control-request/invalid/agent-owner.json
nnew file mode 100644
iindex 00000000..349e203f
---- /dev/null
++++ b/fixtures/common.owned-resource-control-request/invalid/agent-owner.json
@@@ -0,0 +1,12 @@
++{
++ "action": "stop",
++ "owned": {
++ "kind": "bot.robot",
++ "id": "rob_01JAB2C3D4E5F6G7H8J9K0MNPR"
++ },
++ "owner": {
++ "type": "agent",
++ "id": "agt_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "idempotency_key": "idem_stop_robot_2"
++}
ddiff --git a/fixtures/common.owned-resource-control-request/invalid/create.json b/fixtures/common.owned-resource-control-request/invalid/create.json
nnew file mode 100644
iindex 00000000..d8a79ea3
---- /dev/null
++++ b/fixtures/common.owned-resource-control-request/invalid/create.json
@@@ -0,0 +1,12 @@
++{
++ "action": "create",
++ "owned": {
++ "kind": "chat.room",
++ "id": "new"
++ },
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "idempotency_key": "idem_create_room"
++}
ddiff --git a/fixtures/common.owned-resource-control-request/invalid/with-project.json b/fixtures/common.owned-resource-control-request/invalid/with-project.json
nnew file mode 100644
iindex 00000000..c5cc9f96
---- /dev/null
++++ b/fixtures/common.owned-resource-control-request/invalid/with-project.json
@@@ -0,0 +1,13 @@
++{
++ "action": "archive",
++ "owned": {
++ "kind": "chat.room",
++ "id": "lobby"
++ },
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "idempotency_key": "idem_archive_1"
++}
ddiff --git a/fixtures/common.owned-resource-control-request/valid/bot-robot-stop.json b/fixtures/common.owned-resource-control-request/valid/bot-robot-stop.json
nnew file mode 100644
iindex 00000000..1b881de6
---- /dev/null
++++ b/fixtures/common.owned-resource-control-request/valid/bot-robot-stop.json
@@@ -0,0 +1,16 @@
++{
++ "action": "stop",
++ "owned": {
++ "kind": "bot.robot",
++ "id": "rob_01JAB2C3D4E5F6G7H8J9K0MNPR"
++ },
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "on_behalf_of": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "idempotency_key": "idem_stop_robot_1"
++}
ddiff --git a/fixtures/common.owned-resource-control-request/valid/community-paste-delete-dry-run.json b/fixtures/common.owned-resource-control-request/valid/community-paste-delete-dry-run.json
nnew file mode 100644
iindex 00000000..ba81f7f1
---- /dev/null
++++ b/fixtures/common.owned-resource-control-request/valid/community-paste-delete-dry-run.json
@@@ -0,0 +1,14 @@
++{
++ "action": "delete",
++ "owned": {
++ "kind": "community.paste",
++ "id": "my-notes-2026"
++ },
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "idempotency_key": "idem_delete_paste_1",
++ "dry_run": true,
++ "trace_id": "0af7651916cd43dd8448eb211c80319c"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 51ea9041..2173136a 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -74823,6 +74823,104 @@
}
}
},
++ "common.owned-resource-control-request": {
++ "version": "1.0.0",
++ "owner": "contracts",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/common/owned-resource-control-request.v1.json",
++ "title": "OwnedResourceControlRequest",
++ "description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "owned",
++ "owner",
++ "idempotency_key"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "delete",
++ "stop",
++ "suspend",
++ "resume",
++ "archive"
++ ]
++ },
++ "owned": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "id"
++ ],
++ "description": "The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).",
++ "properties": {
++ "kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
++ "description": "The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room)."
++ }
++ }
++ },
++ "owner": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "id"
++ ],
++ "description": "The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.",
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "Who asks: the owner, or their agent (whose sensitive actions wait for the owner's approval)."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++ }
++ },
"common.resource-control-result": {
"version": "1.0.0",
"owner": "contracts",
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex c8fbc119..08ab7513 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 618fd90f..a5867ed4 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 556a9e78..a44da2ec 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex bf2a2a10..15a8d2ba 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 4de68ded..269ac409 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -574,6 +574,62 @@
}
}
},
++ "/api/v1/resources/control": {
++ "post": {
++ "operationId": "post_api_v1_resources_control",
++ "summary": "Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a ",
++ "description": "**bot.resource.control** (first-party): Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a bot.robot (id rob_<ULID>) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: stop (latch the robot's remote e-stop, as POST /api/v1/robots/:id/estop: every actuator stops and nothing moves until it is cleared) and resume (clear that e-stop, as POST /api/v1/robots/:id/estop/clear; a local stop on the robot itself still holds); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "tags": [
++ "bot.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.resource.control"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.resource.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.owned-resource-control-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/robots": {
"get": {
"operationId": "get_api_v1_robots",
@@@ -2365,6 +2421,153 @@
}
}
},
++ "common.owned-resource-control-request.v1": {
++ "title": "OwnedResourceControlRequest",
++ "description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "owned",
++ "owner",
++ "idempotency_key"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "delete",
++ "stop",
++ "suspend",
++ "resume",
++ "archive"
++ ]
++ },
++ "owned": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "id"
++ ],
++ "description": "The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).",
++ "properties": {
++ "kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
++ "description": "The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room)."
++ }
++ }
++ },
++ "owner": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "id"
++ ],
++ "description": "The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.",
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "Who asks: the owner, or their agent (whose sensitive actions wait for the owner's approval)."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++ },
++ "common.resource-control-result.v1": {
++ "title": "ResourceControlResult",
++ "description": "common.resource-control-result@1: the authority's answer to a common.resource-control-request@1 (ADR-048, plan T13). state is the outcome, not the resource's own state: done (applied), pending (accepted and asynchronous; the authority reports completion later on its events), refused (a policy or confirmation gate said no) or failed (an error). State rules: refused carries problem or confirmation_required; failed carries problem; done and pending carry neither; confirmation_required appears only on refused. result carries the authority's own response document and never another service's rows. The state rules are checked by contracts.resources.checkControlResult.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "state",
++ "at"
++ ],
++ "properties": {
++ "action": {
++ "type": "string"
++ },
++ "resource": {
++ "$ref": "#/components/schemas/common.resource-name.v1"
++ },
++ "state": {
++ "enum": [
++ "done",
++ "pending",
++ "refused",
++ "failed"
++ ]
++ },
++ "result": {
++ "type": "object"
++ },
++ "confirmation_required": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "confirmation_id",
++ "reason"
++ ],
++ "properties": {
++ "confirmation_id": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "reason": {
++ "type": "string"
++ }
++ },
++ "description": "The action needs its owner's approval; retry the same request with this confirmation_id once approved."
++ },
++ "problem": {
++ "$ref": "#/components/schemas/errors.problem.v1",
++ "description": "RFC 9457 body when refused or failed."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
"common.resource-list-result.v1": {
"title": "ResourceListResult",
"description": "A page of the resource index (roadmap WS-Z7).",
@@@ -2411,6 +2614,16 @@
}
}
},
++ "common.resource-name.v1": {
++ "title": "ResourceName",
++ "description": "common.resource-name@1: the one stable, cross-service name of an OpenVibe resource (OVRN; ADR-034 section 2, ADR-048, plan T13): ovrn:<service>:<project_id>:<type>/<id>, for example ovrn:media:prj_01J0000000000000000000000A:object/med_01K0000000000000000000000B. The service segment is the authority that owns the resource; the project segment is its tenancy boundary; type is the second half of a common.resource-summary@1 kind and id is that summary's id, so the name composes from any resource summary. It is used in grant scopes, audit rows, event subjects, usage records, bill lines and console URLs. It never names a person, an app, a request, a session or an event. The pattern is the one common.usage-recorded@1 resource and common.resource-summary@1 ovrn already use.",
++ "type": "string",
++ "maxLength": 200,
++ "pattern": "^ovrn:[a-z][a-z0-9-]{0,31}:prj_[0-9A-HJKMNP-TV-Z]{26}:[a-z][a-z0-9-]{0,39}/[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$",
++ "not": {
++ "pattern": "/(usr|gst|app|mod|svc|job|evt|req)_"
++ }
++ },
"common.resource-summary.v1": {
"title": "ResourceSummary",
"description": "One resource of any service, as the resource index lists it (roadmap WS-Z7): every service answers GET /api/v1/resources with these, so OpenVibe.Services shows any resource without owning its data.",
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 0000f7c8..3fe7afd6 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -118,6 +118,62 @@
}
}
},
++ "/api/v1/resources/control": {
++ "post": {
++ "operationId": "post_api_v1_resources_control",
++ "summary": "Control a person's rooms through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a c",
++ "description": "**chat.resource.control** (first-party): Control a person's rooms through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a chat.room (id the room's slug) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: archive (the room leaves every listing and takes no new messages; its history is kept for its members, as an archived room); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "tags": [
++ "chat.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "chat.resource.control"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "chat.resource.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.owned-resource-control-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/chat/dm/block-state": {
"get": {
"operationId": "get_internal_chat_dm_block_state",
@@@ -4845,6 +4901,153 @@
}
]
},
++ "common.owned-resource-control-request.v1": {
++ "title": "OwnedResourceControlRequest",
++ "description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "owned",
++ "owner",
++ "idempotency_key"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "delete",
++ "stop",
++ "suspend",
++ "resume",
++ "archive"
++ ]
++ },
++ "owned": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "id"
++ ],
++ "description": "The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).",
++ "properties": {
++ "kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
++ "description": "The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room)."
++ }
++ }
++ },
++ "owner": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "id"
++ ],
++ "description": "The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.",
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "Who asks: the owner, or their agent (whose sensitive actions wait for the owner's approval)."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++ },
++ "common.resource-control-result.v1": {
++ "title": "ResourceControlResult",
++ "description": "common.resource-control-result@1: the authority's answer to a common.resource-control-request@1 (ADR-048, plan T13). state is the outcome, not the resource's own state: done (applied), pending (accepted and asynchronous; the authority reports completion later on its events), refused (a policy or confirmation gate said no) or failed (an error). State rules: refused carries problem or confirmation_required; failed carries problem; done and pending carry neither; confirmation_required appears only on refused. result carries the authority's own response document and never another service's rows. The state rules are checked by contracts.resources.checkControlResult.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "state",
++ "at"
++ ],
++ "properties": {
++ "action": {
++ "type": "string"
++ },
++ "resource": {
++ "$ref": "#/components/schemas/common.resource-name.v1"
++ },
++ "state": {
++ "enum": [
++ "done",
++ "pending",
++ "refused",
++ "failed"
++ ]
++ },
++ "result": {
++ "type": "object"
++ },
++ "confirmation_required": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "confirmation_id",
++ "reason"
++ ],
++ "properties": {
++ "confirmation_id": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "reason": {
++ "type": "string"
++ }
++ },
++ "description": "The action needs its owner's approval; retry the same request with this confirmation_id once approved."
++ },
++ "problem": {
++ "$ref": "#/components/schemas/errors.problem.v1",
++ "description": "RFC 9457 body when refused or failed."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
"common.resource-list-result.v1": {
"title": "ResourceListResult",
"description": "A page of the resource index (roadmap WS-Z7).",
@@@ -4891,6 +5094,16 @@
}
}
},
++ "common.resource-name.v1": {
++ "title": "ResourceName",
++ "description": "common.resource-name@1: the one stable, cross-service name of an OpenVibe resource (OVRN; ADR-034 section 2, ADR-048, plan T13): ovrn:<service>:<project_id>:<type>/<id>, for example ovrn:media:prj_01J0000000000000000000000A:object/med_01K0000000000000000000000B. The service segment is the authority that owns the resource; the project segment is its tenancy boundary; type is the second half of a common.resource-summary@1 kind and id is that summary's id, so the name composes from any resource summary. It is used in grant scopes, audit rows, event subjects, usage records, bill lines and console URLs. It never names a person, an app, a request, a session or an event. The pattern is the one common.usage-recorded@1 resource and common.resource-summary@1 ovrn already use.",
++ "type": "string",
++ "maxLength": 200,
++ "pattern": "^ovrn:[a-z][a-z0-9-]{0,31}:prj_[0-9A-HJKMNP-TV-Z]{26}:[a-z][a-z0-9-]{0,39}/[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$",
++ "not": {
++ "pattern": "/(usr|gst|app|mod|svc|job|evt|req)_"
++ }
++ },
"common.resource-summary.v1": {
"title": "ResourceSummary",
"description": "One resource of any service, as the resource index lists it (roadmap WS-Z7): every service answers GET /api/v1/resources with these, so OpenVibe.Services shows any resource without owning its data.",
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 7f3862c2..dbfc18e0 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 9f9d2431..07d33c11 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -1454,6 +1454,62 @@
}
}
},
++ "/api/v1/resources/control": {
++ "post": {
++ "operationId": "post_api_v1_resources_control",
++ "summary": "Control a person's pastes through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a ",
++ "description": "**community.resource.control** (first-party): Control a person's pastes through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a community.paste (id the paste's slug) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: delete (the paste goes through Community's own deletion path, as the owner deleting it on openvibe.community); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "tags": [
++ "community.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "community.resource.control"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "community.resource.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.owned-resource-control-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/spaces": {
"get": {
"operationId": "get_api_v1_spaces",
@@@ -2456,6 +2512,153 @@
"type": "object",
"maxProperties": 0
},
++ "common.owned-resource-control-request.v1": {
++ "title": "OwnedResourceControlRequest",
++ "description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "owned",
++ "owner",
++ "idempotency_key"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "delete",
++ "stop",
++ "suspend",
++ "resume",
++ "archive"
++ ]
++ },
++ "owned": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "id"
++ ],
++ "description": "The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).",
++ "properties": {
++ "kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
++ "description": "The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room)."
++ }
++ }
++ },
++ "owner": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "id"
++ ],
++ "description": "The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.",
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "Who asks: the owner, or their agent (whose sensitive actions wait for the owner's approval)."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++ },
++ "common.resource-control-result.v1": {
++ "title": "ResourceControlResult",
++ "description": "common.resource-control-result@1: the authority's answer to a common.resource-control-request@1 (ADR-048, plan T13). state is the outcome, not the resource's own state: done (applied), pending (accepted and asynchronous; the authority reports completion later on its events), refused (a policy or confirmation gate said no) or failed (an error). State rules: refused carries problem or confirmation_required; failed carries problem; done and pending carry neither; confirmation_required appears only on refused. result carries the authority's own response document and never another service's rows. The state rules are checked by contracts.resources.checkControlResult.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "state",
++ "at"
++ ],
++ "properties": {
++ "action": {
++ "type": "string"
++ },
++ "resource": {
++ "$ref": "#/components/schemas/common.resource-name.v1"
++ },
++ "state": {
++ "enum": [
++ "done",
++ "pending",
++ "refused",
++ "failed"
++ ]
++ },
++ "result": {
++ "type": "object"
++ },
++ "confirmation_required": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "confirmation_id",
++ "reason"
++ ],
++ "properties": {
++ "confirmation_id": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "reason": {
++ "type": "string"
++ }
++ },
++ "description": "The action needs its owner's approval; retry the same request with this confirmation_id once approved."
++ },
++ "problem": {
++ "$ref": "#/components/schemas/errors.problem.v1",
++ "description": "RFC 9457 body when refused or failed."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
"common.resource-list-result.v1": {
"title": "ResourceListResult",
"description": "A page of the resource index (roadmap WS-Z7).",
@@@ -2502,6 +2705,16 @@
}
}
},
++ "common.resource-name.v1": {
++ "title": "ResourceName",
++ "description": "common.resource-name@1: the one stable, cross-service name of an OpenVibe resource (OVRN; ADR-034 section 2, ADR-048, plan T13): ovrn:<service>:<project_id>:<type>/<id>, for example ovrn:media:prj_01J0000000000000000000000A:object/med_01K0000000000000000000000B. The service segment is the authority that owns the resource; the project segment is its tenancy boundary; type is the second half of a common.resource-summary@1 kind and id is that summary's id, so the name composes from any resource summary. It is used in grant scopes, audit rows, event subjects, usage records, bill lines and console URLs. It never names a person, an app, a request, a session or an event. The pattern is the one common.usage-recorded@1 resource and common.resource-summary@1 ovrn already use.",
++ "type": "string",
++ "maxLength": 200,
++ "pattern": "^ovrn:[a-z][a-z0-9-]{0,31}:prj_[0-9A-HJKMNP-TV-Z]{26}:[a-z][a-z0-9-]{0,39}/[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$",
++ "not": {
++ "pattern": "/(usr|gst|app|mod|svc|job|evt|req)_"
++ }
++ },
"common.resource-summary.v1": {
"title": "ResourceSummary",
"description": "One resource of any service, as the resource index lists it (roadmap WS-Z7): every service answers GET /api/v1/resources with these, so OpenVibe.Services shows any resource without owning its data.",
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 99e7ed23..f15bab1f 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex bfbeb5c4..b76a5226 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex dbb64eed..165e7a96 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/food.json b/generated/openapi/food.json
iindex 03858098..25f94252 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 72a36c55..2fa062d6 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 1bdbdda0..1860410f 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 9dc14693..cca58896 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -33,15 +33,15 @@
"service": "bot",
"name": "OpenVibe.Bot",
"origin": "https://openvibe.bot",
-- "operations": 20,
-- "capabilities": 6
++ "operations": 21,
++ "capabilities": 7
},
{
"service": "chat",
"name": "OpenVibe.Chat",
"origin": "https://openvibe.chat",
-- "operations": 24,
-- "capabilities": 13
++ "operations": 25,
++ "capabilities": 14
},
{
"service": "codes",
@@@ -54,8 +54,8 @@
"service": "community",
"name": "OpenVibe.Community",
"origin": "https://openvibe.community",
-- "operations": 39,
-- "capabilities": 12
++ "operations": 40,
++ "capabilities": 13
},
{
"service": "coupons",
@@@ -145,8 +145,8 @@
"service": "openre",
"name": "OpenRestream",
"origin": "https://openre.stream",
-- "operations": 24,
-- "capabilities": 8
++ "operations": 25,
++ "capabilities": 9
},
{
"service": "play",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 9d20abea..5f7c7ae0 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex f5cf7911..5099cf3b 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media-hub.json b/generated/openapi/media-hub.json
iindex 49224856..b7213f83 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 918992ea..2ac7648c 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 2e389291..326d36e7 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 09de693b..ed76d6d3 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 814f406e..f403e5c6 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRestream API",
-- "version": "0.175.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -551,6 +551,62 @@
}
}
},
++ "/api/v1/resources/control": {
++ "post": {
++ "operationId": "post_api_v1_resources_control",
++ "summary": "Control a person's restream definitions through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-reque",
++ "description": "**openre.resource.control** (first-party): Control a person's restream definitions through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a openre.stream (id the definition id) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: suspend (state disabled: new ingest is refused, as PATCH state=disabled) and resume (state active again); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "tags": [
++ "openre.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "openre.resource.control"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "openre.resource.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.owned-resource-control-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/sessions": {
"get": {
"operationId": "get_api_v1_sessions",
@@@ -1395,6 +1451,153 @@
"type": "object",
"maxProperties": 0
},
++ "common.owned-resource-control-request.v1": {
++ "title": "OwnedResourceControlRequest",
++ "description": "common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "owned",
++ "owner",
++ "idempotency_key"
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "delete",
++ "stop",
++ "suspend",
++ "resume",
++ "archive"
++ ]
++ },
++ "owned": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "id"
++ ],
++ "description": "The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).",
++ "properties": {
++ "kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
++ "description": "The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room)."
++ }
++ }
++ },
++ "owner": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "id"
++ ],
++ "description": "The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.",
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "Who asks: the owner, or their agent (whose sensitive actions wait for the owner's approval)."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++ },
++ "common.resource-control-result.v1": {
++ "title": "ResourceControlResult",
++ "description": "common.resource-control-result@1: the authority's answer to a common.resource-control-request@1 (ADR-048, plan T13). state is the outcome, not the resource's own state: done (applied), pending (accepted and asynchronous; the authority reports completion later on its events), refused (a policy or confirmation gate said no) or failed (an error). State rules: refused carries problem or confirmation_required; failed carries problem; done and pending carry neither; confirmation_required appears only on refused. result carries the authority's own response document and never another service's rows. The state rules are checked by contracts.resources.checkControlResult.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "state",
++ "at"
++ ],
++ "properties": {
++ "action": {
++ "type": "string"
++ },
++ "resource": {
++ "$ref": "#/components/schemas/common.resource-name.v1"
++ },
++ "state": {
++ "enum": [
++ "done",
++ "pending",
++ "refused",
++ "failed"
++ ]
++ },
++ "result": {
++ "type": "object"
++ },
++ "confirmation_required": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "confirmation_id",
++ "reason"
++ ],
++ "properties": {
++ "confirmation_id": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "reason": {
++ "type": "string"
++ }
++ },
++ "description": "The action needs its owner's approval; retry the same request with this confirmation_id once approved."
++ },
++ "problem": {
++ "$ref": "#/components/schemas/errors.problem.v1",
++ "description": "RFC 9457 body when refused or failed."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
"common.resource-list-result.v1": {
"title": "ResourceListResult",
"description": "A page of the resource index (roadmap WS-Z7).",
@@@ -1441,6 +1644,16 @@
}
}
},
++ "common.resource-name.v1": {
++ "title": "ResourceName",
++ "description": "common.resource-name@1: the one stable, cross-service name of an OpenVibe resource (OVRN; ADR-034 section 2, ADR-048, plan T13): ovrn:<service>:<project_id>:<type>/<id>, for example ovrn:media:prj_01J0000000000000000000000A:object/med_01K0000000000000000000000B. The service segment is the authority that owns the resource; the project segment is its tenancy boundary; type is the second half of a common.resource-summary@1 kind and id is that summary's id, so the name composes from any resource summary. It is used in grant scopes, audit rows, event subjects, usage records, bill lines and console URLs. It never names a person, an app, a request, a session or an event. The pattern is the one common.usage-recorded@1 resource and common.resource-summary@1 ovrn already use.",
++ "type": "string",
++ "maxLength": 200,
++ "pattern": "^ovrn:[a-z][a-z0-9-]{0,31}:prj_[0-9A-HJKMNP-TV-Z]{26}:[a-z][a-z0-9-]{0,39}/[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$",
++ "not": {
++ "pattern": "/(usr|gst|app|mod|svc|job|evt|req)_"
++ }
++ },
"common.resource-summary.v1": {
"title": "ResourceSummary",
"description": "One resource of any service, as the resource index lists it (roadmap WS-Z7): every service answers GET /api/v1/resources with these, so OpenVibe.Services shows any resource without owning its data.",
ddiff --git a/generated/openapi/play.json b/generated/openapi/play.json
iindex 17228d08..7ce9f3bf 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex d5282f30..b8e59866 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/run.json b/generated/openapi/run.json
iindex 0c21f0cd..ad45a8c6 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 368f1743..1f26bed5 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/services.json b/generated/openapi/services.json
iindex 98546770..494ed2e8 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 5c22ac52..cffeff64 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex e0d1cc05..d24cbaaf 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex a453caa3..c5343ef4 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex a787d6f6..42b96e0e 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 0fcdf017..4ec0c5de 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex f9e2932d..0eb35325 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/watch.json b/generated/openapi/watch.json
iindex 87941d1e..e575dea6 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex cce66952..ce20c659 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/work.json b/generated/openapi/work.json
iindex c0a2cef2..449870df 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/zone.json b/generated/openapi/zone.json
iindex b322f09c..8bc5d88d 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.175.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.175.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.176.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.176.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 11d4a4d6..2e52a65f 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -178621,6 +178621,46 @@ export type ResourceControlRequest = {
[k: string]: unknown | undefined;
};
++/** [email protected] (owner: contracts) */
++/**
++ * common.owned-resource-control-request@1 (ADR-048 amendment 2026-10-15, step 8 phase 2): one control operation on a person-owned resource. Robots, chat rooms, restream definitions and pastes belong to a person, not to a project, so they carry no OVRN and the project-scoped common.resource-control-request@1 cannot name them. OpenVibe.Services sends this to the owning authority's POST /api/v1/resources/control (the same route, told apart by `owned`), acting for the person in on_behalf_of. owned names the resource by its index summary's kind and id. owner is the person it belongs to: the authority refuses unless its own record says the same (Services also offers actions only on the viewer's own resources). A sensitive action (deleting, physical control) asked by an agent is refused with confirmation_required until the owner approves, as for project resources. idempotency_key makes a retry safe: the authority answers a repeated key from the same caller with the first result, and refuses the key reused with another request. Answered with common.resource-control-result@1 (without resource: there is no OVRN). The actions each authority takes are in its <service>.resource.control capability; any other action is refused unsupported.
++ */
++export interface OwnedResourceControlRequest {
++ action: "delete" | "stop" | "suspend" | "resume" | "archive";
++ /**
++ * The resource, as its owning authority's resource index lists it (common.resource-summary@1 kind and id).
++ */
++ owned: {
++ kind: string;
++ /**
++ * The summary's id: a prefixed ULID (rob_…) or the service's own slug (a paste, a room).
++ */
++ id: string;
++ };
++ /**
++ * The person the resource belongs to. Person-owned resources belong to people, never to agents or apps.
++ */
++ owner: {
++ type: "user";
++ id: string;
++ };
++ /**
++ * Action-specific input, shaped by the authority's own contract (never redefined here).
++ */
++ params?: {};
++ idempotency_key: string;
++ on_behalf_of?: SubjectRef;
++ /**
++ * The approved confirmation from an earlier confirmation_required answer.
++ */
++ confirmation_id?: string | null;
++ /**
++ * Decide and explain without applying.
++ */
++ dry_run?: boolean;
++ trace_id?: string;
++}
++
/** [email protected] (owner: contracts) */
/**
* common.resource-control-result@1: the authority's answer to a common.resource-control-request@1 (ADR-048, plan T13). state is the outcome, not the resource's own state: done (applied), pending (accepted and asynchronous; the authority reports completion later on its events), refused (a policy or confirmation gate said no) or failed (an error). State rules: refused carries problem or confirmation_required; failed carries problem; done and pending carry neither; confirmation_required appears only on refused. result carries the authority's own response document and never another service's rows. The state rules are checked by contracts.resources.checkControlResult.
ddiff --git a/lib/resources.js b/lib/resources.js
iindex cd4f7899..f5cdadb8 100644
---- a/lib/resources.js
++++ b/lib/resources.js
@@@ -40,7 +40,13 @@ function nameOf(summary) {
function checkName(name) { return registry.validate('common.resource-name@1', name); }
++/**
++ * A control request: common.resource-control-request@1 for a project's resource (named by its OVRN), or
++ * common.owned-resource-control-request@1 for a person-owned one (named by `owned`, its index kind and id; ADR-048
++ * amendment 2026-10-15). The two are told apart by `owned`.
++ */
function checkControlRequest(q) {
++ if (q && typeof q === 'object' && q.owned !== undefined) return checkOwnedControlRequest(q);
const r = registry.validate('common.resource-control-request@1', q);
if (!r.valid) return r;
const errors = [];
@@@ -51,6 +57,18 @@ function checkControlRequest(q) {
return { valid: errors.length === 0, errors };
}
++/** A person-owned resource's control request: valid, and its kind names the authority's service as a resource kind does. */
++function checkOwnedControlRequest(q) {
++ const r = registry.validate('common.owned-resource-control-request@1', q);
++ if (!r.valid) return r;
++ const errors = [];
++ // An agent may ask for its owner; nobody else may ask for someone's resource.
++ if (q.on_behalf_of && q.on_behalf_of.type === 'user' && q.on_behalf_of.id !== q.owner.id) {
++ errors.push({ path: '/on_behalf_of', message: 'a person acts only on what they own' });
++ }
++ return { valid: errors.length === 0, errors };
++}
++
function checkControlResult(res) {
const r = registry.validate('common.resource-control-result@1', res);
if (!r.valid) return r;
@@@ -63,4 +81,4 @@ function checkControlResult(res) {
return { valid: errors.length === 0, errors };
}
--module.exports = { parse, format, nameOf, checkName, checkControlRequest, checkControlResult };
++module.exports = { parse, format, nameOf, checkName, checkControlRequest, checkOwnedControlRequest, checkControlResult };
ddiff --git a/manifests/capabilities/bot.resource.control.json b/manifests/capabilities/bot.resource.control.json
nnew file mode 100644
iindex 00000000..46dfe090
---- /dev/null
++++ b/manifests/capabilities/bot.resource.control.json
@@@ -0,0 +1,22 @@
++{
++ "id": "bot.resource.control",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a bot.robot (id rob_<ULID>) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: stop (latch the robot's remote e-stop, as POST /api/v1/robots/:id/estop: every actuator stops and nothing moves until it is cleared) and resume (clear that e-stop, as POST /api/v1/robots/:id/estop/clear; a local stop on the robot itself still holds); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "permissions": [
++ "resources:control"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "standard",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/resources/control"
++ ],
++ "inputSchema": "common.owned-resource-control-request@1",
++ "outputSchema": "common.resource-control-result@1"
++}
ddiff --git a/manifests/capabilities/chat.resource.control.json b/manifests/capabilities/chat.resource.control.json
nnew file mode 100644
iindex 00000000..6ef27831
---- /dev/null
++++ b/manifests/capabilities/chat.resource.control.json
@@@ -0,0 +1,22 @@
++{
++ "id": "chat.resource.control",
++ "version": "1.0",
++ "owner": "chat",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Control a person's rooms through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a chat.room (id the room's slug) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: archive (the room leaves every listing and takes no new messages; its history is kept for its members, as an archived room); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "permissions": [
++ "resources:control"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "standard",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/resources/control"
++ ],
++ "inputSchema": "common.owned-resource-control-request@1",
++ "outputSchema": "common.resource-control-result@1"
++}
ddiff --git a/manifests/capabilities/community.resource.control.json b/manifests/capabilities/community.resource.control.json
nnew file mode 100644
iindex 00000000..12df1a45
---- /dev/null
++++ b/manifests/capabilities/community.resource.control.json
@@@ -0,0 +1,22 @@
++{
++ "id": "community.resource.control",
++ "version": "1.0",
++ "owner": "community",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Control a person's pastes through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a community.paste (id the paste's slug) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: delete (the paste goes through Community's own deletion path, as the owner deleting it on openvibe.community); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "permissions": [
++ "resources:control"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "standard",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/resources/control"
++ ],
++ "inputSchema": "common.owned-resource-control-request@1",
++ "outputSchema": "common.resource-control-result@1"
++}
ddiff --git a/manifests/capabilities/openre.resource.control.json b/manifests/capabilities/openre.resource.control.json
nnew file mode 100644
iindex 00000000..b7d93afc
---- /dev/null
++++ b/manifests/capabilities/openre.resource.control.json
@@@ -0,0 +1,22 @@
++{
++ "id": "openre.resource.control",
++ "version": "1.0",
++ "owner": "openre",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Control a person's restream definitions through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a openre.stream (id the definition id) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: suspend (state disabled: new ingest is refused, as PATCH state=disabled) and resume (state active again); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "permissions": [
++ "resources:control"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "standard",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/resources/control"
++ ],
++ "inputSchema": "common.owned-resource-control-request@1",
++ "outputSchema": "common.resource-control-result@1"
++}
ddiff --git a/manifests/services/bot.json b/manifests/services/bot.json
iindex 3f5b22a2..245d798e 100644
---- a/manifests/services/bot.json
++++ b/manifests/services/bot.json
@@@ -33,7 +33,8 @@
"bot.computer.control",
"bot.device.connect",
"bot.job.dispatch",
-- "bot.resource.read"
++ "bot.resource.read",
++ "bot.resource.control"
],
"eventsProduced": [
"bot.robot.online",
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex f0b27f91..30622b74 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -26,7 +26,8 @@
"chat.sounds.write",
"chat.stats.read",
"chat.ticket.write",
-- "chat.resource.read"
++ "chat.resource.read",
++ "chat.resource.control"
],
"eventsProduced": [
"chat.message.created",
ddiff --git a/manifests/services/community.json b/manifests/services/community.json
iindex 83cbe87b..02217763 100644
---- a/manifests/services/community.json
++++ b/manifests/services/community.json
@@@ -25,7 +25,8 @@
"community.thread.read",
"community.vote.set",
"community.pulse.read",
-- "community.resource.read"
++ "community.resource.read",
++ "community.resource.control"
],
"eventsProduced": [
"community.comment.created",
ddiff --git a/manifests/services/openre.json b/manifests/services/openre.json
iindex 4d3f2605..3cc371e3 100644
---- a/manifests/services/openre.json
++++ b/manifests/services/openre.json
@@@ -125,7 +125,8 @@
"openre.session.end",
"openre.output.read",
"openre.output.write",
-- "openre.resource.read"
++ "openre.resource.read",
++ "openre.resource.control"
],
"eventsProduced": [
"openre.session.started",
ddiff --git a/package.json b/package.json
iindex a6162d76..95d56afc 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.175.0",
++ "version": "0.176.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/bot-manifest.test.js b/test/bot-manifest.test.js
iindex 4b266730..bd11e616 100644
---- a/test/bot-manifest.test.js
++++ b/test/bot-manifest.test.js
@@@ -9,7 +9,8 @@ const c = require('..');
// bot.resource.read: the robots in OpenVibe.Services' resource index (planned 0.129.0, active 0.130.0; plan T13 step 8).
// bot.computer.control: an owner's agent uses their own machine (0.166.0, plan T14/T17; Bot's STATUS.json lists it with
// the computer kinds).
--const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read'];
++// bot.resource.control: an owner stops or resumes their robot from OpenVibe.Services (0.176.0, ADR-048 step 8 phase 2).
++const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read', 'bot.resource.control'];
const BOT_EVENTS = ['bot.robot.online', 'bot.robot.offline', 'bot.estop.set', 'bot.estop.cleared', 'bot.command.refused'];
const bot = c.services.get('bot');