Skip to content

feat(client): add TypeScript SDK#1015

Merged
buremba merged 73 commits into
mainfrom
feat/client-sdk
May 23, 2026
Merged

feat(client): add TypeScript SDK#1015
buremba merged 73 commits into
mainfrom
feat/client-sdk

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 21, 2026

Summary

Two things ship together here: the consumption SDK (@lobu/client) and the authoring SDK (@lobu/sdk), and the latter fully replaces the old lobu.toml / YAML config path.

@lobu/client (consumption):

  • Hey API generated REST/SSE client + an ergonomic Lobu / AgentSession wrapper (create, send, async-iterable SSE events).
  • Wired into build, typecheck, version bump, publish, and release-please.

@lobu/sdk (authoring) — lobu.config.ts replaces TOML/YAML:

  • Declarative defineConfig / defineAgent / defineEntityType / defineRelationshipType / defineWatcher / defineConnection / defineAuthProfile / secret, plus defineConnector re-exported from @lobu/connector-sdk.
  • lobu apply / run / init / validate / seed all read the TS entrypoint (jiti loader). The entire TOML/YAML parsing path is deleted (dead code removed).
  • All 12 examples migrated to lobu.config.ts and parity-proven against the old loader.
  • lobu init --from-org <slug> bootstraps a clean re-appliable project from an existing org (the inverse of apply).
  • defineConfig({ prune: true }) deletes org-owned definitions absent from config (data/connections/agents exempt; blast-radius confirm).

Hardening (review blockers + idempotency)

A fresh full-diff review surfaced 6 blockers — all fixed and tested:

  • secret importSecretCollector now auto-registers the secret import (the MCP-oauth clientSecret path emitted secret(...) without it → uncompilable config).
  • platform secret diff — a removed config key is detected as a change; opaque-secret in-place rotation stays a documented limitation (a secret-aware compare on the server upsert is a follow-up).
  • auth-schema keysinit --from-org reads connector auth_schema.methods (env_keys fields[].key, oauth clientIdKey/clientSecretKey), not a JSON-schema .properties shape.
  • rel-type delete — sets status='archived' to vacate the (org,slug) WHERE status='active' partial unique index, so prune → re-add of the same slug no longer hits a unique violation.
  • inverse rel-type lookup — scoped to own-org-or-public, and the reciprocal back-link only writes when the caller owns the inverse (was a cross-tenant read/write).
  • skill metadatainit --from-org round-trips per-skill network.judge / judges into SKILL.md.

Two idempotency bugs found via E2E and fixed: rel-type rules churned a perpetual update (the list action omits rules → hydrate them into the diff snapshot); an omitted feed/connection display name churned (optionalNameChanged).

Validation

  • bun test packages/client/src; full CLI suite (316) and server unit (201) green; bun run typecheck + per-package tsc clean.
  • Server prune integration (9/9) against embedded Postgres (rel-type re-create after delete, inverse tenant isolation).
  • Full lifecycle E2E (lobu run auto-applies): init-from-scratch → connector + watcher + entities → apply (connector compiled + installed, all resources created) → worker booted; Slack preview claim minted; lobu init --from-org round-trip validates; 7 consecutive applies converge to noop; all 12 examples validate.

Follow-ups (non-blocking)

  • Platform opaque-secret in-place rotation needs a secret-aware compare on the server (owletto) upsert.
  • init --from-org: relationship-rule round-trip and skill-level mcpServers emission.

Summary by CodeRabbit

  • New Features

    • Added @lobu/client package with typed client, session API, REST/SSE messaging, error type, and public exports.
    • Introduced @lobu/sdk with declarative define-* helpers and secret refs; exported defineConnector from connector SDK.
    • CLI: load project from lobu.config.ts.
  • Tests

    • Added extensive Bun tests for client, CLI mapping/loader, connector helpers, and SDK utilities.
  • Chores

    • Updated scripts, packaging, TypeScript configs, release tooling, and added many example config files.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an @lobu/client TypeScript package (types, OpenAPI generator config, REST + SSE client, session and error types, public barrel, tests) and wires it into monorepo tooling. Adds CLI TypeScript-config bundling/loader and mapping utilities. Adds SDK authoring APIs and connector helper with tests and package manifests.

Changes

Client package & integration

Layer / File(s) Summary
Type contracts and package configuration
packages/client/src/types.ts, packages/client/tsconfig.json, packages/client/openapi-ts.config.ts, packages/client/package.json
Client type contracts, OpenAPI generation config, package manifest, and package tsconfig added.
REST client implementation with SSE streaming
packages/client/src/rest.ts
LobuRestClient implements createSession/sendMessage and streamEvents async generator with buffering, abort handling, and LobuApiError for API failures.
Lobu client and AgentSession wrappers
packages/client/src/errors.ts, packages/client/src/client.ts, packages/client/src/session.ts
Adds LobuApiError, Lobu class to initialize the client and create sessions, and AgentSession wrapping credentials with send/events helpers.
Public API barrel export and test suite
packages/client/src/index.ts, packages/client/src/__tests__/client.test.ts
Exports public API and adds Bun tests that verify session creation, token switching for send, and SSE event parsing/streaming.
Monorepo workspace integration
config/biome.config.json, package.json, release-please-config.json, scripts/bump-version.mjs, scripts/publish-packages.mjs, tsconfig.json
Integrates @lobu/client into root scripts/typecheck, adds TypeScript paths, release-please tracking, publish/version scripts, and biome ignore for generated output.

CLI mapping & loader

Layer / File(s) Summary
Loader tests and apply wiring
packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts, packages/cli/src/commands/_lib/apply/apply-cmd.ts
Adds tests for loading lobu.config.ts and updates apply to prefer the TypeScript config when present.
TypeScript config bundling & loader
packages/cli/src/commands/_lib/apply/desired-state.ts
Implements bundling of lobu.config.ts to a temp .mjs, dynamic import, validation, mapping to DesiredState, merging agent artifacts, local connector discovery, reaction script resolution, and temp-file cleanup.
Mapping utilities
packages/cli/src/commands/_lib/apply/map-config.ts, tests
Adds mapProjectToDesiredState and mergeAgentDirArtifacts with validation, secret-ref collection, cron/slug checks, and merge precedence rules; extensive tests added.

SDK and connector authoring

