# Capability Matrix

This directory maintains the empirical API-method x auth-context coverage for
Yandex Office. The goal is a repeatable process that turns upstream API methods,
real OAuth tokens, and controlled fixtures into generated access-rights evidence.

## Sources Of Truth

- `../references/yandex-oauth-scopes.json` is the canonical OAuth scope list.
- `../references/yandex-office-auth-principles.md` is the persistent auth-model
  source of truth for accounts, apps, tokens, scopes, sub-skills, and runtime
  response authority.
- `methods.json` is the API-method task list. Keep method ids, paths,
  verbs/protocols, classifications, request shapes, setup actions, cleanup
  actions, and service auth contexts here.
- `atomic.client_id.json` maps tested auth-context ids to OAuth app client ids.
- Humans maintain private OAuth bearer fixtures out-of-band for live probes.

`public` is the universal no-OAuth auth context. It is not an OAuth scope.

Credential fixture values must be bearer OAuth tokens, not OAuth app client ids.
`validate.py` rejects entries that equal their `atomic.client_id.json` value or
look like 32-character hex client ids. A service is not probe-ready until every
auth context selected by its probe plan has a human-provided credential fixture.

Do not add `expected_scopes` to method rows. The map is generated from live
probe evidence, not from hardcoded scope guesses.

## Generated Files

- `matrix.json` is generated by `probe.py` and contains the full tested evidence
  matrix.
- `method-scope-map.json` is generated by `probe.py` and is the compact
  runtime-facing API-method x auth-context map.
- `raw/*.json` is generated by `probe.py` and stores redacted, reusable evidence
  summaries. It must not contain raw OAuth tokens or unredacted live response
  bodies.

Do not edit generated files manually.

`method-scope-map.json` uses three result forms:

```json
{
  "methods": {
    "disk.public.resources.get": {"public": true},
    "disk.resources.get.disk": {"one_of": ["cloud_api:disk.read"]},
    "disk.resources.get.app_folder": {"one_of": ["cloud_api:disk.app_folder"]},
    "disk.trash.resources.restore.put": {
      "all_of": ["cloud_api:disk.read", "cloud_api:disk.write"]
    }
  }
}
```

`public` means no OAuth token is required. `one_of` entries are alternative
sufficient auth contexts. `all_of` entries are scopes that must all be present
on the same token.

Runtime policy constraints are intentionally not generated as a separate
artifact yet. Keep the design note here: some services need runtime controls
that are stricter than the generated method-scope map, such as Mail IMAP command
class restrictions under `mail:imap_ro`.

## Method Ids

Method ids describe both the upstream operation and the tested request shape.
Do not merge rows that use different resource namespaces, fixture shapes, tenant
contexts, account ownership, or public/private access modes.

For Disk, for example, the same upstream endpoint can require separate rows:

- `disk.resources.publish.put.disk` uses `disk:/...` paths.
- `disk.resources.publish.put.app_folder` uses `app:/...` paths.

If a method has no request-shape variant, keep a single method id. Public
resource reads stay unsuffixed because they are no-OAuth public-resource
methods, not Disk-path or app-folder methods.

## Mandatory Service Algorithm

This process is mandatory for every yandex-office service. Disk is the reference
implementation: it started from scarce code-level calls and became a complete,
documented, repeatable access-rights coverage system. Every new service must
follow the same algorithm before its generated map is used for onboarding,
app-profile design, diagnostics, or remediation. Live API responses remain final
truth, per `../references/yandex-office-auth-principles.md`.

1. Inventory the service from upstream documentation.
   - Retrieve the full official API method list for the service.
   - For protocol services, retrieve the Yandex documentation that names the
     protocol/endpoint/auth model and the protocol specification that defines
     the method or command surface. Record both as upstream sources.
   - Record every upstream method in `methods.json`, even if current skill code
     does not call it.
   - Classify every method as `current_used`, `implemented_unused`,
     `upstream_available`, or `unsupported_or_deferred`.
   - Preserve upstream provenance in `upstream_source_ids`.
2. Normalize method ids around capability.
   - Include method id, service/sub-skill, protocol, HTTP verb or protocol
     command, path, operation name, request fixture fields, and success evidence.
   - Split request-shape capabilities into separate method ids when one upstream
     operation has different authorization behavior.
3. Encode executable request knowledge in `methods.json`.
   - Put target request shapes in `probe_requests`.
   - Put service-specific setup and cleanup in `probe_plans`.
   - Put tested service auth contexts in `probe_plans.<service>.auth_contexts`.
   - Keep service-specific paths, params, body shapes, fixture steps, and cleanup
     steps out of `probe.py`.
4. Maintain OAuth inputs.
   - Add/update scopes in `../references/yandex-oauth-scopes.json`.
   - Add/update one-scope app client ids in `atomic.client_id.json`.
   - Add combined-scope app client ids only when failed atomic results require a
     combined-token test. Use brace syntax such as `cloud_api:disk.{read,write}`.
   - Coordinate human-provided bearer OAuth fixtures for the selected auth
     contexts. Use bearer fixture values rather than client ids.
5. Probe from least authority to more authority.
   - Test `public` first.
   - If `public` works, stop for that method.
   - If `public` fails, test relevant atomic auth-context tokens.
   - If all relevant atomic tokens fail and matching combined-scope tokens exist,
     test those combined tokens.
   - Record negative results as evidence; do not convert guesses into map output.
6. Generate outputs from evidence.
   - `matrix.json` records every tested cell.
   - `method-scope-map.json` records only proven access paths.
   - `raw/*.json` keeps redacted evidence summaries used to rebuild outputs.
   - Summarize service outcomes by capability result, not by raw probe-row
     mechanics. Use this shape:

```text
<N> methods inventoried
<N> public
<N> single-scoped
<N> multi-scope
<N> failures
```

     "Public" means methods whose generated map entry is `public: true`, not
     the number of no-OAuth baseline requests attempted by the probe.
7. Validate the current state.

```bash
python3 capabilities/validate.py
```

## Commands

Run a full service probe:

```bash
python3 capabilities/probe.py --service disk
```

Run a focused method/context probe:

```bash
python3 capabilities/probe.py \
  --service disk \
  --method disk.trash.resources.restore.put \
  --scope 'cloud_api:disk.{read,write}'
```

`probe.py` must remain a generic wrapper. It may load JSON, render templates,
execute HTTP, summarize responses, write `raw/*.json`, rebuild `matrix.json`,
and rebuild `method-scope-map.json`. It must not contain service-specific method
lists, paths, request params, fixture paths, or capability rules.

Live fixture values that identify real mailboxes, contacts, users, or domains
belong in ignored `capabilities/probe.local.json` when a probe needs them:

```json
{
  "mail": {
    "smtp_allowed_recipient": "...",
    "imap_delete_probe_sender": "..."
  },
  "contacts": {
    "contact_probe_email": "..."
  },
  "directory": {
    "directory_alias_user_email": "...",
    "directory_domain": "..."
  },
  "telemost": {
    "telemost_probe_cohost_email": "..."
  }
}
```

## Specific Services Notes

### Disk

Disk is the reference implementation for this process.

- Disk-path and app-folder request shapes are separate capability rows, for
  example `*.disk` and `*.app_folder`.
- Trash item paths are not `disk:/...` paths. The probe creates a temporary
  file, deletes it, lists Trash, and uses the returned `trash:/...` path.
- `save-to-disk` saves into Downloads when `public_key` identifies a public
  file; `name` is the saved filename. `path` is for selecting an item inside a
  public folder.
- Public resource reads are public only while the fixture is published. Their
  probe requests republish and refresh `public_ref` before testing.
- `disk.resources.copy.post.disk`, `disk.resources.move.post.disk`, and
  `disk.trash.resources.restore.put` returned 403 for every one-scope Disk token
  and returned success with `cloud_api:disk.{read,write}`. They are represented
  as `all_of: ["cloud_api:disk.read", "cloud_api:disk.write"]`.
- `disk.public.resources.save_to_disk.post.app_folder` uses the documented
  `public_key` + `name` request shape. `cloud_api:disk.app_folder` returned
  HTTP 403, but `cloud_api:disk.write` returned HTTP 201, so the generated map
  entry is `one_of: ["cloud_api:disk.write"]`.

### Telemost

Telemost API docs were inventoried from
<https://yandex.ru/dev/telemost/doc/ru/access> and linked method pages. Every
method page includes a permission note in the form "Чтобы выполнить запрос,
приложению требуется разрешение ..."; that documented permission is recorded in
`methods.json` as `documented_permission`.

Telemost is empirically mapped for the nine methods exposed by the current
official docs and OpenAPI specification. The method inventory lives in
`methods.json`; the generated access map lives in `method-scope-map.json`.

- Organization settings rows require the documented read/update scopes, but the
  live account used for probing is not an organization admin. The probe treats
  `OrganizationSettingsAccessForbidden` as positive scope evidence for these
  rows because it is the documented post-scope account-role failure.
- Cohost delete uses a syntactically valid nonexistent Yandex email and treats
  `NoSuchUserPrincipalsFound` as positive scope evidence for the update token,
  because that error is reached only after the request passes the method's
  OAuth-scope gate.
- The current official Telemost access page lists create, read, and update
  scopes. A legacy `telemost-api:conferences.delete` scope remains in local
  OAuth inventories, but the current official Telemost method list and OpenAPI
  specification expose no delete-conference method using it.

### Calendar

Calendar is not a Yandex REST API catalog. Yandex documents Calendar access
through OAuth-backed CalDAV, with examples against `https://caldav.yandex.ru`;
the command surface is therefore split into Yandex-documented commands and
protocol-spec/live-probe commands. The official Yandex service-application docs
directly show event `PUT` and event `GET`, and show calendar deletion through
the Python `caldav` library. No official Yandex page found so far publishes a
complete CalDAV command catalog or explicit `PROPFIND`, `REPORT`, or
event-resource `DELETE` examples.

See `yandex-protocol-command-coverage.md` for the exact Yandex-official
Calendar command coverage and the pages checked.

The current coverage includes principal discovery, calendar-home discovery,
date-range query, event create, event read, and event delete. The probe creates
a temporary VEVENT in the first discovered event calendar, reads it back, and
deletes it in cleanup.

The first Calendar run exposed a bad generated event URL shape: `PUT` succeeded
but `GET` returned 404. The fixed request shape uses the discovered calendar
collection URL directly plus `{calendar_uid}.ics`; the rerun produced success
for create, read, and delete.

### Contacts

Contacts are not a Yandex REST API catalog. Yandex documents contact sync
through CardDAV at `carddav.yandex.ru`. The official Yandex contact-sync docs
name the CardDAV server, login shape, and ports, and describe contact sync
behavior. No official Yandex page found so far publishes a complete CardDAV
command catalog or explicit `PROPFIND`, `REPORT`, vCard `PUT`, vCard `GET`, or
vCard `DELETE` examples.

See `yandex-protocol-command-coverage.md` for the exact Yandex-official Contacts
coverage and the pages checked.

The current coverage includes addressbook discovery, addressbook collection
`PROPFIND`, vCard create, vCard read, and vCard delete. The vCard fixture uses
`{contact_probe_email}` as the contact email and is deleted during cleanup.

Yandex CardDAV returned `supported-report` for `REPORT addressbook-query`, so
that row was removed from the modeled method inventory. The supported collection
listing/read operation for the tested addressbook is `PROPFIND`.

### Mail

Mail is not a Yandex REST API catalog. Yandex documents mailbox access through
OAuth-backed IMAP and SMTP; the command surface is therefore sourced from
Yandex Mail OAuth docs plus the IMAP, SMTP, SMTP AUTH, and OAuth SASL RFCs.

Mail IMAP is probed with XOAUTH2 against the configured IMAP server. The modeled
surface includes RFC 3501 session commands, mailbox lifecycle commands, message
read commands, message mutation commands, and UID variants.

Important behavior with design implications: `mail:imap_ro` is not sufficient
by itself as a read-only safety boundary. The safe boundary is the combination
of OAuth context, IMAP mailbox mode, and command class:

- `mail:imap_ro` must open mailboxes read-only. At the IMAP protocol level this
  means `EXAMINE`; in Python `imaplib`, this means
  `select(mailbox, readonly=True)`, which sends `EXAMINE` internally.
- `mail:imap_full` is required for any read-write `SELECT` or mutation-capable
  mailbox session.

This was verified with a live deletion method in the probe inventory:
`mail.imap.uid.delete_matching.live_realty`. When the probe ran with only
`mail:imap_ro` and forced read-only mailbox selection, Yandex rejected
`UID STORE +FLAGS.SILENT (\Deleted)` with "Can not store in read-only folder",
and the target message remained present. Earlier tests without forced read-only
selection showed deletion could succeed.

This does not mean `mail:imap_ro` is harmless for every IMAP command. Mailbox
lifecycle commands are not governed by selected-mailbox read-only mode. A live
persistence check showed `mail:imap_ro` could `CREATE` a folder, disconnect,
reconnect, observe the folder in `LIST`, and then `DELETE` it. Therefore
yandex-office runtime code must enforce both mailbox mode and allowed command
classes; the generated method-scope map alone is not enough for Mail IMAP
authorization decisions.

See `mail-imap-scenarios.md` for the method-by-method readonly/readwrite
scenario breakdown and current yandex-office usage notes.

SMTP is modeled separately with `mail:smtp`, but it is not empirically mapped
from this host yet. Outbound connections to `smtp.yandex.com:465` fail with
`NetworkUnreachable` while IMAP reaches `imap.yandex.com:993`; SMTP matrix cells
are therefore `unclear_needs_retest`, not negative scope evidence. The SMTP send
probe has a hard recipient allowlist containing only `{smtp_allowed_recipient}`.

### Directory

Directory is inventoried from the official Yandex 360 API reference and access
page. `methods.json` now contains the full Directory method families exposed
there for organizations, users, groups, departments, domains, domain DNS, and
external contacts.

The current inventory has 49 Directory rows:

- organizations: 1 read method
- users: 13 read/write methods
- groups: 10 read/write methods
- departments: 7 read/write methods
- domains: 7 read/write methods
- domain DNS: 4 DNS-management methods
- external contacts: 7 read/write methods

The documented permission on each row is mapped from the official Directory
OAuth permission families listed on the Yandex 360 API access page:
`directory:read_*`, `directory:write_*`, and `directory:manage_dns`.

Directory is live-probed end to end. The probe first discovers the real `orgId`
with `directory.organizations.list`, then tests every Directory method with no
OAuth token and with the documented atomic scope for that method. The generated
evidence lives in `raw/directory-probe.json`; the compact capability result
lives in `method-scope-map.json`.

Most write methods use invalid bodies or nonexistent resource IDs. This is the
preferred Directory fixture pattern because it verifies that the request passed
the OAuth gate without creating or changing real users, groups, departments,
contacts, or DNS records. Status codes listed in `auth_success_status` count
only for requests that actually include an OAuth token; public no-token requests
must never be upgraded by those post-scope status rules.

Two Directory method families require controlled real fixtures because Yandex
returns server-side 500s for safe nonexistent resources:

- User alias create/delete uses the controlled user
  `{directory_alias_user_email}`. The API expects the alias body to contain only the
  local part, not `alias@domain`. The probe creates
  `openclaw-gh38-{run_id}`, deletes it in the paired delete method, and has
  cleanup that deletes the same alias if it is still present.
- DKIM status/disable/enable uses the controlled domain `{directory_domain}`.
  The full probe order is status, disable, then enable, leaving DKIM enabled at
  the end of the run.

Important lessons from this stage:

- A malformed token can look like a scope failure. `directory:read_domains`
  originally returned HTTP 401 because the stored token had a trailing comma;
  after correction, domain list/status mapped cleanly.
- Some Directory reads return HTTP 400 or 404 for nonexistent IDs after passing
  OAuth. Those statuses are valid positive evidence only when the request has an
  OAuth header and the method fixture explicitly declares them.
- Some Directory rows cannot be proven with fake resources. Alias and DKIM rows
  need controlled live fixtures, and the fixture must restore the final state.

Current capability summary:

```text
49 methods inventoried
0 public
49 single-scoped
0 multi-scope
0 failures
```

The final post-run checks found no leftover `openclaw-gh38` alias on
`{directory_alias_user_email}`, and `{directory_domain}` DKIM was enabled after the
disable/enable sequence.

### Forms

Forms is inventoried from the official Yandex Forms API reference. The index
pages were crawled by category and each linked endpoint page was checked for its
declared verb and `https://api.forms.yandex.net/v1/...` path.

`methods.json` now contains 33 Forms rows across Answers, Files, Forms, Form
Filling, Images, Questions, Form Filling Keys, Operations, and Users. The
permission model follows the official Forms access page:

- `forms:read` covers reading.
- `forms:write` covers creation, deletion, and editing.

Forms is live-probed end to end. Every request includes the organization header
`X-Org-ID`; without it, even valid OAuth tokens return `400` with "Требуется
организация". The Forms `orgId` is a fixture value in `methods.json`.

Most Forms rows are proven with nonexistent survey, question, keyset, operation,
answer, or file identifiers. This reaches post-scope validation errors without
creating or changing real Forms objects. A few endpoints close the HTTP stream
early for malformed public/no-token requests; `probe.py` records those transport
errors as negative public evidence instead of aborting the service run.

Important lessons from this stage:

- `POST /v1/surveys` with `{}` is too valid: it creates a blank form named
  `Новая форма`. The safe fixture uses an invalid schema body
  `{"name": ["openclaw-gh38-invalid"]}` and returns HTTP 400 after OAuth. The
  accidental blank form from the first run was deleted and the post-run list
  check found no leftovers.
- File upload and form-submit endpoints should use an empty raw body without a
  JSON content type for access-rights probing. JSON-shaped invalid requests can
  trigger incomplete chunked reads before a useful authorization result.
- Forms has no public methods in the current inventory. All 33 methods require
  either `forms:read` or `forms:write`.

Current capability summary:

```text
33 methods inventoried
0 public
33 single-scoped
0 multi-scope
0 failures
```

Controlled field-type fixture: a real form was created and the committed
evidence redacts its live form/question IDs. It contains one created question
for each question type exposed by the Forms API validation enum:

- `string`
- `boolean`
- `integer`
- `file`
- `comment`
- `date`
- `daterange`
- `payment`
- `enum`
- `suggest`
- `matrix`
- `series`

The persisted question list is recorded in `raw/forms-field-fixture.json`.
Yandex accepts `type=suggest` on creation, but stores it as `type=enum` with
`widget=dropdown`; the normal enum field stores as `type=enum` with
`widget=radio`.

### Tracker

Tracker is inventoried from the official Yandex Tracker API reference graph.
The crawler was seeded from issue and admin API pages, then restricted to
`/support/tracker/en/api-ref/...` pages that declare
`https://api.tracker.yandex.net/v3` endpoints. Overview pages and sample access
snippets are excluded from the method list.

`methods.json` now contains 149 Tracker rows across admin dictionaries, boards,
sprints, bulk operations, dashboards, macros, entities, imports, issues,
projects, queues, automation, components, and users.

Tracker has a two-scope OAuth model:

- `tracker:read` is used for `GET` methods and read-style `POST` searches.
- `tracker:write` is used for create, update, delete, transition, import,
  attachment, queue/admin, and automation mutations.

Tracker is live-probed end to end. Every request includes `X-Org-ID`; without
an organization header, Tracker returns HTTP 422 before method-level behavior.

Most Tracker rows are proven with nonexistent IDs and deliberately type-invalid
JSON bodies such as `{"name": ["openclaw-gh38-invalid"]}`. This reaches
post-scope validation, not-found, or role/access errors without creating real
Tracker objects. Empty mutation bodies are not safe in Tracker: `POST
/v3/boards/` and `POST /v3/projects/` created default objects during fixture
testing. Those accidental objects were deleted, and mutation fixtures now use
type-invalid JSON instead.

Important lessons from this stage:

- `X-Org-ID` is the correct organization header for this Yandex 360
  organization. `X-Cloud-Org-ID` returns an organization-not-available error.
- Some nonexistent IDs trigger Tracker server-side 500s. The stable fixture
  avoids those by using numeric missing IDs or known harmless existing context
  where needed.
- `tracker.issues_move_issue` cannot be proven with a nonexistent issue because
  Tracker returns HTTP 500. The fixture uses existing issue `PERSONAL-1` with
  `queue=PERSONAL`, which returns HTTP 422 without moving the issue.
- The official entity comment edit page describes `PATCH
  /v3/entities/<entity_type>/<entity_ID>/comments`, but its example and live API
  require `comments/<comment_ID>` for a useful method-level result. The modeled
  row uses `/comments/{comment_id}`.

Current capability summary:

```text
149 methods inventoried
0 public
149 single-scoped
0 multi-scope
0 failures
```

### Cloud

Cloud infrastructure is no longer part of `yandex-office`. The Cloud Functions,
Object Storage, IAM-token, service-account, and `yc` CLI material moved to the
private standalone `yandex-cloud` skill repo.

The yandex-office capability matrix now covers the Yandex 360 office service
surface only. Do not add `cloud:auth`, Cloud IAM-token exchange rows, Search API
rows, or downstream Yandex Cloud IAM-role operations back into this matrix.
