Skip to content

feat(agents): public install endpoint for template agents#357

Merged
buremba merged 3 commits into
mainfrom
feat/install-endpoint
Apr 26, 2026
Merged

feat(agents): public install endpoint for template agents#357
buremba merged 3 commits into
mainfrom
feat/install-endpoint

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 25, 2026

Summary

`POST /api/install` takes `{ templateAgentId }` from a signed-in user and:

  1. Resolves the user's personal organization via the `personal_org_for_user_id` metadata tag written by the `user.create.after` hook (feat(auth): auto-provision personal org on user signup #352).
  2. Runs `installAgentFromTemplate()` (feat(agents): schema-mirror install flow for template agents #353), which creates the agent instance in the user's org and mirrors the template's entity_types + entity_relationship_types.
  3. Returns the new agent id, the target org slug, and a `redirectTo` path — enough for the upcoming landing page to POST and redirect without any follow-up calls.

Error paths covered by integration tests:

  • Missing `templateAgentId` → 400
  • Signed-in user has no personal org → 409 with `error="no_personal_org"`
  • Upstream install failure (template not found, self-install, user-authored collision) → 400 with the upstream message.

Stacked on

Targets `feat/schema-mirror-install-flow` (#353). Rebase onto main once #353 (and transitively #351) merge.

Follow-up (separate task)

`packages/owletto-web` submodule: the `/install/personal-finance` landing page — public route, agent overview, "Install" button that POSTs here and redirects to `redirectTo`. Two-PR ship per AGENTS.md: land the submodule PR, then the parent submodule-bump PR.

Test plan

  • Integration test covers happy path + missing-field + no-personal-org error.
  • Pre-commit Biome + tsc pass.
  • Manual: POST to /api/install with a valid template agent id while signed in; confirm agent row appears in personal org and mirrored entity types are present.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@buremba buremba force-pushed the feat/install-endpoint branch from 79cc802 to d28dd50 Compare April 25, 2026 02:36
buremba added a commit that referenced this pull request Apr 25, 2026
Closes the identity-graph gap that was blocking gateway routing — the
gateway can now resolve "which user owns this WhatsApp JID?" via a
single entity_identities lookup once these rows are populated.

New:

- packages/owletto-backend/src/auth/subject-identities.ts
  - provisionMemberAndCoreIdentities(orgId, subject) — creates the
    user's $member entity in the given org and writes
    entity_identities rows for auth_user_id and email.
  - linkWhatsAppToMember({orgId, email, rawPhone}) — normalizes the
    user's phone to E.164, derives the WhatsApp JID, and writes
    phone + wa_jid identity rows. Idempotent.
  - normalizePhoneE164 + phoneToWhatsAppJid pure helpers (unit tested).

Modified:

- packages/owletto-backend/src/auth/personal-org-provisioning.ts
  - After creating a user's personal org, call
    provisionMemberAndCoreIdentities so every signed-up user lands with
    a $member entity in their own org and discoverable identities.
    Failure is logged but doesn't roll back the org creation —
    identities are recoverable via a re-link.

- packages/owletto-backend/src/agents/install-routes.ts
  - POST /api/install accepts an optional whatsapp_phone in the body.
    On a valid phone, writes the wa_jid + phone identities to the
    user's $member. Reports whatsappError="invalid_phone" or
    "no_member" without failing the install (agent is already in;
    user can re-link later).

Tests:

- 7 unit tests for normalizePhoneE164 + phoneToWhatsAppJid (UK +44
  national, leading 0, E.164, +44 (0) trunk-prefix form, non-UK).
- Integration tests: install with whatsapp_phone writes both rows;
  install with unparseable phone returns whatsappError="invalid_phone"
  but still completes the install.

Note on stacking: this PR is based on a snapshot of feat/personal-org-
on-signup (#352) and feat/install-endpoint (#357). Both have since seen
upstream refactors (advisory locks, classifier/watcher mirroring). At
merge time, this branch will need to rebase onto their new tips —
my additions are confined to a new helper file plus one new call
site, so the conflicts should be small.
@buremba
Copy link
Copy Markdown
Member Author

buremba commented Apr 25, 2026

Heads up: per docs/plans/world-model.md (on docs/world-model-plan), this needs to be trimmed once the world-model phase 1 lands. Schema cloning is gone (#351 + #353 closed). Install becomes:

  1. Insert agent row in user's tenant org with template_entity_id pointing at the agent_template entity in a public_catalog org.
  2. Provision $member if missing (logic from feat(auth): provision $member + identities on signup and install #359).
  3. Return redirect.

No mirroring, no entity_types/entity_relationship_types touched. ~30 lines. The current implementation here can be repurposed once template-as-entity lands.

Base automatically changed from feat/schema-mirror-install-flow to main April 26, 2026 16:36
buremba added 3 commits April 26, 2026 17:37
POST /api/install takes { templateAgentId } from a signed-in user and:

  1. Resolves the user's personal organization via the
     personal_org_for_user_id metadata tag written by the
     user.create.after hook.
  2. Runs installAgentFromTemplate(), which creates the agent instance
     in the user's org and mirrors the template's entity_types +
     entity_relationship_types with managed_by_template_agent_id set.
  3. Returns the new agent id, the target org slug, and a redirectTo
     path — enough for the upcoming /install/:slug landing page to
     post and redirect without any follow-up API calls.

Error paths:
  - Missing templateAgentId → 400
  - Signed-in user has no personal org (hook failed / pre-hook user) →
    409 with error="no_personal_org"
  - installAgentFromTemplate failure (template not found, self-install,
    user-authored collision) → 400 with the upstream error message.

Integration test stubs the auth middleware and exercises the happy path,
the missing-field 400, and the missing-personal-org 409.

Pairs with the upcoming /install/personal-finance landing page in the
owletto-web submodule, which ships as a separate two-PR pair.
- Replace metadata LIKE search with `(metadata::jsonb)->>'personal_org_for_user_id' = $1`
  so a userId containing % or _ can't match unintended rows.
- Add per-user hourly rate limit (20/hour) via the standard rate-limiter
  preset to prevent abusive install loops.
@buremba buremba force-pushed the feat/install-endpoint branch from 302a0c3 to 42d58da Compare April 26, 2026 16:37
@buremba buremba merged commit 4fb42ed into main Apr 26, 2026
10 checks passed
@buremba buremba deleted the feat/install-endpoint branch April 26, 2026 16:37
buremba added a commit that referenced this pull request Apr 26, 2026
Closes the identity-graph gap that was blocking gateway routing — the
gateway can now resolve "which user owns this WhatsApp JID?" via a
single entity_identities lookup once these rows are populated.

New:

- packages/owletto-backend/src/auth/subject-identities.ts
  - provisionMemberAndCoreIdentities(orgId, subject) — creates the
    user's $member entity in the given org and writes
    entity_identities rows for auth_user_id and email.
  - linkWhatsAppToMember({orgId, email, rawPhone}) — normalizes the
    user's phone to E.164, derives the WhatsApp JID, and writes
    phone + wa_jid identity rows. Idempotent.
  - normalizePhoneE164 + phoneToWhatsAppJid pure helpers (unit tested).

Modified:

- packages/owletto-backend/src/auth/personal-org-provisioning.ts
  - After creating a user's personal org, call
    provisionMemberAndCoreIdentities so every signed-up user lands with
    a $member entity in their own org and discoverable identities.
    Failure is logged but doesn't roll back the org creation —
    identities are recoverable via a re-link.

- packages/owletto-backend/src/agents/install-routes.ts
  - POST /api/install accepts an optional whatsapp_phone in the body.
    On a valid phone, writes the wa_jid + phone identities to the
    user's $member. Reports whatsappError="invalid_phone" or
    "no_member" without failing the install (agent is already in;
    user can re-link later).

Tests:

- 7 unit tests for normalizePhoneE164 + phoneToWhatsAppJid (UK +44
  national, leading 0, E.164, +44 (0) trunk-prefix form, non-UK).
- Integration tests: install with whatsapp_phone writes both rows;
  install with unparseable phone returns whatsappError="invalid_phone"
  but still completes the install.

Note on stacking: this PR is based on a snapshot of feat/personal-org-
on-signup (#352) and feat/install-endpoint (#357). Both have since seen
upstream refactors (advisory locks, classifier/watcher mirroring). At
merge time, this branch will need to rebase onto their new tips —
my additions are confined to a new helper file plus one new call
site, so the conflicts should be small.
buremba added a commit that referenced this pull request Apr 26, 2026
…ll (#359)

Closes the identity-graph gap that was blocking gateway routing — the
gateway can now resolve "which user owns this WhatsApp JID?" via a
single entity_identities lookup once these rows are populated.

New:

- packages/owletto-backend/src/auth/subject-identities.ts
  - provisionMemberAndCoreIdentities(orgId, subject) — creates the
    user's $member entity in the given org and writes
    entity_identities rows for auth_user_id and email.
  - linkWhatsAppToMember({orgId, email, rawPhone}) — normalizes the
    user's phone to E.164, derives the WhatsApp JID, and writes
    phone + wa_jid identity rows. Idempotent.
  - normalizePhoneE164 + phoneToWhatsAppJid pure helpers (unit tested).

Modified:

- packages/owletto-backend/src/auth/personal-org-provisioning.ts
  - After creating a user's personal org, call
    provisionMemberAndCoreIdentities so every signed-up user lands with
    a $member entity in their own org and discoverable identities.
    Failure is logged but doesn't roll back the org creation —
    identities are recoverable via a re-link.

- packages/owletto-backend/src/agents/install-routes.ts
  - POST /api/install accepts an optional whatsapp_phone in the body.
    On a valid phone, writes the wa_jid + phone identities to the
    user's $member. Reports whatsappError="invalid_phone" or
    "no_member" without failing the install (agent is already in;
    user can re-link later).

Tests:

- 7 unit tests for normalizePhoneE164 + phoneToWhatsAppJid (UK +44
  national, leading 0, E.164, +44 (0) trunk-prefix form, non-UK).
- Integration tests: install with whatsapp_phone writes both rows;
  install with unparseable phone returns whatsappError="invalid_phone"
  but still completes the install.

Note on stacking: this PR is based on a snapshot of feat/personal-org-
on-signup (#352) and feat/install-endpoint (#357). Both have since seen
upstream refactors (advisory locks, classifier/watcher mirroring). At
merge time, this branch will need to rebase onto their new tips —
my additions are confined to a new helper file plus one new call
site, so the conflicts should be small.
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/install-endpoint 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant