Skip to content

feat(host): generic app.yaml via resources, teammate host-share, jq, nested wheels - #121

Merged
dgokeeffe merged 14 commits into
mainfrom
feat/omnigent-host-followups-v2
Aug 5, 2026
Merged

feat(host): generic app.yaml via resources, teammate host-share, jq, nested wheels#121
dgokeeffe merged 14 commits into
mainfrom
feat/omnigent-host-followups-v2

Conversation

@dgokeeffe

Copy link
Copy Markdown
Collaborator

Brings forward feat/omnigent-host-followups (13 commits, 2026-07-20) — the last branch in the repo with unlanded work. It never had a PR. Rebuilt against current main rather than merged, for the reason in §1.

1. Generic app.yaml — fixes a §5 violation on main

OMNIGENTS_SERVER_URL and OMNIGENTS_WHEEL_SPEC become valueFrom resource references. docs/agent-instructions.md §5 says these must be commented out or defaulted off before landing on main; main currently carries one workspace's Omnigent server URL and UC Volume path.

The on/off switch becomes "are the resources attached" rather than "did you edit this committed file" — an unresolved valueFrom yields an empty string and omnigents_host_enabled() returns False. Adds attach_omnigent_resources.sh + make attach-omnigent-resources, using SDK create_update with an update_mask so attaching doesn't wipe git_repository.

Only those two vars changed. The branch's own app.yaml was its author's aws-syd workspace — taking it wholesale would have swapped one set of personal values for another (a different DATABRICKS_GATEWAY_HOST), flipped ENABLE_CODEX/ENABLE_GEMINI back on against main's comments explaining why they're off, dropped MLFLOW_TRACING_ENABLED / CLAUDE_CODE_OTEL_ENABLED / PROXY_TRACE_CONTENT / CLAUDE_INSTALL_METHOD, and removed CODA_DISABLE_OWNER_CHECK. I diffed the env var set against main to confirm nothing else moved.

CHALLENGE_REPO_URL still holds a personal sandbox URL on main — left alone to keep this reviewable, worth the same treatment separately.

2. Teammate host-share

POST /api/omnigent-host/share accepts an optional grant_user in the body, so the owner can share the SP-owned host with a teammate. Previously it could only self-share. This is exactly the gap §5 describes: "An SP-owned host is invisible in a human's personal picker unless shared via PUT /v1/hosts/{id}/permissions/{user_id}". Stays owner-gated via before_request.

3. SDK-authable broker shim

The CLI shim now emits the full access_token/token_type/expiry JSON that databricks-sdk's DatabricksCliTokenSource requires — unconditionally, not only under --output json. The SDK builds the command without that flag yet still json.loads() the output (the real CLI defaults auth token to JSON), so the old raw-token path made Config(profile=...).authenticate() fail with "cannot unmarshal CLI result" and pi fell back to a single-model picker.

Expiry is deliberately short (5 min) so the SDK re-invokes the shim for a fresh token rather than caching one whose real lifetime we can't prove. Verified no caller depends on the old raw output — provision_coda_pats.sh already passes --output json, and nothing uses --output text.

4. jq + nested wheel layouts

Omnigent's native harnesses end their auth command in | jq -r .access_token; there's no jq in the Apps image, so without it the harness reports "Failed to resolve API key". Host wheels are now resolved from wheels/<version>/ subdirs, picking the dir with the newest-uploaded main wheel so a stale version alongside a new one isn't mixed in.

Fixes made while integrating

  • The jq step wasn't registered in setup_state["steps"]. _update_step() silently no-ops on an unknown id, so the step would have run invisibly — no progress line, no error surfaced if it failed.
  • install_jq.sh ignored GITHUB_RELEASE_MIRROR, which install_gh.sh, install_micro.sh and install_databricks_cli.sh all honour — a firewalled deploy couldn't reach github.com. Now mirrored; downloads to a temp path and verifies the binary runs before installing it as jq (a truncated download or HTML error page would otherwise land on PATH and silently produce an empty token — the exact failure this script exists to prevent); and is best-effort, warning and exiting 0 rather than marking the whole app setup errored, since jq only matters on the Omnigent path which is off by default.
  • The branch's Gemini schema-key list is a superset of what I landed in fix(opencode): write auth.json in opencode's real schema + proxy fixes for Gemini/GPT #119 — it also strips minLength/maxLength/pattern, minItems/maxItems, minProperties/maxProperties/patternProperties, with the clearer rationale that Gemini's OpenAPI-3.0 parser has no draft-2020 fields. Took the superset, kept both sets of tests.

