Repository navigation
Description
Found accidentally, while driving the grid in a browser to verify something else — which is the only reason it was noticed at all.
What was measured
Clicking the Status column header to sort the All tasks grid issued, per click:
PUT /api/v1/meta/view/duly_task.default
{"label":"All tasks","type":"grid", … ,"sort":[{"field":"status","order":"asc"}], …}
→ 200 {"success":true,"message":"Saved customization overlay (org=org_mtigfrnu5rygymk5, state=active) …"}
Two clicks, two persisted writes. No save gesture, no confirmation, no indication in the UI that anything was stored — it looks exactly like an ephemeral sort.
The stored row, read straight out of sys_metadata:
type: view name: duly_task.default
scope: "platform" owner: null organization_id: org_… state: active
owner is null and the scope is org-wide. This is not a per-user preference. The overlay replaces the app's declared duly_task.default view for the whole organization until someone deletes it, and it carries the entire view definition — columns, bulk actions, everything — not just the sort. So it also freezes the rest of the view at whatever the app shipped on the day the click happened: a later release that adds a column to this view would be silently ignored in any org where someone once clicked a header.
Cleaned up afterwards with DELETE /api/v1/meta/view/duly_task.default → "Customization overlay deleted — reset to artifact default", and confirmed sys_metadata is empty again.
Why this matters for this product specifically
Duly's views are shared surfaces by design — "Late", "Not moving", "By business unit" are the manager's org lens, and "My week" is every member's daily screen. A view whose definition any one person can overwrite for everyone, by doing the most ordinary thing there is to do to a table, is a different product from the one the view files describe.
The stagnation views are the sharpest case: Not moving is defined by sort: last_update_at asc plus a filter. If a member re-sorts it and that persists org-wide, the manager's headline signal quietly changes meaning for the whole tenant.
What is NOT established, and how to establish it
This was measured as the dev admin, who plausibly holds a customization permission an ordinary member does not. The whole severity turns on that:
- if view customization requires a platform permission our permission sets do not grant, this is a note in
AGENTS.mdand nothing more; - if any authenticated user can do it, it is a p0 for a multi-user product.
src/security/permission-sets.ts grants object-level access only — it says nothing about metadata or view customization either way, so the answer is the platform's default, and we do not know it.
Blocked on #73: the seeded people have no credentials, so there is currently no way to log in as an ordinary member and try. Once pnpm demo yields a loginable non-admin, this is a two-minute test — sort a column as a member, then check whether sys_metadata gained a row.
If it turns out members can do it
Two directions, not mutually exclusive:
- Deny the customization permission to
duly_memberandduly_managerexplicitly, the same way the security model already denies everything else it does not grant. That fits this repo's fail-closed posture. - Raise it upstream — a transient sort persisting org-wide with no save gesture is surprising regardless of who can do it, and the two actions deserve different affordances. Not filed yet, because a report that cannot say who is affected is a report the platform team cannot act on.
No pm:queue — recording it. Restart when #73 lands and the member half can be measured.
Activity
os-warren commentedon Sep 1, 2026
Claiming this — session
session_01SqkTcrxUFci7nqXdbBSe2p, branchclaude/issue-84-view-customization-scope.Unblocked by #73 (
pnpm demo, PR #88). Plan: boot the app, obtain a non-adminrole: "user"identity that holds exactly whatduly_membergrants, and measure whether a column-header click from that identity lands asys_metadataoverlay row. The measurement is the deliverable; the fix follows from which case we are in.Generated by Claude Code
Record who can customize a view: no Duly permission set can
os-warren commentedon Sep 1, 2026
Measured: members cannot. This is the note, not the p0.
#73 unblocked this. Booted
pnpm demo, created three accounts by self-registration (positions: ["user","org_member"],isPlatformAdmin: false) and bound each to exactly one Duly permission set viasys_user_permission_set— nothing beyond whatsrc/security/permission-sets.tsgrants. Binding verified live, not assumed: a boundduly_memberreadsduly_task200, an unbound self-registered account403.Each then issued the request the grid issues — the declared
duly_task.defaultconfig withsortswapped tostatus asc:PUT /api/v1/meta/view/duly_task.default401 UNAUTHENTICATEDduly_member403 FORBIDDEN—Saving a metadata item requires the `manage_metadata` capability.duly_manager403 FORBIDDEN— sameduly_admin403 FORBIDDEN— same200—Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)sys_metadatareadtotal: 0after every refused attempt.The negative result holds up three ways. The control fires: the identical body from the platform-admin session returns 200 and lands exactly the row this card describes (
scope: platform,owner: null, org-scoped) — so the 403s are a capability refusal, not a mis-shaped request. The 401/403 split shows the member's session resolved and was then denied on capability, rather than never authenticating. And both doors plus the reset door were checked — the compound twinPUT /api/v1/meta/duly_task/views/defaultandDELETE, since gating one door and not its twin is the usual bypass.Also swept for a second route a member might reach instead: org-wide
sys_view_definitionanswers403; the one member-writable store,sys_user_preference, requiresuser_idand is per-person by construction — the right affordance, not an overlay.So the capability that made the original observation possible is
manage_metadata, a platform-admin-only capability held viaadmin_full_access. No Duly permission set grants it, and no identity our security model hands out can reach that door.Two consequences
The contingent fix is not just unnecessary — it is not expressible.
systemPermissionsis an additive list with no deny form, so "denymanage_metadatatoduly_memberandduly_manager" cannot be written as enforcement. Writing it anyway would put a declared-and-unenforced key into the one file whose credibility rests on every line being live. Recorded inAGENTS.mdas a ⛔ so the next reader doesn't add it.The upstream filing is left to you, deliberately. This card conditioned it on members being able to do it, and they cannot — but the surprise the card describes is still real for anyone who does hold the capability: an ordinary sort click persists an org-wide overlay carrying the entire view definition, no save gesture, no UI indication, freezing the view at the shipped definition for the whole org. That is an affordance question for
objectstack-ai/objectui, and the "who is affected" half a platform team needs is now answerable: platform admins only. I have not filed it, because the card said to file in the other branch. Say the word and I will.Cleanup: control overlay reset with
DELETE; verifiedsys_metadatatotal: 0andduly_task.defaultback to the shippedsort: [{"field":"due_date","order":"asc"}],_provenance: package. The dev database lives in the task worktree and goes with it.Draft PR #93 —
AGENTS.mdonly, no code change.Generated by Claude Code
os-warren commentedon Sep 1, 2026
{ "issue": 84, "status": "done", "branch": "claude/issue-84-view-customization-scope", "pr": "https://github.com/objectstack-ai/duly/pull/93", "premise_still_valid": true, "summary": "Answered the question the card was blocked on: ordinary members CANNOT persist a view customization overlay, so this is the note branch, not p0, and no security code change was made. Measured against a live `pnpm demo` with three self-registered accounts each bound to exactly one Duly permission set via sys_user_permission_set (binding verified by A/B, not assumed): duly_member, duly_manager AND duly_admin all get 403 FORBIDDEN 'Saving a metadata item requires the `manage_metadata` capability' on PUT /api/v1/meta/view/duly_task.default, on the compound twin door, and on the DELETE reset door; sys_metadata stayed at total 0 throughout. The identical body from the platform-admin session returns 200 and lands exactly the row the card describes, so the 403s are a capability refusal rather than a malformed request. The capability behind the original observation is manage_metadata, granted only by admin_full_access — no Duly set grants it. The PR adds one AGENTS.md section recording the verdict, the table and the method, plus a stop-sign: the card's contingent fix (deny the capability to duly_member/duly_manager) is not expressible, because systemPermissions is an additive list with no deny form, and writing it would add a declared-and-unenforced key to the one file whose credibility rests on every line being live. The upstream objectui filing was left unfiled and flagged for the maintainer — the card conditioned it on the member case, which did not materialise, but the admin-facing surprise is real and the 'who is affected' half is now answerable (platform admins only). Cleanup verified: overlay deleted, sys_metadata total 0, duly_task.default back to the shipped due_date asc with _provenance package; the demo server was stopped by killing only the process group I started.", "tests": "All four gates re-run on the final commit 67c670e with a clean tree, exit codes captured before any pipe. `pnpm validate` exit 0 — its own verdict line `✓ Validation passed (368ms)`, with the one documented hierarchy-security capability-provider warning that AGENTS.md calls this repo's expected state. `pnpm typecheck` exit 0. `pnpm test` exit 0 — `Test Files 25 passed (25)` / `Tests 641 passed (641)`. `pnpm build` exit 0 — `✓ Build complete (746ms)`. No ablation was run: the deliverable is a live-system measurement, not a test-fixture claim, and the measurement carries its own control (the same body from a platform admin returns 200 and lands the row) plus a 401-vs-403 discriminator proving the member session resolved before being denied. No test was added because no code changed — the finding is that the platform already refuses, and a repo-local test asserting a platform gate would pin someone else's behaviour from outside.", "open_questions": [ { "question": "Should the affordance problem still be filed at objectstack-ai/objectui now that the answer is 'platform admins only'? The card made that filing conditional on members being able to customize, and they cannot — but its stated reasoning ('surprising regardless of who can do it') is branch-independent, and a sort click still silently freezes a view org-wide for anyone holding manage_metadata.", "options": [ "A — Do not file. The card's condition was not met; a platform admin surprising themselves on a dev box is a much smaller claim than the p0 this card was written against, and cross-org issues are cheap to open later and awkward to retract.", "B — File it, downgraded, with the measurement attached: affected population is platform admins / admin_full_access holders only, the write is org-wide with no save gesture, and the two actions (transient sort vs. persist an org-wide overlay) deserve different affordances." ], "recommendation": "B, but not by me on this card. The measurement is exactly what made the report actionable — the card itself says a report that cannot say who is affected is one the platform team cannot act on, and that gap is now closed. What argues against filing it here is scope, not merit: writing into another org's tracker was authorized only for the branch that did not happen. One word from the maintainer and I will open it with the table above." } ], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
(Supersedes the report comment above: its leading HTML-comment marker was eaten by GitHub's body sanitizer on write, which makes that copy invisible to a marker scan. Same JSON, verbatim, behind a literal-text marker instead. No edit tool was available to fix the original in place.)
{ "issue": 84, "status": "done", "branch": "claude/issue-84-view-customization-scope", "pr": "https://github.com/objectstack-ai/duly/pull/93", "premise_still_valid": true, "summary": "Answered the question the card was blocked on: ordinary members CANNOT persist a view customization overlay, so this is the note branch, not p0, and no security code change was made. Measured against a live `pnpm demo` with three self-registered accounts each bound to exactly one Duly permission set via sys_user_permission_set (binding verified by A/B, not assumed): duly_member, duly_manager AND duly_admin all get 403 FORBIDDEN 'Saving a metadata item requires the manage_metadata capability' on PUT /api/v1/meta/view/duly_task.default, on the compound twin door, and on the DELETE reset door; sys_metadata stayed at total 0 throughout. The identical body from the platform-admin session returns 200 and lands exactly the row the card describes, so the 403s are a capability refusal rather than a malformed request. The capability behind the original observation is manage_metadata, granted only by admin_full_access — no Duly set grants it. The PR adds one AGENTS.md section recording the verdict, the table and the method, plus a stop-sign: the card's contingent fix (deny the capability to duly_member/duly_manager) is not expressible, because systemPermissions is an additive list with no deny form, and writing it would add a declared-and-unenforced key to the one file whose credibility rests on every line being live. The upstream objectui filing was left unfiled and flagged for the maintainer — the card conditioned it on the member case, which did not materialise, but the admin-facing surprise is real and the 'who is affected' half is now answerable (platform admins only). Cleanup verified: overlay deleted, sys_metadata total 0, duly_task.default back to the shipped due_date asc with _provenance package; the demo server was stopped by killing only the process group I started.", "tests": "All four gates re-run on the final commit 67c670e with a clean tree, exit codes captured before any pipe. `pnpm validate` exit 0 — its own verdict line `Validation passed (368ms)`, with the one documented hierarchy-security capability-provider warning that AGENTS.md calls this repo's expected state. `pnpm typecheck` exit 0. `pnpm test` exit 0 — `Test Files 25 passed (25)` / `Tests 641 passed (641)`. `pnpm build` exit 0 — `Build complete (746ms)`. No ablation was run: the deliverable is a live-system measurement, not a test-fixture claim, and the measurement carries its own control (the same body from a platform admin returns 200 and lands the row) plus a 401-vs-403 discriminator proving the member session resolved before being denied. No test was added because no code changed — the finding is that the platform already refuses, and a repo-local test asserting a platform gate would pin someone else's behaviour from outside.", "open_questions": [ { "question": "Should the affordance problem still be filed at objectstack-ai/objectui now that the answer is 'platform admins only'? The card made that filing conditional on members being able to customize, and they cannot — but its stated reasoning ('surprising regardless of who can do it') is branch-independent, and a sort click still silently freezes a view org-wide for anyone holding manage_metadata.", "options": [ "A — Do not file. The card's condition was not met; a platform admin surprising themselves on a dev box is a much smaller claim than the p0 this card was written against, and cross-org issues are cheap to open later and awkward to retract.", "B — File it, downgraded, with the measurement attached: affected population is platform admins / admin_full_access holders only, the write is org-wide with no save gesture, and the two actions (transient sort vs. persist an org-wide overlay) deserve different affordances." ], "recommendation": "B, but not by me on this card. The measurement is exactly what made the report actionable — the card itself says a report that cannot say who is affected is one the platform team cannot act on, and that gap is now closed. What argues against filing it here is scope, not merit: writing into another org's tracker was authorized only for the branch that did not happen. One word from the maintainer and I will open it with the table above." } ], "out_of_scope_findings": [] }Generated by Claude Code