Layer / File(s) Summary
connector-sdk: defineConnector and exports
packages/connector-sdk/src/define-connector.ts, packages/connector-sdk/src/__tests__/*, packages/connector-sdk/package.json, packages/connector-sdk/src/index.ts
Adds defineConnector helper, serializable connector definition lowering, runtime subclass generation, tests, and exports subpath for ./define-connector.
@lobu/sdk authoring API
packages/sdk/src/define.ts, packages/sdk/src/secret.ts, packages/sdk/src/index.ts, packages/sdk/package.json, tests, tsconfig
Adds define* authoring helpers, secret()/isSecretRef utilities, package manifest, barrel exports, and test coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • lobu-ai/lobu#829: Related changes introducing watcher reactionScript loading and validation in the CLI TypeScript-config loader.

Suggested labels

skip-size-check

🐰 I hopped through types and generated streams,
Wrapped sessions, tokens, and API dreams.
Tests confirmed the events that flow,
A client blooms—ready to go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.50% 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 PR title 'feat(client): add TypeScript SDK' clearly and specifically describes the main change—adding a new TypeScript client package with REST/SSE capabilities.
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.
Description check ✅ Passed The PR description comprehensively covers the objectives, changes, hardening fixes, validation, and follow-ups, aligning with all required template sections.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/client-sdk

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

Comment thread packages/client/src/rest.ts Fixed
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 21, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/client/src/client.ts (1)

7-9: ⚡ Quick win

Move sessions object shape to a named interface.

The inline object type for sessions should be declared as an interface to match the repo TypeScript shape convention.

As per coding guidelines, "**/*.{ts,tsx}: Use interface for defining object shapes in TypeScript files".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/client.ts` around lines 7 - 9, The inline type for the
readonly sessions property should be extracted to a named interface: declare an
interface (e.g., SessionsAPI) that defines create: (input: CreateSessionRequest)
=> Promise<AgentSession>, then update the client class/type to use readonly
sessions: SessionsAPI; keep the existing names CreateSessionRequest and
AgentSession unchanged so callers and types remain compatible.
packages/client/src/types.ts (1)

24-43: ⚡ Quick win

Extract nested object shapes into named interfaces.

CreateSessionRequest still embeds object literal shapes (networkConfig, mcpServers value, nix). Please define these as interfaces and reference them here to align with repo TypeScript conventions and keep contracts reusable.

As per coding guidelines, "**/*.{ts,tsx}: Use interface for defining object shapes in TypeScript files".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/types.ts` around lines 24 - 43, Create named interfaces
for the embedded object shapes and reference them from CreateSessionRequest:
extract networkConfig into an interface (e.g., NetworkConfig) with
allowedDomains and deniedDomains, extract the mcpServers value shape into an
interface (e.g., McpServerConfig) and use Record<string, McpServerConfig> for
mcpServers, and extract nix into an interface (e.g., NixConfig) with flakeUrl
and packages; then update the CreateSessionRequest type to use these interfaces
instead of inline object literals (update any imports/exports as needed to
follow the repository's interface usage convention).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/package.json`:
- Line 3: Remove the manual "version" field change you added (the "version":
"9.1.1" edit) in package.json — revert that line to its previous state or remove
the manual bump so the workspace manifest remains unchanged; do not add a
chore(release) or any manual version commit, as release-please will manage
package versions for the package's package.json "version" key.

---

Nitpick comments:
In `@packages/client/src/client.ts`:
- Around line 7-9: The inline type for the readonly sessions property should be
extracted to a named interface: declare an interface (e.g., SessionsAPI) that
defines create: (input: CreateSessionRequest) => Promise<AgentSession>, then
update the client class/type to use readonly sessions: SessionsAPI; keep the
existing names CreateSessionRequest and AgentSession unchanged so callers and
types remain compatible.

In `@packages/client/src/types.ts`:
- Around line 24-43: Create named interfaces for the embedded object shapes and
reference them from CreateSessionRequest: extract networkConfig into an
interface (e.g., NetworkConfig) with allowedDomains and deniedDomains, extract
the mcpServers value shape into an interface (e.g., McpServerConfig) and use
Record<string, McpServerConfig> for mcpServers, and extract nix into an
interface (e.g., NixConfig) with flakeUrl and packages; then update the
CreateSessionRequest type to use these interfaces instead of inline object
literals (update any imports/exports as needed to follow the repository's
interface usage convention).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a59a16c-b926-4e00-bcac-b19cf9a807c6

📥 Commits

Reviewing files that changed from the base of the PR and between 176a3f1 and 7fab532.

⛔ Files ignored due to path filters (17)
  • bun.lock is excluded by !**/*.lock
  • packages/client/src/generated/client.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/client/client.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/client/index.ts is excluded by !**/generated/**
  • packages/client/src/generated/client/types.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/client/utils.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/auth.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/bodySerializer.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/params.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/pathSerializer.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/queryKeySerializer.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/serverSentEvents.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/types.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/core/utils.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/index.ts is excluded by !**/generated/**
  • packages/client/src/generated/sdk.gen.ts is excluded by !**/generated/**
  • packages/client/src/generated/types.gen.ts is excluded by !**/generated/**
📒 Files selected for processing (16)
  • config/biome.config.json
  • package.json
  • packages/client/openapi-ts.config.ts
  • packages/client/package.json
  • packages/client/src/__tests__/client.test.ts
  • packages/client/src/client.ts
  • packages/client/src/errors.ts
  • packages/client/src/index.ts
  • packages/client/src/rest.ts
  • packages/client/src/session.ts
  • packages/client/src/types.ts
  • packages/client/tsconfig.json
  • release-please-config.json
  • scripts/bump-version.mjs
  • scripts/publish-packages.mjs
  • tsconfig.json

Comment thread packages/client/package.json
buremba added 6 commits May 21, 2026 16:59
Functional sugar over ConnectorRuntime: declare a connector as a spec with
per-feed sync and per-action execute handlers (feed/action keys derived from
the record keys). Lowers to a ConnectorRuntime subclass so connector-worker's
child-runner runs it unchanged; handler closures are stripped from the
serializable definition. Unit test mirrors child-runner's findRuntimeClass
detection to prove a bundled default export is picked up unchanged.
…ector

Adds an optional authenticate(ctx) hook to the functional spec, lowered to
ConnectorRuntime.authenticate. When omitted, the connector inherits the base
behavior (throws). Closes the gap where interactive-auth connectors required
the class form, making defineConnector feature-complete vs the class.
Scaffolds @lobu/sdk (Apache-2.0): defineConfig/defineAgent/defineEntityType/
defineRelationshipType/defineWatcher/defineConnection/defineAuthProfile +
secret(), and re-exports defineConnector + TypeBox Type from connector-sdk so a
project imports its whole authoring surface from one package. Producers are pure
branded data with typed handles (EntityType -> relationship rules, Agent ->
watcher); the CLI loader (next slice) maps them to DesiredState, which stays
CLI-private per the apply-IR boundary. Excluded from root tsconfig like the
other workspace packages (own tsconfig); wired into build:packages + root test.
Adds mapProjectToDesiredState — the single place that translates the public
@lobu/sdk authoring objects (defineConfig default export) into the apply-private
DesiredState IR. Maps agents (providers -> installedProviders/modelSelection,
network, resolved provider keys), entity/relationship types (typed handles ->
slugs), watchers (agent handle -> id, sources record -> array, notification),
and connections/auth profiles (connector class -> key, secret() -> $VAR +
required-secrets). The esbuild entrypoint loader + apply wiring follow next.
Adds loadDesiredStateFromConfig: esbuild-bundles lobu.config.ts (relative
imports inlined; node_modules externalized so @lobu/sdk + @lobu/connector-sdk
resolve from the project), imports the bundle to read the defineConfig() default
export, and maps it via mapProjectToDesiredState. Dynamic imports are
allow-listed in desired-state.ts (esbuild loaded lazily; bundle imported by
URL). E2E test bundles a real fixture config end-to-end. Not yet wired into the
apply command (next).
lobu apply now loads DesiredState from the TypeScript entrypoint when a
lobu.config.ts is present, falling back to lobu.toml otherwise. Downstream
apply logic (required-secrets gate, org resolution, diff, mutations) is
source-agnostic — it operates on DesiredState.
@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 21, 2026

bug_free 62, simplicity 68, slop 8, bugs 1, 0 blockers

Typecheck/unit passed. [env] Integration failed before cases because DATABASE_URL pointed at database "postgres"; harness refused destructive setup. Explored CLI: help and examples/atlas validate exited 0. Probed mapper and found --only still validates/collects skipped families.

Suggested fixes

File Line Change
packages/cli/src/commands/_lib/apply/map-config.ts 675 Respect --only before mapping skipped families: set agents to [] for only === "memory" and entityTypes/relationshipTypes/watchers to [] for only === "agents", so skipped-family secrets and validation cannot block the apply.
packages/cli/src/commands/_lib/apply/__tests__/map-config.test.ts 375 Add tests that --only memory does not collect agent provider/platform secrets, and --only agents does not validate watcher schedules or memory-only definitions.
Full verdict JSON
{
  "bug_free_confidence": 62,
  "bugs": 1,
  "slop": 8,
  "simplicity": 68,
  "blockers": [],
  "change_type": "feat",
  "behavior_change_risk": "high",
  "tests_adequate": false,
  "suggested_fixes": [
    {
      "file": "packages/cli/src/commands/_lib/apply/map-config.ts",
      "line": 675,
      "change": "Respect --only before mapping skipped families: set agents to [] for only === \"memory\" and entityTypes/relationshipTypes/watchers to [] for only === \"agents\", so skipped-family secrets and validation cannot block the apply."
    },
    {
      "file": "packages/cli/src/commands/_lib/apply/__tests__/map-config.test.ts",
      "line": 375,
      "change": "Add tests that --only memory does not collect agent provider/platform secrets, and --only agents does not validate watcher schedules or memory-only definitions."
    }
  ],
  "notes": "Typecheck/unit passed. [env] Integration failed before cases because DATABASE_URL pointed at database \"postgres\"; harness refused destructive setup. Explored CLI: help and examples/atlas validate exited 0. Probed mapper and found --only still validates/collects skipped families.",
  "categories": {
    "src": 10803,
    "tests": 6279,
    "docs": 145,
    "config": 8467,
    "deps": 221,
    "migrations": 20,
    "ci": 56,
    "generated": 4445
  }
}

Local review gate — branch protection can require the pi-review commit status. See docs/REVIEW_SCHEMA.md.

buremba added 6 commits May 21, 2026 17:45
- BLOCKER (#976): @lobu/sdk re-exported defineConnector/Type through
  connector-sdk's barrel, which flakily fails under bun's ESM linker and broke
  the cli unit suite. Source Type/Static from @sinclair/typebox directly and
  deep-import defineConnector via a new connector-sdk '/define-connector'
  subpath export (bypasses the barrel). Full cli suite now 320 pass, 0 fail.
- Thread --only into the TS loader/mapper so 'apply --only agents' doesn't
  demand connector secrets (matches the TOML loader).
- Port the TOML structural validations into the mapper: connection/auth-profile
  slug patterns, cron schedules, duplicate feed keys, and forbidding credentials
  on interactive (oauth_account/browser_session) auth kinds — fail loud in the
  CLI before any remote mutation.
- Register @lobu/sdk in release-please-config, bump-version, and
  publish-packages so it versions and publishes.

Deferred (task #7): local defineConnector definitions authored in lobu.config.ts
are not yet uploaded (connectors.definitions stays []); needs connector
file-discovery wired into the TS loader.
…arity)

cronError now rejects schedules firing more than once a minute, matching the
TOML loader + server validation, so a sub-minute cron fails loud in the CLI
instead of at server mutation. Closes the last codex parity gap (slice 3 now
93%).
…ig.ts

The TypeScript apply path (loadDesiredStateFromConfig) always set
connectors.definitions to []. A connector authored in ./connectors and
referenced by a connection resolved to a key but its source was never
uploaded, so apply failed "not installed".

Discover ./connectors/*.connector.ts (non-recursive, sorted, files only)
and ship each as a DesiredConnectorDefinition{ key: null, sourcePath,
sourceCode } — the same key-null contract the YAML loader uses for
auto-discovered connector files. apply-cmd then compiles each sourcePath on
the CLI and uploads it via install_connector; the server resolves the real
key. Skipped under --only agents|memory, matching the mapper.

Key resolution is intentionally deferred to the server (no eager
compile/instantiate at load time, which would force esbuild + installed
deps + module side effects on every load, including --dry-run).
…onfig

The TS authoring path (defineAgent + mapProjectToDesiredState) only covered
providers + network allowed/denied + the memory schema, while the TOML
loader's buildAgentSettings lifts much more. Applying a migrated example
would silently drop config (office-bot's egress judges, org metadata, etc.),
blocking the TOML-deletion slice. Close that gap.

defineAgent gains: network.judged + judges, egress, tools
(preApproved/allowed/denied/strict), guardrails, nixPackages, mcpServers
(typed type/authScope unions), plus preview and dir (consumed by a later
`lobu run`/loader slice — not mapped into cloud settings; a test guards the
preview non-leak). defineConfig gains orgName/orgDescription/organizationId.

mapProjectToDesiredState now produces the same AgentSettings shape as
buildAgentSettings: judgedDomains deduped by domain (last-wins), egressConfig,
preApprovedTools + toolsConfig, guardrails, nixConfig, mcpServers (with the
same loose cast for authScope/oauth), and collects $VAR/secret() refs from mcp
headers/env + oauth clientId/clientSecret into the apply secrets gate. Org
metadata maps into DesiredState.memory.

Deferred to follow-ups: agent-dir SOUL/IDENTITY/USER markdown + skills/
loading (file IO + skill merge), platforms, dev.ts preview/dir wiring, example
migration, TOML deletion.
buildAgentSettings lifts SOUL.md/IDENTITY.md/USER.md prompt markdown and local
skills (project ./skills + per-agent <dir>/skills, with their network/nix/mcp
declarations merged into agent settings); the TS config path did neither, so a
migrated agent would lose its prompt files and skills.

loadDesiredStateFromConfig now reads each agent's dir (defaulting to
./agents/<id>, overridable via defineAgent.dir) using the existing
readMarkdown/loadSkillFiles/buildLocalSkills helpers, and merges the result via
a new pure mergeAgentDirArtifacts(settings, markdown, skills). The mapper stays
file-IO-free (unit-testable); the merge mirrors buildAgentSettings exactly:
agent-level network/nix/mcp first, skills on top — allowed/denied/nix
unioned+deduped (skill "*" dropped), judged-domains + judges agent-wins on
conflict, skill MCP servers add only ids the agent didn't define.
These two DesiredWatcher fields are used by example watchers (8 and 2 uses
respectively) but had no defineWatcher equivalent, so a migrated watcher would
drop them. Add the SDK fields + mapper passthrough. The executable `reaction`
(reaction_script) field still lands in the reactions slice.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tsconfig.json (1)

31-45: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing TypeScript path mappings for @lobu/sdk.

The packages/sdk directory is added to build, test, publish, and version-bump scripts across this PR, and Line 56 excludes it from the root tsconfig (correct, since it has its own config). However, the paths section is missing mappings for @lobu/sdk and @lobu/sdk/*.

Without these mappings, imports like import { defineAgent } from '@lobu/sdk' in other packages will fail TypeScript resolution.

🔧 Proposed fix

Add the following entries to the paths object:

       "`@lobu/client`": ["packages/client/src/index.ts"],
       "`@lobu/client/`*": ["packages/client/src/*"],
+      "`@lobu/sdk`": ["packages/sdk/src/index.ts"],
+      "`@lobu/sdk/`*": ["packages/sdk/src/*"],
       "`@lobu/openclaw-plugin`": ["packages/openclaw-plugin/src/index.ts"],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tsconfig.json` around lines 31 - 45, The root tsconfig's "paths" is missing
mappings for the new package identifiers `@lobu/sdk` and `@lobu/sdk/`*, so
TypeScript in other packages cannot resolve imports like import { defineAgent }
from '`@lobu/sdk`'; add two entries to the existing "paths" object that map
"`@lobu/sdk`" to the SDK package entry (packages/sdk/src/index.ts) and
"`@lobu/sdk/`*" to the SDK source files (packages/sdk/src/*) so imports across the
monorepo resolve correctly.
package.json (1)

26-26: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SDK typecheck to typecheck:packages script.

The build:packages script (Line 22) includes SDK, but this typecheck script omits cd ../sdk && bun run typecheck. This inconsistency means SDK type errors won't be caught by the packages typecheck workflow.

🔍 Proposed fix
-    "typecheck:packages": "cd packages/core && bun run typecheck && cd ../client && bun run typecheck && cd ../agent-worker && bun run typecheck",
+    "typecheck:packages": "cd packages/core && bun run typecheck && cd ../client && bun run typecheck && cd ../sdk && bun run typecheck && cd ../agent-worker && bun run typecheck",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 26, The typecheck workflow defined by the
"typecheck:packages" script is missing the SDK package; update the
"typecheck:packages" script to include the SDK step (i.e., run "cd ../sdk && bun
run typecheck") in the same sequential fashion as "build:packages" so SDK type
errors are covered; locate the "typecheck:packages" script in package.json and
insert the SDK command into the sequence.
🧹 Nitpick comments (1)
packages/connector-sdk/src/define-connector.ts (1)

75-75: ⚡ Quick win

Use interface for the constructor shape export.

Line 75 should use an interface instead of a type alias to match the TS object-shape rule.

Suggested change
-export type ConnectorClass = new () => ConnectorRuntime;
+export interface ConnectorClass {
+  new (): ConnectorRuntime;
+}
As per coding guidelines, "`**/*.ts`: Use `interface` for defining object shapes; avoid `type` aliases where interfaces would be clearer".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connector-sdk/src/define-connector.ts` at line 75, Replace the
exported constructor type alias with an interface: change the declaration of
ConnectorClass from a type alias to an exported interface that describes the
constructor signature (i.e., export interface ConnectorClass { new():
ConnectorRuntime }), keeping the same constructor shape and retaining the
ConnectorRuntime reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 19: The test:coverage npm script is missing packages/sdk/src while the
test script includes it; update the "test:coverage" script entry so it matches
the "test" script by adding packages/sdk/src to its argument list (modify the
"test:coverage" script in package.json to include packages/sdk/src alongside the
other package paths).

In `@packages/connector-sdk/src/__tests__/define-connector.test.ts`:
- Around line 83-93: The test cases that assert promise rejections are not
awaiting the Jest/Bun promise matcher, so failures can be missed; update the
test functions (e.g., the "sync throws for an unknown feed" test that calls new
Github().sync({...}) and the other similar rejection test around lines 155-159)
to be async and change the assertions to await
expect(<promise>).rejects.toThrow(...) so Bun's bun:test promise matchers run
reliably.

In `@packages/sdk/src/define.ts`:
- Around line 36-38: The factory currently spreads caller-provided config after
the discriminant (return { kind: "entityType", ...config }), allowing callers to
override kind at runtime; change the object shape to spread config first and set
kind last (return { ...config, kind: "entityType" }) in defineEntityType and
apply the same pattern to all other factory functions that set a discriminant
(the other factory returns referenced in the comment) so the discriminant cannot
be overridden by caller-provided properties.

---

Outside diff comments:
In `@package.json`:
- Line 26: The typecheck workflow defined by the "typecheck:packages" script is
missing the SDK package; update the "typecheck:packages" script to include the
SDK step (i.e., run "cd ../sdk && bun run typecheck") in the same sequential
fashion as "build:packages" so SDK type errors are covered; locate the
"typecheck:packages" script in package.json and insert the SDK command into the
sequence.

In `@tsconfig.json`:
- Around line 31-45: The root tsconfig's "paths" is missing mappings for the new
package identifiers `@lobu/sdk` and `@lobu/sdk/`*, so TypeScript in other packages
cannot resolve imports like import { defineAgent } from '`@lobu/sdk`'; add two
entries to the existing "paths" object that map "`@lobu/sdk`" to the SDK package
entry (packages/sdk/src/index.ts) and "`@lobu/sdk/`*" to the SDK source files
(packages/sdk/src/*) so imports across the monorepo resolve correctly.

---

Nitpick comments:
In `@packages/connector-sdk/src/define-connector.ts`:
- Line 75: Replace the exported constructor type alias with an interface: change
the declaration of ConnectorClass from a type alias to an exported interface
that describes the constructor signature (i.e., export interface ConnectorClass
{ new(): ConnectorRuntime }), keeping the same constructor shape and retaining
the ConnectorRuntime reference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae25d823-59fe-4509-a232-20f2256e31bb

📥 Commits

Reviewing files that changed from the base of the PR and between 7fab532 and 93f8895.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • package.json
  • packages/cli/package.json
  • packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts
  • packages/cli/src/commands/_lib/apply/__tests__/map-config.test.ts
  • packages/cli/src/commands/_lib/apply/apply-cmd.ts
  • packages/cli/src/commands/_lib/apply/desired-state.ts
  • packages/cli/src/commands/_lib/apply/map-config.ts
  • packages/connector-sdk/package.json
  • packages/connector-sdk/src/__tests__/define-connector.test.ts
  • packages/connector-sdk/src/define-connector.ts
  • packages/connector-sdk/src/index.ts
  • packages/sdk/package.json
  • packages/sdk/src/__tests__/define.test.ts
  • packages/sdk/src/define.ts
  • packages/sdk/src/index.ts
  • packages/sdk/src/secret.ts
  • packages/sdk/tsconfig.json
  • release-please-config.json
  • scripts/bump-version.mjs
  • scripts/publish-packages.mjs
  • tsconfig.json
✅ Files skipped from review due to trivial changes (2)
  • release-please-config.json
  • packages/cli/package.json

Comment thread package.json Outdated
Comment thread packages/connector-sdk/src/__tests__/define-connector.test.ts Outdated
Comment on lines +36 to +38
export function defineEntityType(config: Omit<EntityType, "kind">): EntityType {
return { kind: "entityType", ...config };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent runtime override of discriminant kind.

Line 37 pattern ({ kind: "...", ...config }) lets callers override kind at runtime, which can corrupt downstream mapping. Set kind last in each factory.

Suggested fix
 export function defineEntityType(config: Omit<EntityType, "kind">): EntityType {
-  return { kind: "entityType", ...config };
+  return { ...config, kind: "entityType" };
 }
@@
 export function defineRelationshipType(
   config: Omit<RelationshipType, "kind">
 ): RelationshipType {
-  return { kind: "relationshipType", ...config };
+  return { ...config, kind: "relationshipType" };
 }
@@
 export function defineAuthProfile(
   config: Omit<AuthProfile, "kind">
 ): AuthProfile {
-  return { kind: "authProfile", ...config };
+  return { ...config, kind: "authProfile" };
 }
@@
 export function defineConnection(config: Omit<Connection, "kind">): Connection {
-  return { kind: "connection", ...config };
+  return { ...config, kind: "connection" };
 }
@@
 export function defineWatcher(config: Omit<Watcher, "kind">): Watcher {
-  return { kind: "watcher", ...config };
+  return { ...config, kind: "watcher" };
 }
@@
 export function defineAgent(config: Omit<Agent, "kind">): Agent {
-  return { kind: "agent", ...config };
+  return { ...config, kind: "agent" };
 }
@@
 export function defineConfig(config: Omit<Project, "kind">): Project {
-  return { kind: "project", ...config };
+  return { ...config, kind: "project" };
 }

Also applies to: 50-54, 81-85, 111-113, 148-150, 266-268, 292-294

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/src/define.ts` around lines 36 - 38, The factory currently
spreads caller-provided config after the discriminant (return { kind:
"entityType", ...config }), allowing callers to override kind at runtime; change
the object shape to spread config first and set kind last (return { ...config,
kind: "entityType" }) in defineEntityType and apply the same pattern to all
other factory functions that set a discriminant (the other factory returns
referenced in the comment) so the discriminant cannot be overridden by
caller-provided properties.

buremba added 3 commits May 22, 2026 00:41
defineAgent() accepted `connections` and `schema` but mapProjectToDesiredState
ignored them — a silent config drop. Connections and the memory schema are
declared at the project level (defineConfig), matching the apply model (there
is no agent-scoped association in DesiredState). Remove the dead fields rather
than leave them silently ignored; project-level remains the wired path.
The generated client vendors its fetch runtime into src/generated/client/ — no
runtime code imports the @hey-api/client-fetch npm package; it is used only by
@hey-api/openapi-ts at generation time (openapi-ts.config.ts plugin). Move it
to devDependencies so consumers of the published @lobu/client don't install it.
defineWatcher gains `reaction?: string` — a relative POSIX path to a sibling
.ts reaction script. loadDesiredStateFromConfig validates + reads it (raw
source) and attaches it to DesiredWatcher.reactionScript; apply ships it via
the existing setReactionScript and the server compiles it. Mirrors the TOML
loader's parseWatcher exactly: relative-POSIX/.ts/no-`..`/under-config-dir/
256KB checks, present-but-empty rejected (gate on absence, not truthiness),
raw-source contract. mapWatcher stays pure; the loader zips
project.watchers[i].reaction -> state.watchers[i].reactionScript.

Unblocks the 6 example watchers that use reaction_script. (The runAction /
operations typing on ReactionClient is a separate enhancement — no example
reaction calls connector actions; they only use client.knowledge.)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/_lib/apply/desired-state.ts`:
- Around line 2113-2126: The containment check for reaction scripts is platform-
and symlink-unsafe: replace the brittle startsWith(`${baseDir}/`) test by
resolving real paths and using path.relative to verify containment.
Specifically, call fs.realpathSync on baseDir and on the candidate path (abs)
before reading, then compute path.relative(realBaseDir, realAbs) and throw the
ValidationError if that relative path starts with '..' or is absolute (or equals
'' handling as needed). Keep the existing error messages (using watcherSlug and
trimmed) but use the resolved realAbs in messages and proceed to
readFileSync(realAbs) so symlink escapes are prevented; update references to
baseDir/abs in the function (variables baseDir, abs, trimmed, watcherSlug,
readFileSync) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 67ba1f42-5f64-4499-bfc5-8dd3bdeac3ca

📥 Commits

Reviewing files that changed from the base of the PR and between 2236e5a and 1b52cc6.

📒 Files selected for processing (3)
  • packages/cli/src/commands/_lib/apply/__tests__/load-config.test.ts
  • packages/cli/src/commands/_lib/apply/desired-state.ts
  • packages/sdk/src/define.ts

Comment thread packages/cli/src/commands/_lib/apply/desired-state.ts
buremba added 2 commits May 22, 2026 01:16
mapAuthProfile shipped the literal `$VAR` placeholder for env/oauth_app
credentials, whereas the TOML loader (loadConnectors) resolves them to the
real env value before pushing to the DB — so a TS-config auth profile would
write "$GITHUB_CLIENT_SECRET" instead of the secret. Add resolveCredentialValue
(mirrors loadConnectors): secret()/$VAR creds resolve against env, the ref is
still collected for the apply secrets gate. mcp oauth client_secret keeps the
literal pass-through (matching buildAgentSettings). Found migrating lobu-crm.
Migrate every example from lobu.toml + models/*.yaml + connectors/*.yaml to a
single TypeScript lobu.config.ts using the @lobu/sdk authoring API (define*).
Each was verified to produce a DesiredState byte-identical to the legacy TOML
loader (modulo object key order, installedAt timestamps, and the connector
sourceFile error-label). Agent dirs, *.connector.ts, and *.reaction.ts files
are unchanged (still file-based, referenced from the config). The old
toml/yaml files remain for now; they are removed when the TOML loader is
deleted in the next commit.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/agent-community/lobu.config.ts`:
- Around line 174-176: In the exported defineConfig block, the org property is
incorrectly set to "market" which can route state to the wrong org; update the
org value in the defineConfig export (the org property alongside orgName) to
match this example’s identity (e.g., "agent-community") so it aligns with
orgName "Agent Community" and avoids collisions with the market example.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e823162f-cb3c-4c39-a8f5-6367e2d57786

📥 Commits

Reviewing files that changed from the base of the PR and between 1b52cc6 and 9d866be.

📒 Files selected for processing (14)
  • examples/agent-community/lobu.config.ts
  • examples/atlas/lobu.config.ts
  • examples/delivery/lobu.config.ts
  • examples/ecommerce/lobu.config.ts
  • examples/finance/lobu.config.ts
  • examples/leadership/lobu.config.ts
  • examples/legal/lobu.config.ts
  • examples/lobu-crm/lobu.config.ts
  • examples/market/lobu.config.ts
  • examples/office-bot/lobu.config.ts
  • examples/personal-finance/lobu.config.ts
  • examples/sales/lobu.config.ts
  • packages/cli/src/commands/_lib/apply/__tests__/map-config.test.ts
  • packages/cli/src/commands/_lib/apply/map-config.ts

Comment thread examples/agent-community/lobu.config.ts
buremba added 3 commits May 22, 2026 01:31
…ig.ts

Extract loadProjectConfig (bundle+import lobu.config.ts → SDK Project) and route
all consumers through it / the TS loader: apply drops the lobu.toml fallback
(always loadDesiredStateFromConfig); doctor, chat, validate, and dev's
preview-bot registration read the SDK Project; dev's auto-apply gate keys on
lobu.config.ts. Drop writeMemoryOrganizationId — TS projects carry org/
organizationId in defineConfig + the .lobu/project.json link, so apply never
rewrites the config file. Update the dryrun + dev tests to lobu.config.ts
fixtures (under the worktree so the externalized @lobu/sdk import resolves).

The TOML loader (loadDesiredState/loadConnectors/parse*, config/loader,
lobu-toml-schema) is now dead and removed in the next commit.
… YAML

`lobu memory seed` read [memory] from lobu.toml and entity/relationship/watcher
schema from models/*.yaml. Migrate it onto loadDesiredStateFromConfig: org +
entity/relationship types now come from lobu.config.ts (same source apply uses,
seeding stays idempotent). Drop watcher seeding — watchers are agent-scoped and
provisioned by `lobu apply`, not the old entity-scoped models path. The ./data
instance seeding (entities + relationships) is unchanged. This removes seed's
last dependency on the TOML/YAML loader, unblocking its deletion.
With every consumer (apply, dev, doctor, chat, validate, seed) on
lobu.config.ts, the TOML/YAML loader is dead code. Remove it:

- packages/cli: config/loader.ts + config/schema.ts; the TOML functions in
  desired-state.ts (loadDesiredState/loadConfig use, collectEnvRefs,
  buildAgentSettings, buildPlatforms, parse{Entity,Relationship,Watcher}Type,
  parse*Doc, loadMemoryModels, loadConnectors, rejectUnsupportedAgentShapes,
  + their TOML-only consts). Kept the TS-path + shared helpers (readMarkdown,
  loadSkillFiles, buildLocalSkills, isRecord/asString, the connector-config
  validators, loadProjectConfig/loadDesiredStateFromConfig). Drop smol-toml.
- packages/core: lobu-toml-schema.ts + its index.ts export block (no server
  importers).
- Delete the 2 TOML-loader test files (coverage replaced by map-config.test.ts
  + load-config.test.ts) and the 2 core schema tests.
- Delete the examples' lobu.toml + models/*.yaml + connectors/*.yaml
  (lobu.config.ts + *.connector.ts + *.reaction.ts + agent dirs remain).

8339 lines removed. cli/core/server typecheck clean; cli suite green.
buremba added 9 commits May 22, 2026 16:33
Found via full lifecycle E2E (init → connector → watcher → apply → run →
init-from-org), re-applying a stable config repeatedly:

- I1 relationship-type rules churned a perpetual '~ rules' update because the
  rel-type `list` action omits rules — the diff compared desired rules against
  an always-empty remote. Hydrate remote rules (new client.listRelationshipTypeRules
  via the existing list_rules action) for the types the config declares with
  rules, so a matching set is a true noop.
- I2 an omitted optional display name (feeds, connections) churned a perpetual
  '~ name' update: stringChanged(undefined, 'Ticks') is always true. Add
  optionalNameChanged — an omitted name means 'no opinion', so the server keeps
  its derived/stored name and it doesn't diff.
- B2 follow-through: the earlier 'upsert every platform every apply' restored
  rotation but made the server restart the platform on EVERY apply (it can't
  tell the resolved secret matches the stored secret://), dropping the bot
  connection each deploy. Revert to upserting only diff-flagged platforms
  (idempotent — no restart churn); KEEP the diff removal-detection (a removed
  key is still applied). In-place opaque-secret rotation needs a secret-aware
  compare on the server upsert (owletto) — tracked as a follow-up.

Verified: 7 consecutive applies of a stable config converge to
'0 update, 9 noop' (connector re-push is by-design idempotent). CLI 316 green.
…arative rules)

Fresh full-diff pi review (bug_free 58) — 2 blockers + 2 bugs, all fixed:

- P1 write-mode requireRelationshipType did an unscoped slug lookup and threw
  'Access denied' for a slug owned only by another (private) org — an existence
  oracle. Scope the lookup to ctx.organizationId; a missing own row is now
  'not found'. A tenant can only update/delete its OWN types anyway (public
  foreign types stay referenceable-but-read-only).
- P2 relationship-type rules are now fully declarative: upsertRelationshipType
  reconciles (add_rule for missing, remove_rule by id for extras) against the
  current remote set, and the snapshot hydrates rules for EVERY config-declared
  rel-type (incl. those declared with no rules) so dropping all rules is
  detected. Was add-only → removing a rule never took effect and churned a
  perpetual 'rules changed' update.
- P3 migration archives pre-fix rel-type tombstones (deleted_at set but
  status='active') so they leave the WHERE status='active' partial unique index
  and re-create can't collide.
- P4 init-from-org platform config emission uses emitKey() so a non-identifier
  config key (e.g. hyphenated) generates valid lobu.config.ts.

Tests: prune integration 10/10 (embedded PG, +foreign-private 'not found');
CLI 316. Live E2E proved the rule reconcile: stable=noop, removal removes +
converges, change add+removes + converges.
…UL delimiter

Final pi review (bug_free 82, 0 blockers) — 1 medium bug + 1 hygiene:

- envVarFor only normalized the slug, not the suffix, so a non-identifier
  platform config key (e.g. `bot-token`) produced an invalid POSIX env-var
  name (`BOT_TELEGRAM_BOT-TOKEN`) — the .env key would be rejected and apply's
  required-secret check would never pass. Normalize both parts. Added a
  regression test (quoted key + sanitized env var + round-trip).
- The rules reconcile rule-key delimiter was an actual NUL byte, which made
  grep/rg treat client.ts as binary. Replaced with a tab (slugs never contain
  whitespace, so it's still an unambiguous composite key).

CLI 317 green; no NUL bytes remain in cli/src.
Found via live prune E2E (the gap I flagged). With prune:true, apply marked the
per-org system entity type $member 'will be deleted' (it's absent from config
and can't be declared — the server reserves $ slugs). The delete is then
refused because member rows exist, which HALTS the entire apply on first
failure — so prune:true apply failed on every run for any org with members
(i.e. every real org). If an org somehow had no member rows, it would instead
DELETE the system type and corrupt the org.

computeDiff now exempts $-prefixed entity/relationship/watcher definitions from
prune: they stay ignorable drift in both modes, never delete. Regression test
added (prune.test covers the server refusal; diff.test covers the verb).

E2E: prune now creates all defs (no halt), $member stays drift, removing
lead/knows/w-drop deletes exactly those, kept defs + $member survive, re-apply
is a clean noop.
Closes the coverage gap: unit/integration prove config MAPS correctly, but
nothing proved the whole SDK path RUNS. scripts/sdk-e2e.sh boots `lobu run`
(embedded Postgres — the linux binary ships as an @embedded-postgres optional
dep, the engine prod uses), auto-applies a prune:true fixture, and drives a
REAL agent turn through a spawned worker against a deterministic mock
OpenAI-compatible provider (scripts/sdk-e2e/, no provider key → reproducible).

Asserts (non-zero exit → red CI): auto-apply completes (not halted — guards the
$member prune-halt class), every definition is created, $member is never
pruned, the agent turn returns the mock reply via the worker→secret-proxy→
upstream path, and a stable re-apply is idempotent (0 deletes).

Wired as the CI `sdk-e2e` job (Node 22 for isolated-vm; failure fails the PR)
and `make test-e2e-sdk`. Verified locally: 5/5 assertions pass.
The sdk-e2e gate boots `lobu run`'s embedded Postgres, whose PG18 binary is
dynamically linked against ICU 60 (libicuuc.so.60). ubuntu-latest ships a newer
ICU, so initdb failed to load the shared lib. Install the 18.04-era libicu60
from the Ubuntu archive (with a security-mirror fallback) before the gate, and
ldd the initdb binary to surface any further missing libs. Prod is unaffected
(external Postgres; embedded-postgres is pruned from the app image).
…pawns

Embedded PG now boots on CI (libicu60), but the agent turn failed: the
orchestrator wraps Linux workers in `systemd-run --user --scope`, which needs
a user systemd/dbus session the CI runner doesn't have → worker exited 1.
Set LOBU_DISABLE_SYSTEMD_RUN=1 for the gate (it only talks to the loopback
mock; not testing the prod network sandbox) and install bubblewrap + enable
unprivileged userns for the worker's exec-sandbox. No-op on macOS.
Clears the recurring biome useOptionalChain warning (p.id && p.id.startsWith
→ p.id?.startsWith) on this PR's platform round-trip code.
…her-reaction gates

Embedded Postgres portability (Task 1): the @embedded-postgres PG18 binaries
are NEEDED-linked against ICU 60 with an rpath of $ORIGIN/../lib, and that lib
dir already ships libicu{uc,i18n,data}.so.60.2 — it was only missing the .so.60
SONAME symlinks the loader resolves. scripts/sdk-e2e/fix-embedded-pg-icu.mjs
creates them (idempotent, Linux-only no-op on macOS), so initdb loads its
bundled ICU with zero system deps. Drops the fragile archive .deb download from
CI; now works identically on a local Linux dev box.

Expanded gate (Task 2):
- Connector sync: a local zero-dep ./connectors/pulse.connector.ts whose sync()
  emits one event; a defineConnection wires its feed. The gate triggers an
  immediate sync via manage_feeds(trigger_feed), polls the run to completed, and
  asserts items_collected>=1 and feed event_count>=1 — proving the compiled
  connector actually RUNS and persists, not just that apply mapped it.
- Watcher reaction: a ./reactions/digest.reaction.ts that saves an assertable
  SDKE2E_REACTION_OK knowledge event. The gate triggers the watcher (asserts the
  run row is enqueued) then deterministically drives read_knowledge ->
  complete_window (the fixed-reply mock never produces the complete_window
  tool-call) so the reaction fires on the connector-emitted window, and asserts
  the side effect via query_sql.

API assertions use an mcp:admin PAT minted with lobu token create against the
local-install org. Gate stays deterministic with generous polling + clear
failures; teardown unchanged.
- client/src/rest.ts: drop the trailing-slash regex (`/\/+$/`) that tripped
  CodeQL's polynomial-ReDoS check; strip with a plain slice instead.
- cli apply desired-state.ts: reaction-path containment check used a hard-coded
  `${baseDir}/` prefix (POSIX-only) that rejects every path on Windows; use
  path.relative + isAbsolute (cross-platform).
- sdk define.ts: spread `config` before `kind` in all define* factories so a
  caller can't override the discriminant at runtime.
- examples/agent-community: org slug was "market" (wrong org); set to
  "agent-community".
- root package.json: add packages/sdk/src to test:coverage (was tracked by
  test only).
- connector-sdk define-connector test: await the .rejects assertion (was never
  asserted — non-awaited promise matcher).

Skipped (false positive): client/package.json "version" — it's a NEW package
tracked in release-please-config.json, so an initial version field is expected;
release-please adopts it on first release.
@buremba buremba force-pushed the feat/client-sdk branch from a57925d to 2e26d36 Compare May 22, 2026 17:28
@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 22, 2026

Review comments addressed (commit 2e26d360)

# Finding Resolution
CodeQL Polynomial ReDoS in client/src/rest.ts (/\/+$/) Replaced the trailing-slash regex with a plain slice — no backtracking.
CodeRabbit sdk/define.ts lets callers override discriminant kind Spread config before kind in all define* factories.
CodeRabbit desired-state.ts reaction containment uses POSIX ${baseDir}/ (breaks on Windows) Switched to path.relative + isAbsolute (cross-platform).
CodeRabbit examples/agent-community org slug = "market" Fixed to "agent-community".
CodeRabbit test:coverage omits packages/sdk/src Added it.
CodeRabbit connector-sdk define-connector test: non-awaited .rejects Made the test async + await the assertion.
CodeRabbit client/package.json manual version Skipped (false positive): @lobu/client is a new package tracked in release-please-config.json; an initial version field is required and release-please adopts it on first release.

Also added a CI gate (sdk-e2e job) that boots lobu run against embedded Postgres (self-contained ICU via SONAME symlinks, no system deps), auto-applies a prune:true fixture, runs a real agent turn through a spawned worker against a mock provider, triggers a connector sync (asserts events emitted), and a watcher reaction (asserts the side effect) — failing the PR if the end-to-end path breaks.

buremba added 6 commits May 22, 2026 18:38
…works under lobu run

getLobuServiceToken inserts an oauth_token with client_id='lobu-internal'
(FK → oauth_clients.id), but that system client is created by no migration or
signup flow. On a fresh DB — notably the embedded `lobu run` one — the FK
insert fails, the function returns null, and every watcher dispatch fails the
run with 'Failed to generate an embedded Lobu service token' (automation.ts),
i.e. watchers never fire locally. Notifications hit the same path.

Ensure the credential-less system client exists (idempotent ON CONFLICT) before
minting; it's only the FK anchor for these short-lived internal tokens, never
used in a real OAuth grant. Verified: a triggered watcher now dispatches to a
worker and the session completes (was: immediate token failure).
…-internal fix)

Now that getLobuServiceToken ensures the lobu-internal oauth_client, the watcher
trigger actually dispatches under lobu run. Strengthen the gate: assert the
trigger returns a run_id, that dispatch did NOT fail on the service token, and
that a watcher worker session started — directly guarding the dispatch fix. The
deterministic complete_window reaction-side-effect assertion stays.
…p was lossy)

emitRelationshipType already emits a rules: [...] block when the RemoteRelationshipType
carries rules, but the rel-type list action omits them, so init --from-org
always dropped every rule binding. Hydrate each type's rules via list_rules
(best-effort per type) before emitting, mirroring the apply-side hydration.
Added a test that lists WITHOUT rules + returns them from list_rules and asserts
the emitted config + reloaded DesiredState carry the rule.
…orm channels)

Full-diff pi review found the TS loader regressed two validations the old
TOML/YAML loader did, so bad config now fails MID-apply (after mutations)
instead of in the plan:
- duplicate identifiers: reject dup agent ids / entity-type + relationship-type
  keys / watcher + connection + auth-profile slugs up front (assertUniqueBy).
- platform channels: reject channels on non-slack platforms and malformed
  channel strings (must be "<teamId>/<channelId>") in the plan.
Plus a @lobu/client doc note: send/events route via baseUrl+agentId (the
server advertises same-origin URLs); the sseUrl/messagesUrl fields are surfaced
for callers needing a divergent origin (follow-up). Tests for all four
rejections; 12 examples still validate; CLI 322 green.

Not changed: platform in-place secret rotation stays a documented limitation —
opaque remote secrets can't be diffed and always-upsert restart-churns; the fix
is a secret-aware compare in the server (owletto) upsert.
Branch commit a5dc5ff accidentally reset packages/owletto from 887e862
(merge-base + origin/main) back to ancestor 06b1543, which would regress
main's pointer on merge and roll back the merged mac-app work. Advance to
current owletto/main (01a242d); clears the Submodule Drift check. No
package.json changed between the two SHAs, so the lockfile is unaffected
(verified with bun install --frozen-lockfile).
examples/atlas migrated to lobu.config.ts (TS authoring SDK). The published
@lobu/cli lags this repo and predates the TS-config loader, so the sync job
failed with "No lobu.toml found". Build packages and run the in-repo CLI bin
so the dry-run (PR) and apply --yes (main) dogfood the code in this commit.
@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 23, 2026

Review summary

Reviewed the full diff + ran make review (pi/GPT-5.5 high) and a focused read of the CLI refactor. Verdict: bug_free 62, simplicity 68, slop 8, 0 blockers. The consumption client and the define* authoring SDK are clean; the desired-state.ts → map-config.ts split is sound; the claimed idempotency fixes (rel-type rules hydration, optionalNameChanged) are present in the code.

Fixed in this push

  • Submodule regression (was failing check-drift). Commit a5dc5ffa had reset packages/owletto from 887e862 (merge-base + origin/main) back to ancestor 06b1543, which would have rolled back the merged mac-app work on merge. Restored the pointer to current owletto/main (01a242d); lockfile unaffected (no owletto package.json change; verified bun install --frozen-lockfile).
  • Atlas sync workflow (was failing apply). lobu-apply-atlas.yml ran the published bunx @lobu/cli, which predates the TS-config loader and failed with "No lobu.toml found". It now builds and runs the in-repo workspace CLI bin, so the dry-run (PR) and apply --yes (main) dogfood this commit. Verified locally: lobu validate loads examples/atlas/lobu.config.ts.

Follow-ups (non-blocking, not addressed here)

  • --only scoping (map-config.ts:675, pi finding): --only memory still collects agent secrets and --only agents still validates watcher schedules — a scoped apply can be blocked by the family it should ignore.
  • init --from-org round-trip gaps (bootstrap.ts): entity/relationship metadata and advanced watcher fields (jsonTemplate, keyingConfig, classifiers, condensation*) are not emitted, so init --from-org → apply loses them.
  • Simplification root cause: map-config.ts and bootstrap.ts independently enumerate the same field set with no shared manifest — that drift is why fields get dropped on the inverse. A shared field list would prevent it.

@mrlubos
Copy link
Copy Markdown

mrlubos commented May 24, 2026

@buremba feedback on @hey-api would be welcome!

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.

4 participants