You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Manage Permissions: separate Read and Write access into two tabs.
7
+
8
+
The skill and skillset permissions editor is now a two-tab editor — a **Read** tab (Public toggle, or a restricted set of orgs/users who can read) and a **Write** tab (orgs/users who can edit). This makes every combination the backend already supports expressible, including **public read + organization/user write**, which the previous single-visibility-ladder couldn't reach. Skillsets also gain the read / read-write level support they previously lacked. No API changes — purely a UI redesign over the existing typed `grants` model.
Separate read and write access (drop the `read_write` level) and show edit UI to write-grantees.
7
+
8
+
The combined `read_write` grant level is renamed to `write`: each grant is now `read` or `write` (a `write` grant implies read), with no combined label. Existing `read_write` grants migrate to `write` automatically at boot — non-disruptive and rollback-safe.
9
+
10
+
This also fixes a bug where a user with **write** access saw no edit controls: the skill detail page (Edit button, inline file editor, Save) and the edit page now reveal content-editing to write-grantees, not just the owner, while admin actions (permissions, transfer, delete, visibility) remain owner/admin-only.
Skills and skillsets now carry a typed `grants` ACL where each user/org grant has a level: **read** (view / pull / execute) or **read-write** (also update the skill's content & metadata). Read-write grantees cannot manage permissions, transfer ownership, or delete — those admin/danger-zone actions stay with the owner and platform admins only.
9
+
10
+
Owners (and platform admins) can **transfer a skill or skillset to another Ornn user** from the Danger Zone; the transfer is immediate and the prior owner keeps read access. All existing shares (public, org, and per-user) migrate to read-only with no disruption to current access.
NyxID composes a **"Platform Admin"** role that grants all `*:admin` + `*:read` permissions above; current platform admins inherit this role with zero UX change. Sub-admin roles (content moderator, tag curator, support) can be composed from subsets when needed.
371
+
Skillset endpoints **reuse** the `ornn:skill:{create,read,update,delete}`
372
+
scopes verbatim (§2.6) — there is no `ornn:skillset:*` scope split in v1.
373
+
374
+
NyxID composes a **"Platform Admin"** role around `ornn:admin:skill` (plus the
375
+
`*:admin` + `*:read` scopes above); a token carrying `ornn:admin:skill`
376
+
bypasses object ownership on every skill/skillset operation. Current platform
377
+
admins inherit this role with zero UX change. Sub-admin roles (content
378
+
moderator, tag curator, support) can be composed from subsets when needed.
365
379
366
-
Adding a new permission requires convention-doc update. NyxID role → permission mapping is owned by NyxID config; this doc is the permission catalog.
380
+
Adding a new permission requires convention-doc update. NyxID role →
381
+
permission mapping is owned by NyxID config; this doc is the permission
382
+
catalog.
367
383
368
384
### 5.3 Scope declaration
369
385
370
386
Every route in OpenAPI tagged with its required scopes. Public endpoints explicitly declare `security: []`.
371
387
388
+
### 5.4 Object-level permission tiers (#1123)
389
+
390
+
Independent of the request scopes above, every **skill AND skillset** carries
391
+
a three-tier object-permission model. All three gates derive from one source
392
+
of truth — `ornn-api/src/domains/skills/crud/authorize.ts` (`canReadSkill` /
393
+
`canWriteSkill` / `canManageSkill`) — and skillsets reuse the same gates. A
394
+
request scope decides whether the caller may *call* the endpoint; the object
395
+
tier decides whether they may act on *this specific* skill/skillset.
396
+
397
+
| Tier | Gate | Who qualifies | What it grants |
398
+
|---|---|---|---|
399
+
|**READ**|`canReadSkill`| Public skill → anyone. Private → author, platform admin, or any grantee (`read`**or**`write`), directly or via a granted org. | View / pull / execute / list versions. |
400
+
|**WRITE**|`canWriteSkill`| Author **OR** platform admin **OR** a `write` grantee (direct or via a granted org). | READ, plus update the skill's **content + metadata only** (publish a new version). |
Request body, query string, or path parameter failed validation. Per-field details are in `errors[]`.
42
42
@@ -65,6 +65,18 @@ The uploaded payload is not a parseable ZIP — a malformed or unreadable archiv
65
65
66
66
**Client action:** re-create the ZIP and re-upload; do not retry the same bytes.
67
67
68
+
### invalid_permission_level
69
+
70
+
A typed `grants` entry on a permissions request (#1123) carried a `level` outside the allowed set. The only accepted values are `read` and `write` (see [`docs/CONVENTIONS.md`](CONVENTIONS.md) §5.4). Surfaced from `PUT /api/v1/skills/{id}/permissions` and `PUT /api/v1/skillsets/{id}/permissions`.
71
+
72
+
**Client action:** set every grant's `level` to `read` or `write` and retry.
73
+
74
+
### invalid_transfer_target
75
+
76
+
The `newOwnerUserId` supplied to `POST /api/v1/skills/{id}/transfer-ownership` or `POST /api/v1/skillsets/{id}/transfer-ownership` (#1123) does not resolve to a known Ornn user — the target has never signed in to Ornn, so the directory cannot resolve them. The transfer is rejected before any mutation.
77
+
78
+
**Client action:** confirm the target user has signed in to Ornn at least once, then retry with their resolved user id. Do not retry the same id without that change.
79
+
68
80
---
69
81
70
82
## authentication_required
@@ -109,7 +121,7 @@ A skill in a dependency closure (#968) could not be resolved — either the refe
The request collides with current state — a duplicate skill name on create, a concurrent modification, a job that's already running, etc.
115
127
@@ -127,6 +139,12 @@ Two different versions of the **same** skill appear in one dependency closure (#
127
139
128
140
**Client action:** align the conflicting pins so every path resolves the skill to the same `<major.minor>` version, then retry.
129
141
142
+
### ownership_conflict
143
+
144
+
A `POST /api/v1/skills/{id}/transfer-ownership` or `POST /api/v1/skillsets/{id}/transfer-ownership` (#1123) named the **current** owner as `newOwnerUserId` — a no-op transfer. The target already owns the resource, so the request is rejected rather than silently succeeding.
145
+
146
+
**Client action:** if a transfer is actually intended, supply a different `newOwnerUserId`. If the resource is already owned by the intended user, no action is needed.
147
+
130
148
---
131
149
132
150
## payload_too_large
@@ -241,6 +259,9 @@ Clients pinned to the old `SCREAMING_SNAKE_CASE` codes need to switch to the low
241
259
|_(new in #968)_| 404 |`skill_dependency_not_found`|`resource_not_found`|
242
260
|_(new in #968)_| 409 |`dependency_cycle`|`resource_conflict`|
243
261
|_(new in #968)_| 409 |`dependency_conflict`|`resource_conflict`|
262
+
|_(new in #1123)_| 400 |`invalid_permission_level`|`validation_error`|
263
+
|_(new in #1123)_| 400 |`invalid_transfer_target`|`validation_error`|
264
+
|_(new in #1123)_| 409 |`ownership_conflict`|`resource_conflict`|
Format rule for future codes: lowercase ASCII, words joined by `_`, no leading/trailing `_`. Pick from the parent §1.4 vocabulary when generic; add a specific subcode only when the caller needs to branch on it.
0 commit comments