Skip to content

feat(server): auto-provision default agent + watcher; add manual-trigger endpoint#824

Merged
buremba merged 2 commits into
mainfrom
feat/auto-provision-and-trigger
May 17, 2026
Merged

feat(server): auto-provision default agent + watcher; add manual-trigger endpoint#824
buremba merged 2 commits into
mainfrom
feat/auto-provision-and-trigger

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 17, 2026

Summary

Server-side half of the Mac-app onboarding flow.

  • Auto-provision a default owletto-default agent for the bootstrap org at boot, sticky against deletion via a sentinel in organization.metadata.
  • Auto-provision a daily-check-in watcher pinned to the user's Mac the first time it polls (deferred to the first `device_workers` insert so the watcher can be pinned to a real device id).
  • New endpoint `POST /api/workers/me/watchers/:watcher_id/trigger` lets the Mac app manually re-fire a watcher with strict bound-worker → `watcher.device_worker_id` enforcement.
  • `listWatchers` now projects `last_fired_at` for the Mac app's watcher list.

Spec / plan: see the conversation that opened this PR.

Pi review concerns — resolution

Concern Resolution
Deletion stickiness Sentinels (`default_agent_provisioned`, `default_watcher_provisioned`) live in `organization.metadata` and are NEVER cleared by deletion. If the user deletes the agent or watcher via the web UI, the sentinel stays and we do NOT auto-recreate. Pinned by tests `is sticky against deletion — recreate refused after sentinel set`.
Provisioning timing Agent creation runs at server boot (in `start-local.ts` after `ensureBootstrapPat`). Watcher creation is deferred to the first `/api/workers/poll` where `device_workers` row is freshly INSERTed — that's the only place we have a real `device_worker_id` to pin to.
Route whitelist The `/api/workers/*` middleware in `packages/server/src/index.ts` explicitly adds a regex match for `/api/workers/me/watchers//trigger` to the user-scoped allowlist, alongside the existing complete-watcher/auth-profiles/feeds paths. Trusted-fleet endpoints stay blocked.
Broad idempotency `createWatcherRun` already checks for an active run in `ACTIVE_RUN_STATUSES = ['pending', 'claimed', 'running']` on the same `watcher_id` and returns `{ created: false }` if found. The trigger handler propagates that as `already_queued: true` + the existing `run_id`. Test `also returns already_queued for claimed/running existing runs` pins this.
Prompt guardrail Both the default agent identity and the default watcher prompt explicitly include "If you don't have access to recent history or context, say so clearly and suggest what the user could connect or track next." — model is instructed to say "I don't have data" instead of fabricating.

Test plan

  • `make typecheck` passes
  • `make build-packages` passes
  • `integration/auth/default-provisioning.test.ts` (9 tests) green under PGlite
  • `integration/watchers/manual-trigger.test.ts` (6 tests) green under PGlite
  • `integration/watchers/automation-contract.test.ts` (17 tests, existing) still green — no regressions in the dispatcher/complete-watcher contract.

Out of scope

Summary by CodeRabbit

  • New Features

    • Mac-app bootstrap orgs auto-provision a default agent and a daily check-in watcher when a device first registers; provisioning is sticky/idempotent and won’t recreate deleted items.
    • Devices can manually trigger watcher runs (response indicates run id, status, and already_queued).
    • Watcher listings now expose last-fired timestamps.
  • Tests

    • Added integration tests covering default provisioning behavior and device-triggered watcher runs (including access, idempotency, and edge cases).

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a57c4e9f-2726-4ed6-9594-9bd46a9da691

📥 Commits

Reviewing files that changed from the base of the PR and between f3adb65 and 4b07070.

📒 Files selected for processing (1)
  • packages/server/src/start-local.ts

📝 Walkthrough

Walkthrough

Adds sentinel-based default provisioning (default agent + device-pinned daily watcher), a device-scoped manual watcher trigger endpoint, wiring to provision watchers during device registration and at startup, and integration tests for provisioning and manual triggering.

Changes

Default provisioning and watcher manual trigger

Layer / File(s) Summary
Provisioning constants and core helpers
packages/server/src/auth/default-provisioning.ts
Exported constants define default agent and watcher identities and sentinel keys; metadata read/write helpers and exported hasOrgSentinel, ensureDefaultAgent, and ensureDefaultWatcher implement sentinel-based idempotency and transactional watcher creation.
Manual trigger handler & run enqueue
packages/server/src/watchers/automation.ts, packages/server/src/worker-api.ts
enqueueWatcherRunForWatcher exported; new triggerWatcherForDevice validates device-bound PAT and watcher pinning, checks active agent assignment, and enqueues or reuses a watcher run with dispatch_source: 'manual'.
Endpoint routing and authorization
packages/server/src/index.ts
Import triggerWatcherForDevice, register POST /api/workers/me/watchers/:watcher_id/trigger, and extend the user-scoped worker allowlist to permit the watcher trigger subpath.
Device onboarding provisioning and startup wiring
packages/server/src/worker-api.ts, packages/server/src/start-local.ts, packages/server/src/tools/admin/manage_watchers.ts
On device worker creation, check org sentinel and call ensureDefaultWatcher to pin a daily watcher to the device; startup calls ensureDefaultAgent(BOOTSTRAP_ORG_ID) as a best-effort step; watcher list query now includes last_fired_at.
Provisioning integration tests
packages/server/src/__tests__/integration/auth/default-provisioning.test.ts
Tests for ensureDefaultAgent and ensureDefaultWatcher covering creation, idempotency via sentinels, deletion stickiness, agent fallback, and sentinel-only skips.
Manual trigger integration tests
packages/server/src/__tests__/integration/watchers/manual-trigger.test.ts
Tests that mint device-bound PATs, set up device-pinned watchers, and validate successful manual trigger behavior, wrong-device rejection, idempotent queuing (already_queued), next_run_at unchanged, and unknown watcher 404s.

Sequence Diagram

sequenceDiagram
  participant Device as Mac App Device
  participant Server as Lobu Server
  participant Database
  rect rgba(76, 175, 80, 0.5)
    note over Device,Database: 1. Device Registration & Default Provisioning
    Device->>Server: pollWorkerJob (device_worker registration)
    Server->>Database: INSERT device_worker (created event)
    Server->>Database: check org agent sentinel
    alt agent sentinel set
      Server->>Database: ensureDefaultWatcher(org, device_worker_id)
      Database-->>Server: daily check-in watcher pinned
    end
  end
  rect rgba(33, 150, 243, 0.5)
    note over Device,Database: 2. Manual Trigger by Device
    Device->>Server: POST /api/workers/me/watchers/:id/trigger (device-bound PAT)
    Server->>Database: verify token bound to device_worker_id
    Server->>Database: load watcher, verify org & device pin
    Server->>Database: enqueueWatcherRunForWatcher('manual')
    Database-->>Server: {run_id, already_queued}
    Server-->>Device: 200 {run_id, status, already_queued}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • lobu-ai/lobu#773: Overlaps in worker-api.ts device-worker onboarding logic and provisioning-related flow.
  • lobu-ai/lobu#814: Related device-pinned run lifecycle and run handling that the manual trigger consumes.
  • lobu-ai/lobu#811: Introduced watcher schema columns (device_worker_id, agent_kind, last_fired_at) relied upon by this change.

Suggested labels

skip-size-check

Poem

🐰 A device wakes up, the server springs to life,
Creating agents and watchers, free of strife!
Device can trigger, blessed with manual might,
Sentinels mark the path, deletion won't rewrite!
Bootstrap org rejoices, Mac apps ready tonight! 🍎

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main changes: auto-provisioning a default agent and watcher, plus adding a manual-trigger endpoint for the Mac-app onboarding flow.
Description check ✅ Passed The description provides a comprehensive summary of changes, detailed test plan with all checkboxes marked complete, and a Pi-review concerns table addressing key architectural decisions with explicit test pinning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auto-provision-and-trigger

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

…ger endpoint

Adds the server half of the Mac-app onboarding flow:

- `auth/default-provisioning.ts` provisions a default `owletto-default` agent
  for the bootstrap org at boot, and a daily-check-in watcher pinned to the
  user's Mac the first time it polls. Both writes are guarded by sentinel
  timestamps in `organization.metadata`: a deletion via the web UI does NOT
  trigger auto-recreation on the next boot/poll.
- `start-local.ts` calls `ensureDefaultAgent` after `ensureBootstrapPat`.
- `pollWorkerJob` (in `worker-api.ts`) calls `ensureDefaultWatcher` only when
  a device_workers row is freshly INSERTed AND the org has the agent
  sentinel set — defers watcher creation until the device id exists, then
  pins the watcher to that exact device via `device_worker_id`.
- New endpoint `POST /api/workers/me/watchers/:watcher_id/trigger` lets the
  Mac app re-fire a watcher on demand. Auth: bound device_worker token with
  `device_worker:run` scope. The handler enforces a strict bound-worker →
  watcher.device_worker_id match, and reuses `enqueueWatcherRunForWatcher`
  so the existing active-run lane (pending/claimed/running) gives broad
  idempotency. Manual fires do NOT advance `watchers.next_run_at`.
- The `/api/workers/*` middleware whitelist now lets the trigger path
  through for user-scoped workers; trusted-fleet/admin paths stay blocked.
- `listWatchers` projects `last_fired_at` so the Mac app's watcher list can
  surface "last fired" without a follow-up request.

Tests:
- `integration/auth/default-provisioning.test.ts` covers all sentinel
  behavior: idempotency, deletion stickiness, has_agents short-circuit,
  agent-fallback when the default agent is gone, no-agent skip-and-stamp.
- `integration/watchers/manual-trigger.test.ts` covers happy path, wrong
  device 403, idempotent re-trigger against pending/claimed/running runs,
  no next_run_at advance, 404 for unknown watcher id.

Pi review concerns addressed:
- **Deletion stickiness**: sentinels in `organization.metadata`, never
  rewritten by deletion; tests pin this.
- **Provisioning timing**: watcher creation deferred to first device
  registration so it can pin to a real device_worker_id.
- **Route whitelist**: explicit regex added to the user-scoped allowlist
  for `/api/workers/me/watchers/<id>/trigger`.
- **Broad idempotency**: any active-status run (pending / claimed /
  running) reuses the existing run_id with `already_queued: true`.
- **Prompt guardrail**: both the agent identity and watcher prompt
  explicitly instruct the model to say "I don't have access to recent
  history or context" when that's the case, instead of fabricating.
@buremba buremba force-pushed the feat/auto-provision-and-trigger branch from 5dd75f7 to f3adb65 Compare May 17, 2026 19:25
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

if (upserted[0]?.inserted && upserted[0]?.organization_id) {

P2 Badge Gate default watcher provisioning to Mac devices

When the first newly registered device in the bootstrap org is not the Mac app (for example a chrome-extension or ios worker), this block still provisions the daily check-in watcher and pins it to that device. The watcher claim path does not require any capability for watcher runs, so the non-Mac device can receive the watcher run while the later Mac device will never get the default watcher because the org sentinel is set. Please require the effective/stored platform to be macos before calling ensureDefaultWatcher.

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

1, NULL, ${'{}'}::text[],
'active', ${createdBy}, NOW(), NOW(),
${watcherId},
${params.deviceWorkerId}::uuid, 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 Unpin default watchers before deleting devices

Because this new default watcher stores a device_worker_id, deleting that device now hits the existing watchers_device_worker_id_fkey (which has no ON DELETE action). The current device delete handler only clears connections.device_worker_id before DELETE FROM device_workers, so users with the auto-provisioned watcher will get a failed device deletion until the watcher is unpinned/archived or the FK behavior is changed.

Useful? React with 👍 / 👎.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Pi review flagged `await import('./auth/default-provisioning')` violating
the project's no-dynamic-imports rule. Hoist to a top-level static import.
@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 17, 2026

Pi review summary

Manual review (pi CLI was broken — stale ctx in chatgpt-multi-account ext).

Findings

# Severity Issue Action
1 fix-before-merge await import('./auth/default-provisioning') at start-local.ts:217 violated project rule (no dynamic imports) Fixed — hoisted to static top-level import in commit 4b07070
2 follow-up Trigger endpoint idempotency race: SELECT-then-INSERT in createWatcherRun is not atomic — two rapid manual clicks within ms could both enqueue pending runs. Dispatcher claim path will only run one, but the second row leaks. Filed as follow-up: add partial unique index on runs (watcher_id) WHERE run_type='watcher' AND status IN ('pending','claimed','running')

Green (no concerns)

  • Auth/scope. Path whitelist regex ^/api/workers/me/watchers/\d+/trigger$ correctly anchored; handler reads workerId from server-bound mcpAuthInfo, not body; device-pin + org-scope checks correct.
  • Boot crash potential. ensureBootstrapPat runs before ensureDefaultAgent; agent INSERT has ON CONFLICT DO NOTHING; outer try/catch prevents boot failure.
  • No re-introduced goals references — diff is clean post-revert(server): drop goals primitive — agents are the grouping concept #823 merge.
  • Data model. device_worker_id::uuid casts match column types; nextRunAt('0 9 * * *') is future-dated, won't fire immediately on boot.
  • Concurrent provisioning. Watcher provisioning gated on per-row xmax=0 from worker upsert — atomic; agent provisioning sentinel-race is benign (ON CONFLICT).

@buremba buremba merged commit 53e9ddd into main May 17, 2026
16 of 19 checks passed
@buremba buremba deleted the feat/auto-provision-and-trigger branch May 17, 2026 19:30
buremba added a commit that referenced this pull request May 17, 2026
Pulls in #823 (revert goals primitive), #824 (auto-provision default agent
+ manual trigger), #825 (owletto submodule bump to #154). Submodule
merged origin/main into our feat/chrome-debugger-executor branch
separately; bumps the parent's submodule pointer to the merge SHA so
both the executor work and main's recent owletto changes (Mac app
LobuClient + connection-settings UI rework + drop default-form-layout)
ship together.
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.

2 participants