Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Record who can customize a view: no Duly permission set can - #93

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope
Sep 1, 2026
Merged

Record who can customize a view: no Duly permission set can#93
os-warren merged 1 commit into
mainfrom
claude/issue-84-view-customization-scope

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#84

#84 was blocked on one question: the org-wide view overlay was measured as the dev admin, and the severity turned entirely on whether an ordinary member could do the same thing. #73 (pnpm demo) unblocked it. It is measured now, and the answer is no — so this is the card's "note, not p0" branch, and there is no security change.

What was measured

Against a live pnpm demo on @objectstack/rest 17.2.0. Three accounts were created by self-registration (POST /api/v1/auth/sign-up/email, which yields positions: ["user","org_member"], isPlatformAdmin: false) and each bound to exactly one Duly permission set through sys_user_permission_set — nothing beyond what src/security/permission-sets.ts grants.

The binding was verified live rather than assumed: a bound duly_member reads duly_task200, an unbound self-registered account reads it 403. So the sets really do resolve for these callers.

Each then issued the request the grid issues — the declared duly_task.default config with sort swapped to status asc:

CallerPUT /api/v1/meta/view/duly_task.default
anonymous401 UNAUTHENTICATED
duly_member403 FORBIDDENSaving a metadata item requires the `manage_metadata` capability.
duly_manager403 FORBIDDEN — same
duly_admin403 FORBIDDEN — same
platform admin200Saved customization overlay (org=org_mtik8rkleytx3x6b, state=active)

sys_metadata read total: 0 after every refused attempt.

Three things make the negative result trustworthy rather than a mis-shaped request:

  • The control fires. The identical body from the platform-admin session returns 200 and lands the row finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone #84 describes — type: view, name: duly_task.default, scope: platform, owner: null, organization_id: org_…, state: active. The 403s are a capability refusal, not a malformed payload.
  • The 401/403 split. An anonymous caller gets a different code from the anonymous-deny gate, so the member's 403 proves its session resolved and was then denied on capability.
  • Both doors and the reset door. The compound twin (PUT /api/v1/meta/duly_task/views/default) and DELETE /api/v1/meta/view/duly_task.default carry the identical gate — checked, because gating one door and not its twin is the usual bypass.

Also swept for a second persistence route a member might reach instead: the org-wide sys_view_definition answers 403, and the one member-writable store, sys_user_preference, requires user_id and is per-person by construction — which is the right affordance, not an org-wide overlay.

Cleanup: the control overlay was reset with DELETE, and the end state was verified — sys_metadatatotal: 0, and duly_task.default back to the shipped sort: [{"field":"due_date","order":"asc"}] with _provenance: package. The dev database lives in the task worktree and goes with it.

What this PR changes

One section in AGENTS.md. No code. It records the verdict, the table above, and how it was measured, so nobody re-derives it — plus two things a future reader would otherwise get wrong:

Gates

All four green on 67c670e, the head of this branch, with a clean tree:

pnpm validate exit 0 ✓ Validation passed (368ms)
pnpm typecheck exit 0
pnpm test exit 0 Test Files 25 passed (25) · Tests 641 passed (641)
pnpm build exit 0 ✓ Build complete (746ms)

validate's one warning is the hierarchy-security capability-provider notice that AGENTS.md documents as this repo's expected state.

No changeset — this repo has no changeset mechanism.

Generated by Claude Code


Generated by Claude Code

Answers the question #84 was blocked on. Measured against a live `pnpm demo`
with three self-registered accounts, each bound to exactly one Duly permission
set through `sys_user_permission_set`:
anonymous 401 UNAUTHENTICATED
duly_member 403 FORBIDDEN — requires the `manage_metadata` capability
duly_manager 403 FORBIDDEN — same
duly_admin 403 FORBIDDEN — same
platform admin 200 — Saved customization overlay (org=…)
`sys_metadata` stayed at total: 0 across every refused attempt. So the org-wide
overlay is reachable only by a platform admin (`admin_full_access`), which is
what the original observation was made as — not by any identity our security
model hands out. This is the "note, not p0" branch of the card: no security
change is warranted, and none is made.
The note also says why the obvious hardening is wrong. `systemPermissions` is
an additive list with no deny form, so denying `manage_metadata` in
`src/security/permission-sets.ts` would add a declared-and-unenforced key to
the one file whose credibility rests on every line in it being live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The answer is "note branch", and the reason not to write the fix is better than the fix.

Gates on the head merged with current main:validate 0, typecheck 0, test 0 (Test Files 26, Tests 662), build 0.

The question this card was blocked on is answered, with controls rather than a single reading:

controlresultwhat it rules out
same body, platform-admin session200, overlay lands exactly as the card describedthe 403s are not a malformed payload
anonymous401 from a different gatethe member's 403 came after its session resolved
compound twin door + DELETE reset doorsame 403a gate on one door and not its twin — the usual bypass
sys_view_definition / sys_user_preference sweeporg-wide refused; the writable one is per-person by constructionan alternate persistence route

And the permission-set binding was verified by A/B rather than assumed — a bound duly_member reads duly_task 200, an unbound self-registered account 403 — which is the step that makes the three 403s mean "this role cannot" rather than "this account has nothing".

Declining to write the contingent fix is the right call

My card said: if members can do it, deny the capability to duly_member and duly_manager. You established they cannot, and then went one better — the fix is not expressible. systemPermissions is an additive list with no deny form, so the entry would not be enforcement, it would be a declared-and-unenforced key sitting in the one file whose entire credibility rests on every line in it being live. Writing it would have looked like hardening and been the exact defect ADR-0049 exists to remove. Putting a stop sign in AGENTS.md so the next person does not "helpfully" add it is worth more than the line itself would have been.

The section is well-judged generally: verdict, result table, method, and the boundary of what remains true. Recording how it was measured is what stops this being re-derived in three months by someone who finds the same PUT in a network log.

On your open question — file it, and I will

Your recommendation B is right and your reason for not doing it yourself is also right: I conditioned the upstream filing on the member branch, and that branch did not happen, so writing into another repository's tracker was outside what you were authorized to do. Declining on scope while saying plainly that the merit is unchanged is the correct shape for that. Consider it authorized retroactively in spirit — but it is mine to file, and I am filing it now with your result table.

The claim is smaller than the p0 this card was written against, and still real: an administrator demoing the app can freeze duly_task.default at the shipped definition by clicking a column header, and nothing tells them. A later release adding a column to that view would then be silently ignored in their org.

Cleanup verified — overlay deleted, sys_metadata back to 0, the view restored to its shipped due_date asc with its _provenance package, server killed by recorded PID, worktree removed without --force.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:17
@os-warren
os-warren merged commit 2d4061f into mainSep 1, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: clicking a column header persists an org-wide view overlay — one person's sort silently rewrites the view for everyone

1 participant

@os-warren