Verification

  • 541 passed, 3 skipped (up from 536)
  • requirements install ✅ · lockfile in sync ✅ · 13 module imports ✅
  • app.yaml env var set diffed against main — only the two intended entries changed
  • install_jq.sh exercised with jq absent and a broken mirror: warns, exits 0, installs nothing

⚠️ Not deploy-verified. The valueFrom change means Omnigent host attach is OFF on main until the resources are attached — which is the §5-intended state, but it is a behavioural change for anyone deploying main today.

dgokeeffe added 14 commits July 20, 2026 17:50
Replace the commented-out OMNIGENTS_* literals with active valueFrom
resource references so app.yaml stays generic AND git-deploy-friendly
(no per-deploy file edit). The host becomes resource-gated: ON when the
omnigent-wheels (UC Volume) + omnigent-server-url (Secret) resources are
attached, OFF otherwise (an unresolved valueFrom yields an empty string
and omnigents_host_enabled() returns False).

- OMNIGENTS_SERVER_URL  -> valueFrom: omnigent-server-url (Secret)
- OMNIGENTS_WHEEL_SPEC  -> valueFrom: omnigent-wheels      (UC Volume)
- OMNIGENTS_FORCE_REINSTALL + ENABLE_SP_APIKEYHELPER now active (were
  commented out alongside the literals).

Add attach_omnigent_resources.sh + make attach-omnigent-resources target
to attach the two resources per app, idempotently, MERGING with existing
resources (apps update --resources replaces, so merge avoids clobbering
workshop challenge-repo-token). Run after grant_omnigent_host.sh.

Makefile: GRANT_YAML var + OMNIGENT_SERVER_URL/WHEEL_VOLUME derivation
+ OMNIGENT_SECRET_SCOPE/KEY defaults for the new target.
…d wiping git_repository

apps update --json is a full-body write that CLEARS unset fields — it wiped
the coda app's git_repository on a live git-linked app, breaking redeploys
(learned during go-live validation). The CLI's create-update --json body
shape is finicky (rejects 'resources' as an unknown field when update_mask
is embedded). Switch the write step to the Apps SDK directly:
create_update(app, update_mask='resources', app=App(resources=[...])) — a
targeted field-mask patch that touches ONLY resources, preserving
git_repository and other app fields. Validated against the live coda app
(git_repository + both omnigent resources present after re-run).
The share endpoint previously granted only to the calling user (the owner),
so you could self-share but not share to a teammate. Add an optional grant_user
in the request body; when present, share to that user instead. The owner-gate
is unchanged (only the app owner may invoke the endpoint, since it acts with
the SP's authority). Default behavior (no grant_user) is unchanged: share to
the calling user, so the browser auto-share path still works.
_materialize_spec listed only the top level of the UC Volume, so it raised
'no .whl in UC Volume' when the omnigent build publishes wheels under
wheels/<version>/ instead of flat at the volume root. Recurse (bounded depth)
when the root has no wheels, pick the directory holding the newest-uploaded
main omnigent- wheel, and install every wheel from that dir. Flat top-level
layout still works unchanged. Tests cover flat, nested-newest-wins, and the
no-wheels error.
Omnigent's native pi/claude/codex harnesses resolve their AI-Gateway bearer
via an auth command ending in `... --output json | jq -r '.access_token'`
(omnigent.inner.codex_executor._databricks_codex_auth_command). Databricks
Apps containers ship no jq, so the pipe yields an EMPTY token and the harness
fails with "Failed to resolve API key" — the reported pi-native breakage.

Add install_jq.sh (static binary to ~/.local/bin, same fetch pattern as
install_tmux.sh) and wire it into both paths: _run_step("jq") in app.py
(interactive) and _ensure_jq() in omnigents_host.py before _run_setup_once
(host). Idempotent; non-fatal on failure.
opencode tool calls 400'd with "Unknown name exclusiveMinimum ... Cannot
find field" on the mlflow chat-completions gateway surface, which validates
tool schemas against an OpenAPI-3.0 subset (shared with the Gemini translator).
exclusiveMinimum/Maximum are NUMBERS in JSON Schema draft-2020 but boolean
modifiers in OpenAPI 3.0, so the parser rejects the whole request.

Add exclusiveMinimum/Maximum, multipleOf, minLength/maxLength/pattern,
minItems/maxItems/uniqueItems, and minProperties/maxProperties/patternProperties
to GEMINI_UNSUPPORTED_SCHEMA_KEYS. strip_unsupported_schema_keys recurses so
they're removed at any depth (incl. nested properties[N].value). These are
advisory constraints — dropping them keeps the tool callable; Claude/GPT
ignore them. Regression test added.
Captures the ucode-duplication findings from the 2026-07-20 debug session:
CoDA's setup_*.py reimplements ucode's per-agent config writers (the root of
the exclusiveMinimum dialect bug), ucode's DATABRICKS_BEARER seam as the clean
auth integration, and — critically — that ucode does NOT cover the Omnigent
runner native-harness path (which is where the reported errors actually came
from). Proposes migrating only the interactive path; keeps the shipped hotfixes.
The base app.yaml targeted an Azure workspace (adb-7405614666872455) with
env-specific Otel/MLflow/challenge-repo config. The active deploy target is
now the aws-syd workspace (coda app, dbc-d8b32eb0-866b, AWS ap-southeast-2):
point DATABRICKS_GATEWAY_HOST there, GEMINI_MODEL=databricks-gemini-3-pro,
ENABLE_SP_APIKEYHELPER=true, MAX_CONCURRENT_SESSIONS=5. Parity with the live
deployed coda config (verified via workspace export). Retains app.yaml.aws-syd
as the untracked source overlay.
pi (and any omnigent native harness) showed only its single hard-coded default
model (databricks-claude-sonnet-4-6) instead of the workspace's full endpoint
list. Root cause: omnigent's pi_native_credentials resolves the model catalog
via resolve_databricks_workspace("omnigents-host") →
Config(profile="omnigents-host").authenticate(), but that profile is
host-only (credentials are brokered over loopback, never persisted). The SDK
raised "cannot configure default credentials", the catalog fetch failed, and
pi fell back to one model.

Two coordinated changes so the SDK can mint via the existing broker shim:
- token_helper.write_databricks_token_wrapper: emit the FULL OAuth shape the
  SDK's DatabricksCliTokenSource requires (access_token + token_type + expiry),
  not a bare {access_token}. Broker gives no expiry and always mints fresh, so
  set a short now+5min expiry (well inside the ~1h SP TTL) → SDK re-invokes the
  shim for a fresh token rather than caching one of unknown lifetime.
- omnigents_host._write_oauth_profile: write auth_type = databricks-cli so
  Config(profile=...).authenticate() runs `databricks auth token --profile
  omnigents-host`, which resolves to the broker shim (first on the runner PATH).

Verified live: broker token → GET /api/2.0/serving-endpoints = HTTP 200,
50 endpoints (11 claude, 9 gemini, 19 gpt). Tests updated for the new shim
shape + profile auth_type.
…SON always

Follow-up to 1fc58d0 — that made the profile 'authable' in theory but two live
failures on the AWS container (sdk 0.106.0) proved it insufficient:

1. The SDK's DatabricksCliTokenSource resolves the `databricks` binary via its
   OWN lookup, NOT $PATH — so it ran the REAL CLI in ~/.local/bin (no OAuth
   cache) → 'databricks OAuth is not configured'. Fix: write
   `databricks_cli_path = <broker shim>` into the omnigents-host profile. It's a
   normal Config attribute the SDK reads straight from .databrickscfg, so it
   reaches the in-runner catalog fetch WITHOUT needing omnigent's host→runner
   env allowlist (which we don't own / would need a wheel rebuild to change).

2. The SDK invokes `auth token --profile <p>` WITHOUT `--output json` yet
   json.loads()s stdout (verified in the container's 0.106.0 source). The shim
   only emitted JSON when --output json was present, so the no-flag path printed
   a raw token → 'cannot unmarshal CLI result: line 1 column 1'. Fix: shim now
   emits the access_token/token_type/expiry JSON unconditionally (matches the
   real CLI, which defaults auth token to JSON).

Verified against the live container: broker token → serving-endpoints = 200,
50 models; shim --output json returns a valid JWT; DATABRICKS_CLI_PATH changes
the SDK's error from 'OAuth not configured' to 'cannot unmarshal' (proving it
now runs the shim). Tests updated.
…nested wheels

Brings forward feat/omnigent-host-followups (13 commits, 2026-07-20) — the last
branch in the repo with unlanded work. It never had a PR. Rebuilt against
current main rather than merged, because its app.yaml is a different personal
workspace (see below).

## Generic app.yaml (docs/agent-instructions.md §5)

OMNIGENTS_SERVER_URL and OMNIGENTS_WHEEL_SPEC become `valueFrom` resource
references instead of hardcoded values. §5 says these must be commented out or
defaulted off before landing on main; main currently carries one workspace's
Omnigent server URL and UC Volume path.

The on/off switch becomes "are the resources attached" rather than "did you edit
this committed file": an unresolved valueFrom yields an empty string and
omnigents_host_enabled() returns False. Adds attach_omnigent_resources.sh and a
`make attach-omnigent-resources` target to attach them, using SDK
create_update with an update_mask so attaching doesn't wipe git_repository.

**Only those two vars changed.** The branch's own app.yaml was its author's
aws-syd workspace — taking it wholesale would have swapped one set of personal
values for another (a different DATABRICKS_GATEWAY_HOST), flipped ENABLE_CODEX
and ENABLE_GEMINI back on against main's comments explaining why they're off,
dropped MLFLOW_TRACING_ENABLED / CLAUDE_CODE_OTEL_ENABLED / PROXY_TRACE_CONTENT
/ CLAUDE_INSTALL_METHOD, and removed CODA_DISABLE_OWNER_CHECK. Verified the env
var set is otherwise byte-identical to main.

CHALLENGE_REPO_URL still holds a personal sandbox URL on main. Left alone here
to keep this reviewable; worth the same treatment separately.

## Teammate host-share

POST /api/omnigent-host/share accepts an optional `grant_user` in the body, so
the owner can share the SP-owned host with a teammate. Previously it could only
ever self-share. This is the gap §5 describes: "An SP-owned host is invisible in
a human's personal picker unless shared via PUT
/v1/hosts/{id}/permissions/{user_id}". The endpoint stays owner-gated by the
before_request check — only the owner can issue a grant.

## SDK-authable broker shim

The CLI shim now emits the full access_token/token_type/expiry JSON that
databricks-sdk's DatabricksCliTokenSource requires, unconditionally rather than
only under `--output json`. The SDK builds the command without that flag yet
still json.loads() the output, because the real CLI defaults `auth token` to
JSON; the old raw-token path made Config(profile=...).authenticate() fail with
"cannot unmarshal CLI result", so pi fell back to a single-model picker. Expiry
is deliberately short (5 min) so the SDK re-invokes the shim for a fresh token
rather than caching one whose real lifetime we can't prove. Checked that no
caller depends on the old raw output — provision_coda_pats.sh already passes
--output json, and nothing uses --output text.

## jq, and nested wheel layouts

Omnigent's native harnesses resolve their bearer via a command ending in
`| jq -r .access_token`; there's no jq in the Apps image, so without it the
harness reports "Failed to resolve API key". Adds install_jq.sh.

Host wheels are now resolved from `wheels/<version>/` subdirectories, picking
the directory holding the newest-uploaded main wheel so a stale version
alongside a new one isn't mixed in.

## Fixes made while integrating

- The jq setup step wasn't registered in setup_state["steps"]. _update_step()
  silently no-ops on an unknown id, so the step would have run invisibly — no
  progress line, and no error surfaced if it failed.
- install_jq.sh ignored GITHUB_RELEASE_MIRROR, which install_gh.sh,
  install_micro.sh and install_databricks_cli.sh all honour. A firewalled
  deploy would have failed to reach github.com. Now mirrored, downloads to a
  temp path, verifies the binary actually runs before installing it as `jq`
  (a truncated download or HTML error page would otherwise land on PATH and
  silently produce an empty token — the exact failure the script exists to
  prevent), and is best-effort: a download failure warns and exits 0 rather
  than marking the whole app setup errored, since jq only matters on the
  Omnigent path, which is off by default.
- The branch's Gemini schema-key list is a superset of what I landed in #119
  (it also strips minLength/maxLength/pattern, minItems/maxItems,
  minProperties/maxProperties/patternProperties, with the clearer rationale
  that Gemini's OpenAPI-3.0 parser has no draft-2020 fields). Took the superset
  and kept both sets of tests.

## Verification

541 passed, 3 skipped (up from 536). requirements install + lockfile + module
imports all pass. app.yaml env var set diffed against main to confirm only the
two intended entries changed. install_jq.sh exercised with jq absent and a
broken mirror: warns, exits 0, installs nothing.
@dgokeeffe
dgokeeffe merged commit 82b45a9 into main Aug 5, 2026
@dgokeeffe
dgokeeffe deleted the feat/omnigent-host-followups-v2 branch August 5, 2026 11:35
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…ml files (#122)

docs/agent-instructions.md §5 requires personal/workspace values to be commented
out or defaulted off before landing on main. This is a public repo and several
had accumulated: a customer workspace id, a UC catalog, a self-hosted MLflow app
URL, and a personal sandbox repo. Each is both an information leak and actively
wrong for anyone else deploying the template.

Removed (commented out with placeholders, or already resolved via valueFrom):

  app.yaml
    DATABRICKS_GATEWAY_HOST          adb-7405614666872455…azuredatabricks.net
    CLAUDE_CODE_OTEL_CATALOG_SCHEMA  edp_aisandbox_aisandbox_dev.ppcs
    MLFLOW_OSS_URL                   coda-mlflow-oss-7405614666872455…
    CHALLENGE_REPO_URL               github.com/david-okeeffe_data/dok-sandbox
  app.yaml.workshop
    the same four, plus OMNIGENTS_SERVER_URL / OMNIGENTS_WHEEL_SPEC switched to
    the valueFrom resources the base app.yaml already uses (#121)
  app.yaml.template
    DATABRICKS_GATEWAY_HOST — see below

## DATABRICKS_GATEWAY_HOST has three states, not two

Unset is the right default and is why this one is commented rather than blanked:

  unset    -> derive from the workspace id (or an Azure DATABRICKS_HOST) and
              PROBE for reachability (utils.get_gateway_host tier 2)
  set URL  -> TRUSTED, no probe (tier 1)
  set ""   -> explicitly DISABLE the gateway, use serving-endpoints

So `value: ""` is not "no value" — it turns the gateway off. And because a set
URL is trusted without probing, a stale one doesn't fall back: a token minted at
one workspace is invalid at another's gateway, so every model call returns
"400 Invalid Token".

That last point is why the new guard caught a real bug in app.yaml.template,
which shipped DATABRICKS_GATEWAY_HOST *uncommented* with the placeholder still
in it. Deploying the template unedited would pin the gateway to the literal
string `https://<your-gateway-id>.ai-gateway.<env>.cloud.databricks.com` and
fail every model call, where leaving it out would have worked. Now commented,
with the trap spelled out.

## Safety of each removal

- CLAUDE_CODE_OTEL_CATALOG_SCHEMA: claude_otel.py returns False when it's
  absent, so OTEL is simply off. CLAUDE_CODE_OTEL_ENABLED also flipped to
  "false" so the flag doesn't advertise a feature that can't run.
- MLFLOW_OSS_URL: proxy_tracing._enabled() already requires both the flag and a
  non-empty URL; the flag was already "false".
- CHALLENGE_REPO_URL: app.py only registers the "challenge" setup step when it's
  set, and install_challenge_repo.sh exits 0 when unset. Commented together with
  CHALLENGE_REPO_READ_TOKEN, since an unresolved valueFrom for it is the source
  of the known "error resolving resource challenge-repo-token" boot warning.

## Guard

tests/test_app_yaml_overlays.py grows two checks over every tracked app.yaml*:
no active env value may contain a workspace id, a concrete UC Volume/catalog, an
AWS workspace host or a personal GitHub repo; and DATABRICKS_GATEWAY_HOST must
not be pinned. Commented-out examples are exempt — that's where placeholders
belong. Verified the guard fails when the UC catalog value is reintroduced.

549 passed, 3 skipped. All four app.yaml* files still parse and still declare
every ENABLE_<CLI> toggle.
dgokeeffe pushed a commit that referenced this pull request Aug 5, 2026
….17.0)

Closes #118, #120, #121, #122

- Session creation prompt: ask users to reuse existing sessions before creating new
- MAX_CONCURRENT_SESSIONS backend cap (env var, default 5) with TOCTOU-safe check
- Session count label in tab bar with updates on all create/close/exit paths
- xterm.js ClipboardAddon for OSC 52 (copy-paste inside Claude Code)
- Write batching with requestAnimationFrame to prevent escape sequence fragmentation
- Alternate screen exit detection (auto-clear after Claude Code no-flicker/vim)
- SIGWINCH-based reattach (force redraw by toggling terminal size)
- 429 error message with hint to increase MAX_CONCURRENT_SESSIONS
- Replaced mlflow-tracing with mlflow-skinny 3.10.1
- PTY read chunk 4096→65536 bytes
- Fixed repo name in deployment docs
- Version bump to 0.17.0
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…nested wheels (#121)

* fix(auth): configure Omnigent Databricks provider

* feat(deploy): generic app.yaml via valueFrom resource references

Replace the commented-out OMNIGENTS_* literals with active valueFrom
resource references so app.yaml stays generic AND git-deploy-friendly
(no per-deploy file edit). The host becomes resource-gated: ON when the
omnigent-wheels (UC Volume) + omnigent-server-url (Secret) resources are
attached, OFF otherwise (an unresolved valueFrom yields an empty string
and omnigents_host_enabled() returns False).

- OMNIGENTS_SERVER_URL  -> valueFrom: omnigent-server-url (Secret)
- OMNIGENTS_WHEEL_SPEC  -> valueFrom: omnigent-wheels      (UC Volume)
- OMNIGENTS_FORCE_REINSTALL + ENABLE_SP_APIKEYHELPER now active (were
  commented out alongside the literals).

Add attach_omnigent_resources.sh + make attach-omnigent-resources target
to attach the two resources per app, idempotently, MERGING with existing
resources (apps update --resources replaces, so merge avoids clobbering
workshop challenge-repo-token). Run after grant_omnigent_host.sh.

Makefile: GRANT_YAML var + OMNIGENT_SERVER_URL/WHEEL_VOLUME derivation
+ OMNIGENT_SECRET_SCOPE/KEY defaults for the new target.

* fix(attach-resources): use SDK create_update with update_mask to avoid wiping git_repository

apps update --json is a full-body write that CLEARS unset fields — it wiped
the coda app's git_repository on a live git-linked app, breaking redeploys
(learned during go-live validation). The CLI's create-update --json body
shape is finicky (rejects 'resources' as an unknown field when update_mask
is embedded). Switch the write step to the Apps SDK directly:
create_update(app, update_mask='resources', app=App(resources=[...])) — a
targeted field-mask patch that touches ONLY resources, preserving
git_repository and other app fields. Validated against the live coda app
(git_repository + both omnigent resources present after re-run).

* feat(host-share): accept grant_user in /api/omnigent-host/share body

The share endpoint previously granted only to the calling user (the owner),
so you could self-share but not share to a teammate. Add an optional grant_user
in the request body; when present, share to that user instead. The owner-gate
is unchanged (only the app owner may invoke the endpoint, since it acts with
the SP's authority). Default behavior (no grant_user) is unchanged: share to
the calling user, so the browser auto-share path still works.

* test: align fixtures with current setup behavior

* chore: remove personal values from resource example

* feat(host): resolve omnigent wheels from nested wheels/<version>/ dir

_materialize_spec listed only the top level of the UC Volume, so it raised
'no .whl in UC Volume' when the omnigent build publishes wheels under
wheels/<version>/ instead of flat at the volume root. Recurse (bounded depth)
when the root has no wheels, pick the directory holding the newest-uploaded
main omnigent- wheel, and install every wheel from that dir. Flat top-level
layout still works unchanged. Tests cover flat, nested-newest-wins, and the
no-wheels error.

* fix(host): install jq so native-harness Databricks auth resolves

Omnigent's native pi/claude/codex harnesses resolve their AI-Gateway bearer
via an auth command ending in `... --output json | jq -r '.access_token'`
(omnigent.inner.codex_executor._databricks_codex_auth_command). Databricks
Apps containers ship no jq, so the pipe yields an EMPTY token and the harness
fails with "Failed to resolve API key" — the reported pi-native breakage.

Add install_jq.sh (static binary to ~/.local/bin, same fetch pattern as
install_tmux.sh) and wire it into both paths: _run_step("jq") in app.py
(interactive) and _ensure_jq() in omnigents_host.py before _run_setup_once
(host). Idempotent; non-fatal on failure.

* fix(proxy): strip JSON-Schema keywords the Gemini surface rejects

opencode tool calls 400'd with "Unknown name exclusiveMinimum ... Cannot
find field" on the mlflow chat-completions gateway surface, which validates
tool schemas against an OpenAPI-3.0 subset (shared with the Gemini translator).
exclusiveMinimum/Maximum are NUMBERS in JSON Schema draft-2020 but boolean
modifiers in OpenAPI 3.0, so the parser rejects the whole request.

Add exclusiveMinimum/Maximum, multipleOf, minLength/maxLength/pattern,
minItems/maxItems/uniqueItems, and minProperties/maxProperties/patternProperties
to GEMINI_UNSUPPORTED_SCHEMA_KEYS. strip_unsupported_schema_keys recurses so
they're removed at any depth (incl. nested properties[N].value). These are
advisory constraints — dropping them keeps the tool callable; Claude/GPT
ignore them. Regression test added.

* docs(plans): design note for adopting ucode on the interactive path

Captures the ucode-duplication findings from the 2026-07-20 debug session:
CoDA's setup_*.py reimplements ucode's per-agent config writers (the root of
the exclusiveMinimum dialect bug), ucode's DATABRICKS_BEARER seam as the clean
auth integration, and — critically — that ucode does NOT cover the Omnigent
runner native-harness path (which is where the reported errors actually came
from). Proposes migrating only the interactive path; keeps the shipped hotfixes.

* chore(deploy): make aws-syd the committed app.yaml base

The base app.yaml targeted an Azure workspace (adb-7405614666872455) with
env-specific Otel/MLflow/challenge-repo config. The active deploy target is
now the aws-syd workspace (coda app, dbc-d8b32eb0-866b, AWS ap-southeast-2):
point DATABRICKS_GATEWAY_HOST there, GEMINI_MODEL=databricks-gemini-3-pro,
ENABLE_SP_APIKEYHELPER=true, MAX_CONCURRENT_SESSIONS=5. Parity with the live
deployed coda config (verified via workspace export). Retains app.yaml.aws-syd
as the untracked source overlay.

* fix(host): make omnigents-host profile SDK-authable for pi model catalog

pi (and any omnigent native harness) showed only its single hard-coded default
model (databricks-claude-sonnet-4-6) instead of the workspace's full endpoint
list. Root cause: omnigent's pi_native_credentials resolves the model catalog
via resolve_databricks_workspace("omnigents-host") →
Config(profile="omnigents-host").authenticate(), but that profile is
host-only (credentials are brokered over loopback, never persisted). The SDK
raised "cannot configure default credentials", the catalog fetch failed, and
pi fell back to one model.

Two coordinated changes so the SDK can mint via the existing broker shim:
- token_helper.write_databricks_token_wrapper: emit the FULL OAuth shape the
  SDK's DatabricksCliTokenSource requires (access_token + token_type + expiry),
  not a bare {access_token}. Broker gives no expiry and always mints fresh, so
  set a short now+5min expiry (well inside the ~1h SP TTL) → SDK re-invokes the
  shim for a fresh token rather than caching one of unknown lifetime.
- omnigents_host._write_oauth_profile: write auth_type = databricks-cli so
  Config(profile=...).authenticate() runs `databricks auth token --profile
  omnigents-host`, which resolves to the broker shim (first on the runner PATH).

Verified live: broker token → GET /api/2.0/serving-endpoints = HTTP 200,
50 endpoints (11 claude, 9 gemini, 19 gpt). Tests updated for the new shim
shape + profile auth_type.

* fix(host): route SDK CLI-auth through the broker shim by path, emit JSON always

Follow-up to 017ab40 — that made the profile 'authable' in theory but two live
failures on the AWS container (sdk 0.106.0) proved it insufficient:

1. The SDK's DatabricksCliTokenSource resolves the `databricks` binary via its
   OWN lookup, NOT $PATH — so it ran the REAL CLI in ~/.local/bin (no OAuth
   cache) → 'databricks OAuth is not configured'. Fix: write
   `databricks_cli_path = <broker shim>` into the omnigents-host profile. It's a
   normal Config attribute the SDK reads straight from .databrickscfg, so it
   reaches the in-runner catalog fetch WITHOUT needing omnigent's host→runner
   env allowlist (which we don't own / would need a wheel rebuild to change).

2. The SDK invokes `auth token --profile <p>` WITHOUT `--output json` yet
   json.loads()s stdout (verified in the container's 0.106.0 source). The shim
   only emitted JSON when --output json was present, so the no-flag path printed
   a raw token → 'cannot unmarshal CLI result: line 1 column 1'. Fix: shim now
   emits the access_token/token_type/expiry JSON unconditionally (matches the
   real CLI, which defaults auth token to JSON).

Verified against the live container: broker token → serving-endpoints = 200,
50 models; shim --output json returns a valid JWT; DATABRICKS_CLI_PATH changes
the SDK's error from 'OAuth not configured' to 'cannot unmarshal' (proving it
now runs the shim). Tests updated.
dgokeeffe added a commit that referenced this pull request Aug 5, 2026
…ml files (#122)

docs/agent-instructions.md §5 requires personal/workspace values to be commented
out or defaulted off before landing on main. This is a public repo and several
had accumulated: a customer workspace id, a UC catalog, a self-hosted MLflow app
URL, and a personal sandbox repo. Each is both an information leak and actively
wrong for anyone else deploying the template.

Removed (commented out with placeholders, or already resolved via valueFrom):

  app.yaml
    DATABRICKS_GATEWAY_HOST          adb-7405614666872455…azuredatabricks.net
    CLAUDE_CODE_OTEL_CATALOG_SCHEMA  edp_aisandbox_aisandbox_dev.ppcs
    MLFLOW_OSS_URL                   coda-mlflow-oss-7405614666872455…
    CHALLENGE_REPO_URL               github.com/david-okeeffe_data/dok-sandbox
  app.yaml.workshop
    the same four, plus OMNIGENTS_SERVER_URL / OMNIGENTS_WHEEL_SPEC switched to
    the valueFrom resources the base app.yaml already uses (#121)
  app.yaml.template
    DATABRICKS_GATEWAY_HOST — see below

## DATABRICKS_GATEWAY_HOST has three states, not two

Unset is the right default and is why this one is commented rather than blanked:

  unset    -> derive from the workspace id (or an Azure DATABRICKS_HOST) and
              PROBE for reachability (utils.get_gateway_host tier 2)
  set URL  -> TRUSTED, no probe (tier 1)
  set ""   -> explicitly DISABLE the gateway, use serving-endpoints

So `value: ""` is not "no value" — it turns the gateway off. And because a set
URL is trusted without probing, a stale one doesn't fall back: a token minted at
one workspace is invalid at another's gateway, so every model call returns
"400 Invalid Token".

That last point is why the new guard caught a real bug in app.yaml.template,
which shipped DATABRICKS_GATEWAY_HOST *uncommented* with the placeholder still
in it. Deploying the template unedited would pin the gateway to the literal
string `https://<your-gateway-id>.ai-gateway.<env>.cloud.databricks.com` and
fail every model call, where leaving it out would have worked. Now commented,
with the trap spelled out.

## Safety of each removal

- CLAUDE_CODE_OTEL_CATALOG_SCHEMA: claude_otel.py returns False when it's
  absent, so OTEL is simply off. CLAUDE_CODE_OTEL_ENABLED also flipped to
  "false" so the flag doesn't advertise a feature that can't run.
- MLFLOW_OSS_URL: proxy_tracing._enabled() already requires both the flag and a
  non-empty URL; the flag was already "false".
- CHALLENGE_REPO_URL: app.py only registers the "challenge" setup step when it's
  set, and install_challenge_repo.sh exits 0 when unset. Commented together with
  CHALLENGE_REPO_READ_TOKEN, since an unresolved valueFrom for it is the source
  of the known "error resolving resource challenge-repo-token" boot warning.

## Guard

tests/test_app_yaml_overlays.py grows two checks over every tracked app.yaml*:
no active env value may contain a workspace id, a concrete UC Volume/catalog, an
AWS workspace host or a personal GitHub repo; and DATABRICKS_GATEWAY_HOST must
not be pinned. Commented-out examples are exempt — that's where placeholders
belong. Verified the guard fails when the UC catalog value is reintroduced.

549 passed, 3 skipped. All four app.yaml* files still parse and still declare
every ENABLE_<CLI> toggle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant