Skip to content

feat(world-model): cross-org list_rules read mode#399

Merged
buremba merged 2 commits into
mainfrom
feat/cross-org-list-rules
Apr 27, 2026
Merged

feat(world-model): cross-org list_rules read mode#399
buremba merged 2 commits into
mainfrom
feat/cross-org-list-rules

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 27, 2026

Summary

Follow-up to #386 surfaced by Pi's review of lobu-ai/owletto-web#26.

rtHandleListRules calls requireRelationshipType, which throws "Access denied: relationship type belongs to another organization" for any RT outside the caller's org. After #386, the frontend now lists public-catalog relationship types — but their rules can't be read, so the read-only sections come up empty.

This PR adds a mode: 'read' | 'write' parameter (default 'write' to preserve existing callers) to requireRelationshipType. In 'read' mode it widens the slug lookup to (caller_org OR visibility = 'public') with tenant-first ORDER BY — same shape as the resolver in entity-management.ts:249–260.

Only rtHandleListRules opts into 'read'. Mutating handlers (add_rule, remove_rule, update, delete) keep the strict 'write' mode and continue to deny cross-org writes — that's the correct behavior because cross-org RTs belong to the catalog and aren't editable from the tenant side.

Test plan

  • bun run typecheck (owletto-backend) clean
  • After merge + the dependent owletto-web PR, hit the entity-types relationship rules tab as a tenant org joined to a public catalog; confirm the cross-org section populates with the catalog's rules.

Follow-up to #386 (Pi review): rtHandleListRules went through
requireRelationshipType, which 403s on any RT outside the caller's org.
That blocks the public-catalog UX in lobu-ai/owletto#26 from
populating the rules under cross-org rel types.

Add a 'read' mode to requireRelationshipType that widens the slug lookup
to (caller_org OR visibility=public) with tenant-first ORDER BY. Mutating
callers (add_rule, remove_rule, update, delete) keep the strict 'write'
mode and continue to deny cross-org access. Only list_rules opts into
read mode.
@buremba buremba enabled auto-merge (squash) April 27, 2026 01:45
@buremba buremba mentioned this pull request Apr 27, 2026
1 task
Copy link
Copy Markdown

@codex-approver codex-approver Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: Codex left a 👍 reaction (no suggestions).

@buremba buremba merged commit b30dc63 into main Apr 27, 2026
12 checks passed
@buremba buremba deleted the feat/cross-org-list-rules branch April 27, 2026 02:00
@github-actions github-actions Bot added the triage:auto-mergeable Triage agent enabled --auto --squash label Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Triage decision: auto-mergeable

Reasons:

  • All CI checks passing (SUCCESS/NEUTRAL/SKIPPED)
  • Approved by codex-approver[bot] on latest commit
  • No CHANGES_REQUESTED reviews
  • PR size within auto-merge limits (33 lines, 1 file vs 300 lines, 10 files)
  • Single-file backend change, no infra/submodule paths
  • No escalation keywords in review content

Next: Queued for auto-merge with squash. Will merge automatically once all checks complete and branch is up to date.

buremba added a commit that referenced this pull request Apr 27, 2026
…ng + tests

Audit follow-up to #386, #399 found three additional consumers of the
widened cross-org reads that need attention:

1. utils/schema-validation.ts getEntityTypeSchema() loaded only the
   caller's org schema. After #374 a tenant entity can carry a public
   catalog type (resolved via the schema search path) — validation then
   ran against an empty schema and silently let bad metadata through.
   Widened with the same (caller_org OR visibility=public) + tenant-
   first ORDER BY pattern as the resolver in entity-management.ts.
   Now creating an entity with a public catalog type validates against
   the catalog's metadata_schema.

2. tools/search.ts entitySelectColumns() — connection_count and
   active_connection_count subqueries were CASE-WHEN-gated on caller-
   org-equality, but the inner FROM feeds f / JOIN connections cn
   didn't restate f.organization_id = e.organization_id like the
   children/watcher_count subqueries on the same function do. Added
   the predicate for consistency: defensive belt-and-suspenders against
   any future cross-org feed.entity_ids reference.

3. tools/get_watchers.ts entity-context query — the LEFT JOIN feeds /
   current_event_records on entity_id was unscoped. requireReadAccess
   blocks cross-org callers from reaching this site today, but the
   join itself should match the entity's org regardless. Added the
   filters so the count is always entity-org-local.

Also adds packages/owletto-backend/src/__tests__/integration/entity-types/
cross-org.test.ts covering the post-#386/#399 contract:
- list returns local + cross-org rows tenant-first with organization_slug
- get resolves public catalog types and surfaces organization_slug
- $member is per-tenant: get auto-provisions in caller, never returns
  the catalog's $member
- tenant-first ordering wins on slug collisions
- list_rules works on cross-org rel types (read mode)
- add_rule still 403s on cross-org (write mode strict)
- create with cross-org type validates against the catalog's schema

Tests run against the project's standard integration test backend
(real Postgres). PGlite mode has a pre-existing auth issue affecting
all integration tests in this repo.
buremba added a commit that referenced this pull request Apr 27, 2026
…ng + tests (#407)

* fix(world-model): cross-org schema validation + defensive count scoping + tests

Audit follow-up to #386, #399 found three additional consumers of the
widened cross-org reads that need attention:

1. utils/schema-validation.ts getEntityTypeSchema() loaded only the
   caller's org schema. After #374 a tenant entity can carry a public
   catalog type (resolved via the schema search path) — validation then
   ran against an empty schema and silently let bad metadata through.
   Widened with the same (caller_org OR visibility=public) + tenant-
   first ORDER BY pattern as the resolver in entity-management.ts.
   Now creating an entity with a public catalog type validates against
   the catalog's metadata_schema.

2. tools/search.ts entitySelectColumns() — connection_count and
   active_connection_count subqueries were CASE-WHEN-gated on caller-
   org-equality, but the inner FROM feeds f / JOIN connections cn
   didn't restate f.organization_id = e.organization_id like the
   children/watcher_count subqueries on the same function do. Added
   the predicate for consistency: defensive belt-and-suspenders against
   any future cross-org feed.entity_ids reference.

3. tools/get_watchers.ts entity-context query — the LEFT JOIN feeds /
   current_event_records on entity_id was unscoped. requireReadAccess
   blocks cross-org callers from reaching this site today, but the
   join itself should match the entity's org regardless. Added the
   filters so the count is always entity-org-local.

Also adds packages/owletto-backend/src/__tests__/integration/entity-types/
cross-org.test.ts covering the post-#386/#399 contract:
- list returns local + cross-org rows tenant-first with organization_slug
- get resolves public catalog types and surfaces organization_slug
- $member is per-tenant: get auto-provisions in caller, never returns
  the catalog's $member
- tenant-first ordering wins on slug collisions
- list_rules works on cross-org rel types (read mode)
- add_rule still 403s on cross-org (write mode strict)
- create with cross-org type validates against the catalog's schema

Tests run against the project's standard integration test backend
(real Postgres). PGlite mode has a pre-existing auth issue affecting
all integration tests in this repo.

* fix(search): scope content_count subquery by entity org

Pi caught one more in entitySelectColumns(): content_count subquery
joined current_event_records by entityLinkMatchSql only, with no
ev.organization_id filter. Cross-org events that share an entity_id
or an identity-namespace match could inflate counts even though the
outer CASE WHEN guards against returning anything for cross-org rows.
Same pattern as the connection_count / active_connection_count fixes
in the parent commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:auto-mergeable Triage agent enabled --auto --squash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant