Skip to content

[eb-v2 review] Code Tools: parameter validation, allowlist isolation, and wizard fixes - #1655

Closed
chiang-daniel wants to merge 4 commits into
review/eb-v2/basefrom
review/eb-v2/code-tools
Closed

chiang-daniel wants to merge 4 commits into
review/eb-v2/basefrom
review/eb-v2/code-tools

Conversation

@chiang-daniel

@chiang-daniel chiang-daniel commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review-only PR — do not merge. These changes are already landed on dchiang/eb-v2-merge (the eval-builder integration branch); this PR is a focused review surface for one area of that work. Every fix carries an inline comment explaining what was broken and why the fix takes this shape. Comments marked Personal review requested are the spots that want human judgment — the rest is context your tooling can skim. Reply on the inline comments (or add new line comments); accepted feedback will be implemented on eb-v2-merge and the commit sha posted back here. When review wraps, this PR is closed, not merged — its base and head are throwaway review refs.

Surface: code-tool execution runtime, the CodeTool data model, and the code-tool authoring wizard. Commits: afe821bd1 (landed as c8876814e) (isolate broken allowlist entries so one dead entry can't take the whole nested-tool surface down; carry tool timeouts as a typed flag instead of sniffing output text), 4a63c3336 (landed as 34e6367a0) (reject reserved-word parameter names at authoring and generate docstrings that stay valid for any description), 9665236fc (landed as 5dd2487f8) (preserve authored code when returning to the wizard's code step), plus one hunk of 1c9a0cb1d (landed as one hunk of 6f1afc872) (make FormList row add/remove emit a change event so ancestor handlers notice structural edits). Around half the diff is tests. Three Personal review requested comments flag the layer/posture choices.

🤖 Generated with Claude Code

CI note: the "Check API Schema Bindings" failure here is an artifact of this review PR's pinned snapshot — the canonical schema verification lives on dchiang/eb-v2-merge. No action needed from reviewers.

chiang-daniel and others added 4 commits August 5, 2026 00:29
…nd as a typed flag

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e generated docstrings valid for any description

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

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change updates code-tool parameter validation, generated code, editor state resolution, form events, timeout classification, and allowlist handling. It adds API and unit coverage for these behaviors.

Changes

Code-tool authoring flow

Layer / File(s) Summary
Placeholder generation and step-two resolution
app/web_ui/src/lib/utils/code_tool_helpers.ts, app/web_ui/src/lib/utils/code_tool_helpers.test.ts
Generated parameters preserve schema names. Descriptions escape backslashes and quotes. resolveStep2Code selects clone, placeholder, or authored code and reports state changes.
Form row change events
app/web_ui/src/lib/utils/form_list.svelte, app/web_ui/src/routes/(app)/tools/[project_id]/add_tools/code_tool/+page.svelte
Row additions and removals dispatch bubbling change events. The code-tool page uses resolveStep2Code for editor state.

Parameter validation

Layer / File(s) Summary
Parameter schema validation and compatibility
libs/core/kiln_ai/datamodel/code_tool.py, libs/core/kiln_ai/datamodel/test_code_tool.py, app/desktop/studio_server/test_code_tool_api.py
Newly authored tools reject reserved Python keywords. File-loaded tools retain legacy compatibility. API and model tests cover the validation rules.

Code-tool execution robustness

Layer / File(s) Summary
Typed timeout results
libs/core/kiln_ai/tools/base_tool.py, libs/core/kiln_ai/tools/code_tool.py, libs/core/kiln_ai/sandbox/test_code_tool_execution.py
ToolCallResult exposes timed_out. Nested execution uses this flag instead of matching error text.
Allowlist resolution and unavailable tools
libs/core/kiln_ai/tools/code_tool.py, libs/core/kiln_ai/tools/tool_registry.py, libs/core/kiln_ai/sandbox/test_code_tool_execution.py
Unresolvable allowlist entries are logged and listed as unavailable. Healthy tools remain dispatchable and visible.

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

Sequence Diagram(s)

sequenceDiagram
  participant Step1Form
  participant CodeToolPage
  participant resolveStep2Code
  participant CodeEditor
  Step1Form->>CodeToolPage: announce schema row change
  CodeToolPage->>resolveStep2Code: pass current code and schema state
  resolveStep2Code->>CodeToolPage: return resolved code and flags
  CodeToolPage->>CodeEditor: update editor content
Loading

Possibly related PRs

Suggested reviewers: scosman

Poem

A rabbit checks the tools at night,
Keeps schema names precise and right.
Timeouts wear a proper sign,
Broken paths stay in line.
The code step keeps its place—
Hop, hop, through the test-case maze.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% 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
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.
Title check ✅ Passed The title clearly summarizes the main changes to CodeTool validation, allowlist isolation, and the authoring wizard.
Description check ✅ Passed The description clearly explains the scope, review-only posture, changes, commits, and CI note, although it omits the template's explicit headings and checklist.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch review/eb-v2/code-tools

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

const requiredParams = params.filter((p) => p.required)
const optionalParams = params.filter((p) => !p.required)

// Schema property names are used verbatim: the sandbox calls run() with

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This deletes safePythonName and the reserved-word table it used to rewrite parameter names in the generated stub. That rewrite was the actual bug behind the datamodel guard: it silently produced a stub whose parameter name didn't match the schema, so the sandbox's keyword call could never reach it. Now the stub uses the schema names verbatim and the reserved-word case is rejected at authoring instead of being papered over here.

const safeDesc = toolDescription.replace(/"""/g, "''")
// Escape backslashes then double-quotes so the description can neither close the
// triple-quoted docstring nor leave a dangling escape (e.g. a trailing " or \).
const safeDesc = toolDescription.replace(/\\/g, "\\\\").replace(/"/g, '\\"')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The placeholder generator embeds the tool description inside a triple-quoted docstring. The old escaping only replaced literal """, which left two ways to emit non-compiling Python: a description ending in a backslash escaped the closing quote, and a stray double-quote could still break out. Escaping backslashes first, then double-quotes, makes the description safe for any input — verified round-trip in the tests.

* Decide the Code step's editor contents when the user advances from the
* Define step.
*
* Keyed on whether code already exists — not on the wizard step — because the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The wizard decided the code editor's contents from current_step, but the documented Back path pops the shallow-routing history so current_step reads "define" on a return — the discriminator was effectively always "first visit", and the code-preserving branch was unreachable. So returning to the code step wiped the user's authored code with a fresh placeholder. This pure helper keys the decision on whether code already exists instead of on the step, and the page (+page.svelte) now calls it. The clone seed is consumed once so a later return can't re-apply it over edits.

// and tested-state tracking — react to structural row edits. The initial
// start_with_one seed runs before mount, so add_button_el is still undefined
// then and no spurious event fires.
function announce_row_change() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding or removing a FormList row is a plain button click, which fires no native input/change event — so ancestor on:change handlers (the unsaved-changes guard, and the eval builder's tested-state tracking) never saw structural row edits. This dispatches a bubbling change from a stable in-tree anchor when a row is added or removed, the way a native control would. The start_with_one seed runs before mount, while the anchor is still undefined, so no spurious event fires on init.

def validate_parameters_schema(self, info: ValidationInfo) -> Self:
validate_schema_dict(self.parameters_schema, require_object=True)

# Parameters are passed to run() as keyword arguments keyed by these names,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Personal review requested: Reserved-word rejection lives in the data-model validator with a loaded_from_file exemption, so an already-broken tool still loads but a PATCH that revalidates it now fails loudly. Do you agree with the layer (data model rather than API or client) and the posture (load old broken tools silently, reject on the next write)? The alternative postures are reject on load too, or warn-not-block on write.

# Parameters are passed to run() as keyword arguments keyed by these names,
# and a Python reserved word can never be declared as a run() parameter, so
# reject it at authoring. Loaded files are exempt so old projects still open.
# Soft keywords (match/case/type/_) are valid identifiers and stay allowed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Personal review requested: The check uses keyword.iskeyword, which deliberately allows soft keywords (match, case, type, _) because they are valid identifiers and work fine as run() parameters — only hard keywords are rejected. Confirm you want soft keywords allowed rather than reserved defensively (the stricter alternative would use keyword.issoftkeyword too).

if not self.loaded_from_file(info):
for param_name in self.parameters_schema.get("properties", {}):
if keyword.iskeyword(param_name):
raise ValueError(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A parameter named after a Python keyword (e.g. from, class) is impossible: the sandbox invokes run() with keyword arguments keyed by the exact schema property names, and no reserved word can be a run() parameter. The old client helper hid this by renaming the parameter in the generated stub, so the stub declared from_param while the sandbox still called from=... — the tool was permanently broken and authoring never warned. Rejecting it here, in the data model, catches every write path. loaded_from_file is exempt so existing projects still open (see the flag on the posture).

default=None,
description="Human-readable error message, set when is_error is True.",
)
timed_out: bool = Field(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the structural signal that replaces the text-sniffing above. Adding it to ToolCallResult lets the timeout be reported once, where it's known (the execution runtime), and read as a boolean everywhere else — no caller has to re-derive "was this a timeout?" by parsing a human-readable message.

kind = "timeout" if is_timeout else "call_error"
# The result carries timeouts as a typed flag; ordinary failures
# whose text merely mentions a timeout must stay call_error.
kind = "timeout" if result.timed_out else "call_error"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nested-call timeout classification was a substring match on the result text ("timed out" in output). Any ordinary failure whose message happened to mention a timeout was misclassified as a real ToolTimeout, and conversely a timeout worded differently would be missed. The result now carries the timeout as a structural timed_out flag (added on ToolCallResult), so classification reads a boolean the runtime sets rather than guessing from free text.

try:
fn_name = await self._canonical_tool_name(tool_id)
except Exception as exc:
# A broken entry (e.g. a deleted RAG config) must not take the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A single unresolvable allowlist entry — e.g. a nested tool pointing at a RAG config the user deleted — used to throw while building the name map and take the entire nested-tool surface down with it, so every healthy tool became uncallable. This isolates the failure per entry: a broken one is logged and left out of the map, and calls to it fail with the standard "not available" error while the rest keep working. list_tools() gets the matching treatment just below.

"name": fn_name,
"description": "(unavailable)",
"name": tool_id,
"description": f"(unavailable: {exc})",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Personal review requested: This changes the list_tools contract: a broken allowlist entry now renders as (unavailable: <reason>) under its raw id, instead of failing the whole call. That's a deliberate degraded-UX choice — the surface stays usable and the broken entry is visible, but its intended name is unknowable without resolving it. Confirm you're happy exposing the id and the raw exception text here, versus a generic "unavailable" with no detail.


# Lazy import — PythonCodeTool is phase 2; for now return a
# lightweight wrapper that has the tool's identity and definition.
# Lazy import — PythonCodeTool pulls in the multiprocessing execution

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The comment described a design that no longer exists — a "phase 2 / lightweight wrapper" placeholder — when this returns the full PythonCodeTool execution runtime. Left as-is it would mislead the next reader into thinking the real tool wasn't wired up yet. Rewritten to state the actual reason the import is lazy: it pulls in the multiprocessing runtime that most registry lookups don't need.

@chiang-daniel
chiang-daniel requested a review from scosman August 5, 2026 23:41

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

🧹 Nitpick comments (1)
app/web_ui/src/lib/utils/code_tool_helpers.ts (1)

197-205: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the as string cast.

useClone is a separate boolean, so TypeScript cannot narrow cloneCode through it. Test cloneCode directly and the cast becomes unnecessary.

♻️ Proposed refactor
   if (code === "") {
-    const useClone = !!cloneCode
     return {
-      code: useClone ? (cloneCode as string) : newPlaceholder,
+      code: cloneCode ? cloneCode : newPlaceholder,
       generatedPlaceholder: newPlaceholder,
       schemaChangedHint: false,
-      cloneConsumed: useClone,
+      cloneConsumed: !!cloneCode,
     }
   }
🤖 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 `@app/web_ui/src/lib/utils/code_tool_helpers.ts` around lines 197 - 205, Update
the empty-code branch in the relevant helper to test cloneCode directly when
selecting the returned code value, rather than using the separate useClone
boolean for narrowing; remove the unnecessary as string cast while preserving
cloneConsumed’s existing boolean behavior.
🤖 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 `@app/web_ui/src/lib/utils/code_tool_helpers.ts`:
- Around line 216-223: Update the schemaChangedHint decision in the
newPlaceholder comparison so description-only edits do not mark the schema as
changed; compare the generated parameter list or equivalent parameter-specific
representation instead of the full placeholder, while preserving cloneConsumed
and generatedPlaceholder behavior.

---

Nitpick comments:
In `@app/web_ui/src/lib/utils/code_tool_helpers.ts`:
- Around line 197-205: Update the empty-code branch in the relevant helper to
test cloneCode directly when selecting the returned code value, rather than
using the separate useClone boolean for narrowing; remove the unnecessary as
string cast while preserving cloneConsumed’s existing boolean behavior.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dc2c2cba-ff2c-47f3-8d30-dfb4c80716bd

📥 Commits

Reviewing files that changed from the base of the PR and between b27d50e and 1c9a0cb.

📒 Files selected for processing (11)
  • app/desktop/studio_server/test_code_tool_api.py
  • app/web_ui/src/lib/utils/code_tool_helpers.test.ts
  • app/web_ui/src/lib/utils/code_tool_helpers.ts
  • app/web_ui/src/lib/utils/form_list.svelte
  • app/web_ui/src/routes/(app)/tools/[project_id]/add_tools/code_tool/+page.svelte
  • libs/core/kiln_ai/datamodel/code_tool.py
  • libs/core/kiln_ai/datamodel/test_code_tool.py
  • libs/core/kiln_ai/sandbox/test_code_tool_execution.py
  • libs/core/kiln_ai/tools/base_tool.py
  • libs/core/kiln_ai/tools/code_tool.py
  • libs/core/kiln_ai/tools/tool_registry.py

Comment on lines +216 to +223
if (newPlaceholder !== generatedPlaceholder) {
return {
code,
generatedPlaceholder: newPlaceholder,
schemaChangedHint: true,
cloneConsumed: 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

The hint fires for description-only edits.

newPlaceholder embeds the escaped toolDescription as well as the parameter list. A user who returns to the Define step, edits only the description, and continues gets schemaChangedHint: true. The UI then shows "Schema changed — check run()'s parameters." even though no parameter changed.

Two options: compare only the generated parameter list to decide the hint, or reword the message so it covers any regenerated-stub difference.

🤖 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 `@app/web_ui/src/lib/utils/code_tool_helpers.ts` around lines 216 - 223, Update
the schemaChangedHint decision in the newPlaceholder comparison so
description-only edits do not mark the schema as changed; compare the generated
parameter list or equivalent parameter-specific representation instead of the
full placeholder, while preserving cloneConsumed and generatedPlaceholder
behavior.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

📊 Coverage Report

Overall Coverage: 93%

Diff: origin/review/eb-v2/base...HEAD

  • libs/core/kiln_ai/datamodel/code_tool.py (100%)
  • libs/core/kiln_ai/tools/base_tool.py (100%)
  • libs/core/kiln_ai/tools/code_tool.py (100%)

Summary

  • Total: 20 lines
  • Missing: 0 lines
  • Coverage: 100%

@chiang-daniel

Copy link
Copy Markdown
Contributor Author

Superseded. The branch is now split into seven review-only views with the current tip; this area is inside them. Closing so there is one set to review.

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