Skip to content

feat(agent): add multi model provider support - #2962

Merged
Chia1104 merged 2 commits into
developfrom
feat/multi-agent-provider
Aug 8, 2026
Merged

Chia1104 merged 2 commits into
developfrom
feat/multi-agent-provider

Conversation

@Chia1104

@Chia1104 Chia1104 commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a provider-organized model picker for agent sessions.
    • Added support for selecting OpenAI and Anthropic provider/model combinations.
    • Models requiring API keys are identified and unavailable until credentials are configured.
    • Added secure use of user-provided API keys during agent requests.
  • Bug Fixes
    • Improved validation for invalid, incomplete, or unavailable model selections.
    • Added clearer credential error reporting.
    • Preserved updated credentials across ongoing conversations and approval steps.

@vercel

vercel Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chia1104 Ready Ready Preview Aug 4, 2026 9:09am

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds provider-aware agent model selection and request-scoped BYOK credentials. It updates model catalogues, runtime execution, workflow transport, API contracts, guards, and the dashboard session picker.

Changes

Provider-aware model catalogue

Layer / File(s) Summary
Model catalogue and writing-model policy
packages/agent-core/src/models.ts, packages/agent-core/src/index.ts, packages/agent-core/__tests__/models.test.ts, packages/agent-writing/src/models.ts, packages/agent-writing/src/index.ts, packages/agent-writing/__tests__/models.test.ts
Agent models now use provider/model references, credential-scoped registries, catalogue listing, and requiresApiKey metadata. Writing-model validation accepts gateway and native providers.
Resolved models in runtime engines
packages/agent-writing/src/runtime.ts, packages/agent-runtime/src/adapters/pi.ts, packages/agent-writing/__tests__/runtime.test.ts
Writing and maintenance engines receive resolved model registries. Pi harnesses clamp thinking levels to model capabilities. Tests cover non-default provider execution.

Encrypted credential transport

Layer / File(s) Summary
Encrypted credential extraction and decryption
apps/service/src/services/agent-credentials.ts, apps/service/__tests__/agent-credentials.test.ts
Provider cookie values are extracted from request headers into encrypted credentials. Decryption uses the configured private key and produces typed errors for invalid ciphertext.
Workflow credential state and transport
apps/service/src/workflows/hooks/agent.hooks.ts, apps/service/src/workflows/agent-session.workflow.ts
Agent hooks validate encrypted-credential schemas. Workflow state retains credentials across messages and turns, refreshes credentials from approvals, and passes them to every agent turn.
Agent turn with credential-scoped models
apps/service/src/steps/agent-turn.step.ts
Agent turn requests accept optional encrypted credentials. Turns decrypt credentials, create per-turn model registries, and pass them to runtime engines instead of relying on implicit model configuration.

Runtime and API integration

Layer / File(s) Summary
Runtime service credential and provider integration
apps/service/src/services/agent-runtime.service.ts
Session creation and settings updates validate provider/model pairs. Maintenance engines receive per-request models built from caller credentials. Prompts and approvals include decrypted request credentials. Model validation and listing report caller-specific API-key requirements.
API contracts for provider/model references
packages/api/orpc/agent-runtime.ts, packages/api/orpc/contracts/agent.contract.ts
The API now uses nested AgentModelRef containing providerId and modelId instead of standalone model IDs. Session creation, settings updates, and model listing include provider-model metadata and API-key requirements.
oRPC session and model guards
packages/api/orpc/guards/agent-session.guard.ts
Session guard resolves and authorizes sessions, validates optional models, derives runtime from stored kind, and supplies caller/runtime context. Model guard validates optional models and returns errors for invalid selections.
Route integration with guards
packages/api/orpc/routes/agent.route.ts
All session-scoped routes use agentSessionGuard() to resolve authorization and context. Session creation uses agentModelGuard(). Session maintenance, interaction, tool, and model listing routes pass guarded caller/runtime context downstream.

Session UI

Layer / File(s) Summary
Session model selection UI
apps/dash/src/components/agent/agent-model-picker.tsx, apps/dash/src/components/agent/agent-session.tsx
The dashboard model picker loads provider/model options, sorts them by provider, disables models requiring unavailable API keys, and updates session settings by provider/model pair. Selection changes invalidate the session query and invoke onChanged.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentSessionUI
  participant AgentRoute
  participant agentSessionGuard
  participant AgentRuntimeService
  participant AgentSessionWorkflow
  participant AgentTurnStep
  participant WritingAgentRuntime

  AgentSessionUI->>AgentRoute: list models request
  AgentRoute->>agentSessionGuard: resolve session & caller
  agentSessionGuard->>AgentRuntimeService: pass caller context
  AgentRuntimeService->>AgentRuntimeService: read & decrypt credentials
  AgentRuntimeService-->>AgentSessionUI: models with API-key status
  
  AgentSessionUI->>AgentRoute: update provider/model selection
  AgentRoute->>agentSessionGuard: validate model for kind
  agentSessionGuard->>AgentRuntimeService: store settings
  AgentRuntimeService->>AgentSessionWorkflow: initialize or update session
  AgentSessionWorkflow->>AgentTurnStep: pass encrypted credentials
  AgentTurnStep->>AgentTurnStep: decrypt credentials
  AgentTurnStep->>WritingAgentRuntime: run turn with per-turn models
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's main change: support for multiple agent model providers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multi-agent-provider

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

@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: 4

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

Inline comments:
In `@apps/service/src/services/agent-runtime.service.ts`:
- Around line 806-828: Update assertKnownModel to validate the exact
providerId/modelId pair against listWritingModels({ configured }), requiring a
matching entry whose requiresApiKey is false instead of relying on
isWritingModel. Preserve the existing empty-selection behavior, reject
incomplete or unknown selections, and map both validation failures to
opts.errors.BAD_REQUEST so agent routes do not rethrow them as unhandled errors.

In `@apps/service/src/steps/agent-turn.step.ts`:
- Around line 176-183: Update the model initialization in the writing-agent turn
flow around decryptAgentCredentials to import and catch AgentCredentialError,
converting it into new FatalError(error.message); rethrow every other error
unchanged, and leave createAgentModels and writingAgentRuntime.runTurn behavior
intact.

In `@apps/service/src/workflows/agent-session.workflow.ts`:
- Around line 79-88: Update the credential handling in the workflow loop around
pending messages and decisions so an empty credential set is represented as {}
rather than retaining stale ciphertext. Replace the fallback assignments to
credentials with direct assignments from next.credentials and
decision.credentials, while preserving the existing credential type and flow.

In `@packages/agent-writing/src/models.ts`:
- Around line 35-46: Update the persistence flows that call createSession and
updateSettings to validate catalogue membership with
createAgentCatalog().getModel(ref.providerId, ref.modelId) before saving the
provider/model pair. Keep assertKnownModel and isWritingModel checks, but reject
native-provider model IDs missing from the catalogue rather than treating every
modelId as valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0431ec09-33b5-486f-aa92-e40bdce2187d

📥 Commits

Reviewing files that changed from the base of the PR and between 02375e9 and 3798cc3.

📒 Files selected for processing (20)
  • apps/dash/src/components/agent/agent-model-picker.tsx
  • apps/dash/src/components/agent/agent-session.tsx
  • apps/service/__tests__/agent-credentials.test.ts
  • apps/service/src/services/agent-credentials.ts
  • apps/service/src/services/agent-runtime.service.ts
  • apps/service/src/steps/agent-turn.step.ts
  • apps/service/src/workflows/agent-session.workflow.ts
  • apps/service/src/workflows/hooks/agent.hooks.ts
  • packages/agent-core/__tests__/models.test.ts
  • packages/agent-core/src/index.ts
  • packages/agent-core/src/models.ts
  • packages/agent-runtime/src/adapters/pi.ts
  • packages/agent-writing/__tests__/models.test.ts
  • packages/agent-writing/__tests__/runtime.test.ts
  • packages/agent-writing/src/index.ts
  • packages/agent-writing/src/models.ts
  • packages/agent-writing/src/runtime.ts
  • packages/api/orpc/agent-runtime.ts
  • packages/api/orpc/contracts/agent.contract.ts
  • packages/api/orpc/routes/agent.route.ts

Comment on lines 806 to 828
/**
* Validates a client-supplied model selection.
*
* The pair is checked as a unit, and a `modelId` without a `providerId` is refused rather than
* defaulted: the same model exists under several providers with different ids and different payers,
* so there is no safe guess. `isWritingModel` is the same predicate the turn resolves through, so a
* selection accepted here cannot fail policy later.
*/
const assertKnownModel = (input: {
providerId?: string;
modelId?: string;
}): void => {
if (!input.modelId && !input.providerId) return;
if (!input.modelId || !input.providerId) {
throw new Error(
"Both providerId and modelId are required to select a model."
);
}
const ref = { providerId: input.providerId, modelId: input.modelId };
if (!isWritingModel(ref)) {
throw new UnknownAgentModelError(ref);
}
};

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check error mapping for runtime throws and available catalogue lookup helpers.
set -euo pipefail

ast-grep outline packages/agent-writing/src/models.ts --items all
ast-grep outline packages/agent-core/src/models.ts --items all
rg -n -C 4 'UnknownAgentModelError|BAD_REQUEST' --glob '*.ts' packages/api apps/service/src

Repository: Chia1104/chia1104.dev

Length of output: 16600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- writing model implementation ---'
sed -n '1,90p' packages/agent-writing/src/models.ts

printf '%s\n' '--- core model implementation ---'
sed -n '100,235p' packages/agent-core/src/models.ts

printf '%s\n' '--- runtime imports, contracts, and handlers ---'
sed -n '1,45p' apps/service/src/services/agent-runtime.service.ts
rg -n -C 12 'createAgentSessionContract|updateAgentSessionSettingsContract|assertKnownModel|createAgentSession|updateAgentSessionSettings|UnknownAgentModelError' packages/api apps/service/src --glob '*.ts'

Repository: Chia1104/chia1104.dev

Length of output: 30677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- agent route error handling ---'
sed -n '1,135p' packages/api/orpc/routes/agent.route.ts

printf '%s\n' '--- model listing route and contracts ---'
rg -n -C 18 'listAgentModels|listWritingModels|configured|BYOK_PROVIDER_IDS|credentials' packages/api apps/service/src --glob '*.ts'

printf '%s\n' '--- runtime credential flow and turn resolution ---'
rg -n -C 14 'createAgentModels|resolveWritingModel|resolveModel|credentials|apiKey|openai|anthropic' apps/service/src/services/agent-runtime.service.ts --glob '*.ts'

Repository: Chia1104/chia1104.dev

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- capability routes ---'
rg -n -C 25 'export const listAgentModelsRoute|export const listAgentCapabilitiesRoute' packages/api/orpc/routes/agent.route.ts

printf '%s\n' '--- runtime model call sites ---'
rg -n -C 10 'createAgentModels|resolveWritingModel|listWritingModels|BYOK_PROVIDER_IDS|AgentRuntimeCaller' apps/service/src/services/agent-runtime.service.ts packages/api/orpc/agent-runtime.ts

printf '%s\n' '--- focused error translation search ---'
rg -n -C 8 'opts\.errors\.(BAD_REQUEST|INTERNAL_SERVER_ERROR)|catch \(error\)' packages/api/orpc/routes/agent.route.ts apps/service/src/services/agent-runtime.service.ts

Repository: Chia1104/chia1104.dev

Length of output: 29329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- turn setup and model injection ---'
sed -n '470,700p' apps/service/src/services/agent-runtime.service.ts

printf '%s\n' '--- all model resolution references ---'
rg -n -C 8 'modelsFor\(|createWriting|resolveWritingModel|settings\.providerId|settings\.modelId' apps/service packages/agent-* --glob '*.ts'

Repository: Chia1104/chia1104.dev

Length of output: 44146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- oRPC error conversion ---'
rg -n -C 12 'onError|errorHandler|INTERNAL_SERVER_ERROR|UnknownAgentModelError|instanceof AppError|toErrorResponse|RPCHandler' packages/api apps/service --glob '*.ts'

printf '%s\n' '--- model-list tests for configured semantics ---'
sed -n '105,180p' packages/agent-writing/__tests__/models.test.ts
sed -n '1,115p' packages/agent-core/src/models.ts

printf '%s\n' '--- credential helpers ---'
rg -n -C 12 'readEncryptedAgentCredentials|decryptAgentCredentials|BYOK_PROVIDER_IDS' apps/service/src --glob '*.ts'

Repository: Chia1104/chia1104.dev

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- configured model tests ---'
sed -n '105,180p' packages/agent-writing/__tests__/models.test.ts

printf '%s\n' '--- agent route source after sessions ---'
sed -n '130,300p' packages/api/orpc/routes/agent.route.ts

printf '%s\n' '--- RPC mounting and error middleware ---'
rg -n -C 10 'RPCHandler|RPCHandler|onError|hooks|error' apps/service/src/routes apps/service/src --glob '*.ts' | head -240

Repository: Chia1104/chia1104.dev

Length of output: 22854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- oRPC error factory ---'
sed -n '1,240p' apps/service/src/factories/orpc.factory.ts
sed -n '1,180p' apps/service/src/factories/orpc-error.factory.ts

printf '%s\n' '--- provider credential helper definitions ---'
rg -l 'readEncryptedAgentCredentials|decryptAgentCredentials' apps/service/src | xargs -r -n1 sh -c 'echo "--- $0"; rg -n -C 10 "readEncryptedAgentCredentials|decryptAgentCredentials" "$0"'

Repository: Chia1104/chia1104.dev

Length of output: 11949


Validate the model against the caller’s available catalogue. isWritingModel accepts any native openai or anthropic ID, so unknown IDs can be persisted and fail during turn resolution. Use listWritingModels({ configured }) and require an exact pair with requiresApiKey === false. Map both validation errors to opts.errors.BAD_REQUEST; the agent routes currently rethrow them as unhandled errors.

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

In `@apps/service/src/services/agent-runtime.service.ts` around lines 806 - 828,
Update assertKnownModel to validate the exact providerId/modelId pair against
listWritingModels({ configured }), requiring a matching entry whose
requiresApiKey is false instead of relying on isWritingModel. Preserve the
existing empty-selection behavior, reject incomplete or unknown selections, and
map both validation failures to opts.errors.BAD_REQUEST so agent routes do not
rethrow them as unhandled errors.

Comment on lines +176 to +183
const models = createAgentModels(
decryptAgentCredentials(request.credentials)
);

return await writingAgentRuntime.runTurn({
createOptions: {
session,
models,

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect FatalError usage and any retry policy configured for steps.
set -euo pipefail

rg -n -C 3 'FatalError' --glob '*.ts' apps/service/src | head -60
fd -t f 'package.json' -d 3 apps/service --exec sh -c 'jq -r ".dependencies.workflow // empty" "$0"' {}

Repository: Chia1104/chia1104.dev

Length of output: 4494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- agent-turn imports and target block ---'
sed -n '1,35p;125,195p' apps/service/src/steps/agent-turn.step.ts

printf '%s\n' '--- credential error definitions and usages ---'
rg -n -C 5 'AgentCredentialError|decryptAgentCredentials|AI_AUTH_PUBLIC_KEY' --glob '*.ts' .

printf '%s\n' '--- workflow dependency declarations ---'
fd -t f 'package.json' -d 4 . --exec sh -c '
  value=$(jq -r ".dependencies.workflow // .devDependencies.workflow // empty" "$0")
  if [ -n "$value" ]; then printf "%s: %s\n" "$0" "$value"; fi
'

printf '%s\n' '--- FatalError declarations and construction sites ---'
rg -n -C 4 'class FatalError|FatalError\s*=|new FatalError|instanceof FatalError' --glob '*.{ts,tsx,js,mjs,cjs}' .

Repository: Chia1104/chia1104.dev

Length of output: 24642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package metadata and lockfiles ---'
sed -n '1,120p' apps/service/package.json
git ls-files '*lock*' '*package.json' | sed -n '1,120p'

printf '%s\n' '--- workflow references in tracked metadata and docs ---'
rg -n -C 3 'workflow|FatalError|retry' --glob '!**/node_modules/**' --glob '*.{json,md,yml,yaml,ts,tsx}' . | head -240

printf '%s\n' '--- exported Models type and credential imports ---'
rg -n -C 4 'export (type )?Models|type Models|interface Models|AgentCredentialError' --glob '*.ts' packages apps

Repository: Chia1104/chia1104.dev

Length of output: 25858


🌐 Web query:

workflow npm 4.6.2 FatalError retries constructor cause documentation

💡 Result:

In the context of the workflow SDK (such as workflow-sdk.dev), a FatalError is used to mark a step as permanently failed, preventing any automatic retries [1][2][3]. Regarding your specific query about version 4.6.2 and the FatalError constructor: 1. FatalError Purpose: When thrown within a step (marked with "use step"), it signals that the workflow should not retry that step, causing the workflow run to fail or trigger compensation logic [1][2][3]. 2. Constructor and Cause: Documentation for the FatalError class in the workflow SDK typically shows a constructor that accepts a message string (e.g., constructor(message: string)) [4]. Unlike some other error classes in the ecosystem—such as WorkflowRunFailedError or general WorkflowError which explicitly support a cause property to wrap underlying errors [4][5]—the standard FatalError implementation in this SDK is primarily designed to stop execution. 3. Note on "cause": If you are looking to preserve the underlying error when throwing a FatalError, standard JavaScript Error patterns (which the SDK follows) allow you to set the cause via the options object in the Error constructor, though you should verify your specific version's type definitions if you need to access it programmatically [6][4]. Some specialized error classes in similar workflows explicitly include a cause property [4][5]. If you are using a specific library or wrapper, please note that different workflow implementations (such as Cloudflare Workflows) may use different error names like NonRetryableError and have their own specific quirks regarding error message handling and retries [7][8]. Always verify your project's specific dependency version, as implementations can vary between SDKs.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

curl -fsSL https://registry.npmjs.org/workflow/4.6.2 > "$tmpdir/workflow.json"
jq -r '.dist.tarball' "$tmpdir/workflow.json"

tarball=$(jq -r '.dist.tarball' "$tmpdir/workflow.json")
curl -fsSL "$tarball" -o "$tmpdir/workflow.tgz"
tar -xzf "$tmpdir/workflow.tgz" -C "$tmpdir"

printf '%s\n' '--- FatalError declarations ---'
rg -n -C 8 'FatalError' "$tmpdir/package" --glob '*.{d.ts,js,mjs,cjs}' | head -160

printf '%s\n' '--- package exports and type entrypoints ---'
jq '{name,version,types,exports}' "$tmpdir/package/package.json"

printf '%s\n' '--- lockfile workflow resolution ---'
rg -n -C 3 'workflow@4\.6\.2|workflow:' pnpm-lock.yaml | head -80

Repository: Chia1104/chia1104.dev

Length of output: 243


🌐 Web query:

site:unpkg.com/workflow@4.6.2 FatalError constructor

💡 Result:

There is no native class named FatalError in the standard PHP library. In PHP, fatal errors (such as E_ERROR, E_PARSE, etc.) generally cannot be caught by standard try/catch blocks because they typically terminate script execution immediately [1][2][3]. If you are encountering a class named FatalError, it is likely part of a third-party library or framework, such as: 1. Symfony: Symfony provides a \Symfony\Component\ErrorHandler\Error\FatalError class, which is used internally to wrap fatal PHP errors for error handling purposes [4]. It is not a standard PHP exception or error class you would typically instantiate manually in application code [4]. 2. MediaWiki: MediaWiki defines its own \FatalError class, which inherits from MWException and is used to abort web requests with specific HTML error messages [5][6]. For standard PHP error handling, developers typically use: - Throwable/Error/Exception: These are the base classes for catchable errors and exceptions [7][8][9]. - ErrorException: This class can be used in custom error handlers (set via set_error_handler) to convert non-fatal PHP errors into catchable exceptions [10][11]. Because E_ERROR types cannot be caught by user-defined error handlers, common workarounds for logging or handling them include using register_shutdown_function in combination with error_get_last to detect that a fatal error has occurred during script shutdown [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- public workflow package metadata ---'
curl -fsSL https://unpkg.com/workflow@4.6.2/package.json | jq '{name,version,types,exports,dependencies}' || true

printf '%s\n' '--- public workflow declarations ---'
for path in \
  dist/index.d.ts \
  dist/errors.d.ts \
  dist/index.js \
  dist/errors.js \
  dist/workflow.d.ts
do
  printf '%s\n' "--- $path ---"
  curl -fsSL "https://unpkg.com/workflow@4.6.2/$path" 2>/dev/null \
    | rg -n -C 8 'FatalError' | head -80 || true
done

printf '%s\n' '--- lockfile entries ---'
rg -n -C 5 '(^|\s)workflow(@|:)|workflow-4\.6\.2' pnpm-lock.yaml | head -120 || true

printf '%s\n' '--- local type usage relevant to proposed patch ---'
rg -n -C 3 'Models|AgentCredentialError' apps/service/src packages --glob '*.ts' | head -160

Repository: Chia1104/chia1104.dev

Length of output: 15763


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- workflow errors package metadata ---'
curl -fsSL https://unpkg.com/@workflow/errors@4.1.5/package.json \
  | jq '{name,version,types,exports}' || true

printf '%s\n' '--- workflow errors declarations ---'
for path in dist/index.d.ts dist/errors.d.ts dist/index.js dist/errors.js; do
  printf '%s\n' "--- $path ---"
  curl -fsSL "https://unpkg.com/@workflow/errors@4.1.5/$path" 2>/dev/null \
    | rg -n -C 10 'FatalError|class .*Error' | head -120 || true
done

printf '%s\n' '--- workflow package references to FatalError ---'
for path in dist/index.d.ts dist/internal/errors.js dist/workflow.js dist/index.js; do
  printf '%s\n' "--- $path ---"
  curl -fsSL "https://unpkg.com/workflow@4.6.2/$path" 2>/dev/null \
    | rg -n -C 8 'FatalError' | head -100 || true
done

Repository: Chia1104/chia1104.dev

Length of output: 10014


Classify AgentCredentialError as fatal.

Import AgentCredentialError, catch it around decryptAgentCredentials, and throw new FatalError(error.message). Rethrow all other errors. Undecryptable credentials cannot succeed on retry without re-registration, so ordinary retry handling only delays the operator message.

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

In `@apps/service/src/steps/agent-turn.step.ts` around lines 176 - 183, Update the
model initialization in the writing-agent turn flow around
decryptAgentCredentials to import and catch AgentCredentialError, converting it
into new FatalError(error.message); rethrow every other error unchanged, and
leave createAgentModels and writingAgentRuntime.runTurn behavior intact.

Comment on lines +79 to +88
let credentials: EncryptedAgentCredentials | undefined =
firstMessage.credentials;

while (turns < MAX_TURNS_PER_RUN) {
if (pending === null) {
// Durable pause: no compute is consumed while waiting for the operator.
const next = await messages;
if (next.text === AGENT_END_SENTINEL) break;
pending = next;
credentials = next.credentials ?? credentials;

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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every producer of hook credential payloads and every reader of the retained value.
set -euo pipefail

rg -n -C 4 'readEncryptedAgentCredentials|agentMessageHook\.resume|agentApprovalHook\.resume|credentials' \
  --glob '*.ts' apps/service/src packages

Repository: Chia1104/chia1104.dev

Length of output: 39726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- credential reader ---'
sed -n '30,55p' apps/service/src/services/agent-credentials.ts

printf '%s\n' '--- hook schemas ---'
sed -n '1,75p' apps/service/src/workflows/hooks/agent.hooks.ts

printf '%s\n' '--- approval producer ---'
sed -n '650,695p' apps/service/src/services/agent-runtime.service.ts

printf '%s\n' '--- all hook resumes ---'
rg -n -C 3 'agent(Message|Approval)Hook\.resume' apps/service/src packages --glob '*.ts'

printf '%s\n' '--- credential payload shape verifier ---'
python3 - <<'PY'
from pathlib import Path
text = Path("apps/service/src/services/agent-credentials.ts").read_text()
start = text.index("export const readEncryptedAgentCredentials")
end = text.index("\n};", start) + 3
print(text[start:end])
PY

Repository: Chia1104/chia1104.dev

Length of output: 8709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- credential reader ---'
sed -n '30,55p' apps/service/src/services/agent-credentials.ts

printf '%s\n' '--- hook schemas ---'
sed -n '1,75p' apps/service/src/workflows/hooks/agent.hooks.ts

printf '%s\n' '--- approval producer ---'
sed -n '650,695p' apps/service/src/services/agent-runtime.service.ts

printf '%s\n' '--- all hook resumes ---'
rg -n -C 3 'agent(Message|Approval)Hook\.resume' apps/service/src packages --glob '*.ts'

Repository: Chia1104/chia1104.dev

Length of output: 8148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

credentials = Path("apps/service/src/services/agent-credentials.ts").read_text()
workflow = Path("apps/service/src/workflows/agent-session.workflow.ts").read_text()
runtime = Path("apps/service/src/services/agent-runtime.service.ts").read_text()

assert "return Object.keys(credentials).length > 0 ? credentials : undefined;" in credentials
assert "credentials = next.credentials ?? credentials;" in workflow
assert "credentials = decision.credentials ?? credentials;" in workflow

# Model the exact JavaScript nullish-coalescing behavior used by both assignments.
old = {"openai": "previous-ciphertext"}
assert (None if False else None) is None
assert (None if None is not None else old) is old       # undefined/null-like payload retains old value
empty = {}
assert (empty if empty is not None else old) is empty   # explicit empty object clears old value

prompt_start = runtime.index("async prompt(caller, input)")
prompt_end = runtime.index("\n  },", prompt_start)
approve_start = runtime.index("async approve(caller, input)")
approve_end = runtime.index("\n  },", approve_start)
prompt = runtime[prompt_start:prompt_end]
approve = runtime[approve_start:approve_end]

assert prompt.count("readEncryptedAgentCredentials(caller.context.headers)") == 1
assert approve.count("readEncryptedAgentCredentials(caller.context.headers)") == 1
assert "text: AGENT_END_SENTINEL" in runtime
print("PASS: empty credentials would clear retained ciphertext, but the current reader returns undefined and both workflow assignments retain it.")
print("PASS: prompt and approve are the only credential-bearing hook producers in the inspected runtime paths.")
PY

printf '%s\n' '--- exact workflow assignments ---'
rg -n 'credentials = (next|decision)\.credentials' apps/service/src/workflows/agent-session.workflow.ts

Repository: Chia1104/chia1104.dev

Length of output: 554


Stale Credentials After Revocation (CWE-613): Insufficient Session Expiration

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  packages/agent-writing/src/models.ts:68
  listModels
│
▼
● Hop
  apps/service/src/services/agent-runtime.service.ts:465
  prompt: * Refuse while an approval is outstanding. * * The run is parked on the *approval* hook, not the message hook. A resumed message would be…
│
▼
● Sink
  apps/service/src/workflows/agent-session.workflow.ts

Clear credentials when all BYOK cookies are absent.

readEncryptedAgentCredentials returns undefined when no keys remain. The ?? assignments at lines 88 and 121 retain the previous ciphertext. Return {} for an empty credential set and assign next.credentials and decision.credentials directly.

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

In `@apps/service/src/workflows/agent-session.workflow.ts` around lines 79 - 88,
Update the credential handling in the workflow loop around pending messages and
decisions so an empty credential set is represented as {} rather than retaining
stale ciphertext. Replace the fallback assignments to credentials with direct
assignments from next.credentials and decision.credentials, while preserving the
existing credential type and flow.

Comment on lines +35 to +46
export const isWritingModel = (ref: AgentModelRef): boolean => {
switch (ref.providerId) {
case AGENT_PROVIDERS.gateway:
return GATEWAY_VENDOR_PREFIXES.some((prefix) =>
ref.modelId.startsWith(prefix)
);
case AGENT_PROVIDERS.openai:
case AGENT_PROVIDERS.anthropic:
return true;
default:
return false;
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the service symbols involved in settings validation.
ast-grep outline apps/service/src/services/agent-runtime.service.ts --items all --type function

# Inspect whether settings mutation paths validate catalogue membership, not only policy.
rg -n -C 12 --type ts \
  '\bassertKnownModel\s*\(|\bisWritingModel\s*\(|\bcreateAgentCatalog\s*\(|\bresolveWritingModel\s*\(' \
  apps/service/src/services/agent-runtime.service.ts

# Inspect callers that receive or persist provider/model settings.
rg -n -C 12 --type ts \
  '\bproviderId\b|\bmodelId\b|\bsettings\b|\bwriteSessionSettings\b' \
  apps/service/src/services/agent-runtime.service.ts

Repository: Chia1104/chia1104.dev

Length of output: 11781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- model catalog symbols ---'
rg -n -C 10 --type ts \
  '\b(createAgentCatalog|createAgentModels|getModel|resolveWritingModel|isWritingModel|listWritingModels)\b' .

printf '%s\n' '--- settings write and validation symbols ---'
rg -n -C 12 --type ts \
  '\b(writeSessionSettings|updateSettings|createSession|assertKnownModel)\b' .

printf '%s\n' '--- candidate file map ---'
fd -t f -e ts . | rg 'agent|model|catalog|session' | head -80

Repository: Chia1104/chia1104.dev

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- agent-core model files ---'
fd -t f . packages/agent-core | sort | rg 'model|provider|index'

printf '%s\n' '--- core model implementation symbols ---'
rg -n -C 8 --type ts \
  '\b(createAgentModels|resolveModel|listModels|UnknownAgentModelError|AgentModelRef)\b' \
  packages/agent-core

printf '%s\n' '--- exact service mutation and runtime paths ---'
sed -n '360,390p' apps/service/src/services/agent-runtime.service.ts
sed -n '436,456p' apps/service/src/services/agent-runtime.service.ts
sed -n '806,829p' apps/service/src/services/agent-runtime.service.ts

Repository: Chia1104/chia1104.dev

Length of output: 27186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact catalog and resolver implementation ---'
sed -n '107,177p' packages/agent-core/src/models.ts

printf '%s\n' '--- exact writing policy implementation ---'
sed -n '30,68p' packages/agent-writing/src/models.ts

printf '%s\n' '--- focused static invariant check ---'
python3 - <<'PY'
from pathlib import Path

service = Path("apps/service/src/services/agent-runtime.service.ts").read_text()
writing = Path("packages/agent-writing/src/models.ts").read_text()
core = Path("packages/agent-core/src/models.ts").read_text()

assert "if (!isWritingModel(ref))" in service
assert "createAgentCatalog" not in service
assert "models.getModel(ref.providerId, ref.modelId)" in core
assert "case AGENT_PROVIDERS.openai:" in writing
assert "case AGENT_PROVIDERS.anthropic:" in writing
assert "return true;" in writing

print("settings validation uses policy only: true")
print("catalogue lookup exists in resolveModel: true")
print("native-provider policy admits arbitrary modelId text: true")
print("catalogue membership is not checked before settings persistence: true")
PY

Repository: Chia1104/chia1104.dev

Length of output: 4614


Validate catalogue membership before persisting model settings.

assertKnownModel checks only isWritingModel. Native providers accept any modelId, including IDs absent from the catalogue. Check createAgentCatalog().getModel(ref.providerId, ref.modelId) before createSession and updateSettings persist the pair.

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

In `@packages/agent-writing/src/models.ts` around lines 35 - 46, Update the
persistence flows that call createSession and updateSettings to validate
catalogue membership with createAgentCatalog().getModel(ref.providerId,
ref.modelId) before saving the provider/model pair. Keep assertKnownModel and
isWritingModel checks, but reject native-provider model IDs missing from the
catalogue rather than treating every modelId as valid.

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

@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: 2

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

Inline comments:
In `@packages/api/orpc/agent-runtime.ts`:
- Around line 167-175: Update validateModel and its callers to make model
validation credential-aware before persistence: pass AgentRuntimeCaller into
validateModel (or perform the equivalent check) and ensure both createSession
and updateSettings reject BYOK models when the caller has no registered provider
key, before either write occurs.

In `@packages/api/orpc/contracts/agent.contract.ts`:
- Line 153: Update the object schemas in createAgentSessionContract and
updateAgentSessionSettingsContract to use z.strictObject({ instead of
z.object({, so removed fields such as modelId or providerId are rejected with
validation errors rather than stripped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 947115f2-0317-44b7-8d8d-97b9d2a0eb8d

📥 Commits

Reviewing files that changed from the base of the PR and between 3798cc3 and 74d475d.

📒 Files selected for processing (10)
  • apps/dash/src/components/agent/agent-model-picker.tsx
  • apps/service/src/services/agent-runtime.service.ts
  • apps/service/src/workflows/agent-session.workflow.ts
  • packages/agent-writing/__tests__/models.test.ts
  • packages/agent-writing/src/index.ts
  • packages/agent-writing/src/models.ts
  • packages/api/orpc/agent-runtime.ts
  • packages/api/orpc/contracts/agent.contract.ts
  • packages/api/orpc/guards/agent-session.guard.ts
  • packages/api/orpc/routes/agent.route.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/dash/src/components/agent/agent-model-picker.tsx
  • apps/service/src/workflows/agent-session.workflow.ts
  • packages/agent-writing/src/index.ts
  • apps/service/src/services/agent-runtime.service.ts

Comment on lines +167 to +175
/**
* Checks a model selection before anything persists it.
*
* Returns a human-readable reason when the pair is unusable, or `null` when it is fine. A return
* value rather than a thrown error, because the caller is a middleware that has to turn this into
* a `BAD_REQUEST` — and because which models a kind admits is the kind's own policy, so the
* *reason* has to come from here rather than be reconstructed at the transport.
*/
validateModel(ref: AgentModelRef): Promise<string | 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline apps/service/src/services/agent-runtime.service.ts --items all

# Expect both write paths to reject a selected model when the caller has no
# usable credential, or to call a credential-aware validation helper.
rg -n -C 12 \
  -e '\b(validateModel|listModels|createSession|updateSettings)\s*\(' \
  -e '\brequiresApiKey\b' \
  -e '\b(credentials?|provider.*key|api.*key)\b' \
  apps/service/src/services/agent-runtime.service.ts

Repository: Chia1104/chia1104.dev

Length of output: 8179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- service write paths ---'
sed -n '330,455p' apps/service/src/services/agent-runtime.service.ts

printf '%s\n' '--- validation and middleware call sites ---'
rg -n -C 10 \
  -e '\bvalidateModel\b' \
  -e '\bassertWritingModel\b' \
  -e '\bcreateSession\b' \
  -e '\bupdateSettings\b' \
  -e '\bwriteSessionSettings\b' \
  apps packages

printf '%s\n' '--- model catalogue policy ---'
rg -n -C 12 \
  -e '\blistWritingModels\b' \
  -e '\bBYOK_PROVIDER_IDS\b' \
  -e '\brequiresApiKey\b' \
  packages apps

Repository: Chia1104/chia1104.dev

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

service = Path("apps/service/src/services/agent-runtime.service.ts").read_text()
guard = Path("packages/api/orpc/guards/agent-session.guard.ts").read_text()
runtime = Path("packages/api/orpc/agent-runtime.ts").read_text()
models = Path("packages/agent-core/src/models.ts").read_text()

def body(name, start, end):
    match = re.search(rf"{re.escape(start)}(.*?){re.escape(end)}", service, re.S)
    assert match, f"missing {name}"
    return match.group(1)

create = body("createSession", "async createSession(caller, input)", "  getSession(caller, input)")
update = body("updateSettings", "async updateSettings(caller, input)", "  /**\n   * Enqueues a turn.")

assert "repo.create({" in create
assert "validateModel(" not in create
assert "validateModel(" not in update
assert re.search(r"runtime\.validateModel\(\s*input\.model\s*\)", guard)
assert re.search(r"getAgentRuntime\(input\.kind\)\.validateModel\(\s*input\.model\s*\)", guard)
assert re.search(r"validateModel\(ref:\s*AgentModelRef\)", runtime)
assert "assertWritingModel(ref)" in service
assert "isByokProviderId(provider.id) && !configured.has(provider.id)" in models

print("createSession persists input.model without a service-level validateModel call")
print("updateSettings persists input.model without a service-level validateModel call")
print("both transport guards call validateModel with only input.model")
print("assertWritingModel validates the model reference, not caller credential configuration")
print("requiresApiKey is true for BYOK providers absent from configured caller keys")
PY

Repository: Chia1104/chia1104.dev

Length of output: 542


Make model validation credential-aware before persistence. createSession and updateSettings can persist a BYOK model when the caller has no registered provider key. Pass AgentRuntimeCaller to validateModel, or add an equivalent check before both writes.

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

In `@packages/api/orpc/agent-runtime.ts` around lines 167 - 175, Update
validateModel and its callers to make model validation credential-aware before
persistence: pass AgentRuntimeCaller into validateModel (or perform the
equivalent check) and ensure both createSession and updateSettings reject BYOK
models when the caller has no registered provider key, before either write
occurs.

/** Seeds the draft buffer from this post so the agent edits rather than starts fresh. */
targetFeedId: z.number().int().optional(),
modelId: z.string().optional(),
model: agentModelRefSchema.optional(),

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/api/orpc/contracts/agent.contract.ts --items all

# Inspect remaining callers that serialize legacy standalone model fields.
rg -nP -C 6 -g '*.ts' -g '*.tsx' \
  '\b(?:modelId|providerId)\s*:' \
  apps/dash packages/api

Repository: Chia1104/chia1104.dev

Length of output: 10902


🌐 Web query:

For Zod 4, does z.object({...})strip unknown keys by default, and doesz.strictObject({...}) reject them? Use official Zod documentation.

💡 Result:

In Zod 4, the default behaviors for object schemas are as follows: - z.object({...}): By default, this schema strips unknown keys from the input object [1][2]. This means that any properties present in the input but not defined in the schema are removed from the parsed output [1]. - z.strictObject({...}): This schema is designed to reject unknown keys [3]. If the input contains keys not defined in the schema, the parser will throw an error [3]. These behaviors are consistent with the established functionality of Zod to ensure type safety and schema enforcement [1][4]. The standard z.object remains the default, non-strict implementation that automatically sanitizes inputs by stripping unspecified fields [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the contract file to inspect the actual definitions
sed -n '144,160p' packages/api/orpc/contracts/agent.contract.ts
echo "---"
sed -n '183,210p' packages/api/orpc/contracts/agent.contract.ts

Repository: Chia1104/chia1104.dev

Length of output: 2028


Use z.strictObject() to reject unknown fields on these mutation inputs.

With Zod 4 default object parsing, z.object() strips unknown input keys. A request that sends the removed modelId or providerId fields parses with model === undefined. Both handlers then receive an omitted model selection and cannot distinguish a stale client from an intentional omission. This can create or update a session without applying the caller's selected model.

Replace z.object({ with z.strictObject({ in createAgentSessionContract (line 144) and updateAgentSessionSettingsContract (line 183) to reject unknown fields and trigger a schema validation error instead of silently normalizing the request.

Proposed fix
 export const createAgentSessionContract = oc
   .errors({ UNAUTHORIZED: {}, FORBIDDEN: {}, BAD_REQUEST: {} })
   .input(
-    z.object({
+    z.strictObject({
       kind: z.string().min(1),
       title: z.string().max(200).optional(),
       targetFeedId: z.number().int().optional(),
       model: agentModelRefSchema.optional(),
       thinkingLevel: thinkingLevelSchema.optional(),
       autoApprove: z.array(toolTierSchema).optional(),
       runtimeConfig: z.record(z.string(), z.unknown()).optional(),
     })
   )

 export const updateAgentSessionSettingsContract = oc
   .errors({ UNAUTHORIZED: {}, FORBIDDEN: {}, NOT_FOUND: {}, BAD_REQUEST: {} })
   .input(
-    z.object({
+    z.strictObject({
       kind: z.string().optional(),
       sessionId: z.string(),
       title: z.string().max(200).optional(),
       model: agentModelRefSchema.optional(),
       thinkingLevel: thinkingLevelSchema.optional(),
       activeToolNames: z.array(z.string()).nullable().optional(),
       autoApprove: z.array(toolTierSchema).optional(),
       runtimeConfig: z.record(z.string(), z.unknown()).optional(),
     })
   )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/orpc/contracts/agent.contract.ts` at line 153, Update the object
schemas in createAgentSessionContract and updateAgentSessionSettingsContract to
use z.strictObject({ instead of z.object({, so removed fields such as modelId or
providerId are rejected with validation errors rather than stripped.

@Chia1104
Chia1104 merged commit 1628ba8 into develop Aug 8, 2026
21 checks passed
@Chia1104
Chia1104 deleted the feat/multi-agent-provider branch August 8, 2026 18:37

This branch was successfully deployed

1 active deployment
Preview — 74d475d1 Deployed Aug 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant