feat: Add Docker Compose deployment support with AgentManager abstraction#7
feat: Add Docker Compose deployment support with AgentManager abstraction#7buremba wants to merge 3 commits into
Conversation
…tion This implements comprehensive Docker-based local development deployment alongside existing Kubernetes production deployment. Core changes: - Created AgentManager interface for job management abstraction - Added DockerJobManager implementation using dockerode - Updated types to support both kubernetes/docker infrastructure modes - Added docker-compose.yml and development configuration - Created comprehensive local development documentation and setup scripts - Added full test suite for DockerJobManager with rate limiting and lifecycle tests The implementation maintains 100% backward compatibility while adding local development option requiring only Docker and Docker Compose. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Burak Emre Kabakcı <buremba@users.noreply.github.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude finished @buremba's task —— View job Pull Request ReviewI've completed a comprehensive review of this Docker Compose deployment PR. Overall, this is a solid implementation that successfully adds Docker support alongside the existing Kubernetes infrastructure. Here's my detailed feedback: Review Progress
✅ Strengths1. Excellent Architecture Design
2. Comprehensive Test Coverage
3. Docker Implementation Quality
|
|
@claude do changes and commit to this branch and then merge the PR |
|
Claude finished @buremba's task —— View job ✅ Changes Completed and CommittedI've successfully implemented all the security fixes and bug improvements from the code review and committed them to this branch. Task Progress
Changes Made:
🔒 PR Merge LimitationI cannot directly merge pull requests through the GitHub API due to security restrictions. To merge this PR, you'll need to:
All the identified security concerns and bugs from the review have been addressed and are ready for merge! 🚀 |
- Fix interval cleanup memory leak by storing and clearing interval reference - Remove race condition in container monitoring by immediate cleanup - Improve memory parsing with proper error handling for invalid specs - Enhance error handling in deleteJob to distinguish error types - Move workspace volume from /tmp to ./workspaces for better security - Add Docker socket security warnings in compose file 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Burak Emre Kabakcı <buremba@users.noreply.github.com>
|
Claude finished @buremba's task —— View job Pull Request ReviewI've completed a comprehensive review of this Docker Compose deployment PR. Overall, this is a high-quality implementation that successfully adds Docker support alongside the existing Kubernetes infrastructure with excellent code quality and comprehensive testing. Review Progress
✅ Strengths1. Excellent Architecture Design
2. Comprehensive Test Coverage
3. Docker Implementation Quality
4. Configuration Management
|
|
Do your changes and commit this branch. |
|
@claude |
|
Claude finished @buremba's task —— View job I've analyzed the current state of the branch and found that all the necessary changes from the code review have already been implemented and committed. Task Progress
Changes Already ImplementedAll the security fixes and bug improvements from the code review are already implemented in this branch (commit
The branch is ready for merge with all identified security concerns and bugs addressed! 🚀 |
Server (packages/server): - HOST default of 0.0.0.0 conflicted with the no-auth bind guard. The Mac runner now explicitly sets HOST=127.0.0.1 in the spawn env (#1), and server.ts also gets the same loopback bind guard so an accidental LOBU_NO_AUTH=1 in production refuses to start instead of silently bypassing auth on the public bind (#5). - ensureBootstrapPat now runs BEFORE httpServer.listen so the very first request can't 503 due to the seeding race (#3). The early-return now trusts the DB row, not the bootstrap-pat.txt file: a wiped LOBU_DATA_DIR with a leftover file used to leave no-auth permanently 503; now we re-mint the user/org/PAT (#4). Production safety guard still skips when OTHER (non-bootstrap) users exist. - getNoAuthUser pins to the BOOTSTRAP_USER_ID + BOOTSTRAP_ORG_ID pair directly, not "first owner/admin membership LIMIT 1" — eliminates the nondeterminism if bootstrap-user ever gets cross-org memberships (#7). - isLoopbackHost moved to packages/server/src/utils/loopback.ts so both start-local and server share it. Handles the full IPv4 loopback /8, ::1, [::1], and IPv4-mapped IPv6 loopback (::ffff:127.x.y.z) (#8). - New CSRF middleware in index.ts. Only fires when LOBU_NO_AUTH=1. On mutating methods (POST/PUT/PATCH/DELETE) requires: * Host header is a loopback alias (defeats DNS rebinding). * Origin or Sec-Fetch-Site says same-origin/none, OR a custom X-Lobu-Client header is present (native clients omit Origin). * Content-Type, if set, must be application/json — defeats CSRF simple-request form posts that browsers allow without preflight. This is the only protection between the no-auth bypass and any malicious site the user visits in their browser, so it MUST be on whenever no-auth is on (#6). Mac app (apps/mac/Lobu): - LocalLobuRunner: pass HOST=127.0.0.1 alongside LOBU_NO_AUTH=1, and restore spawnedThisSession tracking so adoptLocalCredentials can refuse adoption when start() adopted a pre-existing server instead of spawning one. A malicious squatter or someone else's lobu run would otherwise receive our synthesised credentials (#2). - WorkerClient sends X-Lobu-Client: menubar on every request so the new CSRF middleware accepts native client traffic that legitimately omits Origin (#6).
…779) * feat(server,mac): no-auth mode for embedded server (LOBU_NO_AUTH=1) Replaces the closed PR #777 ("lift the bootstrap PAT") with the cleaner Phase B from docs/plans/personal-mode-auth.md: server short-circuits auth when LOBU_NO_AUTH=1, attributes every request to the local user ensureBootstrapPat() seeded. The macOS menu bar spawns the runner with that env set, then sets credentials directly — no PAT to read, no verification call, no ownership tracking. Server (packages/server): - multi-tenant.ts: getNoAuthUser() loads the bootstrap-user + their personal org once and caches; resolveAuth() short-circuits with owner-role attribution when LOBU_NO_AUTH=1. URL-supplied org slug must match the local user's org (single-org by definition). - start-local.ts: post-listen bind assertion refuses to serve on anything other than 127.0.0.1 / ::1 when LOBU_NO_AUTH=1. Surfaces a hard error early instead of silently exposing the local user's data. Mac app (apps/mac/Lobu): - LocalLobuRunner sets LOBU_NO_AUTH=1 in the spawn env alongside LOBU_DATA_DIR. - AppState.connect() — when targeting the managed runner, calls adoptLocalCredentials() with synthesised OAuthCredentials (dummy bearer; server ignores it). No PAT file, no userinfo verification, no spawnedThisSession check — none of that is needed when the server itself bypasses auth. - MenuBarContent button reads "Start" / "Connect" for managed-runner URLs. User experience: click Start once. Server spawns with no-auth env, popover transitions to signed-in within ~1 s. No browser, no code, no approval. The dummy bearer the menu bar sends is never validated; it exists only so the existing WorkerClient/Authorization scaffolding doesn't have to learn a "no header" mode. Defers (still in docs/plans/personal-mode-auth.md): - CSRF middleware on mutating routes — browser-tab exfiltration risk remains until we ship Origin / Sec-Fetch-Site / Host / Content-Type checks. Today no-auth mode trusts that the loopback bind is the only attack surface. - Per-user data dir / port for shared macOS user accounts. * fix(no-auth): address all 8 pi blockers on PR #779 Server (packages/server): - HOST default of 0.0.0.0 conflicted with the no-auth bind guard. The Mac runner now explicitly sets HOST=127.0.0.1 in the spawn env (#1), and server.ts also gets the same loopback bind guard so an accidental LOBU_NO_AUTH=1 in production refuses to start instead of silently bypassing auth on the public bind (#5). - ensureBootstrapPat now runs BEFORE httpServer.listen so the very first request can't 503 due to the seeding race (#3). The early-return now trusts the DB row, not the bootstrap-pat.txt file: a wiped LOBU_DATA_DIR with a leftover file used to leave no-auth permanently 503; now we re-mint the user/org/PAT (#4). Production safety guard still skips when OTHER (non-bootstrap) users exist. - getNoAuthUser pins to the BOOTSTRAP_USER_ID + BOOTSTRAP_ORG_ID pair directly, not "first owner/admin membership LIMIT 1" — eliminates the nondeterminism if bootstrap-user ever gets cross-org memberships (#7). - isLoopbackHost moved to packages/server/src/utils/loopback.ts so both start-local and server share it. Handles the full IPv4 loopback /8, ::1, [::1], and IPv4-mapped IPv6 loopback (::ffff:127.x.y.z) (#8). - New CSRF middleware in index.ts. Only fires when LOBU_NO_AUTH=1. On mutating methods (POST/PUT/PATCH/DELETE) requires: * Host header is a loopback alias (defeats DNS rebinding). * Origin or Sec-Fetch-Site says same-origin/none, OR a custom X-Lobu-Client header is present (native clients omit Origin). * Content-Type, if set, must be application/json — defeats CSRF simple-request form posts that browsers allow without preflight. This is the only protection between the no-auth bypass and any malicious site the user visits in their browser, so it MUST be on whenever no-auth is on (#6). Mac app (apps/mac/Lobu): - LocalLobuRunner: pass HOST=127.0.0.1 alongside LOBU_NO_AUTH=1, and restore spawnedThisSession tracking so adoptLocalCredentials can refuse adoption when start() adopted a pre-existing server instead of spawning one. A malicious squatter or someone else's lobu run would otherwise receive our synthesised credentials (#2). - WorkerClient sends X-Lobu-Client: menubar on every request so the new CSRF middleware accepts native client traffic that legitimately omits Origin (#6). * fix(no-auth): close pi round-2 gaps — startup-leak, partial-state, CSRF holes Pi's verification of the previous fixup commit caught three remaining issues: 1. AppState's init re-spawns the runner and starts polling using the persisted no-auth credentials WITHOUT checking spawnedThisSession. If a squatter happened to win :8787 on startup, the polling client would send our synthesised "noauth" bearer + X-Lobu-Client to it. Now: after startLocalLobu in init, if we didn't actually spawn the process, clear the persisted creds and stop. The user will see the connection card again and can sign in via OAuth. 2. ensureBootstrapPat checked only the user row's existence before the early-return. Partial state (user exists, org or member rows missing) would still wedge getNoAuthUser forever. Now we check all three rows together — any missing one triggers a re-mint. 3. CSRF middleware: tightened in three ways. - Missing Content-Type on a mutation is now rejected (was previously a bypass — `if (ct && ...)` skipped when ct was empty). - WorkerClient.markNotificationRead now sends Content-Type: application/json even with an empty body to satisfy the tightened check. - OAuthClient.postRawJSON and ChromeBridgeHost.mintChildToken now send X-Lobu-Client: menubar so they aren't rejected for missing Origin in no-auth mode. - Host header validation reuses the shared isLoopbackHost util (stripping port + brackets first) so the alias set is consistent with the bind-time enforcement.
- 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.
* feat(client): add TypeScript SDK
* feat(connector-sdk): add defineConnector functional authoring API
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.
* feat(connector-sdk): support optional authenticate flow in defineConnector
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.
* feat(sdk): add @lobu/sdk authoring package (define* + secret)
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.
* feat(cli): map @lobu/sdk authoring project to DesiredState
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.
* feat(cli): load DesiredState from a lobu.config.ts entrypoint
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).
* feat(cli): apply prefers lobu.config.ts over lobu.toml
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.
* fix(sdk,cli): address codex + pi review of the TS authoring path
- 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.
* fix(cli): enforce 1-minute minimum cron interval in TS config (TOML parity)
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%).
* feat(cli): ship local connectors/*.connector.ts source from lobu.config.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).
* feat(sdk,cli): express full agent settings + org metadata in the TS config
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.
* feat(cli): load agent-dir markdown + skills in the TS config path
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.
* feat(sdk,cli): map watcher reactionsGuidance + agentKind
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.
* fix(sdk): drop unwired Agent.connections + Agent.schema (pi review)
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.
* chore(client): move @hey-api/client-fetch to devDependencies (pi review)
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.
* feat(sdk,cli): watcher reaction scripts in the TS config path
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.)
* fix(cli): resolve non-interactive auth-profile credentials to env values
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.
* feat(examples): author all 12 examples in lobu.config.ts (@lobu/sdk)
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.
* refactor(cli): migrate every command off the TOML loader to lobu.config.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.
* refactor(cli): seed memory from lobu.config.ts instead of lobu.toml + 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.
* refactor: delete the dead TOML/YAML config path
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.
* docs(cli): refresh DesiredState comments for the lobu.config.ts world
* refactor(landing): source landing snippets from lobu.config.ts
gen-landing-snippets read the deleted example lobu.toml + models/*.yaml. Rework
it to slice examples/<slug>/lobu.config.ts as text (829 → 472 lines): drop the
TOML/YAML parsers + compressors, add a string-literal-aware defineX(...) slicer,
and source agentConfig/memorySchema/watcher from the config. examples list +
useCases now scan lobu.config.ts. Frontend: agentToml → agentConfig, copy
lobu.toml/YAML → lobu.config.ts/TypeScript. Landing build green.
* refactor(cli): load lobu.config.ts with jiti (the Next.js way)
Replace the hand-rolled esbuild-bundle-to-temp-file loader with jiti — the
runtime TS loader Next.js/Nuxt use for *.config.ts. It transpiles on import and
resolves the config's imports (@lobu/sdk, relative reaction/connector files)
from the project, with no bundling and no temp file written into the user's
cwd. Verified jiti produces byte-identical DesiredState to the esbuild loader
across all 12 examples. The dynamic import("jiti") stays lazy + allow-listed.
* feat(cli): migrate producers to lobu.config.ts (init, agent add, root-finder, help)
The reader side was migrated earlier; this finishes the producer side so the
init → apply flow works end to end:
- lobu init scaffolds lobu.config.ts (defineConfig/defineAgent via @lobu/sdk)
instead of lobu.toml; drops the models/ + data/ YAML scaffolding (schema lives
in the config). Round-trip tested through loadDesiredStateFromConfig.
- lobu agent add scaffolds the agent dir + prints a defineAgent snippet to paste
(no fragile mutation of the user's typed config) instead of appending TOML.
- ensure-deps-installed anchors the connector project root on lobu.config.ts.
- index.ts help text + the seed description say lobu.config.ts.
- Delete init-memory.test.ts (tested the removed TOML scaffolder); cli-ux.test.ts
asserts lobu.config.ts. (lobu export still emits YAML — its single-file design
is an open question, handled separately.)
* feat(cli): lobu init --from-org bootstraps a project from cloud; remove lobu export
Replace the YAML-era `lobu export` (partial, agent-less) with
`lobu init --from-org <slug>`: the inverse of `lobu apply`. It fetches the org's
full declared state and emits a clean, runnable project — one lean lobu.config.ts
(handle consts, real object literals, short arrays inlined, empty/default fields
omitted, resources sorted) plus the files it references (agents/<id>/{SOUL,
IDENTITY,USER}.md, skills/<name>/SKILL.md, reactions/<slug>.reaction.ts). Now
includes agents (the gap export punted on). Write-only secrets become
secret("ENV_VAR") placeholders + a .env.example; never real values. Runs after
init's empty-dir guard, so it never overwrites an existing project.
Round-trip gated: the test bootstraps stubbed cloud state, loads the generated
lobu.config.ts via loadDesiredStateFromConfig, and asserts the DesiredState
matches the input. cli 282 pass, tsc clean.
* chore(cli): remove dead YAML-model code + de-toml stale comments/strings
- memory/_lib/schema.ts: drop the now-dead model-file parsing
(parseModelYamlFile/expandModelDefinition/validateModel/expandModelSection +
the model schema types + their AutoCreateWhenRule/TypeCompiler/yaml imports);
only the ./data seed-record schema + validateDataRecord (used by `lobu memory
seed`) remain. 487 → 124 lines.
- Fix the user-facing "referenced in lobu.toml" message (render.ts) →
lobu.config.ts, and stale lobu.toml comments in apply-cmd/dev/diff.
No lobu.toml/YAML-config reference remains in cli source.
* fix(cli): scaffold @lobu/sdk + type-check lobu.config.ts (pi-review blocker)
A generated lobu.config.ts imports @lobu/sdk, but `lobu init` only added
@lobu/connector-sdk to the scaffolded package.json, and the tsconfig only
included connectors/** — so a fresh project couldn't resolve @lobu/sdk (jiti
`lobu apply` fails) or type-check its config outside this monorepo. Extract a
shared scaffoldProjectPackaging() (adds both SDK devDeps + a tsconfig that
includes lobu.config.ts/reactions/agents), and call it from BOTH `lobu init`
and `lobu init --from-org` (bootstrap wrote no package.json at all). Regression
test asserts the scaffolded package.json + tsconfig.
* feat(cli): code-managed prune — apply deletes definitions removed from lobu.config.ts
Adds a 'delete' diff verb. When the target org is code-managed
(organization.managed_by='code', opted in via 'lobu apply --manage'),
computeDiff({ codeManaged: true }) emits delete rows for entity types,
relationship types, watchers, and connector definitions that exist
remotely but are absent from the config. Data (entity/relationship
instances), connections, auth profiles, feeds, agents, and platforms are
never pruned — they stay 'drift' as before. Connectors still wired to a
surviving remote connection/auth-profile are spared. The unnamed-local-
connector guard suppresses connector prune when keys can't be matched.
Execution runs in reverse-dependency order (watcher -> relationship-type
-> entity-type -> connector) and the server refuses an entity-type delete
while instances exist, so data stays safe. A blast-radius confirm gates
applies that would delete more than 3 definitions; --dry-run never
deletes. UI-managed orgs (default) are unchanged: no delete rows, summary
omits the delete count.
Server managed_by exposure + migration land in the next commit; until
then managed_by is absent and every org stays UI-managed (no prune).
* feat(server): organization.managed_by + managed-by endpoint for code-managed prune
Slice 5b — the server half of code-managed prune (CLI half in the prior
commit). Adds:
- migration 20260522120000: organization.managed_by text NOT NULL
DEFAULT 'ui' (CHECK ui|code). The default backfills every existing org
to 'ui' so none starts prunable — the 2026-05-20 safety lesson.
- /oauth/userinfo organizations[].managed_by, the field the CLI's
listOrgs reads to decide codeManaged.
- PATCH /api/:orgSlug/organization/managed-by (owner/admin + mcp:admin,
same gate as visibility) — the one-time 'lobu apply --manage' opt-in
target. Client points setOrgManagedBy at it.
Verified against a real ephemeral embedded Postgres (PG18) in
managed-by-prune.test.ts (7 tests): migration default + CHECK constraint,
userinfo exposure, definition deletes (entity/relationship type, watcher),
and that an entity-type delete REFUSES while instances exist — so prune
can never cascade into data. CLI client wire contract pinned in
client.test.ts.
* docs: migrate all user-facing docs from lobu.toml/YAML to lobu.config.ts + @lobu/sdk
Rewrites the landing docs (getting-started, guides, reference, platforms),
the lobu/lobu-operator/lobu-builder SKILL.md files, and example/CLI READMEs
to the TypeScript authoring SDK. Renames reference/lobu-toml.md ->
reference/lobu-config.md (sidebar + api-reference link updated) and fixes the
dead /reference/lobu-toml/ route link in the hand-maintained public indexes
(index.md, llms.txt, agent-skills/index.json). Also fixes the AGENTS.md
guardrails line to defineAgent({ guardrails }). Landing build green; no
broken links. Blog posts + server-internal strings swept separately.
* chore: remove remaining lobu.toml references repo-wide
Sweeps lobu.toml out of code comments + agent-facing strings (server
gateway services/auth/guardrails/proxy, agent-worker openclaw tools,
core types/guardrails), test docstrings, the lobu init README template,
docs/SECURITY.md, .env.example, dev-native.sh, the historical docs/plans,
and the blog posts. Rewrites the broken gen-use-case-data.ts to load each
example's lobu.config.ts via jiti (was importing the removed smol-toml +
reading a lobu.toml that no longer exists) and regenerates use-case-models.ts.
Deletes the dead, unreferenced e2e-lobu-apply.sh apply harness (scaffolded
lobu.toml + models/*.yaml that apply no longer reads; superseded by the
managed-by-prune integration test). lobu.toml is now absent from the tracked
tree (CHANGELOG history excepted). core+server typecheck clean, landing build
green, network-domains unit test green.
* chore: drop dead smol-toml direct dependency
Nothing imports smol-toml anymore — its only direct user was
gen-use-case-data.ts, now rewritten to load lobu.config.ts via jiti. It
remains in the lockfile as a transitive dep of astro/just-bash/knip
(unchanged). Removes the root dependency only; bun.lock diff is one line,
owletto submodule intact, frozen-lockfile passes.
* fix(apply): defer code-managed flip until after plan confirmation; static jiti import
pi-review findings:
- apply-cmd: setOrgManagedBy flipped the org to code-managed BEFORE the plan
was rendered/confirmed, so cancelling a --manage apply left the org flipped.
Now the plan is computed as code-managed (prune shown), but the server-side
flip runs only after confirmPlan + the blast-radius confirmDeletions pass
(flipToCodeManaged), right before executePlan; dry-run never flips.
- gen-use-case-data.ts: replaced the lazy await import("jiti") (a dynamic
import not on the AGENTS.md allow-list) with a static top-level import. It's
a build-time script, so the static import is strictly correct.
* fix(prune): harden code-managed prune (multi-angle pi review findings)
Security/correctness fixes from focused pi reviews of the prune feature:
- HIGH: relationship-type delete now refuses while relationship instances
exist (server, mirrors entity-type) — prune can no longer orphan live
relationship data under a deleted definition. Regression-tested.
- HIGH: apply resolves the org strictly by the slug it mutates and hard-stops
if a config-pinned organizationId doesn't match that slug's org id. Removes
the id-fallback that could read provenance from / target the wrong org (and
would 404 mid-apply anyway since the client uses the slug in every URL).
- MED: a code-managed apply now fetches connector definitions even when the
config declares no connectors, so prune can delete the last connector
removed from config.
- MED: a fresh project whose deps aren't installed now gets a
clear 'run bun install' message instead of a raw @lobu/sdk resolution error.
Reviews confirmed: managed_by is DB-backed (no stale per-pod cache), the
migration backfills 'ui' safely + applies idempotently, and no runtime code
reads the deleted lobu.toml.
Known limitations (documented, not data-unsafe): prune is non-atomic — a
mid-batch refusal (instances exist) halts after earlier intended deletes, but
re-running is idempotent and the server refuses every instance-bearing delete,
so data is never lost. Concurrent destructive applies on a single org aren't
serialized (no apply lock) — operators should not run parallel --manage applies
on the same org. Stale 'lobu.toml' UI strings remain in the owletto submodule
(separate repo/PR). --yes intentionally bypasses the blast-radius confirm (CI).
* fix(cli): persist entity-type properties via metadata_schema (idempotent apply)
lobu apply (and lobu memory seed) sent entity-type `properties`/`required` as
top-level keys, but manage_entity_schema only reads `metadata_schema`. The
schema was silently dropped on every create/update, so the stored schema stayed
empty and every subsequent apply re-reported a `properties` update — apply never
reached a clean noop. Fold the flat fields into a `metadata_schema` JSON Schema
on write, and hoist them back out of `metadata_schema` on read so the diff
compares like for like.
* test(cli): init-from-org mock returns server's metadata_schema shape
The listEntityTypes mock returned flat top-level properties/required, which the
real manage_entity_schema list action never emits — it returns metadata_schema.
Align the fixture with the real server contract now that the client hoists
properties/required out of metadata_schema (see the entity-type persist fix).
* fix(prune): never prune public entity/relationship types owned by another org
Confirmation pi review found the last prune-completeness bug: the entity-type
and relationship-type list endpoints return this org's types PLUS public types
from other orgs (manage_entity_schema: `organization_id = $1 OR
o.visibility = 'public'`). computeDiff treated every remote type absent from
desired as a code-managed delete, so a prune could plan deletes for another
org's public types — the server refuses them, but it wrongly halts a legit
apply.
Fix: carry organization_id through listEntityTypes (preserved across the
metadata_schema hoist) + listRelationshipTypes, pass the target org id into
computeDiff, and emit drift/delete only for org-owned remote types. Unit-tested
(foreign-org public types are never pruned; the org's own removed types still
are). pi confirmed the other four fixes correct with no data-deletion paths.
* fix(prune): match entity/relationship types against the org's own definitions only
Final ship-readiness pi caught a matching-shadow bug adjacent to the prior
prune fix: the slug->row Maps were built from the full remote list, and since
the server returns the org's rows first then public rows from other orgs, a
Map kept the LAST (foreign public) entry on a slug collision — so matching
could diff desired against a foreign public type (false noop/update) even
though prune was already org-scoped.
Fix: filter entity/relationship types to org-owned ONCE and use that list for
both matching and prune, so a foreign public type can never shadow the org's
own definition. Unit-tested: a same-slug foreign public type no longer shadows
the org's own (stays noop) and is never pruned.
* fix(server): org-scope relationship-type write resolution + create dedup
Final pi sweep: requireRelationshipType (write mode) resolved by global slug
with no org preference, so when an org owned a rel-type AND a public type from
another org shared the slug, LIMIT 1 could grab the foreign row and the
access-denied guard then blocked the org from updating/deleting its OWN type
(and broke code-managed prune). Now tenant-first ordered, mirroring read mode.
Also org-scoped rtHandleCreate's duplicate check (the unique index is
(organization_id, slug), and entity-type create was already org-scoped) so a
same-slug foreign public type can't block this org's create. Not a cross-org
deletion path (the access-denied guard held) — a correctness/availability fix.
Cross-org integration test added (9 prune tests, 7 entity-schema tests green).
* fix: init-from-org auth-profile safety + complete rel-type inverse org-scoping
Final convergence pi findings on this PR's surface:
- init-from-org (bootstrap.ts): an auth profile with no connector_key was
emitted as `connector: null`, which crashed the next `lobu apply`
(connectorKey(null)). Now skipped with a surfaced warning. Also flagged the
credential placeholders with a TODO — keys must be renamed to the connector's
auth-schema fields (values are write-only, unrecoverable on bootstrap).
- manage_entity_schema: the inverse_type_slug lookups in relationship-type
create/update resolved by global slug; made them tenant-first (ORDER BY
org-owned), completing the org-scoping hardening of the rel-type handlers
this PR already touches.
Out of scope (pre-existing, NOT exercised by this PR — documented for a
separate hardening pass): manage_feeds delete_feed cancels runs before the
ownership check (prune never deletes feeds). CLI 296 tests + 16 schema/prune
integration tests green; server typecheck clean.
* feat(sdk): restore config-authored chat platforms (defineAgent platforms)
Final pi review caught that the migration silently dropped config-authored
platforms: the SDK had no platforms field, mapAgent hardcoded platforms:[],
yet 'lobu init --platform' still prompted for one + wrote bot-token env
placeholders that 'apply' never consumed (on main, lobu.toml emitted
[[agents.platforms]] which apply created). Per the user, restore them:
- @lobu/sdk: defineAgent({ platforms: [{ type, name?, config, channels? }] }).
- mapAgent: maps platforms to DesiredPlatform with a deterministic stable id
(agentId-type[-name]) so apply matches (noop) instead of recreating; config
/secret() values kept as placeholders + collected into requiredSecrets.
- lobu init: emits a platforms block from the chosen --platform + its config.
- lobu init --from-org: round-trips live platform bindings back to config
(strips the route's embedded key; config -> secret() refs).
Unit-tested (stable-id derivation + secret collection + literal passthrough);
full CLI suite 297 green; SDK + CLI typecheck clean.
* fix(cli): platform config diff ignores redacted/secret-ref values (idempotent apply)
A config-authored chat platform restarted on EVERY `lobu apply`: the diff
compared the desired config (`botToken: "$TELEGRAM_BOT_TOKEN"`) against the GET
round-trip, where the server redacts the secret (`"***oken"`) and rewrites the
$VAR into an internal `secret://…` reference. Neither round-trips, so the
config never matched and the platform was needlessly updated + restarted,
dropping in-flight messages. Treat a key as unchanged when the desired value is
a $VAR placeholder and the remote value is opaque (redacted or secret://),
mirroring the auth-profile credentials rule; real (non-secret) config changes
still surface as updates.
* test(cli): platform config diff treats redacted/secret-ref values as noop
Locks the idempotent-apply behavior: a $VAR secret placeholder matching a
redacted (***) or secret:// remote value is a noop, while a real non-secret
config change still surfaces as an update.
* fix(cli): init-from-org emits secret() for redacted platform config values
Bootstrap assumed stored platform config kept $VAR placeholders, but the server
rewrites $VAR into an internal secret:// reference and the GET masks it
(***-suffixed). Neither matched the $VAR regex, so a redacted botToken was
emitted as the literal "***oken" — re-applying the generated config would push
that broken value. Recognize redacted (***) and secret:// values and emit a
deterministic secret("<AGENT>_<PLATFORM>_<KEY>") placeholder (collected into
.env.example), mirroring how provider keys round-trip. Non-secret config fields
stay literals.
* style(cli): biome formatting for platform diff tests
* fix(platforms): store the resolved secret value, not the $VAR placeholder
Final ship-verdict pi caught that config-authored platform secrets were
broken: the server's normalizeConfigForStorage persists whatever plaintext the
CLI sends as the secret (then swaps it for a secret:// ref), so sending the
$VAR placeholder stored a literal '$TELEGRAM_BOT_TOKEN' as the bot token — a
dead connection. Fixes:
- mapAgent resolves platform config secret()/$VAR to the REAL env value
(resolveCredentialValue), mirroring provider keys + auth-profile credentials;
the config row still never holds cleartext at rest (server-side secret store).
- diffPlatform now treats a config key as unchanged when the REMOTE value is
opaque (***/secret://) regardless of the desired form — desired is now the
resolved value, so the prior $VAR-keyed check would restart every apply.
- mapAgent rejects two platforms whose names slugify to the same stable id.
- init-from-org recovers the name from the stable id so NAMED platforms
re-derive the same id (no drift/duplicate on re-apply).
Unit-tested (resolved values + collision + named round-trip); the prior
round-trip test updated to assert the resolved value. 302 CLI tests green.
* docs(cli): correct generateLobuConfig comment — platforms ARE config-authored
The prior comment ('Chat platforms ... are NOT authored here') was added in this
same migration to rationalize dropping platforms; it was never an external
product decision, and platforms were config-driven on main via lobu.toml
[[agents.platforms]]. Now that defineAgent({ platforms }) is restored, the
comment was both stale and the source of a wrong 'platforms are UI-only'
assumption. Fix it to reflect reality.
* fix(cli): init-from-org emits real auth-schema credential keys
emitAuthProfile derived the credential KEY from the env-var name
(<SLUG>_VALUE / _CLIENT_SECRET), but lobu apply sends
credentials: { <key>: <value> } and the server validates <key> against
the connector's auth_schema — so a non-schema key is rejected.
Plumb listConnectorDefinitions(true) through fetchOrgState and emit
credentials keyed by each connector's real required auth-schema field
(or all properties when none are required), with a deterministic
<SLUG>_<FIELD> env-var placeholder. Falls back to the prior single
placeholder + TODO when the connector/auth_schema isn't found.
* fix(server): manage_feeds delete proves feed ownership before cancelling runs
handleDeleteFeed cancelled active runs by feed_id BEFORE the org-scoped
feed delete confirmed ownership. The run-cancel UPDATE is not org-scoped
(runs reach their org only through the feed), so a guessed foreign
feed_id could cancel another org's active runs even though the delete
then no-ops. Reorder: delete the org-owned feed first, bail on no match,
then cancel runs — no cross-org side effect before the ownership check.
Adds a cross-org isolation integration test (red before the reorder).
* docs(server): explain the space delimiter in sync-channels composite key
The desired-channels Map in the Slack sync-channels route is keyed by
`${teamId} ${channelId}`. Document why the single-space separator is
collision-safe: the entry regex (`[^/\s]+`) forbids whitespace and
slashes in either component, so neither can contain the delimiter and
distinct (team, channel) pairs always map to distinct keys. The Map is
in-memory and request-scoped (never persisted — the DB keeps team_id and
channel_id as separate columns), so there are no stored keys to migrate.
Comment-only; no behavior change.
* fix(client): SSE stream rejects on non-OK/aborted instead of hanging
The generated hey-api SSE client retries forever by default: a non-OK
response (401/404/5xx) or network failure throws inside its read loop,
fires onSseError, then sleeps and reconnects with no attempt cap — so a
failed stream never terminated and streamEvents' async iterator hung
indefinitely.
Cap attempts (sseMaxRetryAttempts default 1, overridable via
StreamEventsOptions.maxRetryAttempts) and capture the stream error via
onSseError into the iterator's error slot so the consumer rejects rather
than hanging. Caller-initiated aborts are treated as a clean shutdown,
not an error. Adds tests for the 401-rejects and abort-terminates paths
(both would hang before this fix).
* feat(apply): config-declared prune, replacing org managed_by flag
Replace the persistent per-org `managed_by` provenance flag with a
config-declared `defineConfig({ prune: true })`. When prune is on,
`lobu apply` deletes any org-owned definition (entity/relationship type,
watcher, connector definition) absent from the config — including ones
created via the dashboard/API. Data, connections, auth profiles, and
agents stay exempt. Safety is the existing blast-radius confirm; there is
no applied-set tracking and no server-side state.
Removed:
- migration 20260522120000_organization_managed_by.sql (never shipped)
- PATCH /api/:orgSlug/organization/managed-by endpoint
- managed_by from getUserInfo organizations[]
- RemoteOrg.managed_by, listOrgs parse, setOrgManagedBy
- --manage flag + willManage/flipToCodeManaged opt-in flow
Kept: org/organizationId consistency hard-stop, confirmDeletions
blast-radius gate, the org-owned/public-type filter in computeDiff, and
the entity/relationship-type instance-refusal server gate.
computeDiff: codeManaged -> prune (logic unchanged). SDK + mapper carry
prune into DesiredState.
* fix(ci): build @lobu/sdk + @lobu/client before downstream typecheck/unit
CI's typecheck job (make build-packages) and unit job (inline build step)
never built packages/sdk, so the CLI typecheck and bun test packages/cli
both failed to resolve @lobu/sdk — a cascade of TS2307 + implicit-any +
$secret errors and unhandled import errors in unit. Add packages/sdk to
both build paths and align the Makefile list with root build:packages
(which already builds client + sdk).
* fix(cli): commit warnings field in loadDesiredStateFromConfig return
apply-cmd.ts destructures `warnings` from loadDesiredStateFromConfig, but
the committed desired-state.ts return type was { state, configPath } only —
a real TS2339 (Property 'warnings' does not exist) that the merge resolution
left uncommitted in the working tree. Restores the field + warnings channel.
* fix(apply,init,schema): close 6 review blockers in the TS-SDK apply/init path
From a fresh full-diff pi review (bug_free_confidence 22):
- B1 init --from-org emitted secret("…") on the MCP-oauth clientSecret path
without registering the `secret` import → generated config didn't compile.
Couple SecretCollector to ImportTracker so every secret() ref auto-imports;
drop the 4 now-redundant manual imports.use("secret") calls.
- B2 platform secret rotation/removal was a silent noop (opaque remote can't be
diffed). Apply now upserts EVERY desired platform idempotently (mirrors the
provider-key push); the server's PUT decides noop vs restart. Diff also flags
a removed key (present in remote, absent in desired) as a change.
- B3 init --from-org read auth_schema as JSON Schema (.properties), but it's a
ConnectorAuthSchema (.methods). Rewrite authSchemaFields to read env_keys
fields[].key and oauth clientIdKey/clientSecretKey per profile kind.
- B4 relationship-type delete left status='active', so the (org,slug) partial
unique index (WHERE status='active') blocked re-creating the same slug
(prune → re-add). Delete now also sets status='archived' to vacate the index.
- B5 inverse-type lookup wasn't org/public scoped: it could link to and write
the reciprocal back-link onto another tenant's private rel-type. Scope the
lookup to own-org-or-public and only write the back-link when caller owns it.
- B6 init --from-org dropped per-skill network.judge / judges from SKILL.md.
Emit them as the YAML the frontmatter loader reads back.
Tests: CLI unit (MCP-oauth import, oauth_app methods keys, skill judged
domains, platform-key removal) + server integration (rel-type re-create after
delete, inverse tenant isolation x2). Full CLI 314 / server unit 201 green;
prune integration 9/9 against embedded PG.
* fix(apply): make re-apply converge to noop (idempotency)
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.
* fix(apply,schema): second-round review fixes (tenant isolation + declarative 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.
* fix(init,apply): sanitize derived secret env-var names + drop stray NUL 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.
* fix(apply): never prune system ($-prefixed) definitions like $member
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.
* test(e2e): add SDK lifecycle e2e gate (apply + prune + real worker turn)
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.
* ci(sdk-e2e): install libicu60 so embedded-postgres initdb can load
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).
* ci(sdk-e2e): disable systemd-run + install bubblewrap so the worker spawns
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.
* style(init-from-org): use optional chain for platform name recovery
Clears the recurring biome useOptionalChain warning (p.id && p.id.startsWith
→ p.id?.startsWith) on this PR's platform round-trip code.
* test(sdk-e2e): self-contained embedded-PG ICU + connector-sync & watcher-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.
* fix: address PR review (CodeRabbit + CodeQL) findings
- 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.
* fix(lobu): ensure the lobu-internal oauth_client so watcher dispatch 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).
* test(sdk-e2e): assert watcher dispatches to a worker (guards the lobu-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.
* fix(init-from-org): hydrate + emit relationship-type rules (round-trip 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.
* fix(apply): restore config-validation parity (duplicate slugs + platform 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.
* chore(submodule): restore owletto pointer to current main
Branch commit a5dc5ffa 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).
* ci(atlas): apply with the workspace CLI, not published @lobu/cli
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.
This implements comprehensive Docker-based local development deployment alongside existing Kubernetes production deployment.
Key Features
Testing
Generated with Claude Code