Skip to content

feat(agents): schema-mirror + install flow for template agents#369

Merged
buremba merged 4 commits into
mainfrom
feat/schema-mirror-install-flow
Apr 26, 2026
Merged

feat(agents): schema-mirror + install flow for template agents#369
buremba merged 4 commits into
mainfrom
feat/schema-mirror-install-flow

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 26, 2026

Summary

  • DB migration 20260425120000_add_template_mirror_tracking.sql adds nullable tracking columns to entity-schema tables (managed_by_template_agent_id, mirrored_at, etc.) so per-user installs can be linked back to the canonical template they came from.
  • packages/owletto-backend/src/agents/install.ts implements installAgentFromTemplate: reads the canonical template, mirrors entity types / classifiers / watchers into the user's personal org, idempotent on re-install, errors loudly if a slug collides with user-authored content or a different agent's template.
  • Hardened: requires admin/owner membership in the target org; rejects mirrors that would overwrite user-authored data.

Motivation

Foundation for stacked PRs #357 (public install endpoint) → #359 (identity provisioning on install) → #362 (install manifest). Each user has a personal org (via #352), and template agents like examples/personal-finance need to mirror their schema into that org on first install.

Test plan

  • Migration applied + reverted cleanly on PGlite.
  • Integration tests in packages/owletto-backend/src/__tests__/integration/agents/install.test.ts cover the idempotent re-install path, slug-collision rejection, and the admin/owner membership check.
  • make build-packages clean.

buremba added 4 commits April 25, 2026 02:29
Adds managed_by_template_agent_id and source_template_org_id to
entity_types, entity_relationship_types, event_classifiers, and watchers.

When a user installs a template agent, the install flow mirrors that
agent's canonical entity types, relationship types, classifiers and
watcher definitions from the template org into the user's personal org.
The two new columns record the provenance of those mirrored rows so they
can be re-synced on template updates and treated as read-only by the
user-org owner.

Partial indexes only cover non-NULL rows so user-authored entity
types/relationships/classifiers/watchers stay un-indexed (the common case).
New installAgentFromTemplate(templateAgentId, targetOrganizationId, userId)
creates an agent row in the target org with template_agent_id set, then
mirrors the template org's entity_types and entity_relationship_types
into the target org tagged managed_by_template_agent_id + source_template_org_id.

Core properties:
- Idempotent. Re-running against the same target updates rather than
  creating duplicates.
- Safe. The mirror never overwrites rows the user authored directly
  (managed_by_template_agent_id IS NULL). Slug collisions with user rows
  or with a different template agent abort with a descriptive error.
- Re-syncable. resyncInstalledAgent() lets template schema evolution
  propagate to every installed instance.

Watchers and classifiers are deferred to a follow-up — watchers need the
watcher_versions + reaction_script dance, classifiers are scoped per-
entity so they interact with entity mirroring.

Paired with an integration test under __tests__/integration/agents/ that
covers: create on first install, idempotency, managed_by tagging, re-sync
propagation, refusal to install into the template's own org, refusal to
overwrite user-authored rows.
Defense in depth: previously installAgentFromTemplate trusted the caller
to have already authorised the user against the target org. The public
install route does enforce that (it derives targetOrganizationId from
the authenticated user's personal org), but any future caller could
bypass it.

Add an explicit membership lookup against the "member" table inside
installAgentFromTemplate. Refuses when:
- user has no member row in the target org
- user is a plain member (only owner/admin can install)

Adds two tests for the new error paths.
@buremba buremba merged commit 48bdb20 into main Apr 26, 2026
14 of 15 checks passed
@buremba buremba deleted the feat/schema-mirror-install-flow branch April 26, 2026 16:36
@github-actions github-actions Bot added the triage:needs-human Triage agent escalated for human review label Apr 26, 2026
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: 32cd208913

ℹ️ 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".

Comment on lines +404 to +406
UPDATE watcher_versions
SET version = ${row.version},
name = ${row.name},
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 Preserve watcher version history on resync

When a mirrored watcher already exists, this path updates the existing watcher_versions row in place instead of creating a new version row. Any historical windows that already reference that version_id will now resolve to the new prompt/schema, so past runs lose their original configuration and become semantically incorrect after a template resync. The mirror should append a new version and only move watchers.current_version_id.

Useful? React with 👍 / 👎.

Comment on lines +492 to +495
WHERE organization_id = ${targetOrgId}
AND slug = ${row.slug}
AND status = 'active'
LIMIT 1
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 Detect slug collisions with archived watchers

The lookup only considers status = 'active', but watcher slugs are globally unique per org regardless of status (idx_watchers_org_slug is not filtered by status). If a target org already has an archived watcher with the same slug (for example from a previous install), this code falls into the insert path and then fails with a unique-constraint error instead of handling the collision deterministically.

Useful? React with 👍 / 👎.

Comment on lines +808 to +812
const existingAgentId = await findExistingInstall(
tx,
params.templateAgentId,
params.targetOrganizationId
);
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 Make install upsert atomic for concurrent requests

Idempotency currently relies on a read-then-insert sequence (findExistingInstall followed by insert) without a uniqueness guard on (template_agent_id, organization_id). Two concurrent install calls can both observe no existing row and proceed, causing duplicate installs (or one request failing later on mirror-table unique conflicts) instead of consistently returning the same installed agent.

Useful? React with 👍 / 👎.

buremba added a commit that referenced this pull request Apr 26, 2026
, #359 install-half) (#372)

The install flow as built — schema-mirror clones a template's entity types /
relationship types / classifiers / watchers into each user's personal org —
was the wrong abstraction. Cross-org vocabulary (an entity in tenant org A
referencing a type defined in a public-catalog org B by FK) is the planned
direction; the mirror pipeline duplicated rows per user and added re-sync
complexity for no working installs (verified 0 rows used the mirror columns
in prod).

Removed:
- packages/owletto-backend/src/agents/install.ts (installAgentFromTemplate, resyncInstalledAgent)
- packages/owletto-backend/src/agents/install-routes.ts (POST /api/install)
- packages/owletto-backend/src/agents/install-manifest-routes.ts (GET /api/install/manifest/:slug)
- All associated integration tests
- subject-identities WhatsApp helpers (normalizePhoneE164, phoneToWhatsAppJid, linkWhatsAppToMember) + their unit tests
- db/migrations/20260425120000_add_template_mirror_tracking.sql (rolled back on prod first)
- Route registrations from src/index.ts

Kept:
- subject-identities.ts provisionMemberAndCoreIdentities — used by the signup
  hook in personal-org-provisioning.ts, orthogonal to install flow.
- #352 personal-org-on-signup, #350/#354/#355/#356 personal-finance content —
  no install dependencies.

DB state: prod migrated down via dbmate (mirror columns dropped), then
20260426120000_entities_entity_type_fk re-applied. 0 user-visible data lost.
@buremba buremba restored the feat/schema-mirror-install-flow branch May 12, 2026 00:23
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