Skip to content

Simplify agent config and rename custom agents to agents#906

Merged
larryro merged 8 commits into
mainfrom
refactor/rename-custom-agents-to-agents
Mar 28, 2026
Merged

Simplify agent config and rename custom agents to agents#906
larryro merged 8 commits into
mainfrom
refactor/rename-custom-agents-to-agents

Conversation

@larryro

@larryro larryro commented Mar 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Rename custom_agents / custom-agents to agents across the entire stack (Convex backend, React frontend, shared schemas, CLI tooling, and docs)
  • Remove deprecated features: auto-save, version history, test chat panel, and hardcoded agent definitions in favor of filesystem-based JSON agent configs with example configs in examples/agents/
  • Replace jsondiffpatch with a native table-based diff view for agent history comparison
  • Add legacy schema tables and data migration script for backward compatibility with existing Convex data
  • Fix agents list not refreshing after deletion

Test plan

  • Verify agent CRUD operations (create, read, update, delete) work end-to-end
  • Verify agent routes (/dashboard/$id/agents) load correctly
  • Verify agent chat/delegation still functions with renamed modules
  • Verify webhook endpoints work under new paths
  • Run migration script against a test environment and confirm data integrity
  • Confirm old custom-agents routes redirect or are removed
  • Run npx tsc --noEmit and lint checks pass

Summary by CodeRabbit

  • New Features

    • Added agent configuration versioning with history snapshots and restore capability.
    • Introduced multiple example agent templates (Chat, CRM, File, Integration, Web, and Workflow assistants).
  • Changes

    • Renamed "Custom Agents" to "Agents" across the UI for consistency.
    • Updated agent management routes and navigation paths.
    • Reorganized platform data storage architecture.

larryro added 5 commits March 28, 2026 20:40
… config

Consolidate the custom_agents module into agents/, removing the "custom_"
prefix across backend (Convex), frontend (React components/hooks/routes),
shared schemas, and CLI tooling. Removes deprecated features including
auto-save, version history, test chat panel, and hardcoded agent definitions
in favor of filesystem-based JSON agent configs. Adds examples/agents/ with
sample agent configurations.
…eserve user-modified agents

Replace the jsondiffpatch HTML output in history-diff-dialog with a
native table-based comparison showing field-level changes. Update
docker-entrypoint agent seeding to skip agents that have user
modifications in .history/.
…ackend, and docs

Rename feature directory, route paths, components, hooks, and documentation
from custom-agents/custom_agents to agents — aligning naming with the
simplified agent config model introduced in bda786a.
…gents rename

Retain deprecated customAgents/customAgentWebhooks tables and
customAgentId field in thread metadata for backward compatibility
with existing Convex data. Add migration script to copy volume data
from old platform-convex-data volume and rename org slugs to "default".
Pass AGENTS_DIR env var in docker-entrypoint.
@larryro larryro changed the title Rename custom agents to agents Simplify agent config and rename custom agents to agents Mar 28, 2026
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This is a major architectural refactoring that migrates the agent system from a database-backed versioned model (customAgents table with publish/draft status) to a filesystem-backed JSON configuration model. The changes span frontend routes, UI components, hooks, backend Convex operations, Docker configuration, and permissions. Specific updates include: renaming documentation and navigation labels from "custom agents" to "agents"; adding five example agent JSON configuration files; migrating all dashboard routes from /dashboard/$id/custom-agents/* to /dashboard/$id/agents/* with new route implementations; creating new frontend components and hooks for agent management backed by file operations; removing the entire services/platform/convex/custom_agents directory and replacing it with a new services/platform/convex/agents directory with file-based actions, schema changes (agentBindings and agentWebhooks tables), and refactored mutations/queries; updating Docker volumes from platform-convex-data:/app/convex-data to platform-data:/app/data and adding agent seeding logic; updating permissions and RLS rules to reference agents instead of customAgents; and updating all localization strings accordingly.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

This diff involves a fundamental architectural shift from database-backed versioned agents to filesystem-backed JSON agents, spanning multiple layers: frontend routing and component restructuring (~500+ new lines), backend file I/O with security considerations (480+ lines in file_actions.ts with atomic writes, history snapshots, path traversal prevention), schema migration (200+ lines of new DB mutations and queries), deletion of ~1,500+ lines of legacy custom_agents code, and integration changes across hooks, webhooks, and auth. The heterogeneous nature—not a simple refactor but a complete architectural replacement—combined with dense server-side file-handling logic, extensive client-side rewiring, and cross-cutting concerns (permissions, migrations, Docker setup) demands thorough, multi-faceted review.

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/rename-custom-agents-to-agents

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 60

Caution

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

⚠️ Outside diff range comments (5)
services/platform/convex/lib/rls/helpers/access_control.test.ts (1)

10-28: ⚠️ Potential issue | 🟡 Minor

Add agentWebhooks to the RLS regression matrix.

ALL_TABLES is supposed to exercise the full permission map, but this update only swaps in agentBindings. The new agentWebhooks table introduced by this PR is still untested, so permission regressions on webhook-token rows won't be caught here. Please add it to ALL_TABLES and the matching role bucket(s).

Also applies to: 61-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/lib/rls/helpers/access_control.test.ts` around lines
10 - 28, ALL_TABLES currently omits the new agentWebhooks table; add
'agentWebhooks' to the ALL_TABLES const and also include 'agentWebhooks' in the
same role bucket array(s) later in this test file that list tables per role (the
role-to-table buckets used in the regression matrix around the other
agent-related tables such as where 'agentBindings' is placed, referenced later
in the file). Ensure the added entry follows the same ordering/formatting as the
other table names so the full permission map is exercised by the existing tests.
tools/cli/src/lib/compose/generators/constants.ts (1)

1-9: ⚠️ Potential issue | 🟠 Major

Document and integrate the Convex data migration for upgrades.

A migration script exists at ./scripts/2026-03-28-migrate-convex-data.sh to handle the volume rename, but it is not referenced in the upgrade documentation (docs/production-deployment.md) and is not auto-invoked during tale deploy.

Users upgrading from versions before this change will lose Convex data (modules, files, exports) unless they manually run the migration script before or after deployment. The script should either be:

  1. Auto-invoked in the deploy workflow after volume creation, or
  2. Clearly documented in the "Updating Tale" section with a prerequisite check that warns if the old volume exists.

For now, add a note to the upgrade documentation pointing users to run this script as part of the upgrade procedure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/src/lib/compose/generators/constants.ts` around lines 1 - 9, Update
the "Updating Tale" / upgrade docs to document and link the migration script
./scripts/2026-03-28-migrate-convex-data.sh and instruct users to run it when
upgrading from pre-rename releases; explicitly state the old volume name (the
Convex volume referenced in the compose VOLUMES constant, e.g. check for the
pre-rename convex volume present alongside the new 'convex' or related entry in
the VOLUMES export) and add a prerequisite step in
docs/production-deployment.md: run the migration script before or immediately
after deployment (or provide a warning check) so users do not lose Convex data.
services/platform/app/features/chat/hooks/use-send-message.ts (1)

148-161: ⚠️ Potential issue | 🟡 Minor

Missing validation: organizationId should be checked against the loaded organization context before agent file resolution.

The code hardcodes orgSlug: 'default' (intentional for single-tenant deployments per migration), but passes organizationId dynamically without validating they correspond to the same organization. Unlike send_message_via_integration.ts, which validates conversation.organizationId !== args.organizationId after fetching, chatWithAgent skips this check.

Add validation to ensure the provided organizationId matches the organization record associated with 'default' slug before proceeding with agent file resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/chat/hooks/use-send-message.ts` around lines
148 - 161, Before calling chatWithAgent, validate that the dynamic
organizationId matches the organization record for the hardcoded slug 'default'
from the loaded organization context; fetch or access the organization for slug
'default' and compare its id to organizationId and throw or return an error if
they differ. Update the call site in use-send-message.ts (around the
chatWithAgent invocation that uses selectedAgent.name, currentThreadId,
mutationAttachments, and userContext) to perform this check and only proceed to
call chatWithAgent when the ids match, mirroring the conversation.organizationId
!== args.organizationId check used in send_message_via_integration.ts.
services/platform/convex/lib/agent_chat/start_agent_chat.ts (1)

126-142: ⚠️ Potential issue | 🟠 Major

Validate organization ownership before patching thread metadata.

Line 126 through Line 142 updates threadMetadata by threadId before any organization check. If a threadId leaks, this allows cross-org mutation of generation state and now agentId. Please validate org ownership first, then patch.

Based on learnings: For conversation write paths, validate args.organizationId matches the loaded conversation.organizationId immediately after fetching and before inserts/patches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/lib/agent_chat/start_agent_chat.ts` around lines 126
- 142, The code patches thread metadata (threadMeta) before verifying
organization ownership; modify start_agent_chat flow to validate that
args.organizationId matches the loaded conversation/thread organization
immediately after fetching the conversation or threadMeta and before any
ctx.db.patch or insert operations. Specifically, after obtaining threadMeta (via
ctx.db.query('threadMetadata').withIndex('by_threadId'...) or after loading the
conversation), check conversation.organizationId === args.organizationId (or
threadMeta.organizationId if that field exists) and throw or return an
authorization error if they differ, then only proceed to update
generationStatus, streamId, agentId, etc. Ensure this ownership check occurs
prior to calling ctx.db.patch on threadMeta and before any other write to the
conversation/thread.
services/platform/scripts/dev.ts (1)

396-414: 🧹 Nitpick | 🔵 Trivial

Make AGENTS_DIR sync failure explicit (or fail fast).

If convex env set AGENTS_DIR=... fails, the script currently logs and continues, which can leave agent features broken with delayed/error-prone debugging. Consider treating AGENTS_DIR sync as required once agent features are enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/scripts/dev.ts` around lines 396 - 414, The AGENTS_DIR sync
should be treated as required when AGENTS_DIR is set: if runCommand('bunx',
['convex','env','set', `AGENTS_DIR=${agentsDir}`]) fails, don't just log and
continue — fail fast. Modify the try/catch around the AGENTS_DIR handling (the
code using agentsDir and runCommand) so that on error you either rethrow the
error or call process.exit(1) (and include the full error in the log) instead of
only console.warn; ensure you preserve the existing log message including
err.message when failing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@compose.yml`:
- Around line 328-335: The compose change renames the Docker volume from
platform-convex-data (mounted at /app/convex-data) to platform-data (mounted at
/app/data), which will leave existing deployments using the old volume empty;
update release notes and documentation to include explicit migration steps and
ensure the migration script (or an update hook) detects platform-convex-data,
copies its contents into platform-data, and preserves ownership/permissions
before switching to the new mount; specifically add checks and a fail-safe in
the migration utility to: 1) detect existence of platform-convex-data, 2) create
platform-data if missing, 3) rsync or tar+extract data from /app/convex-data to
/app/data on the host volume, 4) verify content integrity and permissions, and
5) log the actions and any errors so operators can run the migration manually if
needed.

In `@docs/agents.md`:
- Around line 51-53: Replace the existing "## Versioning" section that describes
the old draft/live publish and rollback workflow with copy explaining the new
filesystem-backed editing and history model: remove references to draft/live,
publishing, and rollback, update or delete the auto-save note mentioned around
Line 25, and document how edits are stored on disk, how versions are represented
(e.g., file snapshots or commits), how to view/compare history and restore prior
states in the new UI/state, and any relevant user actions or limitations; update
the "## Versioning" header content to reference the new filesystem-backed
mechanisms so readers aren't directed to non-existent draft/live UI.

In `@examples/agents/file-assistant.json`:
- Line 4: The pdf URL import path in
services/platform/convex/documents/generate_document_helpers.ts forwards
user-supplied URLs to the crawler without SSRF checks; add the same
private-address protection used by isPrivateIp
(services/platform/convex/images/http_actions.ts) by validating sourceType ===
'url' inputs: resolve the hostname, perform an IP check (reuse isPrivateIp)
against all resolved addresses and reject any private/loopback/metadata-range
addresses before calling the crawler; ensure the pdf tool handler (the function
that processes sourceType:'url' in generate_document_helpers) returns a clear
error for blocked URLs and does not attempt the fetch if any resolved IP is
private.

In `@examples/agents/web-assistant.json`:
- Around line 2-9: The metadata is inconsistent: the "description" claims
open-web search while "systemInstructions" restrict searches to manually added
crawled sites; update the "description" (and optionally "displayName") to
accurately reflect the agent's scope (e.g., "Searches the organization's indexed
website content — no open-internet access") so it matches the
"systemInstructions" and avoids misleading users about the web tool's
capabilities.

In `@scripts/2026-03-28-migrate-convex-data.sh`:
- Around line 120-132: The pipeline uses grep -v to filter docker exec output
which can return exit code 1 when no lines match and break the script under set
-euo pipefail; update the post-pipe filter to use sed delete commands instead so
it always exits 0: replace the final "| grep -v \"^Admin key\\|^📋\\|^✅
Admin\\|^━\\|^🌐\\|^$\\|Steps:\\|Open\\|Enter\\|Paste\"" with a sed invocation
that deletes each of those regexes (e.g. multiple -e '/^Admin key/d' -e '/^📋/d'
... or a single script of /pattern/d) so the docker exec block (the command
starting with docker exec "$container" bash -c '...') keeps the same output
filtering but will not cause the migration script to fail when no matches exist.
- Around line 63-72: The copy currently uses cp -rn "$src/"* which omits
dotfiles and hides errors with || true; change the copy to include hidden files
and surface failures by using a robust command such as cp -a "$src"/. "$dst"/
(or rsync -a --links "$src"/ "$dst"/) instead of cp -rn "$src/"* and remove the
trailing "|| true"; then check the cp/rsync exit code and use before/after
counts (variables before and after) to detect no-op copies and log an error or
skip accordingly. Ensure you update the cp invocation around the src and dst
variables (the lines that set before, perform cp, and set after) so dotfiles are
copied and failures are not silently ignored.

In `@services/platform/app/features/agents/components/agent-create-dialog.tsx`:
- Around line 24-34: The CreateAgentDialog component is writing new agents into
a hardcoded org slug ('default'); update the save call(s) that set orgSlug or
tenant to use the passed-in prop instead of the constant. Locate the save/submit
logic inside CreateAgentDialog (the function component handling form submission
around the current lines 81–89) and replace the hardcoded 'default' orgSlug with
the organizationId prop (or thread organizationId into any nested helper calls)
so new agents are stored in the correct tenant.

In `@services/platform/app/features/agents/components/agent-delete-dialog.tsx`:
- Around line 19-25: AgentDeleteDialog declares a displayName prop but doesn't
use it, causing a no-unused-vars error; either incorporate displayName into the
dialog copy (e.g., use displayName in the confirmation description rendered by
AgentDeleteDialog) so the prop is consumed, or remove/underscore-prefix the prop
in the AgentDeleteDialog parameter list to indicate it's intentionally unused
(also fix the same unused reference around the confirmation text at the block
near lines 59-60 where the dialog description is built).

In `@services/platform/app/features/agents/components/agent-knowledge.tsx`:
- Around line 161-166: Currently knowledgeFiles is a fixed empty array, so the
UI never shows uploaded files; replace the hardcoded "const knowledgeFiles:
KnowledgeFile[] = [];" with the reactive data returned from the agent files
binding/query (or component state) so the list reflects backend data and updates
after uploads/removals—use the existing hooks around this area (e.g., the return
from the agent files query or the upload hook usage near useAgentFileUpload and
its uploadFiles/isUploading/accept symbols) and either refetch the query after
upload/remove or update local state so the agent-documents list, RAG status, and
remove action render correctly.
- Around line 139-143: The current fallback sets knowledgeMode to 'off' when
config.knowledgeMode is missing, which ignores legacy rag_search; update the
fallback logic for the knowledgeMode assignment so that if config.knowledgeMode
is undefined but config.rag_search is true you return the equivalent new mode
(e.g., 'web') instead of 'off'. Modify the knowledgeMode initialization (the
const knowledgeMode line) to check config.rag_search as a secondary fallback and
ensure existing includeOrgKnowledge/includeTeamKnowledge logic remains
unchanged.
- Line 154: The teamDocuments useMemo currently always returns an empty array;
update the memo so it computes and returns the filtered list from the fetched
documents (the result of useDocuments()) instead of [] and include the documents
in the dependency array so it recomputes when data changes; locate the const
teamDocuments = useMemo(...) line and replace the empty-return with logic that
derives team-scoped documents (e.g., filter by team or scope field) from the
useDocuments() result.
- Around line 168-184: The handleRemoveFile callback captures organizationId but
it's missing from its dependency array, which can lead to stale values; update
the dependency list for handleRemoveFile to include organizationId (alongside
agentId, removeKnowledgeFile, and t) so the callback is recreated when
organizationId changes and the removeKnowledgeFile.mutateAsync call uses the
current organizationId.

In `@services/platform/app/features/agents/components/agent-navigation.tsx`:
- Around line 62-64: The lint error is due to the unused state isLoadingHistory;
either remove both isLoadingHistory and its setter setIsLoadingHistory from the
useState declaration and delete any setIsLoadingHistory calls in
handleLoadHistory, or wire it into the UI: keep the const [isLoadingHistory,
setIsLoadingHistory] = useState(false) and update handleLoadHistory to
setIsLoadingHistory(true) at start and false on completion/error, then use
isLoadingHistory to render a loading indicator (e.g., disable or show a spinner)
on the History button component so the variable is used.
- Around line 112-139: handleSave is passing a hardcoded orgSlug: 'default' to
snapshotAction.mutateAsync and saveAction.mutateAsync which breaks multi-tenant
behavior; replace the hardcoded value with the component's organizationId (or
derive the expected slug from organizationId before calling) so the calls use
orgSlug: organizationId (or the derived slug) instead; update the two calls in
handleSave (snapshotAction.mutateAsync and saveAction.mutateAsync) to pass the
correct orgSlug and ensure file_actions.ts accepts that slug shape if needed.

In `@services/platform/app/features/agents/components/agents-table.tsx`:
- Around line 32-49: The loadAgents function in AgentsTable is hardcoding
orgSlug: 'default' when calling listAgents, causing every page to load the same
agent directory; change that call in loadAgents to pass the component prop
organizationId (e.g., listAgents({ orgSlug: organizationId })) so agents are
loaded for the active org, and ensure any useCallback/useEffect dependencies
include organizationId so the loader updates when org changes.

In `@services/platform/app/features/agents/components/history-diff-dialog.tsx`:
- Around line 160-172: The two-value row currently renders as "Current →
Snapshot" using ArrowRight which can be counterintuitive for restore actions;
update history-diff-dialog.tsx so the UI explicitly matches restore semantics by
either swapping the displayed columns or conditionally flipping the arrow icon
and header labels (use ArrowLeft or change header text) when the action is a
restore, keeping the existing value rendering (formatValue(change.oldValue),
formatValue(change.newValue)) and using change.type to determine which side is
snapshot vs current.

In `@services/platform/app/features/agents/hooks/queries.ts`:
- Around line 25-40: The async refetch can produce stale updates when rapid prop
changes occur; modify refetch and its useEffect to ignore out-of-order responses
by tracking a per-hook request token (e.g., incrementing requestId or using
AbortController if listAgentsFn supports a signal) captured in the closure, and
only call setData/setError/setIsLoading if the token matches the latest; apply
the same pattern to the other two hooks mentioned (the functions surrounding
lines 55-70 and 85-100) to prevent older responses from overwriting newer state.

In `@services/platform/app/features/agents/hooks/use-agent-config-context.tsx`:
- Around line 52-70: The current markSaving(saving) mutates initialRef.current
when saving ends which doesn't invalidate the isDirty memo (it only depends on
config) and incorrectly promotes in-flight edits as saved; change markSaving to
only set isSaving (do not touch initialRef.current or configRef), and add a new
function markSaved(persisted: AgentJsonConfig) that sets initialRef.current =
persisted and setIsSaving(false); update callers so successful saves call
markSaved(persistedConfig) and error/finally paths call markSaving(false)
without touching refs—this preserves correct dirty detection for isDirty, which
depends on config and initialRef.current.

In `@services/platform/app/features/agents/hooks/use-agents-table-config.tsx`:
- Around line 24-35: AgentsTableConfigOptions declares teamNameMap but it is
unused in useAgentsTableConfig; remove the dead parameter by deleting
teamNameMap from the AgentsTableConfigOptions interface and from the
useAgentsTableConfig function parameter list, then update any callers to stop
passing a teamNameMap argument (or adjust them to supply only
modelPresets/onDuplicated/onDeleted); ensure TypeScript types still compile and
run tests to confirm no remaining references to teamNameMap.

In `@services/platform/app/features/chat/hooks/queries.ts`:
- Around line 72-80: The fetch in useChatAgents is using a hardcoded orgSlug
'default' instead of the provided _organizationId; update the fetchAgents call
(and/or the api.action) so the correct org slug is used: either resolve orgSlug
from _organizationId before calling listAgents({ orgSlug: ... }) (add a lookup
function or service) or change the action to accept organizationId and call
listAgents({ organizationId: _organizationId }) (and update listAgents signature
accordingly); ensure the change is applied where listAgents is invoked inside
fetchAgents in useChatAgents so multi-tenancy works.

In `@services/platform/app/features/chat/hooks/use-effective-agent.ts`:
- Around line 41-58: The fallback selection is non-deterministic because agents
come from filesystem order; before locating DEFAULT_CHAT_AGENT_NAME or using the
firstAgent fallback, sort the agents list deterministically (e.g., by name or
displayName using localeCompare) so the search for defaultAgent and the fallback
const firstAgent = agents[0] produce stable results; modify the logic in
use-effective-agent (the block that defines defaultAgent and firstAgent) to sort
agents first, or explicitly document the fallback strategy if you prefer not to
change ordering.

In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/index.tsx:
- Around line 32-44: The timeout input is only initialized once due to
timeoutInitialized, causing stale UI when config.timeoutMs is updated later;
remove the timeoutInitialized logic and change the useEffect to run whenever
config.timeoutMs changes so timeoutMinutes is updated accordingly: eliminate
timeoutInitialized/setTimeoutInitialized state and the conditional in the
effect, and have the effect set timeoutMinutes based on config.timeoutMs (using
Math.round(config.timeoutMs / 60_000) || DEFAULT_TIMEOUT_MINUTES) with
[config.timeoutMs] as its dependency so the UI always resyncs when
config.timeoutMs changes.

In `@services/platform/convex/agent_tools/delegation/load_delegation_agents.ts`:
- Around line 22-27: The parameter organizationId on loadDelegateAgents is
declared but unused; either remove it from the function signature and update all
call sites to stop passing that argument, or explicitly mark it as intentionally
unused by adding a short clarifying comment (e.g., "// organizationId reserved
for future use") next to the parameter or at the top of the function; reference
the function name loadDelegateAgents when making the change and ensure any
exported type signature / callers are kept in sync.

In `@services/platform/convex/agent_tools/human_input/mutations.ts`:
- Around line 193-206: The fallback agentConfig currently hardcodes tools and
knowledge off (convexToolNames: [], knowledgeMode: 'off', webSearchMode: 'off',
knowledgeFileIds: []), which overrides the original thread’s capabilities when
calling runAgentGeneration; update the logic so that agentConfig is derived from
the existing thread or passed-through original agent configuration (rather than
blindly using the hardcoded object), merging defaults only for missing fields;
specifically, locate the agentConfig construction and the call site of
runAgentGeneration and change it to use the thread's stored agent config (or
accept an agentConfig parameter) and merge with getDefaultModel()/timeout
defaults instead of resetting convexToolNames, knowledgeMode, webSearchMode,
includeTeamKnowledge, includeOrgKnowledge, and knowledgeFileIds to empty/off.

In `@services/platform/convex/agent_tools/location/mutations.ts`:
- Around line 156-169: Replace the synthesized empty agentConfig object with the
actual persisted "chat-agent" configuration loaded from the filesystem-backed
config store; instead of creating agentConfig inline, call the existing loader
that returns the stored agent definition for "chat-agent" (fall back to using
getDefaultModel() and keep properties like timeoutMs and
structuredResponsesEnabled if any fields are missing), and ensure
convexToolNames, knowledgeMode, webSearchMode,
includeTeamKnowledge/includeOrgKnowledge, and knowledgeFileIds reflect the
loaded config so tools/knowledge are preserved after location approval.

In `@services/platform/convex/agent_tools/workflows/internal_mutations.ts`:
- Around line 119-132: The current code builds a blank agentConfig (only
name=String(agentId)) which discards the file-backed agent's instructions,
convexToolNames, model overrides and knowledge settings; instead, fetch and use
the persisted agent config for this agentId (preserving instructions,
convexToolNames, model, enableVectorSearch, knowledgeMode, webSearchMode,
includeTeamKnowledge, includeOrgKnowledge, knowledgeFileIds,
structuredResponsesEnabled, timeoutMs) and pass that full config into
runAgentGeneration rather than the minimal agentConfig; locate the construction
of agentConfig in internal_mutations.ts and replace it with code that
loads/merges the stored agent file-backed config for agentId and supplies it to
runAgentGeneration.
- Around line 98-103: The code currently throws when threadMeta?.agentId is
missing; instead, make triggerWorkflowCompletionResponse tolerate legacy
metadata by falling back from threadMeta?.agentId to the legacy key(s) (e.g.,
threadMeta?.agent_id or other historical field names) when setting agentId, and
only throw if none exist; update the agentId assignment in internal_mutations.ts
(where agentId and threadMeta are used) to use this fallback and optionally emit
a warning/log when a legacy field is used.

In `@services/platform/convex/agents/config.ts`:
- Around line 42-65: Normalize legacy retrieval flags before constructing
convexToolNames: derive knowledgeMode from config.knowledgeMode or from presence
of legacy 'rag_search' (e.g., if config.toolNames?.includes('rag_search') set
knowledgeMode to 'rag', otherwise default 'off'), then create a
normalizedToolNames array by filtering out legacy/internal markers like 'web'
and 'rag_search' from config.toolNames, use that normalizedToolNames for
convexToolNames (convexToolNames: normalizedToolNames as ToolName[]), and
compute webSearchMode from config.webSearchMode or from presence of 'web' in the
original toolNames but do not leave 'web' or 'rag_search' inside convexToolNames
so migrated configs no longer forward hidden markers at runtime.

In `@services/platform/convex/agents/file_actions.ts`:
- Around line 411-413: The catch block returns a template literal using an
unknown-typed err which causes a type error; change the handler in the catch (in
services/platform/convex/agents/file_actions.ts where the History entry catch
is) to normalize err to a string before embedding it (e.g., derive const msg =
err instanceof Error ? err.message : String(err) or use String(err)) and return
{ ok: false, message: `History entry not found: ${msg}` } so the template
literal receives a properly typed string.
- Around line 106-111: The catch block in file_actions.ts returns a template
literal using `err` which is typed as unknown; update the second template
literal (`Invalid JSON in ${agentName}: ${err}`) to convert `err` to a string
explicitly (e.g., use String(err) or extract (err as Error).message) so the
returned `message` is a valid string and TypeScript no longer treats `err` as
unknown; modify the return in the catch of the function handling JSON parsing
accordingly (reference the `agentName` variable and the catch `err` identifier).
- Around line 95-101: The catch block returns a message using an unknown-typed
err in a template literal which violates restrict-template-expressions; update
the return to convert err to a string explicitly (e.g., use String(err) or (err
as Error).message) in the message field so the line becomes message: `Failed to
read agent file: ${agentName} — ${String(err)}` (refer to the catch block where
err and agentName are used in the returned object).
- Around line 233-257: Remove the unused historyDir variable and the stale
comments in the save flow: delete the declaration "const historyDir =
resolveHistoryDir(args.orgSlug, args.agentName);" and prune the multi-line
commentary between the write and return where snapshot logic is discussed,
retaining only the essential lines that serialize config, resolve filePath,
perform atomicWrite(filePath, content), and return { hash: sha256(content) };
keep references to serializeAgentJson, resolveAgentFilePath, atomicWrite, and
sha256 so reviewers can locate the surrounding code.
- Around line 379-385: The current entries processing in the return statement
(the chain that starts with entries.filter(...).map(...) and returns objects
with timestamp and date) doesn't handle non-numeric filenames: Number(ts) can be
NaN producing "Invalid Date" in the returned date field; update the logic to
validate the timestamp before mapping — e.g., parse ts with Number(ts) or
parseInt(ts, 10), check isFinite/Number.isFinite and that new
Date(value).getTime() is not NaN, and only include those entries in the final
array (or assign a safe fallback/skip corrupted files) so the returned objects
(timestamp, date) always have numeric timestamps and valid ISO dates before the
.sort(...) step.
- Around line 320-325: The duplicate-name generation loop (using newName,
counter, existingNames and args.agentName) can spin indefinitely; add a
maxAttempts cap (e.g., const MAX_COPY_ATTEMPTS = 1000) and stop the loop when
counter exceeds it: either throw a clear error or fallback to a safe unique
strategy (e.g., append a timestamp/UUID) to produce newName. Ensure the function
using this logic returns/throws consistently so callers handle the failure.
- Around line 282-284: Replace the non-atomic write in the history snapshot code
— where timestamp, historyPath, and writeFile are used to write currentContent
into historyDir — with the project's atomic write helper (atomicWrite) to avoid
corrupt snapshots on crash; locate the block that constructs historyPath and
calls writeFile(timestampPath, currentContent, 'utf-8') and switch it to
atomicWrite(historyPath, currentContent, 'utf-8') (or adapt to atomicWrite's
signature), and add/import atomicWrite if it's not already imported so history
snapshots use atomic, crash-safe writes.
- Around line 455-459: Replace the non-atomic writeFile call that writes the
pre-restore snapshot with the existing atomicWrite helper to ensure snapshot
consistency; specifically, in the block using writeFile(path.join(historyDir,
`${ts}.json`), currentContent, 'utf-8'), call atomicWrite with the same target
path and content (and same error handling as other atomic writes) so the
snapshot saved by the pre-restore step is written atomically; update any
surrounding .catch(() => {}) behavior to match the project's atomic write
pattern.
- Around line 271-277: The code in snapshotToHistory uses readFile which follows
symlinks; change it to open the file with O_NOFOLLOW and read from the returned
FileHandle to prevent symlink attacks. Specifically, replace the await
readFile(filePath, 'utf-8') call with fs.promises.open(filePath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW), use the returned
FileHandle.readFile('utf8') to get currentContent, and ensure the FileHandle is
closed in a finally block; apply the same pattern used by readAgentFile (and
reference resolveAgentFilePath / snapshotToHistory) so symlink protection is
consistent.
- Around line 437-443: The history and agent file reads in restoreFromHistory
use plain readFile and must enforce O_NOFOLLOW; replace the direct
readFile(historyPath, 'utf-8') and readFile(agentPath, 'utf-8') calls with an
fs.promises.open(...) using fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
then call handle.readFile('utf8') and ensure you close the handle (await
handle.close()). Update the reads that feed parseAgentJson and the
currentContent assignment to use this open/read/close pattern so both
historyPath and agentPath are read with O_NOFOLLOW.
- Around line 408-410: The readFile call that returns parseAgentJson(content)
should be hardened against symlink attacks by opening the file with O_NOFOLLOW
before reading: replace the direct readFile(filePath, 'utf-8') with an
fs.promises.open(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW) to
get a FileHandle, call FileHandle.readFile({ encoding: 'utf8' }) to obtain
content, then close the handle; handle and map symlink-related errors (ELOOP /
EINVAL) to the same failure path so the function (the block that returns { ok:
true, config: parseAgentJson(content) }) fails safely — apply the same pattern
as used to protect snapshotToHistory.

In `@services/platform/convex/agents/file_utils.ts`:
- Around line 110-138: resolveAgentsDir currently trusts orgSlug and
resolveHistoryDir joins agentName without path traversal checks; fix by
validating and normalizing orgSlug and history paths the same way
resolveAgentFilePath does: in resolveAgentsDir (and where orgSlug is consumed)
reject orgSlug values that contain path separators, '.' or '..' or that resolve
outside getBaseDir() by using path.resolve and comparing prefixes, and in
resolveHistoryDir validate agentName with validateAgentName and perform the same
resolved.startsWith(expectedPrefix + path.sep) (or exact match) check before
returning the history directory; ensure you reference and reuse
resolveAgentFilePath / getBaseDir logic to avoid duplication.
- Around line 72-95: Replace the ad-hoc minimal checks in parseAgentJson with a
full schema validation using the shared agentJsonSchema (or AgentJson) to ensure
all fields—including optional ones—are validated; parse the JSON, run
agentJsonSchema.parse/validate on the parsed object, throw or rethrow schema
errors as needed, and return the result typed as AgentJsonConfig instead of the
current double-cast (remove the Record<string,unknown> -> unknown as
AgentJsonConfig pattern); keep the function name parseAgentJson and surface any
validation error messages from the schema for callers to handle.

In `@services/platform/convex/agents/internal_mutations.ts`:
- Around line 49-73: The deleteAgent flow currently removes filesystem files but
doesn't remove agentBindings or agentWebhooks; call the existing
cleanupAgentBinding (internalMutation handler) as part of deleteAgent and also
perform a bulk delete on the agentWebhooks table using the by_agent index
(query('agentWebhooks').withIndex('by_agent', q => q.eq('organizationId',
org).eq('agentFileName', name)) and delete each result) to remove webhook
records and their tokens; ensure this runs before completing deleteAgent so
recreated agents cannot inherit old webhooks.

In `@services/platform/convex/agents/mutations.ts`:
- Around line 107-125: The code inserts fileMetadata before checking
agentBindings for duplicate fileId, causing orphaned metadata on retries; move
the duplicate check to run before calling ctx.db.insert('fileMetadata') by
querying 'agentBindings' with withIndex('by_org_agent') (filtering on
organizationId and agentFileName) and inspecting existing?.knowledgeFiles for a
matching fileId, returning early if found; alternatively perform the check and
the insert inside a single transaction so you only create the fileMetadata when
the binding does not already include the fileId (use the same
ctx.db.query('agentBindings') + knowledgeFiles check and only call
ctx.db.insert('fileMetadata') when not a duplicate).
- Around line 174-201: The code unconditionally deletes RAG entries, storage
blobs, and fileMetadata even when the agent binding is missing or the fileId
isn't present in binding.knowledgeFiles; change the logic in the mutation that
queries agentBindings (the binding variable) so that after computing filtered
and before calling ctx.scheduler.runAfter, ctx.storage.delete, or deleting
metadata, you first verify the binding exists and that the fileId was actually
present (e.g., detect if filtered.length < (binding.knowledgeFiles ?? []).length
or use a boolean found flag). Only if the file was attached to that binding
proceed to call ctx.db.patch(binding._id, ...),
ctx.scheduler.runAfter(internal.agents.internal_actions.deleteKnowledgeFileFromRag,
...), ctx.storage.delete(args.fileId), and the fileMetadata deletion query;
otherwise return/exit without touching scheduler, storage, or fileMetadata.
Ensure you reference binding, binding.knowledgeFiles, ctx.db.patch,
ctx.scheduler.runAfter(internal.agents.internal_actions.deleteKnowledgeFileFromRag),
ctx.storage.delete, and the fileMetadata query when making the change.
- Around line 63-65: The mutations currently stop at authComponent.getAuthUser
and then trust the caller-provided organizationId; update each mutation handler
(the places calling authComponent.getAuthUser — e.g., the call at lines around
the snippet and the other occurrences you noted) to follow authenticate →
getOrganizationMember → mutate: after calling authComponent.getAuthUser, call
the organization membership check (use orgComponent.getOrganizationMember or the
project’s equivalent with authUser.id and the provided organizationId), throw an
error if it returns no membership, and only then proceed to mutate
bindings/files; apply this change to the other occurrences you flagged (around
the calls at 104-105 and 171-172) so every public mutation enforces organization
membership before touching DB or storage.

In `@services/platform/convex/agents/queries.ts`:
- Around line 119-130: The getModelPresets query currently reads process.env
directly; replace that with the shared runtime config by calling
getDefaultAgentRuntimeConfig() and using its model/provider fields to build
presets (e.g., pass the config's fast/standard/advanced model strings into
parseModelList). Import getDefaultAgentRuntimeConfig at top, call it inside the
handler, and use the returned object's properties instead of
process.env.OPENAI_FAST_MODEL, process.env.OPENAI_MODEL, and
process.env.OPENAI_CODING_MODEL so the UI presets match agent execution.

In `@services/platform/convex/agents/seed_system_defaults.ts`:
- Around line 14-29: Add a fail-fast check in the system-default seeding
handlers so org creation validates AGENTS_DIR is present and non-empty; in the
handler for either seedSystemDefaultAgents or ensureSystemDefaults, read the
AGENTS_DIR env var, attempt to stat/read the directory (e.g., using
fs.promises.stat/readdir), and if it is missing, not readable, or contains zero
agent files throw a clear error that surfaces during org initialization (include
the AGENTS_DIR value and a suggestion to set/populate it). This ensures the
handlers validate the environment early rather than allowing downstream runtime
failures when functions like seedSystemDefaultAgents or ensureSystemDefaults
assume agents are present.

In `@services/platform/convex/agents/test_chat.test.ts`:
- Around line 76-86: Replace the ad-hoc "as never" casts in the test fixture
array with a compile-time-checked "satisfies" usage: import the KnowledgeFile
type (or use the appropriate type alias) and annotate the array literal as
"satisfies KnowledgeFile[]" so the fixture (the array containing items with
fileId, fileName, ragStatus) is validated by TypeScript rather than force-cast;
locate the array in test_chat.test.ts (the knowledge files fixture) and change
the casting approach accordingly.

In `@services/platform/convex/agents/unified_chat.ts`:
- Around line 12-23: Remove the unused imports lstat, open, constants, and
sha256 from the top import list (they are not referenced) and update the
catch/rethrow in the block around the parseAgentJson/resolveAgentFilePath usage
so you do not drop the original error object; either rethrow the original error
(throw err) or wrap it while preserving the original (e.g., throw new
Error(`...: ${err.message}`) or use Error.cause) so the original stack/message
is retained. Ensure changes reference the symbols parseAgentJson and
resolveAgentFilePath to find the try/catch, and remove the unused import names
lstat, open, constants, sha256 from the file imports.
- Around line 78-97: The role/team access branches currently "fail open": update
the checks in unified_chat.ts so that when config.roleRestriction ===
'admin_developer' or when binding?.teamId is present and the corresponding
ctx.runQuery checks are not yet implemented, the code throws an explicit error
instead of allowing access; specifically, inside the role check (where
config.roleRestriction is inspected) and the team check (where binding?.teamId
is evaluated after internal.agents.internal_queries.getBindingByAgent), throw a
clear AuthorizationError (or a generic Error if no custom type exists) with a
message indicating the missing access-control implementation, and only allow
execution to continue when the proper ctx.runQuery results confirm access once
those queries are implemented.
- Around line 26-30: The code currently trusts caller-supplied orgSlug while
only authenticating the user via getAuthUser; instead, fetch the organization
record using the provided organizationId and either derive the slug from that
record or validate that the authenticated user is a member/owner of that
organization and that org.slug === args.orgSlug before building or reading the
agent file path. Concretely, in the unified_chat handler flow (where
args.organizationId and args.orgSlug are used and before any filesystem access
around the agent JSON resolution at ~lines 59-63), call the org lookup (e.g.,
getOrganizationById or equivalent), confirm membership/ownership for
getAuthUser(), and replace use of args.orgSlug with the verified org.slug (or
throw/return a permission error if validation fails).

In `@services/platform/convex/agents/webhooks/internal_queries.ts`:
- Around line 8-11: The current lookup uses
ctx.db.query('agentWebhooks').withIndex('by_token', q => q.eq('token',
args.token)).first() and drops the legacy customAgentWebhooks path; change this
to first try the new agentWebhooks lookup and if it returns null/undefined fall
back to querying ctx.db.query('customAgentWebhooks').withIndex('by_token', q =>
q.eq('token', args.token)).first(); return the legacy result when present, add a
short TODO comment that this fallback is temporary for the migration window, and
ensure you reference args.token and preserve the same returned shape so callers
of this function (the existing internal lookup) keep working until migration
completes.

In `@services/platform/convex/agents/webhooks/queries.ts`:
- Around line 6-21: The getWebhooks query handler currently authenticates via
getAuthUserIdentity but does not verify organization membership; after the
authUser check in the handler for getWebhooks, call await
getOrganizationMember(ctx, args.organizationId, authUser) to enforce the same
org-membership authorization used by createWebhook/updateWebhook/deleteWebhook
and other public queries, ensuring sensitive fields like token are protected;
keep this call immediately after the existing "if (!authUser) throw new
Error('Unauthenticated');" check so unauthorized users cannot query another
organization's webhooks.

In `@services/platform/convex/auth.ts`:
- Around line 22-24: The permission map uses the wrong resource key: update
platformResourceStatements and every role object (admin, developer, editor,
member, disabled) to replace the key "agents" with "agentBindings" so it matches
the Convex schema and RLS lookup; specifically edit the
platformResourceStatements constant and each role definition where permissions
are declared (e.g., in the admin/developer/editor/member/disabled role objects)
so authorizeRls(membership?.role, 'agentBindings', ...) can find the correct
permission entries.

In `@services/platform/convex/documents/get_agent_scoped_file_ids.ts`:
- Line 4: Update the JSDoc grammar in the comment for get_agent_scoped_file_ids
(the JSDoc line that currently reads "a agent") to use the correct article "an
agent" so the description reads "Get RAG-indexed file storage IDs scoped to an
agent's knowledge config."; locate the JSDoc block at the top of
get_agent_scoped_file_ids.ts and replace "a agent" with "an agent".

In `@services/platform/convex/lib/context_management/constants.ts`:
- Line 130: Update the stale comment that reads "Agent - user-configurable agent
with DB-stored config" to reflect the new source of truth (filesystem JSON) —
find the "Agent" comment in constants.ts (the inline doc above the Agent
constant/enum entry) and change the wording to something like "Agent -
user-configurable agent with filesystem JSON-stored config" or "Agent -
user-configurable agent with config stored as JSON on the filesystem."

In `@services/platform/convex/README.md`:
- Line 24: The README example uses ctx.db.query("tablename").collect(), but
project convention is to iterate results with a `for await` loop; update the
snippet that references ctx.db.query to remove `.collect()` and instead iterate
the async iterable returned by ctx.db.query using `for await (const row of
ctx.db.query("tablename"))` to accumulate or process documents (e.g., push into
a local array or handle each row inline), ensuring the example shows the async
iteration pattern rather than calling .collect().

In `@services/platform/lib/shared/schemas/agents.ts`:
- Around line 14-51: The new shared schemas (agentJsonSchema, createAgentSchema,
AgentJson, CreateAgent, and retrievalModeSchema) must be used in the runtime
create/edit/read flows: update the agent creation handler, agent update/edit
handler, and the agent file read/parse logic to validate and parse incoming
payloads against createAgentSchema (for create requests) and agentJsonSchema
(for config edits and file reads) instead of the old ad-hoc contract; ensure the
handlers call the Zod parse/parseAsync methods and handle/propagate validation
errors, and replace any existing type annotations with the imported
AgentJson/CreateAgent types so runtime shape and compile-time types remain in
sync.

In `@services/platform/messages/en.json`:
- Around line 709-717: The ICU placeholder in the JSON message key
"history.diffDescription" is using double braces "{{date}}" which won't
interpolate with the rest of the locale file; update the placeholder to the
standard single-brace ICU format "{date}" in the history.diffDescription value
so it matches other entries and will interpolate correctly.

In `@tools/cli/src/lib/compose/services/create-platform-service.ts`:
- Line 13: The compose service currently changes the named volume from
platform-convex-data to platform-data which will create a new empty Docker
volume on upgrade; revert the mount in the volumes array back to the original
named volume (platform-convex-data:/app/data) instead of platform-data so
existing Convex data continues to be mounted into /app/data, and only flip the
name after you add a one-time migration (copy/rename) step in your deploy
tooling.

---

Outside diff comments:
In `@services/platform/app/features/chat/hooks/use-send-message.ts`:
- Around line 148-161: Before calling chatWithAgent, validate that the dynamic
organizationId matches the organization record for the hardcoded slug 'default'
from the loaded organization context; fetch or access the organization for slug
'default' and compare its id to organizationId and throw or return an error if
they differ. Update the call site in use-send-message.ts (around the
chatWithAgent invocation that uses selectedAgent.name, currentThreadId,
mutationAttachments, and userContext) to perform this check and only proceed to
call chatWithAgent when the ids match, mirroring the conversation.organizationId
!== args.organizationId check used in send_message_via_integration.ts.

In `@services/platform/convex/lib/agent_chat/start_agent_chat.ts`:
- Around line 126-142: The code patches thread metadata (threadMeta) before
verifying organization ownership; modify start_agent_chat flow to validate that
args.organizationId matches the loaded conversation/thread organization
immediately after fetching the conversation or threadMeta and before any
ctx.db.patch or insert operations. Specifically, after obtaining threadMeta (via
ctx.db.query('threadMetadata').withIndex('by_threadId'...) or after loading the
conversation), check conversation.organizationId === args.organizationId (or
threadMeta.organizationId if that field exists) and throw or return an
authorization error if they differ, then only proceed to update
generationStatus, streamId, agentId, etc. Ensure this ownership check occurs
prior to calling ctx.db.patch on threadMeta and before any other write to the
conversation/thread.

In `@services/platform/convex/lib/rls/helpers/access_control.test.ts`:
- Around line 10-28: ALL_TABLES currently omits the new agentWebhooks table; add
'agentWebhooks' to the ALL_TABLES const and also include 'agentWebhooks' in the
same role bucket array(s) later in this test file that list tables per role (the
role-to-table buckets used in the regression matrix around the other
agent-related tables such as where 'agentBindings' is placed, referenced later
in the file). Ensure the added entry follows the same ordering/formatting as the
other table names so the full permission map is exercised by the existing tests.

In `@services/platform/scripts/dev.ts`:
- Around line 396-414: The AGENTS_DIR sync should be treated as required when
AGENTS_DIR is set: if runCommand('bunx', ['convex','env','set',
`AGENTS_DIR=${agentsDir}`]) fails, don't just log and continue — fail fast.
Modify the try/catch around the AGENTS_DIR handling (the code using agentsDir
and runCommand) so that on error you either rethrow the error or call
process.exit(1) (and include the full error in the log) instead of only
console.warn; ensure you preserve the existing log message including err.message
when failing.

In `@tools/cli/src/lib/compose/generators/constants.ts`:
- Around line 1-9: Update the "Updating Tale" / upgrade docs to document and
link the migration script ./scripts/2026-03-28-migrate-convex-data.sh and
instruct users to run it when upgrading from pre-rename releases; explicitly
state the old volume name (the Convex volume referenced in the compose VOLUMES
constant, e.g. check for the pre-rename convex volume present alongside the new
'convex' or related entry in the VOLUMES export) and add a prerequisite step in
docs/production-deployment.md: run the migration script before or immediately
after deployment (or provide a warning check) so users do not lose Convex data.
🪄 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: 64947b95-20be-499f-b5dd-0071badac667

📥 Commits

Reviewing files that changed from the base of the PR and between fcc483e and 5ca60fa.

⛔ Files ignored due to path filters (3)
  • bun.lock is excluded by !**/*.lock
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • services/platform/convex/migrations/rename_org_slug.ts is excluded by !**/migrations/**
📒 Files selected for processing (153)
  • .gitignore
  • README.md
  • compose.yml
  • docs/agents.md
  • docs/docs.json
  • docs/roles-and-permissions.md
  • examples/agents/chat-agent.json
  • examples/agents/crm-assistant.json
  • examples/agents/file-assistant.json
  • examples/agents/integration-assistant.json
  • examples/agents/web-assistant.json
  • examples/agents/workflow-assistant.json
  • scripts/2026-03-28-migrate-convex-data.sh
  • services/platform/Dockerfile
  • services/platform/app/features/agents/components/agent-create-dialog.tsx
  • services/platform/app/features/agents/components/agent-delete-dialog.tsx
  • services/platform/app/features/agents/components/agent-knowledge.tsx
  • services/platform/app/features/agents/components/agent-navigation.tsx
  • services/platform/app/features/agents/components/agent-row-actions.tsx
  • services/platform/app/features/agents/components/agent-webhook-section.tsx
  • services/platform/app/features/agents/components/agents-action-menu.tsx
  • services/platform/app/features/agents/components/agents-table.tsx
  • services/platform/app/features/agents/components/history-diff-dialog.tsx
  • services/platform/app/features/agents/components/tool-selector.tsx
  • services/platform/app/features/agents/hooks/mutations.ts
  • services/platform/app/features/agents/hooks/queries.ts
  • services/platform/app/features/agents/hooks/use-agent-config-context.tsx
  • services/platform/app/features/agents/hooks/use-agent-file-upload.ts
  • services/platform/app/features/agents/hooks/use-agents-table-config.tsx
  • services/platform/app/features/automations/hooks/use-assistant-chat.ts
  • services/platform/app/features/chat/components/__tests__/agent-selector.test.tsx
  • services/platform/app/features/chat/components/agent-selector.tsx
  • services/platform/app/features/chat/context/chat-layout-context.tsx
  • services/platform/app/features/chat/hooks/__tests__/use-effective-agent.test.ts
  • services/platform/app/features/chat/hooks/mutations.ts
  • services/platform/app/features/chat/hooks/queries.ts
  • services/platform/app/features/chat/hooks/use-effective-agent.ts
  • services/platform/app/features/chat/hooks/use-send-message.ts
  • services/platform/app/features/custom-agents/components/auto-save-indicator.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-active-toggle.test.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-active-toggle.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-knowledge.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-navigation.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-row-actions.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-version-history-dialog.tsx
  • services/platform/app/features/custom-agents/components/custom-agents-table.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/assistant-message-info.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/test-chat-input.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/test-message-list.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/types.ts
  • services/platform/app/features/custom-agents/hooks/__tests__/use-auto-save.test.ts
  • services/platform/app/features/custom-agents/hooks/mutations.ts
  • services/platform/app/features/custom-agents/hooks/queries.ts
  • services/platform/app/features/custom-agents/hooks/use-auto-save-form.ts
  • services/platform/app/features/custom-agents/hooks/use-auto-save.ts
  • services/platform/app/features/custom-agents/hooks/use-custom-agent-version-context.tsx
  • services/platform/app/features/custom-agents/hooks/use-custom-agents-table-config.tsx
  • services/platform/app/features/custom-agents/hooks/use-test-chat.ts
  • services/platform/app/hooks/use-navigation-items.ts
  • services/platform/app/routeTree.gen.ts
  • services/platform/app/routes/dashboard/$id/agents.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/conversation-starters.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/delegation.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/index.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/instructions.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/knowledge.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/tools.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/webhook.tsx
  • services/platform/app/routes/dashboard/$id/agents/index.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/delegation.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/index.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/instructions.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/tools.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/index.tsx
  • services/platform/app/routes/dashboard/$id/settings/agents.tsx
  • services/platform/app/routes/dashboard/index.tsx
  • services/platform/convex/README.md
  • services/platform/convex/agent_tools/delegation/load_delegation_agents.ts
  • services/platform/convex/agent_tools/human_input/mutations.ts
  • services/platform/convex/agent_tools/location/mutations.ts
  • services/platform/convex/agent_tools/rag/helpers/list_indexed_documents.ts
  • services/platform/convex/agent_tools/rag/rag_search_tool.ts
  • services/platform/convex/agent_tools/workflows/internal_mutations.ts
  • services/platform/convex/agents/__tests__/knowledge_file_rag_info.test.ts
  • services/platform/convex/agents/chat/agent.ts
  • services/platform/convex/agents/chat/internal_actions.ts
  • services/platform/convex/agents/config.ts
  • services/platform/convex/agents/crm/agent.ts
  • services/platform/convex/agents/file/agent.ts
  • services/platform/convex/agents/file_actions.ts
  • services/platform/convex/agents/file_utils.ts
  • services/platform/convex/agents/integration/agent.ts
  • services/platform/convex/agents/internal_actions.ts
  • services/platform/convex/agents/internal_mutations.ts
  • services/platform/convex/agents/internal_queries.ts
  • services/platform/convex/agents/legacy_schema.ts
  • services/platform/convex/agents/mutations.ts
  • services/platform/convex/agents/queries.ts
  • services/platform/convex/agents/schema.ts
  • services/platform/convex/agents/seed_system_defaults.ts
  • services/platform/convex/agents/start_chat.ts
  • services/platform/convex/agents/test_chat.test.ts
  • services/platform/convex/agents/unified_chat.ts
  • services/platform/convex/agents/web/agent.ts
  • services/platform/convex/agents/webhooks/http_actions.ts
  • services/platform/convex/agents/webhooks/internal_actions.ts
  • services/platform/convex/agents/webhooks/internal_mutations.ts
  • services/platform/convex/agents/webhooks/internal_queries.ts
  • services/platform/convex/agents/webhooks/mutations.ts
  • services/platform/convex/agents/webhooks/queries.ts
  • services/platform/convex/agents/webhooks/schema.ts
  • services/platform/convex/auth.ts
  • services/platform/convex/custom_agents/config.ts
  • services/platform/convex/custom_agents/internal_mutations.ts
  • services/platform/convex/custom_agents/internal_queries.ts
  • services/platform/convex/custom_agents/list_custom_agents_paginated.ts
  • services/platform/convex/custom_agents/mutations.ts
  • services/platform/convex/custom_agents/queries.ts
  • services/platform/convex/custom_agents/seed_system_defaults.ts
  • services/platform/convex/custom_agents/system_defaults.ts
  • services/platform/convex/custom_agents/test_chat.test.ts
  • services/platform/convex/custom_agents/test_chat.ts
  • services/platform/convex/custom_agents/unified_chat.ts
  • services/platform/convex/documents/get_agent_scoped_file_ids.ts
  • services/platform/convex/http.ts
  • services/platform/convex/lib/agent_chat/internal_actions.ts
  • services/platform/convex/lib/agent_chat/start_agent_chat.ts
  • services/platform/convex/lib/context_management/constants.ts
  • services/platform/convex/lib/rls/helpers/access_control.test.ts
  • services/platform/convex/lib/rls/helpers/access_control.ts
  • services/platform/convex/lib/rls/helpers/rls_rules.ts
  • services/platform/convex/lib/team_access.ts
  • services/platform/convex/organizations/actions.ts
  • services/platform/convex/schema.ts
  • services/platform/convex/threads/schema.ts
  • services/platform/convex/workflow_engine/instructions/core_instructions.ts
  • services/platform/convex/workflows/definitions/delete_workflow.ts
  • services/platform/docker-entrypoint.sh
  • services/platform/lib/permissions/ability.ts
  • services/platform/lib/shared/constants/agents.ts
  • services/platform/lib/shared/schemas/agents.ts
  • services/platform/lib/shared/schemas/custom_agents.ts
  • services/platform/messages/en.json
  • services/platform/package.json
  • services/platform/scripts/dev.ts
  • tools/cli/src/lib/actions/deploy.ts
  • tools/cli/src/lib/compose/generators/constants.ts
  • tools/cli/src/lib/compose/generators/generate-color-compose.ts
  • tools/cli/src/lib/compose/services/create-platform-service.ts
💤 Files with no reviewable changes (45)
  • services/platform/convex/agents/chat/internal_actions.ts
  • services/platform/app/features/custom-agents/components/custom-agent-version-history-dialog.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-row-actions.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/types.ts
  • services/platform/app/routes/dashboard/$id/custom-agents/index.tsx
  • services/platform/convex/agents/chat/agent.ts
  • services/platform/app/features/custom-agents/components/test-chat-panel/test-chat-input.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-active-toggle.test.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/test-message-list.tsx
  • services/platform/app/features/custom-agents/components/custom-agent-knowledge.tsx
  • services/platform/app/features/custom-agents/hooks/use-custom-agent-version-context.tsx
  • services/platform/app/features/custom-agents/hooks/use-custom-agents-table-config.tsx
  • services/platform/convex/agents/crm/agent.ts
  • services/platform/app/features/custom-agents/components/custom-agent-navigation.tsx
  • services/platform/app/features/custom-agents/components/auto-save-indicator.tsx
  • services/platform/app/features/custom-agents/components/test-chat-panel/assistant-message-info.tsx
  • services/platform/app/features/custom-agents/hooks/use-auto-save.ts
  • services/platform/convex/custom_agents/test_chat.test.ts
  • services/platform/app/features/custom-agents/components/custom-agent-active-toggle.tsx
  • services/platform/app/features/custom-agents/hooks/tests/use-auto-save.test.ts
  • services/platform/convex/agents/file/agent.ts
  • services/platform/app/features/custom-agents/hooks/use-test-chat.ts
  • services/platform/convex/workflow_engine/instructions/core_instructions.ts
  • services/platform/convex/custom_agents/internal_queries.ts
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/delegation.tsx
  • services/platform/convex/custom_agents/list_custom_agents_paginated.ts
  • services/platform/convex/agents/integration/agent.ts
  • services/platform/lib/shared/schemas/custom_agents.ts
  • services/platform/convex/custom_agents/test_chat.ts
  • services/platform/convex/custom_agents/internal_mutations.ts
  • services/platform/convex/agents/web/agent.ts
  • services/platform/convex/custom_agents/seed_system_defaults.ts
  • services/platform/convex/custom_agents/config.ts
  • services/platform/convex/custom_agents/system_defaults.ts
  • services/platform/convex/custom_agents/unified_chat.ts
  • services/platform/app/features/custom-agents/components/custom-agents-table.tsx
  • services/platform/convex/custom_agents/queries.ts
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/instructions.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/tools.tsx
  • services/platform/app/features/custom-agents/hooks/queries.ts
  • services/platform/app/features/custom-agents/hooks/mutations.ts
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId/index.tsx
  • services/platform/app/routes/dashboard/$id/custom-agents/$agentId.tsx
  • services/platform/convex/custom_agents/mutations.ts

Comment thread compose.yml
Comment on lines 328 to 335
volumes:
# Convex local backend data
- platform-convex-data:/app/convex-data
# All persistent platform data (Convex DB, agents, workflows, integrations)
- platform-data:/app/data
# Mount Caddy's CA certificates for trusting self-signed certificates (development only)
- caddy-data:/caddy-data:ro
# Development: mount local agents directory to override defaults
# - ./examples/agents:/app/data/agents

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Volume rename may require migration for existing deployments.

The volume rename from platform-convex-data to platform-data with a new mount path (/app/convex-data/app/data) means existing deployments will start with an empty volume. The PR mentions a migration script exists — ensure documentation or release notes guide users to migrate data from the old volume before upgrading.

Do you want me to help draft migration steps for the release notes, or verify the migration script handles this volume transition?

Also applies to: 504-506

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compose.yml` around lines 328 - 335, The compose change renames the Docker
volume from platform-convex-data (mounted at /app/convex-data) to platform-data
(mounted at /app/data), which will leave existing deployments using the old
volume empty; update release notes and documentation to include explicit
migration steps and ensure the migration script (or an update hook) detects
platform-convex-data, copies its contents into platform-data, and preserves
ownership/permissions before switching to the new mount; specifically add checks
and a fail-safe in the migration utility to: 1) detect existence of
platform-convex-data, 2) create platform-data if missing, 3) rsync or
tar+extract data from /app/convex-data to /app/data on the host volume, 4)
verify content integrity and permissions, and 5) log the actions and any errors
so operators can run the migration manually if needed.

Comment thread docs/agents.md
Comment on lines 51 to +53
## Versioning

Custom agents support versioning. When you edit an agent's instructions, a draft version is created. The live version keeps serving requests until you publish the draft. The version history dialog shows all past versions and lets you compare or roll back.
Agents support versioning. When you edit an agent's instructions, a draft version is created. The live version keeps serving requests until you publish the draft. The version history dialog shows all past versions and lets you compare or roll back.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update this page for the post-versioning agent model.

This section still documents the old draft/live publish flow, rollback behavior, and leaves the auto-save note on Line 25 intact. The PR removes that custom-agent workflow, so readers will be sent to UI/state that no longer exists unless this is rewritten for the new filesystem-backed editing/history flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/agents.md` around lines 51 - 53, Replace the existing "## Versioning"
section that describes the old draft/live publish and rollback workflow with
copy explaining the new filesystem-backed editing and history model: remove
references to draft/live, publishing, and rollback, update or delete the
auto-save note mentioned around Line 25, and document how edits are stored on
disk, how versions are represented (e.g., file snapshots or commits), how to
view/compare history and restore prior states in the new UI/state, and any
relevant user actions or limitations; update the "## Versioning" header content
to reference the new filesystem-backed mechanisms so readers aren't directed to
non-existent draft/live UI.

{
"displayName": "File Assistant",
"description": "Reads and creates files (PDF, Word, Excel, etc.). Can also create visual images by rendering HTML/CSS/SVG \u2014 use it for illustrations, charts, infographics, diagrams, and web page screenshots.",
"systemInstructions": "You are a file assistant specialized in handling file operations.\n\n**KNOWLEDGE SCOPE**\nYou can read files uploaded by users in the chat and generate new files (PDF, Word, Excel, PowerPoint, images, text).\nFor PowerPoint generation, presentation templates must be uploaded to the Knowledge Base on the [Documents page]({{site_url}}/dashboard/{{organization.id}}/documents).\nYou do not search the knowledge base or web \u2014 that is handled by other agents.\n\n**YOUR ROLE**\nYou handle file-related tasks delegated from the main chat agent:\n- Parsing PDF, DOCX, PPTX, and text-based files to extract content\n- Generating PDF, DOCX, PPTX documents, Excel files, and text files\n- Analyzing images using vision capabilities\n\n**ACTION-FIRST PRINCIPLE**\nGenerate with reasonable defaults, ask only when content is truly missing.\n\nALWAYS proceed directly:\n\u2022 Use sensible filenames based on content/context\n\u2022 Choose appropriate formats automatically\n\u2022 For PPTX, pick the first available template unless specified\n\nONLY ask when:\n\u2022 User says \"generate a file\" but provides NO content at all\n\u2022 Image analysis requested but no fileId provided (required)\n\nDo NOT ask about:\n\u2022 Filename preferences (just use a good default)\n\u2022 Format preferences (choose appropriate one)\n\u2022 Number of slides (derive from content provided)\n\n**AVAILABLE TOOLS**\n- pdf: Parse existing PDFs or generate new PDFs from Markdown/HTML\n- docx: Parse Word documents or generate DOCX from sections\n- pptx: Parse or generate PowerPoint presentations (template-based)\n- text: Parse/analyze any text-based file (.txt, .md, .js, .ts, .json, .csv, .log, etc.) OR generate new text files\n- image: Analyze images or generate screenshots from HTML/URLs\n- excel: Generate Excel files or parse uploaded Excel (.xlsx) files\n- document_write: Save a generated file to the documents hub, optionally into a specific folder\n\n**FILE PARSING (pdf, docx, pptx)**\nWhen parsing PDF, DOCX, PPTX files:\n1. Use the URL and filename provided in the user request\n2. Extract ALL relevant content (text, tables, structure)\n3. Preserve document structure in your summary\n4. Note page/slide numbers for reference\n\n**TEXT FILE OPERATIONS (text)**\nThe text tool supports two operations and handles all text-based file formats.\n\nPARSING text files:\n1. Use operation=\"parse\" with fileId parameter for uploaded text-based files\n2. fileId looks like \"kg2bazp7fbgt9srq63knfagjrd7yfenj\" (from attachment context)\n3. Pass the user's question/request as the user_input parameter\n4. For large files, the tool automatically chunks and processes with AI\n5. Supports various encodings (UTF-8, UTF-16, GBK, etc.)\n\nGENERATING text files:\n1. Use operation=\"generate\" to create a new text file\n2. Provide filename (e.g., \"report.txt\") and content (the text to write)\n3. Returns a download URL for the generated file\n4. Example: { \"operation\": \"generate\", \"filename\": \"notes.txt\", \"content\": \"Your text here...\" }\n\n**PPTX GENERATION**\nWhen generating PowerPoint presentations:\n1. First call pptx with operation=\"list_templates\" to find available templates\n2. If no templates are found, tell the user to upload a .pptx template to the Knowledge Base (Documents page) \u2014 NOT in the chat. Include the link URL from the tool result so the user can navigate there directly.\n3. Call pptx with operation=\"generate\" with your content only after you have a valid templateStorageId\n\nThe backend automatically selects the best layout based on your content:\n- title + subtitle \u2192 Title Slide layout\n- title + bulletPoints/textContent \u2192 Title and Content layout\n- title only \u2192 Blank layout\n\nSLIDE CONTENT FIELDS:\n- title: Slide title\n- subtitle: Slide subtitle (for title slides)\n- textContent: Array of text paragraphs\n- bulletPoints: Array of bullet point items\n- tables: Array of {headers: string[], rows: string[][]}\n\nGENERATE FORMAT:\n{\n \"operation\": \"generate\",\n \"templateStorageId\": \"kg...\",\n \"fileName\": \"MyPresentation\",\n \"slidesContent\": [\n {\"title\": \"Welcome\", \"subtitle\": \"Introduction\"},\n {\"title\": \"Agenda\", \"bulletPoints\": [\"Topic 1\", \"Topic 2\"]},\n {\"title\": \"Data\", \"tables\": [{\"headers\": [\"A\", \"B\"], \"rows\": [[\"1\", \"2\"]]}]}\n ]\n}\n\n**FILE GENERATION**\nWhen generating files:\n- PDF: Use sourceType='markdown' for formatted reports\n- DOCX: Provide sections with text/items/tables\n- PPTX: Provide slidesContent with your content\n- Text: Use operation='generate' with filename and content\n- Excel: Provide clear column headers and data structure\n- Images: Use for charts, diagrams, or webpage captures\n\nAfter a successful generate, the file automatically appears as a download card in the chat. Do NOT include the downloadUrl as a markdown link in your response \u2014 just describe what you created.\n\n**DOWNLOADING FILES FROM URLS**\nWhen the user provides URLs to existing PDF files and asks to download/save them:\n1. Use pdf tool with operation=\"generate\", sourceType=\"url\", content=<the URL>\n2. The tool automatically detects direct PDF links and downloads the original file\n3. To save to a folder, follow up with document_write using the returned fileStorageId\n\n**SAVING FILES TO THE DOCUMENTS HUB**\nWhen the user asks to save, store, or download a file to a specific folder:\n1. First generate the file using the appropriate tool (pdf, docx, text, etc.)\n2. Then call document_write with the fileStorageId from the generation result\n3. Set folderPath to the user's requested folder (e.g. \"web_files\", \"reports/2026\")\nFolders are created automatically if they don't exist.\n\n**IMAGE ANALYSIS**\nWhen analyzing images:\n1. ALWAYS use the fileId parameter (not imageUrl) for uploaded images\n2. fileId looks like \"kg2bazp7fbgt9srq63knfagjrd7yfenj\"\n3. Provide a clear question about what to extract/analyze\n\n**RESPONSE GUIDELINES**\n- Return the extracted content in a clear, structured format\n- For generated files, confirm what was created. The file appears as a download card in the chat \u2014 if the user asks for a link or how to download, tell them to use the download button on the card.\n- If parsing fails, explain the error and suggest alternatives\n- For large files, summarize key sections while noting omissions",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Candidates for the PDF tool implementation:"
fd -i 'pdf' services/platform

echo
echo "URL-handling code around PDF generation/import:"
rg -n -C3 'sourceType|url|download|fetch\(' services/platform

echo
echo "SSRF / private-address guardrails:"
rg -n -C3 'allowlist|denylist|localhost|127\.0\.0\.1|169\.254|::1|private.*ip|isPrivate|ssrf' services/platform

Repository: tale-project/tale

Length of output: 50374


🏁 Script executed:

# Read the PDF tool implementation
cat -n services/platform/convex/agent_tools/files/pdf_tool.ts | head -300

Repository: tale-project/tale

Length of output: 9325


🏁 Script executed:

# Search for URL validation patterns in the PDF tool
rg -n 'url|fetch|sourceType|isPrivate|allowlist|denylist|localhost|validation' services/platform/convex/agent_tools/files/pdf_tool.ts -A 5

Repository: tale-project/tale

Length of output: 2716


🏁 Script executed:

# Find the generateDocument action implementation
fd -i 'internal_actions\|generateDocument' services/platform/convex/documents/

Repository: tale-project/tale

Length of output: 43


🏁 Script executed:

# Search for generateDocument function definition
rg -n 'generateDocument|defineAction' services/platform/convex/documents/ -A 10 -B 2 | head -100

Repository: tale-project/tale

Length of output: 8421


🏁 Script executed:

# Read the full generate_document.ts to see URL handling
cat -n services/platform/convex/documents/generate_document.ts

Repository: tale-project/tale

Length of output: 5313


🏁 Script executed:

# Read generate_document_helpers to see buildRequestBody
cat -n services/platform/convex/documents/generate_document_helpers.ts | head -200

Repository: tale-project/tale

Length of output: 6333


🏁 Script executed:

# Search for URL validation logic in the documents directory
rg -n 'isPrivate|allowlist|denylist|localhost|169\.254|::1|validateUrl|SSRF|private.*ip' services/platform/convex/documents/ -i

Repository: tale-project/tale

Length of output: 313


🏁 Script executed:

# Search for crawler service code in the repository
find services -name '*crawler*' -type f 2>/dev/null | head -20

Repository: tale-project/tale

Length of output: 351


🏁 Script executed:

# Search for any SSRF or URL validation utilities anywhere in the codebase
rg -n 'isPrivateIp|isPrivateUrl|validateUrl|SSRF|blocked.*url|url.*blocked' services/platform -i -A 3 | head -50

Repository: tale-project/tale

Length of output: 1120


🏁 Script executed:

# Read the SSRF protection in images/http_actions.ts to understand the pattern
cat -n services/platform/convex/images/http_actions.ts | head -120

Repository: tale-project/tale

Length of output: 4349


Add SSRF protection to PDF URL imports—private address checks are missing.

The pdf tool accepts arbitrary user-supplied URLs and passes them to the crawler service with no validation. Unlike the image proxy (which blocks private IP ranges with isPrivateIp at services/platform/convex/images/http_actions.ts:70), the document generation path at services/platform/convex/documents/generate_document_helpers.ts:98–107 forwards URLs directly to the crawler without any SSRF guards. An attacker can exploit this to scan internal services (localhost, 169.254, 192.168.x.x, etc.). Implement the same isPrivateIp checks before accepting sourceType: 'url' requests in the pdf tool or its underlying document generation handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/agents/file-assistant.json` at line 4, The pdf URL import path in
services/platform/convex/documents/generate_document_helpers.ts forwards
user-supplied URLs to the crawler without SSRF checks; add the same
private-address protection used by isPrivateIp
(services/platform/convex/images/http_actions.ts) by validating sourceType ===
'url' inputs: resolve the hostname, perform an IP check (reuse isPrivateIp)
against all resolved addresses and reject any private/loopback/metadata-range
addresses before calling the crawler; ensure the pdf tool handler (the function
that processes sourceType:'url' in generate_document_helpers) returns a clear
error for blocked URLs and does not attempt the fetch if any resolved IP is
private.

Comment on lines +2 to +9
"displayName": "Web Assistant",
"description": "Searches the web for the latest information",
"systemInstructions": "You are a web assistant specialized in searching crawled website content.\n\n**KNOWLEDGE SCOPE**\nYou search ONLY websites that the organization has manually added on the [Websites page]({{site_url}}/dashboard/{{organization.id}}/websites). You do not have access to the open internet.\nIf no results are found, suggest the user add the relevant website domain on the [Websites page]({{site_url}}/dashboard/{{organization.id}}/websites) so it can be crawled and indexed.\n\n**YOUR ROLE**\nYou handle web-related tasks delegated from the main chat agent:\n- Searching crawled website content (indexed pages from the organization's websites)\n- Answering questions using previously indexed web page content\n\n**AVAILABLE TOOLS**\n- web: Semantic search over crawled website pages (vector + full-text)\n\n**MANDATORY SEARCH-FIRST RULE**\nYou MUST ALWAYS call the web tool before responding. No exceptions.\nDo NOT assume what is or isn't in the knowledge base \u2014 ALWAYS search first.\nEven if the query seems unlikely to match indexed content, search anyway.\nOnly after receiving search results (or confirming no results) may you compose your response.\n\nDo NOT:\n\u2022 Skip the search based on your own judgment about the query\n\u2022 Respond without calling the web tool at least once\n\u2022 Ask for clarification before searching\n\n**SEARCH EXAMPLES**\n- web(query='shipping policy')\n- web(query='product pricing details')\n- web(query='return and refund process')\n\n**RESPONSE GUIDELINES**\n- Be concise and focus on answering the delegated request\n- Include source URLs when citing information\n- If no results are found, remind the user to add the website domain on the [Websites page]({{site_url}}/dashboard/{{organization.id}}/websites)\n- Summarize lengthy content while preserving key information\n- Extract key facts, numbers, and dates\n- Preserve important structured data (tables, lists)",
"toolNames": ["web"],
"maxSteps": 5,
"timeoutMs": 300000,
"outputReserve": 2048,
"modelPreset": "fast"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The label promises open-web search, but the prompt forbids it.

description says this agent "Searches the web for the latest information", while systemInstructions restrict it to the org's crawled websites and frame it as a delegated-only specialist. That mismatch will send users to the wrong agent and produce false negatives for anything outside indexed domains.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/agents/web-assistant.json` around lines 2 - 9, The metadata is
inconsistent: the "description" claims open-web search while
"systemInstructions" restrict searches to manually added crawled sites; update
the "description" (and optionally "displayName") to accurately reflect the
agent's scope (e.g., "Searches the organization's indexed website content — no
open-internet access") so it matches the "systemInstructions" and avoids
misleading users about the web tool's capabilities.

Comment on lines +63 to +72
if [ ! -d "$src" ] || [ -z "$(ls -A "$src" 2>/dev/null)" ]; then
echo " ⏭ $dir/ (empty or missing, skipping)"
continue
fi

mkdir -p "$dst"

before=$(ls "$dst" 2>/dev/null | wc -l)
cp -rn "$src/"* "$dst/" 2>/dev/null || true
after=$(ls "$dst" | wc -l)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp=$(mktemp -d)
mkdir -p "$tmp/src" "$tmp/dst"
touch "$tmp/src/.hidden"

if cp -rn "$tmp/src/"* "$tmp/dst/" 2>/dev/null; then
  echo "unexpected success"
else
  echo "cp failed because * did not match the hidden file"
fi

echo "destination contents:"
ls -la "$tmp/dst"

Repository: tale-project/tale

Length of output: 239


🏁 Script executed:

# Check if the file exists and examine the relevant lines
if [ -f "scripts/2026-03-28-migrate-convex-data.sh" ]; then
  echo "File found. Showing lines 63-75:"
  sed -n '63,75p' "scripts/2026-03-28-migrate-convex-data.sh"
else
  echo "File not found at scripts/2026-03-28-migrate-convex-data.sh"
  # Try to find migration scripts
  find . -name "*migrate*convex*" -o -name "*convex*migrate*" 2>/dev/null | head -20
fi

Repository: tale-project/tale

Length of output: 519


🏁 Script executed:

# Test the suggested fix: cp -an "$src"/. "$dst"/
set -euo pipefail

tmp=$(mktemp -d)
mkdir -p "$tmp/src" "$tmp/dst"
touch "$tmp/src/.hidden"
echo "regular" > "$tmp/src/regular.txt"

echo "=== Testing suggested fix: cp -an ===" 
if cp -an "$tmp/src"/. "$tmp/dst/"; then
  echo "success"
else
  echo "failed with exit code $?"
fi

echo "destination contents:"
ls -la "$tmp/dst" 2>&1 | head -10

# Clean up and test with only hidden files
rm -rf "$tmp/dst"
mkdir "$tmp/dst"
rm "$tmp/src/regular.txt"

echo ""
echo "=== Testing with only hidden files ==="
if cp -an "$tmp/src"/. "$tmp/dst/"; then
  echo "success"
else
  echo "failed with exit code $?"
fi

echo "destination contents:"
ls -la "$tmp/dst"

rm -rf "$tmp"

Repository: tale-project/tale

Length of output: 581


Copy the whole source directory, not "$src/"*.

* excludes dotfiles, and the trailing || true silently masks the resulting cp failure. A Convex directory containing only hidden metadata files will be reported as migrated even though nothing was copied.

Safer copy command
-        cp -rn "$src/"* "$dst/" 2>/dev/null || true
+        cp -an "$src"/. "$dst"/
📝 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.

Suggested change
if [ ! -d "$src" ] || [ -z "$(ls -A "$src" 2>/dev/null)" ]; then
echo "$dir/ (empty or missing, skipping)"
continue
fi
mkdir -p "$dst"
before=$(ls "$dst" 2>/dev/null | wc -l)
cp -rn "$src/"* "$dst/" 2>/dev/null || true
after=$(ls "$dst" | wc -l)
if [ ! -d "$src" ] || [ -z "$(ls -A "$src" 2>/dev/null)" ]; then
echo "$dir/ (empty or missing, skipping)"
continue
fi
mkdir -p "$dst"
before=$(ls "$dst" 2>/dev/null | wc -l)
cp -an "$src"/. "$dst"/
after=$(ls "$dst" | wc -l)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/2026-03-28-migrate-convex-data.sh` around lines 63 - 72, The copy
currently uses cp -rn "$src/"* which omits dotfiles and hides errors with ||
true; change the copy to include hidden files and surface failures by using a
robust command such as cp -a "$src"/. "$dst"/ (or rsync -a --links "$src"/
"$dst"/) instead of cp -rn "$src/"* and remove the trailing "|| true"; then
check the cp/rsync exit code and use before/after counts (variables before and
after) to detect no-op copies and log an error or skip accordingly. Ensure you
update the cp invocation around the src and dst variables (the lines that set
before, perform cp, and set after) so dotfiles are copied and failures are not
silently ignored.

Comment on lines 22 to 24
const platformResourceStatements = {
customAgents: ['read', 'write'],
agents: ['read', 'write'],
documents: ['read', 'write'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Determine the correct table/permission key name.
# Expect: Find the schema table name to determine which key is correct.

# Check Convex schema for agent-related tables
rg -n 'defineTable|defineSchema' services/platform/convex/agents/schema.ts -A5

# Check what table name is used in agent queries/mutations
rg -n "ctx\.db\.(query|get|insert|patch|delete)\(['\"]agent" services/platform/convex/agents/

Repository: tale-project/tale

Length of output: 881


🏁 Script executed:

#!/bin/bash
# Step 1: Read auth.ts to see the exact permission keys used
echo "=== auth.ts platformResourceStatements and role definitions ==="
sed -n '22,135p' services/platform/convex/auth.ts | head -40

echo -e "\n=== auth.ts authorizeRls function (lines 178-202) ==="
sed -n '178,202p' services/platform/convex/auth.ts

Repository: tale-project/tale

Length of output: 2078


🏁 Script executed:

#!/bin/bash
# Step 2: Check rls_rules.ts for authorizeRls calls with agent-related parameters
echo "=== rls_rules.ts authorizeRls calls with agents/agentBindings ==="
rg -n "authorizeRls.*agent" services/platform/convex/lib/rls/helpers/rls_rules.ts -B2 -A2

Repository: tale-project/tale

Length of output: 731


🏁 Script executed:

#!/bin/bash
# Check if "agents" is used as a distinct resource elsewhere (e.g., agent configurations)
# vs agentBindings (which is the actual table for agent bindings)
rg -n "ctx\.db\.(query|get|insert|patch|delete)\(['\"]agents['\"]" services/platform/convex/

Repository: tale-project/tale

Length of output: 43


Change permission key from agents to agentBindings in auth.ts to match the RLS rules and schema table name.

The platformResourceStatements and all role definitions use agents as the permission key, but the Convex schema table is named agentBindings (line 35 of agents/schema.ts), and the RLS rules call authorizeRls(membership?.role, 'agentBindings', ...) (lines 69, 78, 86 of lib/rls/helpers/rls_rules.ts). When authorizeRls() looks up the permission in the role definition, it will fail because the role only defines agents, not agentBindings.

Update all occurrences in platformResourceStatements and all role definitions (admin, developer, editor, member, disabled) from agents to agentBindings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/auth.ts` around lines 22 - 24, The permission map
uses the wrong resource key: update platformResourceStatements and every role
object (admin, developer, editor, member, disabled) to replace the key "agents"
with "agentBindings" so it matches the Convex schema and RLS lookup;
specifically edit the platformResourceStatements constant and each role
definition where permissions are declared (e.g., in the
admin/developer/editor/member/disabled role objects) so
authorizeRls(membership?.role, 'agentBindings', ...) can find the correct
permission entries.


/**
* Get RAG-indexed file storage IDs scoped to a custom agent's knowledge config.
* Get RAG-indexed file storage IDs scoped to a agent's knowledge config.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix JSDoc grammar on Line 4 (“a agent” → “an agent”).

✏️ Proposed fix
- * Get RAG-indexed file storage IDs scoped to a agent's knowledge config.
+ * Get RAG-indexed file storage IDs scoped to an agent's knowledge config.
📝 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.

Suggested change
* Get RAG-indexed file storage IDs scoped to a agent's knowledge config.
* Get RAG-indexed file storage IDs scoped to an agent's knowledge config.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/documents/get_agent_scoped_file_ids.ts` at line 4,
Update the JSDoc grammar in the comment for get_agent_scoped_file_ids (the JSDoc
line that currently reads "a agent") to use the correct article "an agent" so
the description reads "Get RAG-indexed file storage IDs scoped to an agent's
knowledge config."; locate the JSDoc block at the top of
get_agent_scoped_file_ids.ts and replace "a agent" with "an agent".

timeoutMs: 180_000,
},
/** Custom agent - user-configurable agent with DB-stored config */
/** Agent - user-configurable agent with DB-stored config */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update stale storage wording in the comment.

Line 130 says “DB-stored config,” but this PR migrates agent configs to filesystem JSON; this comment should reflect the new source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/lib/context_management/constants.ts` at line 130,
Update the stale comment that reads "Agent - user-configurable agent with
DB-stored config" to reflect the new source of truth (filesystem JSON) — find
the "Agent" comment in constants.ts (the inline doc above the Agent
constant/enum entry) and change the wording to something like "Agent -
user-configurable agent with filesystem JSON-stored config" or "Agent -
user-configurable agent with config stored as JSON on the filesystem."

Comment thread services/platform/convex/README.md Outdated
// Read the database as many times as you need here.
// See https://docs.convex.dev/database/reading-data.
const documents = await ctx.db.query('tablename').collect();
const documents = await ctx.db.query("tablename").collect();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Align query example with repo convention (for await instead of .collect()).

♻️ Proposed doc snippet update
-    const documents = await ctx.db.query("tablename").collect();
+    const documents = [];
+    for await (const doc of ctx.db.query("tablename")) {
+      documents.push(doc);
+    }

Based on learnings: Convex query code in this repo should use for await loops and avoid .collect().

📝 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.

Suggested change
const documents = await ctx.db.query("tablename").collect();
const documents = [];
for await (const doc of ctx.db.query("tablename")) {
documents.push(doc);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/README.md` at line 24, The README example uses
ctx.db.query("tablename").collect(), but project convention is to iterate
results with a `for await` loop; update the snippet that references ctx.db.query
to remove `.collect()` and instead iterate the async iterable returned by
ctx.db.query using `for await (const row of ctx.db.query("tablename"))` to
accumulate or process documents (e.g., push into a local array or handle each
row inline), ensuring the example shows the async iteration pattern rather than
calling .collect().

'platform-convex-data:/app/convex-data',
'caddy-data:/caddy-data:ro',
],
volumes: ['platform-data:/app/data', 'caddy-data:/caddy-data:ro'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Keep the existing named volume until upgrade migration is explicit.

Renaming the compose mount from platform-convex-data to platform-data creates a brand-new Docker volume on upgrade. The old Convex contents never get mounted into /app/data, so the migration logic in this PR can't see them and upgraded installs can boot empty.

🛠️ Safer short-term change
-    volumes: ['platform-data:/app/data', 'caddy-data:/caddy-data:ro'],
+    volumes: ['platform-convex-data:/app/data', 'caddy-data:/caddy-data:ro'],

Flip the volume name only after deploy tooling performs a one-time copy or rename of the existing Docker volume.

📝 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.

Suggested change
volumes: ['platform-data:/app/data', 'caddy-data:/caddy-data:ro'],
volumes: ['platform-convex-data:/app/data', 'caddy-data:/caddy-data:ro'],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/src/lib/compose/services/create-platform-service.ts` at line 13,
The compose service currently changes the named volume from platform-convex-data
to platform-data which will create a new empty Docker volume on upgrade; revert
the mount in the volumes array back to the original named volume
(platform-convex-data:/app/data) instead of platform-data so existing Convex
data continues to be mounted into /app/data, and only flip the name after you
add a one-time migration (copy/rename) step in your deploy tooling.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +25 to +40
const refetch = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const result = await listAgentsFn({ orgSlug });
setData(result);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
} finally {
setIsLoading(false);
}
}, [listAgentsFn, orgSlug]);

useEffect(() => {
void refetch();
}, [refetch]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard these async hooks against stale responses.

All three hooks fire async actions on prop changes, but none versions or cancels the in-flight request. If the user switches orgs or agents quickly, an older slower response can win the race and overwrite the newer list/agent/history state.

Also applies to: 55-70, 85-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/app/features/agents/hooks/queries.ts` around lines 25 - 40,
The async refetch can produce stale updates when rapid prop changes occur;
modify refetch and its useEffect to ignore out-of-order responses by tracking a
per-hook request token (e.g., incrementing requestId or using AbortController if
listAgentsFn supports a signal) captured in the closure, and only call
setData/setError/setIsLoading if the token matches the latest; apply the same
pattern to the other two hooks mentioned (the functions surrounding lines 55-70
and 85-100) to prevent older responses from overwriting newer state.

Comment on lines +95 to +101
} catch (err) {
return {
ok: false,
error: 'not_found',
message: `Failed to read agent file: ${agentName} — ${err}`,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix template literal type error for unknown type.

The err variable is of type unknown, which violates restrict-template-expressions. Convert to string explicitly.

🔧 Proposed fix
   } catch (err) {
     return {
       ok: false,
       error: 'not_found',
-      message: `Failed to read agent file: ${agentName} — ${err}`,
+      message: `Failed to read agent file: ${agentName} — ${String(err)}`,
     };
   }
📝 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.

Suggested change
} catch (err) {
return {
ok: false,
error: 'not_found',
message: `Failed to read agent file: ${agentName}${err}`,
};
}
} catch (err) {
return {
ok: false,
error: 'not_found',
message: `Failed to read agent file: ${agentName}${String(err)}`,
};
}
🧰 Tools
🪛 GitHub Actions: Lint

[error] 99-99: typescript-eslint(restrict-template-expressions): Invalid type used in template literal expression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/file_actions.ts` around lines 95 - 101, The
catch block returns a message using an unknown-typed err in a template literal
which violates restrict-template-expressions; update the return to convert err
to a string explicitly (e.g., use String(err) or (err as Error).message) in the
message field so the line becomes message: `Failed to read agent file:
${agentName} — ${String(err)}` (refer to the catch block where err and agentName
are used in the returned object).

Comment on lines +106 to +111
} catch (err) {
return {
ok: false,
error: 'corrupted',
message: `Invalid JSON in ${agentName}: ${err}`,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Apply same fix for the second template literal with err.

Same issue as above — err is unknown and needs explicit conversion.

🔧 Proposed fix
   } catch (err) {
     return {
       ok: false,
       error: 'corrupted',
-      message: `Invalid JSON in ${agentName}: ${err}`,
+      message: `Invalid JSON in ${agentName}: ${String(err)}`,
     };
   }
📝 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.

Suggested change
} catch (err) {
return {
ok: false,
error: 'corrupted',
message: `Invalid JSON in ${agentName}: ${err}`,
};
} catch (err) {
return {
ok: false,
error: 'corrupted',
message: `Invalid JSON in ${agentName}: ${String(err)}`,
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/file_actions.ts` around lines 106 - 111, The
catch block in file_actions.ts returns a template literal using `err` which is
typed as unknown; update the second template literal (`Invalid JSON in
${agentName}: ${err}`) to convert `err` to a string explicitly (e.g., use
String(err) or extract (err as Error).message) so the returned `message` is a
valid string and TypeScript no longer treats `err` as unknown; modify the return
in the catch of the function handling JSON parsing accordingly (reference the
`agentName` variable and the catch `err` identifier).

Comment on lines +233 to +257
const config = args.config as AgentJsonConfig;
const content = serializeAgentJson(config);
const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
const historyDir = resolveHistoryDir(args.orgSlug, args.agentName);

// Write new data FIRST (critical path)
await atomicWrite(filePath, content);

// Snapshot previous state to history SECOND (best-effort)
// Note: we snapshot the NEW content as a history record of this save.
// The actual "previous" content was the file before this write.
// For simplicity, we read back what we just wrote — the caller should
// pass the previous content if they want true pre-save snapshots.
// In practice, the UI sends the full new config, so we snapshot the
// file that existed before by reading it before writing.
// Let's fix the order: snapshot first, then write.

// Actually, per review findings: write first, snapshot second.
// The snapshot is of the PREVIOUS file content, but since we already
// overwrote it, we need a different approach.
// The correct flow: the caller reads current state before editing,
// so the UI holds the "before" state. We just write the new state.
// History is created by snapshotToHistory action called separately.

return { hash: sha256(content) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused historyDir variable and stale comments.

The historyDir variable is declared but never used (flagged by pipeline). The extensive comments (lines 241-255) discuss abandoned approaches and are confusing — the final design is that snapshotToHistory is called separately by the UI.

🧹 Proposed fix
     const config = args.config as AgentJsonConfig;
     const content = serializeAgentJson(config);
     const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
-    const historyDir = resolveHistoryDir(args.orgSlug, args.agentName);
 
-    // Write new data FIRST (critical path)
     await atomicWrite(filePath, content);
 
-    // Snapshot previous state to history SECOND (best-effort)
-    // Note: we snapshot the NEW content as a history record of this save.
-    // The actual "previous" content was the file before this write.
-    // For simplicity, we read back what we just wrote — the caller should
-    // pass the previous content if they want true pre-save snapshots.
-    // In practice, the UI sends the full new config, so we snapshot the
-    // file that existed before by reading it before writing.
-    // Let's fix the order: snapshot first, then write.
-
-    // Actually, per review findings: write first, snapshot second.
-    // The snapshot is of the PREVIOUS file content, but since we already
-    // overwrote it, we need a different approach.
-    // The correct flow: the caller reads current state before editing,
-    // so the UI holds the "before" state. We just write the new state.
-    // History is created by snapshotToHistory action called separately.
-
     return { hash: sha256(content) };
📝 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.

Suggested change
const config = args.config as AgentJsonConfig;
const content = serializeAgentJson(config);
const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
const historyDir = resolveHistoryDir(args.orgSlug, args.agentName);
// Write new data FIRST (critical path)
await atomicWrite(filePath, content);
// Snapshot previous state to history SECOND (best-effort)
// Note: we snapshot the NEW content as a history record of this save.
// The actual "previous" content was the file before this write.
// For simplicity, we read back what we just wrote — the caller should
// pass the previous content if they want true pre-save snapshots.
// In practice, the UI sends the full new config, so we snapshot the
// file that existed before by reading it before writing.
// Let's fix the order: snapshot first, then write.
// Actually, per review findings: write first, snapshot second.
// The snapshot is of the PREVIOUS file content, but since we already
// overwrote it, we need a different approach.
// The correct flow: the caller reads current state before editing,
// so the UI holds the "before" state. We just write the new state.
// History is created by snapshotToHistory action called separately.
return { hash: sha256(content) };
const config = args.config as AgentJsonConfig;
const content = serializeAgentJson(config);
const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
await atomicWrite(filePath, content);
return { hash: sha256(content) };
🧰 Tools
🪛 GitHub Actions: Lint

[error] 236-236: eslint(no-unused-vars): Variable 'historyDir' is declared but never used. Unused variables should start with a '_'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/file_actions.ts` around lines 233 - 257,
Remove the unused historyDir variable and the stale comments in the save flow:
delete the declaration "const historyDir = resolveHistoryDir(args.orgSlug,
args.agentName);" and prune the multi-line commentary between the write and
return where snapshot logic is discussed, retaining only the essential lines
that serialize config, resolve filePath, perform atomicWrite(filePath, content),
and return { hash: sha256(content) }; keep references to serializeAgentJson,
resolveAgentFilePath, atomicWrite, and sha256 so reviewers can locate the
surrounding code.

Comment on lines +271 to +277
const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
let currentContent: string;
try {
currentContent = await readFile(filePath, 'utf-8');
} catch {
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use O_NOFOLLOW for consistent symlink protection.

readAgentFile carefully uses O_NOFOLLOW to prevent symlink attacks, but snapshotToHistory uses plain readFile which follows symlinks. An attacker who can create symlinks could snapshot arbitrary files into history.

🛡️ Proposed fix
     const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
     let currentContent: string;
     try {
-      currentContent = await readFile(filePath, 'utf-8');
+      const fd = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
+      try {
+        currentContent = await fd.readFile('utf-8');
+      } finally {
+        await fd.close();
+      }
     } catch {
       return null;
     }
📝 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.

Suggested change
const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
let currentContent: string;
try {
currentContent = await readFile(filePath, 'utf-8');
} catch {
return null;
}
import { open } from 'fs/promises';
import { constants } from 'fs';
// ... existing code ...
const filePath = resolveAgentFilePath(args.orgSlug, args.agentName);
let currentContent: string;
try {
const fd = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
currentContent = await fd.readFile('utf-8');
} finally {
await fd.close();
}
} catch {
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/file_actions.ts` around lines 271 - 277, The
code in snapshotToHistory uses readFile which follows symlinks; change it to
open the file with O_NOFOLLOW and read from the returned FileHandle to prevent
symlink attacks. Specifically, replace the await readFile(filePath, 'utf-8')
call with fs.promises.open(filePath, fs.constants.O_RDONLY |
fs.constants.O_NOFOLLOW), use the returned FileHandle.readFile('utf8') to get
currentContent, and ensure the FileHandle is closed in a finally block; apply
the same pattern used by readAgentFile (and reference resolveAgentFilePath /
snapshotToHistory) so symlink protection is consistent.

Comment on lines +107 to +125
await ctx.db.insert('fileMetadata', {
organizationId: args.organizationId,
storageId: args.fileId,
fileName: args.fileName,
contentType: args.contentType,
size: args.fileSize,
});

const existing = await ctx.db
.query('agentBindings')
.withIndex('by_org_agent', (q) =>
q
.eq('organizationId', args.organizationId)
.eq('agentFileName', args.agentFileName),
)
.first();

const knowledgeFiles = existing?.knowledgeFiles ?? [];
if (knowledgeFiles.some((f) => f.fileId === args.fileId)) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Check for duplicate attachments before inserting metadata.

A retried upload currently creates a fileMetadata row and only then discovers that the binding already contains fileId. The early return leaves orphan/duplicate metadata behind, and downstream cleanup only deletes the first matching row.

Suggested fix
-    await ctx.db.insert('fileMetadata', {
-      organizationId: args.organizationId,
-      storageId: args.fileId,
-      fileName: args.fileName,
-      contentType: args.contentType,
-      size: args.fileSize,
-    });
-
     const existing = await ctx.db
       .query('agentBindings')
       .withIndex('by_org_agent', (q) =>
         q
           .eq('organizationId', args.organizationId)
@@
 
     const knowledgeFiles = existing?.knowledgeFiles ?? [];
     if (knowledgeFiles.some((f) => f.fileId === args.fileId)) return null;
+
+    await ctx.db.insert('fileMetadata', {
+      organizationId: args.organizationId,
+      storageId: args.fileId,
+      fileName: args.fileName,
+      contentType: args.contentType,
+      size: args.fileSize,
+    });
📝 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.

Suggested change
await ctx.db.insert('fileMetadata', {
organizationId: args.organizationId,
storageId: args.fileId,
fileName: args.fileName,
contentType: args.contentType,
size: args.fileSize,
});
const existing = await ctx.db
.query('agentBindings')
.withIndex('by_org_agent', (q) =>
q
.eq('organizationId', args.organizationId)
.eq('agentFileName', args.agentFileName),
)
.first();
const knowledgeFiles = existing?.knowledgeFiles ?? [];
if (knowledgeFiles.some((f) => f.fileId === args.fileId)) return null;
const existing = await ctx.db
.query('agentBindings')
.withIndex('by_org_agent', (q) =>
q
.eq('organizationId', args.organizationId)
.eq('agentFileName', args.agentFileName),
)
.first();
const knowledgeFiles = existing?.knowledgeFiles ?? [];
if (knowledgeFiles.some((f) => f.fileId === args.fileId)) return null;
await ctx.db.insert('fileMetadata', {
organizationId: args.organizationId,
storageId: args.fileId,
fileName: args.fileName,
contentType: args.contentType,
size: args.fileSize,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/mutations.ts` around lines 107 - 125, The
code inserts fileMetadata before checking agentBindings for duplicate fileId,
causing orphaned metadata on retries; move the duplicate check to run before
calling ctx.db.insert('fileMetadata') by querying 'agentBindings' with
withIndex('by_org_agent') (filtering on organizationId and agentFileName) and
inspecting existing?.knowledgeFiles for a matching fileId, returning early if
found; alternatively perform the check and the insert inside a single
transaction so you only create the fileMetadata when the binding does not
already include the fileId (use the same ctx.db.query('agentBindings') +
knowledgeFiles check and only call ctx.db.insert('fileMetadata') when not a
duplicate).

Comment on lines +174 to +201
const binding = await ctx.db
.query('agentBindings')
.withIndex('by_org_agent', (q) =>
q
.eq('organizationId', args.organizationId)
.eq('agentFileName', args.agentFileName),
)
.first();

if (binding) {
const filtered = (binding.knowledgeFiles ?? []).filter(
(f) => f.fileId !== args.fileId,
);
await ctx.db.patch(binding._id, { knowledgeFiles: filtered });
}

await ctx.scheduler.runAfter(
0,
internal.agents.internal_actions.deleteKnowledgeFileFromRag,
{ fileId: args.fileId },
);
await ctx.storage.delete(args.fileId);

const metadata = await ctx.db
.query('fileMetadata')
.withIndex('by_storageId', (q) => q.eq('storageId', args.fileId))
.first();
if (metadata) await ctx.db.delete(metadata._id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Only delete blobs that are attached to this agent binding.

Even when the binding is missing or fileId is not present in binding.knowledgeFiles, this mutation still schedules RAG deletion, deletes the storage object, and removes the first fileMetadata row for that storageId. A bad or malicious fileId can therefore delete unrelated files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/mutations.ts` around lines 174 - 201, The
code unconditionally deletes RAG entries, storage blobs, and fileMetadata even
when the agent binding is missing or the fileId isn't present in
binding.knowledgeFiles; change the logic in the mutation that queries
agentBindings (the binding variable) so that after computing filtered and before
calling ctx.scheduler.runAfter, ctx.storage.delete, or deleting metadata, you
first verify the binding exists and that the fileId was actually present (e.g.,
detect if filtered.length < (binding.knowledgeFiles ?? []).length or use a
boolean found flag). Only if the file was attached to that binding proceed to
call ctx.db.patch(binding._id, ...),
ctx.scheduler.runAfter(internal.agents.internal_actions.deleteKnowledgeFileFromRag,
...), ctx.storage.delete(args.fileId), and the fileMetadata deletion query;
otherwise return/exit without touching scheduler, storage, or fileMetadata.
Ensure you reference binding, binding.knowledgeFiles, ctx.db.patch,
ctx.scheduler.runAfter(internal.agents.internal_actions.deleteKnowledgeFileFromRag),
ctx.storage.delete, and the fileMetadata query when making the change.

Comment on lines +119 to +130
export const getModelPresets = query({
args: {},
handler: async (): Promise<{
fast: string[];
standard: string[];
advanced: string[];
}> => {
return {
fast: parseModelList(process.env.OPENAI_FAST_MODEL),
standard: parseModelList(process.env.OPENAI_MODEL),
advanced: parseModelList(process.env.OPENAI_CODING_MODEL),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reuse the centralized runtime config for model presets.

Reading OPENAI_* directly here can drift from the provider/model defaults the runtime actually uses, so the UI may offer presets that do not match agent execution. Source these lists from the same runtime config helper the agent modules use instead. Based on learnings, agent modules should source runtime model and provider via getDefaultAgentRuntimeConfig() instead of reading process.env directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/convex/agents/queries.ts` around lines 119 - 130, The
getModelPresets query currently reads process.env directly; replace that with
the shared runtime config by calling getDefaultAgentRuntimeConfig() and using
its model/provider fields to build presets (e.g., pass the config's
fast/standard/advanced model strings into parseModelList). Import
getDefaultAgentRuntimeConfig at top, call it inside the handler, and use the
returned object's properties instead of process.env.OPENAI_FAST_MODEL,
process.env.OPENAI_MODEL, and process.env.OPENAI_CODING_MODEL so the UI presets
match agent execution.

Comment on lines +14 to +51
export const agentJsonSchema = z.object({
displayName: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
avatarUrl: z.string().url().optional(),
systemInstructions: z.string().min(1),
toolNames: z.array(z.string()).optional(),
integrationBindings: z.array(z.string()).optional(),
delegates: z.array(z.string()).optional(),
workflows: z.array(z.string()).optional(),
modelPreset: modelPresetSchema.optional(),
modelId: z.string().optional(),
knowledgeMode: retrievalModeSchema.optional(),
webSearchMode: retrievalModeSchema.optional(),
includeOrgKnowledge: z.boolean().optional(),
includeTeamKnowledge: z.boolean().optional(),
knowledgeTopK: z.number().int().min(1).max(50).optional(),
structuredResponsesEnabled: z.boolean().optional(),
maxSteps: z.number().int().min(1).max(100).optional(),
timeoutMs: z.number().int().min(1000).optional(),
outputReserve: z.number().int().optional(),
roleRestriction: z.literal('admin_developer').optional(),
conversationStarters: z.array(z.string().max(200)).max(4).optional(),
visibleInChat: z.boolean().optional(),
});
export type AgentJson = z.infer<typeof agentJsonSchema>;

/**
* Schema for creating a new agent (filename validation).
*/
export const createAgentSchema = z.object({
name: z
.string()
.min(1)
.max(100)
.regex(/^[a-z0-9][a-z0-9_-]*$/),
config: agentJsonSchema,
});
export type CreateAgent = z.infer<typeof createAgentSchema>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wire this shared schema into the live agent flows.

Knip is already failing on retrievalModeSchema, agentJsonSchema, createAgentSchema, AgentJson, and CreateAgent, so this PR currently adds a second agent contract without enforcing it anywhere. Until the create/edit/file-read paths actually parse against these schemas, they can drift from the runtime shape.

🧰 Tools
🪛 GitHub Actions: Lint

[error] 14-14: knip: Unused export detected: agentJsonSchema.


[error] 43-43: knip: Unused export detected: createAgentSchema.


[error] 38-38: knip: Unused exported type detected: AgentJson.


[error] 51-51: knip: Unused exported type detected: CreateAgent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform/lib/shared/schemas/agents.ts` around lines 14 - 51, The new
shared schemas (agentJsonSchema, createAgentSchema, AgentJson, CreateAgent, and
retrievalModeSchema) must be used in the runtime create/edit/read flows: update
the agent creation handler, agent update/edit handler, and the agent file
read/parse logic to validate and parse incoming payloads against
createAgentSchema (for create requests) and agentJsonSchema (for config edits
and file reads) instead of the old ad-hoc contract; ensure the handlers call the
Zod parse/parseAsync methods and handle/propagate validation errors, and replace
any existing type annotations with the imported AgentJson/CreateAgent types so
runtime shape and compile-time types remain in sync.

Comment thread services/platform/messages/en.json Outdated
Comment on lines +709 to +717
"history": {
"empty": "No history entries",
"diffTitle": "Compare changes",
"diffDescription": "Review differences before restoring snapshot from {{date}}",
"noDifferences": "No differences found",
"restore": "Restore this version",
"field": "Field",
"current": "Current",
"snapshot": "Snapshot"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the normal ICU placeholder syntax for date.

Line 712 uses {{date}}, but the rest of this locale file uses single-brace placeholders ({date}). As written, the history diff description will not interpolate correctly.

Suggested fix
-        "diffDescription": "Review differences before restoring snapshot from {{date}}",
+        "diffDescription": "Review differences before restoring snapshot from {date}",
📝 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.

Suggested change
"history": {
"empty": "No history entries",
"diffTitle": "Compare changes",
"diffDescription": "Review differences before restoring snapshot from {{date}}",
"noDifferences": "No differences found",
"restore": "Restore this version",
"field": "Field",
"current": "Current",
"snapshot": "Snapshot"
"history": {
"empty": "No history entries",
"diffTitle": "Compare changes",
"diffDescription": "Review differences before restoring snapshot from {date}",
"noDifferences": "No differences found",
"restore": "Restore this version",
"field": "Field",
"current": "Current",
"snapshot": "Snapshot"
🤖 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 709 - 717, The ICU
placeholder in the JSON message key "history.diffDescription" is using double
braces "{{date}}" which won't interpolate with the rest of the locale file;
update the placeholder to the standard single-brace ICU format "{date}" in the
history.diffDescription value so it matches other entries and will interpolate
correctly.

larryro added 3 commits March 29, 2026 00:12
…name

Remove unused jsondiffpatch dependency, dead exports (getAllAcceptForTools,
getAllAllowedMimeTypes, createAgentSchema, AgentJson, CreateAgent), and
stale imports. Replace unsafe type assertions with Zod schema validation
(agentJsonSchema.safeParse) and type guard (isRetrievalMode). Fix
exhaustive-deps, preserve-caught-error, restrict-template-expressions,
and formatting issues across agent components and Convex actions.
…history comparison

The hand-rolled table diff truncated long strings, showed arrays as raw JSON, and
only compared top-level fields. json-diff-kit provides LCS-based array diffing,
inline word-level highlighting for text changes, and collapsible unchanged lines.
The package exports field lacks a "style" condition, so importing via
globals.css failed during vite build. Move the CSS import into the
component file where vite resolves it correctly.
@larryro larryro merged commit c01c1ce into main Mar 28, 2026
17 checks passed
@larryro larryro deleted the refactor/rename-custom-agents-to-agents branch March 28, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant