Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fa0d33f
chore(deps): move workspace to pnpm 11 / TypeScript 7 / Electron 43 s…
ackness Jul 18, 2026
5b30ddb
chore: track mise.toml with pnpm pinned to packageManager version
ackness Jul 18, 2026
78220b8
fix: Batch 0 remediation — dialogue-mode tracking, prompt hygiene, de…
ackness Jul 20, 2026
5c23da0
fix: Batch 1 remediation — trust boundary hardening (C-01/H-01/H-02/H…
ackness Jul 20, 2026
f1df70b
fix: Batch 2 remediation (part 1) — commit-domain integrity (H-07a/H-…
ackness Jul 20, 2026
1cfe15a
fix: Batch 2 remediation (part 2) — turn accounting + SSE isolation (…
ackness Jul 20, 2026
9223d69
refactor: drop audit IDs from code comments; single-pass prompt inter…
ackness Jul 20, 2026
3c7d967
fix(store): serialize writes against open transactions on single-conn…
ackness Jul 20, 2026
351145a
fix: close plugin-data trust gaps, restore hard budget for tool-using…
ackness Jul 20, 2026
d4aa51f
fix: restore event-runtime throttling, correct completion contract, d…
ackness Jul 20, 2026
960373e
fix: world-data write atomicity, real turn clock for the NPC graph, s…
ackness Jul 20, 2026
8994125
fix: freeze execution identity and quarantine failed plugins
ackness Jul 21, 2026
5886c19
fix(server): make commit outcome authoritative and gate community RPC…
ackness Jul 21, 2026
8e9d8e4
fix(store): bind the serialized write gate to the connection, not the…
ackness Jul 21, 2026
25d62c4
fix(server): correct commit ordering, turn accounting, and SSE scoping
ackness Jul 21, 2026
92e2d99
fix: centralize reserved-namespace guard and surface prompt-budget pr…
ackness Jul 21, 2026
b1961e7
fix: scope event fan-out, demote compacted summaries, verify localize…
ackness Jul 21, 2026
f72d8eb
fix(npc-graph): version relationship edges on a real turn clock
ackness Jul 21, 2026
86fb88f
docs: sync references and stale comments with the remediation changes
ackness Jul 21, 2026
25e9368
fix: address review findings on the remediation batch
ackness Jul 21, 2026
65806d7
fix: second-round review follow-ups
ackness Jul 21, 2026
63baae1
fix: scoped retry double-count and world-data media delete rollback
ackness Jul 21, 2026
78682d9
fix(npc-graph): stop lastSeenTurn regressing to -1 without a turn clock
ackness Jul 21, 2026
a280ace
chore: remove dead code, sync stale comments, drop leftover audit IDs
ackness Jul 21, 2026
4989536
test: lock single-pass interpolation and store write-gate coverage
ackness Jul 21, 2026
95dfcfe
chore(deps): drop expired override, pin node, align template vitest
ackness Jul 21, 2026
89809cf
chore: drop audit/spec/PR tracer IDs from comments and test names
ackness Jul 21, 2026
342fe93
perf(web): warm stage media cache during pre-game
ackness Jul 21, 2026
f7a3d3d
chore: bump Node runtime from 22 to 26
ackness Jul 21, 2026
a922272
feat(web): enter the stage once the opening form is submitted
ackness Jul 21, 2026
2e0843e
chore(release): v0.0.17
ackness Jul 21, 2026
0406b5c
fix(ci): disable pnpm run-time deps auto-install
ackness Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
node-version: 26
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check peer dependency compatibility
run: pnpm peers check

- name: Lint
run: pnpm lint

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
node-version: 26
cache: pnpm

- name: Install dependencies
Expand Down Expand Up @@ -115,7 +115,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
node-version: 26
cache: pnpm

# Electron binaries + electron-builder toolchain (bundled NSIS).
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Provider API keys flow through the `SettingsStore` too: writes end up in `keys.e

## Monorepo Structure

- pnpm workspaces + Turborepo. `pnpm@10.33.2`, Node ≥ 22.9 (dev scripts use `--env-file-if-exists`).
- pnpm workspaces + Turborepo. `pnpm@11.9.0`, Node ≥ 26 (dev scripts use `--env-file-if-exists`).
- ESM-only (`"type": "module"`), TypeScript strict, ES2022, NodeNext module resolution — **use `.js` extensions in TS imports**.
- Packages export TS source directly (`"import": "./src/index.ts"`) — no build step for dev.

Expand Down Expand Up @@ -163,7 +163,7 @@ Input/Event → Trigger Router → Priority Scheduler → [per priority group:]
Session lifecycle tracked by three fields on `SessionRecord`:

- `status: 'active' | 'paused' | 'ended'` — `paused`/`ended` halts scheduling.
- `turnCount: number` — band selector. Kernel auto-advances 0 → 1 once all Pre-Game runtimes report done.
- `turnCount: number` — count of completed **player** turns. Kernel auto-advances 0 → 1 once all Pre-Game runtimes report done. Drives the UI turn display, auto-snapshot cadence, and snapshot numbering. It is NOT the band selector — `preGameCompleted` is (band = Pre-Game while any Pre-Game runtime is unreported, main-loop after). Non-player executions (manual plugin-rpc trigger, deferred background follower, nested `recursiveCall`) each persist their own `turn_results` row stamped with `origin` and are excluded from the count; several executions sharing one `turnId` count once.
- `preGameCompleted: string[]` — runtimeIds that reported done.

### Plugin system
Expand Down Expand Up @@ -207,7 +207,7 @@ Correct approach:
- Test files may use real plugin IDs as fixtures; production code must not.
- **UI curation/preset data may list concrete plugin IDs as _data_** (e.g. the front-end plugin packs in `apps/web/src/lib/session-plugin-selection.ts`, which a player picks from). The rule bans hardcoded IDs in **dispatch/control flow** — `if`/`switch` on a plugin ID to change behavior — not curated, user-overridable selection lists. The runtime still discovers and dispatches by `outputKind`/`capabilities`.

**Character creation convention**: forms marked with `_createCharacter: true` cause the framework to auto-create a `CharacterRecord`.
**Character creation convention**: the framework never auto-creates `CharacterRecord`s from forms (the old `_createCharacter: true` marker is gone). Player creation is plugin-owned: the character plugin's pre-game guard reads the submitted opening-form values and synthesises the player deterministically (no LLM) via the character proposal/tool surface.

### Identity model: pluginId vs runtimeId

Expand Down Expand Up @@ -278,6 +278,7 @@ Each SQL backend keeps a thin public factory plus focused method modules:
27 tables via Drizzle; authoritative list in `packages/store/src/{sqlite,postgres}/schema.ts`, transactions contract in [docs/reference/transactions.md](./docs/reference/transactions.md).

- **`sessions.runtime_model_overrides`** — JSONB map of `runtimeId → slot name`, snapshotted into `TurnInput` each turn and consulted by `runtime-slot-resolver` before `manifest.model` / gateway default. Keys still flow via `X-Provider-Keys` + localStorage.
- **`turn_results.commit_status`** — `pending` when the execution artifact is persisted (before proposals commit), settled to `committed` / `failed` by the commit-owning caller. A row still `pending` is a crash signature, not a successful turn.
- **JSONB writes**: use `sql.json(value as JSONValue)` — **never** `JSON.stringify()` (double-serialisation bug).

## Server Bootstrap
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

**English** · [简体中文](./README.zh-CN.md)

[![Version](https://img.shields.io/badge/version-v0.0.16-8b5cf6)](https://github.com/ackness/covel/releases/tag/v0.0.16)
[![Version](https://img.shields.io/badge/version-v0.0.17-8b5cf6)](https://github.com/ackness/covel/releases/tag/v0.0.17)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Stage](https://img.shields.io/badge/stage-early--access-orange)]()
[![Stage](https://img.shields.io/badge/stage-early--access-orange)](<>)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ackness/covel)

![Covel demo — one full session at 6× speed](./.assets/images/demo.gif)

Covel is an AI RPG where the world keeps running between your turns: NPCs track how they feel about you, lore accumulates as you play, and memory carries the thread across the session. Every mechanic behind that is an **autonomous agent shipped as a plugin** — disable one, swap one, or write your own.

> **Current public release: v0.0.16**, early access — APIs, data formats, and plugin frontmatter may change between versions. Prebuilt binaries target macOS Apple Silicon and Windows x64; other platforms build from source.
> **Current public release: v0.0.17**, early access — APIs, data formats, and plugin frontmatter may change between versions. Prebuilt binaries target macOS Apple Silicon and Windows x64; other platforms build from source.

## Highlights

Expand Down
6 changes: 3 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

[English](./README.md) · **简体中文**

[![Version](https://img.shields.io/badge/version-v0.0.16-8b5cf6)](https://github.com/ackness/covel/releases/tag/v0.0.16)
[![Version](https://img.shields.io/badge/version-v0.0.17-8b5cf6)](https://github.com/ackness/covel/releases/tag/v0.0.17)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Stage](https://img.shields.io/badge/stage-early--access-orange)]()
[![Stage](https://img.shields.io/badge/stage-early--access-orange)](<>)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ackness/covel)

![Covel 演示 —— 一局完整游戏,6 倍速](./.assets/images/demo.gif)

Covel 是一款 AI 驱动的 RPG,回合之间世界仍在运转:NPC 记录着对你的态度、世界典籍随游玩积累、记忆贯穿整局。支撑这一切的每个机制都是一个**以插件形式分发的自主 agent** —— 禁用一个、替换一个,或者自己写一个。

> **当前公开版本:v0.0.16**,早期阶段 —— API、数据格式、插件 frontmatter 可能随版本变化。官方预编译包面向 macOS Apple Silicon 与 Windows x64,其余平台从源码构建。
> **当前公开版本:v0.0.17**,早期阶段 —— API、数据格式、插件 frontmatter 可能随版本变化。官方预编译包面向 macOS Apple Silicon 与 Windows x64,其余平台从源码构建。

## 亮点

Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@covel/desktop",
"version": "0.0.16",
"version": "0.0.17",
"private": true,
"type": "module",
"main": "dist/main.mjs",
Expand All @@ -21,15 +21,15 @@
"clean": "node ../../scripts/remove-paths.mjs dist staging ../../release/electron"
},
"dependencies": {
"smol-toml": "^1.6.1",
"tsx": "^4.22.4"
"smol-toml": "^1.7.0",
"tsx": "4.22.4"
},
"devDependencies": {
"@covel/web": "workspace:*",
"@electron/rebuild": "^4.0.4",
"electron": "^42.4.1",
"electron-builder": "^26.15.3",
"@electron/rebuild": "^4.2.0",
"electron": "43.1.0",
"electron-builder": "26.15.6",
"esbuild": "^0.28.1",
"typescript": "^6.0.3"
"typescript": "^7.0.2"
}
}
2 changes: 1 addition & 1 deletion apps/desktop/scripts/ensure-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* lifecycle scripts at all (the download moved to an explicit
* `install-electron` bin), so `pnpm install` never materialises the runtime
* binary under node_modules/electron/dist and whitelisting electron in
* `onlyBuiltDependencies` has nothing to run. Anything that needs the real
* `allowBuilds` has nothing to run. Anything that needs the real
* binary (dev shell, `--electron-node` staging smoke) calls this first.
* install.js skips the download when dist/ is already present, so repeat
* calls are cheap.
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { buildAppMenu, getMainWindow, isTrustedFrameUrl } from "./windows.js";
import { setDesktopLocaleFromSettings, t } from "./main-i18n.js";

/**
* H-09 defense-in-depth: reject secret/config IPC unless the sender frame is
* Defense-in-depth: reject secret/config IPC unless the sender frame is
* the trusted app origin (the local sidecar / dev server). The main-frame
* navigation guard (windows.ts) is the primary block; this stops any other
* frame (a stray iframe, a not-yet-committed cross-origin page) from reading
Expand Down Expand Up @@ -134,7 +134,7 @@ export function registerDesktopIpcHandlers({
// safeStorage only bought us a macOS Keychain prompt with no real security
// uplift on an unsigned build.
//
// TODO(S-07): `covel:keys:load` returns the real decrypted key values to the
// TODO: `covel:keys:load` returns the real decrypted key values to the
// renderer. This is a deliberate tradeoff — the renderer needs the raw keys
// to attach them via the `X-Provider-Keys` header and to mirror them into
// `localStorage` (`covel:keys`) for the pure-web path. Intended proper fix:
Expand All @@ -145,9 +145,9 @@ export function registerDesktopIpcHandlers({
// while the localStorage mirror stays plaintext, and on unsigned builds
// safeStorage degrades to a fixed key (no real encryption) plus a migration
// path for existing plaintext files. Doing it right requires reworking the
// whole key-flow (server-side injection), which is out of scope for S-07.
// whole key-flow (server-side injection), which is out of scope here.
ipcMain.handle("covel:keys:load", (event) => {
// Returns RAW provider keys — reject any untrusted sender frame (H-09).
// Returns RAW provider keys — reject any untrusted sender frame.
if (!isTrustedSender(event, "covel:keys:load")) return {};
return loadKeysEnv(paths.userKeysEnvPath);
});
Expand Down Expand Up @@ -228,7 +228,7 @@ export function registerDesktopIpcHandlers({
// Asset import — the sourcePath always originates from a native dialog in the
// MAIN process (see `pickAndImport` below), never from a renderer-supplied
// string. The old renderer-facing `covel:import:{plugin,world}` channels were
// removed (audit S-08: arbitrary-path vector) — they had no callers.
// removed (arbitrary-path vector) — they had no callers.
async function handleImport(
kind: ImportKind,
payload: unknown,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const INVOKE_CHANNELS = [
"covel:settings:load",
"covel:settings:save",
// Import is dialog-only: the renderer triggers a native file chooser in the
// main process (audit S-08). No renderer-supplied path channels.
// main process. No renderer-supplied path channels.
"covel:import:pick-plugin",
"covel:import:pick-world",
] as const;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/trusted-origin.selfcheck.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Runnable self-check for the H-09 origin-trust boundary. No test framework —
* Runnable self-check for the origin-trust boundary. No test framework —
* `tsx src/trusted-origin.selfcheck.ts` throws on the first failing assertion.
*/
import assert from "node:assert/strict";
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/trusted-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* (http://127.0.0.1:<port>) in production, the Vite dev server
* (http://localhost:5173) in dev. Anything else reaching a main-frame
* navigation or a key/secret IPC is an XSS / redirect pivot that could then
* call `covelIpc` and read raw provider keys — it must be refused (H-09).
* call `covelIpc` and read raw provider keys — it must be refused.
*/

/** Return the origin iff `rawUrl` parses to a loopback **http** URL, else null. */
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function getMainWindow(): BrowserWindow | null {
}

/**
* H-09: true iff `candidateUrl` matches the main window's committed loopback
* True iff `candidateUrl` matches the main window's committed loopback
* origin (the local sidecar / dev server). Shared with ipc-handlers.ts so a
* sensitive IPC can reject any sender frame that isn't the trusted app origin.
*/
Expand Down Expand Up @@ -300,7 +300,7 @@ export function createMainWindow(titleSuffix?: string): BrowserWindow {
return { action: "deny" };
});

// H-09: pin the main frame to the app's own loopback origin. setWindowOpenHandler
// Pin the main frame to the app's own loopback origin. setWindowOpenHandler
// only covers window.open/_blank — an in-window navigation (plugin UI, XSS,
// a redirect) could otherwise land on an attacker page that then calls
// `covelIpc` and reads raw provider keys. Block any main-frame navigation to
Expand Down
12 changes: 6 additions & 6 deletions apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@covel/server",
"version": "0.0.16",
"version": "0.0.17",
"private": true,
"type": "module",
"files": [
Expand Down Expand Up @@ -37,22 +37,22 @@
"@covel/state": "workspace:*",
"@covel/store": "workspace:*",
"@covel/tools": "workspace:*",
"@hono/node-server": "^2.0.5",
"@hono/node-server": "^2.0.8",
"@types/yauzl": "^3.4.0",
"ajv": "^8.20.0",
"gray-matter": "^4.0.3",
"hono": "^4.12.26",
"hono": "4.12.29",
"postgres": "^3.4.9",
"tsx": "^4.22.4",
"tsx": "4.22.4",
"yaml": "^2.9.0",
"yauzl": "^3.4.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/yazl": "^3.3.1",
"drizzle-kit": "^0.31.10",
"typescript": "^6.0.3",
"vitest": "^4.1.9",
"typescript": "^7.0.2",
"vitest": "^4.1.10",
"yazl": "^3.3.1"
}
}
4 changes: 2 additions & 2 deletions apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function resolvePreferredMemorySlot(slotRegistry: {
const app = new Hono();
const env = readRuntimeEnv();

// Fail fast on an unsafe hosted posture (audit S-13) before any route is
// Fail fast on an unsafe hosted posture before any route is
// wired or the caller starts listening. No-op for self-deploy/desktop.
validateSecurityPosture(env);

Expand Down Expand Up @@ -214,7 +214,7 @@ if (storeBackend === "pg" && env.databaseUrl) {
// to llm.toml without requiring code changes here.
const apiKeys = providerApiKeysFromEnv(process.env);
// Env-derived keys ride the `envApiKeys` channel so the provider registry
// origin-gates them (S-01) — the startup paths never carry request-scoped
// origin-gates them — the startup paths never carry request-scoped
// slot overlays today, but the provenance stays honest if that changes.
const llmAdapter = createGatewayAdapter(ai.gateway, { envApiKeys: apiKeys });
// Function-runtime gateway facade — shares the same preset/provider
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/embedding-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function createEmbeddingLockHelper(opts: {
try {
const result = await ai.gateway.embed(
{ values: ["covel-embed-probe"] },
// Boot-path keys are env-derived → origin-gated channel (S-01).
// Boot-path keys are env-derived → origin-gated channel.
apiKeys ? { envApiKeys: apiKeys } : undefined,
);
const vector = result.embeddings?.[0];
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { serverPort: port, bindHost } = readRuntimeEnv();

console.log(`Starting server on ${bindHost}:${port}...`);

// Loopback by default (audit S-02): the API has no per-request auth on the
// Loopback by default: the API has no per-request auth on the
// self-deploy tier, so it must not listen on public interfaces unless the
// operator opts in explicitly (COVEL_BIND_HOST=0.0.0.0 — containers, hosted).
const server = serve(
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/lib/pg-event-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* postgres.js maintains its own dedicated connection for LISTEN, so `max: 1`
* covers the NOTIFY sends without inflating PG connection usage.
*
* Ordering (re-review H-07): the bus publishes each session's frames through
* Ordering: the bus publishes each session's frames through
* a serialized outbox and every frame carries the origin seq, so this
* transport only needs best-effort ordering — `max: 1` sends NOTIFYs in call
* order and PG delivers them in commit order; the bus's receive side
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/lib/session-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* `/api/actions` requests to the same session read the same
* `listTurnResults().length`, compute the same turnNumber, and
* interleave state patches / auto-snapshots.
* - Audit 2026-04-21 F5: the previous `Map`-based lock was process-local,
* - The previous `Map`-based lock was process-local,
* invisible across pods. Multi-instance PG deployments (the documented
* production topology) had no cross-pod mutual exclusion.
*
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/middleware/media-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ export type VerifyMediaTokenResult =
| {
readonly ok: false;
readonly reason:
| "malformed"
| "expired"
| "bad_signature"
| "id_mismatch";
"malformed" | "expired" | "bad_signature" | "id_mismatch";
};

// ── Secret resolution ────────────────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/middleware/per-request-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* an env key when the resolved target's baseUrl origin matches trusted
* server config, so a request-scoped custom preset that redirects a
* built-in provider to a foreign origin can never exfiltrate a server
* key (S-01).
* key.
*
* When neither header is present the middleware is a no-op and the
* base-startup `llmAdapter` stays in place.
Expand Down Expand Up @@ -56,7 +56,7 @@ export interface PerRequestLlmOptions {
* keys / custom presets / slot overrides as the agent-runtime LLM
* adapter. Without this rebuild the function-runtime path silently uses
* the startup env keys and the server-side llm.toml, defeating
* per-session UI settings (audit F2).
* per-session UI settings.
*/
readonly defaultPluginGateway: PluginRuntimeGateway;
}
Expand Down Expand Up @@ -89,7 +89,7 @@ export function createPerRequestLlmMiddleware(
...(slotOverrides ? { slotOverrides } : {}),
});

// Audit F2: keep the function-runtime gateway in lock-step with the
// Keep the function-runtime gateway in lock-step with the
// agent-runtime LLM adapter. Both are rebuilt from the same merged
// keys / slot overrides so `ctx.gateway.resolveSlot(...)` inside a
// function handler resolves the same browser-declared custom presets
Expand Down
Loading