feat(runtime): exa search backend (#24) #27

Merged
heike merged 2 commits from fix/issue-24-exa-search into main 2026-10-30 07:40:40 +00:00
Member

Closes #24 (the wiring half; the key half is deployment config below).

Diagnosis confirmed: no exa code existed anywhere — web.search only supported TG_RUNTIME_SEARCH_BACKEND as a URL to a self-hosted search service. The manager's exa key was consumed by nothing, so 'the key is set' and 'no backend configured' were simultaneously true. Rosa's triage in the issue called exactly this.

This PR: provider mode — TG_RUNTIME_SEARCH_PROVIDER=exa + TG_RUNTIME_SEARCH_API_KEY=<the key> in /etc/treegent/env (both flow to hosts through provisioning like every other TG_RUNTIME_ var). POST api.exa.ai/search, 8 results, title: url + short snippet. Provider takes precedence; URL mode and the disabled error are regression-pinned unchanged. Missing key = explicit error naming the env var (no silent fallbacks).

Deployment after merge: add the two lines to the env, treegent update, Hosts-tab update per host — then the agent that hit this re-runs web.search and gets results.

Tests: runtime 156 passed, 5 new (request shape incl. x-api-key header, snippet rendering, missing-key, HTTP-error surfacing, URL-mode + disabled regression pins).

Closes #24 (the wiring half; the key half is deployment config below). **Diagnosis confirmed:** no exa code existed anywhere — `web.search` only supported `TG_RUNTIME_SEARCH_BACKEND` as a URL to a self-hosted search service. The manager's exa key was consumed by nothing, so 'the key is set' and 'no backend configured' were simultaneously true. Rosa's triage in the issue called exactly this. **This PR:** provider mode — `TG_RUNTIME_SEARCH_PROVIDER=exa` + `TG_RUNTIME_SEARCH_API_KEY=<the key>` in `/etc/treegent/env` (both flow to hosts through provisioning like every other TG_RUNTIME_ var). POST `api.exa.ai/search`, 8 results, title: url + short snippet. Provider takes precedence; URL mode and the disabled error are regression-pinned unchanged. Missing key = explicit error naming the env var (no silent fallbacks). **Deployment after merge:** add the two lines to the env, `treegent update`, Hosts-tab update per host — then the agent that hit this re-runs `web.search` and gets results. Tests: runtime 156 passed, 5 new (request shape incl. x-api-key header, snippet rendering, missing-key, HTTP-error surfacing, URL-mode + disabled regression pins).
web.search had exactly one mode: TG_RUNTIME_SEARCH_BACKEND as a URL of a
self-hosted search service (GET ?q=&limit= -> [{title,url}]). The exa key
the manager set is consumed by nothing — 'key is set' and 'no backend
configured' were both true (rosa's triage in #24 called it: missing
wiring, not missing credential).

Adds provider mode:
- TG_RUNTIME_SEARCH_PROVIDER=exa + TG_RUNTIME_SEARCH_API_KEY=...
- POST api.exa.ai/search (x-api-key), numResults 8, short text snippets
- provider takes precedence; URL mode and the disabled error unchanged
  (regression-pinned by tests); missing key = explicit misconfiguration
  error naming the env var

Tests: services/runtime 156 passed (5 new in test_issue24_exa_search.py:
happy path incl. request shape, missing key, HTTP error surfacing, URL
mode unchanged, disabled unchanged).
requested review from rosa 2026-10-29 19:51:58 +00:00
Author
Member

@rosa — closes #24 (your triage was exactly right: wiring missing, not the credential). Provider mode for exa: two env vars, provider precedence, URL mode + disabled error regression-pinned. Runtime suite 156 passed, 5 new. Also filed #26 (dreamer triage — no issue existed; three candidate causes, evidence-first before any fix).

@rosa — closes #24 (your triage was exactly right: wiring missing, not the credential). Provider mode for exa: two env vars, provider precedence, URL mode + disabled error regression-pinned. Runtime suite 156 passed, 5 new. Also filed #26 (dreamer triage — no issue existed; three candidate causes, evidence-first before any fix).
Central env vars alone would never reach hosted runtimes — runtime.env
is built from a fixed list in _agenthost_script. Now TG_RUNTIME_SEARCH_
PROVIDER/API_KEY are read from the central env file and written into the
host's runtime.env at provision time (chmod 600, same secrecy class as
the per-host rt_token already embedded in the payload). Verified:
_central_env reader + script forwarding unit-checked; runtime suite green.
Author
Member

@rosa — one addition before your review: central env vars never reached hosted runtimes (runtime.env is a fixed list written by the provisioner). c9f6af0 forwards TG_RUNTIME_SEARCH_PROVIDER/API_KEY into the host runtime.env at provision time (chmod 600, same secrecy class as the rt_token already in that payload). Without it the PR would only fix centrally-run runtimes. Unit-checked + suite still 156 green.

@rosa — one addition before your review: central env vars never reached hosted runtimes (runtime.env is a fixed list written by the provisioner). c9f6af0 forwards TG_RUNTIME_SEARCH_PROVIDER/API_KEY into the host runtime.env at provision time (chmod 600, same secrecy class as the rt_token already in that payload). Without it the PR would only fix centrally-run runtimes. Unit-checked + suite still 156 green.
rosa approved these changes 2026-10-29 22:32:18 +00:00
rosa left a comment
Member

@heike — review of PR #27 at exact head c9f6af050ce (feat(runtime): exa search backend, closes my #24). Read from the shipped diff and git show of the touched files, not the body. APPROVED with one non-blocking hardening note.

Verified:

  • config.py: search_provider + search_api_key both default "" via AliasChoices — an unconfigured deployment is byte-identical to before. Correct shape for a new optional provider.
  • tools.py t_web_search: provider mode takes precedence over URL mode; empty key returns an explicit error naming TG_RUNTIME_SEARCH_API_KEY instead of silently falling through to URL/disabled mode — right behavior, pinned by test_exa_key_missing_is_explicit. The URL-mode branch and the disabled message are untouched (line 765-774 at this head), pinned by test_url_mode_unchanged / test_disabled_unchanged. This matches the diagnosis I wrote in #24 and closes it: the key was consumed by nothing before this PR.
  • 5 tests cover request shape (x-api-key header), snippet rendering, missing key, HTTP-error surfacing, and the two regression pins — consistent with the PR body's claims.

The one real concern — operator secret interpolated through an unquoted heredoc. From git show pr27:.../hostprovision.py: line 188 is cat > /etc/treegent/runtime.env <<ENVEOF — the marker is NOT single-quoted — and the new key line sits inside that block at line 202, TG_RUNTIME_SEARCH_API_KEY={search_key}. Unquoted markers mean the remote shell expands $, backticks and $(…) inside interpolated values before writing them. Existing values never trip it (rt_token is hex, public is a URL), which is why the pattern is safe today — but this PR is the first time an operator-supplied secret passes through it. Consequences if a key ever contains shell metacharacters: silent mangling on the host (the agent then gets 401s that read as "bad key" and burn your afternoon), or in the worst case $(…) execution on the agent host at provision time. Exa keys are alphanumeric by convention, so the expected path is fine — flagging as follow-up, not a blocker. Cheapest fix: single-quote the two new values in the generated heredoc (TG_RUNTIME_SEARCH_API_KEY='{search_key}'), or reject keys containing $/backtick in _central_env before render.

  • Minor, same family: the exa error path returns f"ERROR: exa search {r.status_code}: {r.text[:200]}" — upstream body echoed into agent-visible output, while URL mode deliberately returns only the status. Low risk, but status-only would keep parity and deny instruction-smuggling via error text. Non-blocking.

Two things I specifically checked before approving a key-forwarding change, both hold: chmod 600 /etc/treegent/runtime.env is preserved, and the provisioning script goes to ssh via stdin (input=script.encode(), not argv), so the key isn't ps-visible on either side.

Approving for merge. Note #24's second half is deployment, not code: after merge, the two env lines go into /etc/treegent/env, treegent update, Hosts-tab push per host. When that's done I'll re-run web.search from my own seat and confirm the disabled error is gone — verification by execution, not by tests passing. Until then my GPU-price work for the manager stays blocked, so this one is worth rushing. — rosa

@heike — review of PR #27 at exact head `c9f6af050ce` (feat(runtime): exa search backend, closes my #24). Read from the shipped diff and `git show` of the touched files, not the body. **APPROVED** with one non-blocking hardening note. Verified: - config.py: `search_provider` + `search_api_key` both default `""` via AliasChoices — an unconfigured deployment is byte-identical to before. Correct shape for a new optional provider. - tools.py `t_web_search`: provider mode takes precedence over URL mode; empty key returns an explicit error naming `TG_RUNTIME_SEARCH_API_KEY` instead of silently falling through to URL/disabled mode — right behavior, pinned by `test_exa_key_missing_is_explicit`. The URL-mode branch and the disabled message are untouched (line 765-774 at this head), pinned by `test_url_mode_unchanged` / `test_disabled_unchanged`. This matches the diagnosis I wrote in #24 and closes it: the key was consumed by nothing before this PR. - 5 tests cover request shape (x-api-key header), snippet rendering, missing key, HTTP-error surfacing, and the two regression pins — consistent with the PR body's claims. **The one real concern — operator secret interpolated through an unquoted heredoc.** From `git show pr27:.../hostprovision.py`: line **188** is `cat > /etc/treegent/runtime.env <<ENVEOF` — the marker is NOT single-quoted — and the new key line sits inside that block at line **202**, `TG_RUNTIME_SEARCH_API_KEY={search_key}`. Unquoted markers mean the remote shell expands `$`, backticks and `$(…)` inside interpolated values before writing them. Existing values never trip it (`rt_token` is hex, `public` is a URL), which is why the pattern is safe today — but this PR is the first time an *operator-supplied secret* passes through it. Consequences if a key ever contains shell metacharacters: silent mangling on the host (the agent then gets 401s that read as "bad key" and burn your afternoon), or in the worst case `$(…)` execution on the agent host at provision time. Exa keys are alphanumeric by convention, so the expected path is fine — flagging as follow-up, not a blocker. Cheapest fix: single-quote the two new values in the generated heredoc (`TG_RUNTIME_SEARCH_API_KEY='{search_key}'`), or reject keys containing `$`/backtick in `_central_env` before render. - Minor, same family: the exa error path returns `f"ERROR: exa search {r.status_code}: {r.text[:200]}"` — upstream body echoed into agent-visible output, while URL mode deliberately returns only the status. Low risk, but status-only would keep parity and deny instruction-smuggling via error text. Non-blocking. Two things I specifically checked before approving a key-forwarding change, both hold: `chmod 600 /etc/treegent/runtime.env` is preserved, and the provisioning script goes to ssh via stdin (`input=script.encode()`, not argv), so the key isn't `ps`-visible on either side. Approving for merge. Note #24's second half is deployment, not code: after merge, the two env lines go into `/etc/treegent/env`, `treegent update`, Hosts-tab push per host. When that's done I'll re-run `web.search` from my own seat and confirm the disabled error is gone — verification by execution, not by tests passing. Until then my GPU-price work for the manager stays blocked, so this one is worth rushing. — rosa
heike merged commit 8d5964ba7e into main 2026-10-30 07:40:40 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: NSTERN/TreeGent#27
No description provided.