Skip to content

feat(assistant): workspace custom tools that run scripts on dev machines - #103

Merged
carbogninalberto merged 1 commit into
mainfrom
carbninai/r10-s8-custom-tools
Oct 29, 2026
Merged

carbogninalberto merged 1 commit into
mainfrom
carbninai/r10-s8-custom-tools

Conversation

@carbogninalberto

Copy link
Copy Markdown
Owner

Problem

Phase 3 of the orchestrator plan (step S8, backend only): admins should be able to give the assistant workspace-specific tools — scripts that run on a dev machine — and the assistant should be able to draft new ones, without ever running code on the Kuayle server or letting arguments become shell syntax.

This branch is built on S6 (skills, #100) with S2 (run check, #99) merged in, so the diff against main includes them until they merge. The S8 change is the last commit.

Change

  • Data (migration 99): assistant_custom_tools + assistant_custom_tool_versions. A tool has a snake_case name (unique per workspace; it can't be a built-in name, with or without custom_), a description, a strictly validated parameter schema, a script (≤ 64 KiB), an interpreter (sh/bash/node/python3, all present in the ide and headless images), a run mode (approval, or auto only when read_only, enforced in the DB too), a timeout (10–1800 s, default 600), enabled, status draft|active, versions and author kind. Versions, drafts, activation and restore work the same way as skills.
  • Schema validation (domain.ParseToolSchema / ValidateInputs): an object of at most 20 string (min/maxLength ≤ 8192), number/integer (finite bounds), boolean or string-enum properties, with no additional properties, nesting, unknown keywords, duplicate keys or quoted numbers. Schemas are capped at 16 KiB and inputs at 16 KiB.
  • Runs (migration 100): assistant_tool_runs, the run_tool operation, the run_custom_tool approval action (the constraint carries the full union, including run_check), and tool-call names may now contain digits. Queuing uses the manual check's lockMachineForCheck, and a queued or running tool run holds the machine just like a manual check does.
  • Execution (machine/custom_tool.go): each run gets its own check container, with the developer image, the hardened check host config, egress only through the proxy, no credentials, and the attempt's snapshot used only for redaction. The command line is a fixed sh -c <bootstrap> kuayle-tool <interpreter>. The script and inputs arrive only in the environment, the bootstrap writes them to private mktemp files (KUAYLE_TOOL_INPUT_FILE), and every input is also KUAYLE_ARG_<NAME>. Output goes through the agent output buffer (redaction and caps) and is stored as a 64 KiB tail. The run has a timeout, its container is removed when it finishes, and a run that fails for good is recorded with a safe reason. Every write re-checks the operation lease and generation, the requester's role, that the tool is still active at that version, and that the task or machine still belongs to the requester.
  • API (session-only): /assistant-tools CRUD, /versions, /activate, /versions/:v/restore, /enabled, POST /:id/test-run {inputs, machine_id|identifier, version?} (owners/admins; can run drafts), GET /:id/runs, GET /assistant-tool-runs/:runId. Members read tools; non-admins read only the runs they requested.
  • Assistant: active, enabled tools are offered as custom_<name> with {identifier?, machine_id?, inputs: <schema>}. Approval-mode calls create a run_custom_tool card bound to the issue, version, inputs and task generation, and approving queues one run (idempotent). Auto tools run directly and are awaited for up to 2 minutes. Results go back to the model as untrusted_data, and get_tool_run reads them later. propose_tool only saves drafts (always in approval mode) and is limited to owners/admins. Custom tools show up in /assistant-capabilities (custom: true), and their switch is the tool's own enabled flag. Telegram has approval/failure answers and names saved draft tools.
  • UI: UI/src/lib/api/assistant-tools.ts client types only (the settings UI is S9).
  • Docs: BE/docs/conversations.md#custom-tools, BE/docs/agent-tasks.md#custom-tool-runs, TECHNICAL.md.

Verification

  • go build ./... && go vet ./... pass.
  • Full suite on a fresh migrated database: go test ./... -count=1 -p 2 gave 1878 passed (tests and subtests), 9 skipped (the Docker/sudo integration tests and the env-gated issue/analytics Postgres tests), 0 failed.
  • New tests:
    • schema and input table tests;
    • the real bootstrap run under sh, bash, python3 and node: ; rm -rf /, $(…) and backticks arrive literally and nothing they contain executes;
    • a fake-Docker container config test: fixed command line, arguments only in the environment, a single workspace mount, read-only root;
    • manager tests with fakes: success with redaction, a refused tool, timeout, and a stale machine;
    • repository tests: admin-only writes, drafts, the draft/disabled/wrong-machine/busy rules, run visibility, lease-fenced output tail, failure recording;
    • service tests: offering, propose_tool, the approval flow including a changed version, auto runs and ownership;
    • an HTTP role matrix including PATs;
    • a migration round trip that refuses rollback while a run is active;
    • Telegram texts.
  • Interpreters checked in the local dev-machine-ide and dev-machine-headless images (sh, bash, node, python3, mktemp).
  • npm run check (0 errors) and node AGENTS/TOOLS/check-harness.mjs pass.

Limits

  • Calls in approval mode need an identifier, because conversation_approvals.issue_id is NOT NULL. machine_id-only calls are for auto tools and test runs.
  • An auto run is awaited for up to 2 minutes. After that the model gets queued/running and uses get_tool_run.
  • A run's output is a stored 64 KiB tail plus tool_run.updated conversation events. There are no per-line log chunks and no artifacts page yet.
  • No real-Docker end-to-end run of a tool container (an ad-hoc docker run smoke test wasn't allowed in this environment). The bootstrap itself is covered by the local-shell test.

@carbogninalberto
carbogninalberto force-pushed the carbninai/r10-s8-custom-tools branch from b62127d to 8f6e651 Compare October 29, 2026 13:40
@carbogninalberto
carbogninalberto marked this pull request as ready for review October 29, 2026 13:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T13:47:22.219862Z 8f6e651 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f6e651aa2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
summary := *run
summary.Output = outputTail(run.Output, 4096)
_, err := conversationEvent(ctx, tx, *run.ConversationID, "tool_run.updated", summary)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Lock the conversation before allocating tool-run event cursors

When a tool run changes state while its conversation is streaming a reply, this inserts an event without first locking that conversation. conversation_events.cursor is identity-generated, so a concurrent ConversationRepository.Append can commit a higher cursor before this transaction commits its lower cursor; the SSE reader then advances past the tool-run event and permanently misses it on replay. toolRunTransaction locks the run and operation, not the conversation, and machine-targeted QueueRun also lacks that lock. Acquire the conversation's cursor lock before inserting these events, preserving the established lock ordering. AGENTS/SKILLS/kuayle-backend/SKILL.mdL43-L45

Useful? React with 👍 / 👎.

if err != nil {
return nil, nil, err
}
approval, err := s.Repo.CreateApproval(ctx, a, toolCallID, "run_custom_tool", prepared.issue.ID, data, assistantApprovalTTL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Render custom-tool approvals before offering them in Web chat

For any approval-mode custom tool called from Web chat, the new run_custom_tool approval falls through to the delegation branch in UI/src/lib/features/assistant/ToolCalls.svelte: the prompt says “Start a coding agent” and renders the absent payload.instructions, rather than showing the tool, version, machine and validated inputs. The Approve button still executes the script, so users authorize a different operation from the one described by the confirmation. Add the corresponding conversation API contract and a dedicated approval rendering branch before exposing this action. AGENTS/SKILLS/kuayle-backend/SKILL.mdL35-L36

Useful? React with 👍 / 👎.

Comment on lines +107 to +109
if req.ApprovalID != nil {
existing := &domain.AssistantToolRun{}
err = tx.GetContext(ctx, existing, `SELECT `+toolRunColumns+` FROM assistant_tool_runs r WHERE r.approval_id=$1 AND r.workspace_id=$2`, req.ApprovalID, ws)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize approval replay checks before queuing machine-targeted runs

If two requests concurrently approve the same tool run with an explicit machine_id, both can receive the approving claim and find no existing run here, because this path has no task transaction to serialize them. The first request queues the run; the second then acquires the machine lock and receives ErrMachineBusy without checking the approval's run again. AssistantApprovals.runCustomTool treats that error as a terminal approval failure, so if its FinishApproval wins the race, the approval is permanently marked failed even though its script was successfully queued. Serialize requests for the same approval before this lookup, or recheck the existing run after acquiring the machine lock and before rejecting it as busy.

Useful? React with 👍 / 👎.

Comment on lines +100 to +102
labels := machineLabels(machine)
labels["com.kuayle.service"] = "agent"
labels["com.kuayle.tool-run-id"] = run.ID.String()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Attach task-targeted tool runs to physical attempt cancellation

When an awaiting-review task on a reused machine is cancelled while its custom tool is running without output, DockerRuntime.CancelAgent misses this container because it selects com.kuayle.agent-run-id, which these labels omit. The manager also excludes run_tool from active-run cancellation, and the queued operation lacks the attempt/checkout identifiers checked by FinishCancellation. Consequently, task cancellation can report runtime cleanup complete while the script continues modifying the checkout until its next output write or timeout. Bind task-targeted tool operations and containers to their attempt and include them in physical cancellation. AGENTS/SKILLS/kuayle-backend/SKILL.mdL54-L56

Useful? React with 👍 / 👎.

Add assistant_custom_tools and assistant_custom_tool_versions (migration 99)
and assistant_tool_runs with the run_tool machine operation and the
run_custom_tool approval (migration 100). Tools have a strictly validated
scalar parameter schema, a script for sh, bash, node or python3, a run mode
(auto only when read-only) and versions with drafts, activation and restore;
owners and admins write and test-run them through /assistant-tools, members
read tools and their own runs.

The assistant is offered active, enabled tools as custom_<name>, validates
inputs against the schema, asks for approval (or runs read-only auto tools)
and reads results with get_tool_run; propose_tool only saves drafts. Runs use
the manual check's machine lock and a credential-free check container with a
fixed command line: the script and arguments arrive only as environment
variables and a JSON file, never interpolated.
@carbogninalberto
carbogninalberto force-pushed the carbninai/r10-s8-custom-tools branch from 8f6e651 to fb653cb Compare October 29, 2026 13:56
@carbogninalberto
carbogninalberto merged commit 5d0b5b3 into main Oct 29, 2026
30 checks passed
@carbogninalberto
carbogninalberto deleted the carbninai/r10-s8-custom-tools branch October 29, 2026 14:14
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.

2 participants