Skip to content

feat(agent-model-api): agent turn execution slice (rebase) - #2195

Open
hognek wants to merge 12 commits into
jaylfc:devfrom
hognek:tsk-gkh4mi-agent-turn-slice-dev
Open

feat(agent-model-api): agent turn execution slice (rebase)#2195
hognek wants to merge 12 commits into
jaylfc:devfrom
hognek:tsk-gkh4mi-agent-turn-slice-dev

Conversation

@hognek

@hognek hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Rebase of #2176 onto origin/dev (d728dd8).

Conflict: CHANGELOG.md — dev added push notifications entry, PR added Agent-as-a-Model entry. Kept both.

Supersedes #2176 (head on jaylfc repo, cannot update from fork).

Summary by CodeRabbit

  • New Features
    • /v1/chat/completions now executes a real one-shot “agent-as-a-model” turn and returns an OpenAI-compatible response (no longer 501).
    • Supports streaming when stream is an explicit JSON boolean.
    • Opencode servers/sessions are cached per model, with per-model session-cache synchronization after session setup.
  • Bug Fixes
    • Missing user messages now return 400 (invalid_request).
    • Turn/transport failures return 502 (agent_error).
    • Model-specific degraded startup/self-healing resets correctly on model switches.
  • Tests
    • Updated/added coverage for turn success/failure, validation, caching behavior, and model switching.
    • Added safety tests for agent_ids against traversal/unsafe patterns.
  • Documentation
    • Corrected the GPU inference flow description to match one-shot behavior.

jaylfc added 5 commits July 28, 2026 17:05
Replace the 501 stub in POST /v1/chat/completions with a real one-shot
agent turn via the opencode host-server seam (consent key -> agent ->
opencode server + LiteLLM virtual key -> one non-streaming turn ->
OpenAI ChatCompletion shape). Reuses ensure_taos_opencode_server and
drive_turn from the existing taOS agent chat path. Returns 502 on
transport failure; never leaks internals.

LIVE ROUND-TRIP: pending consent-key mint (owner session required) — see PR.
Replace the 501-until-implemented test with two mock-driven tests that assert
the new turn behaviour headlessly (no opencode binary in CI): a consented
request returns the OpenAI ChatCompletion envelope (200), and a transport
failure degrades to 502. The runtime calls (ensure_taos_opencode_server,
drive_turn) are referenced by module-level name so tests monkeypatch them.

Also updates the module docstring which still claimed 501.
…fc#2176)

- Per-agent opencode server cache in ensure_taos_opencode_server (dict keyed
  by agent_id/model) so concurrent agents do not churn a shared singleton and
  race (Kilo correctness bug). stop_taos_opencode_server stops all entries.
- Missing user message -> 400 (invalid_request), not 502 (Kilo).
- stream requires explicit JSON boolean; bool('false') no longer coerces to True.
- Validate message content is str/list before forwarding to drive_turn.
- Add CHANGELOG entry + fix stale 'returns 501' claim in gpu-work-queue.md.
- Add test_chat_missing_user_message_returns_400.
Set install.entrypoint to 'python -m taosr1.ui' so the App Store LXC runs the
3-slot assignment UI (the surface Jay opens to attach agents to the 3 Rabbit
slots). Slot UI + persistence live in github.com/jaylfc/taOSr1.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The /v1/chat/completions endpoint now executes one-shot agent turns through opencode, returns OpenAI-compatible responses, validates requests, and maps failures. Opencode servers, sessions, and degraded-state tracking are cached per model, with path-safe agent and model identifiers.

Changes

Agent model turn execution

Layer / File(s) Summary
Per-model opencode lifecycle
tinyagentos/taos_agent_runtime.py, tinyagentos/routes/taos_agent.py, tests/test_taos_agent_chat.py, tests/test_taos_agent_config.py
Opencode servers, sessions, persistence homes, degraded flags, and shutdown handling are now model-scoped; session synchronization, model switching, self-healing, and safe path handling are tested.
Chat completion turn path
tinyagentos/routes/agent_model_api.py, tests/test_routes_agent_model_api.py, CHANGELOG.md, docs/design/...
The endpoint drives one agent turn, returns non-streaming or SSE OpenAI-shaped responses, validates user messages and boolean streaming, and maps turn failures to 502 errors. Tests replace the former 501 expectation.
Agent identifier validation
tinyagentos/routes/agent_model_keys.py, tinyagentos/agent_model_key_store.py, tests/test_agent_model_keys_route.py, tests/test_agent_model_key_store.py
Route and store validation reject empty, unsafe, and path-separator-containing agent identifiers before key minting.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant agent_model_api
  participant ensure_taos_opencode_server
  participant drive_turn
  Client->>agent_model_api: POST /v1/chat/completions
  agent_model_api->>ensure_taos_opencode_server: ensure model server
  agent_model_api->>drive_turn: drive one agent turn
  drive_turn-->>agent_model_api: final reply or error
  agent_model_api-->>Client: OpenAI ChatCompletion response or SSE stream
Loading

Possibly related PRs

  • jaylfc/taOS#813: Both changes modify opencode server provisioning and related turn/server lifecycle behavior.
  • jaylfc/taOS#816: Both changes modify born-degraded tracking and self-healing behavior.
  • jaylfc/taOS#1383: Both changes modify the agent model chat-completions contract and its transition from 501 behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.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 is concise and clearly points to the main change: implementing agent turn execution for agent-model API during a rebase.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Execute consented agents through OpenAI chat completions

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Executes consented agent models through the existing opencode turn runtime.
• Isolates opencode servers and sessions per agent to prevent cross-agent churn.
• Returns OpenAI-compatible completions, streams, validation errors, and transport failures.
Diagram

sequenceDiagram
    actor Client
    participant API as Chat API
    participant Keys as Consent Keys
    participant Cache as Agent Cache
    participant OpenCode as OpenCode Server
    participant LiteLLM
    Client->>API: Chat request
    API->>Keys: Resolve consent
    Keys-->>API: Agent binding
    API->>Cache: Ensure agent server
    Cache->>OpenCode: Start or reuse
    API->>OpenCode: Drive turn
    OpenCode->>LiteLLM: Model request
    LiteLLM-->>OpenCode: Model response
    OpenCode-->>API: Final reply
    API-->>Client: OpenAI envelope
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated agent execution service
  • ➕ Separates external process lifecycle from the API server
  • ➕ Supports independent scaling and resource limits
  • ➖ Introduces another service boundary and deployment dependency
  • ➖ Duplicates existing opencode runtime integration
2. Shared opencode server
  • ➕ Uses fewer host processes and less memory
  • ➕ Simplifies shutdown and health monitoring
  • ➖ Requires synchronized model and credential reconfiguration
  • ➖ Reintroduces cross-agent contention and session isolation risks

Recommendation: Reusing the established opencode host-server and turn-driver seam is the best incremental approach because it preserves existing agent behavior and avoids a parallel execution stack. Per-agent caching is preferable to mutable singleton reuse for isolation; a dedicated execution service should only be considered if process count or scaling becomes operationally significant.

Files changed (6) +304 / -59

Enhancement (2) +157 / -13
agent_model_api.pyImplement OpenAI-compatible agent turn execution +153/-13

Implement OpenAI-compatible agent turn execution

• Executes the latest user message through the consented agent's opencode server and formats the final response as a completion or single-chunk SSE stream. Adds content validation and maps malformed requests and runtime failures to sanitized OpenAI-style 400 and 502 responses.

tinyagentos/routes/agent_model_api.py

taos_agent.pySynchronize sessions with the per-agent cache +4/-0

Synchronize sessions with the per-agent cache

• Stores newly established opencode session identifiers under the active model while retaining the legacy singleton session attribute.

tinyagentos/routes/taos_agent.py

Bug fix (1) +50 / -42
taos_agent_runtime.pyCache opencode runtimes per agent model +50/-42

Cache opencode runtimes per agent model

• Replaces singleton server state with model-keyed server, session, and degraded-state caches to prevent cross-agent replacement races. Uses isolated homes, rebuilds degraded entries independently, and stops every cached server during shutdown.

tinyagentos/taos_agent_runtime.py

Tests (1) +87 / -3
test_routes_agent_model_api.pyTest successful turns and mapped failures +87/-3

Test successful turns and mapped failures

• Replaces the obsolete 501 assertion with mock-driven coverage for successful OpenAI completion envelopes, transport failures returning 502, and missing user messages returning 400. Runtime seams are monkeypatched so tests remain headless.

tests/test_routes_agent_model_api.py

Documentation (2) +10 / -1
CHANGELOG.mdDocument Agent-as-a-Model turn execution +9/-0

Document Agent-as-a-Model turn execution

• Adds an Unreleased entry covering real chat execution, per-agent server isolation, request validation, and explicit stream handling.

CHANGELOG.md

2026-07-17-gpu-work-queue.mdCorrect the documented agent inference path +1/-1

Correct the documented agent inference path

• Updates the GPU work queue design to reflect that the Agent-as-a-Model route now executes through opencode and returns OpenAI-compatible completions.

docs/design/2026-07-17-gpu-work-queue.md

@qodo-code-review

qodo-code-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Opencode data remains trackable 📜 Skill insight ≡ Correctness
Description
The new per-model opencode home writes runtime files beneath data/taos-agent-opencode-*, but
.gitignore contains no matching entry. Generated runtime state can therefore be committed
accidentally.
Code

tinyagentos/taos_agent_runtime.py[134]

+        home = str(data_dir / f"taos-agent-opencode-{model}") if data_dir else f"taos-agent-opencode-{model}"
Relevance

⭐⭐⭐ High

Small deterministic repository-hygiene fix required by the stated compliance rule.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2234003 requires components writing under data/ to add a corresponding ignore
entry in the same PR. The new path derives each opencode home from data_dir, while the branch
.gitignore has no taos-agent-opencode pattern.

tinyagentos/taos_agent_runtime.py[133-134]
.gitignore[18-35]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Per-model opencode runtime directories under `data/` are not ignored by Git.

## Issue Context
Add an ignore pattern covering every dynamically named `taos-agent-opencode-*` directory without ignoring unrelated data.

## Fix Focus Areas
- .gitignore[18-35]
- tinyagentos/taos_agent_runtime.py[133-134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Conversation context is discarded 🐞 Bug ≡ Correctness
Description
_run_agent_turn forwards only the last user message and drops every system, assistant, and earlier
user message from the OpenAI request. Because drive_turn constructs a fresh adapter and session
for this invocation, the omitted messages are unavailable and multi-turn prompts or system
instructions produce incorrect responses.
Code

tinyagentos/routes/agent_model_api.py[R190-191]

+    # The last user message is the prompt; system/earlier messages are context
+    # the agent harness already carries per-turn, so we pass the latest user text.
Relevance

⭐⭐⭐ High

PR #234 directly accepted restoring omitted conversation context.

PR-#234

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The endpoint scans backward and forwards exactly one user_text, while drive_turn constructs a
new adapter and calls ensure_session; it does not receive the omitted request history. Existing
agent routing explicitly builds and forwards a context window, matching the context-loss pattern
accepted in PR #234.

tinyagentos/routes/agent_model_api.py[190-237]
tinyagentos/opencode_runtime.py[377-399]
tinyagentos/agent_chat_router.py[165-224]
PR-#234

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Preserve the supplied OpenAI message history, including system instructions and prior assistant/user turns, when driving the agent turn.

## Issue Context
The current implementation reduces the request to one string while the runtime creates a new adapter session. Convert supported content parts without dropping message roles or earlier turns.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[180-237]
- tinyagentos/opencode_runtime.py[377-399]
- tinyagentos/agent_chat_router.py[165-224]
- tinyagentos/agent_chat_router.py[272-285]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Cold-start cache races 🐞 Bug ☼ Reliability
Description
The new server cache performs an unsynchronized check-then-create sequence containing multiple
awaits, so concurrent first requests for one model can create and start duplicate servers before
overwriting the same cache entry. This can leak an untracked process, duplicate key operations, or
fail both turns through home and port contention.
Code

tinyagentos/taos_agent_runtime.py[R44-47]

+    servers = getattr(app_state, "taos_opencode_servers", None)
+    if servers is None:
+        servers = {}
+        app_state.taos_opencode_servers = servers
Relevance

⭐⭐⭐ High

PR #347 directly accepted synchronization around race-prone lazy initialization.

PR-#347

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cache miss is checked at line 53, but insertion does not occur until line 145 after several
awaited operations, allowing another task to observe the same miss. This is the same
lazy-initialization race previously accepted in PR #347.

tinyagentos/taos_agent_runtime.py[41-53]
tinyagentos/taos_agent_runtime.py[76-150]
PR-#347

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent cold starts can both observe a cache miss and initialize the same model. Add per-model synchronization or cache a shared initialization task, including cleanup on failed initialization.

## Issue Context
The cache lookup occurs before several awaited settings and key operations, and insertion happens only afterward. Different models may initialize concurrently, but one model must have exactly one in-flight initialization.

## Fix Focus Areas
- tinyagentos/taos_agent_runtime.py[41-53]
- tinyagentos/taos_agent_runtime.py[76-150]
- tinyagentos/taos_agent_runtime.py[163-182]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Per-agent servers share port ✓ Resolved 🐞 Bug ☼ Reliability
Description
Every cached per-agent OpenCodeServer is still configured on fixed port 4188, so the second server
collides with the first and its health check/base URL can address the first process. Multi-agent
requests consequently fail startup or execute against another agent's server configuration.
Code

tinyagentos/taos_agent_runtime.py[145]

+        servers[model] = server
Relevance

⭐⭐⭐ High

Fixed-port server cache deterministically defeats concurrent multi-agent operation.

PR-#347

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Each new cached server receives TAOS_OPENCODE_PORT, and OpenCodeServer derives both its bind
argument and base_url from that port. Multiple entries in the new cache therefore cannot represent
independently running servers.

tinyagentos/taos_agent_runtime.py[133-150]
tinyagentos/opencode_runtime.py[204-207]
tinyagentos/opencode_runtime.py[262-294]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The per-agent cache creates multiple server objects, but every object binds the same fixed port. Allocate a unique, reserved port per cached server and retain it for that server's lifetime.

## Issue Context
Separate home directories do not isolate network listeners. Port allocation must prevent collisions and ensure each server's `base_url` identifies its own process.

## Fix Focus Areas
- tinyagentos/taos_agent_runtime.py[18-21]
- tinyagentos/taos_agent_runtime.py[133-150]
- tinyagentos/opencode_runtime.py[204-207]
- tinyagentos/opencode_runtime.py[262-294]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Agent ID used as model 🐞 Bug ≡ Correctness
Description
The endpoint passes the consented agent identifier directly as the OpenCode/LiteLLM model ID instead
of resolving and executing that agent's configured harness. A normal agent such as agent-a
configured with model gpt-4o therefore requests nonexistent model agent-a and the advertised
agent turn fails.
Code

tinyagentos/routes/agent_model_api.py[234]

+        model_id=agent_id,
Relevance

⭐⭐ Medium

Identifier mismatch appears serious, but closest rejection precedent is from another subsystem.

PR-#1662
PR-#280

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
drive_turn documents and installs model_id into OpenCodeConfig as the backend model
identifier, while normal agent routing resolves an agent record and separately reads its model and
framework. The new endpoint skips that resolution and supplies agent_id directly.

tinyagentos/opencode_runtime.py[339-381]
tinyagentos/agent_chat_router.py[181-190]
tinyagentos/agent_chat_router.py[232-288]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Agent-as-a-Model endpoint passes an agent identifier as the OpenCode/LiteLLM model identifier. Resolve the consented agent and execute its configured framework/runtime with its configured model instead.

## Issue Context
Agent identity and backend model identity are separate concepts. Existing routing resolves an agent record before selecting the framework and configured model.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[180-242]
- tinyagentos/agent_chat_router.py[181-190]
- tinyagentos/agent_chat_router.py[232-288]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Comment contains em dash 📜 Skill insight ✧ Quality
Description
The newly added malformed-content comment contains the prohibited em dash character . The rule
explicitly applies to code comments.
Code

tinyagentos/routes/agent_model_api.py[212]

+                # content is int/null/object — malformed request.
Relevance

⭐⭐⭐ High

Trivial deterministic compliance fix in a newly added comment.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2212258 prohibits Unicode em dashes in code comments. Line 212 contains `content is
int/null/object — malformed request`.

tinyagentos/routes/agent_model_api.py[212-212]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly added code comment uses an em dash prohibited in public-facing text.

## Issue Context
Replace the em dash with a regular hyphen or rewrite the sentence without changing behavior.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[212-212]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Degraded state map lost ✓ Resolved 🐞 Bug ☼ Reliability
Description
When taos_opencode_born_degraded is absent, the function mutates a temporary dictionary without
assigning it to app_state. A server created before LiteLLM is ready therefore loses its degraded
flag, so later calls skip the intended rebuild and retain stale key/model configuration.
Code

tinyagentos/taos_agent_runtime.py[54]

+    born_degraded = getattr(app_state, "taos_opencode_born_degraded", {})
Relevance

⭐⭐⭐ High

Clear deterministic state-loss bug; persistence consistency fixes are historically accepted.

PR-#1542

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Unlike the server and session dictionaries, the default degraded-state dictionary is never stored on
app_state. The later self-healing condition reads a newly created empty dictionary and cannot see
the flag written during the prior call.

tinyagentos/taos_agent_runtime.py[44-60]
tinyagentos/taos_agent_runtime.py[71-74]
tinyagentos/taos_agent_runtime.py[145-146]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Initialize and attach the per-model degraded-state dictionary to app state before reading or mutating it.

## Issue Context
The servers and sessions dictionaries use explicit initialization, but the degraded-state dictionary uses an unattached default. The self-healing branch depends on this state surviving between calls.

## Fix Focus Areas
- tinyagentos/taos_agent_runtime.py[44-60]
- tinyagentos/taos_agent_runtime.py[71-74]
- tinyagentos/taos_agent_runtime.py[145-146]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Non-boolean stream accepted 🐞 Bug ≡ Correctness
Description
body.get("stream") is True silently treats strings, numbers, objects, and null as non-streaming
instead of rejecting them. This violates the PR's explicit JSON-boolean contract and returns
successful completions for malformed requests.
Code

tinyagentos/routes/agent_model_api.py[152]

+    stream = body.get("stream") is True
Relevance

⭐⭐⭐ High

Explicit contract and accepted precedents favor rejecting malformed JSON types.

PR-#1542
PR-#269

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changelog says stream requires an explicit JSON boolean, but the implementation merely tests
identity with True, accepting every other JSON type as the false branch.

CHANGELOG.md[22-27]
tinyagentos/routes/agent_model_api.py[144-168]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Validate that a present `stream` field is a JSON boolean and return an OpenAI-shaped 400 response for every other type.

## Issue Context
An absent field may retain the non-streaming default, but present strings, numbers, null, arrays, and objects must not be silently coerced to false.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[119-156]
- tests/test_routes_agent_model_api.py[89-195]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

9. Completion response bypasses Pydantic 📜 Skill insight ✧ Quality
Description
The new completion response is assembled directly as a raw dictionary in JSONResponse, without a
Pydantic response schema. This bypasses response validation and generated API schema guarantees.
Code

tinyagentos/routes/agent_model_api.py[R247-258]

+    return JSONResponse({
+        "id": f"chatcmpl-{_short_id()}",
+        "object": "chat.completion",
+        "created": int(time.time()),
+        "model": model,
+        "choices": [{
+            "index": 0,
+            "message": {"role": "assistant", "content": content},
+            "finish_reason": "stop",
+        }],
+        "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
+    })
Relevance

⭐ Low

PR #2122 specifically rejected requiring Pydantic response models for raw route responses.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2185155 requires route response payloads to use Pydantic models. The branch code
constructs the successful completion payload directly as a dictionary passed to JSONResponse.

tinyagentos/routes/agent_model_api.py[245-258]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new chat completion response uses a raw dictionary instead of a Pydantic response model.

## Issue Context
Define appropriate OpenAI completion response models and use them for response construction and route schema declaration while preserving the existing error envelope behavior.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[93-169]
- tinyagentos/routes/agent_model_api.py[245-269]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread tinyagentos/taos_agent_runtime.py Outdated

data_dir = getattr(app_state, "data_dir", None)
home = str(data_dir / "taos-agent-opencode") if data_dir else "taos-agent-opencode"
home = str(data_dir / f"taos-agent-opencode-{model}") if data_dir else f"taos-agent-opencode-{model}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Opencode data remains trackable 📜 Skill insight ≡ Correctness

The new per-model opencode home writes runtime files beneath data/taos-agent-opencode-*, but
.gitignore contains no matching entry. Generated runtime state can therefore be committed
accidentally.
Agent Prompt
## Issue description
Per-model opencode runtime directories under `data/` are not ignored by Git.

## Issue Context
Add an ignore pattern covering every dynamically named `taos-agent-opencode-*` directory without ignoring unrelated data.

## Fix Focus Areas
- .gitignore[18-35]
- tinyagentos/taos_agent_runtime.py[133-134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

parts.append(p)
user_text = " ".join(parts)
else:
# content is int/null/object — malformed request.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Comment contains em dash 📜 Skill insight ✧ Quality

The newly added malformed-content comment contains the prohibited em dash character . The rule
explicitly applies to code comments.
Agent Prompt
## Issue description
A newly added code comment uses an em dash prohibited in public-facing text.

## Issue Context
Replace the em dash with a regular hyphen or rewrite the sentence without changing behavior.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[212-212]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

trace_id=None,
sink=_sink,
base_url=server.base_url,
model_id=agent_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Agent id used as model 🐞 Bug ≡ Correctness

The endpoint passes the consented agent identifier directly as the OpenCode/LiteLLM model ID instead
of resolving and executing that agent's configured harness. A normal agent such as agent-a
configured with model gpt-4o therefore requests nonexistent model agent-a and the advertised
agent turn fails.
Agent Prompt
## Issue description
The Agent-as-a-Model endpoint passes an agent identifier as the OpenCode/LiteLLM model identifier. Resolve the consented agent and execute its configured framework/runtime with its configured model instead.

## Issue Context
Agent identity and backend model identity are separate concepts. Existing routing resolves an agent record before selecting the framework and configured model.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[180-242]
- tinyagentos/agent_chat_router.py[181-190]
- tinyagentos/agent_chat_router.py[232-288]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/taos_agent_runtime.py
Comment on lines +44 to +47
servers = getattr(app_state, "taos_opencode_servers", None)
if servers is None:
servers = {}
app_state.taos_opencode_servers = servers

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

6. Cold-start cache races 🐞 Bug ☼ Reliability

The new server cache performs an unsynchronized check-then-create sequence containing multiple
awaits, so concurrent first requests for one model can create and start duplicate servers before
overwriting the same cache entry. This can leak an untracked process, duplicate key operations, or
fail both turns through home and port contention.
Agent Prompt
## Issue description
Concurrent cold starts can both observe a cache miss and initialize the same model. Add per-model synchronization or cache a shared initialization task, including cleanup on failed initialization.

## Issue Context
The cache lookup occurs before several awaited settings and key operations, and insertion happens only afterward. Different models may initialize concurrently, but one model must have exactly one in-flight initialization.

## Fix Focus Areas
- tinyagentos/taos_agent_runtime.py[41-53]
- tinyagentos/taos_agent_runtime.py[76-150]
- tinyagentos/taos_agent_runtime.py[163-182]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/taos_agent_runtime.py Outdated
Comment on lines +190 to +191
# The last user message is the prompt; system/earlier messages are context
# the agent harness already carries per-turn, so we pass the latest user text.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

8. Conversation context is discarded 🐞 Bug ≡ Correctness

_run_agent_turn forwards only the last user message and drops every system, assistant, and earlier
user message from the OpenAI request. Because drive_turn constructs a fresh adapter and session
for this invocation, the omitted messages are unavailable and multi-turn prompts or system
instructions produce incorrect responses.
Agent Prompt
## Issue description
Preserve the supplied OpenAI message history, including system instructions and prior assistant/user turns, when driving the agent turn.

## Issue Context
The current implementation reduces the request to one string while the runtime creates a new adapter session. Convert supported content parts without dropping message roles or earlier turns.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[180-237]
- tinyagentos/opencode_runtime.py[377-399]
- tinyagentos/agent_chat_router.py[165-224]
- tinyagentos/agent_chat_router.py[272-285]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# opencode server, so we resolve it the same way the chat endpoint does.
# `stream` must be an explicit JSON boolean; a string like "false" must
# NOT coerce to True (bool("false") is True). Kilo finding.
stream = body.get("stream") is True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

9. Non-boolean stream accepted 🐞 Bug ≡ Correctness

body.get("stream") is True silently treats strings, numbers, objects, and null as non-streaming
instead of rejecting them. This violates the PR's explicit JSON-boolean contract and returns
successful completions for malformed requests.
Agent Prompt
## Issue description
Validate that a present `stream` field is a JSON boolean and return an OpenAI-shaped 400 response for every other type.

## Issue Context
An absent field may retain the non-streaming default, but present strings, numbers, null, arrays, and objects must not be silently coerced to false.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[119-156]
- tests/test_routes_agent_model_api.py[89-195]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@hognek red on a genuine conflict-resolution miss, not infra. Two failures in tests/test_taos_agent_chat.py: AttributeError, SimpleNamespace has no attribute taos_opencode_born_degraded. That state field landed on dev while #2176 sat conflicted; your rebase kept the old app.state initialization and dropped it. Restore dev's born-degraded plumbing in the rebased initialization path (diff your branch's app-state setup against origin/dev to find what else the resolution lost), rerun those two tests locally, push. Same branch, fix-forward. Kilo red is rate-limit, ignore it. Once green with a real bot review I verify against the #2176 conditions (my earlier re-review there listed the substance as met) and merge; #2176 closes superseded at that point, not before.

The rebase onto origin/dev dropped the app_state attachment for the
per-model taos_opencode_born_degraded dict.  getattr(..., {}) created a
temporary dict that was never stored on app_state, so downstream code
mutated a throwaway and tests saw AttributeError on SimpleNamespace.

Fix: initialize born_degraded with the same None-guard pattern used
for servers and sessions, attaching the fresh dict to app_state.
Update the two degraded-birth/self-heal tests to use dict access
(taos_opencode_born_degraded['gpt-4o']) now that the flag is per-model.
@hognek

hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Fixed: the rebase dropped app_state.taos_opencode_born_degraded dict initialization. The getattr(..., {}) created a temporary dict never attached to app_state. Now initialized with the same None-guard pattern as servers and sessions. Two degraded-birth/self-heal tests updated to dict access (["gpt-4o"]). All 13 tests in test_taos_agent_chat.py pass locally.

app_state.taos_opencode_sessions = sessions

existing: OpenCodeServer | None = servers.get(model)
born_degraded = getattr(app_state, "taos_opencode_born_degraded", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: born_degraded = True at line 118 overwrites this dict with a boolean, causing AttributeError at line 149

born_degraded is correctly initialized as a dict here, but the legacy born_degraded = True assignment at line 118 (inside the if stored_key: re-scoping block) overwrites the dict with a bare boolean when key re-scoping returns False. Line 149 then calls born_degraded.get(model, False), which crashes with AttributeError: 'bool' object has no attribute 'get'.

Suggested change
born_degraded = getattr(app_state, "taos_opencode_born_degraded", None)
born_degraded[model] = True

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 file)
  • tinyagentos/routes/agent_model_keys.py
Previous Review Summaries (3 snapshots, latest commit 5e29bbc)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 5e29bbc)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 file)
  • tests/test_agent_model_keys_route.py - 0 issues

Previous review (commit 8b20b6d)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • tinyagentos/taos_agent_runtime.py
  • tinyagentos/routes/agent_model_api.py
  • tinyagentos/routes/agent_model_keys.py

Previous review (commit b556457)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/taos_agent_runtime.py 54 born_degraded = True at line 118 overwrites the per-model dict with a boolean when key re-scoping fails, causing AttributeError at line 149 when born_degraded.get(model, False) is called
Files Reviewed (3 files)
  • tinyagentos/taos_agent_runtime.py - 1 issue
  • tinyagentos/routes/agent_model_api.py
  • tinyagentos/routes/taos_agent.py

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 85.1K · Output: 20.6K · Cached: 798.7K

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
tinyagentos/taos_agent_runtime.py (1)

105-149: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

born_degraded = True reassigns the dict, crashing on the next born_degraded[model] = ....

At line 118, on the "re-scoping stored key failed/returned False" branch, the local name born_degraded (the per-model dict from line 54-57) is reassigned to the boolean True. This isn't inside the surrounding try for that specific statement's later use — it's outside the try/except at line 149 (born_degraded[model] = born_degraded.get(model, False) or born_degraded_now), so any request that reaches this branch now raises TypeError: 'bool' object does not support item assignment, turning a "key scope may be stale" warning into an unhandled crash inside ensure_taos_opencode_server (surfacing as a 502/503 to callers instead of the intended graceful degraded-mode continuation).

This looks like leftover code from the pre-refactor singleton-boolean pattern that wasn't updated for the new per-model dict.

🐛 Proposed fix
                     if not rescoped:
                         logger.warning(
                             "taos_agent_runtime: re-scoping the taOS agent key returned False "
                             "(key scope may be stale)"
                         )
-                        born_degraded = True
+                        born_degraded[model] = True

Would you like a regression test added for the update_agent_key returns-False path?

🤖 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 `@tinyagentos/taos_agent_runtime.py` around lines 105 - 149, In the stored-key
re-scoping branch of ensure_taos_opencode_server, replace the boolean
reassignment of born_degraded with an update to the existing per-model
degraded-state tracking, preserving the later born_degraded[model] assignment
and graceful continuation when update_agent_key returns False. Do not overwrite
the born_degraded dictionary.
🧹 Nitpick comments (1)
tinyagentos/routes/agent_model_api.py (1)

94-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale docstring: still describes the old 501 behavior.

The docstring says "a contract-valid request returns 501 rather than a fabricated completion," but the code right below it (143-170) now actually drives a real agent turn and returns a completion. Worth updating to avoid confusing future readers.

📝 Proposed fix
-    Enforces the consent contract: a valid key is required, and the requested
-    model must be one of the agents that key is consented for. Running the turn
-    through the agent's harness is the next slice (pending the seam choice), so a
-    contract-valid request returns 501 rather than a fabricated completion.
+    Enforces the consent contract: a valid key is required, and the requested
+    model must be one of the agents that key is consented for. A contract-valid
+    request drives one non-streaming agent turn through the opencode
+    host-server seam and returns an OpenAI ChatCompletion envelope.
🤖 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 `@tinyagentos/routes/agent_model_api.py` around lines 94 - 106, Update the
chat_completions docstring to describe the current behavior: after consent
validation, the request is routed through the agent harness and returns a real
completion. Remove the outdated statement that contract-valid requests return
501.
🤖 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 `@tinyagentos/taos_agent_runtime.py`:
- Around line 148-158: Update the session propagation logic in
ensure_taos_opencode_server so app_state.taos_opencode_session_id is assigned
the current model’s session_id on every path, including when sessions.get(model)
returns None. Preserve the existing cached-session value when present, and
explicitly clear the legacy singleton field for models without a cached session
to prevent cross-model leakage.
- Line 137: Sanitize or validate the model/agent identifier in
ensure_taos_opencode_server before constructing the home path, rejecting path
separators and traversal segments or normalizing them to a safe directory-name
form. Apply this consistently to values originating from SettingsPatch.model,
PermittedModelsUpdate.models, and MintIn.agent_ids while preserving valid
identifiers and the existing OpenCodeServerConfig path behavior.

---

Outside diff comments:
In `@tinyagentos/taos_agent_runtime.py`:
- Around line 105-149: In the stored-key re-scoping branch of
ensure_taos_opencode_server, replace the boolean reassignment of born_degraded
with an update to the existing per-model degraded-state tracking, preserving the
later born_degraded[model] assignment and graceful continuation when
update_agent_key returns False. Do not overwrite the born_degraded dictionary.

---

Nitpick comments:
In `@tinyagentos/routes/agent_model_api.py`:
- Around line 94-106: Update the chat_completions docstring to describe the
current behavior: after consent validation, the request is routed through the
agent harness and returns a real completion. Remove the outdated statement that
contract-valid requests return 501.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e0b8eb5f-ab8d-49e6-b131-1d43a792fce2

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6dcaa and b556457.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/design/2026-07-17-gpu-work-queue.md
  • tests/test_routes_agent_model_api.py
  • tests/test_taos_agent_chat.py
  • tinyagentos/routes/agent_model_api.py
  • tinyagentos/routes/taos_agent.py
  • tinyagentos/taos_agent_runtime.py

Comment thread tinyagentos/taos_agent_runtime.py Outdated
Comment thread tinyagentos/taos_agent_runtime.py Outdated
@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Rebase verified clean: range-diff shows 2176's substance carried intact, the born-degraded dict fix is correct, both tests pass on your tree, and nothing from dev is lost. Two substance items before merge, both carried over FROM 2176, one of which is on me:

  1. CRITICAL, one line: taos_agent_runtime.py:118 in the rescope-failure branch does born_degraded = True, rebinding the per-model dict to a bool; line 149 then calls .get() on it and the whole chat request 500s. Should be born_degraded_now = True. Kilo's review of your head flagged exactly this while its check still shows pass, which is why the check is not the gate. Add a rescope-failure test while you are there; nothing covers that path.

  2. Correcting my own 00:56 approval: I credited this slice with stopping and popping the old server on model change. It does not; those lines are the self-heal branch only, and the PR deletes dev's stop-on-model-change block. With every per-model server on the same fixed port, a second model's server can fail to bind while the first answers the health check, so the new model's turns route silently to the old server's config and key. Restore the stop-on-model-change semantics (simplest, matches dev) rather than inventing per-model ports in this slice.

Adjudicated so you do not chase ghosts: qodo bug 3 (cold-start double-spawn race) real but minor, follow-up; bug 5 unconfirmed and counter-evidenced by the live round-trip; bug 8 cosmetic. Bug 2 (multi-turn context discarded, fresh session per call) is REAL and gets its own card; do not fold it here.

Both fixes on this branch, then green + a real bot pass on that head and this merges with #2176 closing superseded.

… stop-on-model-change

- Line 118: born_degraded = True rebinds the per-model dict to a bool,
  causing .get() at line 149 to raise AttributeError. Fixed to
  born_degraded_now = True.
- Restore stop-on-model-change semantics: all per-model servers share
  TAOS_OPENCODE_PORT, so stop other models' servers before starting a
  new one.
- Add test_ensure_server_rescope_failure_marks_degraded covering the
  rescope-failure path (update_agent_key returns False).
@hognek

hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Both items fixed on the same branch (d35c763):

  1. CRITICAL — born_degraded dict-to-bool rebind: taos_agent_runtime.py:118 changed born_degraded = Trueborn_degraded_now = True. The old code rebound the per-model dict to a bool, so .get() at line 149 raised AttributeError on every chat request. Added test_ensure_server_rescope_failure_marks_degraded covering the rescope-failure path (update_agent_key returns False).

  2. Stop-on-model-change restored: Added block at if existing is None: that stops all other models' servers before starting a new one. All per-model servers share TAOS_OPENCODE_PORT, so without this a second model's server fails to bind while the first answers health checks.

Targeted tests: 14/14 pass (including new rescope-failure test). Full gate running.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Item (a) is fixed and properly tested: born_degraded_now at line 134, the per-model dict is never rebound, and the new rescope-failure test drives update_agent_key False and asserts the dict entry. 27 tests pass on your tree.

Item (b) is HALF restored, and the missing half is user-facing. The reaping loop at lines 80-94 correctly stops and pops other-model servers so the shared port is free, which closes the bind-collision concern. But dev also cleared the legacy session id on model change and that was not restored:

dev did: taos_opencode_server = None AND taos_opencode_session_id = None
yours: servers.pop(other_model), sessions.pop(other_model), session id untouched

Line 166 only reassigns the legacy attr if session_id is not None, so on a first-use model sessions.get(model) is None and the attr keeps the PREVIOUS model's session id. routes/taos_agent.py:440 feeds that attr into adapter.session_id, and opencode_adapter.ensure_session() returns a preset id WITHOUT validating it exists. Each model has its own opencode home, so the stale id does not exist on the new server: the first desktop chat turn after a model switch drives a nonexistent session, and line 476 writes the stale id straight back so it never self-clears. This is CodeRabbit's Major on taos_agent_runtime.py:174 and I adjudicate it correct.

FIX: clear the legacy session id in the reaping loop AND in the self-heal branch, or make line 166 an unconditional assignment from sessions.get(model). Add the stop-on-model-change test that switches models and asserts the attr is cleared; there is currently no such test at all.

Not in scope here, already carded or noted: multi-turn context (tsk-qb3f23), concurrent different-model cold-start race, agent-id-as-model, non-boolean stream. Do not fold them.

Merge gate note: no bot has reviewed d35c763 either; the comments carrying that SHA are re-anchored older reviews. Re-triggering.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 47 minutes.

Two changes:
1. Reaping loop (line 94): after stopping+polling other-model servers for
   port binding, explicitly set app_state.taos_opencode_session_id = None
   so the desktop chat path does not feed a stale session into the new server.
2. Line 170-174: make the legacy attr assignment unconditional —
   sessions.get(model) may be None for a first-use model, and the guard
   previously kept the stale session from the previous model alive.

Added test_ensure_server_model_switch_clears_legacy_session_id that
switches gpt-4o -> claude-sonnet and asserts the legacy attr is None.

15/15 tests pass in test_taos_agent_chat.py.
@hognek

hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Item (b) stop-on-model-change: legacy session id now cleared on model change (9b0482e).

Two changes in taos_agent_runtime.py:

  1. Reaping loop (line 94): After stopping+popping other-model servers, explicitly set app_state.taos_opencode_session_id = None so the desktop chat path does not feed a stale session into the new server.
  2. Line 170-174: Made the legacy attr assignment unconditional — sessions.get(model) may be None for a first-use model, and the if session_id is not None guard previously kept the stale session from the previous model alive.

New test: test_ensure_server_model_switch_clears_legacy_session_id — switches gpt-4o → claude-sonnet, asserts old server stopped, old entries popped from servers+sessions, and taos_opencode_session_id is None.

15/15 test_taos_agent_chat.py pass.

@hognek

hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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 `@tests/test_taos_agent_chat.py`:
- Around line 481-505: The test currently treats a persistent update_agent_key
failure as born degraded, causing subsequent ensure_taos_opencode_server calls
to recreate the shared server. Update the degradation handling around
ensure_taos_opencode_server to distinguish proxy-startup degradation from
routing-only key-scope failure, preserving the cached server when the proxy is
already running; add a second ensure call and assert it does not restart the
server while retaining the persisted key.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e1b42d83-7c5e-459c-a3b8-9730e93f93dc

📥 Commits

Reviewing files that changed from the base of the PR and between b556457 and 9b0482e.

📒 Files selected for processing (2)
  • tests/test_taos_agent_chat.py
  • tinyagentos/taos_agent_runtime.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tinyagentos/taos_agent_runtime.py

Comment thread tests/test_taos_agent_chat.py

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tinyagentos/routes/agent_model_api.py (1)

28-35: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Narrow the import guard's exception type.

Catching bare Exception around these two imports means any genuine bug inside opencode_runtime/taos_agent_runtime (not just "module absent") silently degrades drive_turn/ensure_taos_opencode_server to None. The failure then only resurfaces later as a TypeError: 'NoneType' object is not callable inside _run_agent_turn, further masked by the outer except Exception in chat_completions into a generic 502 — making the real cause very hard to diagnose in production.

♻️ Proposed fix
 try:
     from tinyagentos.opencode_runtime import drive_turn
-except Exception:  # pragma: no cover - import shape guard
+except ImportError:  # pragma: no cover - import shape guard
     drive_turn = None
 try:
     from tinyagentos.taos_agent_runtime import ensure_taos_opencode_server
-except Exception:  # pragma: no cover - import shape guard
+except ImportError:  # pragma: no cover - import shape guard
     ensure_taos_opencode_server = None
🤖 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 `@tinyagentos/routes/agent_model_api.py` around lines 28 - 35, Replace the
broad Exception handlers around the drive_turn and ensure_taos_opencode_server
imports with ImportError-specific guards, preserving the None fallback only when
the modules cannot be imported. Allow other exceptions raised while importing
opencode_runtime or taos_agent_runtime to propagate so the underlying
initialization failure remains visible.

Source: Linters/SAST tools

🤖 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 `@tinyagentos/taos_agent_runtime.py`:
- Around line 167-178: Reset the degraded-state flag when rebuilding an evicted
model: in the server-creation path around `servers[model]` and
`born_degraded[model]`, assign `born_degraded[model]` from the current
`born_degraded_now` value instead of OR-ing with the cached flag. Also remove
`born_degraded[other_model]` when the model-switch eviction loop removes that
model’s server and session, and add regression coverage for switching away from
and back to a previously born-degraded model.

---

Nitpick comments:
In `@tinyagentos/routes/agent_model_api.py`:
- Around line 28-35: Replace the broad Exception handlers around the drive_turn
and ensure_taos_opencode_server imports with ImportError-specific guards,
preserving the None fallback only when the modules cannot be imported. Allow
other exceptions raised while importing opencode_runtime or taos_agent_runtime
to propagate so the underlying initialization failure remains visible.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 00b2cace-41aa-4c53-b532-786ebd4cfdcc

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6dcaa and 9b0482e.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/design/2026-07-17-gpu-work-queue.md
  • tests/test_routes_agent_model_api.py
  • tests/test_taos_agent_chat.py
  • tinyagentos/routes/agent_model_api.py
  • tinyagentos/routes/taos_agent.py
  • tinyagentos/taos_agent_runtime.py

Comment thread tinyagentos/taos_agent_runtime.py
…tion leak

Three fixes targeting CodeRabbit Major findings (PR jaylfc#2195, review e1b42d83):

1. Remove born_degraded_now=True when update_agent_key returns False
   while proxy IS running — this is a routing-only key-scope no-op,
   not a proxy-startup degradation. Marking it degraded caused every
   ensure_taos_opencode_server call to stop+recreate the server,
   defeating per-model caching.

2. Pop born_degraded[other_model] in the stop-on-model-change eviction
   loop (alongside servers+sessions). Without this, an evicted model
   later re-requested picks up the stale True flag and triggers a
   spurious self-heal rebuild cycle.

3. born_degraded[model] = born_degraded_now (no OR with stale flag).
   When building a fresh server, only the current proxy status matters
   — OR-ing with born_degraded.get(model, False) preserved a stale
   True from before eviction forever.

Test: renamed test_ensure_server_rescope_failure_marks_degraded to
test_ensure_server_rescope_failure_keeps_cached_server — now asserts
degraded=False when proxy running, and verifies second ensure call
reuses the cached server (no restart churn).
@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Addendum to the earlier fix list, one NEW blocking item found while adjudicating the bot reviews against head 2a93529. The two routed items (born_degraded, legacy session id) are confirmed FIXED at head; this is separate and introduced by this PR.

Path traversal in the opencode home path. taos_agent_runtime.py:156 builds home = data_dir / f"taos-agent-opencode-{model}"; dev uses a fixed "taos-agent-opencode" name, so the interpolation is new here. The chain: agent_model_api.py:126-133 only checks model in binding.agent_ids; mint accepts arbitrary strings (routes/agent_model_keys.py:28 MintIn has no format check, agent_model_key_store.py:108 checks non-emptiness only); opencode_runtime.py:175-176 then mkdirs that home and writes opencode.json containing the LiteLLM key into it. Net effect: any authenticated user can mint a key with agent_ids=["../../x"] and drive config writes outside data_dir.

Fix both ends: validate agent_ids format at mint (reject path separators and dot segments; a strict slug pattern is fine) AND sanitize at the path-build site so the runtime does not trust the store. Note also that legitimate LiteLLM model ids contain / (openai/gpt-4o), so even honest input currently creates unintended nested dirs; slugify the component (replace non [A-Za-z0-9._-]) rather than assuming it is flat. Add a red-first test that a traversal-shaped model id is rejected and produces no filesystem write outside data_dir.

For the avoidance of doubt: E (update_agent_key churn) and F (born_degraded OR/eviction) from the 19:0xZ reviews are already fixed at head, do not rework them. D and G are nits, ignore. A full CodeRabbit review has been forced on the current head; expect one more pass of findings after it lands.

@coderabbitai coderabbitai 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.

Caution

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

⚠️ Outside diff range comments (1)
tinyagentos/routes/agent_model_api.py (1)

111-117: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Inconsistent error.code for the same 400 validation category.

The pre-turn validation path uses code="invalid_request_error" (line 116), while _BadRequest raised from _run_agent_turn (missing/malformed user message) uses code="invalid_request" (line 156). Both are client-side 400s in the "invalid request" family; a caller branching on error.code will see two different strings for functionally the same failure class.

🔧 Proposed fix (pick one convention)
     except _BadRequest as e:
-        return _openai_error(str(e), type="invalid_request_error", code="invalid_request", status=400)
+        return _openai_error(str(e), type="invalid_request_error", code="invalid_request_error", status=400)

Note: tests/test_routes_agent_model_api.py asserts code == "invalid_request" (line 195) and would need updating alongside this fix.

Also applies to: 155-156

🤖 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 `@tinyagentos/routes/agent_model_api.py` around lines 111 - 117, Standardize
the 400 validation error code across the pre-turn validation path and
_BadRequest handling in _run_agent_turn, choosing one consistent invalid-request
convention and applying it to both _bad_request and the raised error. Update the
affected assertion in tests/test_routes_agent_model_api.py to match the chosen
code.</code>
🧹 Nitpick comments (5)
tinyagentos/taos_agent_runtime.py (3)

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

Unused loop variable model in stop_taos_opencode_server.

Flagged by Ruff (B007); model isn't referenced in the loop body.

🧹 Proposed fix
-    for model, server in list(servers.items()):
+    for _model, server in list(servers.items()):
🤖 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 `@tinyagentos/taos_agent_runtime.py` around lines 186 - 205, Update the loop in
stop_taos_opencode_server to avoid binding the unused model key, while
preserving iteration over all server values and the existing stop and cleanup
behavior.

Source: Linters/SAST tools


21-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring still describes the old singleton, not the per-model cache.

Says "Stores the server on app_state.taos_opencode_server" and "the old server is stopped and a new one is created" — this file now maintains app_state.taos_opencode_servers/taos_opencode_sessions/taos_opencode_born_degraded as per-model dicts. Worth updating so the doc matches the actual per-model/self-heal contract.

📝 Proposed docstring update
-    Stores the server on ``app_state.taos_opencode_server``.  If the model
-    changed since last start the old server is stopped and a new one is
-    created so the LiteLLM provider config and key scope track the chosen model.
+    Stores the server on ``app_state.taos_opencode_servers[model]`` (one
+    cached server per model/agent_id).  Since all cached servers share
+    ``TAOS_OPENCODE_PORT``, requesting a different model stops any other
+    cached server before starting the new one.
🤖 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 `@tinyagentos/taos_agent_runtime.py` around lines 21 - 36, Update the
ensure_taos_opencode_server docstring to describe the per-model state stored in
app_state.taos_opencode_servers, taos_opencode_sessions, and
taos_opencode_born_degraded, including per-model replacement and born-degraded
self-healing behavior. Remove the outdated singleton wording and preserve the
documented permitted_models key scoping and return contract.

155-157: 🚀 Performance & Scalability | 🔵 Trivial

Per-model home directories are never cleaned up.

Each distinct model/agent_id now gets its own persisted taos-agent-opencode-{model} directory (previously one shared dir). Stop-on-model-change/eviction only stops the process — it doesn't remove the directory, so long-running deployments with many rotating agent_ids will accumulate opencode state dirs indefinitely.

🤖 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 `@tinyagentos/taos_agent_runtime.py` around lines 155 - 157, Update the
per-model home-directory lifecycle around the home path construction in the
agent runtime so directories created for stopped, changed, or evicted
model/agent_id processes are removed during cleanup. Ensure cleanup targets only
the corresponding persisted taos-agent-opencode-{model} directory after its
process stops, while preserving the current directory selection and
active-process behavior.
tinyagentos/routes/agent_model_api.py (2)

94-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring is stale: says the endpoint "returns 501," but it now executes a real turn.

This whole slice's point is to replace the 501 stub with actual execution (lines 143-169). The docstring should reflect that this now drives a real turn and describe the resulting error mapping instead.

🤖 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 `@tinyagentos/routes/agent_model_api.py` around lines 94 - 106, The
chat_completions docstring still describes the removed 501 stub. Update it to
state that contract-valid requests execute a real agent turn, and document the
endpoint’s resulting error mapping instead, while retaining the existing
authentication and manual body-parsing behavior.

261-269: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

SSE stream combines content delta and terminal finish_reason in one chunk; also re-imports StreamingResponse unnecessarily.

Standard OpenAI streaming clients expect content chunk(s) with finish_reason: null followed by a separate terminal chunk carrying finish_reason with an empty delta. Bundling both in a single chunk may not parse cleanly in strict clients. Separately, StreamingResponse is already imported at the module level (line 24); the local import at line 263 is redundant.

♻️ Proposed fix
 def _chat_completion_stream(content: str, model: str):
     """OpenAI SSE stream envelope (single completion chunk)."""
-    from fastapi.responses import StreamingResponse
-
+    completion_id = f"chatcmpl-{_short_id()}"
+    created = int(time.time())
     def _gen():
-        yield f"data: {json.dumps({'id': f'chatcmpl-{_short_id()}', 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': model, 'choices': [{'index': 0, 'delta': {'role': 'assistant', 'content': content}, 'finish_reason': 'stop'}]})}\n\n"
+        yield f"data: {json.dumps({'id': completion_id, 'object': 'chat.completion.chunk', 'created': created, 'model': model, 'choices': [{'index': 0, 'delta': {'role': 'assistant', 'content': content}, 'finish_reason': None}]})}\n\n"
+        yield f"data: {json.dumps({'id': completion_id, 'object': 'chat.completion.chunk', 'created': created, 'model': model, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
         yield "data: [DONE]\n\n"
 
     return StreamingResponse(_gen(), media_type="text/event-stream")
🤖 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 `@tinyagentos/routes/agent_model_api.py` around lines 261 - 269, Update
_chat_completion_stream so the content chunk uses finish_reason: null, then
yield a separate terminal SSE chunk with an empty delta and finish_reason set to
stop before emitting [DONE]. Remove the redundant local StreamingResponse import
and reuse the module-level import.
🤖 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.

Outside diff comments:
In `@tinyagentos/routes/agent_model_api.py`:
- Around line 111-117: Standardize the 400 validation error code across the
pre-turn validation path and _BadRequest handling in _run_agent_turn, choosing
one consistent invalid-request convention and applying it to both _bad_request
and the raised error. Update the affected assertion in
tests/test_routes_agent_model_api.py to match the chosen code.</code>

---

Nitpick comments:
In `@tinyagentos/routes/agent_model_api.py`:
- Around line 94-106: The chat_completions docstring still describes the removed
501 stub. Update it to state that contract-valid requests execute a real agent
turn, and document the endpoint’s resulting error mapping instead, while
retaining the existing authentication and manual body-parsing behavior.
- Around line 261-269: Update _chat_completion_stream so the content chunk uses
finish_reason: null, then yield a separate terminal SSE chunk with an empty
delta and finish_reason set to stop before emitting [DONE]. Remove the redundant
local StreamingResponse import and reuse the module-level import.

In `@tinyagentos/taos_agent_runtime.py`:
- Around line 186-205: Update the loop in stop_taos_opencode_server to avoid
binding the unused model key, while preserving iteration over all server values
and the existing stop and cleanup behavior.
- Around line 21-36: Update the ensure_taos_opencode_server docstring to
describe the per-model state stored in app_state.taos_opencode_servers,
taos_opencode_sessions, and taos_opencode_born_degraded, including per-model
replacement and born-degraded self-healing behavior. Remove the outdated
singleton wording and preserve the documented permitted_models key scoping and
return contract.
- Around line 155-157: Update the per-model home-directory lifecycle around the
home path construction in the agent runtime so directories created for stopped,
changed, or evicted model/agent_id processes are removed during cleanup. Ensure
cleanup targets only the corresponding persisted taos-agent-opencode-{model}
directory after its process stops, while preserving the current directory
selection and active-process behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ef21e7a5-534a-409e-8e90-3c975d9425e6

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6dcaa and 2a93529.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/design/2026-07-17-gpu-work-queue.md
  • tests/test_routes_agent_model_api.py
  • tests/test_taos_agent_chat.py
  • tinyagentos/routes/agent_model_api.py
  • tinyagentos/routes/taos_agent.py
  • tinyagentos/taos_agent_runtime.py

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Adjudicated the full CodeRabbit review on head 2a93529: no new blocking items beyond the path traversal already posted (which CodeRabbit did not find). Its per-model home-dir cleanup point is real; fold that into the traversal fix since it touches the same lines. Everything else (error.code inconsistency, B007 loop var, stale docstrings, single-chunk SSE) is nit-level and optional. The blocking list stands: the traversal addendum plus nothing else.

Validate agent_ids at both the route (Pydantic field_validator) and store
(mint()) layers: reject path separators, backslashes, spaces, and anything
outside the safe slug set [A-Za-z0-9._-].  The Pydantic layer catches it
before the store, the store layer is defense-in-depth for direct callers.

Sanitize model name in opencode home path (taos_agent_runtime.py): replace
non-safe chars with '_' so 'openai/gpt-4o' → 'openai_gpt-4o' and traversal
payloads like '../../x' → '.._.._x' (harmless without real slashes).

Add per-model home-dir cleanup in stop_taos_opencode_server so stale configs
and serve logs don't accumulate across model switches.

Tests: store rejection (7 traversal patterns), route validation (9 patterns),
_safe_path_component unit tests (10 patterns).  All existing taos_agent_chat
tests pass (15/15).

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 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 `@tests/test_agent_model_keys_route.py`:
- Around line 40-54: Update test_mint_allows_safe_agent_ids to assert the
documented store-initialization failure status, rather than merely checking that
the response is not 422. Alternatively, mock the store initialization and verify
the request reaches it after validation, while retaining coverage for both safe
agent IDs.
- Around line 15-27: Add "." and ".." to the rejected-input parametrization in
tests/test_agent_model_keys_route.py lines 15-27 and the corresponding
store-level parametrization in tests/test_agent_model_key_store.py lines
138-160; make no other changes.

In `@tinyagentos/routes/agent_model_keys.py`:
- Around line 27-40: Reject the exact agent IDs "." and ".." in every validation
layer while preserving the existing safe-slug rules. Update _validate_agent_id
in tinyagentos/routes/agent_model_keys.py:27-40, the store regex/validator in
tinyagentos/agent_model_key_store.py:37-39, and ensure mint() at
tinyagentos/agent_model_key_store.py:115-120 validates before persistence so
neither dot segment can be stored.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c58df8c-fefc-4c60-b913-2faeb6e5f36a

📥 Commits

Reviewing files that changed from the base of the PR and between 2a93529 and 8b20b6d.

📒 Files selected for processing (6)
  • tests/test_agent_model_key_store.py
  • tests/test_agent_model_keys_route.py
  • tests/test_taos_agent_config.py
  • tinyagentos/agent_model_key_store.py
  • tinyagentos/routes/agent_model_keys.py
  • tinyagentos/taos_agent_runtime.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tinyagentos/taos_agent_runtime.py

Comment on lines +15 to +27
@pytest.mark.parametrize(
"agent_ids",
[
["../../x"],
["a/b"],
["openai/gpt-4o"], # real-world LiteLLM model name with /
["a\\b"],
["/etc/passwd"],
["valid", "../../escape"],
["a b"],
[""],
],
)

Copy link
Copy Markdown

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

Cover exact dot segments in both regression suites.

  • tests/test_agent_model_keys_route.py#L15-L27: add "." and ".." to the rejected route inputs.
  • tests/test_agent_model_key_store.py#L138-L160: add the same values to the store-level parametrization.
📍 Affects 2 files
  • tests/test_agent_model_keys_route.py#L15-L27 (this comment)
  • tests/test_agent_model_key_store.py#L138-L160
🤖 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 `@tests/test_agent_model_keys_route.py` around lines 15 - 27, Add "." and ".."
to the rejected-input parametrization in tests/test_agent_model_keys_route.py
lines 15-27 and the corresponding store-level parametrization in
tests/test_agent_model_key_store.py lines 138-160; make no other changes.

Comment on lines +40 to +54
async def test_mint_allows_safe_agent_ids(self, client):
"""Valid slug-format agent ids pass Pydantic validation (the store is
reached — a store-init error proves validation passed the 422 gate).
"""
resp = await client.post(
"/api/agent-model-keys",
json={"agent_ids": ["safe-agent-1", "gpt_4o.test"]},
)
# The store is not initialised in the default conftest client, so we
# expect 500 (store error), NOT 422 (validation rejection). A 422
# here would mean the regex is too strict and safe slugs are blocked.
assert resp.status_code != 422, (
f"safe slugs should not be rejected by validation, "
f"got {resp.status_code}: {resp.text}"
)

Copy link
Copy Markdown

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

Assert the expected failure, not merely “not 422.”

!= 422 also passes for unrelated responses such as 401 or 404. Assert the documented store-initialization response, or mock the store and verify that the request reaches it after validation.

🤖 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 `@tests/test_agent_model_keys_route.py` around lines 40 - 54, Update
test_mint_allows_safe_agent_ids to assert the documented store-initialization
failure status, rather than merely checking that the response is not 422.
Alternatively, mock the store initialization and verify the request reaches it
after validation, while retaining coverage for both safe agent IDs.

Comment on lines +27 to +40
# Agent ids are used as filesystem path components (opencode home dirs) and
# LiteLLM model identifiers; reject traversal, dot-segments, and anything
# that isn't a safe slug so a malicious mint can't write configs outside
# the data_dir boundary. Matches the sanitizer in taos_agent_runtime.py.
_AGENT_ID_RE = re.compile(r"^[A-Za-z0-9._-]+$")


def _validate_agent_id(v: str) -> str:
if not _AGENT_ID_RE.match(v):
raise ValueError(
f"agent_id {v!r} contains invalid characters; "
"only A-Z, a-z, 0-9, '.', '_', '-' are allowed"
)
return v

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject exact dot-segment IDs in both validation layers. The shared filesystem-safety contract currently allows "." and "..".

  • tinyagentos/routes/agent_model_keys.py#L27-L40: reject exact dot segments in _validate_agent_id.
  • tinyagentos/agent_model_key_store.py#L37-L39: update the store regex/validator consistently.
  • tinyagentos/agent_model_key_store.py#L115-L120: ensure mint() rejects them before persistence.
📍 Affects 2 files
  • tinyagentos/routes/agent_model_keys.py#L27-L40 (this comment)
  • tinyagentos/agent_model_key_store.py#L37-L39
  • tinyagentos/agent_model_key_store.py#L115-L120
🤖 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 `@tinyagentos/routes/agent_model_keys.py` around lines 27 - 40, Reject the
exact agent IDs "." and ".." in every validation layer while preserving the
existing safe-slug rules. Update _validate_agent_id in
tinyagentos/routes/agent_model_keys.py:27-40, the store regex/validator in
tinyagentos/agent_model_key_store.py:37-39, and ensure mint() at
tinyagentos/agent_model_key_store.py:115-120 validates before persistence so
neither dot segment can be stored.

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Red on 8b20b6d is your new validation tests, not the product change: tests/test_agent_model_keys_route.py::TestAgentModelKeysRouteValidation::test_mint_allows_safe_agent_ids fails with RuntimeError: AgentModelKeyStore not initialised, call init() first (multiple shards, same class). Look at how the sibling tests in that file bootstrap the store fixture and use the same path in the new test class. The traversal hardening itself is the right shape from the diff title; I will run the full probe set (mint ../../x rejected, no filesystem write outside data_dir, legit slash-bearing model ids still work) once CI is green. Fix-forward on this branch.

@hognek

hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Fix-forward pushed at 5e29bbc: added _ensure_agent_model_key_store autouse fixture to test_agent_model_keys_route.py (same pattern as test_routes_agent_model_keys.py and test_routes_agent_model_api.py). All 10 tests in the class pass (3.5s).

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Red on 5e29bbc is down to one test and it is a CONTRACT decision, so making the call here: tests/test_routes_agent_model_keys.py::test_mint_without_agent_ids_returns_400 gets 422 because the traversal hardening moved agent_ids validation into the Pydantic model, and Pydantic failures are 422. That pre-existing test IS the public contract for the mint route (taosctl and the UI consume it), so keep 400: leave MintIn permissive (list[str] as before) and do the format validation in the handler or store layer, returning 400 with a clear error naming the offending id, exactly where the old non-emptiness check lived. Same traversal protection, same red-first tests, no contract change. Do not update the old test to expect 422; a stricter body schema silently changing an error contract is how API consumers break without a changelog entry. Everything else from the addendum stands; re-request review when green.

…dler layer

Revert MintIn to permissive list[str] — remove field_validator that caused
Pydantic to surface 422 ValidationError instead of 400 on empty/invalid
agent_ids.  Move the traversal-format check into the mint_key handler
inside the existing try/except ValueError block so it returns the original
400 contract.  Guards the same surface with the same _validate_agent_id() +
non-emptiness checks.

Fixes test_mint_without_agent_ids_returns_400 regression from PR jaylfc#2195.
@hognek

hognek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Fix pushed: reverted MintIn to permissive list[str] (removed field_validator), moved agent_ids traversal-format validation into the handler inside the existing try/except ValueError block. Same guards (_validate_agent_id() + non-emptiness check), same 400 contract.

Commit: 7309d276 on hognek:tsk-gkh4mi-agent-turn-slice-dev.
Test: test_mint_without_agent_ids_returns_400 — 6/6 pass.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
tests/test_agent_model_keys_route.py (1)

50-60: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update these tests for the handler-level 400 contract.

Validation was moved out of Pydantic and mint_key catches ValueError to return HTTP 400. These tests still expect 422, so every unsafe-ID and empty-list case will fail.

Proposed test update
-        """Pydantic field_validator must reject path traversal before the store."""
+        """Handler validation must reject unsafe IDs before minting."""
...
-        assert resp.status_code == 422, (
+        assert resp.status_code == 400, (
...
-        assert resp.status_code == 422
+        assert resp.status_code == 400

Also applies to: 78-84

🤖 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 `@tests/test_agent_model_keys_route.py` around lines 50 - 60, Update
test_mint_rejects_unsafe_agent_ids and the related empty-list test to expect
HTTP 400 instead of 422, matching mint_key’s ValueError handling after
validation moved out of Pydantic. Preserve the existing request payloads and
response-detail assertions unless they depend specifically on the old validation
status.
🤖 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 `@tests/test_agent_model_keys_route.py`:
- Around line 13-30: Update the _ensure_agent_model_key_store fixture to stop
swallowing exceptions from store.close() during setup and teardown. Let close
failures propagate, or convert the fixture to an async fixture and await init()
and close() directly, ensuring the previous connection is closed before
replacing db_path and teardown failures remain visible.

---

Outside diff comments:
In `@tests/test_agent_model_keys_route.py`:
- Around line 50-60: Update test_mint_rejects_unsafe_agent_ids and the related
empty-list test to expect HTTP 400 instead of 422, matching mint_key’s
ValueError handling after validation moved out of Pydantic. Preserve the
existing request payloads and response-detail assertions unless they depend
specifically on the old validation status.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 517e8cfc-83bc-449f-bff4-21b318b95580

📥 Commits

Reviewing files that changed from the base of the PR and between 8b20b6d and 7309d27.

📒 Files selected for processing (2)
  • tests/test_agent_model_keys_route.py
  • tinyagentos/routes/agent_model_keys.py

Comment on lines +13 to +30
@pytest.fixture(autouse=True)
def _ensure_agent_model_key_store(client, tmp_path_factory):
"""Init app.state.agent_model_keys on a fresh DB; the test client registers
the store but does not run the lifespan that init()s it (production does)."""
store = client._transport.app.state.agent_model_keys
if store._db is not None:
try:
asyncio.get_event_loop().run_until_complete(store.close())
except Exception:
pass
tmp_dir = tmp_path_factory.mktemp("agent_model_keys_route_test")
store.db_path = tmp_dir / "agent_model_keys.db"
asyncio.get_event_loop().run_until_complete(store.init())
yield
try:
asyncio.get_event_loop().run_until_complete(store.close())
except Exception:
pass

Copy link
Copy Markdown

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

Do not silently suppress store-close failures.

If close() fails during setup, the fixture replaces db_path while the previous connection may remain open; teardown can likewise pass while leaking resources. Let cleanup failures surface, or convert this to an async fixture and await init()/close() directly.

🧰 Tools
🪛 Ruff (0.16.0)

[error] 21-22: try-except-pass detected, consider logging the exception

(S110)


[warning] 21-21: Do not catch blind exception: Exception

(BLE001)


[error] 29-30: try-except-pass detected, consider logging the exception

(S110)


[warning] 29-29: Do not catch blind exception: Exception

(BLE001)

🤖 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 `@tests/test_agent_model_keys_route.py` around lines 13 - 30, Update the
_ensure_agent_model_key_store fixture to stop swallowing exceptions from
store.close() during setup and teardown. Let close failures propagate, or
convert the fixture to an async fixture and await init() and close() directly,
ensuring the previous connection is closed before replacing db_path and teardown
failures remain visible.

Source: Linters/SAST tools

@jaylfc

jaylfc commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Red on 7309d27 is the last self-inconsistency and it is your own tests from the previous round: the handler now correctly returns 400 with a clear message (exactly as routed), but TestAgentModelKeysRouteValidation::test_mint_rejects_unsafe_agent_ids still asserts the 422 from when validation lived in Pydantic. Update those parametrized assertions to expect 400 and, while there, assert the error body contains the offending id string, which pins the useful half of the message. The rejection set itself is right, including slash-bearing values like openai/gpt-4o: agent_ids are identity slugs and stay strict at mint; the slash-tolerant slugification belongs only to the runtime path-build for MODEL components, which your 8b20b6d already handles. Once green this PR needs a real bot review on the final head before I run the probe set; request it when CI passes.

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