Skip to content

feat(world-model): cross-org schema CRUD + read-side tolerance#386

Merged
buremba merged 3 commits into
mainfrom
feat/cross-org-schema-crud
Apr 27, 2026
Merged

feat(world-model): cross-org schema CRUD + read-side tolerance#386
buremba merged 3 commits into
mainfrom
feat/cross-org-schema-crud

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 26, 2026

Summary

Closes the tenant-facing surface that consumes the agent-side cross-org plumbing landed in #374/#377. Implements items #1 and #4 from the "Outstanding work" section of docs/plans/world-model.md (added in #385), and collapses items #3 and #5 to doc-only updates.

Backend (~120 LOC)

  • Item Remove Github Actions Integration and replace with Slack Bolt Application #1manage_entity_schema list and get for both entity types and relationship types now widen to (caller_org OR visibility = 'public') with tenant-first ORDER BY. Rows carry organization_slug so callers can distinguish local from cross-org. Pattern matches the entity-type resolver at packages/owletto-backend/src/utils/entity-management.ts:249-260.
  • Item feat: Replace GitHub Actions with Slack Bolt Application #4 — read-side cross-org tolerance:
    • resolve_path (intermediate + leaf): a tenant path can now traverse into a public-catalog entity that is reached via a cross-org relationship. Without this, list_links would surface a target that a follow-up navigation 404s on.
    • getEntity in entity-management.ts: widened the read; the existing JSDoc already promised "own org or public" but the WHERE clause didn't.
  • Re-keyed getEntityCountsByType / getEntityCountForType from slug to entity_type_id. Cross-org slug collisions (tenant company + catalog company) would otherwise merge counts across the two list rows.

Doc-only

  • Item Add Claude Code GitHub Workflow #3 marked as shipped — organization-dropdown.tsx already renders "Your Organizations" / "Public Organizations" with a separator. No work needed.
  • Item Issue #2: Changes from Claude #5 deferred — there's no exposed updateOrganization / visibility mutation today (verified via tools/organizations.ts). Guarding a non-existent path is dead code. The guard SQL is preserved inline so the future implementer can plug it into the eventual admin visibility-control mutation.

Out of scope (next PR)

Test plan

  • make build-packages (gateway/worker/core/sdk) clean
  • cd packages/owletto-backend && bun run typecheck clean
  • bun run typecheck (repo root) clean
  • make dev against a tenant org joined to a public catalog; call manage_entity_schema action='list' and confirm cross-org rows appear with organization_slug populated, tenant-first ORDER BY
  • resolve_path with a path ending at a public-catalog entity referenced by a tenant relationship — pre-fix returns 404, post-fix resolves
  • Dependent owletto-web PR (item Kubernetes Integration #2) verifies the new field renders correctly

buremba added 2 commits April 27, 2026 00:56
Closes the tenant-facing surface that consumes the agent-side cross-org
plumbing landed in #374/#377. Items #1, #4 from
docs/plans/world-model.md "Outstanding work"; #3, #5 collapse to doc-only.

- manage_entity_schema list/get widen to (caller_org OR visibility=public)
  with tenant-first ORDER BY; rows now carry organization_slug. Same
  pattern used in entity-management.ts:249-260 resolver.
- resolve_path widens both intermediate and leaf entity lookups so a
  tenant path can traverse into a public-catalog entity referenced via
  a cross-org relationship.
- getEntity widens the read; comment already promised "own org or public".
- Re-key entity_count helpers from slug to entity_type_id so cross-org
  slug collisions don't merge counts across rows.
- Item #3 noted as already shipped (organization-dropdown.tsx already
  splits Your Organizations / Public Organizations with a separator).
- Item #5 deferred — no exposed updateOrganization mutation today; the
  guard SQL is preserved inline for the future implementer.
Pruned classification-test-brand (id=45) from market-intelligence.
Held back the $member rows (real membership, not cruft) and the
template-seed verticals (need user call before pruning whole orgs).
@buremba buremba enabled auto-merge (squash) April 26, 2026 23:59
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a059aeb14a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

GROUP BY relationship_type_id
) rc ON rc.relationship_type_id = rt.id
WHERE rt.organization_id = $1
WHERE (rt.organization_id = $1 OR o.visibility = 'public')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope relationship-type counts to the caller org

This list query now includes public relationship types, but relationship_count is still sourced from rc which aggregates entity_relationships across all organizations by relationship_type_id. When a public type is reused by multiple tenants, callers will see cross-tenant counts instead of their own usage, which leaks other orgs’ activity and misranks types. Please constrain the count subquery to r.organization_id = $1 (or zero out counts for non-local rows).

Useful? React with 👍 / 👎.

Comment on lines +413 to +414
WHERE (e.organization_id = ${workspace.id} OR eo.visibility = 'public')
AND e.deleted_at IS NULL
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep resolve_path follow-up reads on the resolved entity org

With the widened predicate, _resolvePath can now resolve a leaf entity from a public catalog org while the owner slug is a tenant org. The later children/siblings queries in the same function still use workspace.id, so a cross-org leaf returns incorrect navigation data (typically empty children and unrelated local siblings). This is user-visible whenever a tenant path falls through to a public entity; use the resolved entity’s organization for subsequent reads.

Useful? React with 👍 / 👎.

…rg widening

Pi review of #386 flagged three real regressions introduced by the
cross-org read widening. Fixes:

- getEntity: scope total_content / active_connections / watchers_count /
  children_count by caller org. When `e` is a public-catalog row, totals
  now reflect the caller's references to it, never aggregate cross-tenant
  activity.
- resolve_path leaf: same scoping for total_content (events) and
  watchers_count.
- Exclude $member from public-catalog fallback in getEntity, resolve_path
  intermediate, and resolve_path leaf. Member-redaction uses
  ctx.memberRole (caller's workspace role), so a tenant admin/owner could
  otherwise read a public catalog's $member email by virtue of being
  admin of their own org. $member rows are per-tenant by design.
- rtHandleList relationship_count: scope by caller's organization so
  public relationship-type rows don't expose global usage volume.

Pre-existing concerns flagged in review but out of scope for this PR
(documented for follow-up): resolve_path bootstrap entity-type counts
(unscoped + missing deleted_at), schema get's slug ambiguity across
multiple public catalogs, requireRelationshipType denying list_rules on
public RTs.
@github-actions github-actions Bot disabled auto-merge April 27, 2026 01:05
@github-actions github-actions Bot added the triage:needs-human Triage agent escalated for human review label Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Triage decision: needs-human

Reasons:

  • Trusted review comment contains escalation keyword "P1"
  • Code review bot flagged a P1 issue regarding cross-tenant relationship count leakage
  • Code review bot flagged a P2 issue regarding resolve_path follow-up reads on resolved entity org

Next: Human review required for P1/P2 security and data isolation concerns before merge

@buremba buremba merged commit 1fbdd35 into main Apr 27, 2026
11 of 12 checks passed
@buremba buremba deleted the feat/cross-org-schema-crud branch April 27, 2026 01:11
buremba added a commit that referenced this pull request Apr 27, 2026
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 added a commit that referenced this pull request Apr 27, 2026
Picks up lobu-ai/owletto#26 (42b8970): cross-org public-catalog
entity types and relationship types now render in the type pickers
under a 'From public catalogs' heading with a read-only badge,
consuming the organization_slug field landed in #386.
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:needs-human Triage agent escalated for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant