Commit Graph

82 Commits

Author SHA1 Message Date
Rdmclin2 1334209461 ✨ feat(openapi): expose eval resources and external run lifecycle (#19428)
* ✨ feat(openapi): expose eval resources and external run lifecycle

* 🐛 fix(openapi): report and aggregate shared eval dataset cases
2026-09-11 17:00:23 +08:00
Rdmclin2 e85ae51880 ✨ feat(openapi): add eval read routes for runs, datasets and topics (#19423)
* 🐛 fix(openapi): exempt heterogeneous relay routes from spec parity

`POST /api/v1/anthropic/v1/messages` and `POST /api/v1/openai/v1/responses`
are registered without `describeRoute`, so the generator's parity check has
been failing since #18558 and `openapi.yml` could no longer be regenerated.

Both relays speak the upstream Anthropic/OpenAI wire formats and are gated by
`requireHeteroModelInvocation` (operation JWT), so no API key holder can reach
them and their schemas are not LobeHub's to publish. Exempt them the same way
the documentation-serving routes already are, instead of describing endpoints
that are not part of the public API surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmyR6qUiKJX9VteSJR1hiu

* ✨ feat(openapi): add eval read routes for runs, datasets and topics

`/api/v1/eval` could create a run and read that one run back, but offered no
way to discover what already exists, so API consumers had to know a run id out
of band. Add the four missing read endpoints:

- `GET /eval/runs` (paginated, filterable by dataset and status)
- `GET /eval/datasets`
- `GET /eval/datasets/:id`
- `GET /eval/runs/:id/topics`

All four reuse the domain's existing posture: `AGENT_READ` RBAC, mandatory
pagination, and public-field allowlists so internal columns never reach the
response. Cross-tenant reads stay 404 rather than 403 so the endpoints do not
confirm the existence of other tenants' rows.

The models gain only the read primitives the routes need (count + paginated
list); no schema or migration changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmyR6qUiKJX9VteSJR1hiu

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 12:28:50 +08:00
Innei aba9e8150c 👷 build: migrate the test suite to vitest 5 (#19355)
* 👷 build: upgrade vitest to 5.0.0

Bump vitest and @vitest/coverage-v8 to 5.0.0 and migrate the configs for Vitest 4/5: drop the removed coverage.all option and replace poolOptions.forks.singleFork with maxWorkers/isolate.

* 👷 build: shim jest-dom matcher types for vitest 5

Vitest 5 exposes Assertion<R, T> and no longer reads the jest.Matchers augmentation that @testing-library/jest-dom ships, so its matchers vanish from expect(). Re-declare them on the Vitest interface, and replace the removed describe.sequential with { concurrent: false }.

* 👷 build: migrate the test suite to vitest 5

Vitest 5 (and @vitest/coverage-v8) replaces the esbuild dep optimizer with Rolldown, removed environmentMatchGlobs/coverage.all/poolOptions, forwards `new` to mock implementations, requires top-level vi.mock, and no longer resets automocks in restoreAllMocks. Migrate the root config to app/server projects with Rolldown optimizer options, shim the jest-dom matchers on Assertion<R, T>, and update the affected tests (constructor mocks, hoisted mocks, awaited assertions, explicit mock types, per-test mock defaults).

* 👷 build: make the openapi knowledge-base FileService mock constructable

tests/ was outside the per-area sweep; vitest 5 forwards `new` to the mock implementation, so the arrow implementation threw "is not a constructor".

* 👷 build: point the CI blob-report artifacts at the vitest 5 output dir

Vitest 5 writes blob reports to .vitest/blob instead of .vitest-reports, so the sharded jobs uploaded nothing and the merge jobs failed with ENOENT. Track the new directory in .gitignore too.

* 👷 build: refresh the desktop lockfile for vitest 5

apps/desktop/pnpm-lock.yaml is committed and the desktop build action installs with --frozen-lockfile, so the vitest/override change has to be reflected in it.

* 👷 build: keep the pre-vitest-4 coverage exclusions explicit

Vitest 4+ ships an empty coverageConfigDefaults.exclude, so configs that spread it silently lost the default exclusions (node_modules, dist, *.config.*) and counted more files. Inline the previous list.

* 🎨 style: format the migrated vitest configs

* 👷 build: keep canary's new class mocks constructable under vitest 5

exploreGraph/dispatchAuthorization/scopedDevices (canary #19375, #19352) mock classes with arrow implementations; vitest 5 constructs the implementation when the mock is newed, so the arrows throw 'is not a constructor'.
2026-09-10 22:18:49 +08:00
Innei a5e74a9eb4 ⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path (#19294)
* ⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path

Four package-level import edges put ~1.5 MB raw on the first screen:

- GlobalProvider/Editor.tsx imported @lobehub/editor/react, and rolldown does
  not shake that entry down to EditorProvider, so lexical, yjs and fuse landed
  in the entry chunk. The provider is reached by path until the package
  exposes a provider-only entry.
- @lobechat/types re-exported the ajv-backed AgentGraphSchema through the
  barrel that every store touches; it now lives on the
  @lobechat/types/agent/graph subpath and the agent barrel re-exports types
  only.
- @lobechat/context-engine's root barrel dragged the database models barrel
  (drizzle) in through AgentDocumentInjector; the app reaches the
  ./agent-document subpath and the injector reads loadPolicy directly.
- packages/locales bundled locales/en-US/*.json on top of the default
  sources it mirrors; the overlay now applies only in development.

Web production build: first-screen gzip 2299 KB -> 1968 KB.

Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH

* 🐛 fix(vite): keep one EditorContext in dev for the path-imported provider

Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH

* 🐛 fix(locales): refresh consumers after the dev English overlay lands

Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH
2026-09-09 02:45:25 +08:00
Innei a20880a125 🐛 fix(file): reserve storage before upload (#19119)
* 🐛 fix(file): reserve storage before upload

* 🐛 fix(file): gate storage quota on upload paths that skip reservations

The reservation flow only covers pre-signed tRPC uploads. Three surfaces write
`files` rows without ever consulting the quota: the OpenAPI upload endpoint,
server-side image generation, and bot-channel attachment ingest.

- OpenAPI upload checks the quota once per path that actually adds bytes, and
  maps the rejection to a 403 instead of a generic 500. Re-requesting a record
  the caller already owns adds nothing, so it stays exempt.
- Image generation admits at submission rather than at persist — blocking after
  the model call is already paid for is worse than not blocking.
- Attachment ingest charges the post-compression buffer, and a quota skip now
  says so instead of leaving the sender to guess.

Claude-Session: https://claude.ai/code/session_014iyxmqssnrPopmKV1xKJ7k

* 🐛 fix(file): release reservations on every dataset import failure

Claude-Session: https://claude.ai/code/session_01Xmacr8RYT7rZ3zmjZrrENm

* 🐛 fix(file): stop reservations from deleting pre-existing objects

Reject a reservation whose pathname already holds an object, so aborting one
can only ever delete a key it created. Serialize server-owned uploads with the
owner-row lock by threading the transaction through the quota check and the
file row it authorizes.

Claude-Session: https://claude.ai/code/session_01Xmacr8RYT7rZ3zmjZrrENm

* 🐛 fix(file): keep the S3 write out of the quota transaction

Holding the owner-row lock across the upload blocked every other write to that
user for the length of the transfer, and Postgres kills the transaction outright
wherever DATABASE_STATEMENT_TIMEOUT is set, orphaning the object it had already
written. Check the quota once before spending the transfer, upload outside any
transaction, then re-check under the lock the file row is written with.

Claude-Session: https://claude.ai/code/session_01Xmacr8RYT7rZ3zmjZrrENm
2026-09-06 13:41:18 +08:00
AmAzing- 5b9ef458e1 🐛 fix(openapi): normalize Claude metadata for model relays (#19170) 2026-09-05 14:13:05 +08:00
AmAzing- 7ce4ff0b7f 🐛 fix(openapi): defer heterogeneous upstream protocol routing (#19135) 2026-09-04 17:54:00 +08:00
AmAzing- 2e7f415428 🐛 fix(openapi): relay Gemini parts through Responses (#19134) 2026-09-04 15:33:27 +08:00
AmAzing- 08a22e1dff 🐛 fix(heterogeneous-agent): preserve TRAE auth for model bindings (#18932) 2026-09-04 13:29:59 +08:00
AmAzing- e720c0af76 🐛 fix(openapi): relay Gemini part events to Claude Code (#18880) 2026-08-30 19:15:08 +08:00
AmAzing- ec466bdbd4 🐛 fix(openapi): complete Responses usage for Grok Build (#18875) 2026-08-30 16:38:58 +08:00
AmAzing- 8e240c0c52 🐛 fix(openapi): accept Pi Responses easy input messages (#18876) 2026-08-30 16:38:40 +08:00
AmAzing- fba5305562 ✨ feat(heterogeneous-agent): expand LobeHub provider support (#18785) 2026-08-29 09:54:15 +08:00
LiJian bc08dbe17d ♻️ refactor: make API key prefix and CLI product naming distribution slots (#18637)
* ♻️ refactor(cli): collect every product-naming string into one module

The CLI's identity was spread across a dozen files: bin names in
`utils/completion.ts`, the config directory duplicated in five places, the
systemd unit name defined twice, the product name in a handful of command
descriptions, and — least visibly — inside `buildNotifyProtocol`, a prompt
handed to heterogeneous agents that the model then repeats back to the
user.

That is fine for a single distribution and leaves a white-label build with
no seam. Several of these are reached at runtime along paths no UI gating
can intercept, and one of them is a prompt, so there is nothing to gate.

`src/constants/identity.ts` now holds all of them. Values are upstream's,
unchanged, and the default build's observable output is identical —
verified by building with this repo's own config and diffing what the CLI
prints:

    Usage: lh [options] [command]
    LobeHub CLI - manage and connect to LobeHub services
    complete -o nosort -F _lobehub_completion lh lobe lobehub
    lh - LobeHub CLI - manage and connect to LobeHub services

Two things that were already wrong, fixed on the way:

  - `skillLocator.ts` compared `pkg.name` against a hardcoded
    `'@lobehub/cli'`, so the package-root walk breaks for any fork or
    rename. It reads `cliPackageName` now, which is the same value from
    the same package.json.
  - `constants/auth.ts` exposed a single API-key env var name. It is a
    list now, most-preferred first, so a distribution can introduce its
    own prefix without breaking a variable a user already exported. Only
    USER-set variables moved; `LOBEHUB_SERVER`, `LOBEHUB_JWT` and the
    agent-run ids are injected by the desktop app and the server, so
    renaming those needs all three sides to move together and is left
    alone.

`man/roff.ts` also stops hardcoding `LH` and `@lobehub/cli` in the roff
header, taking them from the bin name and `pkg.ts` instead — the man page
described a command that would not exist under any other name.

* ♻️ refactor(api-key): make the key prefix a slot instead of a literal

`sk-lh-` was hardcoded in four places and asserted in four test files. It
is the one brand string a deployment's end users actually handle — they
copy it out of the UI, paste it into CI config, quote it in support
threads — so on a white-label distribution it undoes the rebranding
everywhere else.

It is also load-bearing rather than cosmetic. `validateApiKeyFormat` is
what the OpenAPI auth middleware uses to tell an API key from an OIDC
JWT, so generation and validation must read the same value or every
issued key fails auth as a malformed JWT. That is why the prefix is one
constant now, used by both.

`API_KEY_PREFIX` defaults to `sk-lh-`, so upstream's behaviour is
unchanged — verified by generating a key and round-tripping it through
the validator.

Two deliberate choices in the implementation:

  - Validation compares the prefix with `startsWith` and tests only the
    16-character suffix against a static pattern, rather than
    interpolating the prefix into a RegExp. The prefix is data flowing
    into a pattern, and escaping it correctly is a thing to get wrong;
    this way there is nothing to escape and the check stays correct for
    whatever prefix a distribution picks.
  - The tests derive their fixtures from the constant instead of
    hardcoding a spelling, so they pass for any prefix. Several of them
    fed a literal straight into `validateApiKeyFormat`, so they would
    have gone red the moment anyone overrode it — failing on the
    override rather than on a real defect.

`spec.ts`'s `bearerFormat` follows the constant too; it is the public
API document, so it was advertising a format the deployment no longer
issues.

* ♻️ refactor(cli): collect the completion word-index env var into identity.ts

CLI_COMPLETION_CWORD_ENV joins the other product-naming strings in
identity.ts. `LOBEHUB_COMP_CWORD` was the one string the earlier
identity refactor (a65fa6a9) missed — it's set by the generated
completion script and read right back by `__complete` in the same
process, so it never surfaces in help text and slipped past every
rendered-output check.

Purely internal plumbing (unlike CLI_API_KEY_ENV_NAMES), so no
back-compat list is needed: both sides always come from the one
installed binary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* 🛡️ fix(api-key): fall back to sk-lh- when a distribution override omits the slot

A downstream `@lobechat/business-const` override replaces the whole module
instead of extending it, so one written before this slot existed simply
omits API_KEY_PREFIX rather than inheriting upstream's default — that
either fails typecheck or, if typecheck is skipped, crashes key generation
and validation on undefined.length.

Resolve it once in packages/utils/apiKey.ts with an explicit runtime
fallback to 'sk-lh-', and have every other consumer (openapi spec, tests)
import the resolved constant from there instead of the raw slot.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 15:43:24 +08:00
YuTengjing 3ba63cd033 ✨ feat(model-runtime): improve routing context and fallback (#18596) 2026-08-24 11:34:40 +08:00
LiJian 5cf4638a1c 🐛 fix(heterogeneous-agent): make the server-default binding usable end to end (#18621)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: AmAzing129 <115673583+AmAzing129@users.noreply.github.com>
2026-08-23 17:46:56 +08:00
AmAzing- fe35c716dd 🐛 fix: sanitize adaptive thinking for relay models (#18620) 2026-08-23 16:26:17 +08:00
AmAzing- 9a28c5fc59 🐛 fix: use lobehub/model for Claude Code server-default (#18613) 2026-08-23 14:17:47 +08:00
AmAzing- 7069482430 🐛 fix: forward input tokens on Anthropic relay usage (#18612) 2026-08-23 13:53:40 +08:00
AmAzing- 06b6447df5 ✨ feat(heterogeneous-agent): support relay models in Codex (#18609) 2026-08-23 12:44:40 +08:00
AmAzing- 56f564e43d 🐛 fix(heterogeneous-agent): accept namespaced Codex models (#18593) 2026-08-22 21:34:34 +08:00
AmAzing- 45a531a952 🐛 fix(heterogeneous-agent): pin server-default provider contract (#18581) 2026-08-22 11:21:59 +08:00
AmAzing- 486207cbb7 ✨ feat(heterogeneous-agent): add server-default operation auth (server half) (#18558) 2026-08-22 00:12:40 +08:00
Rdmclin2 5f54953211 ✨ feat(agent): let agent authors restrict who can share their topics (#18557)
* ✨ feat(agent): let agent authors restrict who can share their topics

Add a per-agent `topicSharePolicy` on `agencyConfig`. `member` (the
default, and what legacy rows without the field resolve to) keeps today's
behaviour: any workspace member may publish a share link for any of the
agent's topics they can open. `restricted` reserves publishing to the
agent's creator and workspace owners.

Only publishing is gated — revoking a link and the private placeholder the
share popover creates on open stay open to everyone, so taking a topic out
of circulation is always possible.

Enforced server-side in the topic share procedures; the Permission page
(agent + group) writes it, and the share controls mirror it so a restricted
member sees a disabled option with a reason instead of a request that fails.

* 🌐 style(i18n): sync generated locales

* 🐛 fix(agent): stop the Permission page offering policy edits it cannot save

A workspace Admin holds `agent:update:all`, so `canManage` is true and the
Permission page enabled all three policy selects for them. The server accepts
those keys only from the agent's creator or the workspace owner, and strips
them from an otherwise successful mutation — so the choice was accepted,
reported no error, and silently discarded.

Gate the model, execution-target and topic-share rows on the same authority
the server uses, and swap their description for the existing
"only the creator or a workspace owner" line so an Admin can still see what
exists and who to ask.

* 🐛 fix(openapi): merge JSON config patches instead of replacing them

`PATCH /api/v1/agents/:id` assigned `agencyConfig` and `chatConfig` straight
onto the column, but the request schemas expose only a slice of each: two of
`agencyConfig`'s keys (graph mode) and 13 of `LobeAgentChatConfig`'s 37. Any
edit therefore deleted every field the caller had no way to send — the member
permission policies, device bindings and execution settings on one, two dozen
chat settings on the other.

For the topic-share policy that meant an unrelated graph edit silently
reopened a restricted agent's conversations to every workspace member, since
a missing value resolves to `member`.

Merge both through one `mergeJsonPatch` helper, which `params` already did
inline; `undefined` still drops a key and `null` is still kept as the
schemas' explicit "cleared" value.

* 🐛 fix(agent): apply the topic-share policy to group topics too

`createTopic` accepts a `groupId` with no agent and no session, so a group
topic can carry neither — and `resolveTopicShareAgent` read `agentId` first,
resolved nothing, and let the publish gate fall open. The group Permission
page writes the policy onto the group's supervisor, so a restricted group's
conversations could still be published by any member who could open them.

Resolve a topic's group to its supervisor first: a group conversation is a
conversation with that supervisor, and it is the row the policy lives on.

Also keep an explicit `agencyConfig: null` / `chatConfig: null` clearing the
column in the OpenAPI update, which the new merge had turned into a no-op.
A whole-column null is a caller deliberately asking to clear; a partial edit
that cannot name the other keys is not.

* 🐛 fix(openapi): keep the member permission policies across a config clear

`PATCH /api/v1/agents/:id` authorizes on `AGENT_UPDATE`, which workspace
Admins hold for every member's agent, while writing the `agencyConfig` policy
keys is reserved to the agent's creator and the workspace primary owner — the
TRPC writer strips them from anyone else. An `agencyConfig: null` through the
public API went around that gate: the policies vanished, and a missing
`topicSharePolicy` resolves to `member`, reopening a restricted agent's topics
to the whole workspace.

The request schema cannot express those keys in the first place, so clearing
them was never something a caller could ask for. A whole-column clear now
keeps them and drops everything else.

`AGENT_PERMISSION_POLICY_KEYS` moves to `@lobechat/types` next to the config
it describes, so both writers work from one list instead of two copies.

* 🐛 fix(openapi): let an authorized caller actually clear the agent config

The previous commit kept the member permission policies across every
`agencyConfig: null`, including one sent by the agent's creator or the
workspace primary owner — the two callers `updateAgentConfig` does let write
those keys. Their clear silently did nothing, so a `restricted` policy could
not be reset through the API at all.

Resolve the same authority the TRPC writer uses and branch on it: an
authorized caller clears the column outright, anyone else keeps the policies.
The decision is a named pure function so both halves are covered by tests
rather than by mocking a transaction.
2026-08-21 18:10:23 +08:00
LiJian e52dc85cfd 🐛 fix(openapi): stop denying resources that nobody owns (#18525)
* 🐛 fix(openapi): stop denying resources that nobody owns

`POST /api/v1/chat`, `/chat/translate` and `/chat/generate-reply` answer
403 for every account without a DB role — which is every personal account.
Reproduced on app.lobehub.com and on a self-hosted deployment; both return
the identical `no permission,current user has no ALL permission,and
resource not belong to current user`. The route-level RBAC middleware
passes (an invalid body still reaches the validator and 400s), so this is
the service-layer gate, not the API key's scope.

`getResourceBelongTo` answers `undefined` for two unrelated situations: a
row that does not exist, and a row that has no per-user owner at all.
`resolveOperationPermission` collapsed both into "belongs to somebody
else" and denied before ever reaching the owner check.

The three endpoints above check `AI_MODEL_INVOKE` against
`{ targetModelId }`. Built-in models have no owner, so "does this model
belong to you" has no true answer — and the absent answer denied every
caller holding `ai_model:invoke:owner`, which is exactly what
`PERSONAL_DEFAULT_PERMISSIONS` grants.

Split the two cases:

- `ALL_SCOPE` without ALL permission → still denied. An owner-scoped grant
  cannot answer an unscoped question.
- a resource owned by another user → still denied.
- no owner resolvable → falls through to the owner check, with the
  returned `condition` pinned to the caller. An unowned resource therefore
  cannot widen a query past the caller's own rows, and a row that really
  does not exist now surfaces as "not found" rather than as a permission
  error.

Also drops the target-less retry in `translate()`. It was a local
workaround for this exact gate, it can no longer change any outcome, and
leaving it in place implies its two siblings in the same file were merely
missing it.

Six tests cover the split, including the three denials that must not
loosen. Verified they fail against the previous gate: the two encoding the
new behaviour go red, the rest stay green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 fix(openapi): stop crashing when a provider row stores no key

With the permission gate fixed, all three `/api/v1/chat*` endpoints reach
the model call and die there instead:

    聊天对话失败: Cannot read properties of null (reading 'split')

`getApiKey` asserts `providerConfig.keyVaults!` and hands it to
`decrypt`, whose first statement is `encryptedData.split(':')`. A provider
row whose `keyVaults` is null therefore throws — from inside a helper whose
name gives no hint that a credential lookup is what failed.

That row shape is not an edge case. It is the ordinary one for any
deployment whose provider credentials come from the environment: the row
exists so the provider can be enabled and ordered, and no per-user vault is
ever written. Confirmed against a live deployment where `deepseek`,
`lobehub` and `openai` all have empty vaults, and where naming a provider
that has NO row at all got further — the `'{}'` fallback for a missing row
already did the right thing, so only the half-configured case crashed.

Both cases mean the same thing to the caller: no user-supplied key for this
provider. Return the empty vault and let the model runtime fall back to the
environment, exactly as the missing-row branch already did.

Three tests: the null vault, the absent row, and a row that does store a
key (which must still decrypt). Verified non-vacuous — restoring the `!`
turns the first one red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 fix(openapi): default /chat to the product's model, not to gpt-3.5-turbo

A request to `/api/v1/chat` that names no model asked for `gpt-3.5-turbo`
on `openai`, and no caller could change that: `ChatServiceConfig` is only
reachable through the constructor and all three controller call sites pass
none. So the field was unreachable configuration, and the literals were the
behaviour.

Any deployment not running OpenAI therefore answered a model-less request
with a credential failure naming a provider the caller never mentioned —
including the default one, whose `DEFAULT_PROVIDER` is `deepseek`.

`DEFAULT_MODEL` / `DEFAULT_PROVIDER` are what `DEFAULT_AGENT_CONFIG` and the
rest of the product already resolve to. Using them here makes
`/api/v1/chat` agree with the product it is part of, and gives every
distribution the default it has already declared, with no new setting.

Test asserts the payload and the runtime both receive the constants rather
than any literal of this service's own; verified non-vacuous by restoring
the hard-coded pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 fix(openapi): import the model defaults from where they are declared

`@/const/settings` re-exports `DEFAULT_MODEL` but not `DEFAULT_PROVIDER` —
`llm.ts` imports the latter for its own use only. Pulling both from that
barrel broke the build:

    Error: Export DEFAULT_PROVIDER doesn't exist in target module

Import both from `@lobechat/business-const`, which declares them, matching
what `provider.service.ts` in this same package and
`services/verify/modelConfig.ts` already do.

The tests did not catch this: they mocked `@/const/settings` with a factory
listing both names, and a factory mock invents whatever key it is asked
for, so the missing re-export was invisible until a real bundler resolved
it. Moved the mock onto the module that actually declares them, so it can
no longer paper over the same mistake.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 fix(openapi): let the chat default name any provider, not five of them

CI:

    chat.service.ts(63,7): error TS2322:
    Type '"deepseek"' is not assignable to type 'AIProvider | undefined'.

Casting `DEFAULT_PROVIDER` would have silenced that, and left the wrong
thing in place. `ChatServiceConfig.defaultProvider` was typed `AIProvider`
— a five-name list written by hand in the same file that types the request
field beside it as `provider?: string`, validated as `z.string().max(64)`.

So the file already said a caller may name any provider while the default
may only be one of five, and nothing downstream honours that split: both
values land in the same `ai_providers` lookup, and the runtime resolves
custom providers through `sdkType` precisely because ids are open. A list
of built-ins cannot express that set — `deepseek`, the product's own
default, was never in it.

Widened to `string` to match the request field. `AIProvider` stays exported
and is marked deprecated rather than deleted, so nothing importing it
breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 19:26:07 +08:00
Arvin Xu 9396a5b315 ♻️ refactor(agent): move Graph Agent config from chatConfig to agencyConfig (#18419)
* ♻️ refactor(agent): move Graph Agent config from chatConfig to agencyConfig

A graph is an agent: the ReasoningGraph defines the agent's behavior body
(node policies, routing conditions, data contracts), so it belongs on the
agency config (how the agent behaves and executes) rather than the chat
config (per-session preferences).

- types: drop enableGraphMode/graph from LobeAgentChatConfig, add to
  LobeAgentAgencyConfig next to heterogeneousProvider
- runtime: GraphAgent factory reads agencyConfig.graph with a legacy
  chatConfig.graph fallback so existing rows keep running
- cli: --enable-graph / --disable-graph / --graph-file now write
  agencyConfig
- db: graph full-document replace moved to agencyConfig; legacy
  chatConfig.graph is forwarded to agencyConfig on the next write,
  migrating old rows in place
- openapi / sdk: graph fields moved into agencyConfig, regenerated

* ♻️ refactor(agent): rename ReasoningGraph to AgentGraph

The graph is the behavior body of a Graph Agent, not a 'reasoning' data
structure — the old name was misleading. Aligns the top-level type with
its children (AgentGraphNode / AgentGraphEdge / AgentGraphField) and the
AGENT_GRAPH_ROOT_NODE_ID constant.

- ReasoningGraph -> AgentGraph
- ReasoningGraphSchema -> AgentGraphSchema
- UI copy in locales updated to match

* 🐛 fix(agent): address codex review on graph agency config

- openapi: thread agencyConfig through createAgent / updateAgent and the
  public projection, so API clients can actually persist the graph instead
  of getting a silent no-op
- db: an explicit agency-level graph (including null) now takes ownership
  of the graph and drops the legacy chatConfig fields, so the runtime
  fallback cannot resurrect a cleared snapshot
- ui: AgentGraphRuntime falls back to the legacy chatConfig values so
  pre-migration rows render their effective graph instead of an empty,
  disabled editor
2026-08-17 15:24:42 +08:00
Arvin Xu 941403a556 chore: remove internal LOBE-XXX issue references from code comments (#18327)
* chore: remove internal LOBE-XXX issue references from code comments

Rewrite code comments that referenced internal Linear issue IDs
(LOBE-XXXX) into self-contained descriptions of the scenario, keeping
code semantics unchanged. User-facing locale strings and LOBE_* constant
names are left untouched.

* chore: remove internal LOBE-XXX issue references from SQL migration comments

---------

Co-authored-by: arvinxx <arvinxx@users.noreply.github.com>
2026-08-15 12:15:19 +08:00
Rdmclin2 0da862f57d ✨ feat(knowledge-base): soft-hide restricted knowledge bases from members (#18282)
* ✨ feat(knowledge-base): support workspace resource permissions for knowledge bases

Knowledge bases join the polymorphic resource-permission system with
`use` / `edit` levels. `use` (member No-access) soft-hides a shared KB
from members in the UI — library list, search, pages sidebar, direct
reads — while agents attached to it keep retrieval untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(knowledge-base): harden the soft-hiding surface across UI paths

- keep permission rows staged on a still-private KB/document inert until
  publish, and preserve a staged access level through publishing
- exclude restricted KBs and their linked files/folders/pages from unified
  search, and restricted-KB pages from the Pages listing (query-level, so
  pagination and totals stay correct)
- guard direct document reads and history routes against stale restricted-KB
  URLs
- return a memberRestricted flag from getKnowledgeBases so the manager lock
  badge needs no per-row permission request
- move the knowledgeBaseAccess helpers to @/server/services (router _helpers
  re-exports)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ✅ test(daily-brief): expect the server-provided label for the unregistered action key

#18281 switched the taskless-acceptance fixture to an unregistered key so
the server-sent label renders as-is, but left the assertion on the old
localized copy — the case (and canary) has been red since.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:36:40 +08:00
Rdmclin2 7abba9672f ✨ feat(openapi): add api-keys, evals, mcp-servers and usage to the v1 API (#18141)
* ✨ feat(openapi): expand v1 REST surface with api-keys, evals, mcp-servers and usage

Add new OpenAPI v1 resources (API keys, evals, MCP servers, usage) and
harden the existing controllers with shared validators, pagination
helpers and public-field projection so responses no longer leak internal
columns. Usage reporting goes through a business-server slot so
self-hosted deployments fall back to an unavailable summary.

* 🐛 fix(openapi): enforce row-owner checks on MCP writes and drop the unused upload pathname

The connector model's ownership predicate is workspace-wide, so the REST MCP
write paths let any member with only `agent:update:owner` patch, delete or sync
a server created by another member. The connector tRPC mutations already guard
this with `assertWorkspaceRowManageable`; add the equivalent `assertRowManageable`
to the OpenAPI BaseService and apply it before every MCP mutation.

Also remove `pathname` from the public upload schema, route docs and spec: it
never reached the storage key, and its validator allowed any 1-1024 char string,
so wiring it through would have handed clients an unchecked object path. The
batch upload schema already omitted it.

* 🐛 fix(openapi): drop request fields the v1 run paths never honor

`n` never reached ChatStreamPayload and /chat returns a single content string,
so accepting it promised multiple choices and delivered one. `caseSelection` is
storage-only for internal runs — this endpoint always creates `mode: 'internal'`,
which pre-creates topics for every case in the dataset — so a caller asking for a
subset would silently run the whole thing. `maxConcurrency` is consumed nowhere.

Remove all three from the public schemas and regenerate the spec, matching the
`pathname` removal: the contract should only advertise knobs the run path honors.
`k`, `maxSteps` and `timeout` stay because the run service does read them.

* 🐛 fix(openapi): admin-gate workspace API-key routes

`workspaceAuthMiddleware` only enforces the owner/admin role for `apikey` auth,
so an OIDC/session-authenticated workspace member holding `api_key:update:owner`
could PATCH or DELETE another member's workspace key by id — `ApiKeyModel`'s
predicate is workspace-wide, not row-owner.

The tRPC side already gates this: `apiKeyProcedure` applies
`requireWorkspaceRoleWhenScoped('admin')` to the whole router, so the REST
surface was strictly more permissive. Add the equivalent Hono middleware and
apply it to every api-keys route so both transports gate identically.

* 🐛 fix(openapi): preserve an explicit temperature of 0 on /chat

`ChatStreamPayload` was built with `params.temperature || 1`, so a request
asking for deterministic output at `temperature: 0` was silently sent at 1 —
the schema has accepted `min(0)` since the endpoint was introduced.

Pre-existing since #12758 rather than a regression here, but it is the same
contract-vs-implementation mismatch this PR has been clearing up, and the fix is
`??` instead of `||`. Scoped to the request path: `generateReply` applies the
same falsy fallback to an agent's stored config, which is a separate behavioural
change for existing agents and is left alone.

* 🐛 fix(openapi): require chat:write to queue an eval run

An internal eval run pre-creates real chat topics through `TopicModel`, and the
QStash workflow keeps writing topic/message state afterwards. The route gated
only on `agent:write` + `model:invoke`, so a restricted API key deliberately
denied `chat:write` could still persist chat artifacts through `/eval/runs`.

`/responses` already enforces the full `agent:write` + `chat:write` +
`model:invoke` trio for exactly this reason; match it here.

* 🐛 fix(openapi): correct file response schemas and regenerate the SDK

The spec fell back to the bare resource schema for any one-segment path, so
`POST /files`, `GET /files/{id}`, `POST /files/batches` and `POST /files/queries`
all advertised `File` while the controllers actually return wrapped payloads
(`{ file, parsed? }`, `{ successful, failed, summary }`, `{ files, failed, ... }`).
Generated clients would have mistyped every one of them. Describe the four
explicitly ahead of the fallback.

`packages/sdk/src/generated` was also stale — its drift test was already failing
locally, since the SDK is not in the PR CI whitelist. Regenerated it from the
corrected spec, so the new api-keys / eval / mcp-servers / usage resources are
callable and the file operations carry the right types.

* 🐛 fix(openapi): keep MCP server routes to base-scope connectors

`isPublicMcpServer` ignored `agentId`, but `queryPublic()` (the list path)
filters `agent_id IS NULL` while `findPublicById` does not. So the id routes —
GET/PATCH/DELETE and POST /:id/sync — could reach agent-scoped connectors the
list route cannot even surface.

Deleting one that way skipped the connector router's agent-plugin unpin, which
exists specifically so a delete never leaves the owning agent with a dangling
pinned tool. Require `agentId === null` in the predicate so the whole group is
consistently base-scoped; agent-scoped connectors stay managed through the agent
profile.

* 🐛 fix(openapi): stop typing named sub-operations as the group resource

The success-schema fallback treated any single-segment path as a resource
operation, so `GET /messages/count` was advertised as `Message` (it returns
`{ count }`) and `GET /users/me` as `User` (a caller without `user:read`
deliberately gets only `{ id }`). Generated SDK callers were typed against the
wrong shapes.

Narrow the fallback to the two real resource operations — the collection root
and `{id}` — and describe `messages/count`, `messages/replies` and `users/me`
explicitly. An undescribed sub-operation now degrades to a permissive object
rather than a confidently wrong resource type, which is the failure mode that
produced both this and the earlier `/files` mistyping. SDK regenerated.
2026-08-10 16:21:49 -07:00
Rdmclin2 a9bf96d950 ✨ feat(apikey): scope workspace API keys to member permissions (#18136)
Members can now issue and manage their own workspace API keys instead of
owners-only. Keys inherit the issuer's current workspace permissions, scopes
became editable after creation, and admins can restrict creation to admins
only via a workspace API key policy.
2026-08-10 13:39:05 -07:00
Rdmclin2 b0fbd5f185 🐛 fix(openapi): keep /users/me reachable by restricted API keys (#18063)
* 🐛 fix(openapi): keep /users/me reachable by restricted API keys

* 🐛 fix(openapi): keep the message count on /users/me behind chat:read

* 🐛 fix(openapi): narrow /users/me to the id for keys without user:read

* 📝 docs(openapi): describe the scoped /users/me payload in the route comment
2026-08-08 10:13:02 -07:00
Rdmclin2 0c7e2c7135 ✨ feat(auth): support API key scopes (#18029)
* ✨ feat: support API key scopes

* 🐛 fix: enforce API key scopes on public tRPC procedures and AI-generating REST routes

* 🐛 fix: gate model-invoking procedures with procedure-level extra scopes

* 🐛 fix: require chat:write for stateful aiChat calls and model:invoke for provider connectivity test

* 🐛 fix: require chat:write and model:invoke for aiAgent run-execution procedures

* 🐛 fix: gate agentNotify runs and ComfyUI generation with run/model scopes

* 🐛 fix: require model:invoke for follow-up extraction and onboarding generation triggers

* 🐛 fix: guard MCP tool execution and keyVaults updates for restricted keys

* 🐛 fix: gate task execution, agent signal triggers and memory extraction with model scopes

* 🐛 fix: close remaining restricted-key write bypasses on responses, settings and generation assets

* 🐛 fix: redact decrypted credentials from restricted-key reads and gate settings reset

* 🐛 fix: block sensitive nested market sub-surfaces for restricted keys

* 🐛 fix: require user:read for the permissionless /users/me route

* 🐛 fix: redact provider write responses and guard market tokens as credential settings

* 🐛 fix: block backup import/export for restricted keys and guard ASR stored-file reads

* 🐛 fix: require model:invoke for external tools search on restricted keys

* 🐛 fix: gate market tool-execution surface for restricted keys

* 🐛 fix: require file:write for generation topic cover and visibility mutations

* 🐛 fix: require chat scopes for remaining aiAgent thread and intervention mutations

* 🐛 fix: withhold unrestricted gateway JWTs from restricted API keys

* 🐛 fix: block bot channel calls and scope agent knowledge listing for restricted keys

* 🐛 fix: require agent:write on the Responses agent-run route

* 🐛 fix: require agent scopes for home sidebar operations and legacy sidebar moves

* 💄 style: move API key management into a detail drawer
2026-08-07 22:02:37 -07:00
Rdmclin2 d365a2e283 ✨ feat: support async agent transfer (#17997)
* ✨ feat: support async agent transfer

* 🔒 fix: scope transfer-migration endpoints, dedupe polling, resume jobs at boot

* ♻️ refactor: bound transfer status polls to visible topics, keep group transfers sync

* 🐛 fix: reserve the active topic in capped transfer status polls

* 🐛 fix: guard OpenAPI user deletes during pending transfers, tighten topic gating

* 🔒 fix: require bounded topic candidates on transfer status polls
2026-08-07 01:49:52 -07:00
Rdmclin2 3ea7afd5d8 ✨ feat(openapi): generate openapi.yml from routes via hono-openapi (#17944)
* ✨ feat(openapi): generate openapi.yml from routes via hono-openapi

- add src/common/validator.ts: drop-in zValidator built on hono-openapi so
  every request schema registers into the spec while keeping the exact
  @hono/zod-validator 400 response shape
- describe the four validator-less routes (users/me, agent-groups list,
  file upload, batch upload incl. multipart bodies) and /health
- add scripts/generate-openapi.ts: runtime spec generation with
  route-vs-spec parity check, --check mode for CI, placeholder responses
- commit the generated openapi.yml (79 operations / 38 paths) and guard
  it with scripts/generate-openapi.test.ts
- ignore the generated file in eslint/prettier so formatters don't fight
  the generator output

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ✨ feat(openapi): serve API docs at /api/v1/docs via Scalar

- extract spec assembly into src/spec.ts, shared by the generator script
  and the new runtime endpoint so both always emit the same document
- GET /api/v1/openapi.json: build spec from live routes on first request,
  cached in memory
- GET /api/v1/docs: Scalar interactive reference reading that endpoint
- exempt the two docs routes from the generator parity check (real
  endpoints, deliberately not part of the described API surface)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 style(openapi): match docs site theme to the app's neutral monochrome style

- add src/docs-theme.ts: Scalar CSS variable overrides — black accent on
  light, white accent on true-black dark, antd system font stack
- theme: 'none' so the runtime bundle doesn't inject its default theme
  stylesheet after our customCss (which silently overrode every variable)
- reuse the app favicon

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🌐 feat(openapi): localize docs UI following the app's language preference

Resolve the Scalar UI locale per request with the same priority the main
app uses (?hl query > LOBE_LOCALE cookie > Accept-Language), mapped to
Scalar's built-in translations (zh-CN, de, fr, es, ru, ar; zh-* all map
to zh-CN). English stays the default for unsupported locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:48:08 -07:00
Lee Junho bedee6b332 ✨ feat(file-loaders): support Jupyter notebook (.ipynb) upload with token-efficient markdown conversion (#17855)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-08-05 21:56:04 +08:00
Rdmclin2 d7eaa44051 ⏪️ revert(db): restore snapshot-based message scope on transfer (revert #17780) (#17927)
Revert "⚡️ perf(db): derive message scope from topics/sessions on transfer (#17780)"

This reverts commit 3402718bac.
2026-08-04 23:09:52 -07:00
Rdmclin2 3402718bac ⚡️ perf(db): derive message scope from topics/sessions on transfer (#17780)
* chore: optimize transfer

* 🐛 fix: re-snapshot transferred message scopes before owner deletion

- resnapshotTransferredMessagesBeforeOwnerDelete: rows whose snapshot
  workspace_id disagrees with their anchor are re-materialized from the
  anchor before a workspace/user row is deleted, so cascade FKs cannot
  destroy history that a transfer moved away (Codex P1)
- replace RQB findFirst/findMany with plain selects at child-scope call
  sites: RQB table aliasing broke the correlated EXISTS predicate (42P01)
- agentGroup copy: filter message_plugins by derived parent scope instead
  of stale snapshot columns
- update stats/transcript tests to derived-scope semantics; add owner
  deletion regression tests

* 🐛 fix: make scope predicates db-free and route OpenAPI user delete through resnapshot

- build predicates with a standalone QueryBuilder so they no longer need a
  live db handle (fixes agentSignal tests whose db stubs lack .select)
- OpenAPI user.service.deleteUser now routes through UserModel.deleteUser
  so the pre-delete resnapshot runs (Codex P1 round 2)
- dataExporter: derived-scope child tables bypass RQB findMany, which
  aliases the table and breaks the correlated EXISTS (Codex P2)

* 🐛 fix: derive message scope in OpenAPI message read paths

- message.service / message-translations.service: replace snapshot-column
  buildWorkspaceWhere(messages/messageTranslates) with the derived scope
  predicates so transferred conversations stay readable over OpenAPI
  (Codex P1 round 3)
- whole-scope counts route through MessageModel.count (index-bounded
  three-arm fan-out instead of an unbounded EXISTS scan)
- messageTranslates lookups switch from RQB findFirst to plain selects
  (RQB aliasing breaks the correlated EXISTS)

* 🐛 fix: derive message ownership in permission checks and remaining server readers

- BaseService.getResourceBelongTo resolves targetMessageId owner from the
  topic/session anchor instead of the stale user_id snapshot (Codex P1
  round 4) — permission checks now agree with the derived read scope
- convert the remaining topic-bounded server readers off snapshot filters:
  agentSignal feedback context, completed-turn trajectory, and user-memory
  topic extraction
- audited every other messages.user_id/workspace_id filter: the rest are
  author-semantics (usage attribution, foreign-author guards, onboarding,
  nightly-review windows) and intentionally keep the snapshot

* 🐛 fix: rewrite anchorless agent messages on transfer

Messages with no topic AND no session (e.g. OpenAPI-created with only an
agentId) have nothing to derive scope from — their snapshot IS
authoritative. Transfer now rewrites just these rows (bounded by agent_id,
rare by construction), keeping the mass-rewrite avoidance for everything
else (Codex P1 round 5).

* 🐛 fix: preserve anchorless message recency and gate workspace scrub on ownership

- anchorless message rewrite keeps updatedAt (a scope transfer does not
  make content newer)
- WorkspaceModel.delete verifies primary ownership before running the
  resnapshot scrub, so unauthorized callers cannot trigger its writes

* 🐛 fix: derive scope in usage aggregation and follow-up extraction

- usage findByDateRange / getAgentUsageStats fan out over the three
  derivation arms (topic/session/orphan) so transferred history stays in
  the right scope while each arm remains index-bounded
- followUpAction latest-assistant lookup uses the derived message scope
- update both unit test mocks accordingly (Codex P2 round 7)

* 🐛 fix: derive nightly-review activity scope from topic/session anchors

listActiveAgentTargets fans personal-scope activity out over the three
derivation arms (topic/session/orphan, each index-bounded) via UNION ALL,
so histories transferred into a personal scope count toward nightly
review; plugin failure counts join purely by message id since the parent
row is already scope-derived (Codex P2 round 8)

* 🐛 fix: scrub owned workspaces before user deletion cascades them

Deleting a user cascades every workspace they primary-own, which in turn
cascades messages.workspace_id — rows transferred away can carry the old
workspace snapshot under a different author, so the user-keyed scrub
alone missed them. deleteUser now runs the workspace-keyed scrub for each
owned workspace first (Codex P1 round 9). Regression test added.

* 🐛 fix: derive message scope in recent previews and eval message reads

Two read paths still filtered `messages` by its creation-time
user_id/workspace_id snapshot, so they returned nothing once an agent
transfer rehomed the owning topic:

- RecentModel's last-assistant-message subquery is already correlated to
  `topics.id`, which inherits the topic arm's scope check — the snapshot
  predicate was redundant and broke the preview in both transfer
  directions. Dropped it rather than re-deriving the same topic.
- agentEvalExternal.messagesList has no such anchor, so it now uses
  buildMessageScopeWhere; the topicId equality keeps it index-bounded.

* 🐛 fix: guard topic-anchored teammate rows in transfer foreign checks

Messages and message_groups derive their scope from the owning topic at
read time, so every row anchored to a transferred topic moves with it —
but transferHasForeignRows (agent + agent-group variants) only probed
direct sessionId/agentId/groupId linkage. A teammate row carrying only a
topicId (the OpenAPI create shape: agentId optional, sessionId always
null) escaped the guard, letting a non-owner member transfer teammates'
conversations out of the workspace.

Add hasForeignTopicAnchoredMessageRows next to the other derived-scope
helpers (index-bounded innerJoin probes, LIMIT 1) and call it from both
guards, mirroring the existing hasForeignTopicComments pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ⚡️ perf: fan queryAll out over the three derivation arms

The bare derived-scope predicate was queryAll's only filter before
ordering/paging the shared messages table — an unbounded scan on the
TRPC message.listAll / CLI message list path. Fan out over the
topic/session/orphan arms (each index-bounded, same shape as count())
and let the database order/page the UNION ALL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: recheck topic-anchored teammate rows under the transfer lock

transferHasForeignRows runs before the transfer transaction, so it races
message creation: a teammate topic-only message inserted in that window
followed the moved topic past the owner-only guard.

Close it with the same two-sided protocol topic comments already use:

- transferAgents / group transferToWorkspace re-run the topic-anchored
  foreign-row probe INSIDE the transaction, after the topics FOR UPDATE
  lock (message inserts hold FK FOR KEY SHARE on their topic, which
  conflicts with FOR UPDATE — every topic-anchored insert is either
  committed and visible to the recheck, or blocked until the transfer
  ends), gated by a new rejectForeignMessageAuthors option the routers
  set for workspace non-owners.
- The OpenAPI createMessage path (source of the topic-only shape) now
  locks its topic FOR SHARE in the insert transaction and revalidates
  the scope, so a create that raced a committed transfer is rejected
  instead of landing in the moved scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:14:05 -07:00
Arvin Xu 03d4b88a2b chore: remove LOBE-XXX annotation markers from source code (#17689) 2026-07-29 13:09:30 -07:00
Rdmclin2 407da050ca ✨ feat: workspace four-role model with membership role as source of truth (#17555)
* feat: workspace four-role model with membership role as source of truth

* 🐛 fix: align stale role tests and admin API-key manageability with four-role model

- provider settings route test mocks usePermission/useIsWorkspaceLoading
  instead of the removed useIsWorkspaceOwner gate
- workspaceMember model test uses 'admin' (owner is not an assignable role)
- ApiKey settings manage gate follows the server's admin-or-higher rule
  instead of the creator-or-owner row check
- grant workspace_admin the topic_comment content permissions added on canary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: reconcile drifted membership roles from legacy RBAC grants on convergence

Column/RBAC drift predates the reversal: a non-primary row can carry a
stale owner label while its RBAC grant (the pre-reversal truth) says
member. Blanket-demoting stray owners to admin would silently elevate
such members, so the convergence now writes the RBAC projection back
into the column first and only falls back to admin for grant-less rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🔒 feat: hide workspace service-model settings from members

Service-model preferences steer the shared workspace model policy —
Admin-or-higher like Provider: nav entry hidden and the route wrapped
in WorkspaceAdminOnly. Server writes were already admin-gated via
workspace settings update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🔒 fix: clamp legacy non-primary owner labels to admin in permission expansion

Owner is bound to workspaces.primaryOwnerId. Before the convergence
script runs, a stray non-primary role='owner' row must not expand to
the Owner permission matrix — it now counts as Admin at every judgment
point (RbacModel matrix expansion and the workspace owner/admin access
helpers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: harden convergence predicates and ownership-transfer CAS

- convergence projection ignores inactive roles and expired grants,
  mirroring the RBAC checks the data replaces
- transferPrimaryOwnership compare-and-swaps on the still-current
  primaryOwnerId so concurrent transfers cannot produce two owner rows
  before the unique index ships

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 fix: let workspace admins manage OAuth apps from the UI

The server gate is admin-or-higher; the reused OAuth Apps page still
computed canEdit from useIsWorkspaceOwner, hiding create/edit/delete
from Admins. Switch to the manage_settings permission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: unblock transfer for unconverged co-owners and gate API-key creation

- transfer target check accepts a legacy non-primary owner label (counts
  as Admin everywhere else)
- workspace API-key create button follows the server's admin gate so
  Members don't get an enabled flow that always 403s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 07:53:26 +08:00
Rdmclin2 5d6453c798 ✨ feat: workspace-scoped API keys with RBAC & workspace data deletion (#17263)
* ✨ feat: workspace-scoped API keys with RBAC & workspace auth data deletion

Squashed from 6 commits:
- fix: workspace apikey rbac and workspace scope
- feat: support workspace auth delete data
- fix: apikey creator display and messenger back to lobe
- fix: api keys
- chore: optimize dropdown menus
- chore: update i18n files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: project topic row owner userId in TopicModel.query for client ownership filters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: gate owner select-all batch delete behind workspace acknowledge modal & drop stale provider footer test

- BatchActionsDropdown: workspace owners deleting a select-all query hit the
  server's workspace-wide delete (restrictToCreator=false); show the
  workspace-wide label and the elevated WorkspaceDeleteAllModal, mirroring the
  header trash button (Codex P1).
- The provider roadmap footer was removed in #17217 but the workspace provider
  test still asserted it renders; update the stale test (canary CI is red on
  the same test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: bind workspace API keys on the lambda TRPC surface

Mirror the OpenAPI resolveWorkspaceId contract in createLambdaContext:
a personal key is rejected when X-Workspace-Id is present, and a
workspace key is pinned to its own workspace (mismatched header
rejected, workspace applied even without the header).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: enforce workspace API key entitlement on lambda auth & seed RBAC roles at workspace creation

- createLambdaContext: apply the same canUseWorkspaceApiKeys availability
  gate as the OpenAPI workspace middleware before accepting a
  workspace-scoped key (Codex P1).
- WorkspaceModel.create: seed the built-in RBAC roles and grant the creator
  workspace_owner inside the creation transaction, matching the documented
  assignWorkspaceRoleToUser contract; widen seed helpers to accept
  transactions.
- OpenAPI workspace middleware: fall back to the loaded membership role for
  the API-key owner check so pre-seeding workspaces are not rejected
  (Codex P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: re-check workspace API-key issuer owner status on lambda auth

Workspace API keys are owner-only: createLambdaContext now verifies the
issuer still holds owner status (RBAC role with membership fallback via the
new shared hasWorkspaceOwnerAccess helper) before accepting a
workspace-scoped key, matching the OpenAPI workspace middleware. A demoted
or removed owner's key stops authenticating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🌐 chore: refill locales after rebase onto canary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:39:28 +08:00
Rdmclin2 f0c5cd091b 🐛 fix: openapi auth errors return 401 instead of 500 & mask real API key prefix (#17143)
* 📝 chore(agent-testing): log E18 fresh-worktree seed-user NODE_PATH recipe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: openapi auth errors return 401 instead of 500; mask real API key prefix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: keep middleware HTTPException errors in the ApiResponse JSON envelope

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:01:17 +08:00
rdmclin2 fc985b5a76 🐛 fix: attribute openapi chat billing/tracing to the API key's workspace
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:24:45 +08:00
Arvin Xu 20238b7b02 ⬆️ refactor: upgrade Zod to v4 (#16998)
* ⬆️ chore: upgrade Zod to v4

* ⬆️ chore: align drizzle-zod with Zod 4

* 🧰 chore(agent-testing): make verify runs lifecycle-safe

* ✅ test: align Zod 4 validation expectations

* ✅ test: use a valid tracing UUID
2026-07-11 15:09:20 +08:00
Rylan Cai e8af5258a7 ♻️ refactor(Agent): Configurable graph agent (#16944)
* ♻️ refactor: rebuild graph agent runtime

* ✅ test graph runtime integration paths

* 🐛 fix(agent-runtime): stabilize graph agent tests

* ✅ test: fix agent settings content mock

* 🐛 fix graph prompt value typing

* 🐛 fix(agent-runtime): preserve dynamically activated tools

* ✅ test: use offered web browsing tool name

* 🔧 remove graph runtime feature flag gate

* 📝 restore internal flag comment
2026-07-10 15:01:35 +08:00
Rdmclin2 261c7652e5 🐛 fix: workspace problems (#16683)
* fix: workspace optimize

* feat: init market org profile on community

* chore: remove workspace profile

* chore: update community 成员页

* fix: commnity memeber table

* feat: support reserve onboarding

* chore: update i18n files

* feat: workspace ai model and divider scope

* chore: replace  workspace_memeber.role with rbac_user_roles

* fix: workspace ai provider lost

* chore: update i18n files

* fix: codex preview
2026-07-03 23:10:22 +08:00
YuTengjing be0255b746 🔨 chore: update dependency baseline (#16465)
* 🔨 chore: update dependency baseline

* 🔨 chore(cli): update dependency baseline

* 🔨 chore(desktop): update dependency baseline

* 🔨 chore: fix dependency baseline blockers

* 🔨 chore: update TypeScript and Next baseline
2026-06-30 23:18:08 +08:00
Arvin Xu ea597c9bff ♻️ refactor(zod): collapse .nullable().optional() into .nullish() (#16156)
`.nullish()` is defined as `.nullable().optional()` in Zod — same
validation behavior and inferred type (`T | null | undefined`). Pure
mechanical de-duplication, no behavior change.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:31:37 +08:00
Arvin Xu 7b932a01d0 ♻️ refactor(model-bank): rename model type stt to standard asr (runtime migration, non-breaking) (#16002)
* ♻️ refactor(model-bank): rename model type `stt` to the standard `asr`

`asr` (Automatic Speech Recognition) is the conventional term for the
speech-to-text model category, matching the modern model-runtime `transcribe`
API and the `asr.transcribe` tRPC procedure. Standardize the model-type
taxonomy on `asr` across the type schema, model data, UI, API contract and
i18n — without a bulk DB data migration.

Application layer (full rename, no behavioral impact — `type` is only a
category label; nothing branches on it at runtime):
- model-bank: `AiModelTypeSchema` enum `stt` → `asr`; `AISTTModelCard` →
  `AIASRModelCard`; `openaiSTTModels` → `openaiASRModels`
- UI: provider ModelList tab/count/pricing-case/create-form options + community
  ModelTypeIcon map
- aihubmix: map the platform `stt` identifier to LobeHub `asr`
- i18n: rename `providerModels.tabs.stt` / `type.options.stt` keys (default +
  en-US + zh-CN); other locales left to the auto-i18n CI

Persistence — runtime migration instead of a SQL migration ("don't fetch, don't
touch"):
- read-time mapping: the aiInfra repository normalizes any residual `stt` row to
  `asr` on read (builtin models already get their type from model-bank)
- write-time heal: `AiModelModel` create/update/toggle/batch persist `asr`, so
  data lazily migrates whenever a row is naturally written; new writes are `asr`
- shared `normalizeAiModelType` helper in model-bank

OpenAPI — non-breaking:
- input still accepts `stt` as a deprecated alias (no 4xx for old clients) and
  normalizes it to `asr`; responses always emit `asr`; type filter matches both
  `asr` and legacy `stt` rows

No `ai_models` data migration is shipped; old rows stay valid and convert lazily.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(cli): sync `lh model` type inputs with the `stt` → `asr` rename

`stt` was removed from `AiModelTypeSchema`, so the CLI paths that still
advertised and forwarded `--type stt` broke for users following the help text:
`lh model create/edit --type stt` was rejected by the router schema and
`lh model list --type stt` returned no ASR models.

Advertise `asr` in the `--type` help for list/create/edit, and normalize the
legacy `stt` alias to `asr` before forwarding to the API / filtering results,
so existing scripts keep working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 12:54:58 +08:00
Arvin Xu 95dc8e00b0 🐛 fix(agent): centralize inbox agent meta fallback across read paths (#15948)
* 🐛 fix(agent): centralize inbox agent meta fallback across read paths

Extract inbox title/avatar fallback into a shared `inboxAgent` util and
apply it consistently wherever agents are read (models, repositories,
openapi service, topic router) so the inbox agent always resolves to its
default title and avatar when the DB row is blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent): push agent reads into the model layer instead of router-level fallback

Routers/services were querying the `agents` table directly and re-applying the
inbox meta fallback on top — a layering leak. Move those reads down so the
normalization happens once, where the data is owned:

- topic.recentTopics: use `AgentModel.getAgentAvatarsByIds` +
  `ChatGroupModel.getMemberAvatarsByGroupIds` instead of inline `.from(agents)`
- add `ChatGroupModel.getMemberAvatarsByGroupIds`, dedupe the duplicated
  member-avatar query that also lived in `HomeRepository`
- add `AgentModel.listMessengerBindableAgents` (virtual-or-inbox filter,
  `updatedAt DESC`, inbox pinned + normalized) and use it from both
  `messenger.listAgentsForBinding` and `MessengerRouter.fetchUserAgents`,
  unifying the inbox title fallback on `DEFAULT_INBOX_TITLE` (was a stray
  hardcoded `'LobeAI'`)

The `inboxAgent` util is now confined to `packages/database`; no router/service
re-patches inbox meta anymore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent): construct agent models via ctx instead of in-handler

Follow the router convention of building models in the procedure ctx
middleware rather than ad-hoc inside handlers:

- topic: move `agentModel` / `chatGroupModel` into `topicProcedure` ctx
  (ambient `wsId` scope)
- messenger: the bindable-agents scope is input-driven (the scope picker
  passes the workspace), and the procedure is identity-scoped, so expose a
  workspace-parameterized `getAgentModel` factory on ctx and call it in
  `listAgentsForBinding`

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent): drop dead inbox normalization from openapi agent service

The base commit's blanket sweep added `normalizeInboxAgentMeta` to the
openapi `AgentService`, but it doesn't belong there:

- `queryAgents` filters `virtual=false OR NULL` with no `OR slug=inbox`
  escape hatch, and the inbox agent is `virtual=true`, so it's excluded by
  design — the normalize never sees an inbox row (dead code).
- `updateAgent` only normalizes when updating the inbox via the public REST
  CRUD, which shouldn't manage that virtual/system agent.

Remove both calls (restoring pre-branch behavior) and the util import; this
surface lists/edits real agents only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent): own messenger bindable-agent title fallback in the model

- return `isInbox` from `AgentModel.listMessengerBindableAgents` so callers no
  longer recompute it from `slug`
- move the blank-title fallback into the model via an optional `fallbackTitle`
  option; `MessengerRouter.fetchUserAgents` passes "Custom Agent" (text-only
  channel) while `messenger.listAgentsForBinding` omits it and keeps null so the
  web picker applies its own i18n default
- `messenger.listAgentsForBinding` now returns the model rows directly
- revert openapi `agent.service.ts` to canary: the inbox is virtual and excluded
  from that surface, so its earlier normalization was dead code

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 02:18:28 +08:00
LobeHub Bot 3d594e77f5 🌐 chore: translate non-English comments to English in openapi-remaining-services (#15913)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 13:43:26 +08:00