Repository navigation
Labels
domain:specDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
Description
Part of #12701 (hotcrm single-DB multi-org SaaS). Maintainer-decided direction (2026-08-27 live PM chat, direct-dispatch channel; quotes in the epic) — the implementation shape below is open, the contract is not.
The measured gap (all on origin/main @ 15bf9e8)
manage_metadatais the only metadata-authoring capability, and it isscope: 'platform'(packages/spec/src/security/capabilities.ts:43). Every/metawrite door demands it (packages/runtime/src/domains/meta.ts, pinned bymeta-save-capability-gate.test.ts).- Org-scoped writes are threaded only for the five registry
allowOrgOverride: truetypes (view, dashboard, report, translation, email_template) viaorganizationIdForMetaWrite(packages/metadata-core/src/meta-write-org-scope.ts); every other type lands env-wide (org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 discipline). - Consequence in a walled posture (
group/isolated): granting a tenant org admin the one key that unlocks per-org tier-A overlays also unlocks env-wide tier-B authoring (flows, objects — cross-tenant reach). So a SaaS operator cannot let tenants customize views/dashboards/reports/translations/email templates at all, even though the per-org overlay mechanism (ADR-0005) is complete under the door.
Contract to build
A tenant org admin in a walled posture can author org-scoped overlays for exactly the five tier-A types without holding platform manage_metadata:
- A new org-scoped authoring capability (suggested name
manage_org_presentation; final name is the dev's call inside ADR-0066 conventions) declared inPLATFORM_CAPABILITIES, seeded like its siblings. - The metadata write door accepts it only when ALL of: the type's registry entry declares
allowOrgOverride: true; the write is org-scoped to the caller's own active organization (never env-wide, never another org); posture semantics unchanged otherwise. Platformmanage_metadatakeeps its exact current behavior — this capability is a subset key, not a re-keying. - Refusals stay loud and name the sanctioned path (no caller-grant disclosure, The runtime dispatcher serialises a PermissionDeniedError's
detailsto the client, sopositions/permissionSetsreach the browser on the/datatransport #7450): a holder attempting a tier-B write or an env-wide write is refused with the existing envelopes (NOT_OVERRIDABLE/PERMISSION_DENIEDfamily — reuse codes, do not mint). - Read side: verify the org-overlay read path (
organizationIdForMetaRead) needs no change for this caller class; state the conclusion in the PR body either way.
Walls (restated so the card cannot drift)
- ⛔ No
allowOrgOverrideflips on any type — the org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 phantom-overlay wall and its identity pin (protocol.org-scoped-write-refused.test.ts, exactly five types) stand; the pin turning red on a flip is the pin working. - ⛔ No tier-B reach and no env-wide (
organization_id NULL) writes through the new capability, under any posture. - ⛔ Predicates stay registry-derived (
declaresOrgOverride) — no hand-written type lists. - ADR-0126 surfaces (activation ledger, §5 gate) untouched.
Acceptance
- Capability declared + seeded + documented in the capability registry's own conventions.
- Walled-posture pins in both directions: org admin holder CAN write an org-scoped view overlay; the same holder is refused (code + status asserted, ADR-0112 envelope) for a tier-B write, an env-wide write, and a foreign-org write.
single-posture behavior byte-unchanged for existing callers. - Changeset (published packages).
Clause-②: yes (new capability = widened public surface) — dispatch and review at the contract-review tier.
Metadata
Metadata
Assignees
Labels
domain:specDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
Activity
os-zhuang commentedon Aug 27, 2026
Claim: PM loop round 1 — maintainer direct-dispatch channel (2026-08-27 live PM chat, verbatim: 「按照你的建议,创建卡片,并使用项目经理技能派发完成所有相关的开放」, then 「开发」)
Session:
session_01SVYmuhHW6qZmNBqciaS7BNBranch:
claude/issue-12702-org-presentation-capabilityWorktree:
objectstack-issue-12702Domain:
domain:specFile surface:
packages/spec/src/security/(capability registry),packages/runtime/src/domains/meta.ts+ its gate tests,packages/metadata-protocol/src/(write-authorization seam),.changeset/(stop on breach; explain in the report)Container & model: M,
mode:subagent,model: claude-fable-5— clause-② content mandate (new capability widens the public surface); dispatch-gates run at15bf9e8reports no path-derived mandate and flagspackages/spec/src/**as clause-② suspect surfaceClause-②: yes
Serial constraints cleared: none — no open in-flight claim or PR touches these paths (
git log origin/main -8 -- packages/spec/src/security/capabilities.ts packages/runtime/src/domains/meta.ts packages/metadata-protocol/src/protocol.ts: latest churn is the #12508 rest package-door fix, disjoint)Generated by Claude Code
claude commentedon Aug 27, 2026
Dev claim — os-dev seat, subagent of
session_01SVYmuhHW6qZmNBqciaS7BN(dispatched by the PM claim above; same shared session, dev seat distinct from the PM loop).Branch:
claude/issue-12702-org-presentation-capability(pushed)Worktree:
objectstack-issue-12702Starting implementation now.
Generated by Claude Code
Generated by Claude Code
wip: org-scoped presentation capability (#12702)
wip: ledger notes, changeset, seeder-test index derivation (#12702)
claude commentedon Aug 27, 2026
os-dev-report
{ "issue": 12702, "status": "done", "branch": "claude/issue-12702-org-presentation-capability", "pr": "https://github.com/objectstack-ai/objectstack/pull/12705", "premise_still_valid": true, "summary": "Implemented the org-scoped presentation authoring capability manage_org_presentation exactly per the card's contract: declared scope 'org' in PLATFORM_CAPABILITIES (auto-seeded at boot; granted by no shipped set, so single-posture defaults are unchanged), plus a shared registry-derived acceptance predicate metaWriteCapabilityVerdict in metadata-core, run by every /meta item write door on both transports (REST PUT/DELETE/publish/rollback and the dispatcher PUT). It admits the new capability ONLY for a type whose registry entry declares allowOrgOverride true AND a session with an active organization — the very organization every door threads — so tier-B, env-wide and foreign-org writes stay walled; both _migrate-stored doors deliberately stay manage_metadata-only. All three PM mechanism assumptions verified true on origin/main at 15bf9e8 (manage_metadata was the only metadata-authoring capability and platform-scoped at capabilities.ts line 43; the doors and org threading are where the card said; the read path needs no change for this caller class — organizationIdForMetaRead threads ctx.tenantId for every caller with no capability input, stated in the PR body). metadata-protocol was left untouched by design: capability decisions live at the transport doors, and the protocol's existing orgScopedWriteRefusal (NOT_OVERRIDABLE 403) is the standing second wall; the five-type identity pin is untouched.", "tests": "All at branch head 51bf989. Full suites green: spec 435 files/11541 tests, runtime 196/2887, rest 151/2501, metadata-core 13/218, plugin-security 86/1561, lint 81/2300 (verdict lines: 'Tests 11541 passed', 'Tests 2887 passed', 'Tests 2501 passed', 'Tests 218 passed', 'Tests 1561 passed', 'Tests 2300 passed'; every run through scripts/pm/os-verify-lock.sh, VERDICT command-exit 0). Typecheck green for the four changed packages; measured coverage: metadata-core and rest compile the new/edited test files (tsc --listFiles hits 1 and 2), runtime's tsconfig excludes the test layer repo-wide (pre-existing debt-ratchet posture) so its new cases are exercised by vitest execution, not tsc. New pins both directions: org-active holder admitted for tier-A with the threaded organizationId asserted equal to the caller's own org on all five doors; same holder refused (code+status: REST 403 FORBIDDEN, dispatcher 403 PERMISSION_DENIED) for tier-B (object, flow), env-wide (no active org), and org-id smuggling via query/body proven inert; _migrate-stored exclusion pinned. Reverse verification with direction predicted first: both door files reverted to base 15bf9e8 keeping tests+helper, mutation confirmed on disk by verdict-anchor grep counts falling to 0 in both files; no rebuild was needed for the ablation legs because both gate suites import their door via same-package relative source paths (no package-exports/dist resolution in the loop); measured 4 red (runtime suite) + 8 red (two REST suites) exactly on the predicted admission/message cases, refusal-direction cases and the 41-test helper control stayed green; restore proven by anchor counts (7/4), git hash-object equality with the HEAD blobs, and empty git diff HEAD. spec check:generated: all 14 artifacts up to date.", "gates": "UNION RESULT: 0 failing gate(s) of 25 at 51bf989 — run after the final commit, sha cited from that run. Set = the 21 families scripts/pm/dispatch-gates.mjs derived from the real changeset (no hand-fed paths; derivation header cites objectstack-ai/objectstack at 51bf989) plus dispatch-named check:durability-log-level, check:filter-alias-parity, check:nul-bytes, check:error-code-casing. Every dispatch-named family was either in the derived set or run explicitly. Derived-new beyond the dispatch list, run green: check:authz-resolver, check:changeset-gate-self-tests, check:dispatcher-error-vocabulary, check:objectql-double-limit, check:objectui-changeset, check:pm-half-states. Per-gate exits captured before any pipe (no tail-masking).", "files_changed": [ ".changeset/org-scoped-presentation-capability.md", "packages/metadata-core/src/index.ts", "packages/metadata-core/src/meta-write-capability.test.ts", "packages/metadata-core/src/meta-write-capability.ts", "packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts", "packages/rest/src/meta-item-save-capability-gate.test.ts", "packages/rest/src/meta-write-door-capability-enumeration.test.ts", "packages/rest/src/rest-route-ledger.ts", "packages/rest/src/rest-server.ts", "packages/runtime/src/domains/meta-save-capability-gate.test.ts", "packages/runtime/src/domains/meta.ts", "packages/runtime/src/route-ledger.ts", "packages/spec/src/security/capabilities.ts" ], "deviations": [ "metadata-protocol untouched despite being in the card's expected file surface: the capability decision belongs to the transport doors (the protocol deliberately owns none), and its existing orgScopedWriteRefusal already walls org-scoped non-tier-A writes below the door; landing points named in the PR body.", "Acceptance applied to all four REST item doors (save, reset, publish, rollback), not the save door alone: they are one authoring family behind one policy, each already threads organizationIdForMetaWrite (so the same conditions bound the blast radius to the caller's own org partition), and save's draft mode makes publish the loop's second half. Flagged for contract-review attention since the card's acceptance text names only the write door.", "Tier-A refusal message text extended to name the new sanctioned path (the card requires loud refusals naming it); tier-B refusal sentences stay byte-identical, and no test pinned the previous texts. Refusal codes/statuses unchanged on both transports.", "Collateral test fix: plugin-security bootstrap-system-capabilities.test.ts hard-coded truncation-lost curated names as indices 6/7 of an 8-entry list; now derived with slice(-2) since the list has 9 entries.", "Channel: gh CLI absent in this container; repo-scoped REST probed 200 and used for all GitHub reads/writes; bulk issue reads used the zero-quota issue-page embedded-JSON channel." ], "mcp_calls": "0 — no MCP GitHub calls; claim comment, PR creation, and all read-backs went through repo-scoped REST after a green probe, and the issue/epic bodies came from the zero-quota page-payload channel", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Review of record — PM ACCEPT + clause-② contract review PASS, both for PR #12705 at head
51bf989.Contract-review chain (
needs:contract-reviewhung on both carriers this stroke, reviewed, cleared same-stroke below): eligibility per the downgrade fuse's machine reading — this seat'sexternal_metadata.last_served_model=claude-fable-5=CONTRACT_REVIEW_TIER(dispatch-seat self-review path, 2026-08-21 ruling). Contract increment read from the diff itself, not the report:PLATFORM_CAPABILITIESgainsmanage_org_presentation(scope: 'org'), granted by no shipped set —single-posture deployments unchanged by its existence; the entry's comment forbidsPLATFORM_ADMIN_ONLY_CAPABILITIESmembership. Declared AND enforced in the same PR (no paper capability).metaWriteCapabilityVerdict, metadata-core) run by all five/metaitem write doors on both transports; admits the new key only fordeclaresOrgOverride(canonicalType)AND a non-empty caller-own active org — the same value each door threads intoorganizationIdForMetaWrite, so an admitted write cannot land env-wide or in a foreign org by construction._migrate-storedon both transports staysmanage_metadata-only. Refusal envelopes unchanged (REST 403FORBIDDEN, dispatcher 403PERMISSION_DENIED); the common tier-B sentence byte-identical; org 作用域的 flow overlay 只在「本进程内发布后」绑定触发器,重启后静默失绑——冷启动两条读路径都把 organization_id 非空的行滤掉了 #6190 five-type identity pin untouched.PM checklist: draft ✓ base
main✓Fixes #12702first line, no other closure keywords ✓ scope = 13 files matching the report, deviations (metadata-core as predicate home, rest as REST-door producer, plugin-security collateral test fix) all declared with landing-point reasoning ✓ changeset present (4 published packages, minor) ✓ rejection pins assert code+status on both transports (spot-checked in the diff) ✓ reverse verification predicted-then-measured with on-disk mutation/restore proof ✓ gate union self-reported 0/25 at51bf989, to be superseded by CI job conclusions before enqueue.Landing: waits on full CI green on
51bf989(Lint & Repo Gates, Type Check · workspace, Test Core shards, Dogfood gates still in progress at review time — a converging draft is the dispatch contract's normal state). On full green: ready → merge queue, per the clause-② enqueue gate (dispatched AND reviewed atCONTRACT_REVIEW_TIER; path limbpackages/spec/src/**satisfied).Generated by Claude Code
feat(security,meta): org-scoped presentation authoring capability for…
docs(runtime): re-anchor the 20 dead tracker citations in domains/met…