feat: file-based provider configuration with SOPS-encrypted secrets#949
Conversation
Move LLM provider settings from environment variables to JSON config files with SOPS-encrypted secrets. Add provider management UI, settings page with table view, and wire provider resolution through agent config and workflow engine.
…and masked API keys Replace inline editing with dialog-based forms for general settings, API keys, and models. Show masked API keys in the UI, use a table layout for models, and add startsWith nav matching for nested provider routes.
Add pure-JS age keypair generation (via @noble/curves + @scure/base) so tale init auto-generates SOPS encryption keys without requiring external CLI tools. Wire providers/ directory through init, start, and deploy flows: - tale init: generate age keypair, scaffold providers/, write .sops.yaml - tale start: bind-mount providers/ into dev container - tale deploy: sync providers/ to container, warn on key mismatch - sops.ts: auto-fallback to shared dev key when SOPS_AGE_KEY unset - file_actions.ts: gracefully skip providers with undecryptable secrets
Add provider config files to the Docker build and entrypoint seeding logic, matching the existing pattern for agents, workflows, and integrations. Includes .sops.yaml for SOPS encryption config.
…_AGE_KEY Remove .sops.yaml entirely — encryption now uses `sops -e --age` with the public key derived at runtime from SOPS_AGE_KEY via X25519. This collapses the entire SOPS config to a single env var. - Delete .sops.yaml from repo root and examples/providers/ - saveProviderSecret derives public key and passes --age flag - Add @noble/curves + @scure/base to platform for key derivation - Remove dev key fallback from sops.ts (no hardcoded secrets) - Remove .sops.yaml generation from tale init - Remove consistency check from tale deploy - Add providers bind-mount to compose.yml for dev - Remove .sops.yaml seed from entrypoint
…r env Remove SOPS_AGE_KEY from Docker Compose environment blocks since it is now managed as a Convex environment variable. Add SOPS_AGE_KEY to the entrypoint's env var sync list so it gets pushed to the Convex backend. Also comment out the local providers volume mount for production use.
Add optional `provider` field to agent JSON config so agents can pin a specific LLM provider (e.g. openrouter) instead of searching all providers. Centralize the read-parse-resolve pattern into a new `resolveAgentConfig` internal action, removing duplicated filesystem reads from unified_chat, webhooks, and workflow triggers. Introduce a ModelSelector component in the chat input that lets users pick from the agent's supportedModels list, passing the override through to the backend via a new `modelId` arg.
Place both Agent and Model selectors together in the bottom-right of the chat input, keeping the attach button alone on the left. This matches the intended layout where agent and model selection are visually grouped.
…rovider config - Extract resolve_model.ts to eliminate repeated resolve→create→getModel boilerplate across conversations, agent_chat, summarization, workflows, image analysis, and attachments - Delete agent_runtime_config.ts and openai_provider.ts (superseded by file-based provider resolution) - Remove hardcoded "provider" field from example agent JSON files - Replace v.any() return types on resolveModelData/resolveModelByTag with typed object validators - Add client-side validation and error toasts to provider edit UI - Internationalize provider tag labels and error messages - Add unique model ID and single-default refinements to provider schema - Python config: add get_chat_config/get_embedding_config/get_vision_config with provider-file-first, env-var-fallback resolution - CLI: include provider configs in init/update checksums, fix ensureEnv destructuring - Docker entrypoint: skip re-seeding providers with user .history, skip .secrets.json during seed - Improve error logging in provider file loading (no silent catches)
The provider name was lost during model resolution — only the languageModel was destructured, discarding modelData.providerName. This caused messageMetadata to store an empty string for provider and the UI to render empty parentheses next to the model name. Capture modelData from resolveLanguageModel and pass the resolved provider name through the generation chain. Also guard the UI so old records with empty provider don't show "()".
Prefix unused destructured `provider` variable, add @ai-sdk/provider as explicit dependency, remove unused exports, and fix package.json formatting.
📝 WalkthroughWalkthroughThis PR introduces a comprehensive provider-based model selection system to replace environment-variable-driven model presets. It adds file-based provider configuration loading with SOPS encryption support, refactors agent configuration to use explicit Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 45
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
services/crawler/Dockerfile (2)
19-48:⚠️ Potential issue | 🟠 MajorUse
--no-install-recommendsin apt install.This Docker layer currently pulls recommended packages unnecessarily, increasing size and risk surface.
Suggested fix
-RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/crawler/Dockerfile` around lines 19 - 48, The apt install in the Dockerfile RUN line currently installs recommended packages; update the RUN command that starts with "apt-get update && apt-get install -y" to include the flag --no-install-recommends so only required packages are installed (preserve the existing package list and trailing "&& rm -rf /var/lib/apt/lists/*"). This change should be applied to the Dockerfile RUN that installs tini, curl, fonts-*, lib*- packages to reduce image size and attack surface.
18-50:⚠️ Potential issue | 🔴 CriticalSOPS installation hardcodes x86-64 architecture and lacks binary integrity verification.
The Dockerfile will fail on ARM64 systems and downloads an unverified binary, even though SOPS 3.9.4 provides both
linux.amd64andlinux.arm64binaries along with checksums for verification.Use
ARG TARGETARCHto support multiarch builds and verify the binary against the published checksums:Suggested fix
ARG SOPS_VERSION=3.9.4 +ARG TARGETARCH RUN apt-get update && apt-get install -y \ @@ - && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64" -o /usr/local/bin/sops && chmod +x /usr/local/bin/sops + && case "${TARGETARCH}" in amd64|arm64) ;; *) echo "Unsupported arch: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.checksums.txt" -o /tmp/sops.checksums \ + && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.${TARGETARCH}" -o /usr/local/bin/sops \ + && grep "sops-v${SOPS_VERSION}.linux.${TARGETARCH}\$" /tmp/sops.checksums | sha256sum -c - \ + && chmod +x /usr/local/bin/sops \ + && rm -f /tmp/sops.checksums🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/crawler/Dockerfile` around lines 18 - 50, The SOPS install currently hardcodes the linux.amd64 binary (ARG SOPS_VERSION and the curl to /usr/local/bin/sops) and skips checksum verification; change it to support multi-arch by adding ARG TARGETARCH and use it to select the correct artifact suffix (e.g., linux.${TARGETARCH}) when downloading /usr/local/bin/sops, then download the corresponding checksums file (or a published SHA256 for that release), verify the downloaded binary against the checksum before chmod +x, and fail the build if verification fails to avoid installing unverified or wrong-arch binaries.services/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.ts (1)
89-101:⚠️ Potential issue | 🟠 MajorTie the prompt-budget check to the resolved model.
checkPromptBudget()still uses the staticAGENT_CONTEXT_CONFIGS.workflowlimit, but this entry point now runs against caller-suppliedlanguageModels. If the selected provider/model has a smaller context window than the workflow default, oversized prompts will clear preflight and then fail at the provider call.Also applies to: 110-110
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.ts` around lines 89 - 101, The prompt-budget check currently uses the static AGENT_CONTEXT_CONFIGS.workflow limit but must use the actual resolved model from this entry point; update executeAgentWithTools so calls to checkPromptBudget (the ones near executeAgentWithTools signature and at the later call around line 110) pass the context-window/limit derived from the provided _args.languageModel (or resolveModel helper if you canonicalize the model) instead of AGENT_CONTEXT_CONFIGS.workflow, e.g., compute the model's max context size from LanguageModelV3 and supply that value to checkPromptBudget so oversized prompts are rejected based on the selected provider/model.
♻️ Duplicate comments (2)
services/platform/convex/agent_tools/files/helpers/analyze_text.ts (1)
8-9:⚠️ Potential issue | 🟡 MinorSame unlisted dependency issue:
@ai-sdk/provider.As flagged by the pipeline,
@ai-sdk/providerneeds to be added topackage.jsondependencies.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/agent_tools/files/helpers/analyze_text.ts` around lines 8 - 9, The file imports an external type from "@ai-sdk/provider" (see the import of LanguageModelV3) but that package is not listed in package.json; add "@ai-sdk/provider" with an appropriate version to the dependencies in package.json (or update existing dependency entries) and run install so the import in services/platform/convex/agent_tools/files/helpers/analyze_text.ts resolves.services/platform/convex/lib/summarize_context.ts (1)
11-12:⚠️ Potential issue | 🟡 MinorSame unlisted dependency issue:
@ai-sdk/provider.As flagged by the pipeline in multiple files,
@ai-sdk/providerneeds to be added topackage.jsondependencies.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/lib/summarize_context.ts` around lines 11 - 12, The file imports LanguageModelV3 from the unlisted package '@ai-sdk/provider' (see the import in summarize_context.ts), which causes the pipeline failure; add '@ai-sdk/provider' to package.json dependencies with the correct version used across the repo, run your package manager to update node_modules and the lockfile (npm install / yarn install / pnpm install), and commit the updated package.json and lockfile so the import in summarize_context.ts resolves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/providers/openrouter.json`:
- Around line 60-63: Update the German localization strings by correcting the
spelling mistakes in the description values: change "fuhrenden" to "führenden"
and "fur" to "für" (specifically in the top-level "description" value
"Multi-Modell-KI-Gateway mit Zugang zu fuhrenden LLM-Anbietern" and in the model
description for "anthropic/claude-opus-4.6" where "fur" appears); ensure the
corrected UTF-8 characters are used so the strings read "führenden" and "für".
In `@packages/tale_shared/src/tale_shared/config/providers.py`:
- Around line 137-152: The current helpers (e.g., get_chat_model) coerce
provider.api_key None to an empty string which bypasses the env-key fallback;
instead, if provider.api_key is None the helper should raise a descriptive error
(e.g., ValueError("No API key found for provider X")) so BaseServiceSettings can
catch and fall back to env secrets; update get_chat_model (and the equivalent
functions handling "embedding" and "base" models) to return the base_url and
model.id as before but do not convert None to ""; raise when provider.api_key is
None, otherwise use provider.api_key.
- Around line 42-53: The loader in load_providers currently builds base from
config_dir, TALE_CONFIG_DIR, CONFIG_DIR but ignores the PROVIDERS_DIR env
override used by the JS loader; update load_providers (and its computation of
base/providers_dir) to first check os.environ.get("PROVIDERS_DIR") and, if
present, use that path (or its parent) as the providers directory so Python
resolves providers the same way as
services/platform/convex/providers/file_utils.ts; ensure you still respect an
explicit config_dir param and fall back to TALE_CONFIG_DIR/CONFIG_DIR only when
PROVIDERS_DIR is not set.
In `@packages/tale_shared/src/tale_shared/utils/sops.py`:
- Around line 30-32: The cached JSON object (data) is returned directly which
allows callers to mutate shared state and also doesn't enforce that JSON is an
object; change the logic that sets and returns _cache[str(path)] = (data, mtime)
so that you validate that data is a dict (raise TypeError if not), store a deep
copy in the cache, and always return a fresh deep copy to callers; use the
existing locals (data, _cache, path, mtime, and result.stdout) and Python's
copy.deepcopy (or json.loads+json.dumps roundtrip) to implement immutability and
shape enforcement.
- Around line 18-23: The subprocess.run call uses a bare "sops" executable which
can be shadowed; resolve the sops binary to an absolute path (e.g., via
shutil.which("sops")) before spawning and use that path in the argument list
instead of "sops"; if which returns None, raise a clear error (or log and exit)
so you don't call subprocess.run with a non-resolved name. Ensure you import
shutil and update the invocation that builds the argv (the call currently
creating result = subprocess.run([... "sops", ...], ...)) to use the resolved
path variable.
In `@services/platform/app/features/agents/components/agent-create-dialog.tsx`:
- Line 85: The hardcoded default model string in the supportedModels array
('moonshotai/kimi-k2.5') should be centralized; update AgentCreateDialog (look
for supportedModels in agent-create-dialog.tsx) to import or read a shared
DEFAULT_MODEL constant (or derive it from the provider configuration module)
instead of embedding the literal, and replace the inline value so the component
uses that single source of truth.
In `@services/platform/app/features/agents/hooks/use-agents-table-config.tsx`:
- Around line 49-55: The column cell currently only shows the first supported
model by reading row.original.supportedModels?.[0], which hides additional
allowed models; update the 'model' column's cell renderer to either display the
full supportedModels array (e.g., map to multiple Badge elements, join into a
truncated string with an ellipsis, or show a tooltip with the full list) or
explicitly indicate it's the default model (e.g., label "default: <model>").
Modify the cell function in use-agents-table-config.tsx to iterate over
row.original.supportedModels when present and render all entries (with
truncation/tooltip handling) or change the label to clarify it is the default,
keeping support for empty/null values.
In `@services/platform/app/features/chat/components/model-selector.tsx`:
- Around line 64-75: The effect is re-running because the whole
selectedModelOverrides object reference changes; to stabilize it, replace
selectedModelOverrides in the dependency array with the specific value used
(e.g. selectedModelOverrides[effectiveAgent?.name]) or a memoized selector so
the effect only depends on the relevant override value; update the useEffect
dependencies and keep the body logic using effectiveAgent?.name,
supportedModels, selectedModelOverrides[effectiveAgent?.name], and
setSelectedModelOverride (or memoize selectedModelOverrides via
useMemo/useCallback in the provider) so unnecessary runs are avoided.
In
`@services/platform/app/features/settings/providers/components/provider-add-dialog.tsx`:
- Around line 28-32: The ProviderAddDialog component currently destructures
organizationId as _organizationId but never uses it; either remove
organizationId from the ProviderAddDialogProps and the destructuring in
ProviderAddDialog, or if org-scoped behavior is planned, restore the param name
and add a brief TODO comment above ProviderAddDialog (e.g., "// TODO: use
organizationId for multi-org support") and keep organizationId in
ProviderAddDialogProps; update any callers or types accordingly to keep
signatures consistent.
In
`@services/platform/app/features/settings/providers/components/provider-edit-panel.tsx`:
- Around line 88-118: Replace the unchecked type assertion on readResult.config
in the useEffect with a runtime validation using the shared providerJsonSchema:
import providerJsonSchema, call providerJsonSchema.safeParse(readResult.config)
inside the existing if-block (before using config), log parse errors (e.g.,
processLogger or console.error) and return early if validation fails, then use
parseResult.data as the validated config to call setDisplayName, setDescription,
setBaseUrl, setModels and setApiKey; keep the rest of the state-setting logic
and ensure you reference readResult, providerJsonSchema, and the setter
functions (setModels, setDisplayName, setDescription, setBaseUrl, setApiKey).
In
`@services/platform/app/features/settings/providers/components/providers-page.tsx`:
- Around line 23-30: The action listProviders currently returns v.any(), and the
local ProviderListItem interface is missing fields (i18n, error fields) causing
unsafe casts in useListProviders; replace v.any() with a proper v.object()
schema that matches the handler's success shape (including i18n and model
fields) and the error shape (status, message), export/derive the TypeScript type
from that schema and update useListProviders to use that type instead of casting
to ProviderListItem[], then remove the incomplete local ProviderListItem
interface and any unsafe "as ProviderListItem[]" casts so callers receive a
typed response consistent with the handler.
In
`@services/platform/app/features/settings/providers/components/providers-table.tsx`:
- Around line 39-53: The current useMemo mapper for providers drops any provider
missing displayName; update the mapping in the providers useMemo so it doesn't
filter those out — iterate rawProviders and for each p create a ProviderRow
using displayName: p.displayName ?? p.name (or fallback to p.name when
p.displayName is undefined) and copy the other fields (name, description,
baseUrl, modelCount); ensure you only skip truly null/undefined p values but not
objects missing displayName so the table can fall back to name as the UI
expects.
In `@services/platform/app/features/settings/providers/hooks/queries.ts`:
- Around line 27-33: The hook useHasProviderSecret currently inlines its cache
key; add a corresponding helper to the shared configKeys (e.g., a
providers.secret or providers.providerSecret key factory) and update
useHasProviderSecret to use that helper instead of the inline array; keep the
same parameters and still call api.providers.file_actions.hasProviderSecret with
{ orgSlug, providerName } so only the cache-key construction moves to
configKeys.
In
`@services/platform/app/features/settings/providers/hooks/use-provider-config-context.tsx`:
- Around line 58-65: The effect currently avoids resetting state when there are
unsaved edits but misses provider switches; modify the useEffect in
use-provider-config-context.tsx to also force a reset when the provider changes
by (1) comparing the current providerName to initialRef.current?.providerName
(e.g., const providerChanged = initialRef.current?.providerName !==
providerName), (2) updating the conditional to run setConfig(initialConfig) and
initialRef.current = initialConfig when either !hasUnsavedEdits or
providerChanged, and (3) include providerName in the effect dependency array so
provider changes trigger the check; keep the existing configRef and initialRef
usage and update only the conditional and deps.
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/instructions.tsx:
- Around line 69-75: The <ul className="space-y-1 text-sm"> element is missing
an explicit role, so VoiceOver/Safari may not announce it as a list; add
role="list" to that <ul> (the one rendering {(config.supportedModels ??
[]).map(...)} with <li key={model}> entries) to restore list semantics for
screen readers.
In
`@services/platform/app/routes/dashboard/`$id/settings/providers/$providerName.tsx:
- Around line 501-556: The list rows use key={index} on the TableRow for
config.models which is fragile; generate and use a stable client-side id for
each model (e.g., add a temporary id field when models are created/loaded) and
use that field as the React key instead of index. Update the code paths that
create or normalize config.models to assign model._clientId (or model.tempId)
and then change the renderer to key={model._clientId} (while keeping
openEditDialog(index) and setDeleteIndex(index) logic intact or preferably
update those handlers to locate the model by _clientId when necessary) so rows
remain stable across reorders or future drag-and-drop additions.
- Around line 638-639: The placeholder "e.g., 1536" in the Input JSX is
hardcoded; replace it with a localized string from the file's translation hook
(e.g., the t returned by useTranslation/useTranslations) so the Input's
placeholder prop uses t(...) instead of the literal. Locate the Input component
(placeholder="e.g., 1536") and change it to
placeholder={t('dashboard.providers.placeholderExample', { example: '1536' })}
(or the project's standard key), ensuring you import/use the existing
translation hook already used elsewhere in this file.
- Around line 159-161: Add a confirmation dialog before calling handleDelete:
introduce a boolean state like deleteConfirmOpen and change the destructive
Button's onClick to set deleteConfirmOpen=true instead of calling handleDelete
directly; render a FormDialog/ConfirmDialog (similar to the model deletion flow)
that shows the destructive warning, has Cancel (closes dialog) and Confirm
(calls handleDelete, then closes dialog) actions, and ensure the confirm action
uses the same destructive styling/props as the original Button; reference
handleDelete, Button, and deleteConfirmOpen when implementing.
In `@services/platform/convex/agent_tools/files/helpers/analyze_image.ts`:
- Around line 56-61: The call to resolveLanguageModel is currently outside the
try block causing visionModelId to be undefined if it throws and making the
function throw instead of returning an AnalyzeImageResult error; move the
resolveLanguageModel call (and the const { languageModel, modelData } /
visionModelId assignment) into the existing try block or wrap it in its own
try-catch, and on error return the same structured AnalyzeImageResult error
object used in the existing error handling (matching the pattern around lines
163-174) so resolveLanguageModel, visionModelId, and AnalyzeImageResult are
handled consistently.
In `@services/platform/convex/agent_tools/files/helpers/vision_agent.ts`:
- Line 8: The package `@ai-sdk/provider` is imported directly (e.g.
LanguageModelV3 in vision_agent.ts and ~10 other files) but is not declared in
the services/platform package.json; add `@ai-sdk/provider` as a direct dependency
in services/platform/package.json (choose the same version used elsewhere in the
repo or workspace), run install to update the lockfile, and ensure CI sees the
new dependency so direct imports of LanguageModelV3 succeed.
In `@services/platform/convex/agent_tools/files/text_tool.ts`:
- Around line 213-216: The inline type cast "as
import('@ai-sdk/provider').LanguageModelV3" should be replaced with a top-level
type import; add "import type { LanguageModelV3 } from '@ai-sdk/provider'" at
the top of the file and change the cast to "as LanguageModelV3" where
ctx.agent!.options.languageModel is used (keep the existing disable comment if
the non-null assertion is needed).
In `@services/platform/convex/agents/file_actions.ts`:
- Around line 428-431: The current logic in file_actions.ts silently ignores an
unauthorized model override (checking args.modelId against
result.config.supportedModels and only setting config.model when matching),
which can cause frontend/backend mismatch; change this to explicitly reject
unsupported overrides: when args.modelId is present and NOT included in
result.config.supportedModels, return or throw a validation error (or set an
explicit failure response) instead of falling back to the agent default,
otherwise continue to set config.model = args.modelId as before—update the
handler that reads args.modelId/result.config.supportedModels/config.model to
enforce this validation and surface a clear error back to the caller.
In `@services/platform/convex/agents/file_utils.ts`:
- Around line 36-37: parseAgentJson currently validates raw agent JSON against
agentJsonSchema which requires supportedModels, so legacy agent files using
modelPreset or modelId fail validation; add a normalization step in
parseAgentJson (before calling agentJsonSchema.parse/validate) that checks for
legacy keys modelPreset and modelId and maps them into supportedModels (e.g., if
modelPreset present, set supportedModels = [modelPreset]; if modelId present,
set supportedModels = [modelId]; also remove or ignore legacy keys afterward),
or alternatively update agentJsonSchema with a Zod.preprocess/transform that
accepts modelPreset/modelId and outputs supportedModels; update references to
supportedModels in downstream code to rely on the normalized field.
In `@services/platform/convex/agents/translate_fields.ts`:
- Around line 106-109: The call to resolveLanguageModel() is happening before
the retry/fallback logic so any provider/decryption failure throws instead of
returning the fallback; move resolveLanguageModel (and subsequent
createTranslationAgent) inside the retry path or surround it with try/catch
inside the existing retry loop in translateFields so that on failure you return
the existing fallback object ({ translated: args.fields, error }) and only
attempt agent creation when resolveLanguageModel succeeds; ensure you catch and
propagate the error into the fallback rather than letting it escape.
In `@services/platform/convex/agents/unified_chat.ts`:
- Around line 53-60: After resolving agent config in startAgentChat/startChat
(where you call internal.agents.file_actions.resolveAgentConfig and read
agentConfig.roleRestriction), add the same roleRestriction validation used in
create_delegation_tool (check for agentConfig.roleRestriction ===
'admin_developer') and call checkRoleAccess(ctx, args.organizationId,
args.actor) (or the existing checkRoleAccess helper) before proceeding; ensure
the validation happens immediately after the resolveAgentConfig call and
short-circuits with an appropriate error/permission response if the check fails
so restricted agents are not accessible to non-admins/developers.
In `@services/platform/convex/lib/age_keygen.ts`:
- Around line 23-26: The code currently lowercases the raw secretKey and checks
for the bech32 separator but doesn't trim it, causing trailing
whitespace/newlines to produce false negatives; update the handling of secretKey
(the variable used to compute lowercase and the includes('1') check) to trim
whitespace first (e.g., use secretKey.trim() before toLowerCase and the includes
check) so valid keys with surrounding whitespace succeed, keeping the same error
throw for missing separator.
In `@services/platform/convex/lib/attachments/process_attachments.ts`:
- Line 8: The import of LanguageModelV3 from `@ai-sdk/provider` is missing a
declared dependency; add "@ai-sdk/provider" to the
services/platform/package.json dependencies (use the same version used by
`@ai-sdk/provider-utils` or the repo's provider versioning convention), then run
the package manager install (npm/yarn/pnpm) and rebuild so imports like
LanguageModelV3 in process_attachments.ts resolve correctly; ensure package.json
and lockfile are committed.
In `@services/platform/convex/lib/create_agent_config.ts`:
- Around line 1-2: The import of the type LanguageModelV3 in
create_agent_config.ts is a type-only import from `@ai-sdk/provider` but the
package is missing from package.json; add "@ai-sdk/provider" to the
devDependencies section of services/platform/package.json (as a devDependency
since it’s used only for typing) so TypeScript resolution and CI installs
succeed, then run install to update lockfile.
In `@services/platform/convex/lib/sops.ts`:
- Around line 20-27: The decryptSecretsFile function currently calls
stat(filePath) which will throw if the file is missing; wrap the stat call in a
try/catch (or perform an explicit existence check) and rethrow a clearer,
contextual error message that includes the filePath and original error (or
return a controlled error) so callers get a descriptive failure; reference
decryptSecretsFile, stat(filePath), and the cache lookup when implementing this
change.
In `@services/platform/convex/lib/summarization/auto_summarize.ts`:
- Around line 17-18: The import of LanguageModelV3 in auto_summarize.ts
references the package `@ai-sdk/provider` which is not declared in
services/platform/package.json; update that package.json by adding
"@ai-sdk/provider" with a compatible version (match the version used by other
ai-sdk packages or the latest compatible semver) to the "dependencies" section
so the import is explicit and deterministic; after updating, run npm/yarn
install and verify the build imports (e.g., LanguageModelV3) resolve without
relying on transitive deps.
In `@services/platform/convex/lib/summarization/internal_actions.ts`:
- Around line 18-24: The current code always resolves the default chat model
with resolveLanguageModel(ctx, { tag: 'chat' }) and ignores any per-thread
override; fix it by loading the thread using the provided threadId (from args),
read its stored model/provider override field (e.g., thread.languageModel or
thread.modelOverride), and use that when resolving the model or passing the
languageModel into autoSummarizeIfNeededModel — e.g., call resolveLanguageModel
with the thread’s override (or include the override in the options passed to
resolveLanguageModel) so autoSummarizeIfNeededModel receives the thread-specific
languageModel instead of the org default.
In `@services/platform/convex/providers/file_actions.ts`:
- Around line 331-345: The resolver currently picks the first model whose tags
include args.tag, which is order-dependent; change resolveModelByTag so for each
provider you first filter provider.config.models to those whose (m.tags as
readonly string[]).includes(args.tag) and then pick the one with m.default ===
true if present, otherwise fall back to the first filtered model; return the
same object shape (providerName, baseUrl, apiKey, modelId, dimensions,
supportsStructuredOutputs) using that selected definition instead of the current
single find() result.
- Around line 137-145: The readProvider action currently authenticates the
caller with authComponent.getAuthUser but does not authorize args.orgSlug, so
any signed-in user can access another org's provider files; update the
readProvider handler to perform an organization-bound authorization check before
calling readProviderFile by invoking your auth/authorization helper (e.g., an
assertOrganizationMember or authorizeOrganizationAccess-style function) with
ctx, the resolved authUser and args.orgSlug (do this in the readProvider handler
right after authComponent.getAuthUser and before readProviderFile), and avoid
performing membership queries later in downstream code—only proceed to call
readProviderFile(args.orgSlug, args.providerName) if the authorization check
passes.
In `@services/platform/convex/providers/resolve_model.ts`:
- Around line 46-55: The resolver wrappers (e.g., resolveLanguageModel) call
org-scoped internal actions (internal.providers.file_actions.resolveModelByTag)
but never accept or forward an org identifier, so lookups default to the
implicit 'default' provider directory; update the function signatures (opts) to
include orgSlug?: string and pass that orgSlug through to the ctx.runAction call
(and any sibling wrappers around lines 62-71 that call resolveModelByTag) so the
internal action receives the orgSlug and resolves provider configs under
/providers/<orgSlug>/...; ensure the ResolvedLanguageModel return type and any
callers are adjusted to accept the new optional orgSlug parameter.
- Around line 10-12: The code imports the type LanguageModelV3 from
`@ai-sdk/provider` but that package is not declared in the workspace dependencies,
causing lint/type-aware install failures; either add "@ai-sdk/provider" to
services/platform/package.json dependencies or change the import to obtain
LanguageModelV3 from `@ai-sdk/openai-compatible` if that package re-exports it
(the relevant symbols to update are the LanguageModelV3 import and any files
that reference it, e.g., resolve_model.ts where createOpenAICompatible is used);
update package.json and run the linter/build to confirm the unlisted-dependency
error is resolved.
In `@services/platform/convex/providers/validators.ts`:
- Around line 1-5: Replace the local regex-based validator with the shared Zod
schema: remove PROVIDER_NAME_REGEX and change validateProviderName to import the
provider name schema from lib/shared/validators (e.g. import {
providerNameSchema } from 'lib/shared/validators') and use
providerNameSchema.safeParse(name).success (or providerNameSchema.parse inside
try/catch) to perform validation so server and client use the identical Zod
rule.
In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.ts`:
- Line 12: The new import of LanguageModelV3 from `@ai-sdk/provider` in
execute_agent_with_tools.ts introduces a missing package dependency; add
"@ai-sdk/provider" to the services/platform/package.json dependencies (use the
same version used elsewhere in the repo or the workspace root to avoid
duplicates), run the package manager install to update the lockfile, and ensure
any workspace tooling (pnpm/yarn workspace) is aware of the new dependency so
the import of LanguageModelV3 resolves correctly at build time.
In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/utils/validate_and_normalize_config.ts`:
- Around line 28-34: The variable name envModel is misleading now that it holds
the caller-provided defaultModel; rename envModel to resolvedModel (or similar)
throughout validate_and_normalize_config (and any referenced helpers) so the
identifier reflects its source, update all usages and any related JSDoc/comments
to use resolvedModel, and run tests/typechecks to ensure no references remain to
envModel.
In `@services/platform/Dockerfile`:
- Around line 128-132: The Dockerfile currently downloads a hardcoded amd64 SOPS
binary (ARG SOPS_VERSION and the curl line) which breaks arm64 builds; change
the download to select the correct binary based on the build target (use
Docker's build-time platform variable like TARGETPLATFORM or detect platform)
and map it to the proper suffix (e.g., amd64 vs arm64) before curling the file
and making it executable, ensuring the curl target uses the resolved
architecture variable instead of the fixed "amd64".
- Around line 129-132: The Dockerfile currently downloads the SOPS binary and
makes it executable without verification; update the RUN step that references
SOPS_VERSION so it first fetches the corresponding release checksums (and
optional Cosign signature), verify the downloaded
sops-v${SOPS_VERSION}.linux.amd64 against the checksums (for example using
sha256sum -c or validating the checksums signature with cosign), and only then
chmod +x /usr/local/bin/sops; ensure failure of verification aborts the build
and that the checksum file is removed after verification.
In `@services/platform/lib/config-watcher.ts`:
- Around line 101-103: The current filter uses rel.endsWith('.secrets.json')
which excludes any *.secrets.json globally; restrict this to provider sidecars
by only returning when the path points into the providers folder (e.g. check
that rel includes or starts with "providers/" in addition to ending with
".secrets.json"). Update the conditional in config-watcher.ts that examines rel
so it only early-returns for provider sidecar files (e.g. combine a providers/
path check with the .secrets.json suffix test) while leaving other
*.secrets.json files untouched.
- Around line 73-77: The providers branch currently trusts rest[0] and derives
slug from filename, which lets extra path segments slip through; update the
providers handling in config-watcher.ts (the branch using variables type, rest,
orgSlug, slug) to first validate the path shape by ensuring rest has exactly one
element and that rest[0] matches the expected filename pattern (e.g.,
/^[^\/]+\.json$/) before computing slug = filename.replace(/\.json$/, ''), and
if validation fails reject the path (return null/undefined or throw) so only
canonical "providers/[`@org/`]name.json" shapes are accepted.
In `@services/platform/messages/en.json`:
- Around line 1402-1458: The JSON has a duplicate "description" key in the
settings.providers block (one with "Manage LLM providers and their models" and
one with "Description"), causing the first value to be overwritten; rename one
of the keys (for example change the short label "description" =>
"descriptionLabel" or "description_short") and update any places that reference
that key (e.g., UI code that reads settings.providers.description) so each key
is unique (alternatively keep "description" for the section header and rename
the shorter label to "descriptionLabel" and adjust references to that new key).
In `@services/rag/Dockerfile`:
- Around line 16-22: The Dockerfile currently hardcodes the x86 binary via ARG
SOPS_VERSION and the RUN that writes to /usr/local/bin/sops; change it to detect
the container architecture (e.g., via uname -m or dpkg --print-architecture ->
map to linux.amd64 vs linux.arm64), download the matching
sops-${SOPS_VERSION}.${ARCH} binary and the accompanying checksums file
(checksums.txt) from the same GitHub release, verify the downloaded binary
against the checksum (sha256) before installing, and then chmod +x
/usr/local/bin/sops; ensure these changes reference ARG SOPS_VERSION, the binary
target /usr/local/bin/sops, and the checksums file so ARM64 builds succeed and
integrity is verified.
In `@tools/cli/src/lib/crypto/age-keygen.ts`:
- Around line 11-14: The function deriveAgePublicKey is currently unused in the
CLI; either remove its definition or explicitly mark it as part of the public
API so it doesn't trigger warnings—locate the deriveAgePublicKey function in the
age-keygen module and if you intend it for external use add a clear `// `@public``
comment and ensure it's exported in the module's public surface, otherwise
delete the deriveAgePublicKey implementation and any related unused imports;
keep the AgeKeypair interface and generateAgeKeypair (used by generateAgeKeypair
-> ensure-env usage) intact.
---
Outside diff comments:
In `@services/crawler/Dockerfile`:
- Around line 19-48: The apt install in the Dockerfile RUN line currently
installs recommended packages; update the RUN command that starts with "apt-get
update && apt-get install -y" to include the flag --no-install-recommends so
only required packages are installed (preserve the existing package list and
trailing "&& rm -rf /var/lib/apt/lists/*"). This change should be applied to the
Dockerfile RUN that installs tini, curl, fonts-*, lib*- packages to reduce image
size and attack surface.
- Around line 18-50: The SOPS install currently hardcodes the linux.amd64 binary
(ARG SOPS_VERSION and the curl to /usr/local/bin/sops) and skips checksum
verification; change it to support multi-arch by adding ARG TARGETARCH and use
it to select the correct artifact suffix (e.g., linux.${TARGETARCH}) when
downloading /usr/local/bin/sops, then download the corresponding checksums file
(or a published SHA256 for that release), verify the downloaded binary against
the checksum before chmod +x, and fail the build if verification fails to avoid
installing unverified or wrong-arch binaries.
In
`@services/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.ts`:
- Around line 89-101: The prompt-budget check currently uses the static
AGENT_CONTEXT_CONFIGS.workflow limit but must use the actual resolved model from
this entry point; update executeAgentWithTools so calls to checkPromptBudget
(the ones near executeAgentWithTools signature and at the later call around line
110) pass the context-window/limit derived from the provided _args.languageModel
(or resolveModel helper if you canonicalize the model) instead of
AGENT_CONTEXT_CONFIGS.workflow, e.g., compute the model's max context size from
LanguageModelV3 and supply that value to checkPromptBudget so oversized prompts
are rejected based on the selected provider/model.
---
Duplicate comments:
In `@services/platform/convex/agent_tools/files/helpers/analyze_text.ts`:
- Around line 8-9: The file imports an external type from "@ai-sdk/provider"
(see the import of LanguageModelV3) but that package is not listed in
package.json; add "@ai-sdk/provider" with an appropriate version to the
dependencies in package.json (or update existing dependency entries) and run
install so the import in
services/platform/convex/agent_tools/files/helpers/analyze_text.ts resolves.
In `@services/platform/convex/lib/summarize_context.ts`:
- Around line 11-12: The file imports LanguageModelV3 from the unlisted package
'@ai-sdk/provider' (see the import in summarize_context.ts), which causes the
pipeline failure; add '@ai-sdk/provider' to package.json dependencies with the
correct version used across the repo, run your package manager to update
node_modules and the lockfile (npm install / yarn install / pnpm install), and
commit the updated package.json and lockfile so the import in
summarize_context.ts resolves.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f51a313d-0feb-4ef7-aedd-87b36b528ef1
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockservices/platform/convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (108)
.env.examplecompose.ymlexamples/agents/chat-agent.jsonexamples/agents/crm-assistant.jsonexamples/agents/file-assistant.jsonexamples/agents/integration-assistant.jsonexamples/agents/web-assistant.jsonexamples/agents/workflow-assistant.jsonexamples/providers/openrouter.jsonexamples/providers/openrouter.secrets.jsonpackages/tale_shared/src/tale_shared/config/__init__.pypackages/tale_shared/src/tale_shared/config/base.pypackages/tale_shared/src/tale_shared/config/providers.pypackages/tale_shared/src/tale_shared/utils/__init__.pypackages/tale_shared/src/tale_shared/utils/sops.pyservices/crawler/Dockerfileservices/platform/Dockerfileservices/platform/app/features/agents/components/agent-create-dialog.tsxservices/platform/app/features/agents/components/agents-table.tsxservices/platform/app/features/agents/hooks/queries.tsservices/platform/app/features/agents/hooks/use-agents-table-config.tsxservices/platform/app/features/chat/components/chat-input.tsxservices/platform/app/features/chat/components/chat-interface.tsxservices/platform/app/features/chat/components/model-selector.tsxservices/platform/app/features/chat/context/chat-layout-context.tsxservices/platform/app/features/chat/hooks/queries.tsservices/platform/app/features/chat/hooks/use-send-message.tsservices/platform/app/features/settings/components/settings-navigation.tsxservices/platform/app/features/settings/providers/components/provider-add-dialog.tsxservices/platform/app/features/settings/providers/components/provider-edit-panel.tsxservices/platform/app/features/settings/providers/components/providers-page.tsxservices/platform/app/features/settings/providers/components/providers-table.tsxservices/platform/app/features/settings/providers/hooks/mutations.tsservices/platform/app/features/settings/providers/hooks/queries.tsservices/platform/app/features/settings/providers/hooks/use-provider-config-context.tsxservices/platform/app/features/settings/providers/hooks/use-providers-table-config.tsxservices/platform/app/routeTree.gen.tsservices/platform/app/routes/dashboard/$id/agents/$agentId/instructions.tsxservices/platform/app/routes/dashboard/$id/settings/providers.tsxservices/platform/app/routes/dashboard/$id/settings/providers/$providerName.tsxservices/platform/app/routes/dashboard/$id/settings/providers/index.tsxservices/platform/convex/agent_tools/delegation/create_delegation_tool.tsservices/platform/convex/agent_tools/delegation/load_delegation_agents.tsservices/platform/convex/agent_tools/files/helpers/analyze_image.tsservices/platform/convex/agent_tools/files/helpers/analyze_image_by_url.tsservices/platform/convex/agent_tools/files/helpers/analyze_text.tsservices/platform/convex/agent_tools/files/helpers/vision_agent.tsservices/platform/convex/agent_tools/files/image_tool.tsservices/platform/convex/agent_tools/files/text_tool.tsservices/platform/convex/agent_tools/human_input/mutations.tsservices/platform/convex/agent_tools/location/mutations.tsservices/platform/convex/agent_tools/workflows/__tests__/trigger_completion_action.test.tsservices/platform/convex/agent_tools/workflows/internal_mutations.tsservices/platform/convex/agent_tools/workflows/trigger_completion_action.tsservices/platform/convex/agents/config.tsservices/platform/convex/agents/file_actions.tsservices/platform/convex/agents/file_utils.tsservices/platform/convex/agents/queries.tsservices/platform/convex/agents/start_chat.tsservices/platform/convex/agents/test_chat.test.tsservices/platform/convex/agents/translate_fields.tsservices/platform/convex/agents/unified_chat.tsservices/platform/convex/agents/webhooks/internal_actions.tsservices/platform/convex/agents/webhooks/internal_mutations.tsservices/platform/convex/conversations/actions.tsservices/platform/convex/conversations/improve_message.tsservices/platform/convex/lib/age_keygen.tsservices/platform/convex/lib/agent_chat/internal_actions.tsservices/platform/convex/lib/agent_chat/start_agent_chat.tsservices/platform/convex/lib/agent_chat/types.tsservices/platform/convex/lib/agent_response/types.tsservices/platform/convex/lib/agent_runtime_config.tsservices/platform/convex/lib/attachments/process_attachments.tsservices/platform/convex/lib/create_agent_config.tsservices/platform/convex/lib/openai_provider.tsservices/platform/convex/lib/sops.tsservices/platform/convex/lib/summarization/auto_summarize.tsservices/platform/convex/lib/summarization/internal_actions.tsservices/platform/convex/lib/summarize_context.tsservices/platform/convex/providers/file_actions.tsservices/platform/convex/providers/file_utils.tsservices/platform/convex/providers/resolve_model.tsservices/platform/convex/providers/validators.tsservices/platform/convex/workflow_engine/helpers/nodes/llm/execute_agent_with_tools.tsservices/platform/convex/workflow_engine/helpers/nodes/llm/execute_llm_node.tsservices/platform/convex/workflow_engine/helpers/nodes/llm/utils/validate_and_normalize_config.tsservices/platform/convex/workflow_engine/helpers/validation/steps/llm.tsservices/platform/convex/workflow_engine/types/nodes.tsservices/platform/convex/workflows/triggers/actions.tsservices/platform/docker-entrypoint.shservices/platform/lib/config-watcher.tsservices/platform/lib/shared/schemas/agents.tsservices/platform/lib/shared/schemas/providers.tsservices/platform/messages/en.jsonservices/platform/package.jsonservices/rag/Dockerfiletools/cli/package.jsontools/cli/src/commands/deploy/index.tstools/cli/src/commands/reset.tstools/cli/src/commands/rollback.tstools/cli/src/lib/actions/deploy.tstools/cli/src/lib/actions/init.tstools/cli/src/lib/actions/start.tstools/cli/src/lib/actions/update.tstools/cli/src/lib/compose/generators/generate-dev-compose.tstools/cli/src/lib/config/ensure-config.tstools/cli/src/lib/config/ensure-env.tstools/cli/src/lib/crypto/age-keygen.ts
💤 Files with no reviewable changes (4)
- services/platform/app/features/agents/hooks/queries.ts
- services/platform/convex/lib/openai_provider.ts
- services/platform/convex/agents/queries.ts
- services/platform/convex/lib/agent_runtime_config.ts
| "description": "Multi-Modell-KI-Gateway mit Zugang zu fuhrenden LLM-Anbietern", | ||
| "models": { | ||
| "anthropic/claude-opus-4.6": { | ||
| "description": "Leistungsstarkstes Modell fur komplexe Aufgaben und Programmierung" |
There was a problem hiding this comment.
German localization strings contain spelling errors.
Please fix "fuhrenden" → "führenden" and "fur" → "für".
✍️ Suggested patch
- "description": "Multi-Modell-KI-Gateway mit Zugang zu fuhrenden LLM-Anbietern",
+ "description": "Multi-Modell-KI-Gateway mit Zugang zu führenden LLM-Anbietern",
@@
- "description": "Leistungsstarkstes Modell fur komplexe Aufgaben und Programmierung"
+ "description": "Leistungsstarkstes Modell für komplexe Aufgaben und Programmierung"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "description": "Multi-Modell-KI-Gateway mit Zugang zu fuhrenden LLM-Anbietern", | |
| "models": { | |
| "anthropic/claude-opus-4.6": { | |
| "description": "Leistungsstarkstes Modell fur komplexe Aufgaben und Programmierung" | |
| "description": "Multi-Modell-KI-Gateway mit Zugang zu führenden LLM-Anbietern", | |
| "models": { | |
| "anthropic/claude-opus-4.6": { | |
| "description": "Leistungsstarkstes Modell für komplexe Aufgaben und Programmierung" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/providers/openrouter.json` around lines 60 - 63, Update the German
localization strings by correcting the spelling mistakes in the description
values: change "fuhrenden" to "führenden" and "fur" to "für" (specifically in
the top-level "description" value "Multi-Modell-KI-Gateway mit Zugang zu
fuhrenden LLM-Anbietern" and in the model description for
"anthropic/claude-opus-4.6" where "fur" appears); ensure the corrected UTF-8
characters are used so the strings read "führenden" and "für".
| def get_chat_model( | ||
| config_dir: str | None = None, | ||
| ) -> tuple[str, str, str]: | ||
| """Return (base_url, api_key, model_id) for the default chat model. | ||
|
|
||
| Finds the first model marked default that has a "chat" tag, | ||
| or falls back to the first model with a "chat" tag. | ||
| """ | ||
| providers = load_providers(config_dir) | ||
| match = _find_model(providers, "chat", prefer_default=True) | ||
| if match is None: | ||
| raise ValueError("No chat model found in provider configuration files.") | ||
|
|
||
| provider, model = match | ||
| api_key = provider.api_key or "" | ||
| return (provider.base_url, api_key, model.id) |
There was a problem hiding this comment.
Missing secrets currently bypass the env fallback.
BaseServiceSettings only falls back when these helpers raise, but Lines 151, 165, and 185 coerce None to "" and return success. A provider JSON without a decrypted .secrets.json therefore resolves to an empty API key and makes unauthenticated upstream calls instead of using the existing env-based key.
Also applies to: 155-172, 175-186
🧰 Tools
🪛 Ruff (0.15.7)
[warning] 148-148: Avoid specifying long messages outside the exception class
(TRY003)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/tale_shared/src/tale_shared/config/providers.py` around lines 137 -
152, The current helpers (e.g., get_chat_model) coerce provider.api_key None to
an empty string which bypasses the env-key fallback; instead, if
provider.api_key is None the helper should raise a descriptive error (e.g.,
ValueError("No API key found for provider X")) so BaseServiceSettings can catch
and fall back to env secrets; update get_chat_model (and the equivalent
functions handling "embedding" and "base" models) to return the base_url and
model.id as before but do not convert None to ""; raise when provider.api_key is
None, otherwise use provider.api_key.
| result = subprocess.run( | ||
| ["sops", "-d", "--output-type", "json", str(path)], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=10, | ||
| ) |
There was a problem hiding this comment.
Resolve sops to an absolute executable path before spawning.
Relying on a partial executable name allows PATH shadowing and weakens execution safety.
🔒 Suggested patch
import json
import subprocess
+import shutil
from pathlib import Path
@@
path = Path(file_path)
+ sops_bin = shutil.which("sops")
+ if not sops_bin:
+ raise RuntimeError("sops binary not found in PATH")
@@
result = subprocess.run(
- ["sops", "-d", "--output-type", "json", str(path)],
+ [sops_bin, "-d", "--output-type", "json", str(path)],
capture_output=True,
text=True,
+ check=False,
timeout=10,
)🧰 Tools
🪛 Ruff (0.15.7)
[error] 18-18: subprocess call: check for execution of untrusted input
(S603)
[warning] 18-18: subprocess.run without explicit check argument
Add explicit check=False
(PLW1510)
[error] 19-19: Starting a process with a partial executable path
(S607)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/tale_shared/src/tale_shared/utils/sops.py` around lines 18 - 23, The
subprocess.run call uses a bare "sops" executable which can be shadowed; resolve
the sops binary to an absolute path (e.g., via shutil.which("sops")) before
spawning and use that path in the argument list instead of "sops"; if which
returns None, raise a clear error (or log and exit) so you don't call
subprocess.run with a non-resolved name. Ensure you import shutil and update the
invocation that builds the argv (the call currently creating result =
subprocess.run([... "sops", ...], ...)) to use the resolved path variable.
| data = json.loads(result.stdout) | ||
| _cache[str(path)] = (data, mtime) | ||
| return data |
There was a problem hiding this comment.
Avoid returning a shared mutable cached object and enforce JSON object shape.
Right now callers can mutate cached data globally, and non-object JSON will violate the dict contract.
🛡️ Suggested patch
+from copy import deepcopy
@@
- data = json.loads(result.stdout)
- _cache[str(path)] = (data, mtime)
- return data
+ parsed = json.loads(result.stdout)
+ if not isinstance(parsed, dict):
+ raise RuntimeError(f"Failed to decrypt {path}: expected JSON object")
+ _cache[str(path)] = (deepcopy(parsed), mtime)
+ return deepcopy(parsed)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/tale_shared/src/tale_shared/utils/sops.py` around lines 30 - 32, The
cached JSON object (data) is returned directly which allows callers to mutate
shared state and also doesn't enforce that JSON is an object; change the logic
that sets and returns _cache[str(path)] = (data, mtime) so that you validate
that data is a dict (raise TypeError if not), store a deep copy in the cache,
and always return a fresh deep copy to callers; use the existing locals (data,
_cache, path, mtime, and result.stdout) and Python's copy.deepcopy (or
json.loads+json.dumps roundtrip) to implement immutability and shape
enforcement.
| description: data.description, | ||
| systemInstructions: 'You are a helpful assistant.', | ||
| modelPreset: 'standard', | ||
| supportedModels: ['moonshotai/kimi-k2.5'], |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider centralizing the default model identifier.
The hardcoded model 'moonshotai/kimi-k2.5' duplicates the default from .env.example. If the default model changes, this would need manual synchronization. Consider importing a shared constant or deriving the default from provider configuration to maintain consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/features/agents/components/agent-create-dialog.tsx` at
line 85, The hardcoded default model string in the supportedModels array
('moonshotai/kimi-k2.5') should be centralized; update AgentCreateDialog (look
for supportedModels in agent-create-dialog.tsx) to import or read a shared
DEFAULT_MODEL constant (or derive it from the provider configuration module)
instead of embedding the literal, and replace the inline value so the component
uses that single source of truth.
| if (type === 'providers') { | ||
| // providers/[@org/]name.json | ||
| const filename = rest[0]; | ||
| return { type, orgSlug, slug: filename.replace(/\.json$/, '') }; | ||
| } |
There was a problem hiding this comment.
Validate provider path shape before deriving slug.
The providers branch accepts extra path segments and will mis-derive slugs (e.g., providers/@acme/team/openai.json becomes team). Reject non-canonical paths (providers/[@org/]name.json) to avoid incorrect downstream resolution events.
Suggested fix
if (type === 'providers') {
// providers/[`@org/`]name.json
+ if (rest.length !== 1) return null;
const filename = rest[0];
- return { type, orgSlug, slug: filename.replace(/\.json$/, '') };
+ if (!filename.endsWith('.json')) return null;
+ return { type, orgSlug, slug: filename.slice(0, -5) };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (type === 'providers') { | |
| // providers/[@org/]name.json | |
| const filename = rest[0]; | |
| return { type, orgSlug, slug: filename.replace(/\.json$/, '') }; | |
| } | |
| if (type === 'providers') { | |
| // providers/[`@org/`]name.json | |
| if (rest.length !== 1) return null; | |
| const filename = rest[0]; | |
| if (!filename.endsWith('.json')) return null; | |
| return { type, orgSlug, slug: filename.slice(0, -5) }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/lib/config-watcher.ts` around lines 73 - 77, The providers
branch currently trusts rest[0] and derives slug from filename, which lets extra
path segments slip through; update the providers handling in config-watcher.ts
(the branch using variables type, rest, orgSlug, slug) to first validate the
path shape by ensuring rest has exactly one element and that rest[0] matches the
expected filename pattern (e.g., /^[^\/]+\.json$/) before computing slug =
filename.replace(/\.json$/, ''), and if validation fails reject the path (return
null/undefined or throw) so only canonical "providers/[`@org/`]name.json" shapes
are accepted.
| // Only react to JSON file changes; ignore secret sidecar files | ||
| if (!rel.endsWith('.json')) return; | ||
| if (rel.endsWith('.secrets.json')) return; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Scope .secrets.json ignore rule to provider files only.
Current filtering ignores any *.secrets.json across all config domains. If only provider sidecars should be excluded, narrow this check to providers/ paths.
Suggested refinement
- if (rel.endsWith('.secrets.json')) return;
+ const normalizedRel = rel.replace(/\\/g, '/');
+ if (
+ normalizedRel.startsWith('providers/') &&
+ normalizedRel.endsWith('.secrets.json')
+ ) {
+ return;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/lib/config-watcher.ts` around lines 101 - 103, The current
filter uses rel.endsWith('.secrets.json') which excludes any *.secrets.json
globally; restrict this to provider sidecars by only returning when the path
points into the providers folder (e.g. check that rel includes or starts with
"providers/" in addition to ending with ".secrets.json"). Update the conditional
in config-watcher.ts that examines rel so it only early-returns for provider
sidecar files (e.g. combine a providers/ path check with the .secrets.json
suffix test) while leaving other *.secrets.json files untouched.
| "description": "Manage LLM providers and their models", | ||
| "addProvider": "Add provider", | ||
| "editProvider": "Edit provider", | ||
| "deleteProvider": "Delete provider", | ||
| "deleteConfirm": "Are you sure you want to delete this provider? This action cannot be undone.", | ||
| "apiKey": "API Key", | ||
| "apiKeyConfigured": "API key configured", | ||
| "apiKeyNotConfigured": "API key not configured", | ||
| "models": "Models", | ||
| "addModel": "Add model", | ||
| "editModel": "Edit model", | ||
| "deleteModel": "Delete model", | ||
| "deleteModelConfirm": "Are you sure you want to delete \"{model}\"?", | ||
| "default": "Default", | ||
| "modelId": "Model ID", | ||
| "displayName": "Display name", | ||
| "description_field": "Description", | ||
| "baseUrl": "Base URL", | ||
| "tags": "Tags", | ||
| "defaultModel": "Default", | ||
| "dimensions": "Dimensions", | ||
| "saved": "Provider saved", | ||
| "deleted": "Provider deleted", | ||
| "secretSaved": "API key saved", | ||
| "name": "Provider name", | ||
| "namePlaceholder": "e.g., openai", | ||
| "nameHelp": "Lowercase letters, numbers, and hyphens only.", | ||
| "namePatternError": "Must start with a letter and contain only lowercase letters, numbers, and hyphens.", | ||
| "displayNamePlaceholder": "e.g., OpenAI", | ||
| "descriptionPlaceholder": "e.g., OpenAI GPT models", | ||
| "baseUrlPlaceholder": "e.g., https://api.openai.com/v1", | ||
| "modelIdPlaceholder": "e.g., gpt-4o", | ||
| "modelDisplayNamePlaceholder": "e.g., GPT-4o", | ||
| "modelDescriptionPlaceholder": "e.g., Multimodal model with vision capabilities", | ||
| "dimensionsPlaceholder": "e.g., 1536", | ||
| "noProviders": "No providers configured", | ||
| "noProvidersDescription": "Add an LLM provider to get started with AI features.", | ||
| "general": "General", | ||
| "saveFailed": "Failed to save provider", | ||
| "deleteFailed": "Failed to delete provider", | ||
| "secretSaveFailed": "Failed to save API key", | ||
| "createFailed": "Failed to create provider", | ||
| "discard": "Discard", | ||
| "save": "Save", | ||
| "saving": "Saving...", | ||
| "saveKey": "Save key", | ||
| "editGeneral": "Edit details", | ||
| "editKey": "Edit", | ||
| "addKey": "Add API key", | ||
| "addApiKey": "Add API Key", | ||
| "replaceApiKey": "Replace API Key", | ||
| "replaceApiKeyDescription": "Current key: {maskedKey}", | ||
| "apiKeyReplace": "Enter new API key to replace ({maskedKey})", | ||
| "apiKeyEnter": "Enter API key", | ||
| "searchProvider": "Search providers...", | ||
| "description": "Description", | ||
| "created": "Provider created", |
There was a problem hiding this comment.
Duplicate description key in settings.providers causes silent overwrite.
In JSON, the later key wins, so "Manage LLM providers and their models" is dropped and replaced by "Description".
🧩 Suggested patch
- "description": "Description",
+ "descriptionLabel": "Description",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "description": "Manage LLM providers and their models", | |
| "addProvider": "Add provider", | |
| "editProvider": "Edit provider", | |
| "deleteProvider": "Delete provider", | |
| "deleteConfirm": "Are you sure you want to delete this provider? This action cannot be undone.", | |
| "apiKey": "API Key", | |
| "apiKeyConfigured": "API key configured", | |
| "apiKeyNotConfigured": "API key not configured", | |
| "models": "Models", | |
| "addModel": "Add model", | |
| "editModel": "Edit model", | |
| "deleteModel": "Delete model", | |
| "deleteModelConfirm": "Are you sure you want to delete \"{model}\"?", | |
| "default": "Default", | |
| "modelId": "Model ID", | |
| "displayName": "Display name", | |
| "description_field": "Description", | |
| "baseUrl": "Base URL", | |
| "tags": "Tags", | |
| "defaultModel": "Default", | |
| "dimensions": "Dimensions", | |
| "saved": "Provider saved", | |
| "deleted": "Provider deleted", | |
| "secretSaved": "API key saved", | |
| "name": "Provider name", | |
| "namePlaceholder": "e.g., openai", | |
| "nameHelp": "Lowercase letters, numbers, and hyphens only.", | |
| "namePatternError": "Must start with a letter and contain only lowercase letters, numbers, and hyphens.", | |
| "displayNamePlaceholder": "e.g., OpenAI", | |
| "descriptionPlaceholder": "e.g., OpenAI GPT models", | |
| "baseUrlPlaceholder": "e.g., https://api.openai.com/v1", | |
| "modelIdPlaceholder": "e.g., gpt-4o", | |
| "modelDisplayNamePlaceholder": "e.g., GPT-4o", | |
| "modelDescriptionPlaceholder": "e.g., Multimodal model with vision capabilities", | |
| "dimensionsPlaceholder": "e.g., 1536", | |
| "noProviders": "No providers configured", | |
| "noProvidersDescription": "Add an LLM provider to get started with AI features.", | |
| "general": "General", | |
| "saveFailed": "Failed to save provider", | |
| "deleteFailed": "Failed to delete provider", | |
| "secretSaveFailed": "Failed to save API key", | |
| "createFailed": "Failed to create provider", | |
| "discard": "Discard", | |
| "save": "Save", | |
| "saving": "Saving...", | |
| "saveKey": "Save key", | |
| "editGeneral": "Edit details", | |
| "editKey": "Edit", | |
| "addKey": "Add API key", | |
| "addApiKey": "Add API Key", | |
| "replaceApiKey": "Replace API Key", | |
| "replaceApiKeyDescription": "Current key: {maskedKey}", | |
| "apiKeyReplace": "Enter new API key to replace ({maskedKey})", | |
| "apiKeyEnter": "Enter API key", | |
| "searchProvider": "Search providers...", | |
| "description": "Description", | |
| "created": "Provider created", | |
| "description": "Manage LLM providers and their models", | |
| "addProvider": "Add provider", | |
| "editProvider": "Edit provider", | |
| "deleteProvider": "Delete provider", | |
| "deleteConfirm": "Are you sure you want to delete this provider? This action cannot be undone.", | |
| "apiKey": "API Key", | |
| "apiKeyConfigured": "API key configured", | |
| "apiKeyNotConfigured": "API key not configured", | |
| "models": "Models", | |
| "addModel": "Add model", | |
| "editModel": "Edit model", | |
| "deleteModel": "Delete model", | |
| "deleteModelConfirm": "Are you sure you want to delete \"{model}\"?", | |
| "default": "Default", | |
| "modelId": "Model ID", | |
| "displayName": "Display name", | |
| "description_field": "Description", | |
| "baseUrl": "Base URL", | |
| "tags": "Tags", | |
| "defaultModel": "Default", | |
| "dimensions": "Dimensions", | |
| "saved": "Provider saved", | |
| "deleted": "Provider deleted", | |
| "secretSaved": "API key saved", | |
| "name": "Provider name", | |
| "namePlaceholder": "e.g., openai", | |
| "nameHelp": "Lowercase letters, numbers, and hyphens only.", | |
| "namePatternError": "Must start with a letter and contain only lowercase letters, numbers, and hyphens.", | |
| "displayNamePlaceholder": "e.g., OpenAI", | |
| "descriptionPlaceholder": "e.g., OpenAI GPT models", | |
| "baseUrlPlaceholder": "e.g., https://api.openai.com/v1", | |
| "modelIdPlaceholder": "e.g., gpt-4o", | |
| "modelDisplayNamePlaceholder": "e.g., GPT-4o", | |
| "modelDescriptionPlaceholder": "e.g., Multimodal model with vision capabilities", | |
| "dimensionsPlaceholder": "e.g., 1536", | |
| "noProviders": "No providers configured", | |
| "noProvidersDescription": "Add an LLM provider to get started with AI features.", | |
| "general": "General", | |
| "saveFailed": "Failed to save provider", | |
| "deleteFailed": "Failed to delete provider", | |
| "secretSaveFailed": "Failed to save API key", | |
| "createFailed": "Failed to create provider", | |
| "discard": "Discard", | |
| "save": "Save", | |
| "saving": "Saving...", | |
| "saveKey": "Save key", | |
| "editGeneral": "Edit details", | |
| "editKey": "Edit", | |
| "addKey": "Add API key", | |
| "addApiKey": "Add API Key", | |
| "replaceApiKey": "Replace API Key", | |
| "replaceApiKeyDescription": "Current key: {maskedKey}", | |
| "apiKeyReplace": "Enter new API key to replace ({maskedKey})", | |
| "apiKeyEnter": "Enter API key", | |
| "searchProvider": "Search providers...", | |
| "descriptionLabel": "Description", | |
| "created": "Provider created", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/messages/en.json` around lines 1402 - 1458, The JSON has a
duplicate "description" key in the settings.providers block (one with "Manage
LLM providers and their models" and one with "Description"), causing the first
value to be overwritten; rename one of the keys (for example change the short
label "description" => "descriptionLabel" or "description_short") and update any
places that reference that key (e.g., UI code that reads
settings.providers.description) so each key is unique (alternatively keep
"description" for the section header and rename the shorter label to
"descriptionLabel" and adjust references to that new key).
| ARG SOPS_VERSION=3.9.4 | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| curl \ | ||
| build-essential \ | ||
| libpq-dev \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64" -o /usr/local/bin/sops && chmod +x /usr/local/bin/sops |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the full Dockerfile
fd Dockerfile$ | grep -E 'rag|services'Repository: tale-project/tale
Length of output: 161
🏁 Script executed:
# Read the Dockerfile to see full context and base image
cat -n services/rag/DockerfileRepository: tale-project/tale
Length of output: 4082
🏁 Script executed:
# Check if there are any existing TARGETARCH references in Dockerfiles
rg "TARGETARCH|--platform" services/rag/Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Check SOPS GitHub releases for available architectures and checksums
curl -s https://api.github.com/repos/getsops/sops/releases/tags/v3.9.4 | jq -r '.assets[] | .name' | head -20Repository: tale-project/tale
Length of output: 610
🏁 Script executed:
# Also check if checksums.txt exists in releases
curl -s -I "https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.checksums.txt"Repository: tale-project/tale
Length of output: 5062
🏁 Script executed:
# Download and inspect the checksums file to verify the grep pattern in suggested fix
curl -sL "https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.checksums.txt" | head -20Repository: tale-project/tale
Length of output: 589
Hardcoded linux.amd64 binary breaks ARM64 support and lacks integrity verification.
The image declares multi-arch support (line 2) but line 22 hardcodes the x86-64 binary, causing ARM64 builds to fail. Additionally, the binary is executed without checksum verification. SOPS v3.9.4 provides both linux.amd64 and linux.arm64 binaries alongside a checksums.txt file.
Suggested fix
ARG SOPS_VERSION=3.9.4
+ARG TARGETARCH
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/* \
- && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64" -o /usr/local/bin/sops && chmod +x /usr/local/bin/sops
+ && case "${TARGETARCH}" in amd64|arm64) ;; *) echo "Unsupported arch: ${TARGETARCH}"; exit 1 ;; esac \
+ && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.checksums.txt" -o /tmp/sops.checksums \
+ && curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.${TARGETARCH}" -o /usr/local/bin/sops \
+ && grep "sops-v${SOPS_VERSION}.linux.${TARGETARCH}\$" /tmp/sops.checksums | sha256sum -c - \
+ && chmod +x /usr/local/bin/sops \
+ && rm -f /tmp/sops.checksums🧰 Tools
🪛 Hadolint (2.14.0)
[warning] 17-17: Pin versions in apt get install. Instead of apt-get install <package> use apt-get install <package>=<version>
(DL3008)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/rag/Dockerfile` around lines 16 - 22, The Dockerfile currently
hardcodes the x86 binary via ARG SOPS_VERSION and the RUN that writes to
/usr/local/bin/sops; change it to detect the container architecture (e.g., via
uname -m or dpkg --print-architecture -> map to linux.amd64 vs linux.arm64),
download the matching sops-${SOPS_VERSION}.${ARCH} binary and the accompanying
checksums file (checksums.txt) from the same GitHub release, verify the
downloaded binary against the checksum (sha256) before installing, and then
chmod +x /usr/local/bin/sops; ensure these changes reference ARG SOPS_VERSION,
the binary target /usr/local/bin/sops, and the checksums file so ARM64 builds
succeed and integrity is verified.
| export interface AgeKeypair { | ||
| secretKey: string; // "AGE-SECRET-KEY-1..." | ||
| publicKey: string; // "age1..." | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for usages of deriveAgePublicKey and AgeKeypair
rg -n "deriveAgePublicKey|AgeKeypair" --type=ts -g '!**/age-keygen.ts'Repository: tale-project/tale
Length of output: 578
🏁 Script executed:
cat tools/cli/src/lib/crypto/age-keygen.tsRepository: tale-project/tale
Length of output: 1899
🏁 Script executed:
# Search for type annotations using AgeKeypair
rg -n "AgeKeypair" --type=tsRepository: tale-project/tale
Length of output: 411
🏁 Script executed:
# Check ensure-env.ts to see if it uses the type
cat tools/cli/src/lib/config/ensure-env.ts | head -30Repository: tale-project/tale
Length of output: 952
🏁 Script executed:
# Check the Convex version to see if this is a duplicate
cat services/platform/convex/lib/age_geygen.ts 2>/dev/null || echo "File not found or different path"Repository: tale-project/tale
Length of output: 93
🏁 Script executed:
rg "deriveAgePublicKey" tools/cli/ --type=tsRepository: tale-project/tale
Length of output: 163
🏁 Script executed:
# Also check the context around line 194 in ensure-env.ts to see how ageKeypair is used
sed -n '190,210p' tools/cli/src/lib/config/ensure-env.tsRepository: tale-project/tale
Length of output: 595
deriveAgePublicKey is unused in the CLI and should be removed or documented for future use.
The pipeline warnings are partially accurate. AgeKeypair is legitimately used as the return type of generateAgeKeypair(), which is called in ensure-env.ts:194 to generate the sopsAgeKey secret. However, deriveAgePublicKey has no callers within the CLI codebase and should either be removed or explicitly marked as part of a public API with a // @public`` comment if intended for external consumption.
🧰 Tools
🪛 GitHub Actions: Lint
[warning] 11-11: knip: Unused exported type 'AgeKeypair' reported at tools/cli/src/lib/crypto/age-keygen.ts:11:18.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/cli/src/lib/crypto/age-keygen.ts` around lines 11 - 14, The function
deriveAgePublicKey is currently unused in the CLI; either remove its definition
or explicitly mark it as part of the public API so it doesn't trigger
warnings—locate the deriveAgePublicKey function in the age-keygen module and if
you intend it for external use add a clear `// `@public`` comment and ensure it's
exported in the module's public surface, otherwise delete the deriveAgePublicKey
implementation and any related unused imports; keep the AgeKeypair interface and
generateAgeKeypair (used by generateAgeKeypair -> ensure-env usage) intact.
| def load_providers(config_dir: str | None = None) -> list[ProviderConfig]: | ||
| """Read all provider JSON files from {config_dir}/providers/. | ||
|
|
||
| Reads *.json (excluding *.secrets.json) and decrypts matching | ||
| *.secrets.json files via SOPS. | ||
| """ | ||
| base = Path( | ||
| config_dir | ||
| or os.environ.get("TALE_CONFIG_DIR") | ||
| or os.environ.get("CONFIG_DIR", DEFAULT_CONFIG_DIR) | ||
| ) | ||
| providers_dir = base / "providers" |
There was a problem hiding this comment.
Honor PROVIDERS_DIR in the Python loader too.
services/platform/convex/providers/file_utils.ts resolves providers from PROVIDERS_DIR before TALE_CONFIG_DIR, but this loader ignores that override. Any deployment that sets only PROVIDERS_DIR will have JS and Python reading different directories, so Python silently falls back to env vars while Convex uses the file-based config.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/tale_shared/src/tale_shared/config/providers.py` around lines 42 -
53, The loader in load_providers currently builds base from config_dir,
TALE_CONFIG_DIR, CONFIG_DIR but ignores the PROVIDERS_DIR env override used by
the JS loader; update load_providers (and its computation of base/providers_dir)
to first check os.environ.get("PROVIDERS_DIR") and, if present, use that path
(or its parent) as the providers directory so Python resolves providers the same
way as services/platform/convex/providers/file_utils.ts; ensure you still
respect an explicit config_dir param and fall back to TALE_CONFIG_DIR/CONFIG_DIR
only when PROVIDERS_DIR is not set.
| export const readProvider = action({ | ||
| args: { orgSlug: v.string(), providerName: v.string() }, | ||
| returns: v.any(), | ||
| handler: async (ctx, args): Promise<ProviderReadResult> => { | ||
| const authUser = await authComponent.getAuthUser(ctx); | ||
| if (!authUser) throw new Error('Unauthenticated'); | ||
| return readProviderFile(args.orgSlug, args.providerName); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Authorize orgSlug before touching provider files.
Line 141 authenticates the caller, but none of the public actions in this module authorizes args.orgSlug. As written, any signed-in user can read, write, delete, or overwrite another organization's provider config and secrets by passing a different slug.
Based on learnings, public Convex actions should enforce auth at the boundary and perform organization membership checks in queries/mutations rather than calling getOrganizationMember directly from the action.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/convex/providers/file_actions.ts` around lines 137 - 145,
The readProvider action currently authenticates the caller with
authComponent.getAuthUser but does not authorize args.orgSlug, so any signed-in
user can access another org's provider files; update the readProvider handler to
perform an organization-bound authorization check before calling
readProviderFile by invoking your auth/authorization helper (e.g., an
assertOrganizationMember or authorizeOrganizationAccess-style function) with
ctx, the resolved authUser and args.orgSlug (do this in the readProvider handler
right after authComponent.getAuthUser and before readProviderFile), and avoid
performing membership queries later in downstream code—only proceed to call
readProviderFile(args.orgSlug, args.providerName) if the authorization check
passes.
| for (const provider of candidates) { | ||
| const definition = provider.config.models.find((m) => | ||
| (m.tags as readonly string[]).includes(args.tag), | ||
| ); | ||
| if (definition) { | ||
| return { | ||
| providerName: provider.name, | ||
| baseUrl: provider.config.baseUrl, | ||
| apiKey: provider.secrets.apiKey, | ||
| modelId: definition.id, | ||
| dimensions: definition.dimensions, | ||
| supportsStructuredOutputs: | ||
| provider.config.supportsStructuredOutputs ?? false, | ||
| }; | ||
| } |
There was a problem hiding this comment.
resolveModelByTag should prefer the model marked default.
This currently returns the first model whose tags contain args.tag, so resolution depends on JSON order. The Python resolver added in this PR already prefers model.default for chat, so JS and Python can now pick different models from the same provider config.
🔧 Minimal fix
- for (const provider of candidates) {
- const definition = provider.config.models.find((m) =>
- (m.tags as readonly string[]).includes(args.tag),
- );
+ for (const provider of candidates) {
+ const taggedModels = provider.config.models.filter((m) =>
+ m.tags.includes(args.tag),
+ );
+ const definition =
+ taggedModels.find((m) => m.default) ?? taggedModels[0];
if (definition) {
return {
providerName: provider.name,
baseUrl: provider.config.baseUrl,
apiKey: provider.secrets.apiKey,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/convex/providers/file_actions.ts` around lines 331 - 345,
The resolver currently picks the first model whose tags include args.tag, which
is order-dependent; change resolveModelByTag so for each provider you first
filter provider.config.models to those whose (m.tags as readonly
string[]).includes(args.tag) and then pick the one with m.default === true if
present, otherwise fall back to the first filtered model; return the same object
shape (providerName, baseUrl, apiKey, modelId, dimensions,
supportsStructuredOutputs) using that selected definition instead of the current
single find() result.
| import type { LanguageModelV3 } from '@ai-sdk/provider'; | ||
|
|
||
| import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
from pathlib import Path
import json
target = Path("services/platform/convex/providers/resolve_model.ts").resolve()
sections = ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies")
p = target.parent
while True:
pkg = p / "package.json"
if pkg.exists():
data = json.loads(pkg.read_text())
print(f"nearest package.json: {pkg.relative_to(Path.cwd())}")
for dep in ("@ai-sdk/provider", "@ai-sdk/openai-compatible"):
found = False
for section in sections:
version = data.get(section, {}).get(dep)
if version is not None:
print(f"{section}.{dep}={version}")
found = True
if not found:
print(f"MISSING {dep}")
break
if p == p.parent:
print("No package.json found up the directory tree")
break
p = p.parent
PYRepository: tale-project/tale
Length of output: 185
🏁 Script executed:
# Check the actual imports in the file
cat -n services/platform/convex/providers/resolve_model.ts | head -20Repository: tale-project/tale
Length of output: 774
🏁 Script executed:
# Check if `@ai-sdk/openai-compatible` lists `@ai-sdk/provider` as a peer dependency
# First, find where the packages are installed or referenced
cd services/platform && find . -name "package.json" -type f | head -5Repository: tale-project/tale
Length of output: 75
🏁 Script executed:
# Check if there's a pnpm-workspace or yarn workspace configuration at the root
find . -maxdepth 2 -name "pnpm-workspace.yaml" -o -name "package.json" -path "./package.json" | head -1Repository: tale-project/tale
Length of output: 75
🏁 Script executed:
# Look for TypeScript or linting configuration that might flag unlisted dependencies
find services/platform -maxdepth 2 -name "tsconfig.json" -o -name ".eslintrc*" -o -name "eslint.config.*" | head -5Repository: tale-project/tale
Length of output: 131
🏁 Script executed:
# Check package.json for `@ai-sdk/openai-compatible` to see if it declares `@ai-sdk/provider`
# First, let's check if node_modules is available
ls -la services/platform/node_modules/@ai-sdk/ 2>/dev/null | head -10 || echo "node_modules not available"Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Check the actual npm/yarn/pnpm lock files to see if `@ai-sdk/provider` is transitively installed
find . -maxdepth 2 -name "pnpm-lock.yaml" -o -name "yarn.lock" -o -name "package-lock.json" | head -1Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Check the root package.json and services/platform/package.json for workspace setup
cat services/platform/package.json | head -30Repository: tale-project/tale
Length of output: 1617
🏁 Script executed:
# Look at tsconfig to understand any strict checking
cat services/platform/tsconfig.jsonRepository: tale-project/tale
Length of output: 450
🏁 Script executed:
# Search for other uses of LanguageModelV3 in the codebase
rg "LanguageModelV3" --type ts --type tsx -nRepository: tale-project/tale
Length of output: 88
🏁 Script executed:
# Check the root package.json to see if `@ai-sdk/provider` is declared there
cat package.json | grep -A 50 "dependencies"Repository: tale-project/tale
Length of output: 43
🏁 Script executed:
# Check if any package in the monorepo explicitly lists `@ai-sdk/provider`
rg "@ai-sdk/provider" --type json -nRepository: tale-project/tale
Length of output: 134
🏁 Script executed:
# Search for LanguageModelV3 without type filter
rg "LanguageModelV3" -n --max-count 20Repository: tale-project/tale
Length of output: 3888
🏁 Script executed:
# Check if `@ai-sdk/openai-compatible` package.json or docs mention `@ai-sdk/provider`
# First, let's see if we can find info about what `@ai-sdk/openai-compatible` exports
rg "from '@ai-sdk/provider'" -nRepository: tale-project/tale
Length of output: 1153
🏁 Script executed:
# Check if `@ai-sdk/provider` is in the root package.json dependencies
cat package.json | jq '.dependencies, .devDependencies, .peerDependencies, .optionalDependencies' 2>/dev/null | grep -A 5 -B 5 "@ai-sdk" || echo "No `@ai-sdk` entries found"Repository: tale-project/tale
Length of output: 85
🏁 Script executed:
# List what's imported from `@ai-sdk` modules across the codebase
rg "from '@ai-sdk/" -n | head -30Repository: tale-project/tale
Length of output: 1406
Declare @ai-sdk/provider in services/platform/package.json.
@ai-sdk/provider is imported as a type in this file and used across 9+ other files in the codebase, but is not declared in the workspace's dependencies. The oxlint lint job with --type-aware is flagging this as an unlisted dependency. Type-only imports can be tree-shaken away in isolated installs, making builds non-hermetic. Either declare @ai-sdk/provider as an explicit dependency or import LanguageModelV3 through @ai-sdk/openai-compatible if it re-exports the type.
🧰 Tools
🪛 GitHub Actions: Lint
[warning] 10-10: knip: Unlisted dependency reported for '@ai-sdk/provider' at services/platform/convex/providers/resolve_model.ts:10:38.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/convex/providers/resolve_model.ts` around lines 10 - 12,
The code imports the type LanguageModelV3 from `@ai-sdk/provider` but that package
is not declared in the workspace dependencies, causing lint/type-aware install
failures; either add "@ai-sdk/provider" to services/platform/package.json
dependencies or change the import to obtain LanguageModelV3 from
`@ai-sdk/openai-compatible` if that package re-exports it (the relevant symbols to
update are the LanguageModelV3 import and any files that reference it, e.g.,
resolve_model.ts where createOpenAICompatible is used); update package.json and
run the linter/build to confirm the unlisted-dependency error is resolved.
| export async function resolveLanguageModel( | ||
| ctx: ActionCtx, | ||
| opts: { tag: string; providerName?: string }, | ||
| ): Promise<ResolvedLanguageModel> { | ||
| // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- resolveModelByTag returns v.any() but shape is guaranteed by file_actions contract | ||
| const modelData = (await ctx.runAction( | ||
| internal.providers.file_actions.resolveModelByTag, | ||
| { tag: opts.tag, providerName: opts.providerName }, | ||
| )) as ResolvedModelData; | ||
| return { languageModel: createLanguageModel(modelData), modelData }; |
There was a problem hiding this comment.
Expose orgSlug on the shared resolver API.
Lines 51 and 67 call org-scoped internal actions, but these wrappers never let callers pass an org, so everything routed through resolveLanguageModel* resolves against the implicit 'default' provider directory. That means /providers/<orgSlug>/... configs are ignored and multi-org requests can pick the wrong tenant's credentials.
🔧 Minimal fix
export async function resolveLanguageModel(
ctx: ActionCtx,
- opts: { tag: string; providerName?: string },
+ opts: { tag: string; orgSlug?: string; providerName?: string },
): Promise<ResolvedLanguageModel> {
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- resolveModelByTag returns v.any() but shape is guaranteed by file_actions contract
const modelData = (await ctx.runAction(
internal.providers.file_actions.resolveModelByTag,
- { tag: opts.tag, providerName: opts.providerName },
+ {
+ tag: opts.tag,
+ orgSlug: opts.orgSlug,
+ providerName: opts.providerName,
+ },
)) as ResolvedModelData;
return { languageModel: createLanguageModel(modelData), modelData };
}
@@
export async function resolveLanguageModelById(
ctx: ActionCtx,
- opts: { modelId: string; providerName?: string },
+ opts: { modelId: string; orgSlug?: string; providerName?: string },
): Promise<ResolvedLanguageModel> {
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- resolveModelData returns v.any() but shape is guaranteed by file_actions contract
const modelData = (await ctx.runAction(
internal.providers.file_actions.resolveModelData,
- { modelId: opts.modelId, providerName: opts.providerName },
+ {
+ modelId: opts.modelId,
+ orgSlug: opts.orgSlug,
+ providerName: opts.providerName,
+ },
)) as ResolvedModelData;
return { languageModel: createLanguageModel(modelData), modelData };
}Also applies to: 62-71
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/convex/providers/resolve_model.ts` around lines 46 - 55,
The resolver wrappers (e.g., resolveLanguageModel) call org-scoped internal
actions (internal.providers.file_actions.resolveModelByTag) but never accept or
forward an org identifier, so lookups default to the implicit 'default' provider
directory; update the function signatures (opts) to include orgSlug?: string and
pass that orgSlug through to the ctx.runAction call (and any sibling wrappers
around lines 62-71 that call resolveModelByTag) so the internal action receives
the orgSlug and resolves provider configs under /providers/<orgSlug>/...; ensure
the ResolvedLanguageModel return type and any callers are adjusted to accept the
new optional orgSlug parameter.
Summary
providers/with SOPS-encrypted secrets, replacing env-var-based configurationtale initauto-generates age keypairs; secrets are encrypted/decrypted at runtime using a singleSOPS_AGE_KEYenv var (no.sops.yamlneeded)resolve_model.tseliminates duplicated provider-resolve boilerplate across conversations, agent chat, summarization, workflows, and image analysisget_chat_config/get_embedding_config/get_vision_confighelpers with provider-file-first, env-var-fallback resolutionTest plan
tale initon a fresh project — verify age keypair is generated andproviders/directory is scaffoldedproviderfield — verify the model selector shows the provider's modelstale deploy— verify provider files sync to the containerproviderfield continue to work with default resolutionSummary by CodeRabbit
Release Notes