Compare commits

..

638 Commits

Author SHA1 Message Date
Tomu Hirata 6a6722aaca fix(opencode-native): stop leaking opencode serve processes across teardown paths
Opencode-native has the same process-leak shape codex-native did (fixed in
#3925): each session runs a runner-owned `opencode serve` subprocess tracked
in _AUTO_OPENCODE_SERVERS plus the opencode TUI pane. Only DELETE /v1/sessions
cancelled the forwarder (whose finally closes the server); the other ways the
TUI pane goes away left the server orphaned for the runner's lifetime:

- the idle pane reaper closed the tmux pane but never touched
  _AUTO_OPENCODE_SERVERS,
- an unexpected TUI exit (crash / OOM / host recycle) evicted the pane
  without cancelling the forwarder, and
- a graceful host/runner stop tore the runner down without a per-session
  DELETE, so _stop_pm never closed the servers.

Mirror the codex fix: add teardown_opencode_native_server (cancel the
forwarder, close any leftover registered server; no-op when none is
registered) and teardown_all_opencode_native_servers (shutdown sweep). Wire
them into the idle-reaper reap, the terminal-exit publisher, and _stop_pm
alongside the codex calls.

No boot-time reconcile: opencode has no crash-safe process registry and
`opencode serve` is a plain Popen (not start_new_session=True), so it shares
the runner's process group and dies with a hard runner death — the
graceful-stop + reaper + exit paths cover the observed leak.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 22:21:27 +09:00
Tomu Hirata d794ef4f9f feat(telemetry): accept "default" omnigent_version in remote config (#4054)
omnigent-telemetry#15 introduces a CloudFront default config
(omnigent_version: "default") served for any version that lacks an
explicit config file.  Without this change, the version check on line 190
always rejects the default payload and silently disables telemetry.

Accept "default" as an equivalent of the current VERSION so the default
config is honoured.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 12:58:38 +00:00
Constantin-Tiberiu Craiu e4686b9286 Use index for retrieving conversation (#3451)
Signed-off-by: Constantin-Tiberiu Craiu <57532657+craiuconstantintiberiu@users.noreply.github.com>
2026-08-05 21:54:12 +09:00
Tomu Hirata 232a753903 fix(claude-sdk): redact base64 image/document source blocks on replay (#3120)
The historical-replay redaction (_redact_inline_base64) only matched
whole-string "data:*;base64,..." URIs — the resolver form under
image_url / file_data. But Claude Code's Read tool returns an image file
as an Anthropic content block {"type":"image","source":{"type":"base64",
"data":"..."}} — raw base64 with no data: prefix — carried in a
function_call_output. That shape slipped past redaction, so if it reached
the "Conversation so far:" text prefix json.dumps flattened the full
base64 into prompt text (the same class of overrun that wedges resume on
the native path).

Extend _redact_inline_base64 to also rewrite image/document base64
"source" blocks to a compact "[image/attachment: <media>, <N> base64
chars]" placeholder. Verified: image and document source blocks now
redact (base64 absent), data-URI and plain-text paths unchanged.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 21:45:40 +09:00
Tomu Hirata a0f50d2c1b fix(harness): raise idle watchdog default to 600s so long compaction survives (#4013)
The per-turn idle watchdog fails a turn that emits no non-heartbeat
events for the window. Context compaction's summarizing LLM call runs
as a single long await that emits nothing until it returns, so on a
near-full context it can exceed the 240s default and trip the watchdog.
That wedges the session in a "Prompt is too long" -> compaction ->
240s-timeout loop, since every retry re-triggers the same slow compaction.

Raise the default from 240s to 600s so a healthy long compaction has
room to finish. The HARNESS_TURN_TIMEOUT_S env knob and the absolute
ceiling are unchanged.

Co-authored-by: Isaac
2026-08-05 21:44:10 +09:00
Hubert e63661394c Update the composer button shape + default composer rows amount (#4134)
* Fix composer styles

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* comment

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-05 14:08:07 +02:00
Serena Ruan 06a5f7945b fix(web): persist open shell tabs per session (#4125)
* fix(web): persist open shell tabs per session

Shell tabs lived only in transient component state and the
conversation-switch effect cleared them on every navigation, so opening
a shell, switching sessions, and returning lost the tab. The PTYs
themselves live on the server and are re-fetched by useTerminals — only
the tab strip was being discarded.

Persist openTerminals/selectedTerminalKey per session in
sessionWorkspaceState (mirroring the open file tabs), seed and restore
them on mount/switch, and gate the dead-tab prune effect on the
terminals list's loading state so a restored tab isn't wiped by the
transient empty list before the session's terminals load.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e): cover shell-tab persistence; skip prune on errored terminal fetch

Add an e2e_ui test that opens a real shell in one session, switches to
another via the sidebar (client-side nav), and returns — asserting the
shell tab and its live PTY are restored. This exercises the
conversation-switch effect that regressed, which a full page reload
wouldn't.

Also address review feedback: the dead-tab prune effect ran whenever the
terminals query wasn't loading, but an errored fetch also yields an empty
list — a non-authoritative one. Pruning against it would wipe restored
tabs whose PTYs we simply couldn't reach. Gate the effect on
terminalsError as well, with a component test for the errored-read case.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-05 19:40:40 +08:00
Hubert 1282f6099a [OMNI-2351] Hide message actions when not hovered/focused (#4123)
* [OMNI-2351] Hide message actions when not hovered/focused

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-05 13:40:12 +02:00
Serena Ruan cf34d7b909 fix(claude-native): map Claude's new "shell" status to idle (#4132)
Claude Code >= v2.1.197 writes `status: "shell"` to its per-session status
file when a turn ends but a background shell is still alive. The status-file
poller's map didn't know that literal, so `read_session_status` returned
`None`, the poller fired no edge and stayed stuck on its last `running` (while
also suppressing the PTY watcher's `idle`). The session never reported idle
while a background shell ran, so `sessionStatus` stayed `running`,
`shouldQueueSend` returned true, and every new message queued client-side —
regressing the "don't queue while only background work runs" behavior.

Map `shell` to `idle`: the agent loop is idle, and the Stop hook separately
relabels its own `idle` to `waiting` with the shell tally, which is what keeps
the "N background tasks still running" spinner lit.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-05 19:39:24 +08:00
Daniel Lok c7961f1d24 Revert "perf(web): cache recent conversation transcripts (#3932)" (#4124)
This reverts commit 617293d3d9.

Painting a cached transcript before revalidation meant the contents
moved under the reader: the window appeared instantly, then shifted as
newer commits were gap-bridged onto it. A hydrate spinner that resolves
into a settled transcript reads better than a fast paint that jumps, so
go back to the cold-load spinner on every conversation switch.

Co-authored-by: Isaac

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
2026-08-05 11:38:10 +00:00
Serena Ruan 4b10c3a1de ci: mirror linked issue priority onto closing PRs (#4114)
* ci: mirror linked issue priority onto closing PRs

Add a workflow that copies an issue's priority label (P0-P3) onto the
PR that closes it. Only closing links (closes/fixes/resolves #n) count;
a plain "related to #n" mention is ignored. When a PR closes several
issues the highest priority wins, and stale priority labels are dropped.

Runs on PR events and re-syncs when an issue's priority label changes;
the issue-label trigger is gated to priority labels only so other label
edits don't spin up the job.

Co-authored-by: Isaac

* ci: address review feedback on priority sync

- Tolerate null GraphQL nodes (unknown PR number, data: null) instead of
  crashing on AttributeError; cover the parsing with tests.
- Add a 30s urlopen timeout so a stalled connection fails fast.
- Validate PR_NUMBER is an integer with a clear message.
- Surface a warning when the issue->PR GraphQL lookup fails rather than
  silently succeeding.
- Pass the resolved PR list through an env var instead of interpolating
  it into the run block.

Co-authored-by: Isaac
2026-08-05 19:18:11 +08:00
Hubert 8a7a015b9b Fix reference font sizes (#4122)
* Fix reference font sizes

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-05 13:15:49 +02:00
Hubert 559504d9fe feat(web): add a session filter menu and tidy sidebar header actions (#4055)
* Sidebar ownership/archived filters

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* test(e2e-ui): regenerate visual baselines

* dropdown visibility

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-05 12:33:28 +02:00
Serena Ruan eb396b83a5 chore(triage): rename issue type labels to Feature and Docs (#4116)
Rename the `enhancement` label to `Feature` and `documentation` to `Docs`
across the issue-triage system. The triage agent's `type` value is applied
verbatim as an issue label, so update the validator allow-list, the agent
schema and classification rule, the feature-request template's auto-label,
and the design proposal doc to keep them coherent.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-05 18:19:12 +08:00
Tomu Hirata d03d1c131d perf(host): cache auth headers and parallelize status payloads (#4033)
* perf(host): cache auth headers and parallelize status payloads

Two follow-on speedups for omni host status:

1. Cache _remote_headers() per base_url within a process.
   Databricks SDK credential resolution (~3s) ran on every
   _host_http_json call. Since tokens are valid for the lifetime
   of a CLI invocation, resolving once and reusing is safe.
   A threading.Lock serialises concurrent first-time resolution
   for the same URL.

2. Build daemon status payloads in parallel with ThreadPoolExecutor.
   With the dead-process skip from the previous commit, only live
   daemons make HTTP calls. Parallelising them lets independent
   servers be queried concurrently instead of sequentially.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* chore: restore uv.lock to main

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: move header cache resolution inside try/except in _host_http_json

_remote_headers() does file I/O and Databricks SDK calls that can raise
OSError. The cache-populating call was outside the try block, so such a
failure propagated unhandled. Under ThreadPoolExecutor (added in this
PR) that aborted the entire omni host status listing.

Move the resolution inside the existing try/except so auth/file errors
remain recoverable and produce a status_code=0 result per daemon,
matching the pre-change behaviour.

Also adds test_host_http_json_handles_remote_headers_oserror to pin
this contract.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* chore: fix import order (ruff)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 09:20:29 +00:00
Serena Ruan f0ff685e4f ci(triage): re-triage issues when needs-info is cleared (#4108)
* ci(triage): re-triage issues when needs-info is cleared

Add a hybrid needs-info lifecycle. When the issue author comments on an
issue that still carries needs-info, needs-info-response.yml removes the
label using the omnigent-ci App token (the default GITHUB_TOKEN would not
re-trigger downstream workflows). That removal fires issue-triage.yml's
new `unlabeled` trigger, which reads the reporter's follow-up comments,
reclassifies, and assigns an owner — re-adding needs-info only if the
issue is still too vague. Issues the reporter never clarifies are closed
by the existing stale.yml.

issue-triage.yml changes:
- trigger on issues [opened, unlabeled]; the unlabeled path fires only
  for needs-info on an open issue, and allows a bot actor (the App)
- feed the author's follow-up comments into the triage prompt
- remove needs-info on re-triage when the LLM no longer flags it
- suppress the duplicate-of comment on the re-triage path
- add a per-issue concurrency group

Co-authored-by: Isaac

* ci(triage): address review — idempotent label removal, dormant-App notice

- needs-info-response.yml: re-check live labels before `gh --remove-label`
  so a stale event payload / race can't fail the step (gh errors on a
  missing label); emit a ::notice:: when the omnigent-ci App is
  unconfigured so a dormant feature is distinguishable from a broken one.
- issue-triage.yml: also suppress the `duplicate` label on the re-triage
  path (not just the comment), keeping the label and its explanation
  consistent; hoist `import os` to the top of the block.

Co-authored-by: Isaac
2026-08-05 17:12:17 +08:00
Tomu Hirata b02575de77 feat(webui): capture raw SSE events and show in execution logs panel (#4111)
* feat(webui): capture raw SSE events and show in execution logs panel

- sseEventLog.ts: module-level ring buffer (max 500 events/session)
  with subscribe/snapshot API for useSyncExternalStore
- useSseEventLog.ts: React hook that subscribes to the ring buffer
- chatStore.ts: tap tapSessionEvents to push each StreamEvent into the
  ring buffer; clear on fresh stream bind (not reconnect)
- ExecutionLogsPanel.tsx: add Items/SSE toggle — SSE tab shows
  timestamped raw events with expand-to-pretty-print, auto-scrolls
  to bottom as events arrive

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* perf(webui): skip SSE ring buffer when debug mode is off

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* perf(webui): cache isDebugMode as module-level boolean

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(webui): return new array ref on push so useSyncExternalStore re-renders

Object.is on the same mutated array always returns true, causing React
to skip re-renders. Produce a fresh array on every push/trim so the
snapshot reference changes and the SSE list updates in real time.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(webui): support localStorage debug flag in addition to ?debug=1

Both useDebugMode and the SSE ring buffer guard now check
localStorage.getItem("debug") === "1" as a fallback, so debug mode
can be toggled once in the console without keeping ?debug=1 in every URL:
  localStorage.setItem("debug", "1")   // enable
  localStorage.removeItem("debug")      // disable

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(webui): stable snapshot ref and correct debug flag detection

- snapshotSseLog: return shared EMPTY constant instead of allocating a
  new [] on every call; prevents useSyncExternalStore render-loop from
  the unstable reference on sessions with no log yet
- isDebugMode: re-read window.location.search + localStorage on every
  call instead of caching against popstate; React Router uses pushState/
  replaceState which never fires popstate, so the cached value stayed
  stale when navigating to ?debug=1 in-app

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 09:09:16 +00:00
Tomu Hirata 1d4abc9a5e feat(web): split the harness picker by support level and remember used harnesses (#4107)
* fix(web): split the harness picker by support level

The landing composer's harness picker split its primary list and "More"
group by host readiness, so any configured harness led: Claude Code,
Codex, Cursor, and Pi all competed for the few primary slots, while "More"
held only harnesses that happened to need setup. Support level — what
actually distinguishes these integrations — wasn't represented at all.

Add a `fullySupported` flag to `NativeCodingAgentSpec` and set it on
Claude Code and Codex, the integrations we maintain and test end to end.
Only those lead; every other harness folds into "More" whether or not it
is configured on the host. The flag is opt-in, so the supported set is two
lines in one file rather than a marker on each of the nine others, and a
test asserts the set is exactly claude + codex so it can't drift silently.

Two behaviors are preserved: selecting a harness pins it inline via the
existing `effectiveAgentId` rule, so the active pick is never buried; and
the hide-unconfigured preference still outranks support level, dropping
harnesses that can't launch here (and the "More" trigger with them when
that empties the group).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(web): promote previously-launched harnesses in the picker

Splitting the picker by support level left Pi and Cursor users a hover
away from their harness on every new session, even though the split is
right for a first-time user. Nothing recorded which harnesses someone
actually launches.

Add a localStorage-backed `useRecentHarnesses` (modeled on
`useRecentWorkspaces`, but not host-scoped — a preference for Pi follows
the person across machines) and record the canonical harness id on a
successful create. The picker then promotes any recorded harness into the
primary list alongside the fully supported ones, so a regular Pi user
gets one click instead of one hover, while a fresh install still leads
with Claude Code and Codex only.

Recording happens only after the create succeeds, so a harness the user
merely browsed past never earns a slot, and the hide-unconfigured
preference still outranks recency: promotion applies within what can
launch on the host, never resurrecting a harness that can't run there.
Stored ids fold through the reversed-alias map, so `native-pi` matches
the canonical `pi-native` spec.

Also fixes the two CI failures from the support-level split: the flow
test's `selectAgent` helper now drills into "More" only when the row
isn't already inline, and the harness-install e2e no longer drills for
Codex (fully supported, so it leads inline even while needing setup).

Adds tests/e2e_ui coverage for both behaviors, stubbing every harness as
configured so the split is provably driven by support level rather than
host readiness.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 17:55:49 +09:00
Tomu Hirata 7552ab77b4 feat(webui): wire SessionRail into AppShell behind ?debug=1 (#4109)
* feat(webui): wire SessionRail into AppShell behind ?debug=1

SessionRail and ExecutionLogsPanel were implemented but never rendered.
Add SessionRail as a desktop-only column between the chat and workspace
panel, gated on debugMode so it only appears with ?debug=1. The column
hides automatically when a push panel (terminals or execution logs) is
open.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(webui): remove TerminalsCard from SessionRail debug rail

Terminals are already shown in WorkspacePanel. The debug rail should
only show the Execution logs card. Also removes the onExpandTerminals
prop and all terminal-related dead code from SessionRail.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(webui): fix execution logs card title overflow in debug rail

Widen the debug column from w-48 to w-56 and add truncate/min-w-0 to
the CardTitle so the text doesn't overflow into the action buttons.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(webui): add top padding to debug rail column

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 08:08:05 +00:00
Serena Ruan cbe381955e fix(web): keep chat content clear of the TurnRail as the area narrows (#4106)
* fix(web): keep chat content clear of the TurnRail as the area narrows

PR #4085 replaced the transcript's md:pl-12 left inset with a symmetric
px-4 gutter, dropping the clearance that kept the centered chat column off
the left-edge TurnRail (the tick minimap). On a narrow conversation area
the prose crowded the ticks.

Restore the clearance as a continuous, width-driven clamp keyed on the
conversation area (@container/chat) rather than the viewport: the column
slides left with the area until its edge nears the rail, then the left
inset ramps up to hold a minimum gap and caps at 3rem so it stops moving
instead of snapping. Because it reads the area width, opening the sidebar
feeds it too.

Add a multi-turn visual-snapshot test that mounts the rail (it only renders
for >= 2 turns, so the one-turn baseline never covered it), rendered at a
narrower viewport so the inset is actually engaged in the capture.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

* fix(web): shrink rail gap to 24px and stop the pill leaking into snapshots

Reduce the restored TurnRail clearance cap from 3rem to 1.5rem (24px) so the
column sits closer to the ticks while still clearing them.

Park the pointer out of the transcript's top hover band before capture in both
chat snapshot tests. Playwright's virtual mouse starts at (0,0), inside the band
that reveals the "Jump to top" pill (and, on the rail test, over a tick), so a
load-timing race could flash that transient chrome into the resting-state
baseline. Moving the pointer low pins it hidden.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

* test(e2e-ui): hide the Jump-to-top pill from chat snapshots deterministically

The pill is transient chrome: the initial layout settle (LatestTurnSpacer +
StickToBottom pinning to the bottom) fires a scroll that reveals it for ~2s, so
whether it lands in a capture is a race — which is why a regenerated baseline
picked it up. Force it hidden via an injected style, the same way the shared
settle kills the blinking caret, so the resting-state baseline is deterministic
regardless of when the scroll settles.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-05 14:26:47 +08:00
Tomu Hirata 40761123c5 fix(gateway): strip bracket context-window suffixes from model IDs and skip model override on cli-config path (#4105)
- PiExecutor._resolve_model: strip trailing [1m]-style bracket suffixes before
  passing model IDs to the Databricks AI Gateway. The direct Anthropic API
  accepts e.g. system.ai.claude-opus-5[1m] but the gateway endpoint does not
  (returns 404).
- CodexExecutor.run_turn: when model_provider_override is set (cli-config path)
  pass model=None to thread/create so the codex binary uses its own configured
  model rather than forwarding an unresolvable alias (e.g. gpt-5.6) to the UC
  API.
- credential_label: cli-config providers now label from the entry name
  (provider_display_name) rather than the display_name field, for consistency
  with other provider kinds.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 05:33:50 +00:00
Rajarshi Datta a4a924ae75 fix(policies): shell gates no longer fail open on option-taking command wrappers (#3559)
* Root cause fix — omnigent/policies/builtins/_shell.py

sudo/env/command/time/exec moved out of CMD_WRAPPERS (skip-one-word) into _FLAG_WRAPPERS with their value-consuming flags. CMD_WRAPPERS is now just {"nohup"}, which genuinely takes no options. -- needs no entry — it's consumed as a valueless flag.

While verifying, I found the same hole one level down, which also affects the original GHSA-fixed wrappers: _skip_flag_wrapper_args matched value flags by whole-token equality, so bundled short options bypassed too — sudo -nu root git push, env -iu FOO git push, and (pre-existing) nice -qn 10 git push. It now scans the bundle's characters and consumes a separate value only when the value-taking option is the bundle's last character, so -n 10/-o L still consume while -n10/-oL stay attached. This mirrors orchestration.py:236-245, which already got this right for blast_radius.

Fail-safe backstop — new is_unresolved_invocation(), wired into both consumers

The wrapper tables are an enumeration, so I didn't want the next unmodelled wrapper to be another silent ALLOW. A head still starting with - now routes through each policy's existing "can't parse this" path rather than abstaining — ASK in github.py, the configured action in working_dir.py. Reachable today via nohup -- git push …. Detection is shared; the response stays per-policy, per the module's stated contract.

* 1. env -S / --split-string (the blocker). Reviewer was right: modelling -S as a value flag swallowed the command into the flag's value, leaving zero tokens — which is_unresolved_invocation([]) can't see. Fix takes the reviewer's option (b): env -S is a command interpreter like sh -c, so it's unwrapped and re-parsed on the path that already exists for bash -c / eval.

- _skip_flag_wrapper_args gained a capture_flags set and now returns (index, captured) — reusing the existing flag walk (which already handles --flag=v, -S v, -Sv, bundles like -iS v) instead of writing a second scanner.
- real_invocation_tokens stops at env when a split-string is captured; unwrap_shell_command returns it → recursion gates the inner command.

env -S 'git push <evil> main' → DENY. env -S 'npm test' → still abstains.

2. /usr/bin/sudo -u root git push — same fail-open, not flagged in either review. Wrapper lookup matched the bare word only, so a path token became the apparent command and the segment abstained → ALLOW. Wrappers now match on basename (unwrap_shell_command already did).

* fix(policies): add BSD sudo -a/--auth-type and -c/--login-class to value-flag set

These two options were missing from _FLAG_WRAPPERS["sudo"], leaving a
residual silent-ALLOW bypass: sudo -a foo git push ... left "foo" as
the apparent command head, which does not start with "-" so is_unresolved_invocation
could not catch it. Add both flags and tests for each form.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-05 14:24:30 +09:00
Serena Ruan 83c207beb7 test(e2e): deflake scheduled-task time-picker dismiss clicks (#4103)
test_scheduled_task_create_edit_modal_and_time_picker flaked ~30% of runs,
always timing out on `_pick_minute`'s `name_input.click()` with
"dialog-overlay intercepts pointer events". While the time-picker Popover is
open, the Radix Dialog owns pointer hit-testing over the modal, so a normal
actionability-gated click at the input's coordinates resolves to the overlay
and blocks the full 30s under load.

Force every dismiss click on the name input (`click(force=True)`) — the same
technique the picker's open click already uses. A forced click still
dispatches a real pointerdown on the input, which Radix registers as the
interaction-outside that closes the popover, without waiting on overlay
actionability. Covers all three dismiss sites: the retry path and final
dismiss in `_pick_minute`, plus the two post-typed-time blurs in the test body
(focusing the time input reopens the picker via onFocus).

Verified: reproduced the flake (multiple failures across batches of 5-8 runs),
then 12/12 green after the fix; the full file's 9 tests pass.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-05 12:26:30 +08:00
Pat Sukprasert 02bbb7dd4e fix(sdk): validate response model scalars (#4100)
* fix(sdk): validate response model scalars

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(sdk): narrow session stream events (#4101)

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-05 04:23:12 +00:00
Ajay Alfred 408583d52b Polish sidebar density and visual hierarchy (#4085)
* refactor(web): decouple typography from interface geometry

Make the desktop font preference drive semantic text tokens while keeping icons, controls, and spacing fixed.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* refactor(web): migrate interface body text to text-ui

Use the settings-controlled semantic body token across shared components and application pages for consistent sizing.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* style(web): refine sidebar typography and empty states

Align sidebar hierarchy with settings-controlled tokens and make empty projects easier to scan.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* style(web): tighten sidebar density and theme polish

Unify sidebar row geometry, refine theme-specific colors and canvas treatments, and standardize compact controls.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* test(e2e-ui): align font size checks with typography tokens

Update browser assertions for the discrete desktop font token and its current bounds.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* test(ui-snapshot): update typography visual baselines

Adopt the CI-rendered snapshots for the intentional settings-driven typography changes.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* fix(web): preserve dark active sidebar hover

Keep selected row colors stable when hovering in dark mode across both sidebars.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* style(web): polish sidebar actions and overlays

Align sidebar controls, dropdowns, and tooltips with shared density, typography, and interaction tokens for a more consistent visual hierarchy.

* style(web): normalize mobile sidebar scale

Keep mobile sidebar typography and icon geometry predictable without changing the desktop presentation.

* style(web): refine responsive sidebar and chat density

Use responsive sidebar spacing and settings-driven chat typography so mobile and desktop retain clear, consistent reading rhythm.

* test(web): align CI expectations with sidebar polish

Update E2E assertions and reviewed visual baselines to reflect the intentional typography, navigation, and density changes.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

---------

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
2026-08-04 21:17:08 -07:00
Serena Ruan acffa6afed dev/repro-agent: pin the verdict handoff to a single JSON block (#4099)
* dev/repro-agent: pin the verdict handoff to a single JSON block

The output contract only said "a single structured verdict block" without
pinning a format, so the agent rendered YAML on some runs and JSON on others,
and the shape drifted (missing facets, prose bullets instead of objects). That
makes the `verdict` field — which the caller parses to label the issue —
unreliable to extract.

Pin it: exactly one fenced ```json block as the final message, JSON only, every
key always present, and `verdict` restricted to the four lowercase literals so
it matches verbatim. `facets` becomes an array of {symptom, verdict, evidence}
objects instead of free-form bullets. README step 4 updated to match.

Co-authored-by: Isaac

* dev/repro-agent: require the JSON block be the last chunk, allow prose above

Some runs split the artifacts into separate markdown sections (a small
"Reproduction Verdict" block, then prose "Journey"/"Facets" headers) with no
single consolidated handoff, so there was no reliable last block to parse.

Clarify the contract: comprehensive prose above the block is fine, but the
```json block must be the LAST chunk of the final message (nothing after its
closing fence) and must carry the complete self-contained handoff. Explicitly
forbid splitting the artifacts across separate sections/headers. There is no
output-schema enforcement for the claude-sdk agentic loop (AgentSpec.output_type
is inert), so this is enforced by instruction plus last-```json-fence parsing on
the caller side.

Co-authored-by: Isaac
2026-08-05 12:06:27 +08:00
Serena Ruan af98d9b517 fix(web): reseed composer prefill when a project's defaults change (#4097)
The "new session in project" pencil navigates to /?project=<name> while
the landing screen stays mounted. The project-prefill state machine only
restarted when the ?project= param changed, so re-clicking the SAME
project's pencil after editing its default settings kept the stale seeds
— the fix only showed up after clicking another project (or Home) and
back, which flipped the param away and back.

Track a signature of the config the machine last settled from and restart
the prefill when that content changes for the same project, mirroring the
project-switch reset. The saved config is already fresh in the react-query
cache; this makes the machine re-read it.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-05 11:43:23 +08:00
Pat Sukprasert 110676f76e fix(sdk): tighten client helper types (#4096)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-05 10:32:46 +07:00
Ilya Bogin d178e8a363 Add a deep-research example agent (#95)
* Add deep-research example (single agent over an MCP search server)

A single-agent example that answers a question with a cited, cross-checked
report: it plans sub-queries, searches the live web and reads full pages
through an MCP search server, and verifies claims across independent sources.

It is the repo's first example that wires an MCP server via tools/mcp/*.yaml
(auto-discovered), so it also documents the MCP extension path. One agent plus
one MCP server, no sub-agents — the simplest example to copy from. Runs
zero-config against a public, keyless endpoint.

Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>

* test: add e2e coverage for the deep-research example agent

The examples-coverage-sync drift guard (test_every_agent_has_a_dedicated_test_file)
requires every example agent to have a dedicated e2e test. The deep-research
example shipped without one, failing E2E Tests (shard 0/4).

Add a structural test via validate_agent_def_structure (infra-free: the agent's
tools come from the hosted Keenable MCP server and it runs on the claude-sdk
harness, so it can't run end-to-end in CI). Because the agent name 'deep-research'
has a hyphen (not a valid Python test-module name), the test lives in
test_deep_research_example.py and the guard is told via a 'deep-research' entry
in _ALT_COVERED, mirroring the existing 'openai-coder' handling.

Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>

* docs: show deep research search provider options

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-05 03:24:40 +00:00
Enes Yilmaz 300c5fd933 feat(qwen,goose): record delegated fs I/O and gate it with result-phase policy (#1222)
* feat(qwen,goose): record delegated fs I/O and gate it with result-phase policy

Omnigent's OSEnvironment but left two layers as documented follow-ups: the
delegated I/O was invisible in history and no content policy ran on it.

Wire both onto the existing _handle_fs_read / _handle_fs_write handlers:
- emit a paired ToolCallRequest + ToolCallComplete per op so the I/O shows in
  history (the adapter renders them as observed function_call items)
- run PHASE_TOOL_RESULT content policy on the bytes; an explicit deny refuses
  the op (a write is gated before it happens), failing open otherwise

Content-only: the harness policy round-trip carries no request_data, so the
payload is {"result": content}. Closes the file-I/O recording / content policy
item in docs/QWEN_FOLLOWUPS.md.

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(qwen,goose): gate delegated fs at the call phase and audit stale ops

Addresses the review on the delegated-fs recording/policy work.

1. Phase semantics. A delegated write was gated by a result-phase policy eval
   before the write, which is content-only and fails open, so a policy timeout
   would let the write through. Gate writes (and reads) at PHASE_TOOL_CALL with
   the tool name, path, and content, failing closed on an eval error or an ASK
   verdict (delegated fs has no elicitation path). Reads keep the result-phase
   content check that decides whether the read bytes reach the model.

2. Audit records. Stale prior-turn server fs requests were answered at turn
   start, running real I/O, and then had their ToolCall events cleared before
   they reached history. Drain those events into history instead of dropping
   them, so the I/O they performed is recorded.

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(qwen,goose): evaluate result-phase policy after a delegated write

The write handlers gated at PHASE_TOOL_CALL and then wrote, but never ran a
result-phase evaluation, so the value env.write() returned was never policy
checked and the audit record dropped it. Reads already did both phases.

Run PHASE_TOOL_RESULT after the write carrying the actual result. A denial
records BLOCKED and refuses the response; it cannot undo the write, since it
runs after the operation. The success record now carries the real result too,
matching the read path.

_fs_content_policy_denies was read-specific, so it is now
_fs_result_policy_denies and takes any result. Read behavior is unchanged.

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-05 10:04:50 +07:00
Yi Lyu b0ba58283d fix(cli): restore omni server start as a deprecated alias (#3578) (#3597)
PR #3105 removed the `server start` subcommand in favor of
`server --background` and updated the Electron shell-out in the same
commit. The desktop app ships on its own electron-updater channel, so a
client built before v0.7.0 is a normal steady state against a v0.7.0
CLI — and it still runs `omni server start`, which now dies with
"No such command 'start'". "Start locally" is broken for those users.

Restore the subcommand as a hidden alias that routes to the same helper
as the flag, so the two spellings cannot drift. The deprecation notice
goes to stderr; the desktop parses the URL off stdout, which is
unchanged.

Signed-off-by: Yi Lyu <isabellalyu1130@gmail.com>
Co-authored-by: Serena Ruan <82044803+serena-ruan@users.noreply.github.com>
2026-08-05 10:58:38 +08:00
Anas Khan 872ff28bf5 fix(omnidev): pin the pod's backend to Python 3.12 (#3883)
* fix(omnidev): pin backend Python 3.12

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>

* fix(omnidev): reuse Python version pin

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-05 02:51:03 +00:00
Serena Ruan e9dd11258a chore(ci): pause Discord watch rotation schedules (#4094)
Remove the cron schedule triggers from both discord-watch-rotation
workflows so they no longer fire automatically. workflow_dispatch is
kept for manual runs, and the original crons are left commented out so
the schedules can be restored later.

Co-authored-by: Isaac
2026-08-05 09:43:29 +08:00
Corey Zumar 4ae9c9bf46 fix(web): don't show the previous session's model in the composer (#4093)
Switching from a Codex session to a Claude Code session briefly painted
the Codex model (e.g. gpt-5.5) in the Claude session's composer before
correcting itself.

`switchTo` clears the session-scoped model fields but deliberately keeps
`selectedModel`, the cross-session sticky pick, so a CLI-created new chat
inherits the user's last choice. The native picker kind flips to Claude
immediately (the session query and sidebar row are already cached), so
for the whole snapshot round trip the composer resolved the sticky and
read the outgoing session's model.

Only surface the sticky once the session's own catalog vouches for it.
Pre-bind the catalog is empty, so the label waits instead of advertising
a model this session would reject; post-bind it is a no-op, since the
store only ever leaves a catalog-compatible sticky (or the override) in
`selectedModel`.

Co-authored-by: Isaac

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-04 18:42:42 -07:00
Corey Zumar a9400f9959 fix(web): keep reasoning indicators stable during active turns (#4091)
* fix(server): file forked sessions into the source's project

Forking a session filed in a first-class project left the fork unfiled:
fork_conversation built the fork's metadata row without project_id. The
fork route now carries the source's project onto the fork, gated on the
forker owning that project (projects are owner-private, so a fork of a
shared session filed in someone else's project stays unfiled).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): refresh the project folder when a session is forked

A fork inherits the source's project, but the dialog only invalidated the
flat session list — each folder renders its own ["project-sessions", name]
query, which has no poll and converges only on an explicit invalidation.
The push stream can't cover it either: it skips the active session, and the
fork becomes active on navigate. So the clone stayed missing from its folder
until a reload or a re-navigation.

Adds an e2e regression test. It seeds the committed turn the fork action
anchors on straight into the store (new seed_committed_turn helper) instead
of driving a model turn, so it neither waits on nor inherits the flakiness
of the mock-LLM harness.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): stabilize reasoning indicators during active turns

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-04 18:33:34 -07:00
Corey Zumar 7eaae4ab28 fix(web): one host-disconnect UX in the composer badge (#4090)
A dropped host rendered two different indicators depending on incidental
state. The badge read the host tunnel directly (name + red dot), while
ChatPage passed a separate `hostOffline` prop derived from
`liveness.kind === "host_offline"` that replaced the name with generic
"Host is offline — click to reconnect" copy.

`host_offline` is far narrower than "the host tunnel is down": it also
requires the runner to be down (a live runner short-circuits to `online`),
the startup grace to have lapsed, and the host to be non-resumable. So the
same event — the host dropping — showed a passive, unclickable name when
the runner outlived the host, and a nameless reconnect prompt when it
didn't. The name is what tells the user which machine to go restart.

The badge now owns the decision: one shape (name + status dot) that turns
into a button opening the reconnect instructions whenever its bound host is
offline and reconnectable. A dormant resumable managed host stays passive —
the next message wakes it, so `omnigent host` would be wrong advice.

The reconnect dialog's state now comes from the session's host binding
rather than liveness, so a session whose runner outlived its host gets the
`omnigent host` command instead of the local `omnigent run --resume` one.

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-04 18:13:13 -07:00
Zeyi (Rice) Fan fc1997d312 fix(release): generate a Homebrew formula that actually builds (#4080)
Closes #866

The `omnigent` Homebrew formula has not built since 0.7.0, and the tap reported
green anyway, so 0.7.0, 0.8.0 and 0.8.1 all merged with no bottle — every user
compiled from source, and CEL policies silently did not work.

- **Root cause was the CEL migration.** #2970 swapped `cel-expr-python` for
  `cel-python` on the premise that it is pure Python. It is not: `cel-python`
  hard-depends on `google-re2`, whose sdist runs `bazel build` whenever
  `GITHUB_ACTIONS` is set. The bazel dependency moved rather than disappeared.
- **Pin compiled extensions to upstream wheels.** `generate_formula.py` gains
  `WHEEL_REQUIRED` / `PREFER_WHEEL` / `PURE_WHEEL` with abi3 and universal2
  handling, so grpcio (by far the most expensive build), protobuf, regex,
  uvloop, httptools, argon2-cffi-bindings, markupsafe, pyyaml, zstandard and
  google-re2 stop being compiled. Native wheels rank above pure-Python ones, so
  protobuf keeps its upb build instead of the slow fallback.
- **jiter, tiktoken and watchfiles keep building from source.** Their maturin
  wheels carry no Mach-O install-name padding, so Homebrew relocation fails with
  "Failed changing dylib ID" (#866). `pendulum` can go neither way — its wheel
  cannot be relocated and its sdist does not link on 3.14 (pyo3 leaves
  `_Py_NoneStruct` undefined) — so it takes the pure-Python wheel, which ships
  no extension module at all.
- **A dropped dependency is now an error, not a warning.** A missing sdist used
  to be skipped silently, yielding a formula whose venv lacked an import;
  `--allow-no-sdist` is the explicit waiver. The formula test also asserts
  `import re2, celpy`, since omnigent imports celpy behind `try/except
  ImportError` and would otherwise disable policies silently.
- **Delete `update-homebrew.yml`.** It raced `homebrew-tap-pr.yml` on the same
  `release: published` event and asserted on hand-maintained stanzas the
  template no longer emits, so it failed on every run. Its one worthwhile part
  moves into `homebrew-tap-pr.yml`: an admin/maintain gate on manual dispatch
  (it writes to another repo with an App token), plus
  `persist-credentials: false`. Its nightly `schedule` is deliberately NOT
  carried over -- that cron only existed because `brew
  update-python-resources` resolves through pip's `--uploaded-prior-to=P1D`
  window and so could never see a same-day release. The generator runs `uv pip
  compile --no-config` straight against PyPI, so the blindness it worked around
  no longer exists, and a nightly regeneration would just burn a runner to
  print "nothing to do".

Verified by building the generated formula in the tap, not by inspection.

- `omnigent-ai/homebrew-tap#18` contains **verbatim output of this
  `generate_formula.py`** and bottled successfully on macos-15 and macos-26
  (run 30944428771, `bottles_macos-15` / `bottles_macos-26` ≈ 37 MB each). This
  is the check that matters: it proves the generator — not a hand-edit —
  produces a buildable formula, so the next release regenerates something that
  works.
- `omnigent-ai/homebrew-tap#17` carries the same fix for the shipped 0.8.1
  formula and is green on all three runners, with `brew test` running
  `import re2, celpy`. Inspected the bottle: `celpy/__init__.py`,
  `re2/_re2.cpython-314-darwin.so`, and a relocated
  `jiter/jiter.cpython-314-darwin.so`.
- Audited every pinned wheel by replaying Homebrew's own operation,
  `install_name_tool -id <Cellar path>` against each extracted `.so`, so the
  wheel/source split is evidence-based rather than guessed.
- `python3.12 -m py_compile`, `ruff check`, `ruff format --check`, `brew style`
  (no offenses), `ruby -c`, plus stubbed-PyPI unit checks of the new failure
  paths (missing sdist is fatal, `--allow-no-sdist` waives it, abi3 accepted,
  free-threaded `cp314t` rejected).
- Confirmed generator output matches the green formula: same 100 resources,
  identical sdist/wheel split, no non-comment differences.

N/A — release tooling, no user-visible UI.

- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

The generator has no test suite in this repo, and its real contract — "the
emitted formula builds under Homebrew on macOS" — cannot be asserted here. It is
covered instead by building the generated formula on the tap's `brew test-bot`
matrix (homebrew-tap#18, bottles produced on macos-15 and macos-26). The two new
generator failure paths were exercised locally against stubbed PyPI metadata,
and every wheel pin was verified relocatable with `install_name_tool`.

`brew install omnigent` works again, and installs prebuilt wheels instead of
compiling grpcio and friends from source.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-05 00:25:16 +00:00
s-sanjay 71c97d46eb fix(electron): make recent server URLs copyable (#2555) 2026-08-05 08:14:28 +08:00
Avri Chen-Roth 6b17f23c2e feat(boxlite): make box disk size configurable (#4072)
The boxlite SDK's BoxOptions already supports disk_size_gb, but the
omnigent wrapper never threaded it through — every box got the SDK's
own default disk size with no way to override it. Add
sandbox.boxlite.disk_size_gb to the server config, alongside the
existing image/env knobs.

Signed-off-by: Avri Chen-Roth <11185446+the-mentor@users.noreply.github.com>
Co-authored-by: Avri Chen-Roth <11185446+the-mentor@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 16:52:14 -07:00
Ajay Alfred b02449cd40 Make app typography follow interface font settings (#4073)
* refactor(web): decouple typography from interface geometry

Make the desktop font preference drive semantic text tokens while keeping icons, controls, and spacing fixed.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* refactor(web): migrate interface body text to text-ui

Use the settings-controlled semantic body token across shared components and application pages for consistent sizing.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* style(web): refine sidebar typography and empty states

Align sidebar hierarchy with settings-controlled tokens and make empty projects easier to scan.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* test(e2e-ui): align font size checks with typography tokens

Update browser assertions for the discrete desktop font token and its current bounds.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

* test(ui-snapshot): update typography visual baselines

Adopt the CI-rendered snapshots for the intentional settings-driven typography changes.

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>

---------

Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
2026-08-04 16:16:57 -07:00
Mark Tai b2bf6834ba fix(server): end the sender loop and guard generations on host deregister (#4066)
`HostRegistry.deregister` was a bare `dict.pop`, and the host tunnel's receive
loop refreshed `conn.last_frame_at` without checking whether its connection was
still the registered one. The runner side already guards both (
`TunnelRegistry.deregister` takes a session guard and `mark_frame_seen` rejects a
superseded session); the host side did not, so a host could be left in a state
its own route handler never noticed.

Dropping a host registration from outside the route handler did not close the
socket or cancel its tasks. The ping loop kept writing `host_store.heartbeat`, so
the durable row stayed **online** while every `host_registry.get` reported the
host offline. Anything that resolves liveness from that row then waits for a
reconnect the host was never told to make, because from the host's side nothing
happened. `register` already poisons a replaced connection's outbound queue for
exactly this reason; `deregister` now does the same.

Three changes:

- `deregister` queues the `None` sentinel so the sender loop exits and the
  socket tears down, letting the host redial.
- `deregister` takes an optional `conn` generation guard and returns whether it
  removed an entry. The tunnel route gates its `set_offline` write on that
  return, so a superseded handler reaching cleanup after a reconnect replaced it
  can no longer evict the live connection or mark a live host offline.
- `mark_frame_seen` mirrors `TunnelRegistry.mark_frame_seen`: a frame only
  refreshes liveness while its connection is current, and the receive loop stops
  when it is not.

Six tests added to `tests/server/test_host_registry.py`; five of them fail
against the previous behavior.

Co-authored-by: Isaac

Signed-off-by: Mark Tai <mark.tai@databricks.com>
Co-authored-by: Mark Tai <mark.tai@databricks.com>
2026-08-04 16:08:47 -07:00
Corey Zumar 3a4d5bdfac feat(web): fold settled turns behind a 'Worked for Xs' row (#3786)
* feat(web): fold settled turns behind a 'Worked for Xs' row

Once a turn completes, the chat view collapses its whole process trace
(interstitial narration, tool-run folds, resolved approval cards,
reasoning) behind one muted 'Worked for Xs' expander with a hairline
rule, leaving only the final answer visible - mirroring the Codex
desktop treatment so it's obvious where reading starts instead of a
wall of uniform prose. Expanding the row replays the trace inline.

- Live turns keep their trace expanded; liveness comes from the
  bubble's own lifecycle, not session status, so a completed turn
  folds even while a later turn streams (and vice versa).
- partitionTurn splits a settled turn into foldable process, exempt
  always-visible cards (pending elicitations, persistent
  dispatch/routing cards, in-progress spinners), and the trailing
  final answer; a turn with no trailing answer (interrupted / failed
  / tool-only) never folds. Resolved approval cards fold with the
  trace in document order. Codex's trailing turn_diff bookkeeping
  folds as process instead of masquerading as the answer.
- The 'Worked for Xs' duration spans the live stream clock while
  streaming, or the items' server created_at stamps on reload;
  ConversationItem.to_api_dict() now exposes created_at (additive)
  to make the reload path possible.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(stores): expect created_at in the item API-shape round-trip

to_api_dict() now serializes created_at, so the exact-shape assertion
gains the store-assigned stamp.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* docs(web): demo screenshots + cross-clock note for the turn fold

Adds the collapsed/expanded 'Worked for Xs' screenshots referenced by
the PR description, documents that turnWorkedForS's first block picks
the clock branch, and pins the reverse mixed-clock direction
(live-first, epoch-last) as undefined.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): settle the turn lifecycle on bare terminal status edges

The 'Worked for Xs' fold (and the Fork action) only appeared after
navigating away and back: the turn lifecycle finalized live ONLY on a
session.status edge carrying a matching response id, but most idle
publishes carry none (the PTY-activity relay, orchestration teardown).
So a native turn ending on a bare idle cleared 'Working…' while the
bubble stayed 'streaming' forever — settled state was only re-derived
from the snapshot on reload.

- session_status: any terminal edge (idle/failed/waiting) now
  finalizes a still-streaming turn, id-matched or not; cancelled is
  preserved. The stray running->idle pair the policy-deny
  short-circuit publishes mid-turn is healed by
  reviveStrayCompletedResponse: live deltas for the turn flip it back
  to streaming, so the misread is a brief flicker, not a mid-turn
  fold.
- Mid-turn first open: the initial session bind now reopens the
  streaming lifecycle from the snapshot's activeResponseId (mirroring
  reconnectStatusPatch), so a running session's live turn renders
  expanded instead of prematurely folded.
- e2e: test_bare_idle_finalizes_turn_and_folds drives the exact event
  sequence (running+id -> items -> bare idle) against a real server
  and asserts the fold forms in place, no reload.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): fold turns split by a sub-agent await, and ease the collapse

Two gaps in the 'Worked for Xs' fold, both visible on a turn that
dispatches sub-agents.

Fold never formed. Dispatching sub-agents ENDS the parent turn — it
must yield to await their results — and the inbox wake starts a new
turn under a new response id carrying the answer. That splits one
logical turn across bubbles: the first holds narration + tool calls
and no answer, the second holds the answer and no work. The fold
required both halves in ONE bubble, so neither qualified and the
narration stayed spread out unfolded. buildBubbles now flags a bubble
whose turn continues in a later assistant bubble (scanning past the
runtime [System: ...] wake markers, stopping at a real user turn),
and such a bubble folds its whole trace despite carrying no answer.
The flag participates in bubblesEqual so the memoized bubble actually
re-renders when its continuation lands.

Collapse was abrupt. The settled render swapped a tall expanded trace
for a one-line row in a single frame, which read as a partial page
reload. The fold now MOUNTS OPEN when the turn settles on screen and
closes on the next frame, so the steps visibly fold into the summary
row; settled history still mounts closed (nothing to animate away).
The height animation lives in index.css because it needs Radix's
measured --radix-collapsible-content-height, and is disabled under
prefers-reduced-motion.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): remove the jolt at the start of the turn-fold collapse

The collapse read as two motions. Measuring the bubble height every
frame through a settle showed why: inserting the summary row and the
fold's own padding/border grew the bubble ~43px TALLER in one frame,
and only then did the 200ms collapse run — a jolt up, then a ramp
down.

- The summary row now grows in (grid-template-rows 0fr -> 1fr) over
  the same beat instead of appearing at full height, so row expanding
  and trace shrinking net one monotonic shrink.
- The animated element carries no padding or border of its own: any
  chrome there is height that lands before the collapse starts, which
  is exactly the jolt. Expanded spacing comes from the row's hairline
  above and the message column's gap below.
- The fold also animates when it appears on an already-mounted bubble,
  not only when the turn itself settles — a turn split by a sub-agent
  await folds when its continuation lands, and that case was snapping
  shut with no animation at all.

Measured on a live server, same turn shape both times: leading jolt
43px -> 10px, and both the plain and sub-agent-split cases now show a
single animated ramp instead of a jump followed by one.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): stop the turn fold oscillating on codex sessions

On codex the fold flipped collapsed/expanded repeatedly as a turn
streamed. Instrumenting a live turn showed why: the server recorded
that turn as ONE response, but the client showed five to seven
bubbles. A streamed narration renders as its own transient 'live:'
preview bubble until its authoritative item replaces it, and reasoning
bursts group separately, so bubbles appear and merge away on every
delta. Each appearance gave an earlier bubble 'a later assistant
bubble' and marked it continued, folding a fragment; the merge
unmarked it and unfolded it again. Two fragments folded mid-turn as
'Worked for 1s' / 'Worked' rows carrying only a reasoning burst.

- markContinuedTurns only runs between turns: while a response is
  streaming the transcript is mid-restructure, so nothing is marked.
  Marks are sticky, so a bubble that has folded never reopens when the
  next turn starts streaming.
- A continued bubble must also have RUN something (a tool call in its
  process) to fold. That is the shape the flag exists for — narration
  plus tool calls, then a yield to await sub-agents — and it keeps a
  narration- or reasoning-only fragment from folding into a lone
  'Worked' row with nothing behind it.

Measured on live codex turns, same prompt shape: fragments folding
mid-turn 2 -> 0, and the only remaining fold is the real one at turn
end.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): never fold a bubble made only of streaming artifacts

Residual codex flicker: the fold still appeared and vanished mid-turn,
just less often. This path never involved the continued flag, which is
why the previous guards only reduced the frequency.

Codex splits an in-flight turn into fragment bubbles — a reasoning
burst (ctx.itemId is null until its item is finalized) plus a 'live:'
narration preview. Their synthetic response id never matches
activeResponse, so walkBubbles labels them 'completed', and a fragment
holding reasoning + text satisfied the ordinary process-plus-answer
rule and folded. When the authoritative item replaced the preview the
fragment merged away and its fold went with it.

A genuine turn always carries at least one server-assigned item id, so
a bubble whose items are ALL null-id or 'live:'-prefixed is a fragment
of the turn still arriving and never folds. LIVE_ITEM_PREFIX moves to
lib/blocks.ts so the renderer and the store share one definition.

Verified by assertion: before this change a reasoning + live-preview
bubble rendered a fold; now it renders expanded. Two frame-exact
recordings of the reported prompt (63k frames, with approvals) showed
no fold disappearing, so this was found by construction rather than by
reproducing it live.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): render a native turn as one bubble live, so it folds like it does on reload

Root cause of the codex fold flicker, found by comparing the same
conversation live vs reloaded: 4 assistant bubbles and NO fold live, 1
bubble folded after a reload. A native turn was being split into
fragment bubbles while streaming and merged back into one on reload,
so the two views disagreed. walkBubbles groups by response id, and
three kinds of block carried the wrong one:

- Live text previews were stamped with a synthetic 'live:<id>' as
  their response id, so each streamed narration broke the run. They
  now adopt the live turn's id (falling back to the synthetic id when
  no turn is tracked, so a preview can't join an unrelated bubble).
- A native harness emits no response.created, so the reducer never
  learned the turn id and stamped its own blocks (reasoning, streamed
  text) with a stale or empty one. A 'running' status edge carrying a
  turn id IS the native turn-start signal, so the reducer adopts it --
  without sealing an already-open section, since codex opens reasoning
  ~2s BEFORE that edge lands and closing would split one thought in
  two.
- Blocks emitted in that ~2s window still carry no id, so the store
  attributes the trailing unattributed run to the turn when the edge
  names it.

With one bubble per turn, the fold condition stops oscillating: it was
flipping because the fragment boundaries moved as previews appeared
and merged, so whichever fragment momentarily had the
process-plus-answer shape folded and then unfolded.

Also: a trailing reasoning item no longer blocks the fold. Codex opens
a reasoning section as the turn ends, landing it after the final
message; reasoning is process, never the answer, so it peels into the
trace like the turn_diff wrap-up already did.

Measured on the reported prompt (with approvals), same shape each
time: bubbles 4 -> 1, and fold transitions went from 'never appears
live' to exactly one 0->1 the instant the turn ends, with zero
decreases (no flicker).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): make bubble grouping and the turn fold robust to a mid-turn connect

The codex flicker survived the response-id stamping fixes because their
premise was fragile: they depend on the client CATCHING the one
'running' status edge that names the turn. A tab that connects (or
reconnects) mid-turn never sees it — SSE replays no status edges — so
reasoning blocks carry rid "" and live previews fall back to their
synthetic ids. Attaching a fresh client to the reporting user's live
session reproduced it exactly: the persisted turn was ONE response, but
the page rendered up to ELEVEN bubbles, five of which folded mid-turn,
including one fold flip back open.

Two structural fixes, replacing edge-dependence with invariants:

- walkBubbles no longer splits a bubble on ANONYMOUS response ids
  ("" or live:*): such blocks only ever come from the live stream of
  the turn around them, so they join it, and a group that OPENED on
  anonymous blocks adopts the first real id that arrives. One turn is
  now one bubble regardless of which edges the client happened to see.
  Bubbles also stop keying off transient live: preview ids, so the
  authoritative-item swap no longer remounts the bubble.

- The LAST assistant bubble never folds while the session is running,
  even when its lifecycle reads settled — a mid-turn connect misreads
  the live turn as 'completed', and folding it collapsed and reopened
  the trace as its tail alternated between text and tools. The
  session's terminal status edge folds it, which is the natural moment
  anyway. Earlier bubbles still fold as usual while a later turn runs.

Verified by attaching mid-turn to a live codex run of the reported
prompt (with approvals): before, 8+ bubbles with 5 mid-turn folds and
a fold flip; after, one bubble, expanded throughout, folding exactly
once when the turn ends.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): one bubble — and one 'Worked for' fold — per user turn

The reported flow (a codex step-wise/goal turn) rendered SEVEN
'Worked for Xs' folds under one user message: codex publishes a
distinct response id per STEP on its status edges while the items all
carry the thread id, so each step opened a new bubble, and every
settled fragment folded separately once the turn ended. The server had
persisted the whole thing as ONE response.

- walkBubbles now groups ONE bubble per user turn: a response-id
  change between two assistant blocks with no user message between
  them is a continuation (step-wise sub-turns, retries, pre-edge
  blocks), not a new turn. The group tracks the LATEST real id so
  lifecycle follows the live edge. Blocks stamped a distinct id ON
  PURPOSE — deny/failure sentinels and REQUEST-phase elicitations —
  still open their own bubble, in both directions.

- Fold appearance is debounced (500ms of held eligibility): a
  step-wise turn's between-step idle edge, or a stray idle before its
  revive, reads settled for a moment and would otherwise fold and
  reopen the trace. Losing eligibility hides the fold immediately, and
  settled history still mounts folded with no delay.

Tests that pinned per-response grouping modeled adjacent turns with no
user message between them; real streams separate turns with one (the
inbox wake marker in the sub-agent flow), so they now include it. The
reducer-driven reused-callId test keeps its no-cross-pollination
assertions within the merged bubble.

Verified live: a simulated 5-step turn (distinct per-step edge ids,
one thread id) renders one bubble with zero mid-run folds and exactly
one fold at the end, and a real codex approval run folds once, 0.5s
after the turn ends.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): don't fold a live turn's partial work on a mid-turn refresh

Refreshing while a turn was parked on an approval collapsed the
partial trace into a premature 'Worked for' row. Two holes let the
last-bubble fold suppression miss the live turn on reload:

- The parked elicitation forms its own trailing assistant bubble whose
  card ChatPage floats to the page bottom, leaving the bubble
  item-less (it renders null) — and that phantom was counted as the
  'last assistant' bubble, handing the actual trace to the fold.
  lastRenderableAssistantIndex now skips item-less bubbles.

- On a step-wise codex turn the snapshot's active_response_id names
  the STEP id while the items carry the thread id, so on reload the
  trace's lifecycle reads 'completed' even though the turn is parked.
  A pending elicitation now suppresses the last bubble's fold
  directly: a card awaiting the user proves the turn is in flight
  regardless of what the lifecycle or session status read.

Verified live: reloading a session parked on a codex command approval
keeps the trace expanded with the card visible, and a simulated
mid-turn reload with the step/thread id mismatch stays expanded until
the terminal idle edge, then folds once.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): no 'Worked for' flash when a reload lands between turn steps

Approving an elicitation and refreshing flashed the fold: a step-wise
codex turn publishes an idle edge when each step completes, and a
reload landing in the between-step gap reads fully settled — status
idle, no pending card, trace ending in narration text — so the fold
mounted instantly (the settled-history fast path), then the next
step's running edge cancelled it. Reproduced deterministically: fold
at 0.27s, gone at 1.66s.

Nothing in that snapshot can distinguish the gap from a real turn end,
but the trace's AGE can say how ambiguous it is: items carry server
created_at stamps, so the bubble now records its newest item's time.
The last assistant bubble mounted over a JUST-active trace (newest
item < 15s old) holds its fold for 3s instead of showing it instantly
— long enough for the next step's running edge to cancel it, so the
gap reload never folds at all. A reload after a genuine turn end folds
once the hold elapses, and old history still mounts folded with no
delay.

Verified live against the simulated gap: reload-in-gap shows no fold
ever (was flash-then-hide), reload-after-real-end folds at ~3s, and
stale-history mounts fold instantly (unit-tested).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): don't pop a settled turn's fold open while the next turn spins up

Once a real user message follows the last assistant bubble, a running
status belongs to the reply-in-flight for that newer input, so the
settled bubble's 'Worked for' fold must not be suppressed. Closes the
opencode dip where the prior fold opened for seconds until the new
turn's first item mirrored through the TUI.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* feat(web): scroll the expanded 'Worked for' trace into view

Clicking the fold expands the trace above the reading position, and the
browser's scroll anchoring keeps the answer below it stationary — the
work opens off the top of the viewport and the click looks like a no-op.
On a user-initiated expand whose row+trace don't fit the scroller, snap
the fold row to the top (before paint) so the trace reads from its
beginning. Fits-on-screen expands and the programmatic mount-collapse
animation don't scroll.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e): cover the 'Worked for' fold across native wire shapes

Four deterministic events-API tests: step-wise per-step status edges
fold once with no mid-run flicker; items that switch response id
mid-turn still yield one fold per user message; a mid-turn reload keeps
partial work expanded until the terminal edge; and a settled turn's
fold holds through a follow-up send's item-less gap.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): always snap the fold row on user expand

The fits-on-screen fast path never held in practice: on the last turn
the stick-to-bottom scroller treats the 200ms expand animation as
appended content and re-pins the bottom, and elsewhere native scroll
anchoring pins the answer below — either way the growing trace glides
the row off the top and the click looks like a no-op. Snap the row to
the scroller top on every user expand (the upward scroll also unpins
stick-to-bottom) and park overflow-anchor for the animation.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): make the fold's expand snap win against the bottom-lock

Clicking 'Worked for' while the view is pinned at the bottom (the
resting position on the last turn) did nothing: the expand animation
opens at height 0, so the snap clamps against a scroller with no room,
and stick-to-bottom's resize handler then rides the growth to the
bottom — programmatic scrolls never unpin it. User expands now open at
full height in one frame (no height animation), release the bottom-lock
via a null-safe ConversationScrollLockContext (same recipe as
JumpToTopButton), and then snap the row to the top.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): land the fold's snap below the chat top fade

The snap parked the row 8px below the scroller edge — inside
chat-scroll-fade's transparent band (opaque only from 80px), so the
'Worked for' label sat scrolled-to-top yet invisible. The row's
scroll-margin-top now lives next to the fade definition (88px, plus the
iOS inset variant) so the two can't desync.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): restore the session busy signal when a live delta revives a turn

A stray idle edge clears sessionStatus before the revive flips the
turn back to streaming, so shouldQueueSend saw an idle session and let
a mid-turn send bypass the queue (and the Working indicator stayed dark
until the next running edge). The delta that triggers the revive proves
the session is mid-turn — restore sessionStatus: 'running' with it.
Local send status stays untouched: cross-client and TUI-typed turns
have no local send in flight.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* docs(web): drop the turn-fold demo screenshots from the repo

The PR description references them by pinned commit SHA, so the binary
assets don't need to live in the tree.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-04 15:51:15 -07:00
Zeyi (Rice) Fan c1af638b9f fix(claude): send x-databricks-use-coding-agent-mode header to Databricks AI gateway (#4082)
## Related issue

N/A

## Summary

- The Databricks AI gateway only serves Claude requests in coding-agent mode when the `x-databricks-use-coding-agent-mode: true` request header is present; omnigent's Claude launches to the gateway did not send it.
- `ClaudeSDKExecutor`'s Databricks gateway env (`_resolve_gateway_env`) and native-claude's ucode launch config now pass `ANTHROPIC_CUSTOM_HEADERS=x-databricks-use-coding-agent-mode: true`, which Claude Code forwards verbatim as request headers (this survives the thinking-display gateway shim, which forwards all request headers).
- Generic-provider gateway envs (non-Databricks `key`/`gateway` providers) deliberately do not receive the header.

## Test Plan

- `uv run pytest tests/test_claude_native.py tests/inner/test_claude_sdk_executor.py -q` — 283 passed.
- Updated the ucode env exact-equality assertion and gateway-env tests to assert the header; added `test_generic_provider_gateway_omits_databricks_header` to pin the Databricks-only scoping.
- `uv run ruff check` and `uv run ruff format --check` on the touched files — clean.

## Demo

N/A

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

N/A

## Changelog

Claude sessions routed through the Databricks AI gateway now send the `x-databricks-use-coding-agent-mode` header the gateway requires

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-04 22:11:42 +00:00
Dhruv Gupta 70fdbdf0cd chore(triage): pause aravind-segu as a review owner (#4079)
Moves aravind-segu from `owners` to `owners_paused` in the 12 areas they
owned, so PR reviewer assignment and issue triage stop routing to them.
Readers use only `owners`; the 2+ owner check counts paused owners, so no
backfill was needed and no area is left without an active owner.

`policies` is now down to a single active owner (TomeHirata).

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-04 14:45:44 -07:00
Corey Zumar 7558fde43f Add ajayalfred to MAINTAINER list (#4078) 2026-08-04 14:28:28 -07:00
Corey Zumar 943e964c79 fix(codex-native): clear the MCP startup band once the model starts working (#4071)
* fix(codex-native): clear the MCP startup band once the model starts working

The web chat showed 'Starting MCP servers (3/4): <name>' underneath an
agent that was visibly already working, sometimes for minutes.

Codex delivers per-server startup edges only to the connection that owns
the thread, so the forwarder synthesizes the round and settles it when
the thread goes idle after a turn, or when a config-derived window
elapses. Both are late: a server that never reaches a terminal state
(e.g. a misconfigured command that never handshakes) keeps the band
pinned for the whole first turn, and the window stretches to the slowest
configured startup_timeout_sec plus grace (135s for a 120s budget).

Settle on the first model-produced turn item as well. Codex defers turn
EXECUTION until the startup round ends, so assistant-side output proves
the round is over while the turn is still running - the same invariant
the idle-edge settle already relies on, observed at the earliest point
it can be. The band now covers only the genuine pre-turn wait.

The turn's userMessage item is excluded, and only parent-thread events
count: a turn is ACCEPTED (thread flips active, user message
materializes) mid-startup, and a collab child's turn says nothing about
the parent's round.

Two adjacent fixes fall out: a mid-turn reload no longer re-shows the
stale band from the session snapshot, and hitting Stop during a first
turn no longer reports 'cancelled' for servers whose startup had in fact
finished. The failed-turn diagnostic that names still-pending servers is
unaffected - a failed turn/start produces no model output, so no settle
precedes it.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(codex-native): settle the MCP round once, and add before/after visuals

Addresses review feedback on the settle-on-model-output change:

- Settle at most once per forwarder connection. The round is seeded
  once per connection and never on thread rotation, so once model
  output settles it the outcome cannot change; without a guard every
  later item in the session re-read the bridge file to reach the same
  idempotent no-op. A state flag short-circuits them, and the new test
  re-populates the map behind the flag so dropping the guard fails
  rather than passing on idempotency alone.

- Add the before/after chat captures the review asked for, taken at the
  same point in the turn (agent running 'sleep 40') against servers
  built from the same web UI, differing only in this fix.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* chore(codex-native): drop the committed demo screenshots

The before/after captures don't need to live in the repo; the same
evidence is in the PR description as the sampled A/B table and the
runner-log timeline.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-04 14:16:23 -07:00
Gen Li cb8b3cf01a fix(cursor-native): merge Omnigent MCP bridge into existing mcp.json instead of overwriting (#3119)
* fix(cursor-native): merge Omnigent MCP bridge into existing mcp.json instead of overwriting

write_mcp_config() previously called build_mcp_config() which returned a
dict with only the Omnigent bridge MCP server, then wrote it wholesale to
.cursor/mcp.json. This destroyed any user-configured MCP servers.

Now read the existing mcp.json, merge the Omnigent entry into mcpServers
leaving other keys intact, and write back the merged config.

Fixes #3083

Signed-off-by: lg320531124 <155300404+lg320531124@users.noreply.github.com>

* fix(cursor-native): guard malformed mcp.json and cover the merge path

A hand-edited .cursor/mcp.json can hold any JSON shape. The merge read it
and indexed straight into it, so a list/null root or a non-dict mcpServers
raised AttributeError/TypeError and took down the session launch, where the
old overwrite-always code could not.

Discard non-dict shapes before merging, swap the try/except/pass for
contextlib.suppress (SIM105), and add tests for the merge path (user server
plus a sibling top-level key survive) and the malformed shapes.

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* fix(cursor-native): type the mcp.json merge against JsonObject

main moved this module off typing.Any onto JsonObject (dict[str, object]),
so the merge's `dict[str, Any]` annotation broke ruff F821 and pyrefly
once rebased, and indexing the object-valued mcpServers failed bad-index.

Narrow the loaded JSON with isinstance into a local `servers` dict (the
pattern opencode_native_provider already uses) and bind it back into
`existing`, so the write lands through the alias and stays typed.

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: lg320531124 <155300404+lg320531124@users.noreply.github.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-04 21:02:07 +00:00
Dhruv Gupta a2a4bcbeac docs: use non-matching placeholder DSNs in docstrings to quiet secret scanners (#4075)
The example Postgres URLs in these docstrings are placeholders, but gitleaks'
`postgres-connection-string` rule matches the `scheme://name:secret@host` shape
and can't tell a placeholder from a live DSN. That makes them permanent false
positives: they show up in GitGuardian digests, and the Databricks pre-push hook
re-flags them on every new-branch push, since pushing a new branch re-scans
commits already on main. Working around that means reaching for
SKIP_SECRET_SCAN, which is a habit worth not having.

Switching the examples to angle-bracket placeholders sidesteps the rule (`<` and
`>` fall outside its username/password character classes), and reads more
clearly as a placeholder besides.

Docstrings and comments only: with docstrings stripped, the AST of every touched
file is byte-identical to before. Test files are deliberately left alone: their
URLs are live inputs and expected values, and one case exists specifically to
prove percent-encoded credentials survive the prefix rewrite, so rewriting it
would defeat the test. Those remaining findings are best marked as false
positives in the scanner instead.

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-04 13:52:38 -07:00
Dhruv Gupta 60c41a0e2d docs(release): scrub the private secure-release repo name from the public repo (#4069)
The private Databricks secure-release repo was named in 9 places: three
workflow header comments, the `release.yml` run summary, a design-doc table
row, and four direct links into the private repo's file tree from
`editors/vscode/PUBLISHING.md`. None of it resolves for anyone outside
Databricks.

`release.yml` printed the name into its run summary on every release. Public
run summaries are world-readable, so a repo variable would keep leaking it.
The summary now prints the full command with `<secure-release-repo>` as the
only placeholder, so a release manager still gets something to paste and fill
in, and points at the runbook for the value.

The rest is a straight substitution to "a Databricks-internal secure-release
repo". `PUBLISHING.md` keeps the build half and defers the repo name and
workflow paths to the runbook.

No behaviour change: no trigger, input, permission, or step logic is touched.
The only executable change is the summary `echo` block, verified by extracting
it from the YAML and running it.

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-04 13:37:09 -07:00
Dhruv Gupta cf9e745f43 docs(release): move the release runbook to the internal repo (#4068)
`RELEASING.md` documents the whole release pipeline, including the private
Databricks secure-release repo, its workflow filenames, and its dispatch
inputs. A public reader can't act on any of that, so per the thread with Corey
and Rice it moves to `omnigent-internal` (`RELEASING.md`).

This deletes the file here and repoints the six inbound "see RELEASING.md"
pointers (4 workflows, the changelog script) at "the maintainer release
runbook", so nothing links to a path that no longer exists.

Scrubbing the private repo name from the workflow comments and
`editors/vscode/PUBLISHING.md` is a separate follow-up.

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-04 13:35:03 -07:00
Anthony Ivan 6ac341819e fix(codex-native): trust headless session workspace (#3709)
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
2026-08-04 19:24:18 +00:00
Annie Zhou 322e50f56f feat: name all application database queries (#4059)
Signed-off-by: AnnieZhou08 <yuting.zhou@databricks.com>
2026-08-04 19:07:15 +00:00
Solaris-star 420199e988 fix(sessions): expose persisted activity heartbeat (#3279)
* fix(sessions): expose persisted activity heartbeat

Signed-off-by: Solaris-star <820622658@qq.com>

* docs: broaden updated_at wording to cover session metadata edits

Per review feedback: updated_at also advances on title renames
(including auto-titling), agent switches, and archive toggles — not
just conversation item appends. An orchestrator treating it as a pure
item-append heartbeat should know a mid-stall rename resets the clock.

Broadened the docstring in SessionResponse and the SDK Session class,
and re-ran scripts/dump_openapi.py so the OpenAPI description matches.

---------

Signed-off-by: Solaris-star <820622658@qq.com>
2026-08-04 19:06:13 +00:00
Evan Goh db6e7c5e5a Fix Kimi harness login detection and remove broken logout (#3292)
Two bugs in the Kimi Code (kimi) harness integration:

Bug 1 - Omnigent could never detect a completed kimi login. The KIMI_KEY
install spec had no file-based login detector and the setup overview row was
hardcoded to "Not configured"/warn whenever the CLI was installed, so a
successful `kimi login` always showed as not signed in.

Fix: add a subprocess-free detector `kimi_auth.kimi_login_detected()` that
returns True when `~/.kimi-code/credentials/kimi-code.json` exists and is
non-empty (the file `kimi login` writes; verified against kimi CLI v0.29.1),
mirroring the Gemini `gemini_login_detected()` pattern. Wire it into
`harness_readiness._FAMILY_CREDENTIAL_CHECK` (binary + credential gating, like
agy) and make the setup overview row render green "Signed in" when detected.

Bug 2 - Sign-out was broken. The spec declared `logout_args=("logout",)` but
kimi has no `logout` subcommand (`kimi logout` errors "unknown command" on
v0.29.1). Set `logout_args=None` so `harness_logout` is a no-op for kimi (same
as Qwen / agy) and remove the "Sign out (kimi logout)" row and its branch from
the Kimi drill-in. Docstrings/comments claiming kimi ships `kimi logout` are
corrected.

Tests: add tests/onboarding/test_kimi_auth.py (present/absent/empty credential
via tmp paths), update the harness_install/harness_readiness onboarding tests
for the new logout_args=None and binary+credential readiness, and update the
CLI drill-in / setup-overview tests (no sign-out row; signed-in vs
not-configured overview row).

Signed-off-by: evangoh122 <evangohsg@gmail.com>
Signed-off-by: Evan Goh <authoremail@example.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 12:02:36 -07:00
John Surles f19a3acecd Fix #3550: support additional OIDC signing algorithms (#3661)
Signed-off-by: 0utsights <surlesjohn@outlook.com>
2026-08-04 19:00:24 +00:00
Pranav Setlur 15399a600d fix(claude-native): apply web plan verdicts to the TUI plan dialog (#4067)
Approving a plan from the web UI did nothing: the card showed as
approved but the plan never ran, and answering in the terminal view was
the only way through. Claude Code ignores a PermissionRequest hook's
`allow` for ExitPlanMode (that dialog only accepts a TUI answer), so the
`setMode` decision the server builds never took effect. As a result
Claude's `auto` mode was unreachable from the web UI, since the plan
card is the only surface that offers it.

Key the verdict into the pane instead, the way a local user would:
option 1 for accept-with-auto-mode, 2 for accept, Escape for reject.
The bridge only presses a key when the plan dialog is actually on
screen, which keeps a non-plan verdict (or one already answered in the
terminal) a no-op. Rides the approval event the server already forwards
to the runner, so no new event type or server plumbing.

Co-authored-by: Isaac

Signed-off-by: Pranav Setlur <psetlur@gmail.com>
2026-08-04 18:58:48 +00:00
Thomas Jankowski c78c7dc01f fix(agy): wait for model readiness before cold start (#3878)
Signed-off-by: TJ@axp-dev <prawiefiolek@gmail.com>
Co-authored-by: TJ@axp-dev <prawiefiolek@gmail.com>
2026-08-04 18:57:28 +00:00
Randy 🌞 fb7c08c0a3 fix(databricks): wire project_store in the Databricks Apps entrypoint (#3866)
The Databricks Apps entrypoint built every other store but never the
project store, and create_app mounts the projects router only when a
project store is wired — so first-class Projects were non-functional
on every Databricks Apps deployment while the bundled web UI still
offered project creation. The CLI server and Docker entrypoint paths
already wire it.

Construct SqlAlchemyProjectStore from the Lakebase DB URI and pass it
to create_app, mirroring the other stores.

Co-authored-by: Isaac
Claude-Session: https://claude.ai/code/session_01P9dr2dYHrwMvnXvJsjLDKk

Signed-off-by: Randy 🌞 <randypitcherii@gmail.com>
2026-08-04 17:47:57 +00:00
Annie Zhou 8e17c9ec08 feat: expose semantic database query names (#4007)
Signed-off-by: Annie Zhou <19739773+AnnieZhou08@users.noreply.github.com>
2026-08-04 15:32:41 +00:00
Hubert 5e9f9479fd Central CTA + background bugfix (#4052)
* Central CTA + background bugfix

Landing screen:
- Headline moves to Hanken Grotesk at 400 weight ("What should we build?"),
  self-hosted via @fontsource-variable so no CDN is involved, exposed as the
  `font-display-alt` token.
- The project variant swaps the bare folder glyph for a pink rounded tile,
  using a new `tag-pink` token from the design's tag palette.
- The composer placeholder and its aria-label now name the selected project
  ("Start a new session in <project>") instead of always reading the generic
  task prompt.

Bug fix — the mobile sidebar was see-through. Below md the sidebar is a
full-screen overlay on top of the chat, but the per-theme canvas rules paint
it with the `background` shorthand, which resets background-color and silently
overrode Sidebar.tsx's max-md:bg-card-solid; the dark stack is entirely
translucent, so the conversation showed straight through. Restores an opaque
fill under the gradients below md only, at matching specificity and after the
theme rules, so desktop keeps its intended translucency.

Adds regression tests for that contract, and updates the landing-screen tests
and visual-suite docs for the new headline.

Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-04 15:55:13 +02:00
Tomu Hirata 566fc5bb5a perf(runner): bound per-runner memory via glibc arenas + threadpool cap (#3901)
* perf(runner): bound per-runner memory via glibc arenas + threadpool cap

Each session spawns its own runner process, and each grows to ~200MB in
prod, over-using host resources. Profiling shows ~123MB is the irreducible
import floor; the growth on top is runtime bloat from threaded Python on
glibc: the runner offloads heavily via asyncio.to_thread, the default
executor sizes to min(32, cpu+4) threads, and glibc opens up to 8*ncpu
malloc arenas that never return to the OS. Nothing tuned any of this.

Three low-risk, env-gated levers (all no-ops or benign off Linux):

- MALLOC_ARENA_MAX=2 + a 128 MiB trim threshold, injected into the runner
  child env at both spawn sites via a shared _proc.malloc_tuning_env()
  helper. Empty off Linux; OMNIGENT_RUNNER_MALLOC_ARENA_MAX=0 reverts.
- Cap the asyncio default executor at 8 workers (runner.threadpool_max_workers
  config key, OMNIGENT_RUNNER_THREADPOOL_MAX_WORKERS env override), set before
  any to_thread use so the 20-thread default pool is never created.
- gc.freeze() after app construction to drop the static import graph from
  GC's tracked set.

This targets the runtime growth, not the import floor; collapsing the floor
itself (a copy-on-write zygote) is tracked separately.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): apply the glibc arena cap at the zygote exec

The zygote forkserver landed and is now the default runner spawn path, which
silently defeated this branch's MALLOC_ARENA_MAX injection. glibc reads that
variable once, when its allocator initializes at exec; a zygote-forked runner
never execs, it just replaces os.environ, so the value arrived far too late to
configure an allocator and the cap stopped applying to every runner.

Move the injection to the zygote's own Popen -- the single real exec on this
path -- so all forked runners and harnesses inherit an already-capped
allocator. Two tests pin the contract at that boundary, including that an
operator's explicit export still wins.

The other two levers on this branch (the 8-worker threadpool cap and
gc.freeze()) live inside _run_tunnel_from_env, which every runner reaches
regardless of how it was started, so they were unaffected. Note in
malloc_tuning_env why the arena cap is glibc-only: macOS libmalloc uses
per-CPU magazines with madvise reclaim and ignores MALLOC_ARENA_MAX, so macOS
hosts get their reduction from the threadpool cap (measured: 21 threads -> 9).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 13:49:59 +00:00
Tomu Hirata d15fa5f90e fix(lint): suppress pyrefly missing-import on optional nimble-python (#4053)
nimble-python is the optional `nimble` extra and the import is already
guarded by try/except ImportError. Pyrefly has no way to know it's
intentionally absent, so annotate with `# pyrefly: ignore[missing-import]`
to silence the false-positive without changing runtime behaviour.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 13:46:26 +00:00
Yuan Tang 0af9ad141a feat(policies): add force-push protection to GitHub policy (#3570)
* feat(policies): add force-push protection to GitHub policy

Add a `deny_force_push` parameter (default `True`) to the GitHub
policy that blocks `git push` with force flags (`--force`, `-f`,
`--force-with-lease`, `--force-if-includes`) regardless of
repo/branch allowlists. This prevents agents from rewriting remote
history, which can destroy commits and break collaborators' clones.

The check fires before repo/branch gating so even a force push to
an undeterminable remote alias is denied rather than surfaced as ASK.
Set `deny_force_push=False` to let force pushes through normal
write gating.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(policies): merge startswith calls to satisfy ruff PIE810

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* style(policies): join force-push condition onto one line for ruff format

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-08-04 13:24:07 +00:00
Tomu Hirata 4d4ddb2617 feat(runner): copy-on-write zygote forkserver for runner processes (#3921)
* spike(runner): measure copy-on-write savings from a warm-fork zygote

Each session spawns its own runner process, and each pays a ~123MB import
floor for omnigent's own graph plus pydantic/fastapi/httpx. Runtime tuning
trims the growth on top but can't touch that floor; the only way to collapse
it is to import the graph once in a warm parent and os.fork() a child per
session, sharing the read-only import pages copy-on-write.

This standalone script measures whether that COW sharing actually
materializes before we commit to the full zygote architecture. It imports the
runner graph once, forks N idle children, and reports aggregate memory against
an N-process Popen baseline, optionally with gc.freeze().

Not wired into the daemon — this is a measurement gate, not a feature. On this
macOS box (N=8) the fork path showed ~82% lower aggregate footprint than the
Popen baseline, but macOS phys_footprint is only an indicative analog to Linux
Pss and the children idle (no COW erosion from refcount page-dirtying), so a
Linux-under-load measurement is still required before productionizing.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(runner): add copy-on-write zygote forkserver for runner processes

Every session spawns its own runner, and each pays the full ~120MB import
floor (omnigent's graph + pydantic/fastapi/httpx). On a host running N
sessions that floor is duplicated N times. This adds a zygote: a single
long-lived process that imports the runner graph once and os.fork()s a child
per session, so on Linux the read-only import pages are shared copy-on-write
and each extra runner costs only the pages it dirties.

Design (grounded in the daemon/runner lifecycle, not the naive sketch):

- omnigent/runner/_zygote.py — the forkserver. Single-threaded, no event loop
  or network; imports the graph once, gc.freeze()s it, then blocks on an
  AF_UNIX control socket forking a child per request. The child reopens its
  log, replaces os.environ with the request env, and calls the unchanged
  _entry.main() — so it behaves exactly like `python -m omnigent.runner._entry`.
  It is Popen-exec'd by the daemon (never forked from it), so it inherits none
  of the daemon's asyncio loop / websocket / worker threads — the classic
  fork-in-multithreaded-async deadlock is avoided by construction.

- omnigent/host/runner_zygote.py — the daemon-side client. ZygoteManager owns
  the control socket; ZygoteRunnerProc is a Popen-shaped shim so the existing
  _RunnerHandle / _watch_runner / _handle_stop paths are unchanged. The daemon
  is NOT the forked runner's parent, so poll()/returncode/wait() round-trip to
  the zygote (the real parent) for exit status while terminate()/kill() signal
  the pid directly.

- connect.py — _handle_launch forks via the zygote when enabled, else the
  original Popen. RUNNER_PARENT_PID is set to the ZYGOTE's pid (not the
  daemon's) because the runner's orphan watchdog compares os.getppid(); daemon
  death -> control-socket EOF -> zygote exit -> runners reparent -> each tears
  itself down, preserving today's parent-death semantics through one hop.

Gated behind OMNIGENT_RUNNER_ZYGOTE=1 and Linux-only; any zygote failure
disables it for the daemon's life and falls back to a direct Popen, so it is
never a hard dependency. Also removes the Phase-1 measurement spike script,
which this supersedes.

Verified on macOS: a real zygote subprocess forks children, reports pids and
exit codes, isolates per-fork env, reaps cleanly, and tears down on stop
(fork works on macOS even though the COW savings are Linux-only). The
production memory win and a full session-through-the-tunnel run are unverified
here — they need a Linux host under load, which this change is written to be
turned on for.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): address zygote review feedback

- connect.py: a failed fork no longer stops the running zygote. Stopping it
  would kill healthy runners already forked from it (their orphan watchdog
  sees the parent die), so one bad fork could take down unrelated live
  sessions. Latch a `_zygote_disabled` flag for future launches instead and
  retain the manager so the zygote is still reaped on daemon shutdown.
- runner_zygote.py: wait() after kill() in stop() so a zygote that ignored
  SIGTERM is reaped rather than lingering as a zombie.
- _zygote.py: unify the _entry/app/native import to a single `from ... import`
  (CodeQL flagged mixed import styles).
- test: build the fresh-interpreter probe via an explicit newline join instead
  of implicit adjacent-string concatenation (CodeQL).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): forward --log-to-stderr TTY fd through the zygote

The direct-Popen launch path forwards OMNIGENT_LOG_TTY_FD via
child_logging_popen_kwargs so a detached runner can still mirror logs to the
daemon's terminal. The zygote path dropped it, so --log-to-stderr mirroring
was lost for zygote-forked runners.

Forward it across both hops:
- daemon -> zygote: reuse child_logging_popen_kwargs to dup the TTY fd and add
  it to the zygote's pass_fds (the helper also rewrites env[LOG_TTY_FD] to the
  duped number).
- zygote -> forked runner: the valid fd number inside the child is the one the
  zygote inherited, not the daemon-side number the payload carries, so the
  child restores LOG_TTY_FD from the zygote's own value (and clears a stale
  payload value when the zygote has no terminal mirror).

Adds a test asserting a bogus payload LOG_TTY_FD is cleared in the child.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): address second round of zygote review feedback

- _zygote.py: create the forked child's log file 0o600, not 0o644. Runner
  logs can carry secrets (tokens, prompts); matches create_process_log_path.
- _zygote.py: the child guard now preserves SystemExit's code instead of
  flattening it to a traceback + exit 1, so a zygote-forked runner exits with
  the same code as `python -m omnigent.runner._entry` (main() raises
  SystemExit on a tunnel rejection). New test covers it via a raise seam.
- runner_zygote.py: stop the partially-started zygote if the initial ping
  raises (timeout / EOF), so a failed start never leaks a process + socket.
- runner_zygote.py: signal via signal.SIGTERM / signal.SIGKILL instead of the
  raw 15 / 9.
- test: mark the suite posix_only (it uses os.fork / pass_fds) so cross-
  platform sweeps skip it on Windows.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(runner): enable the zygote on all POSIX hosts, not just Linux

The host daemon runs on the user's own machine — most often macOS — so a
Linux-only gate denied the copy-on-write import-floor savings to the majority
of hosts. Gate on IS_POSIX instead (the zygote needs os.fork + AF_UNIX
fd-passing, both POSIX; Windows still takes the direct Popen path).

macOS is the platform where fork-without-exec is riskiest (CoreFoundation/GCD
abort a forked child that touches them), so this was verified rather than
assumed. The abort is triggered by forking from a MULTI-threaded process, which
the zygote already designs against: it forks from a single-threaded parent
(asserted active_count()==1) and does create_app + all network work in the
child. Evidence on this macOS box:

- A faithful fork probe (fork from the single-threaded import state, child runs
  create_app + getaddrinfo + TLS ctx + asyncio + httpx) survived 5/5. The same
  work forked from a multi-threaded parent SIGSEGV'd 2/3 — confirming the
  single-threaded fork is what makes it safe.
- test_host_launch_runner_and_session_round_trip passes with
  OMNIGENT_RUNNER_ZYGOTE=1: a real host daemon forks a runner through the
  zygote, the runner connects its tunnel, and a full mock-LLM session round-trip
  completes. The daemon log confirms the zygote path (distinct zygote/runner
  pids), not a Popen fallback.

Also adds an info log on the successful zygote-fork path so operators can see
the zygote is active and which pids are involved.

Still opt-in behind OMNIGENT_RUNNER_ZYGOTE=1 with the full Popen fallback; the
steady-state Pss win under load remains best measured on a Linux host.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(runner): fork harness subprocesses from the runner zygote

The harness subprocess (`python -m omnigent.runtime.harnesses._runner`) is a
separate exec per conversation, so it re-pays its import floor — and that floor
is ~54MB of the same common graph (fastapi/pydantic/omnigent-core) the runner
zygote already holds resident. This extends the zygote to fork harness children
too, sharing that graph copy-on-write instead of exec'ing a fresh interpreter.

- _zygote.py: the serve loop becomes a single-threaded `selectors` multiplexer
  over the daemon socket PLUS one inherited control socket per forked runner.
  A new `fork_harness` command forks a child that reproduces `_runner.main(argv)`
  in-process. The runner-fork request/response bytes are unchanged; the new
  multiplexer wraps them rather than rewriting them. A forked child closes every
  inherited zygote-side socket (it never speaks the fork protocol).
- _harness_zygote_client.py (new): the runner-side client. `HarnessZygoteClient`
  reads the inherited control-socket fd from OMNIGENT_RUNNER_ZYGOTE_HARNESS_FD;
  `ZygoteHarnessProc` is an asyncio.subprocess.Process-shaped shim (pid /
  returncode / wait / send_signal / kill) with a background poll task keeping
  returncode fresh for _wait_for_bind's synchronous reads.
- process_manager.py: `_spawn_harness_process` forks via the zygote when the
  runner was itself zygote-forked, else the original create_subprocess_exec;
  disabled on first failure so it falls back for the process's life.
- _runner.py: a zygote-forked harness has the zygote (not the runner) as OS
  parent, so its watchdog probes the runner pid explicitly instead of trusting
  os.getppid(), and skips PR_SET_PDEATHSIG (which would bind death to the
  zygote). Gated by OMNIGENT_HARNESS_ZYGOTE_FORKED.

Present only when the runner itself was zygote-forked; any failure falls back to
a direct exec, so the harness fork is never a hard dependency. The win is
bounded to the ~54MB Python wrapper (the external claude/codex CLI is a separate
exec no Python zygote can share) and materializes under multi-conversation
fan-out. Verified on macOS: fork_harness forks, reports pid + exit code,
round-trips argv, reaps, and leaves the daemon socket serving; existing
process_manager tests unchanged. Linux Pss savings still unverified.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): clear pyrefly type errors in the zygote

- ZygoteRunnerProc.wait: narrow on `timeout` (not just `deadline`) so
  TimeoutExpired(timeout=...) gets a `float`, not `float | None`.
- _spawn_zygote_process: pass stdin/stdout/stderr explicitly with a typed
  `BinaryIO | None` log handle instead of a `dict[str, object]` splat that
  matched no Popen overload.
- _ZygoteServer.serve: cast selector key.fileobj (HasFileno | int) to socket
  — only sockets are ever registered.
- _ZygoteServer._on_readable: wrap the bytearray partition result in bytes()
  before dispatch, which expects bytes.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): harden zygote failure paths (crash recovery, exit-code leak)

Review flagged three correctness bugs in the unhappy lifecycle paths; none are
security issues but each is reachable in prod.

1. Unexpected zygote crash stranded the daemon's view of every child. The
   daemon isn't the runner's OS parent, so once the zygote died it had no
   channel to learn a runner exited — ZygoteManager.poll returned None
   ("still live") forever, so _watch_runner looped, _handle_runner_status
   reported gone sessions as alive, and _handle_stop's final wait() could hang.
   Now poll() probes the runner pid directly when the zygote is gone: a dead
   pid surfaces a non-zero sentinel (254) so the runner reads as dead-and-
   failed, not eternal alive. _handle_stop's post-kill wait() is now bounded.

2. _exit_codes leaked for a dropped runner's harness children. Exit codes were
   only popped via poll, but a dropped runner's harnesses have no remaining
   client to poll them — the entries accumulated (unbounded map growth +
   pid-reuse misattribution). _drop_runner now discards those descendants'
   codes and marks still-live ones orphaned: _reap waitpid's them (no zombies)
   but discards the code instead of storing it.

3. ZygoteHarnessProc.wait() masked a crashed harness as exit 0. If the zygote
   went away, wait() returned 0, so a harness that crashed on boot (bind
   failure, import error) read as a clean exit and the process manager could
   hang waiting for a bind that never comes. Now probes the harness pid and
   returns a non-zero sentinel when the code is unrecoverable.

Also: tighten "Linux-only" docstrings to "POSIX; COW savings on Linux" (the
gate is IS_POSIX and the path runs on macOS), and add a sleep test-seam so the
new failure-path tests can hold a child genuinely alive.

Tests: kill the zygote under a live runner and assert the daemon eventually
sees it dead (not hanging); a dropped runner's harness code is not retained; a
crashed harness with an unrecoverable code surfaces as failure, not 0.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): keep zygote poll/wait off the daemon event loop

Review flagged a liveness regression on the enabled path: for a zygote-forked
runner, poll()/wait() are blocking control-socket round-trips (with lock
contention against a booting zygote that holds the lock across its ~120MB
import), not the lock-free waitpid the direct-Popen path used. Calling them on
the loop thread could freeze the whole daemon — all sessions, websocket
traffic, heartbeats — until the import finishes or the 30s control timeout
elapses.

- _watch_runner: poll() now runs via asyncio.to_thread.
- _handle_stop: now async; the poll/terminate/wait sequence runs off-loop in a
  _stop_runner_proc helper. Its dispatch site and three tests updated to await.
- _tracked_runner_pids: include the zygote pid so the orphan reaper never
  waitpid's the zygote out from under ZygoteManager._proc on an unexpected
  crash (which would confuse is_running()/stop()).

Also updates test_poll_after_stop to use a live child, since the crash-recovery
sentinel (254) now correctly fires for an already-exited pid after stop().

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): status query off-loop + enable zygote by default

- _handle_runner_status did its poll() on the event loop, the one place the
  PR hadn't moved off it. For a zygote-forked runner poll() is a blocking
  control-socket round-trip (bounded only by the 30s control timeout, and
  contended against a booting zygote), so a slow zygote could stall the whole
  daemon for a single status query. Made it async and run the poll via
  asyncio.to_thread, matching _watch_runner / _handle_stop. Dispatch site and
  the three status tests updated to await.

- Enable the zygote by default: OMNIGENT_RUNNER_ZYGOTE is now opt-OUT
  (=0/false/no/off), not opt-in. The host daemon runs on the user's own
  machine (most often macOS), so defaulting on lets most users share the
  ~120MB import floor. Still POSIX-gated with a full Popen fallback, so an
  unsupported platform or any zygote failure is transparent.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: prevent mid-spawn launch leaks and harden zygote request handling

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 22:16:01 +09:00
Hubert a47a9ee3bf feat(web): set the text size steps from the design (#4021)
* feat(web): set the text size steps from the design

Body and chat-thread text are both 13px/18px in the design; the shared
`text-13` step was on a 20px line, so tighten it to 18px. Adds the 12px/16px
caption step used by sidebar section subtitles (Projects, Sessions).

Defines the steps only — switching each surface onto them is follow-up work.

Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* feat(web): put chat and sidebar text on the design's type scale

The chat thread hard-coded its own 15px/24px with negative tracking, and
sidebar rows set a size but no line height, so neither matched the design.

- Chat bubbles (user and assistant share the wrapper): 13px/18px, and the
  -0.01em tracking is dropped — the design specifies 0.
- Sidebar body rows: pin the line height to 18/13 of the font size, which was
  previously left to inherit.

Both stay in rem/unitless so the mobile root-font bump and the Appearance
font-size setting keep scaling them. Sidebar section captions were already
12px/16px and are unchanged.

Co-authored-by: Isaac

* test(e2e-ui): regenerate visual baselines

* refactor(web): express the sidebar line height in rem

1.3846 was the 18/13 ratio written as a unitless number — unreadable, and it
took arithmetic to confirm it meant 18px. 1.125rem is 18px directly and
scales the same way, matching how the chat wrapper states it.

Co-authored-by: Isaac

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-04 14:17:15 +02:00
Serena Ruan 3cc3777413 fix(host): forward OMNIGENT_RUNNER_ENV_PASSTHROUGH through the remote daemon (#4050)
OMNIGENT_RUNNER_ENV_PASSTHROUGH lets an operator name extra env vars for the
host to forward on to spawned runners (provider gateway wiring, config env: refs,
etc.). It worked locally but was a silent no-op in --server mode: the remote
daemon env is allowlisted by a prefix set of DATABRICKS_ + LC_/MLFLOW_/OTEL_/
OMNIGENT_OTEL_ — NOT plain OMNIGENT_ — so the control var itself was stripped at
the CLI->daemon hop, and _build_runner_env never saw the names it listed. Any var
forwarded through the passthrough (e.g. a Linear API key for the repro-agent)
reached the runner locally but never remotely.

Add OMNIGENT_RUNNER_ENV_PASSTHROUGH to _RUNNER_ENV_ALLOWLIST so it survives both
hops. It carries only env var NAMES, not secrets, so allowlisting it leaks
nothing on its own — each named var must still independently reach the daemon
(here via the DATABRICKS_ prefix).

Tests: a daemon-hop test (remote env keeps the control var) and an end-to-end
two-hop test (a named var survives CLI->daemon->runner, an unnamed one doesn't).
Both fail without the one-line allowlist change.

Co-authored-by: Isaac
2026-08-04 19:59:38 +08:00
Serena Ruan 45eab11d53 dev/repro-agent: give Linear tickets a real fetch path + follow linked GitHub issues (#4047)
* dev/repro-agent: give Linear tickets a real fetch path + follow linked GitHub issues

The local repro-agent pointed Linear tickets at nonexistent "Linear tools",
so Linear runs had no way to read the ticket body and fell back to guessing
from the URL slug — noticeably worse reproductions than GitHub issues, which
have a working `gh issue view` path.

Wire Linear to the same GraphQL path the internal issue-sync agent uses
(api.linear.app/graphql, `Authorization: $LINEAR_API_KEY`, no Bearer), pulling
description/comments/attachments via sys_os_shell. When the key is absent or
auth fails, stop with needs_more_info naming the missing key instead of
guessing. Also: when a Linear ticket links a GitHub issue, always fetch that
issue too and treat it as authoritative for the technical journey — that
richer thread is why GitHub-first runs reproduced better.

Co-authored-by: Isaac

* dev/repro: forward the Linear key through the --server env strip

Reading a Linear ticket needs the key in the agent's shell, but under --server
the CLI->daemon->runner hops strip everything not allowlisted. The DATABRICKS_
prefix survives only the first hop; the daemon->runner hop has no DATABRICKS_
prefix. So dev/repro.py now names DATABRICKS_LINEAR_API_KEY in
OMNIGENT_RUNNER_ENV_PASSTHROUGH (itself allowlisted) when a Linear URL is passed
and the key is set, which forwards it the rest of the way. AGENTS.md reads
whichever name is present (LINEAR_API_KEY locally, DATABRICKS_LINEAR_API_KEY
under --server). Warns rather than fails when the key is missing.

Companion change (omnigent-internal): the repro-agent CI workflow must set
DATABRICKS_LINEAR_API_KEY from secrets.LINEAR_API_KEY in the run step, mirroring
how it already sets DATABRICKS_BEARER for the LLM key.

Co-authored-by: Isaac

* dev/repro: mirror LINEAR_API_KEY into the DATABRICKS_ name

Maintainers typically export the plain LINEAR_API_KEY locally, so copy it into
DATABRICKS_LINEAR_API_KEY when only the plain name is set — then the same
passthrough forwarding carries it past the --server env strip. Warn only when
neither is set.

Co-authored-by: Isaac
2026-08-04 19:34:48 +08:00
Hubert a858a6be9a feat(web): make the rails flush boxes and move the canvas gradient (#4020)
* feat(web): make the rails flush boxes and move the canvas gradient

The sidebar and workspace rails were floating cards (margin, rounded
corners, border, shadow) on a gradient canvas. The design has them flush to
the window edges, reading as part of the canvas.

- Left sidebar and right workspace rail sit flush: no outer margin, no
  rounding, no drop shadow. The workspace rail keeps a left divider.
- Light canvas is flat white; the brand gradient moves onto the left
  sidebar, joined by the mock's dot-grid and pink corner glow.
- Dark canvas carries the mock's purple gradient; the dark sidebar gets the
  same dot-grid plus a purple bottom wash and the diagonal sheen.
- Both rails are excluded from the dark glass rule instead of overriding it,
  so they no longer pick up its blur, sheen, fill, or border. The workspace
  rail's panel contents are transparent too.
- Dark surface tokens (--card, --card-solid, --tray, --muted, --background)
  move off their purple tint onto neutral slate.

Consolidates the canvas/rail CSS so each surface owns its full background in
one rule, and drops the now-redundant ::before dot overlay.

Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-04 13:34:37 +02:00
Pat Sukprasert e1f9939325 fix(logging): preserve exception tracebacks (#4048)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 10:54:34 +00:00
Serena Ruan 4d57fb20dc chore(triage): assign every triaged issue an owner; refresh area ownership (#4046)
Broaden issue-triage auto-assignment from P0/P1-only to every triaged
issue except needs_info ones. The gate now keys off needs_info alone, so
any bug/enhancement/doc issue with enough info to triage gets a
load-balanced area owner (least open assigned issues first, LLM rank as
tiebreaker) instead of only high-priority ones. Drops the now-unused
priority/type branch from the shell gate.

Also refresh .github/areas.json ownership:
- remove SabhyaC26 from all areas
- add PattaraS to harness-antigravity (keeps it at the 2-owner minimum)
- reactivate dbczumar (owners_paused -> owners) across their areas

Co-authored-by: Isaac
2026-08-04 18:11:25 +08:00
Pat Sukprasert b68f073578 chore(lint): enforce VS Code type checks (#4044)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 17:10:41 +07:00
Serena Ruan 0ca0d42ae2 fix(web): remount terminal view when switching same-vendor sessions (#4043)
* fix(web): remount terminal view when switching same-vendor sessions

Two sessions of the same shape share a fixed agent-terminal id (e.g. every
claude-native session's `terminal_claude_main`, every SDK session's
`terminal_tui_main`). ChatPage stays mounted across a session switch and only
feeds MainTerminalView / TerminalsPanel a new conversationId, so keying the
xterm wrapper on the terminal id alone let React reuse the existing mount —
the pane kept the previous session's 20k-line scrollback until the new
WebSocket reconnected and tmux repainted. The stale history cleared only on a
manual refresh.

Scope the wrapper key to `${conversationId}:${terminalId}` in both surfaces so
a session switch forces a clean remount (fresh xterm + WebSocket, no stale
buffer). Add regression tests that switching conversationId with the same
terminal id remounts the TerminalView.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(web): assign terminal mount id once per mount

Copilot review flagged that useRef(++terminalMountSeq) evaluates the
increment on every render (useRef ignores the arg after first render),
so the module counter advanced on re-renders — contradicting the
comment. The read value (instance.current) was still stable, so the
assertion held, but assign the id conditionally so the counter tracks
real mounts.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-04 18:08:39 +08:00
Pat Sukprasert ebf38dea90 fix(native harnesses): keep provider auth out of process arguments (#4030)
* fix(codex): materialize provider configuration

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(claude): materialize invocation settings

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(claude-native): verify private invocation settings

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 17:07:05 +07:00
Pat Sukprasert b06722c2a8 test(vscode): update Vitest mock typing (#4042)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 09:42:09 +00:00
Pat Sukprasert 3dbe83374e test(e2e-ui): de-flake view-mode toggle open in native-parity helpers (#4036)
The Chat/Terminal switcher moved into the header as a Radix DropdownMenu
whose trigger toggles on pointer-down and carries a controlled hover
tooltip on the same node (ViewModeToggle.tsx). On a busy page — a live
terminal stream plus that tooltip re-rendering during the click — a lone
`.click()` occasionally nets the menu back to closed, so the follow-up
`expect(menuitemradio).to_be_visible()` times out. That is the observed
flake in test_codex_goal_mode and the native render-parity suites: the
failure snapshot shows `tooltip "Terminal view"` (rendered only while the
menu is closed) with no menu items.

Add a shared `_select_view_mode(page, option)` helper that reopens the
menu in a retry loop until the target radio item is actually visible, then
selects it, instead of trusting a single toggle click. Route
`_ensure_chat_view` and every native-parity `_open_terminal_view`
(codex, claude, goose, hermes, cursor, kiro) through it.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 09:32:00 +00:00
Hubert 8546ee2bd1 feat(web): adopt shadcn Zinc color tokens (#4019)
Repoint the gray text and border tokens onto the shadcn Zinc scale so the
UI's neutrals match the design system:

- Primary text (--foreground, --card-foreground, --secondary-foreground,
  --sidebar-foreground) -> Zinc 800 #27272a
- Secondary text (--muted-foreground) -> Zinc 500 #71717a
- Default border (--border, --input, --sidebar-border) -> Zinc 200 #e4e4e7
- Strong border (--border-strong) -> Zinc 400 #a1a1aa

Also adds the two tokens the palette needs but the app lacked:
--border-weak (Zinc 150) and --foreground-tertiary (Zinc 400), exposed as
Tailwind utilities.

Co-authored-by: Isaac

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
2026-08-04 11:31:39 +02:00
Pat Sukprasert fa849b87b2 ci(flake-stress-ui): prebuild codex-parity sidecar once (#4039)
The codex goal-mode + native-parity targets need the Codex-parity Rust
sidecar. flake-stress-ui.yml relied on the fixture's inline `cargo build`
at test time, capped by --timeout=300. On a cold Rust cache every parallel
attempt independently compiles the ~1100-crate tree and overruns the
per-test timeout, so all attempts die at fixture setup before the test
body ever runs — masquerading as a 100% failure rate unrelated to the
target under test.

Mirror e2e-ui.yml / ci.yml: add a dedicated build-sidecar job that
compiles the sidecar once (same main-scoped cache key so it usually
restores), uploads the ~10MB binary, and has each attempt download it and
set CODEX_PARITY_SIDECAR_BIN. build_sidecar_bin() then returns the prebuilt
path and skips cargo entirely. Drops the per-attempt Rust toolchain + target
-dir cache that never made the inline build fit the timeout.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 09:30:35 +00:00
Serena Ruan bd675eaec0 dev/repro: add --public flag to share the reproduction session at start (#4041)
`dev/repro.py --public` sets `public: true` in the agent's input contract, and
the agent shares the session read-only (`sys_session_share __public__`) at the
start of its run so it is browsable live — useful when watching a run or
reproducing against a shared --server. Off by default (a local session is
already yours to browse).

- dev/repro.py: add --public; include `"public": true` in the payload when set.
- config.yaml: re-add `agent_session_sharing: public` to grant the __public__
  capability (opt-in via the flag).
- AGENTS.md: document the `public` input; make sharing the first preflight step.
- README.md: document the --public flag.

Co-authored-by: Isaac
2026-08-04 17:25:50 +08:00
Pat Sukprasert b8fd1952ac chore(web): reject stale lint suppressions (#4035)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 09:20:17 +00:00
Serena Ruan 31898a379a dev/repro: worktree-isolating driver script + compound-bug handling (#4034)
* dev/repro: add worktree-isolating driver script; clarify browser context

Add dev/repro.py — a maintainer-only wrapper around `omnigent run
dev/repro-agent`. It prompts for the bug URL (or takes it as an argument /
bare id like OMNI-1234 / 3987), creates an isolated git worktree off the
current checkout's HEAD (branch repro/<slug>, auto-suffixed on collision),
and runs the agent FROM that worktree so the authored e2e test lands on its
own branch without dirtying your checkout. The worktree is always kept; the
script prints its path + branch + cleanup command at the end.

It lives under dev/ (not shipped in the wheel) rather than as an `omni`
subcommand because it depends on a source checkout — the repro-agent authors
into tests/e2e_ui/ / tests/e2e/, which only exist here.

Also, from PR review:
- AGENTS.md: note that UI-journey reproduction drives the desktop app's
  embedded browser, so it expects a desktop / embedded-browser context (fall
  back to the backend path / needs_more_info when there's no browser pane).
- README.md: document the dev/repro.py driver.

Co-authored-by: Isaac

* dev/repro-agent: handle compound / multi-symptom bug reports

Ported from the internal repro-agent (omnigent-internal#24). A single bug
report often bundles several distinct symptoms (e.g. "picker unavailable AND
catalog defaults lag"), and they can have different truth on the running
build — one already fixed, the other still live. Averaging them into one
verdict hides the part that's still broken.

AGENTS.md now instructs the agent to:
- enumerate each claimed sub-symptom in Step 1 (don't collapse a compound
  report into one journey),
- reproduce and judge each independently in Step 2, and
- roll up to an overall verdict where ANY live sub-symptom ⇒ reproduced
  (already_fixed only when every facet is fixed), emitting a per-facet
  breakdown (`facets`) in the output so a partial fix stays visible.

Wording adapted to the local variant (running build / local session; no
deployed-app or public-share references).

Co-authored-by: Isaac

* dev/repro: drop the `ref` input — always reproduce against the running build

`ref` never controlled what was validated: the agent always reproduces against
the app it is connected to (the running build / latest main), and `ref` was
only informational — and redundant, since the reported version is already in
the bug report the agent reads. Simplify the input contract to just `bug_url`.

- dev/repro.py: remove the --ref option; the payload is {"bug_url": ...}.
- config.yaml / AGENTS.md / README.md: drop the ref bullet/examples; keep the
  guidance that reproduction is always against the running build (so an
  old-version report can still land already_fixed).

Co-authored-by: Isaac
2026-08-04 16:58:42 +08:00
Pat Sukprasert 2ee95e1a3e chore(web): require explicit returns (#4028)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 15:42:48 +07:00
Serena Ruan 7405414015 Add dev/repro-agent: reproduce a bug live in your running app + author an e2e test (#4032)
A developer-facing repro agent under dev/. Given just a bug (a bug_url — GitHub
issue or Linear ticket — plus an optional ref), it reconstructs the user journey
from the linked report, drives the running Omnigent app it is connected to (the
server `omnigent run` spins up, or one passed with --server) through that journey
until the failure happens live, and authors a durable e2e test (Playwright under
tests/e2e_ui/ for UI bugs, or tests/e2e/ for backend) as the regression artifact.

It reproduces against whatever app it is connected to and authors the test into
the current checkout, so a developer can run it against their own local server:

  omnigent run dev/repro-agent -p '{"bug_url":"https://github.com/omnigent-ai/omnigent/issues/1234"}'

It does not fix the bug, merge, or push — it produces a live-confirmed
reproduction plus the test and hands off (the fix half owns the before/after
fail->pass proof).

Files:
- config.yaml — claude-sdk brain, os_env shell/file access, blast-radius guard.
- AGENTS.md — the operating procedure (confirm workspace -> reconstruct journey
  -> reproduce live -> author the e2e test -> structured verdict).
- README.md — prerequisites, usage, and what it produces.

Co-authored-by: Isaac
2026-08-04 16:35:01 +08:00
Tomu Hirata c5888b6ec1 perf(host): skip host-status HTTP call for dead daemon processes (#4031)
omni host status was slow because _add_daemon_host_status made a
GET /v1/hosts/{id} request for every daemon record, including the many
stale records accumulated over dev sessions (39 in one measured case).
Dead processes can't have an online tunnel, so the correct answer is
host_status=offline with no network round-trip.

Skip the HTTP call when process=offline and set host_status directly.
This cut omni host status from ~14s to ~5s on a workstation with many
stale daemon records.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 08:32:41 +00:00
Tomu Hirata cd5bcd2d04 fix(host): recover from workspace-missing runner launch failures (#4023)
When a session's workspace directory no longer exists on the host
(e.g. a worktree was deleted), the host was returning a generic
failed status with no error_code, causing the server to silently
wait out the full connect timeout and then surface a generic
'runner_failed_to_start' banner.

Changes:
- Add WORKSPACE_MISSING_ERROR_CODE ('workspace_missing') to host/frames.py
- Host returns this code when workspace.is_dir() fails, alongside the
  existing descriptive error message
- Server (routes_events.py post_event) handles workspace_missing the same
  way as harness_not_configured: immediately consumes the user message and
  persists an actionable runner_failed_to_start error item with the host's
  'workspace path does not exist: ...' message instead of timing out into
  a generic RUNNER_UNAVAILABLE
- orchestration.py _ensure_runner_relay_ready skips the connect-timeout
  wait for workspace_missing (same as harness_not_configured), and records
  the refusal in runner_exit_reports so snapshot-based renders also show
  the actionable cause

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 17:09:57 +09:00
Serena Ruan 0a8567e8a6 fix(web): stop rendering shell-style env vars in prose as LaTeX math (#4026)
* fix(web): stop rendering shell-style env vars in prose as LaTeX math

Error messages like "Unresolved environment variable '$LLM_API_KEY' … Set
$LLM_API_KEY or $OMNIGENT_LLM_API_KEY" render through the assistant markdown
renderer, which has single-dollar math enabled. The paired `$` tokens collapsed
into a garbled inline formula.

normalizeExplicitMathDelimiters already escaped a lone `$` before a digit
(currency); extend that heuristic to also escape shell-style variable
references ($VAR_NAME and ${VAR_NAME}, SCREAMING_CASE) so they stay literal text
instead of flipping the math span.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* docs(web): clarify SHELL_VAR_RE handles single-char braced refs

Address Copilot review: the comment said "2+ chars" but the braced
alternative uses `*`, so `${A}` matches. That's intended — braces
disambiguate a variable reference, so one char is enough there, while the
bare form still requires 2+ so `$X …` reads as inline math. Fix the comment
and add a test for both cases.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): require full-token boundary for bare shell-var match

Address Copilot review: SHELL_VAR_RE's bare branch matched a SCREAMING_CASE
prefix of a mixed-case token (e.g. `$FOOBar$`), escaping the opening `$` while
leaving the closing `$` as a delimiter — an unbalanced span that breaks
genuine inline math. Add a `(?![A-Za-z0-9_])` boundary so only full
SCREAMING_CASE tokens match, and greedy backtracking can't settle on a prefix.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-04 15:41:05 +08:00
Tomu Hirata 21febb6cc8 fix(host): retry 401/403 on an already-connected host (#4025)
When the VPN drops, a corporate proxy answers the host tunnel's
WebSocket upgrade with 401/403 before the request reaches the Omnigent
server. `_classify_http_status` treated those as permanently fatal, so a
live, already-registered host exited with code 1 and the user had to
re-run `omnigent host` after reconnecting.

A host that already completed an upgrade proved its credentials and
authorization are valid, so a later 401/403 is almost always a transient
network-path artifact. For a connected host, 401/403 now retries forever
via the normal reconnect path (mirroring the existing login-redirect
design), with a once-per-outage stderr notice so a foreground
`omnigent host` isn't silent. A fresh, never-connected host still fails
loud on the first 401/403.

Fixes OMNI-2367.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 07:37:19 +00:00
Pat Sukprasert 67b88fc2cd chore(lint): enforce web TypeScript checks (#4022)
* chore(lint): enforce web TypeScript checks

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* chore(lint): skip web tsc without dependencies

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-04 14:19:47 +07:00
Tomu Hirata e1ba799606 fix(runner): prevent transient 400 from permanently latching mint declined (#4024)
Two fixes to _ManagedMintTokenFactory and _InitialAuthTokenFactory:

1. Only latch declined=True on 400/404 if the factory has never successfully
   minted a token. A 400 mid-session (e.g. during an IP ACL flip) is
   transient — the server already proved it mints for this runner, so treat
   it like any other transient failure instead of bricking the factory.

2. Add a declined property to _InitialAuthTokenFactory that proxies the
   inner fallback factory. Without this, auth_flow sees declined=False on
   the outer wrapper and raises 'no token' instead of falling back to bare
   requests, causing infinite retry loops in PATCH external_session_id and
   other callbacks after the inner factory latches declined.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 07:10:31 +00:00
Edwin He 15dd7becff Report launch stages on managed-host wake (#4016)
A managed-host wake (resume_managed_host: resuming a dormant resumable
sandbox on the next message) never forwarded launch-pipeline stages to the
caller, unlike the fresh-launch path (_arm_and_start_host), which threads
on_stage through. As a result _run_managed_wake left the session on the
single "provisioning" band that _kick_managed_wake seeded for the entire
resume — even while the host was already re-execing and dialing back — so
the UI showed a frozen "Provisioning sandbox" band for the whole wake.

Thread on_stage through resume_managed_host into _start_sandbox_host (which
already accepts it), and have _run_managed_wake pass a _publish_sandbox_status
closure. The wake now advances to "starting" (emitted by base start_host)
before "connecting"/"ready", matching a fresh launch.

Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
2026-08-03 23:31:29 -07:00
Serena Ruan f848f341a0 feat(cli): add --profile to omni run for headless Databricks SP auth (#4017)
Connecting a host to a Databricks-App-deployed omnigent server as a service
principal failed: `omni run --server <app>` resolves credentials through the
Databricks SDK's default chain, which reads only the DEFAULT ~/.databrickscfg
profile. When DEFAULT points at a different workspace than the one fronting the
app, the minted token is for the wrong workspace and the Apps proxy bounces the
request to interactive OIDC (302) instead of admitting it.

Add a `--profile NAME` option to `omni run` that sets DATABRICKS_CONFIG_PROFILE
for the CLI process, so every remote-auth path (_remote_headers, _server_auth,
_DatabricksTokenAuth) resolves the named service-principal profile. This enables
headless M2M access to a deployed app without a prior interactive `omnigent
login`. An explicit --profile wins over an ambient DATABRICKS_CONFIG_PROFILE;
omitting it leaves any preset untouched.

Prereq (Databricks-side, not code): the service principal must have CAN USE on
the app.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-04 14:25:13 +08:00
Tomu Hirata ab4bcaa752 fix(runner): treat HTTP 403 as refreshable on tunnel reconnect (#3943)
* fix(runner): treat HTTP 403 as refreshable on tunnel reconnect

A runner whose auth token expires while the machine is offline can
receive HTTP 403 (not 401) when DNS resolves again and the server
rejects the stale credential. Previously 403 was in
_FATAL_SERVER_HTTP_STATUSES and caused the runner to exit immediately
with no retry, killing any active session.

Move 403 into _REFRESHABLE_HTTP_STATUSES alongside 401. The existing
_handle_refreshable_auth_failure path already handles this correctly:
it attempts one token refresh, and if the factory is invalidatable
(or returns None) the second 403 raises a fatal RuntimeError instead
of looping forever. A runner with no factory still exits fatally on
the first 403.

Add three tests covering the new behaviour:
- 403 with factory → refresh → retry → success
- 403 with invalidatable factory → refresh → persistent 403 → fatal
- 403 without factory → fatal immediately

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): guard 403/401 refresh against transient factory errors

- Drop the inline to_thread(factory) call in the refreshable-status
  handler; rely on the loop-top _refresh_auth_token instead, which
  already wraps factory calls in try/except for OSError/ValueError.
  This prevents a transient IdP error on wake-from-sleep from crashing
  serve_tunnel rather than falling back and retrying.
- Also removes the redundant double-refresh-per-cycle that the inline
  call introduced.
- Update _handle_refreshable_auth_failure docstring: 401/403 now go
  through the streak path, not this function; only 302 redirects
  reach it.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(server): import _spawn_archive_stop in routes_core

Missing import introduced in 2ce9c60b.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-04 05:20:04 +00:00
Corey Zumar 2ce9c60bf5 perf(sessions): stop and archive in parallel so archiving isn't gated on stop timeouts (#3783)
* perf(sessions): stop and archive in parallel so archiving isn't gated on stop timeouts

Archiving a live session took 5-10s: the sidebar serialized stop -> archive,
and the PATCH handler awaited its own best-effort stop (5s runner / 10s host
teardown ceilings per running session) before flipping the flag — even though
the archive proceeds regardless of the stop's outcome. Fire the client legs
in parallel and detach the server-side stop into a retained background task;
the stop still runs to completion, it just no longer holds the response.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(sessions): let the server own the archive stop so it can't race the client's

Review follow-ups on the parallel-archive change:

- The client no longer sends its own stop_session alongside the archive
  PATCH. Two concurrent stops raced the same runner, and because the
  runner's stop handlers are not idempotent (kill_session raises once
  the pane is gone -> 503), the loser's failure aborted the client stop
  before it reached the host-runner teardown -- orphaning a host-spawned
  session's dedicated runner. Archive now sends one PATCH.
- The server's detached stop carries the host-runner teardown that only
  the client stop used to do, so archiving still drops the runner's
  tunnel and flips runner_online. Bulk archive gains this too; it never
  sent a client stop.
- The stop is spawned only after the archived flag commits. It ran
  ahead of later validations, so a PATCH rejected after that point
  (reserved label, runner_id permission) could stop a session it did
  not archive.

Adds an e2e_ui browser test for the archive flow plus server coverage
for the teardown and the rejected-PATCH case.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-03 17:44:34 -07:00
Corey Zumar dabdd2a7b0 fix: file forked sessions into the source's project (#3793)
* fix(server): file forked sessions into the source's project

Forking a session filed in a first-class project left the fork unfiled:
fork_conversation built the fork's metadata row without project_id. The
fork route now carries the source's project onto the fork, gated on the
forker owning that project (projects are owner-private, so a fork of a
shared session filed in someone else's project stays unfiled).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): refresh the project folder when a session is forked

A fork inherits the source's project, but the dialog only invalidated the
flat session list — each folder renders its own ["project-sessions", name]
query, which has no poll and converges only on an explicit invalidation.
The push stream can't cover it either: it skips the active session, and the
fork becomes active on navigate. So the clone stayed missing from its folder
until a reload or a re-navigation.

Adds an e2e regression test. It seeds the committed turn the fork action
anchors on straight into the store (new seed_committed_turn helper) instead
of driving a model turn, so it neither waits on nor inherits the flakiness
of the mock-LLM harness.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-03 16:12:14 -07:00
Dhruv Gupta e19bbc9227 feat(release): fold the desktop app version into the lockstep stamp (#4005)
web/electron/package.json was deliberately excluded from
update_versions.py because lockstep versions are not valid semver, so
it rotted: v0.7.0 and v0.8.0 shipped a desktop app still calling
itself 0.6.0, and 0.8.1's desktop bump had to be pushed by hand onto
the release branch (and still reads 0.8.0 at the v0.8.1 tag).

Stamp it with the semver translation of the lockstep version instead
(0.6.0rc1 -> 0.6.0-rc.1, 0.7.0.dev0 -> 0.7.0-dev.0, finals
unchanged) — semver orders these the way PEP 440 does (dev < rc <
final), so desktop auto-update comparisons stay correct. check() now
gates the translation, so a drifted desktop version fails the version
lockstep lint. Aligns main's desktop version to 0.9.0-dev.0.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 15:41:16 -07:00
Dhruv Gupta a0df125083 fix(ci): normalize uv.lock after /regen resolutions (#4004)
* fix(ci): normalize uv.lock after /regen resolutions

The regen workflow was the one lock-writing CI path left out when the
normalize-then-verify step was added to release/bump/nightly: its
uv lock --upgrade-package runs re-add the size fields the canonical
form forbids, ballooning a /regen'd PR's lockfile diff by ~3k lines of
formatting noise and failing the pre-commit lint on the PR.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* fix(ci): /regen upgrade touches only uv.lock

A targeted Python package upgrade was also deleting and re-resolving
pnpm-lock.yaml from scratch, burying a ~100-line dependency fix under
thousands of lines of npm churn. Plain /regen keeps refreshing both
lockfiles.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 14:51:56 -07:00
omnigent-ci[bot] 99e5ab4d59 Bump version to 0.9.0.dev0 (#3991)
* Bump version to 0.9.0.dev0

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(deps): drop the stale gitpython cooldown exemption

The per-package cutoff (2026-07-24) was added to make 3.1.55 resolvable
while it was inside the P7D window; it aged out, and the frozen cutoff
now excludes 3.1.56/3.1.57, which fix GHSA-p538-c434-8v24 and
GHSA-3f7w-8rr8-f37f — so the OSV audit fails on any PR touching the
lock. The global P7D cooldown admits 3.1.57 on its own now. Lockfile
regen follows via /regen upgrade gitpython.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* chore(oss): regenerate public lockfiles against public PyPI/npm

* chore(oss): regenerate public lockfiles against public PyPI/npm

* chore(deps): normalize the lockfile back to canonical form

The /regen runs regenerate uv.lock without the normalize step the
other lock-writing workflows gained, re-adding the size fields the
canonical form forbids. Text-only cleanup; the resolved versions
(gitpython 3.1.57, aiohttp 3.14.2) are unchanged.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* chore(deps): restore main's pnpm-lock.yaml

The /regen runs regenerate the npm lockfile from scratch even for a
Python-only package upgrade; this PR changes no JS dependency, so
main's lockfile is exactly right for it.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-03 21:43:27 +00:00
Dhruv Gupta 8b468c8b9e docs(changelog): v0.8.1 ships the switcher revert, not nothing (#4003)
The auto-generated entry said no user-facing changes: the release's one
change is a cherry-picked revert whose PR is still open against main,
which the changelog curation (merged-PRs-in-range) cannot see.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 14:22:40 -07:00
omnigent-ci[bot] 5daa8e0d54 docs(changelog): record v0.8.1 (#4002)
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-03 21:16:58 +00:00
Dhruv Gupta 0f1a3101ea ci(nightly): watch nightly-release in the failure monitor; document the lane (#3994)
The nightly cut is fully unattended, so a broken run blocks nobody and
consumers silently stop getting new builds. Add Nightly Release to the
failure monitor's watch list: its two-consecutive-failures rule and
close-on-green behavior apply unchanged, and skipped quiet nights
conclude success so they close any open tracking issue.

RELEASING.md gains a Nightly builds section: what the workflow does,
how consumers install and update from tags (no PyPI), and that a bad
nightly needs no recovery beyond fixing main.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 20:54:46 +00:00
omnigent-ci[bot] 4c8ad6ae72 docs(changelog): record v0.8.0 (#3992)
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-03 20:53:19 +00:00
Andrew Peltekci c5544d3788 feat(harness): add Grok Build (xAI) as a first-class ACP harness (#3075)
* feat(harness): add Grok Build (xAI) as a first-class ACP harness

Grok Build (`grok`) had no first-class harness — only usable as a custom `acp:`
agent or as `xai/grok-*` behind openai-agents. Add `harness: grok` (alias
`grok-build`) driving `grok agent stdio` over ACP via the generic AcpExecutor,
the reuse path the issue suggests (like qwen).

- inner/grok_harness.py: thin create_app wrapping AcpExecutor with a fixed
  `grok agent stdio` command; auth is Grok's own (grok login / XAI_API_KEY),
  Omnigent stores no credential.
- Registry: valid_harnesses / harness_modules / alias grok-build / capabilities
  (ACP profile: own-auth, cold resume, SSE permission, interrupt) / label
  "Grok Build" / HARNESS_GROK_MODEL.
- Install spec (curl x.ai/cli/install.sh, grok login --device-auth) and
  binary-gated readiness, matching the other own-auth CLI harnesses.

Closes #2881

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>

* test(onboarding): include grok spellings in configured-harness-map test

The grok harness added `grok` + `grok-build` to the configured-harness map;
test_configured_harness_map_covers_all_spellings pinned an expected_keys set
that omitted them, so it failed with both as extra items. Add them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>

* test(e2e): exclude grok from the live no-agent harness matrix

Registering grok as a coding harness added it to the matrix's expected set,
but grok is a headless ACP harness driven over stdio: it authenticates from
the grok CLI's own xAI login rather than the shared gateway/profile probe
wiring, so there is nothing for this binary-less no-agent matrix to probe.
Exclude it alongside goose, which is excluded for the same reason, and name
tests/inner/test_grok_harness.py as its coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>

* fix(harness): drop the grok model-override claim nothing implements

The harness registered HARNESS_GROK_MODEL in model_env_keys, but the executor
never read it, so a spec model or /model pick was silently dropped rather than
applied — and the docstring pointed at a session/set_model path this harness
doesn't implement.

Remove the registry entry and the claim. Grok selects its model in its own CLI;
an Omnigent-driven override for ACP-backed harnesses is a separate concern and
should land with the mechanism that actually applies it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>

* refactor(harness): declarative catalog for builtin ACP CLI harnesses

Promoting an ACP-speaking vendor CLI to a first-class harness has meant
touching 6+ registration points (capabilities, valid set, module map,
aliases, labels, install spec, readiness, setup steps, a per-harness
spawn-env builder, the live e2e matrix exclusion) plus a near-identical
thin inner module. Recent PRs each re-derived this by hand and one shipped
without its spawn-env builder, silently dropping the session cwd and the
spec sandbox.

Add omnigent/acp_cli_harnesses.py: one AcpCliHarness row per vendor CLI
(label, binary, ACP argv, aliases, install and login metadata). Every
registration derives from the row:

- harness_plugins: validity, module routing (all rows run the shared
  omnigent/inner/acp_harness.py wrap), aliases, labels, capabilities
  (the generic acp profile), install specs and install keys
- onboarding: one-click install allowlist (npm rows) and vendor-login
  setup steps derive; readiness rides the existing install-key gate
- runtime/workflow: one shared _build_acp_cli_spawn_env forwarding the
  session cwd and serialized os_env, shell-quoting the resolved binary
- runner dispatch: one membership check covers every current and future
  row
- tests: readiness spelling lists and the live-matrix exclusion extend
  from the catalog; tests/test_acp_cli_harnesses.py drives a fake row
  through the builder and dispatch and asserts full registration per
  real row

The catalog ships empty; the first rows land with the Grok Build (#3075)
and Qoder (#3560) PRs, each reduced to one dict entry plus docs.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* refactor(grok): ride the ACP CLI harness catalog, one row instead of hand wiring

Rebase the Grok Build harness onto the declarative catalog from
feat/acp-cli-catalog: the thin inner module, the per-registry entries,
the install/readiness edits, and the manual e2e-matrix exclusion all
collapse into one ACP_CLI_HARNESSES row carrying the same label, alias,
command, install hint, and login metadata.

Riding the shared builder also fixes two gaps the hand wiring had: the
session working folder and the spec os_env/sandbox now reach the grok
subprocess (grok_harness.py read HARNESS_GROK_CWD / HARNESS_GROK_OS_ENV
but nothing ever set them), and a resolved binary path containing spaces
survives the shlex-split command string.

Registration, spawn env, readiness gating, setup steps, and the live
matrix exclusion are asserted per row by tests/test_acp_cli_harnesses.py,
replacing tests/inner/test_grok_harness.py.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: apeltekci <andrew@peltekci.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 20:36:15 +00:00
Kobi Kadosh e94c5f8b1a feat: add nimble_extract and nimble_research Nimble builtins (#3117)
* feat: add nimble_research builtin backed by Nimble Agent API v2

Add a nimble_research built-in tool that delegates a research task to a
Nimble Web Search Agent through the asynchronous Agent API v2: start a
run (POST /v2/agents/{agent_id}/runs), poll it to a terminal status on
a monotonic deadline, then fetch the cited result. The tool returns a
bounded JSON envelope - run id, output (text or structured JSON), and
trust metadata (confidence, sources, per-claim citations) - capped so a
large result cannot blow the model context.

The builtin registers like web_search: a registry factory plus
runner-local dispatch, so a non-OpenAI model's nimble_research call
resolves to the backend. api_key and agent_id come from spec config
(the tool never creates agents; one-time bootstrap is documented in the
module); errors are returned as strings and always carry the run id,
including timeout, failure, cancellation, and unknown-status paths.
Polling honors Retry-After on 429 and retries transient failures within
a bounded budget; run creation is never retried.

Includes unit, dispatch, and e2e tests (respx transport mocks and a
fake-clock seam; the e2e drives the full lifecycle against a local
Agent API stub).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* feat: add nimble_extract builtin backed by Nimble Extract Templates

Add a nimble_extract built-in tool that runs one of the account's Nimble
extract templates (POST /v2/extract/templates/run) and returns the
template's structured, parsed results as JSON in one synchronous call.
The template is named in spec config; the LLM supplies the template's
params (each template declares its own input schema, discoverable via
GET /v2/extract/templates/{name}).

This is the migration target for the deprecated one-call /v1/agent
site-scraping path: same structured-entities output contract, now on
the current Extract Templates API. The predecessor tool name is retired
rather than aliased - the registry does not reserve it, and a test
locks that in - so the old name can never silently point at a
different API.

Wiring mirrors nimble_research: registry factory plus runner-local
dispatch. api_key and template come from spec config; errors are
returned as strings with the template named and the server's task id
preserved for supportability (parsing failures, template-not-found,
params rejection, and server error bodies are each mapped to clear
messages); output is capped to keep the model context bounded.

Includes unit, dispatch, and e2e tests (respx transport mocks; the e2e
drives the flow against a local Extract Templates stub), with
captured-request assertions that every request carries the
X-Client-Source header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix(nimble_research): harden malformed-config and envelope bounds

Catch httpx.InvalidURL when building the run URL so a control character
in the configured agent_id returns the builtin's own clean error string
instead of escaping its documented never-raises contract (agent_id is
interpolated into the run URL path).

Cap each API-supplied trust string - reasoning, source and citation url
and title, and the output type - so a single oversized value cannot
inflate the returned envelope past its intended bound, matching the
list-length caps already applied to sources, claims, and citations.

Adds tests: a control-char agent_id returns an error with no request
made, and an oversized trust.reasoning is capped with the envelope
still valid JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix(nimble_research): complete the never-raises and envelope bounds

Follow-up to the previous hardening pass, which covered only part of each
surface.

Never-raises: httpx.InvalidURL is not a subclass of RequestError, so it
also had to be handled on the poll and result hops. The run id comes from
the API and is only prefix-validated, so a control character after the
prefix could raise out of the tool. Polling treats it as permanent and
returns immediately rather than spending its transient-retry budget on an
error that cannot become valid.

Envelope bounds: cap the remaining API-supplied strings that reached the
envelope uncapped - trust confidence, per-claim path and confidence - and
drop a non-string source or citation url instead of passing the raw value
through. Also cap API-supplied text reflected into error strings, which
could otherwise be arbitrarily long.

Adds regression tests for both hops, for every capped field, for the
dropped non-string url, and for an oversized server error message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix(nimble_research): bound run status, run id, and the trust section

The result body's run status was accepted as any string and interpolated
raw into the failure message, so a malformed status could turn into a
multi-megabyte error string. Only a known terminal status is trusted now,
and the message caps the values it reflects.

Bound the accepted run id at creation instead of echoing an arbitrary one
through later messages, and cap the trust section as a whole: the
per-field caps still multiplied across sources, claims and citations.

Includes regression tests for each bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* feat(nimble_research): adopt nimble-python 1.2 typed run fields

Create runs through the released nimble-python 1.2.0 client instead of
hand-rolled requests, and expose the typed per-run fields it added.

agent_id is now optional and selects the create route: when it is omitted
the run is created with agents.run() so Nimble provisions the agent, and
when it is set the run is created with agents.runs.create() against that
agent. Both routes forward input_data, output_schema, sources, agent_name,
skill, and use_case as typed arguments, so no extra_body escape hatch is
needed. The client is built with max_retries=0, because creating a run is
billable and not idempotent and the API exposes no idempotency key.

effort stays an optional override, so leaving it unset lets the selected
agent or template default apply. low, medium, high, and x-high are
selectable per run. max is a coming-soon custom-budget tier: it stops with
a pointer to the Nimble product team, and only degrades to x-high when a
spec opts in explicitly. The degradation is reported on every outcome, so
a run that was downgraded and then failed still says so.

The agent id returned by creation addresses the rest of the lifecycle,
since on the generated route it is the only one that exists, and a run
that comes back owned by a different agent is rejected rather than
retargeted. Identifiers are checked against an allowlist before they are
interpolated into a request path. Status polling defaults to ten seconds.

Includes unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix(nimble_research): warn against resubmitting an unresolved create

A create that fails in transport or times out may still have been received,
and a 408 or 5xx reached Nimble before the failure was reported, so the run
can be live and billed while the call reports an error. A 202 carrying an
unusable body is the settled version of the same problem: the run exists,
but the response cannot address it.

All of these now say so and tell the caller not to resubmit, since a
resubmission pays for the task a second time. The guidance names the run id
when one survived, and points at the account's recent run history when none
did. A clear rejection still carries no such warning: 401, 403, 404 and 422
create nothing, and attaching the warning to them would only teach the
reader to skip it.

Includes unit tests for the ambiguous and settled paths, and for the
rejections that must stay silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix(deps): upgrade GitPython to 3.1.55 to clear advisories

The lockfile pinned GitPython 3.1.50, which carries eight advisories whose
fixes land across 3.1.51, 3.1.53, 3.1.54 and 3.1.55. The dependency audit
only runs when the lockfile changes, so the pin was invisible until it was
touched, and then it failed the scan.

3.1.55 is the first release that clears all eight. It sits one day past the
P7D resolution window, so it needs a per-package exception alongside the
existing ones; the cutoff is set to land on 3.1.55 rather than the latest
release, keeping the change to the smallest version that resolves the
advisories.

GitPython is a transitive dependency, so this is a lockfile-only change and
no declared requirement moves. No other package version changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix: align Nimble 1.2 run controls with released contract

Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* fix(nimble_research): never invite a resubmit of a billed run

Once run creation succeeds the run exists and has been billed, but only the
create path said so. The 409 branch told the caller to "retry the task to
fetch it" — on a run already observed complete — which reads as an instruction
to call the tool again and pay for a second run to read the first one's
result. Timeout, polling and result-fetch failures said nothing either.

Every post-create failure now ends with the same guidance the create path
gives, keyed to the run id: do not resubmit, reconcile the run that already
exists. A create-time 429 stays a clear rejection, since a rate limiter
refuses the request before a run is started; that classification is now
documented and covered.

Also drops the notice channel left behind when the effort downgrade was
removed. _resolve_effort returned None for it at every exit, so the value was
always None and the code that consumed it was unreachable; a resolved effort
is now simply what the caller asked for. The tool schema's sources object is
tightened to match what the tool already enforces, so a schema-conformant call
is not rejected at runtime.

Includes unit tests for each post-create path and the rejection that must stay
silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>

* feat(onboarding): advertise the nimble builtins to the agent builder

list_builtin_tools.py is the onboarding assistant's sole source of truth
for recommendable builtins; without these entries the assistant can
never surface nimble_extract or nimble_research when building an agent.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* refactor(deps): move nimble-python behind a `nimble` extra

nimble-python was a baseline dependency, so every install pulled a
partner SDK that only the nimble_research builtin uses (nimble_extract
talks raw httpx). Follow the hindsight-client pattern: the SDK moves to
an optional `nimble` extra, nimble_research imports it lazily inside
_start_run and reports which extra to install (checked before anything
is sent, so nothing is billed), and the onboarding catalog advertises
the tool only when the SDK is importable. The client stays in the dev
set so the credential-free suites keep driving the real SDK, and mypy
gets the same ignore_missing_imports override as the other lazy-import
extras.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* fix(nimble): address Polly review findings

Blocking items, all verified before fixing:

- Guard use_case with isinstance before the frozenset membership test; a
  list/dict argument raised TypeError (unhashable) out of invoke(),
  breaking the never-raises contract. Now a clear tool error, unbilled.
- Catch APIError (e.g. APIResponseValidationError, which subclasses
  APIError, not APIStatusError/APIConnectionError) in the create path
  and route it through the unresolved-create guidance: a 2xx whose body
  fails SDK validation means the run may exist and be billed, which is
  exactly the case the do-not-resubmit warning exists for.
- Clamp each HTTP call's timeout to the remaining deadline via
  _request_timeout, so a single create/poll/result request can no longer
  overrun the tool's documented timeout_seconds budget.

Also apply the research module's error-string caps to nimble_extract
(message, task id, parsing detail, status), closing the one reflected
uncapped path Polly's non-blocking notes and the maintainer review both
flagged. Regression tests for all four.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 13:05:52 -07:00
Dhruv Gupta f86c4ccaa1 fix(ci): normalize uv.lock to canonical form after every CI uv lock (#3990)
The release cut, main bump, and nightly cut all regenerate uv.lock in
CI. The runner's uv now writes size fields on file entries, which the
repo's canonical lockfile form (scripts/normalize_uv_lock_registry.py,
enforced by the pre-commit hook) forbids — so the v0.8.0 release
commit went red on the branch-push lint run, and the next cut from
that branch would fail the green-CI gate. Run the normalizer after
uv lock (fixer exits non-zero when it rewrites, so tolerate that),
then hard-verify with --check so a genuinely broken lockfile still
fails the step.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 13:04:11 -07:00
Dhruv Gupta 14eb2a515d refactor(harness): declarative catalog for builtin ACP CLI harnesses (#3988)
Promoting an ACP-speaking vendor CLI to a first-class harness has meant
touching 6+ registration points (capabilities, valid set, module map,
aliases, labels, install spec, readiness, setup steps, a per-harness
spawn-env builder, the live e2e matrix exclusion) plus a near-identical
thin inner module. Recent PRs each re-derived this by hand and one shipped
without its spawn-env builder, silently dropping the session cwd and the
spec sandbox.

Add omnigent/acp_cli_harnesses.py: one AcpCliHarness row per vendor CLI
(label, binary, ACP argv, aliases, install and login metadata). Every
registration derives from the row:

- harness_plugins: validity, module routing (all rows run the shared
  omnigent/inner/acp_harness.py wrap), aliases, labels, capabilities
  (the generic acp profile), install specs and install keys
- onboarding: one-click install allowlist (npm rows) and vendor-login
  setup steps derive; readiness rides the existing install-key gate
- runtime/workflow: one shared _build_acp_cli_spawn_env forwarding the
  session cwd and serialized os_env, shell-quoting the resolved binary
- runner dispatch: one membership check covers every current and future
  row
- tests: readiness spelling lists and the live-matrix exclusion extend
  from the catalog; tests/test_acp_cli_harnesses.py drives a fake row
  through the builder and dispatch and asserts full registration per
  real row

The catalog ships empty; the first rows land with the Grok Build (#3075)
and Qoder (#3560) PRs, each reduced to one dict entry plus docs.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 19:20:49 +00:00
Dhruv Gupta cfb431c20c feat(ci): nightly prerelease builds — tag-only dev-datestamp cuts from main (#3475)
* feat(ci): nightly prerelease builds — tag-only dev-datestamp cuts from main

Adds nightly-release.yml: every night at 04:30 UTC it walks main to the
newest commit with completed green CI, stamps the lockstep version to
X.Y.Z.devYYYYMMDD, commits the stamp detached on top of that base, and
pushes only the tag via the omnigent-ci App token. Quiet nights (no new
commits since the last nightly tag) and same-day reruns no-op.

Deliberately not the release.yml flow: no release branch, no main bump,
no benchmark gate. Downstream is already dev-quiet: no GitHub release,
no notes, no changelog, no homebrew; images publish the immutable
version tag; update-check ignores dev releases and omni upgrade --pre
opts in. The datestamp is fixed-width because PEP 440 compares the dev
segment as one integer — a wider stamp would sort above every narrower
one forever.

PyPI publishing follows separately via the secure release repo's
scheduled lane.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* feat(ci): nightly consumer update script; drop the stale PyPI hand-off note

scripts/update_nightly.sh resolves the newest vX.Y.Z.devYYYYMMDD tag
(version sorts before date, so the first nightly after a main version
bump outranks all older ones; the 8-digit date requirement screens out
legacy .dev0-style tags) and installs it with uv, pinning the lockstep
trio to that one tagged commit. Idempotent, so it is cron-safe: it
exits fast when the newest nightly is already installed instead of
redoing the web-UI build.

The workflow header no longer claims the secure release repo publishes
nightly tags to PyPI: that lane was dropped, nightlies are consumed
straight from the tag.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* feat(cli): omni upgrade --nightly moves onto the newest nightly tag

Nightlies are vX.Y.Z.devYYYYMMDD git tags that never reach the package
index, so the flag answers 'is there something newer' from the repo's
tags (git ls-remote + PEP 440 max, so the first nightly after a main
version bump outranks all older ones) and reinstalls with a git spec
pinned to that tag, per installer (uv/pipx/pip/poetry). It dispatches
before the VCS-vs-registry split: a registry install hops onto the
channel, and a VCS install pinned to an older nightly moves tags
instead of re-pulling its pinned ref. Same drain/stop, --check, and
probe-the-disk verification contracts as the release path.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 12:08:41 -07:00
Zeyi (Rice) Fan b2b1002ee4 fix(setup): don't hang on corepack's pnpm download prompt (#3986)
## Related issue

N/A

## Summary

- After the switch to corepack/pnpm, `pip install .` / `uv sync` could hang
  indefinitely for users who have a corepack `pnpm` shim on PATH but have
  never downloaded pnpm. Corepack prints `! Corepack is about to download
  .../pnpm-11.15.1.tgz` and then blocks on `? Do you want to continue? [Y/n]`.
  Build backends capture output, so the prompt is invisible and the install
  just sits there until the 600s timeout.
- The trigger is the shim, not the `corepack pnpm` fallback: corepack's
  `dist/pnpm.js` does `COREPACK_ENABLE_DOWNLOAD_PROMPT ??= '1'` while explicit
  `dist/corepack.js` uses `'0'`. `shutil.which("pnpm")` finds the shim, so the
  prompting path is the one that looked fine. CI is unaffected because corepack
  skips the prompt when `$CI` is set.
- Run both pnpm commands in `setup.py` with
  `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` (download without asking) and
  `stdin=DEVNULL` so nothing else in the toolchain can block on input we can
  never deliver. Applied the same fix to `tests/e2e_ui/conftest.py`, which had
  the identical latent hang under captured pytest output.

## Test Plan

Reproduced the hang and verified the fix against the pinned `pnpm@11.15.1`,
handing the child a real TTY via `pty.openpty()` and an empty `COREPACK_HOME`:

```
BEFORE (shim default prompt=1, TTY stdin): HUNG (timeout)
        err='! Corepack is about to download .../pnpm-11.15.1.tgz\n? Do yo'
AFTER  (prompt=0 + stdin=DEVNULL):         proceeds straight to download
```

End-to-end check of the install path:

```bash
rm -rf ~/.cache/node/corepack "$COREPACK_HOME"
corepack enable                 # pnpm shim on PATH, pnpm not yet fetched
rm -rf omnigent/server/static/web-ui
pip install .                   # previously stalled with no output
```

`ruff check` / `ruff format --check` clean on both files.

## Demo

N/A

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Verified manually: the failure only reproduces with a corepack `pnpm` shim, an
unpopulated `COREPACK_HOME`, and a TTY on stdin, so an automated test would
have to stand up a pty plus a registry fetch inside the build backend. Covered
instead by the pty-based before/after check in the Test Plan.

## Changelog

`pip install` / `uv sync` no longer hangs when pnpm is provided by a corepack
shim that has not downloaded pnpm yet.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-03 11:34:11 -07:00
Dhruv Gupta 981d6143ab fix(release): stage the full lockstep stamp in the release commit (#3474)
When omnigent-slack joined the lockstep, the cut job's hand-kept git
add list kept staging only the original five paths, so the release
commit shipped integrations/slack/pyproject.toml unstamped. At the
v0.7.0 tag the tree pins omnigent-slack==0.7.0 while the in-tree
package still says 0.7.0.dev0: uv sync --locked fails at the tag, a
source install with the slack extra cannot resolve, and lint went red
on both release/v0.7.0 pushes without blocking the tag. Stage with
git add -A like bump-version.yml so the staged set tracks whatever
update_versions.py stamps.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-08-03 11:23:27 -07:00
Shivam Mittal dc00417841 Add WebSocket load test (dev/loadtest/) + run-load-test skill (#3591)
* Add WebSocket load test (dev/loadtest/) + run-load-test skill

Adds a Locust load test that opens N concurrent WebSocket connections to
WS /v1/sessions/updates and holds them open, measuring the server's
WebSocket fan-out (handshake, origin/auth gating, watch-set diffing,
heartbeat) under concurrency — no runner, LLM, or agent turns.

- dev/loadtest/ws_load_test.py: the locustfile (SessionUpdatesUser).
- dev/loadtest/run.py: runner taking server + host + load params, runs
  locust headless, and writes a result set (summary.md, CSV, HTML, config).
- loadtest extra (locust + websocket-client) in pyproject.toml + uv.lock.
- .claude/skills/run-load-test: skill that gathers inputs, runs, and
  explains the latency results.

Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>

* Launch locust via sys.executable -m locust in the load-test runner

run.py launched locust as a bare `locust` command, which resolves through
PATH and can pick up a stale/broken locust from a different Python (e.g. a
~/.local 3.10 install missing gevent's zope.event) even when run.py itself
runs under a venv — crashing the run with ModuleNotFoundError before locust
starts. Launch it as `sys.executable -m locust` so it always uses the same
interpreter + site-packages that run.py runs under. Preflight now checks
importlib.util.find_spec (the actual interpreter) instead of shutil.which
(PATH), and --web execs sys.executable too.

Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>

* Genericize --mount-prefix docs to reverse-proxy sub-paths

Replace deployment-specific mount-prefix details with a provider-neutral
"behind a reverse proxy at a sub-path" framing (neutral /omnigent example)
across the README, the run-load-test skill, and the run.py / ws_load_test.py
help + docstrings. The --mount-prefix flag itself is unchanged.

Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>

* Add runner-level turn load test (real multi-turn conversations, mocked LLM)

turn_load.py drives real agent turns through the runner — the full
POST .../events → server → runner → executor → LLM → stream → idle loop —
under concurrency, with the LLM mocked (zero latency) so the numbers isolate
Omnigent's own per-turn / history-handling overhead. Runs N concurrent
conversations of M sequential turns each on one durable session, so history
grows across the turns (a real long conversation, not N one-shots).

It boots the whole stack itself (server + zero-latency mock LLM + runner) by
reusing the benchmark harness's BenchEnvironment, using the in-process
openai-agents harness — no vendor CLI, no real API key — so it runs from a repo
checkout with no server to point at. Concurrency is asyncio (the runner stack
is async), not Locust. Writes the same summary.md / run_config.json result
format as the WS runner.

Documents both scenarios (WebSocket fan-out vs runner turns) in the README and
the run-load-test skill.

Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>

* Address review: fix socket leak, URL/timeout edge cases, double-count; add tests

Copilot review follow-ups on the load-test harness:

- ws_load_test: assign self.ws before the send/recv steps so a post-create
  failure closes the socket instead of leaking it.
- ws_load_test: _ws_url now treats a schemeless host (localhost:8000, which
  Locust accepts) as ws:// rather than emitting an invalid URL.
- ws_load_test: _read_until_snapshot caps each recv to the remaining deadline
  so a late frame can't overrun by a full read timeout.
- ws_load_test: WS_READ_TIMEOUT falls back to the default on a non-numeric
  value instead of raising in on_start.
- run.py: preflight websocket-client as well as locust; rename _fmt_ms ->
  _fmt_num (it also formats Requests/s).
- run.py: _write_summary skips locust's Aggregated row when totaling, which was
  double-counting the headline request/failure counts.
- docs: the scenario reads AUTH_TOKEN from the environment; drop the wrong
  `-e AUTH_TOKEN` locust-flag examples (AUTH_TOKEN=... locust ...).
- tests: add tests/loadtest unit tests for the pure helpers (URL/env/argv
  wiring, summary formatting, timeout parsing) — deterministic, no server boot.

Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>

* Redesign as one load test: each user is a real host driving real turns

Collapse the two scenarios (ws_load_test.py + turn_load.py) into a single
load test where each Locust user IS a real omnigent host. Each user spawns a
real `omnigent host` subprocess (unique identity + per-host $HOME so the
host-daemon singleton guard doesn't collide), registers it over the host
tunnel, then creates host-bound sessions and drives real multi-turn
conversations — every turn is a genuine post→idle loop through a runner the
host spawns, with the LLM mocked (zero latency). `-u N` scales the number of
hosts; Locust does the concurrency.

run.py boots the whole stack (server + mock LLM via BenchEnvironment),
registers one agent, sets the mock reply, then runs Locust against it — there
is no --server to pass, since mocking the LLM requires a stack we control. It
reuses the CSV→summary.md machinery (Aggregated-row dedupe kept).

Capacity-limited by design: N hosts × M sessions = N×M real runner processes on
the load box, so it drives genuine end-to-end turns rather than faking the
runner, but does not scale to hundreds on one machine (documented). Removes the
websocket-client dep (no longer used); needs [loadtest,dev,agents-sdk]. README,
skill, and tests updated for the single scenario.

Verified locally: 5 hosts × 3 turns → 133 turns, 0 failures.

Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>

---------

Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
Co-authored-by: Shivam Mittal <shivam.mittal@databricks.com>
2026-08-03 10:29:45 -07:00
Pat Sukprasert 1262652a03 chore(lint): enforce pyrefly type checking (#3972)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 23:32:26 +07:00
Pat Sukprasert 7f00c6899f refactor: resolve remaining REPL type errors (#3966)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 22:44:42 +08:00
Pat Sukprasert c3b0c16b64 Type model-backed event snapshots (#3962)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 14:17:26 +00:00
Pat Sukprasert d643f4bb55 Type native terminal close metadata (#3963)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 14:11:22 +00:00
Pat Sukprasert 21706331f7 Type default policy phases explicitly (#3960)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 14:01:19 +00:00
Pat Sukprasert 743bc11343 Type Pi model catalog entries explicitly (#3961)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 13:59:33 +00:00
Pat Sukprasert 4dfbecc043 Tighten runner boundary contracts (#3959)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 13:58:23 +00:00
Pat Sukprasert 5f83e83364 Clarify executor cleanup lifecycles (#3958)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 21:52:29 +08:00
Pat Sukprasert 452adf7217 Narrow validated server request fields (#3957)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 21:52:14 +08:00
Pat Sukprasert 47e415bc3f Narrow CLI lifecycle type checks (#3956)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 21:52:06 +08:00
Pat Sukprasert 25aafbdf25 Bind MCP elicitation exception before dispatch (#3954)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 21:51:56 +08:00
Pat Sukprasert 4d7fad52f1 Type child status payload as JSON (#3953)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 21:51:37 +08:00
Pat Sukprasert 074a467efc docs(openclaw): reflect live-verified compatibility status (#3955)
PR #3420 validated the OpenClaw Gateway ACP path end-to-end against a live
Gateway, but docs/openclaw.md still read as if streaming/final replies were
only protocol-matched and the integration provisional. Update the
compatibility section to state what live validation confirmed — streaming
assistant replies, native tool execution, ACP permission routing, and session
resume — and reframe the remaining Control-UI-sync gap as a known limitation
rather than an open question. Keep the note that CI cannot run OpenClaw.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-08-03 20:50:46 +07:00
Pat Sukprasert de0f62ea2d Align compressed text dialect hooks (#3948)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 13:21:48 +00:00
Pat Sukprasert 5d573c0489 Align UUID dialect hook signatures (#3947)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 13:18:52 +00:00
Pat Sukprasert 54bc0d7208 Type timed formatter options explicitly (#3938)
* fix typing for timed formatter options

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* Avoid duplicated formatter defaults

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 13:11:12 +00:00
Pat Sukprasert 678ba9bc0d Type Bedrock client configuration (#3946)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:05:25 +07:00
Pat Sukprasert 47087bc08e Narrow detected harness credential families (#3945)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:04:57 +07:00
Pat Sukprasert 5d0eaa4f67 Narrow workspace text decoding state (#3944)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:04:48 +07:00
Pat Sukprasert 7b778bbb2e handle non-json runner stream frames (#3942)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:04:40 +07:00
Pat Sukprasert 0e46accde4 narrow lazy import boundaries (#3941)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:04:29 +07:00
Pat Sukprasert 9689a5a807 narrow process owner lock resources (#3940)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:04:10 +07:00
Pat Sukprasert 5315dc2ffd narrow resolved egress addresses (#3939)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 20:03:46 +07:00
Daniel Lok 617293d3d9 perf(web): cache recent conversation transcripts (#3932)
* perf(web): cache recent conversation transcripts

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* refactor(web): remove pending assistant skeleton

* refactor(web): decouple transcript cache from sidebar status

* refactor(web): scope transcript eviction to deletion

* refactor(web): page forward from cached transcripts

* Revert "refactor(web): page forward from cached transcripts"

This reverts commit 8a40141164e85ff7c9b3eb4108810d39d2b9ebfb.

* fix(web): apply session metadata after cache backfill errors

---------

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
2026-08-03 21:01:00 +08:00
占永杰 a30ba15f93 fix(ap-web): harden math rendering (#1666)
* fix(ap-web): harden math rendering

Load KaTeX runtime styles in every web entrypoint and normalize common TeX delimiters so streamed formulas, radicals, and display math render reliably across chat surfaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: buyicoder <169354621+buyicoder@users.noreply.github.com>

* fix(ap-web): make math delimiter normalization region-aware

Address Polly review notes on the math-rendering hardening:

- Skip normalization inside existing $…$/$$…$$ spans and treat a
  literal backslash-backslash as a verbatim escape, so a LaTeX line break
  like \\[1em] inside an aligned display block is no longer mistaken for
  a \[ opener and corrupted.
- Track backtick-run length so \(/\[ inside a multi-backtick inline-code
  span is left verbatim.
- Correct the stale FILE_PATH_AWARE_COMPONENTS comment now that the memo
  comparator is gone and MessageResponse shallow-compares props.

Co-authored-by: Isaac

* fix(ap-web): guard currency dollars and indented fences in math normalizer

Follow-up on Polly review notes:

- A single $ immediately before a digit reads as currency ($5), so it is
  escaped and does not flip the math-span toggle. Prevents prose like
  "it costs $5 or $10" from parsing as inline math now that
  single-dollar math is enabled globally. An escaped \$ is copied verbatim.
- Fence detection now allows CommonMark's 0-3 leading spaces and matches the
  full fence run, so an indented ```-fenced block containing \(...\) is not
  normalized (and a 4-backtick run no longer leaks into inline-code tracking).

Co-authored-by: Isaac

* fix(ap-web): use String.match for fence detection to clear exfil scan

The security Exfil scan flags RegExp.prototype.exec() because its text-only
regex matches the substring 'exec(', which is meant to catch Python dynamic
code execution (exec/eval/__import__). This is a pure in-memory regex match
against local string data, so switch to the equivalent String.match(), which
returns the same match array for a non-global regex and avoids the token.

Co-authored-by: Isaac

* fix(ap-web): address Copilot review on math normalizer and styles

- Track the opening fence marker so a fenced code block closes only on a
  matching fence char with a run at least as long (CommonMark). A stray
  `~~~` line inside a ```-fenced block no longer flips the fence off and
  lets math normalization run inside code.
- Drop the no-op `overflow-y: visible` on `.katex-display`; with a
  non-visible overflow-x the browser computes overflow-y as auto anyway, so
  it only risked stray vertical scrollbars.
- Resolve the entrypoint-style guard test's paths from import.meta.url
  instead of process.cwd() so it doesn't depend on the runner's directory.

Co-authored-by: Isaac

---------

Signed-off-by: buyicoder <169354621+buyicoder@users.noreply.github.com>
Co-authored-by: zhanyongjie <zhanyongjie@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 20:44:33 +08:00
Pat Sukprasert bc12d9a881 fix typing for subprocess handles (#3935)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 18:59:26 +07:00
Pat Sukprasert dbc709d945 Narrow server liveness fallbacks (#3936)
* fix typing for health liveness fallbacks

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refine liveness fallback lookup

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 18:59:07 +07:00
Pat Sukprasert b460bd5e89 fix typing for session usage accumulator (#3937)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 18:58:55 +07:00
Tomu Hirata c74faadc1e fix(runner): forward provider api_key_ref env vars into runner subprocess (#3915)
* fix(pi): surface credential resolution error when gateway provider's env var is unset

When a `kind: gateway` provider is configured as the pi harness default
via `default: pi` and its `api_key_ref: env:VAR` cannot resolve (because
VAR is not exported in the runner's environment), `_optional_provider_family`
previously caught the OmnigentError from `resolve_secret` and returned None
silently. The outer `_apply_provider_to_pi` then raised a generic
"no family whose credentials resolve — set the api_key env var for its
'anthropic' or 'openai' family" message with no mention of which specific
variable to export, making the error hard to act on.

Change `_optional_provider_family` to return the captured error alongside
None (as a tuple), and surface that error in the "no family resolves"
message so the user sees exactly which env var (e.g. `$MY_TOKEN` from
`api_key_ref: env:MY_TOKEN`) needs to be set.

The design intent of the silent catch is preserved: a family whose key is
unset is still treated as absent so pi can fall back to the other family
when only one key is exported. The only change is that the fallback-failure
error now carries the root cause.

Closes #3788

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* address review: fix return type annotation, correct keychain docstring, remove issue refs from tests

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): forward provider api_key_ref env vars into runner subprocess

_build_runner_env filters the host environment before spawning the runner
subprocess, passing only an allowlist of known credential vars
(ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.). A user who configures a gateway
provider with a custom env var via api_key_ref: env:MY_TOKEN would find that
MY_TOKEN is present in their shell and daemon process but stripped before
reaching the runner — resolve_secret then fails, _optional_provider_family
returns None for the family, and _apply_provider_to_pi raises the no-family-
resolves error.

Add provider_credential_env_vars(config) to provider_config.py, which scans
all inline-family providers for api_key_ref: env:VAR and api_key: $VAR
references and returns the set of env var names (plus OMNIGENT_-prefixed
aliases). Wire this into _build_runner_env so those vars are automatically
forwarded alongside the standard HARNESS_CREDENTIAL_ENV_VARS, without
requiring users to list them in OMNIGENT_RUNNER_ENV_PASSTHROUGH by hand.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): add authHeader to generic openai provider entries in models.json

Generic (non-Databricks) OpenAI-compatible gateways expect
Authorization: Bearer <token>. The 'databricks' and 'databricks-completions'
provider entries in the generated models.json were missing authHeader: True
on the generic provider path, so Pi used the Databricks-native auth scheme
instead — causing a 401 Missing Authentication header from the gateway.

Add authHeader: True to both entries when is_generic_provider is true,
matching the pattern already used by databricks-openai, databricks-anthropic,
and databricks-mlflow.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi-native): qualify namespaced model ids in --model arg to prevent builtin routing

When a gateway provider's model id contains a slash (e.g. an OpenRouter
namespaced id like 'moonshotai/kimi-k2.5'), Pi's arg parser treats
'provider/model' in --model as a provider override, routing to the builtin
'moonshotai' provider instead of our custom 'omnigent' provider. The builtin
has no API key, producing 'No API key for provider: openai-codex'.

Pass the fully-qualified 'provider/model' form (e.g.
'omnigent/moonshotai/kimi-k2.5') when the model id contains a slash, so
Pi's findExactModelReferenceMatch matches the canonical form under our
provider first.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-03 11:39:39 +00:00
Rajarshi Datta 1c6dfedce7 fix(policies): normalize worktree_guard paths with posixpath, not os.path (#3856)
* fix(worktree-guard): switch to posixpath for path normalization to ensure consistent behavior across platforms

* Windows-only escape in worktree_guard, the sole write confinement for unsandboxed workers: it reasoned in POSIX but normalized with os.path, which is ntpath on Windows and rewrites / to \ — so startswith("/") never fired and /etc/passwd returned ALLOW.

Fixed by normalizing with posixpath explicitly, plus a drive-letter reject for C:/Windows/x, which posixpath reads as an ordinary relative dir named C:.

Two follow-ups from Copilot: the drive check ran on the raw path, so ./C:/… (and a/../C:/…) normalized past it — moved it after normalization; and isalpha() narrowed to ASCII, since Windows drives are [A-Za-z] and the Unicode form over-rejected.

109 passed on Windows, where four of those cases fail on main. Audited environment_filesystem.py:190 in the same pass — it pairs normpath with os.path.isabs, which holds on both platforms, so it needs no change.
2026-08-03 20:31:20 +09:00
Anthony Ivan 7edb2978ec fix(pi-native): surface task plans in shared Tasks panel (#2884)
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Serena Ruan <82044803+serena-ruan@users.noreply.github.com>
2026-08-03 19:03:48 +08:00
Pat Sukprasert e72be826e9 refactor(python): replace sessions wildcard imports (#3934)
* refactor(python): replace sessions wildcard imports

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(python): drop redundant sessions imports

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 10:45:44 +00:00
David O'Keeffe 91ffc9d288 fix(pi-native): allow tool relay bridge root (#3920)
Signed-off-by: David O'Keeffe <david.okeeffe@databricks.com>
2026-08-03 17:51:15 +09:00
Serena Ruan e7ae96daef feat(web): move Chat/Terminal switcher into the header (#3931)
* feat(web): move Chat/Terminal switcher into the header

Terminal-first sessions previously toggled between chat and terminal via
an in-page pill above the composer. Replace it with a MessagesSquare +
chevron icon button in the ChatHeader (next to the agent-info icon) that
opens a Chat/Terminal dropdown, freeing the composer area and keeping the
switcher with the other session controls.

The new ViewModeToggle reads the same TerminalFirstContext the pill did,
so behavior is unchanged: it self-gates for non-terminal-first sessions,
the iOS shell (native Liquid Glass bar), and rail-opened shell views, and
disables the Terminal option (with a spinner while starting up) until a
PTY is reachable. A tooltip names the current view. Removes the pill, its
dead CSS, and the now-redundant iOS keyboard guard.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): update e2e locators + a11y for header view toggle

The render-parity e2e helpers located the old in-page pill via
`role="group" name="View mode"` and clicked its inner Chat/Terminal
buttons. The header switcher is a dropdown, so point them at the
`view-mode-toggle` trigger and click the Chat/Terminal menuitemradio.

Also address review feedback on ViewModeToggle: import the shared
`TerminalFirstView` type instead of a duplicated union in the setView
cast, and only suppress dropdown close-refocus for pointer closes so
keyboard/AT users keep their place (mouse closes still avoid the stuck
ghost-button focus ring).

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 16:47:09 +08:00
Tomu Hirata f68abff463 fix(codex-native): stop leaking codex app-server processes across all teardown paths (#3925)
* fix(codex-native): tear down app-server when TUI pane is reaped or exits

Each codex-native session (Polly dispatches every codex sub-agent this
way) runs two codex processes on the runner: the codex app-server backend
and the codex --remote TUI pane. Only DELETE /v1/sessions ran the full
cleanup that cancels the forwarder and closes the app-server. Two other
ways the TUI pane goes away left the app-server orphaned for the runner's
lifetime:

- the idle pane reaper closes the tmux pane after the idle window but
  never touched _AUTO_CODEX_APP_SERVERS, and
- an unexpected TUI exit (crash / OOM / host recycle) evicted the pane
  without cancelling the forwarder.

On a long-lived multi-session runner, every idle or crashed codex
sub-agent leaked a codex app-server process — the pile-up reported in
omnigents-qa.

Add teardown_codex_native_app_server(session_id): cancel the session's
forwarder (whose finally closes the app-server) and close any leftover
registered server. It's a no-op for a session with no registered codex
app-server, so it's safe to call from the shared pane-teardown paths for
every harness. Wire it into the reaper's reap and the terminal-exit
publisher.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(codex-native): reap codex app-server even if pane close raises

Move the codex app-server teardown in the idle-pane reaper into the
finally block. close_terminal() can propagate (TerminalInstance.close()
raises anything but TimeoutError), and in that partial-failure mode the
teardown line in the try body was skipped — leaving the exact orphaned
app-server this fix targets. The helper is idempotent and suppresses its
own errors, so running it in finally never masks the original exception.

Addresses Copilot review on #3925.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(codex-native): close app-servers on host/runner stop + boot reconcile

Host-spawned codex app-servers are spawned start_new_session=True, so
they survive their runner's death. Two gaps left them orphaned:

- On a graceful host/runner stop the host SIGTERMs the runner without a
  per-session DELETE /v1/sessions, so per-session teardown never fired and
  _stop_pm never closed _AUTO_CODEX_APP_SERVERS — every host-spawned codex
  app-server leaked even on a clean stop. (The TUI panes were already
  closed by the terminal registry's shutdown; only the app-server half
  leaked.)
- On a hard death (SIGKILL / OOM / crash) nothing runs at all, and the
  crash-safe registry was only reconciled when a NEW codex session
  started — so orphans lingered until the next codex launch, if ever.

Add teardown_all_codex_native_app_servers() and call it from _stop_pm so a
graceful stop takes the app-servers down with the runner. Add a boot-time
reconcile_codex_native_process_registry() in _start_pm so a fresh runner
reaps orphans a dead predecessor left (owner-lock held => live sibling,
skipped). Reconcile runs in a thread since it does blocking file/PID work.

The --remote TUI self-exits when its app-server dies (observed: every
orphan seen in the field was an app-server, zero orphaned TUIs), and the
graceful path already closes TUI panes, so no tmux-name plumbing is added.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-03 17:23:16 +09:00
Serena Ruan 42f3d703c4 feat(cli): add omnigent diagnose environment snapshot (#3928)
* feat(cli): add `omnigent diagnose` environment snapshot

Add a read-only `omnigent diagnose` command that prints a small, secret-free
environment snapshot for bug reports: CLI version, OS/Python, and the server's
auth mode. With `--server <url>` (or a resolvable configured/local server) it
reads the server version and real auth mode from the unauthed `GET /v1/info`
endpoint, so version skew between CLI and server is visible — the same reason
the session-info popover shows `server · host`.

The auth mode doubles as the OSS-vs-managed signal: accounts | single_user |
oidc | header, derived from `/v1/info` when the server is reachable and falling
back to the local environment otherwise (tagged by `auth_source_origin` so the
two are never confused). The snapshot carries no secrets — only versions, OS,
and the coarse auth mode.

`omnigent doctor` (install-ledger migration) is left untouched.

Co-authored-by: Isaac

* fix(cli): address diagnose review — redact server_url, e2e test, help caution

Review follow-ups on the `omnigent diagnose` PR:

- Redact userinfo and query/fragment from the reported `server_url` so a
  `--server https://user:pass@host` value can't leak credentials into the
  snapshot (the "safe to paste into an issue" invariant).
- Add CLI-level tests (CliRunner + respx over /v1/info) exercising the command
  wiring and output format end-to-end, alongside the existing unit tests.
- Note in `--help` that `--server` should point only at a trusted server, since
  reaching a managed server may attach stored/ambient credentials to the request
  (same behavior as `session export` / `run --server`).

Auth is intentionally still attached to the /v1/info probe: a managed server
sits behind an auth proxy that 401s an unauthenticated request, so dropping it
would break the OSS-vs-managed signal for exactly the managed case. Attaching
credentials to the request does not put secrets in the output, which is what the
"secret-free" guarantee covers.

Co-authored-by: Isaac

* fix(cli): harden diagnose URL redaction + register in subcommand allowlist

- _redact_url: fix two leaks the review found. Scheme-less inputs with userinfo
  (`user:pass@host:6767`) were returned unchanged because urlsplit reads the
  `user:` as a scheme — now scrubbed. IPv6 literals lost their required `[...]`
  brackets when netloc was rebuilt from hostname/port — now the userinfo is
  dropped off the authority in place, preserving brackets and host casing.
- Add `diagnose` to `_CLICK_SUBCOMMANDS` so `omnigent diagnose` is reachable
  from main() (a registered command missing from the allowlist is rejected as
  removed ad-hoc chat). Fixes test_click_subcommands_allowlist_covers_registered_commands.

Co-authored-by: Isaac

* fix(cli): make diagnose URL redaction leak-proof on malformed/scheme-less input

Follow-up on review: _redact_url used urlsplit, which raises ValueError on a
malformed IPv6 URL (the fallback then returned the raw string, leaking any
user:pass@) and left query/fragment intact on scheme-less inputs. Rewrote it as
pure string surgery — cut at the first ?/#, then drop a user:pass@ prefix from
the authority — so credentials and tokens are stripped uniformly regardless of
URL shape, with no parser that can raise. IPv6 brackets and host casing are
preserved.

Co-authored-by: Isaac
2026-08-03 16:07:23 +08:00
Serena Ruan e9184c4254 fix(web): hide empty Projects header kebab when no projects (#3930)
The Projects group-header kebab (⋯) rendered next to "New project" even
when its menu had no items to show. With no projects filed, neither the
expand/collapse controls (need projectNames.length > 0) nor "Select
sessions" (needs project sessions) apply, so the menu opened empty.

Gate the kebab on whether either item is available, leaving only the
"New project" button when there's nothing to offer.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 16:01:00 +08:00
Pat Sukprasert 3df84178a0 refactor: type remaining runner app boundaries (#3926)
* refactor: type runner app boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor: type runner spec unwrapping

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 07:52:49 +00:00
Serena Ruan 9e568ae3fa fix(web): keep session scope tabs during bulk selection (#3927)
The "My sessions" / "Shared with me" tabs were hidden whenever bulk
selection mode was active, stranding the viewer on whichever scope they
happened to be on. Keep the tabs visible during selection so the scope
stays switchable.

Selection is a single global set while the tabs show disjoint,
ownership-scoped slices, so changing the visible tab now exits selection
mode — otherwise the bulk-action bar would show a stale count carried
over from the other tab. This is centralized in a `switchTab` helper used
by both the tabs' onValueChange and the "New session" snap-back (which
sets the tab outside Radix's onValueChange path), so no tab change can
skip the selection cleanup.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 15:16:55 +08:00
Pat Sukprasert 67ae4ef92b refactor: type runner app JSON payloads (#3923)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 07:04:22 +00:00
Hubert a4248e34d2 Add product-analytics abstraction to web frontend (#3569)
* Add product-analytics abstraction to web frontend

Introduce an opt-in, host-injected analytics seam so an embedding host can
collect user actions (clicks, field value-changes, page views) keyed by a
stable componentId. Fully inert standalone: when no host sink is configured
via OmnigentHostConfig.analytics, every emit is a no-op.

- lib/host.ts: OmnigentAnalyticsEvent type + analytics? sink + getter.
- lib/analytics.ts: emitOmnigentAnalytics, useOmnigentAnalytics
  (trackClick/trackValueChange, values redacted by default for PII), and
  useOmnigentPageView (re-fires on pathname change, like the unified router).
- Button/Input: optional componentId prop that reports clicks/value-changes.
- lib/routing.tsx: optional componentId on Link (OmnigentLinkProps) so a
  link can opt into per-link analytics; standalone strips it.
- App.tsx: central <PageView id> wrapper declares each route's page-view id
  next to the route table; SettingsPage keeps its own hook (param-derived
  settings.<section> id) as the escape hatch.
- Example componentIds: chat composer send, tasks search, sidebar
  conversation switcher, settings "Back to Omnigent" link.

Distinct from lib/telemetry.ts (low-level OTEL HTTP tracing); this is
application-level user-action analytics.

Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

* Ci

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>

---------

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
2026-08-03 09:03:11 +02:00
Serena Ruan cbd4a4700a feat(sessions): auto-connect a wakeable runner on shell create (#3919)
* feat(sessions): auto-connect a wakeable runner on shell create

Creating a shell from the web UI on a session whose runner had gone to
sleep dead-ended on a 502 ("no runner available"), even though the host
was still up and the next chat message would have transparently woken it.

Add `ensure_runner_connected`, which runs the same runner-acquisition
ladder `post_event` uses (wake a stale resumable managed sandbox, launch
a runner on a live host, or relaunch a managed sandbox) without the
message-specific side effects, and call it from `create_session_terminal`
before proxying. Wakeable states reconnect and the shell opens; a
non-host-bound stranded session or an offline external host still 502s
(the CLI reconnect path owns those).

Surface connect state on the "+" → Shell menu item: it stays enabled and
shows "Reconnecting…" with a spinner while the server wakes the runner on
a wakeable session, and is disabled + labeled "Offline" for states the
browser can't reconnect. Widen the menu so the longer label isn't clipped.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(sessions): wait the connect grace before relaunching on shell create

Address PR review: ensure_runner_connected went straight to
_launch_runner_on_host whenever no runner client resolved, so opening a
shell against a session whose runner was merely booting (tunnel not yet
registered) would spawn a second runner and orphan the booting one —
diverging from post_event, which it claims to mirror.

When the session has a pinned runner_id and a live host, first wait
_HOST_BOUND_RUNNER_CONNECT_GRACE_S for it to connect (racing a
host.runner_status query that cuts the wait short if the host reports it
gone), and only relaunch if it's truly dead. Also drop the unused
tuple binding at the call site (the proxy re-resolves the client).

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 14:40:20 +08:00
Tomu Hirata f0d70e6859 fix(runner): allow managed mint in InitialAuthTokenFactory fallback (#3902)
* fix(runner): allow managed mint in InitialAuthTokenFactory fallback

When a managed sandbox runner starts with a host-provided bearer
(_InitialAuthTokenFactory), and that bearer is rejected (401), the
fallback resolver was called with _allow_delegated_mint=False. This
blocked the managed-mint path entirely, leaving the runner with no
credential for its HTTP callbacks.

For managed runners (OMNIGENT_RUNNER_DELEGATED_AUTH=1 + binding token),
the fallback must be able to reach the managed-mint path after the
initial bearer expires — the same path used by runners that start
without a host bearer. Removing _allow_delegated_mint=False restores
this: SDK/OIDC auth still wins when present; managed mint is the
natural last resort for sandbox runners with no user credential.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): pass proxy bearer through managed mint so Apps proxy lets it through

The managed-mint endpoint (POST /v1/runners/{id}/token) is authenticated
by the runner's binding token, but on Databricks Apps deployments the
proxy layer sits in front and requires a valid Authorization header on
every request. With no bearer, the proxy returns 401 before the request
reaches Omnigent — the same symptom as the _allow_delegated_mint=False
regression, but a separate root cause.

Fix: thread an optional proxy_bearer through _make_managed_mint_factory,
_ManagedMintTokenFactory, and _mint_managed_owner_token, passed to
databricks_request_headers as the Authorization header. The initial
host bearer seeds it; after the first successful mint the minted JWT
replaces it as the proxy bearer for subsequent refreshes.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): reuse runner auth factory in codex discover-and-forward

_codex_discover_thread_and_forward was calling _make_auth_token_factory()
fresh, but RUNNER_INITIAL_AUTH_TOKEN is already popped from env by
runner startup — so the fresh call went straight to managed mint with no
proxy bearer, getting 401 from the Apps proxy before reaching Omnigent.

Fix: accept auth_token_factory at the call site, extracted from the
server_client's _RunnerDatabricksAuth (which already carries the correct
proxy bearer). supervise_forwarder also reuses it.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): store auth factory as singleton so all call sites share proxy bearer

Every _make_auth_token_factory() call after runner startup (harness setup,
terminal creation, forwarders) was building a fresh factory with no proxy
bearer, because RUNNER_INITIAL_AUTH_TOKEN had already been popped from env.
Each fresh factory hit the delegated-mint path, got 401 from the Apps proxy,
and left that call site with no credential.

Fix: store the factory built by serve_runner in a module-level singleton
(_runner_auth_factory). Subsequent _make_auth_token_factory() calls with
default args return it directly, so all call sites across orchestration.py
and app.py share the proxy bearer without any individual patching.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): fix __main__ vs omnigent.runner._entry module identity

When the runner runs as python -m omnigent runner, _entry.py executes
as __main__, creating a module object separate from omnigent.runner._entry.

Two bugs:
1. _runner_auth_factory was set on __main__ but read from
   omnigent.runner._entry (always None). Fix: set it on the canonical
   module via import omnigent.runner._entry as _self_module.

2. isinstance(server_client.auth, _RunnerDatabricksAuth) was False
   because server_client.auth is __main__._RunnerDatabricksAuth while
   the check used omnigent.runner._entry._RunnerDatabricksAuth. Fix:
   use getattr(server_client.auth, _factory, None) instead.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(runner): drop auth_token_factory param from codex discover-and-forward

Now that _make_auth_token_factory() returns the runner singleton (which
carries the proxy bearer), the explicit param and the server_client auth
introspection that fed it are no longer needed.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(runner): introduce _set_runner_auth_factory to set singleton

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: use sys.modules to set singleton, restore docstring, remove dup comment

- Replace self-import with sys.modules lookup to avoid the module
  importing itself (also sets on __main__ as a fallback).
- Move singleton early-return to after the docstring so __doc__ is
  preserved on _make_auth_token_factory.
- Remove duplicated comment block in _codex_discover_thread_and_forward.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: import canonical module before setting singleton to ensure sys.modules registration

sys.modules.get() returns None when running as __main__ because the
canonical name isn't registered yet. Importing it first forces
registration, then both the canonical module and __main__ get the
singleton set.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: shorten overlong docstring in test

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: reuse singleton when server_url matches runner URL

Callers like native_policy_hook.py pass server_url explicitly but still
want the shared factory. The singleton guard now matches on both None
and the runner's own RUNNER_SERVER_URL.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-03 06:21:27 +00:00
Pat Sukprasert 05b59d6eaa refactor: type native runner orchestration (#3911)
* refactor: type native runner orchestration

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* style: use pass in typing stubs

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: preserve dynamic resolved spec compatibility

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: preserve pi fallback tools without spec

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 12:55:49 +07:00
Tomu Hirata b7182c80ec fix(setup): count visual terminal lines for clear-on-exit erase (#3904)
rendered.count("\n") undercounts when Rich wraps a long status label
(e.g. "✓ Isaac-Databricks-Ai-Gateway") across multiple terminal rows.
The cursor-up escape then doesn't move far enough, leaving stale menu
frames in the scrollback — which makes the "Configure harnesses" block
appear to stack on every loop iteration.

Replace the newline count with _count_terminal_lines(), which strips ANSI
escapes and uses ceiling division of each line's cell width by the terminal
width to count actual visual rows.

Tests cover no-wrap, wrapping, exactly-full-width, ANSI stripping, and the
empty-string edge case.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-08-03 14:46:13 +09:00
Rajarshi Datta 4c593a5140 fix(shell-tools): unify shell tool defaults across policies for consistent command inspection (#3888) 2026-08-03 05:42:10 +00:00
Daniel Lok 77ef173992 feat(claude-native): derive idle/working status from Claude's session file (#3906)
The claude-native session's Working/idle badge is driven by diffing the
tmux pane (the PTY watcher in resource_registry). That heuristic can't
tell "blocked on a prompt" from "working", and only flips to idle after
~1s of pane quiescence rather than on the real turn edge.

Claude Code writes a per-process status file at
`<config_dir>/sessions/<pid>.json` (its internal "concurrentSessions"
registry, present since v2.1.139) whose `status` flips idle/busy/waiting
on the actual turn edges. Prefer that for the claude-native running/idle
status, falling back to the PTY watcher when the file is absent (old
Claude, missing config dir) or never resolves.

- New `omnigent/claude_native_status_file.py`: `resolve_status_file`
  (pid-first via the tmux pane pid, which equals Claude's pid on this
  launch path; sessionId cross-check + freshness-bounded scan fallback),
  `read_session_status` (busy/waiting -> running, idle -> idle), and a
  `SessionStatusPoller` that lazily resolves then mtime-polls the cached
  path and emits deduped status edges, deactivating when the file
  vanishes on clean exit.
- terminal.py: add `pane_pid_sync()` and an `on_tick` hook so the poller
  runs on the existing watcher cadence — no second thread.
- resource_registry.py: for the claude-native role only, build the poller
  and drive it via `on_tick`; while it is active the PTY on_activity/
  on_idle edges defer status to the file. The PTY watcher keeps owning
  the activity badge and exit detection, and reclaims status if the file
  never resolves or disappears.

waiting maps to running for now (no new status vocabulary); surfacing a
distinct "needs input" state is a possible fast-follow.


Co-authored-by: Isaac

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
2026-08-03 13:28:44 +08:00
Pat Sukprasert 77209694c2 feat(acp): support OpenClaw Gateway ACP registration (#3420)
* feat(acp): support per-agent Omnigent MCP toggle

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(acp): preserve empty MCP session field

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(acp): honor MCP toggle for embedded agents

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(acp): validate MCP toggle type

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(setup): report invalid ACP config

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-08-03 05:01:03 +00:00
Serena Ruan 48249e8154 chore(ci): update Discord watch rotation (#3913)
Update the Discord-watch roster (rotation_roster.json), leaving 9 people in the rotation. Prune elapsed dates from the schedule and extend the
horizon through 2026-10-30 so every upcoming weekday is assigned to a
current roster member.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 12:55:41 +08:00
Serena Ruan abd363d232 fix(web): tune sidebar vertical spacing rhythm (#3908)
* fix(web): tune sidebar vertical spacing rhythm

Refine the sidebar's padding and gaps so the primary nav reads as a
proper section and the row lists sit on a consistent rhythm:

- Primary nav (New session / Automations / Inbox): 8px gap to the
  Omnigent header (pt-2), no bottom padding of its own (pb-0); the 16px
  gap below now comes from the scrolling list (pt-4), matching the
  section-to-section gap-4 rhythm.
- Nav rows and session rows are 32px tall (h-8) with 4px vertical
  padding (py-1).
- Section headers (Pinned / Projects / Sessions) get 8px bottom
  padding (pb-2).
- Session rows and project folder rows stack flush (gap-0).
- Bulk-action bar uses uniform 6px padding (p-1.5).

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

* test(web): update sidebar spacing assertions to new rhythm

Bring the existing layout assertions in line with the tuned spacing:
primary nav pt-2/pb-0, nav + session rows h-8, iconless section header
pb-2.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): expect 32px session row height after spacing bump

Session rows moved from h-7 (28px) to h-8 (32px) in the sidebar
spacing tune; update the row-layout e2e assertion to match.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-08-03 12:45:20 +08:00
Serena Ruan df5f39fd51 fix(web): drop active-session highlight in sidebar selection mode (#3912)
When "Select sessions" is toggled on, the currently-viewed session's row
kept its active background even though it wasn't explicitly selected,
making the selection state ambiguous. Gate the active-route highlight on
`!selectionMode` so a row shows a background only when it's the active
session (normal mode) or explicitly checked (selection mode).

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-08-03 12:44:42 +08:00
Pat Sukprasert be7dfb2491 refactor: type runner tool dispatch (#3907)
* refactor: type runner tool dispatch

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: preserve closed labels with mixed metadata

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 11:43:01 +07:00
Pat Sukprasert 2fcc0c4781 chore: scope mypy exceptions to generated routing stubs (#3909)
* refactor: type generated routing stubs

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* chore: scope mypy exceptions to generated routing stubs

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-03 11:42:33 +07:00
github-actions[bot] 468e104065 chore(ci): extend Discord watch rotation schedule (#3819)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-03 11:23:26 +08:00
Pat Sukprasert 042f0ddc43 chore(web): remove unused react-router dependency (#3692)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-02 13:19:00 +00:00
Pat Sukprasert a31e9afcc8 refactor: type Codex native forwarder boundaries (#3887)
* refactor: type Codex native forwarder boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs: explain idless Codex elicitation handling

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-02 20:18:46 +07:00
Pat Sukprasert b28ca03c7e refactor: type Claude native bridge boundaries (#3885)
* refactor: type Claude native bridge boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test: validate OpenCode MCP config strings

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-02 10:13:21 +00:00
Pat Sukprasert b26bffc1bf refactor: centralize Python JSON type aliases (#3884)
* refactor: centralize JSON type aliases

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs: clarify shared JSON type contracts

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-02 08:55:13 +00:00
Pat Sukprasert 02cfde1c0d refactor: type Claude native boundaries (#3879)
* refactor: type Claude native boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor: simplify Claude JSON narrowing

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-02 16:20:46 +08:00
Pat Sukprasert 297425b08b refactor: type Codex native boundaries (#3859)
* refactor: type Codex native boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: preserve malformed Codex resume handling

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-02 11:09:08 +08:00
Corey Zumar d880afec01 fix(web): file new-in-project sessions under their project immediately (#3869)
* fix(web): file new-in-project sessions under their project immediately

Stamp the omni_project label at session create so a session created from
the new-session composer is born filed under its project, instead of
flashing under the ungrouped "Sessions" section for a couple of seconds
until the follow-up project_id move catches up in the search-indexed
session list. The sidebar dual-reads project membership from the label
or the first-class project_id, so the row groups under its project from
its first appearance; the existing move then promotes it to project_id.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): cover born-filed new-session-in-project flow

Add a Playwright e2e that lands on the /?project=<name> composer and asserts
the create POST /v1/sessions carries the omni_project label, so a session
created inside a project is filed under it immediately (satisfies the
E2E UI Required coverage gate for this web behavior change).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* docs(web): correct the born-filed move-failure catch comment

If the project_id move fails, the session stays filed via its create-time
omni_project label (not unfiled) — fix the stale catch comment.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-01 09:39:22 -07:00
Corey Zumar 1f6b06975d perf(web): make add-to-project instant — optimistic sidebar move + slim PATCH response (#3784)
* perf(web): make add-to-project instant — optimistic move + slim PATCH

Moving a session into a project waited on resolve→PATCH→refetch, with
the PATCH shipping a ~415KB items snapshot, so the row sat in its old
section for seconds. Overlay the membership optimistically from the
cached project id, render folder bodies as the union of their own pages
and the loaded window (so the row lands in-folder in one frame), and
return the PATCH snapshot without items (~1KB).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* chore(server): regenerate openapi.json for the PATCH sessions docstring

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): keep folder-only rows visible through an optimistic move

A row loaded only via an expanded folder's own pagination has no copy in
the flat window for the folder union to re-home, so dropping it from its
source folder blanked it from the sidebar until the refetches landed.
Insert such rows into the target folder's cached page and skip the
removal when nothing else can show them. Adds a browser e2e covering the
sidebar move flow end-to-end.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-01 08:51:54 -07:00
Pat Sukprasert 33765c215e refactor: type resume picker boundaries (#3858)
* refactor: type resume picker boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: honor mapping labels in resume picker

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 14:07:40 +00:00
Pat Sukprasert 8ca004a514 refactor: type REPL session contracts (#3860)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 14:03:50 +00:00
Pat Sukprasert 9e95a3604e refactor: narrow CLI typing boundaries (#3857)
* refactor: narrow CLI typing boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: reject malformed routing config values

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 13:46:49 +00:00
Pat Sukprasert ded6d0f333 refactor: narrow session orchestration contracts (#3853)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 13:15:21 +00:00
Pat Sukprasert 86d2ab8714 refactor: narrow session helper boundaries (#3851)
* refactor: narrow session helper boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: narrow policy hook payload fields

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 13:12:55 +00:00
Pat Sukprasert eac9579aa3 refactor: narrow server app router contracts (#3849)
* refactor: narrow server app router contracts

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test: cover custom auth login URL

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: clarify custom auth route handling

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:58:53 +00:00
Pat Sukprasert 8177a4bce6 refactor: type Goose tmux payload (#3845)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:52:53 +00:00
Pat Sukprasert b57890e2c4 refactor: isolate psutil typing boundaries (#3850)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:52:13 +00:00
Pat Sukprasert de77d23fc6 refactor: type native shell terminals (#3846)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:47:39 +00:00
Pat Sukprasert 1362209448 refactor: type native prompt builder (#3847)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:47:23 +00:00
Pat Sukprasert 27fa0c06f3 refactor: type Antigravity MCP config (#3844)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:47:21 +00:00
Pat Sukprasert 42159f5936 refactor: distinguish launcher temp directories (#3841)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:47:04 +00:00
Pat Sukprasert 7b36dec178 refactor: narrow executor usage span (#3843)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:46:49 +00:00
Pat Sukprasert 3b6123a509 refactor: narrow Antigravity response text (#3848)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:42:36 +00:00
Pat Sukprasert 5402e45748 refactor: type generated build info (#3840)
* refactor: type generated build info

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs: link build info generator contract

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:19:39 +00:00
Pat Sukprasert 9960369b31 refactor: type Hermes model config (#3842)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:17:13 +00:00
Pat Sukprasert 00bfea24f3 refactor: validate runner compaction responses (#3837)
* refactor: validate runner compaction responses

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: reject malformed compaction token counts

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:11:24 +00:00
Pat Sukprasert 3d8693ad41 refactor: type project store session (#3839)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 12:10:54 +00:00
Pat Sukprasert df49a1b489 refactor: type compressed text column (#3838)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:55:07 +00:00
Pat Sukprasert 38e5a66aef refactor: type Kimi executor boundaries (#3836)
* refactor: type Kimi executor boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor: accept read-only Kimi mappings

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:51:38 +00:00
Pat Sukprasert afb8379ea0 refactor: narrow spec parsing boundaries (#3833)
* refactor: narrow spec parsing boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor: reuse shared executor auth union

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:47:28 +00:00
Pat Sukprasert a7d7090c19 refactor: type runner service contracts (#3832)
* refactor: type runner service contracts

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* style: use protocol method bodies

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:42:41 +00:00
Pat Sukprasert b760776f60 refactor: narrow Claude hook payloads (#3835)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:41:28 +00:00
Pat Sukprasert d3dd6282a0 refactor: narrow egress CA key types (#3831)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:25:03 +00:00
Pat Sukprasert 4ab4d40287 refactor: type session route boundaries (#3830)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:19:07 +00:00
Pat Sukprasert 587c24e45b refactor: type sandbox host launchers (#3828)
* refactor: type sandbox host launchers

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: preserve legacy sandbox start kwargs

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* style: use protocol method body

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:18:01 +00:00
Pat Sukprasert b1e0e15ddf refactor: narrow provider discovery payloads (#3829)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:05:28 +00:00
Pat Sukprasert 008550b745 refactor: align native executor content types (#3827)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 11:04:24 +00:00
Pat Sukprasert 2b346f0418 refactor: type Kimi bridge payloads (#3825)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:48:18 +00:00
Pat Sukprasert 14984fd9c4 refactor: narrow residual Python boundaries (#3826)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:46:25 +00:00
Pat Sukprasert d8976c69b4 refactor: type Hermes bridge payloads (#3824)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:41:32 +00:00
Pat Sukprasert 0124706cbd refactor: type native interrupt dependencies (#3821)
* refactor: type native interrupt dependencies

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* style: use explicit protocol bodies

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:39:46 +00:00
Pat Sukprasert ae1e4181ec refactor: narrow runner policy payloads (#3818)
* refactor: narrow runner policy payloads

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: enforce runner policy transform contracts

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:39:14 +00:00
Pat Sukprasert 5ad2812c68 fix: reject malformed install ledgers (#3822)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:35:31 +00:00
Pat Sukprasert 7c112a2281 refactor: type cursor bridge payloads (#3823)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:34:31 +00:00
Pat Sukprasert aef4acf106 refactor: narrow native dispatch hooks (#3820)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:20:47 +00:00
Pat Sukprasert 64eb2ab434 refactor: type identity migration updates (#3813)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:15:28 +00:00
Pat Sukprasert d65f150e7d refactor: narrow migration driver values (#3810)
* refactor: narrow migration driver values

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: validate binary migration values

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs: clarify migration UUID inputs

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:08:00 +00:00
Pat Sukprasert c771d3562a refactor: type install ledger payloads (#3817)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:06:21 +00:00
Pat Sukprasert c3201a342d refactor: narrow session metadata state (#3816)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 10:02:30 +00:00
Pat Sukprasert c352b8a3cf refactor: narrow server request boundaries (#3815)
* refactor: narrow server request boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: handle malformed runner not-found responses

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: reject malformed runner JSON

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:59:27 +00:00
Pat Sukprasert b693a91a23 refactor: narrow harness metadata types (#3811)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:48:42 +00:00
Pat Sukprasert b6f2ca5f0a refactor: narrow update metadata parsing (#3812)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:42:59 +00:00
Pat Sukprasert 71aba90938 refactor: narrow cursor usage inputs (#3809)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:40:29 +00:00
Pat Sukprasert 924cda6f04 refactor(loader): type sandbox defaults (#3776)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:37:42 +00:00
Pat Sukprasert ddb90b1735 refactor(egress): type proxy transports (#3780)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:37:00 +00:00
Pat Sukprasert b23a8da7c9 refactor: tighten built-in policy types (#3808)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:36:43 +00:00
Pat Sukprasert aaf2fd35f5 fix: require model for fresh harness turns (#3806)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:29:00 +00:00
Pat Sukprasert 02137007ce refactor: preserve UI environment type (#3805)
* refactor: preserve UI environment type

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor: accept mapping banner environments

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:22:04 +00:00
Pat Sukprasert 62c9fa3cea fix: require builtin session context (#3804)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:05:49 +00:00
Pat Sukprasert bdb0ae455a refactor: export session stream explicitly (#3803)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 09:05:26 +00:00
Pat Sukprasert 2648a80aa8 refactor: type policy hook requests (#3802)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 08:59:29 +00:00
Pat Sukprasert ceca01c45a refactor: narrow local tool paths (#3801)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 08:57:35 +00:00
Zeyi (Rice) Fan 5072ba7387 feat(cli): tidy omnigent --help command copy and hide the update alias (#3797)
Follow-up polish on the grouped/colored help (#3795). Focused on the
one-liner copy and a duplicate listing entry.

- Normalize harness short help to `Launch <Name> with Omnigent.` — was
  an inconsistent mix of `Launch [the] <Name> [TUI] in an Omnigent
  terminal`, and "in an Omnigent terminal" was noisy.
- Trim over-long / over-specific one-liners:
  - `attach`: drop the "— never starts anything" clause (the body still
    explains it's a pure client).
  - `uninstall`: `Uninstall Omnigent from this machine.`
  - `usage`: `Show your Omnigent usage and costs.` (was pinned to
    today / 7 / 30 days).
  - `upgrade`: `Upgrade Omnigent to the latest release.`
  - `debug`: `Internal maintenance commands.`
- Hide the `update` alias (same Click object as `upgrade`) from the
  listing via `_ALIAS_COMMANDS`, so it no longer shows as a duplicate
  line; it stays registered and runnable.
- Update/extend tests for the new copy and the hidden `update` alias.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-01 03:23:34 +00:00
Zeyi (Rice) Fan ec5b1e55be feat(cli): group, colorize, and gate omnigent --help harnesses (#3795)
Split the top-level `omnigent --help` command list into two sections —
`Harnesses` (agent/harness launch commands) and `Commands` (everything
else) — add brand-accent color, and hide harnesses whose optional extra
isn't installed (with a small notice pointing at `omnigent setup`).

- Add a `format_commands` override on `_OmnigentCLI` that partitions
  visible subcommands using a `_HARNESS_COMMANDS` set, sharing one
  aligned help column across both sections.
- Colorize headings (`Usage:`, `Options`, `Harnesses`, `Commands`) in
  the brand accent, harness names in accent, other command names in
  cyan, and option flags in green — via `format_usage`/`format_options`
  overrides and a `_help_style` helper.
- Hide extras-gated harnesses (`cursor`, `antigravity`) from the listing
  when their SDK isn't importable, via `_harness_extra_checks` (lazy
  `find_spec` predicates). The commands stay runnable — running one
  offers to install the extra. When any are hidden, show a dim notice
  pointing at `omnigent setup` (which lists those harnesses and offers
  the install), rather than enumerating extras that may change.
- Color is gated on `NO_COLOR` and Click strips ANSI on non-TTY sinks,
  so piped/CI help stays plain. Alignment is ANSI-safe (Click's
  `term_len` strips escapes before measuring columns).
- Add tests covering grouping, the extras-gated show/hide, and the notice.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-01 02:47:35 +00:00
Zeyi (Rice) Fan cb98a14d98 feat(cli): preserve extras and refuse unsafe installers in omni upgrade (#3796)
## Related issue

N/A

## Summary

- Added `--extra`, `--target-version`, and `--dry-run` flags to `omni upgrade`.
- Upgrade commands for `uv tool` and `pipx` now read the originally requested extras from the installer's receipt/metadata and preserve them.
- Explicitly refuses auto-upgrade for `pip` and `uv pip` because those installers do not record requested extras, making a safe automatic upgrade impossible.
- Fixed installer metadata detection in `uv tool` installs by avoiding `Path.resolve()` on the `bin/python` symlink, which previously pointed to the shared uv interpreter and missed `uv-receipt.toml`.
- Added/updated unit and CLI tests covering the new behavior.

## Test Plan

- `uv run pytest tests/cli/test_upgrade_command.py tests/cli/test_update_check.py tests/cli/test_cli.py -q --timeout=60` → **383 passed**.
- `uv run pytest tests/cli/test_update_check.py -q --timeout=60` → **112 passed**.
- Manually built a local wheel, installed it as a `uv tool`, and verified dry-run output.
- Verified `--extra` unions with detected extras.
- Verified `uv pip` install is refused with a manual-upgrade message.

## Demo

N/A

## Type of change

- [x] Feature
- [ ] Bug fix
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Manual verification was done by building local wheels and installing the current dev version as a `uv tool`:

```bash
# 1. Build wheels
rm -rf /tmp/omnibuild && mkdir -p /tmp/omnibuild
uv build --wheel -o /tmp/omnibuild .
uv build --wheel -o /tmp/omnibuild sdks/python-client
uv build --wheel -o /tmp/omnibuild sdks/ui

# 2. Install as uv tool with the "all" extra
rm -rf /tmp/omni-dev-test
UV_TOOL_DIR=/tmp/omni-dev-test uv tool install --find-links /tmp/omnibuild \
  '/tmp/omnibuild/omnigent-0.8.0.dev0-py3-none-any.whl[all]' --force

# 3. Dry-run upgrade from outside the source repo
cd /tmp && /tmp/omni-dev-test/omnigent/bin/omni upgrade --dry-run --target-version 0.8.0
```

Output:

```text
Targeting v0.8.0.
Detected installer: uv
Detected extras: all
Would run: uv tool install --reinstall omnigent==0.8.0[all]
```

Adding `--extra server` unions with the detected extra:

```bash
cd /tmp && /tmp/omni-dev-test/omnigent/bin/omni upgrade --dry-run --target-version 0.8.0 --extra server
```

Output:

```text
Targeting v0.8.0.
Detected installer: uv
Detected extras: server
Would run: uv tool install --reinstall omnigent==0.8.0[all,server]
```

A `uv pip` install is correctly refused:

```text
omnigent was installed with `uv pip`, not `uv tool install`. `uv pip` does not record which extras were requested, so `omni upgrade` cannot preserve them safely. Upgrade manually:

    uv pip install -U omnigent
    # or, if you need extras:
    uv pip install -U 'omnigent[your,extras,here]'
```

## Changelog

`omni upgrade` now preserves requested extras for `uv tool` and `pipx` installs, supports `--extra`, `--target-version`, and `--dry-run`, and refuses to auto-upgrade `pip` / `uv pip` installs because those installers don't record extras.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-31 19:23:27 -07:00
Zeyi (Rice) Fan c5b3310edd chore(pnpm): drop nodeLinker: hoisted for the default isolated layout (#3497)
## Related issue

N/A

## Summary

- `nodeLinker: hoisted` was a compatibility shim from the npm→pnpm migration
  that forced an npm-style flat `node_modules`. Removing it returns pnpm to its
  default isolated/symlinked layout (packages under `node_modules/.pnpm/…`),
  restoring strict dependency isolation — dependencies must be declared, so
  phantom/undeclared deps stop resolving by accident.
- Validated that the blockers the shim was assumed to guard against don't
  actually block under the isolated layout (details in Test Plan). The Shiki
  cyclic-import crash is handled by the existing `manualChunks` guard in
  `web/vite.config.ts` (a chunking concern, independent of the node linker), and
  electron-builder v26 collects the production dependency tree correctly through
  pnpm's symlinks.

## Test Plan

Validated locally under the isolated layout:
- `pnpm install --frozen-lockfile` — clean and lockfile-consistent (the linker
  setting is not part of the lockfile, so no lockfile churn).
- `pnpm --filter web run build` — succeeds; Shiki resolves to a single acyclic
  chunk via the existing `manualChunks` guard.
- Electron packaging: `pnpm --filter web run build:overlay` then
  `electron-builder --dir` builds and signs the app; inspected the resulting
  `app.asar` — it bundles exactly the production dep tree (`electron-updater`,
  `js-yaml` + their 14 transitive deps) with zero dev-dependency bloat.
- Tailwind v4 `@source` scan follows the symlink: the emitted CSS is
  byte-identical between the hoisted and isolated builds.
- oxlint (schema) and prettier run; `node web/node_modules/vite/bin/vite.js
  --version` (Android Gradle entry) and `web/node_modules/.bin/tsc --version`
  (iOS Fastlane probe) resolve via pnpm's direct-dependency symlinks.

Not runnable locally — relying on CI to confirm: Docker image build,
`electron-build` full installers, and `android-bundle` / iOS app builds.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [x] Not applicable

## Coverage notes

Node-linker layout has no unit-test surface; verified manually via a full
install + web build + electron `--dir` packaging (inspecting the packaged
`app.asar` dependency tree) + a Tailwind CSS byte-diff, and confirmed the
hardcoded node_modules paths (vite entry, tsc/prettier/oxlint) resolve through
pnpm's direct-dependency symlinks. Remaining platform builds are covered by CI.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-31 18:18:26 -07:00
Zeyi (Rice) Fan 1ebe83f40c refactor(sandboxes): remove legacy registry fallback and add provider docs (#3471)
N/A

- Remove the legacy `_LAUNCHERS` fallback from `__init__.py` — all providers
  are now resolved exclusively through the `SandboxProviderRegistry`
  contribution-based registry.
- Simplify `get_launcher()` to a single code path (no more
  `DeprecationWarning` / legacy import fallback).
- Remove unused `warnings` / `importlib` / `importlib.util` imports from
  `__init__.py`.
- Add `docs/extending/sandbox_providers.md` documenting how to implement and
  register a third-party sandbox provider, including a minimal example
  package with `pyproject.toml` entrypoint, the namespace requirement, and
  the capability reference table.

```bash
uv run pytest tests/onboarding/sandboxes tests/sandbox tests/cli/test_cli.py tests/server -k managed -q
pre-commit run --files <changed files>
```

All 782 selected tests pass and pre-commit is clean.

N/A

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

Existing tests pass unchanged. The test that expected a `DeprecationWarning`
from the legacy path was updated to no longer suppress it. New docs are
prose-only and need no test coverage.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-31 18:11:11 -07:00
Zeyi (Rice) Fan f63b297508 perf(web): load Shiki language grammars lazily instead of in the eager core chunk (#3496)
## Related issue

N/A

## Summary

- The `manualChunks` guard that keeps Shiki in one chunk (to avoid a cyclic
  import crash — the language-index ↔ alias-map split that throws "Cannot read
  properties of undefined (reading 'flatMap')" and blanks the Monaco/file
  viewer) matched both `/shiki` and `/@shikijs/`. That also swept every
  `@shikijs/langs/<lang>` grammar — which Shiki loads via dynamic import as
  per-language chunks — into the single, eagerly `modulepreload`ed core chunk.
  So ~200 language grammars (~1.68 MB gzip) were downloaded on every initial
  page load, even though a session uses only a few languages.
- Exclude `@shikijs/langs/<lang>` from the `shiki` chunk so grammars stay lazy
  per-language chunks. Keep Shiki's core, engines, and bundle glue together so
  the cyclic core stays intra-chunk — the engines must stay too: excluding them
  re-splits the cycle across chunks and reintroduces the `flatMap` crash.
- Initial-load eager JS drops from ~11.8 MB to ~4.37 MB (Shiki 1.68 MB → 466 KB
  gzip); grammars become 427 on-demand chunks. Layout-independent (same result
  under pnpm hoisted and isolated).

## Test Plan

- `pnpm --filter web run build` succeeds.
- Verified the emitted `shiki` chunk statically imports only the rolldown
  runtime (no cross-chunk cycle) and contains `bundledLanguagesAlias`
  co-located with its reader — under both hoisted and isolated node_modules.
- Verified per-language grammar chunks (python, rust, typescript, …) are
  emitted separately and are NOT `modulepreload`ed by `index.html`.
- Recommended pre-merge smoke test: open the file viewer / Monaco editor and a
  markdown code block and confirm syntax highlighting renders.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Verified via build output analysis: the `shiki` core chunk is acyclic with the
alias map co-located (cycle fix preserved), and language grammars are emitted as
separate, non-preloaded chunks. Existing Shiki/code-block tests exercise the
runtime highlighting path; this change only affects chunk grouping, not module
behavior.

## Changelog

Faster initial load — syntax-highlighting language grammars now load on demand instead of all upfront.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-01 01:04:43 +00:00
Pat Sukprasert 0ba64ba906 refactor(sessions): narrow elicitation params (#3782)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 18:09:25 +00:00
Pat Sukprasert 3bace5d505 fix(antigravity): delegate default model to SDK (#3762)
Remove the release-specific Gemini fallback from the Antigravity SDK executor. Preserve explicit per-turn and HARNESS_ANTIGRAVITY_MODEL precedence, but omit LocalAgentConfig.model when neither is set so every supported google-antigravity 0.1.x release owns its current default for both API-key and Vertex sessions.

Expand the no-hardcoded-model scanner to recognize dotted, canonical, and normalized Gemini release ids. Add coverage that distinguishes an omitted SDK model from an explicit override, and remove the stale release example from runtime error text.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-08-01 01:57:14 +08:00
Pat Sukprasert 9c5caf4111 refactor(sessions): type policy hook boundaries (#3781)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:55:15 +00:00
Pat Sukprasert a1a91b3a22 refactor(config): narrow setup menu values (#3779)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:45:42 +00:00
Pat Sukprasert 938d03457b refactor(pi): type managed settings (#3777)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:37:06 +00:00
Pat Sukprasert e420bc9643 refactor: type crash UI tracebacks (#3778)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:37:03 +00:00
Pat Sukprasert 47b9de3253 refactor(policies): type cache lookups (#3775)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:23:21 +00:00
Pat Sukprasert c468002ecc refactor: type native wrapper JSON boundaries (#3774)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:19:12 +00:00
Hubert a384f060ec build(web): emit source maps from the embed build (#3702)
The embed build set sourcemap: false, so downstream bundlers that embed this
output (e.g. the Databricks monolith's rspack/webpack) had no input map to
chain through — host-side error stack frames bottomed out at
omnigent-embed.js:<line> instead of the original src/**.

Emit maps so the embedding host can compose them to source. dist-embed is a
build artifact (gitignored), so this ships nothing new; it only enriches the
maps hosts consume via source-map-loader.

Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
2026-07-31 10:17:09 -07:00
Pat Sukprasert e711e907a8 refactor: tighten host process typing (#3773)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:14:10 +00:00
Pat Sukprasert cad51e4a40 refactor(sessions): separate route result types (#3770)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:08:40 +00:00
Pat Sukprasert 2b9fa5f154 refactor(acp): type MCP relay boundaries (#3772)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:07:26 +00:00
Pat Sukprasert 567c281775 refactor(server): narrow optional app config (#3771)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 17:05:48 +00:00
Pat Sukprasert a860818682 refactor(databricks): type auth and stream boundaries (#3765)
* refactor(databricks): type auth and stream boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(databricks): make protocol stub explicit

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:52:09 +00:00
Pat Sukprasert 3f685f2943 refactor(types): import symbols from owners (#3769)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:49:37 +00:00
Pat Sukprasert ed615b6f4b refactor(sessions): narrow resource replay events (#3768)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:41:49 +00:00
Pat Sukprasert f61def4b35 refactor(openai): type response replay boundaries (#3766)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:38:57 +00:00
Pat Sukprasert e0779bf0ab refactor(types): document optional import boundaries (#3767)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:38:04 +00:00
Thomas Garnier fe6851e6c4 feat(sandbox): scan write_paths for dotfiles too (#3596)
* feat(sandbox): scan write_paths for dotfiles too

The dotfile / escaping-symlink masker walked cwd and every read_paths
root but skipped write_paths, so a writable directory granted outside
cwd could still leak — and let the helper overwrite — top-level secrets
like .env / .aws / .ssh.

Fold read_paths and write_paths into one deduplicated, ancestor-first
set via a new merge_scan_roots helper so every granted root is masked,
and a path granted by more than one lever (or nested under another
grant) is walked once instead of once per lever. The dedup resolves
each root a single time and skips nested roots with a lexicographic
cover scan, so the big-grant profile-size guard stays fast.

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>

* fix(sandbox): only drop nested grants when scanning recursively

Review caught that merge_scan_roots dropped a granted root whenever
another grant was its ancestor — but that subsumption only holds when
the walk is recursive. cwd_hidden_scan_recursive defaults to False,
where each walk masks only a root's immediate children, so dropping a
nested grant (e.g. write_paths: [/a/deep/nested] under read_paths:
[/a]) left its top-level dotfiles visible and writable — reintroducing
the exact leak this branch closes, and regressing the prior
per-read-root behavior.

Thread the recursive flag into merge_scan_roots: keep the cwd drop
(unchanged, pre-existing), but only collapse a grant into a kept
ancestor when recursive=True; in top-level-only mode keep every
distinct grant and drop only exact duplicates. Walk the full ancestor
chain (not just the last kept root) so an interleaving sibling name
cannot hide a real ancestor and leave a redundant walk.

Adds regression tests in both backends for the non-recursive nested
grant, plus merge_scan_roots unit coverage for both modes.

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>

* fix(sandbox): exclude framework scratch roots from the write-root dotfile scan

Extending the dotfile mask scan to write_paths also swept in the
framework-added scratch tmpdir (folded into write_roots via
with_additional_write_roots). That dir holds the sandbox's own egress
relay socket `.egress.sock` — a dotfile — so the scan masked it with
`--bind-try /dev/null` (bwrap) / a deny rule (seatbelt), cutting the
relay endpoint and resetting every egress connection. This is what broke
the inner-rest `test_egress_e2e[linux_bwrap]` cases.

Track framework write roots on the policy as `mask_scan_skip_roots` and
drop them (and anything nested under them) from `merge_scan_roots`. These
dirs are created fresh by the framework and never hold pre-existing user
secrets, so scanning them is both pointless and harmful. Genuine
user-declared read/write grants are still scanned.

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>

---------

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
2026-07-31 09:32:32 -07:00
Pat Sukprasert 98616b2aa3 refactor(runtime): narrow dynamic helper returns (#3764)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:32:09 +00:00
Pat Sukprasert 0716807dc4 refactor(config): narrow dynamic helper returns (#3763)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:28:40 +00:00
Pat Sukprasert 9b1c38da40 refactor(stores): type collection and blob boundaries (#3761)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 16:22:23 +00:00
Pat Sukprasert 73657266ed refactor(native): type pending approvals (#3760)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:55:15 +00:00
Pat Sukprasert c4c1002c3b refactor(repl): remove stale mypy ignores (#3758)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:53:02 +00:00
Pat Sukprasert f7900a811f refactor(types): remove stale mypy ignores (#3756)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:51:28 +00:00
Pat Sukprasert 5513d6f89a refactor(cli): remove stale mypy ignores (#3757)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:51:02 +00:00
Pat Sukprasert 85740d5f74 refactor(native): type read-only SQLite connects (#3759)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:49:59 +00:00
Pat Sukprasert c6b0ac4ce3 refactor(claude): type local HTTP addresses (#3751)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:49:32 +00:00
Pat Sukprasert d1d0a3dad5 refactor(codex): narrow elicitation request types (#3755)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:47:32 +00:00
Pat Sukprasert 80be19ad7b refactor(sandbox): type Win32 job APIs (#3747)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:44:18 +00:00
Pat Sukprasert 376ce558f9 refactor(runner): type transport helpers (#3753)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:43:03 +00:00
Pat Sukprasert cb245ea64a [lint] Enforce baseline-free model hardcode scanning (#3738)
* lint(models): remove the hardcode baseline

Delete the empty path/count allowlist and its parser, stale-count logic, tests, and special pre-commit trigger. The scanner now rejects every non-owned production model literal while retaining only the AST-verified StaticModelFallback boundary.

Update the migration plan to describe the final configuration/catalog/fallback state. The fully merged issue 3426 audit passes 136 focused tests, mypy, the hardcode scan, and full pre-commit.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* lint(models): scan every production model literal

Remove the model-context name heuristic so positional arguments, bare collections, and config values under arbitrary keys cannot bypass the hardcoded-model check. Preserve docstrings and structurally owned fallback records as explicit non-runtime exceptions, and distinguish complete model ids from stable family-prefix compatibility checks.

Curate the newly exposed production literals by resolving Claude's direct-login custom model through the central owned fallback and replacing release-specific CLI, Bedrock, and loader examples with provider-neutral guidance.

Validated with 172 lint/Claude tests, 63 loader tests, focused mypy, the baseline-free repository scan, and pre-commit run --all-files.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* lint(models): cover the full production tree

Run the hardcoded-model scanner across every tracked Python and supported config/shell file rather than a curated directory list. Exclude tests and generated OpenAPI explicitly, and keep the pre-commit trigger exactly aligned with the scanner surface.

Remove the unused root server config that pinned a stale Databricks model and profile. A repository-wide dry run found no other non-generated production literals outside the existing scan surface.

Validated with the focused lint suite, the baseline-free full repository scan, focused mypy, and pre-commit run --all-files.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): keep Claude custom fallback routable

Resolve the private Sonnet custom-picker id through the exact owned fallback when available, then through the first routable Sonnet-family entry if release naming drifts. Fail clearly when the owned subscription catalog contains no Sonnet entry instead of forwarding an invalid picker id.\n\nRemove the vestigial full-tree scan-root constant, keep the pre-commit parity probe direct, make the Gemini docstring fixture exercise a recognized id shape, and update the migration guide to describe the actual full-tree literal scan and runtime-prose expectations.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 22:41:56 +07:00
Pat Sukprasert 5c4702179a refactor(egress): type proxy lifecycle state (#3752)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:27:14 +00:00
Pat Sukprasert e3be897b7e refactor(runner): type session init payloads (#3745)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:11:16 +00:00
Pat Sukprasert 2ede602030 refactor(cursor): type SQLite reads (#3741)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:03:05 +00:00
Pat Sukprasert 40aa344b4a refactor(runner): type filesystem boundaries (#3742)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:02:44 +00:00
Pat Sukprasert 0564969e63 refactor(codex): narrow bridge state (#3740)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:02:25 +00:00
Pat Sukprasert 86d3761c37 refactor(policies): narrow JSON boundaries (#3735)
* refactor(policies): narrow JSON boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(policies): enforce prompt output schema

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:02:20 +00:00
Pat Sukprasert 056753e4dc refactor(sandbox): type Islo boundaries (#3748)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:51:40 +00:00
Pat Sukprasert 1636ff476c refactor(sandbox): type Seatbelt boundaries (#3746)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:42:14 +00:00
Pat Sukprasert 4d6a060324 refactor(runner): narrow entrypoint types (#3743)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:35:33 +00:00
Pat Sukprasert 4a38d85b20 refactor(executor): name event payload types (#3744)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:34:19 +00:00
Pat Sukprasert 0cc37f8e73 refactor(scheduled): type recurrence boundaries (#3734)
* refactor(scheduled): type recurrence boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(scheduled): clarify recurrence protocol

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:09:27 +00:00
Pat Sukprasert 225dd5025b refactor(tracing): type tracer boundary (#3739)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:09:11 +00:00
Pat Sukprasert 7b9a7c30cd refactor(policies): type CEL adapter boundary (#3736)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:08:33 +00:00
Pat Sukprasert 06999337f7 refactor(logging): type diagnostics state (#3737)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 14:00:20 +00:00
Pat Sukprasert 3efa197e1e refactor(onboarding): type sandbox SDK returns (#3729)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:44:37 +00:00
Pat Sukprasert 5d2cc79b8b refactor(qwen): type native bridge JSON records (#3722)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:35:02 +00:00
Pat Sukprasert 3e6038b958 refactor(pi): type native bridge JSON payloads (#3726)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:32:41 +00:00
Pat Sukprasert 43829fcc2d refactor(config): type YAML mappings (#3727)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:30:15 +00:00
Pat Sukprasert 8ee1e330bd refactor(kiro): type bridge payloads (#3725)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:26:37 +00:00
Pat Sukprasert 934bfc1b36 refactor(auth): narrow cookie claims (#3717)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:21:37 +00:00
Pat Sukprasert d6c58b983a refactor(kiro): type JSON boundaries (#3724)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:20:00 +00:00
Pat Sukprasert 589ec07e84 refactor(telemetry): type OTLP exporters (#3719)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:15:13 +00:00
Pat Sukprasert 35b06e67b2 refactor(accounts): type SQLAlchemy write results (#3718)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:12:49 +00:00
Pat Sukprasert 0dcfc7530e refactor(scheduled): type local task ownership (#3715)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:11:59 +00:00
Pat Sukprasert afcc296720 [ci] Configure automation model roles (#3453)
* ci(models): configure automation model roles

Replace provider release ids in credentialed workflows with six repository-variable roles covering Anthropic, fast Anthropic, OpenAI, E2E judge, E2E model pool, and image generation workloads.

Make the shared Omnigent agent action require an explicit model input, validate required configuration before writing provider files, and keep fail-open reviewer/image helpers on their existing degradation paths.

Use the protocol-level mock-model fixture for mock-only integration matrices and remove their unused production model-spread configuration.

Validation: parsed all action/workflow YAML; generated integration and backcompat matrices; hardcode lint and staged pre-commit passed.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* ci: fail fast without E2E judge model

Require the repository-level E2E judge model variable before running the required-check script. This turns an absent CI configuration into an immediate, actionable failure instead of allowing a later command to fail ambiguously.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* ci: clarify missing optional model variables

Keep the advisory reviewer ranker, VS Code changelog drafter, and feature-blog image generator fail-open when their repository model variables are empty.\n\nEmit actionable variable names before skipping or falling through to the existing warning path, avoiding malformed gateway requests while preserving the best-effort behavior of all three jobs.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:05:42 +00:00
Pat Sukprasert 95078c0316 refactor(routing): type smart router auth (#3714)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 13:00:09 +00:00
Pat Sukprasert cdeff996b0 refactor(stores): type scheduled task collections (#3711)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:38:32 +00:00
Pat Sukprasert d7517c154b refactor(cursor): type permission payloads (#3713)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:36:02 +00:00
Pat Sukprasert daf0baf6ed refactor(codex): type goal request boundaries (#3712)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:31:11 +00:00
Pat Sukprasert f41c51c0e9 refactor(repl): type session log boundaries (#3710)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:21:08 +00:00
Pat Sukprasert 537fa4056e refactor(migrations): type compressed text decoding (#3708)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:12:01 +00:00
Pat Sukprasert 68c96827df refactor(tunnel): use typed ASGI messages (#3707)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:09:02 +00:00
Pat Sukprasert 24bd67fbce refactor(claude): type forwarder payloads (#3706)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:03:26 +00:00
Pat Sukprasert 882fbe9c28 refactor(spec): type parser boundaries (#3705)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 11:55:03 +00:00
Serena Ruan dcda8f801c ci(web): check pnpm overrides satisfy declared ranges (#3704)
Add a lint step that fails when a `pnpm-workspace.yaml` `overrides:` pin
doesn't satisfy the range the same package declares in a workspace
`package.json`. Overrides outrank package.json, so such a mismatch
silently ignores the declared version — the trap that let a postcss
security bump (`^8.5.18`) land while the override still pinned the
vulnerable `8.5.15`, invisible to both `--frozen-lockfile` and the
lockfile-regen gate (the lock was internally consistent for the pin).

Runs in the lint job beside the existing "Check pnpm-lock.yaml is up to
date" step. The checker uses a small npm-flavored semver comparison
(`^`, `~`, exact, comparators) over the operators this repo uses;
unrecognized ranges are reported rather than passed silently.

Co-authored-by: Isaac
2026-07-31 19:15:16 +08:00
Serena Ruan 7af7c896c1 ci(release): add source-PR demo-video table to release-post PRs (#3700)
* ci(release): add source-PR demo-video table to release-post PRs

The publish-changelog workflow reformats a published release into a site post
that leaves a `TODO` demo placeholder under each feature, with no pointer to
the source PRs that may already ship a recording. Parse the feature PR refs
from the curated release body (Major new features / Breaking changes sections;
bug fixes are dropped from the post, so from the table too), detect whether
each PR already has a demo video attached (same detection as feature-blog.yml
— uploaded asset links, bare .mp4/.mov/.webm/.m4v URLs, <video> tags; images
not counted), and inject a per-section PR | Title | Demo video? table into the
release-post PR body (and the dry-run preview) so reviewers can drop an
existing clip into a placeholder instead of re-recording.

Runs independent of the LLM reflow so it also helps the raw-body fallback;
best-effort (continue-on-error), leaving the table empty on failure.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* ci(release): group demo-video table by the post's curated features

The demo-video table grouped PRs by the raw release-body sections, so it listed
every feature PR (e.g. all 20 under "Major new features") even though the
published post is curated down to a handful of headline features, each with one
demo placeholder. Reviewers saw far more PRs than the post has slots for.

Have the release-post-formatter emit a RELEASE_POST_PRS map (feature title ->
contributing PR refs) after the post, and build the table from that so its
groups match the post's numbered features and only list the PRs behind them.
Validate the map against the harvested PR set. When no map is present (raw-body
fallback, where the post keeps every feature), fall back to grouping by the raw
release sections as before.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* ci(release): match feature-section headings at any level

The demo-table section parser matched only `## ` headings, but the release body
uses `### ` (h3) section headings, so it found zero feature sections and built
an empty table. Match `#{2,}` and test the heading TEXT with startswith, so
"Major new features" / "Breaking changes" match at any level while "Bug fixes
& hardening" and "Thanks to our community" are still excluded.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-31 18:57:16 +08:00
Serena Ruan 383225f877 fix(web): bump postcss + linkify-it overrides to patched versions (#3703)
The pnpm-workspace.yaml `overrides:` block force-pinned postcss to
8.5.15 and linkify-it to 5.0.1 — both flagged by open high-severity
advisories (postcss GHSA-r28c-9q8g-f849, linkify-it
GHSA-v245-v573-v5vm). Because the override sits above package.json, the
earlier dependabot bump of postcss to ^8.5.18 (#3385) was inert: the
lock kept resolving 8.5.15, so the CVE was never actually fixed, and the
frozen-lockfile gate saw no drift.

Bump the two override pins to the patched releases and regenerate the
lock (postcss 8.5.18, linkify-it 5.0.2). Both are same-minor patch
bumps confined to security/bug fixes — unlike the vite/tailwind/
lightningcss pins in the same block, they aren't the bundler, so they
don't affect chunk splitting or the Shiki/PDF-worker asset emission the
override comment warns about. CI's Docker build + web test validate the
bundle.

Co-authored-by: Isaac
2026-07-31 18:47:48 +08:00
Pat Sukprasert b38d4a8dda [models] Persist last-known-good provider catalogs (#3641)
* feat(models): persist last-known-good catalogs

Persist validated MLflow provider catalogs under the platform user-cache directory so catalog-backed defaults survive transient GitHub and release-CDN outages after one successful fetch.

Keep the existing one-hour freshness window, fall back to stale validated data for at most seven days after a live failure, and record cache schema, upstream schema, source URL, and fetch time. Atomic replacement keeps concurrent writers from exposing partial JSON, while corrupt, incompatible, wrong-source, and over-age entries fail closed.

Make OMNIGENT_DISABLE_CATALOG_LOOKUP bypass memory, disk, and network state for hermetic tests. Cover persistence, fresh reuse, stale provenance logging, corruption repair, schema/source rejection, over-age behavior, concurrent writes, and first-run failure.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): accept compatible catalog schemas

Validate the current MLflow catalog shape without coupling live discovery or persistent cache reuse to one exact minor schema string. Accept major-version-compatible string and integer forms, continue rejecting unsupported majors and malformed values, and document when stale in-memory fallbacks retry discovery.

Production release assets for Anthropic, OpenAI, Gemini, and OpenRouter were verified against the validator; focused catalog tests and full pre-commit pass.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): preserve cache across empty catalogs

Reject empty live catalog model maps so transient or truncated upstream payloads cannot overwrite useful last-known-good data. Tighten compatible schema parsing to ASCII digits so corrupt cache metadata is ignored instead of raising.

Percent-encode provider names in release asset URLs to keep path and query delimiters inert. Add regression coverage for empty-result fallback preservation, non-ASCII schema corruption, and URL construction.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 10:16:37 +00:00
Pat Sukprasert 073bf66b5b refactor(codex): type app-server boundaries (#3691)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 10:12:42 +00:00
Serena Ruan ea0f8c74cc ci(oss): gate lockfile regen on a consistency check (#3685)
* ci(oss): gate lockfile regen on a consistency check

The OSS lockfile-regen job deleted pnpm-lock.yaml and re-resolved from
scratch every 12h, so any in-range transitive drift on public npm
produced a ~1500-line churn PR that advanced ~20 unreviewed deps for no
functional reason (e.g. #3631). The job exists to keep the tree
Docker-buildable when a manifest change desyncs the lock — not to chase
newer upstream versions.

Add a check-first gate: `uv lock --check` and pnpm
`--frozen-lockfile --lockfile-only` verify each lock still satisfies its
manifests. These pass on a consistent-but-not-latest lock, so routine
drift no longer triggers a regen; only a real manifest/lock desync flips
`drifted=true` and runs the regenerate → Docker smoke → PR steps.

Also correct the PR-body text, which claimed it regenerated "uv.lock +
web/package-lock.json" (the repo locks pnpm-lock.yaml, not
package-lock.json).

Co-authored-by: Isaac

* ci(oss): keep the Docker smoke on the no-drift path

Per PR review: ungate the Docker build + CLI smoke so they run every 12h
regardless of drift. On the drifted path they still validate the freshly
regenerated locks before commit; on the clean path they remain the
ongoing proof that the committed locks + public registries build a
working image — catching buildability regressions independent of
manifest state (a yanked-but-in-range package, a Dockerfile break) that
the check-only gate would otherwise miss.

Co-authored-by: Isaac

* ci(oss): gate each ecosystem's regen on its own drift flag

Per PR review: a single shared `drifted` flag meant a desync in one
ecosystem (say uv.lock) still ran the `rm -f pnpm-lock.yaml &&
pnpm install` from-scratch regen of the other, re-resolving it against
public npm and reintroducing exactly the in-range transitive churn this
job is meant to avoid.

Split into `drifted_uv` / `drifted_pnpm` and gate each Regenerate step
on its own flag. A combined `drifted` (either) still drives the shared
token-mint and open-PR steps; the commit stages only whichever lockfile
actually changed.

Co-authored-by: Isaac
2026-07-31 18:11:47 +08:00
Pat Sukprasert c4f377f027 refactor(migrations): type batch recreation mode (#3696)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 10:10:52 +00:00
Serena Ruan 67fe971769 feat(web): redesign sidebar bulk-selection bar and scope it per section (#3677)
* feat(web): redesign sidebar bulk-selection bar and scope it per section

Rework the sidebar's bulk-selection UI into a single bordered "pill" bar
rendered directly under the header of the section it targets, and give
selection an explicit scope so it acts on the right rows.

- Bar redesign: one pill row with an Exit (X) button, an "N selected"
  count at the session-title font size, and icon-only Archive + Delete
  actions. Archive shows by default and is disabled until an archivable
  session is selected (Delete likewise). Unarchive replaces Archive only
  when the selection is entirely archived.
- Row checkbox moved to the left of the session title.
- Selection scope: the Sessions-header trigger selects the flat session
  list; the Projects-header kebab's "Select sessions" selects the
  sessions nested inside project folders (bar renders under the Projects
  header). Entering a scope preserves current folder expansion. The
  shift-select range and a stranding guard follow the active scope.
- Fold the Projects expand-all/collapse controls plus "Select sessions"
  into a kebab to the right of the New-project (+) button.

Test-only: update unit tests for the new layout/scoping and rewrite the
e2e-ui bulk-actions suite (5 passing) to match the redesign, including a
projects-scope round-trip.

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): resolve projects-scope selection against folders' own rows

Projects-scope bulk selection sourced its action set, shift-select range,
and stranding guard from the global paginated window
(sections.projectGroups), but each ProjectFolder renders from its own
independent useProjectSessions query. A folder member outside the global
window would toggle the count yet silently drop from bulk archive/delete,
break shift-select, or trip the stranding guard.

Each ProjectFolder now reports its rendered rows up via
onConversationsLoaded; the parent unions them (deduped) into a
projectSessionPool that backs the bulk-action bar, the shift-select range,
and the guard — so all three agree on what's selectable regardless of the
global pagination window.

Adds a regression test: with the folder query returning p1,p2,p3 while the
global window holds only p1,p2, shift-select p1->p3 spans all three and
bulk-archive fires with p3 included.

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): surface owned Delete count and guard selection-mode against transient empties

Addresses two non-blocking review notes on the bulk-selection bar:

- Delete acts only on owned rows, so a mixed-ownership selection (reachable
  in projects scope, where a folder can hold others' sessions) read
  "N selected" while Delete hit fewer. The Delete control's label/tooltip
  now shows the owned count ("Delete 2") when it differs from the selection
  size. Archive needs no such hint (its enable-gate already forces a
  uniform archive group, and archived rows never appear in a selectable
  section).
- The stranding guard that exits selection mode when the pool empties now
  skips while the sessions query is refetching, so a background refetch
  that briefly yields an empty page can't kick the user out mid-task.

Adds a mixed-ownership Delete-label test and updates the layout spec's
label assertion.

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* docs(web): clarify projects-scope stranding guard can't exit on a folder refetch

The exit-on-empty guard suppresses the global query's refetch via
conversationsQuery.isFetching, but the projects pool is fed by per-folder
queries too. Note that the pool unions global-derived membership, so a
single folder's transient-empty refetch can't zero it while any member is
in the global window — only a genuinely empty pool exits. Comment-only.

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-31 18:05:52 +08:00
Serena Ruan f7a42b157f ci(blog): add source-PR demo-video table to feature-blog draft PRs (#3698)
The feature-blog workflow leaves a `DEMO REQUIRED` marker in each drafted
post and tells the reviewer to record a demo, with no hint that the source
PRs may already ship one. Collect the contributing PRs per feature and detect
whether each already has a demo video attached (uploaded asset links, bare
.mp4/.mov/.webm/.m4v URLs, or <video> tags — images are not counted), then
inject a PR | Title | Demo video? table into the draft PR body so reviewers
can pull an existing recording into the marker instead of re-recording.

Reuses the gh pr view call already made to pick the reviewer (extended with
title/body/url). The table is written per feature even when no PR has a video.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-31 18:04:21 +08:00
Serena Ruan db7f65437c feat(web): rework workspace rail — tabs, shell-as-tab, maximize, + menu (#3333)
* feat(web): rework workspace rail — tabs, shell-as-tab, maximize, + menu

Reworks the desktop right "Workspace" rail's tab strip so open items and
navigation read as one editor-style set, and gives shells a home inside the
rail instead of taking over the chat column.

- Reorder the strip: open file/shell tabs own the flexible left region; the
  static nav tabs (Files/Agents/Shells/Tasks/Browser) sit right when tabs are
  open, else stay anchored left.
- Shells open as top-strip tabs (desktop): clicking a shell row opens it as a
  closable rail tab whose xterm renders in the rail's content slot — the chat
  page is undisturbed. Mobile keeps the full-screen drawer.
- Add a full-screen (maximize) toggle pinned to the rightmost edge; maximized
  keeps the docked card styling (same inset/height), only the width changes.
- Add a "+" menu ("Open new" → Shell) that trails the last tab when tabs are
  open, else sits by the nav tabs. Browser stays a pinned tab (one embedded
  WebContentsView per conversation).
- Tighten strip spacing and give the nav icons a consistent hover background;
  smaller shell-tab label text.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): keep one ml-auto in the rail strip; drop phantom gap & maximize padding

Follow-up layout fixes to the workspace rail tab strip:

- Only ever one ml-auto in the strip row — two siblings both claiming it split
  the free space and stranded the nav group mid-strip. With open tabs the
  divider owns ml-auto (dragging nav + maximize right together); with no tabs
  the maximize button owns it (nav group stays left).
- The divider dropped its ≥500px container-query gate so it shows at any rail
  width instead of vanishing on a narrow rail.
- FileTabsStrip / TerminalTabsStrip return null when empty — an empty wrapper
  still consumed a slot in the region's gap and left a phantom gap before the
  trailing "+".
- Removed the maximize button's pl-0.5 so it sits flush like the other icons.

Adds regression tests asserting exactly one ml-auto per strip state, the
divider's presence/placement, the no-phantom-gap child count, and no maximize
padding.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): flat tab hover background — opaque fill, no gradient patch

The tab hover used bg-muted, but --muted is a translucent token (6% black).
The close-button overlay then faded in a second translucent gradient on top,
stacking alpha on the right edge into a visible darker patch. Use the same
opaque color-mix selection surface the active tab uses for both the hover
background and the overlay gradient, so hover is a flat even fill.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e): update shell-open tests for rail-tab behavior

Shells now open as tabs in the workspace rail (xterm in the rail content
slot) instead of taking over the main column via MainTerminalView. Update
the three e2e tests that asserted the old main-column flow:

- shells/test_new_shell: assert the shell opens as a rail tab (Close
  "zsh · u-…" x + rail-scoped xterm) with the chat surface undisturbed.
- files/test_right_panel: clicking a shell row opens a "zsh · main" rail
  tab; xterm connects in the rail, chat not replaced.
- sessions/test_terminal_theme: resolve the connected xterm inside the
  Workspace rail rather than main-terminal-view.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

* feat(web): shell-type picker, pinned rail tab strip, sidebar restore

Follow-ups to the workspace-rail rework:

- "+" menu Shell entry: clicking Shell launches the remembered default type
  immediately (selection optional); the submenu check-marks and remembers the
  last-picked type (persisted to localStorage), used as the next default.
- Removed the Shells tab's "+ New shell" row — shell creation now lives solely
  in the "+" menu. The Shells tab is a pure list.
- Hide the Shells tab (and mobile entry) unless a shell actually exists; merely
  declaring shell access no longer surfaces an empty tab.
- Tab strip: nav icons + divider stay pinned left and the "+" stays pinned right
  at every rail width — the tabs region is the sole horizontal scroller, and the
  "+" sits outside it (no scroll/overlap). Divider shows at all widths again.
- Full screen: collapse the left sidebar on enter and restore its prior state on
  exit (collapsed stays collapsed, open reopens).

Updated unit + e2e tests to match (shell-open via the "+" menu; Shells-tab gate).

Co-authored-by: Isaac

* fix(web): keep "+ New shell" in the mobile Shells drawer

Removing the "+ New shell" row broke first-shell creation on mobile, which has
no tab-strip "+" menu. Restore it there only:

- InlineTerminalsSection gains an opt-in ``showNewShell`` prop (default off);
  the desktop rail stays list-only, the mobile drawer passes it to surface the
  create row.
- The mobile Shells menu entry gates on existing-shell OR declared shell access
  (so the drawer is reachable at zero shells), while the desktop rail tab stays
  gated on an existing shell.
- Update the two e2e tests that opened a shell via the removed row to use the
  "+" menu; the mobile drawer test's docstring clarifies the mobile-only create
  path.

Co-authored-by: Isaac

* test(e2e-ui): regenerate visual baselines

* fix(web): restore sidebar on session-switch un-maximize; detangle toggle

Addresses Polly review notes on the full-screen sidebar handling:

- The session-switch reset un-maximizes the rail directly, but didn't restore
  the sidebar it collapsed on entry — so maximize → switch conversation left the
  sidebar silently collapsed. Extract restoreSidebarAfterMaximize() and call it
  from the reset (only when we were maximized).
- Move the sidebar side effect out of the setRightPanelMaximized updater into a
  plain toggleRightPanelMaximized handler, so the state setter stays a pure
  prev→next flip instead of nesting other setters.

Co-authored-by: Isaac

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-31 18:03:17 +08:00
Pat Sukprasert 9afea35772 refactor(openai-agents): type SDK executor boundaries (#3697)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:55:20 +00:00
Pat Sukprasert 88733d7033 refactor(native-server): type transport payloads (#3695)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:51:57 +00:00
Pat Sukprasert 19ef8aad89 refactor(spec): type legacy policy shim boundaries (#3688)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:51:19 +00:00
Pat Sukprasert 4830abc87a refactor(stores): type conversation SQLAlchemy boundaries (#3694)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:44:44 +00:00
Pat Sukprasert b4d2caf7c7 refactor(copilot): type SDK session boundaries (#3689)
* refactor(copilot): type SDK session boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* style(copilot): use pass in session protocol

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:40:39 +00:00
Pat Sukprasert b757f5568d refactor(policies): type registry metadata (#3687)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:24:46 +00:00
Pat Sukprasert e469bf87f2 [models] Discover inner Pi gateway models live (#3629)
* refactor(pi): discover inner gateway models live

Replace the seven-model Databricks registry embedded in the inner Pi executor with the workspace's Unity Catalog model-service listing.

Enrich live entries with MLflow context and output limits when available, while retaining the selected-model registration path so catalog outages do not prevent a configured session from launching.

Expose normalized max-output metadata, cover live routing and offline behavior, and ratchet all seven Pi entries out of the hardcode baseline.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(pi): normalize selected catalog aliases

Rewrite a live Unity Catalog alias to the exact configured Pi launch selector before rendering models.json. This keeps the menu deduplicated without dropping the concrete id Pi must resolve at startup.

Also document why explicit selections bypass picker compatibility filtering, remove a stale static-list reference, and make scalar metadata precedence explicit. Preserve live token metadata in the alias regression test.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:13:34 +00:00
Pat Sukprasert e6a47234fb refactor(policies): type safety policy boundaries (#3686)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:07:32 +00:00
Pat Sukprasert 0085e7a319 refactor(harnesses): type plugin registry boundaries (#3683)
* refactor(harnesses): type plugin registry boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: make spawn builder protocol explicit

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:06:23 +00:00
Pat Sukprasert e89014f17c refactor: type cursor executor boundaries (#3682)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:54:39 +00:00
Pat Sukprasert e0abb0d52d refactor(policies): type async callable contracts (#3680)
* refactor(policies): type async callable contracts

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: make policy protocol stubs explicit

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:54:21 +00:00
Pat Sukprasert 0d070d5ac6 refactor(claude-sdk): type executor boundaries (#3681)
* refactor(claude-sdk): type executor boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix: use concrete Claude session default

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:52:29 +00:00
Tomu Hirata 13ed59ff35 fix(sessions): suppress recovery turn when server forwards message after init (#3488)
The first message is silently ignored (sandbox/lakebox wake) or
double-processed (managed relaunch) because of a race between the
server's persist-before-forward invariant and the runner's
crash-recovery turn detection.

When the server calls session-init (POST /runner/v1/sessions) immediately
before forwarding a message — managed sandbox wakes, sub-agent binding
repairs, host relaunches — the runner loads history during create_session.
Since the server already persisted the message to DB (invariant I1), the
runner sees it as a pending user message and starts a crash-recovery turn.
The subsequent message forward then arrives to an occupied _active_turns,
gets buffered, and is processed a second time once the recovery turn
finishes.

Add suppress_recovery_turn to the session-init envelope. The server sets
it True whenever it calls session-init as part of the message-forward
flow, so the runner skips recovery-turn detection and the forward is the
sole trigger for the turn.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-31 17:49:49 +09:00
dependabot[bot] 07f486eba1 chore(deps): bump quinn-proto (#3306)
Bumps the sidecar-security group with 1 update in the /tests/codex_parity/sidecar directory: [quinn-proto](https://github.com/quinn-rs/quinn).


Updates `quinn-proto` from 0.11.14 to 0.11.16
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.14...quinn-proto-0.11.16)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.16
  dependency-type: indirect
  dependency-group: sidecar-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 08:48:45 +00:00
dependabot[bot] d5702ad837 chore(deps-dev): bump postcss from 8.5.15 to 8.5.18 (#3385)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.15 to 8.5.18.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.18)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.18
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 08:48:25 +00:00
Pat Sukprasert 6bd47251bc refactor(cursor): type native session boundaries (#3684)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:43:02 +00:00
Pat Sukprasert e56b2b347f refactor(policies): type cost usage contracts (#3679)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:31:23 +00:00
Pat Sukprasert 7bdaf78c10 refactor(policies): type evaluator boundaries (#3674)
* refactor(policies): type evaluator boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(policies): make protocol stubs explicit

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:28:24 +00:00
Pat Sukprasert bb710e2deb refactor(auth): type device grant write results (#3676)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:17:19 +00:00
Pat Sukprasert 6464c67db3 [models] Remove release-specific runtime examples (#3630)
* docs(models): remove stale runtime model examples

Describe Bedrock inference profiles, routing policy inputs, and child-session overrides in provider-neutral terms instead of recommending release-specific model ids in runtime help.

Ratchet the five corresponding hardcode-baseline entries and document that concrete examples belong in tests or provider-owned documentation, where they cannot become stale runtime guidance.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs(models): retain Bedrock id shape guidance

Keep the setup prompt provider-neutral while showing the non-obvious inference-profile identifier shape. The hint uses placeholders instead of a release-specific model id, so it remains useful without becoming stale or expanding the hardcode baseline.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* Document provider-neutral model id shapes

Restore useful model-format guidance with synthetic, non-release examples in Bedrock setup, routing policy, and child-session help. Keep concrete release ids out of runtime text so examples teach syntax without becoming stale recommendations.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 15:15:55 +07:00
Pat Sukprasert da027e1768 refactor(qwen): type ACP wire boundaries (#3678)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:10:04 +00:00
Pat Sukprasert 2ccaef8117 refactor(acp): type executor wire boundaries (#3675)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:08:05 +00:00
Pat Sukprasert c6cd36cad2 Filter Codex picker to compatible OpenAI models (#3668)
* fix codex launch model compatibility filtering

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(codex): tolerate model discovery failures

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:06:39 +00:00
Yuan Tang d221a5b8fc feat(policies): add destructive operation gating to GitHub policy (#3622)
Add an `allow_destructive` parameter (default `False`) to the GitHub
policy that separately gates irreversible destructive operations
(deletes). Normal writes (create, update, push) are still governed
by `write_repos` / `write_branches`; destructive operations require
BOTH being in `write_repos` AND `allow_destructive=True`.

Destructive operations gated:
- MCP: delete_file, delete_branch, delete_release
- Shell git: git push --delete, git push origin :branch
- Shell gh: delete actions across 13 groups (repo, release, issue,
  gist, cache, codespace, project, variable, ssh-key, gpg-key,
  secret, label, run)

For MCP, the destructive check fires after the repo allowlist so a
destructive op on a non-allowed repo still gets the repo DENY. For
shell ops, the destructive DENY fires early since even an
undeterminable-repo destructive op should be DENY not ASK.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-31 08:05:01 +00:00
Tomu Hirata 9ba584fa8e fix(sessions): reject undeclared sub_agent_name at create (#3526) (#3662)
* fix(sessions): reject undeclared sub_agent_name at create (#3526)

POST /v1/sessions persisted an arbitrary `sub_agent_name` with no check
that the parent's spec declares it. Every downstream site that swaps in
the resolved child spec is guarded by `if ... is not None` with no
`else`, so a name that resolves to nothing left the parent spec, workdir,
harness and instructions in place — silently booting the child as a full
clone of the parent (runaway recursion for an orchestrator), with nothing
logged and nothing failing.

Fail loud at the create route: `_require_declared_subagent` loads the
trusted parent bundle and rejects a name the spec does not declare with
404, before any row is persisted. This mirrors normal `sys_session_send`
dispatch and the AGENTSPEC.md contract that unlisted names are rejected.
The check only fires when the bundle loads and the name is positively
absent; a load failure or absent cache cannot prove the negative and is
left to fail-loud downstream.

Defense-in-depth: the four runner spec-swap sites now log a warning on a
resolve-miss (`_warn_unresolved_sub_agent`) so stale rows or post-create
bundle edits that still reach the fallback are diagnosable instead of
invisible.

Test: test_subagent_create_rejects_undeclared_name asserts the create
route 404s on an undeclared name.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* test: declare sub-agents in tests that create children (#3526)

The new create-time gate rejects a `sub_agent_name` the parent spec
doesn't declare, which broke existing tests that spawned children of a
sub-agent-less parent:

- test_sessions_endpoints.py: two external-status tests created a
  `worker` child of the default (no-sub-agent) agent. `create_test_agent`
  now takes `sub_agents`; both declare `worker`. `build_agent_bundle`
  gives each bundled sub-agent a default `claude-sdk` harness (the strict
  spec_version:1 parser requires one for an omnigent executor).
- e2e_ui/conftest.py: the `hello_world` fixture now declares a
  `researcher` sub-agent inline, so the mobile-workflow and
  subagent-tab-title fixtures can spawn a `researcher` child.

Full tests/server/integration/ suite passes (995 passed, 3 xfailed).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-31 16:47:03 +09:00
Serena Ruan 9d4695b792 docs(changelog): record v0.5.1 and v0.6.0, drop stale Unreleased (#3673)
The changelog on main skipped from v0.5.0 to v0.7.0, missing both
released tags. Backfill v0.6.0 and v0.5.1 in version order, and remove
the orphaned [Unreleased] block (its two entries — the Nord theme #2561
and per-harness command overrides #2933 — are already covered by the
v0.6.0 section).

v0.6.0 entries are cleaned from the auto-drafted PR #2960: dropped
non-entries (placeholder "written by Isaac" lines, "DELETE THIS SECTION"
markers, N/A refactor/cleanup notes), de-duplicated entries already
recorded under v0.5.0 (#1835, #2371), and normalized doubled tag
prefixes. v0.5.1 is from PR #2395.

Supersedes and closes #1843, #1897, #2395, #2960.

Co-authored-by: Isaac
2026-07-31 15:43:34 +08:00
Pat Sukprasert 3bf29f677c refactor(pi): type executor JSON boundaries (#3671)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:38:00 +00:00
Tomu Hirata ae65962a90 fix(runner): evaluate PHASE_TOOL_CALL policy for sys_call_async background tasks (#3347)
* fix(runner): evaluate PHASE_TOOL_CALL policy for sys_call_async background tasks

Out-of-turn sys_call_async dispatches run in a detached asyncio task after
the originating turn ends. The executor adapter's _stable_policy_evaluator
reads _current_ctx which is cleared to None by run_turn's finally block, so
PHASE_TOOL_CALL evaluations always fail closed to DENY regardless of the
configured policy.

Fix by evaluating PHASE_TOOL_CALL directly via the AP server's REST endpoint
before executing the background tool. This bypasses the SSE round-trip (which
requires a live turn stream) and instead calls POST /sessions/{id}/policies/evaluate
inline from _bg(). ASK is treated as DENY since there is no active turn to
surface an approval prompt.

Sessions without a server_client or conversation_id (e.g. tests) skip
evaluation, preserving existing behavior.

Fixes #3233.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runner): pass arguments as dict in async PHASE_TOOL_CALL evaluation

The initial commit sent target_args (a JSON-encoded string) as the
arguments field. Every other PHASE_TOOL_CALL evaluation path sends a
dict, and the server's policy context builder + built-in safety policies
(e.g. argument-aware rules that inspect arguments.command) expect a dict.
Sending a string caused isinstance(args, dict) checks to fail silently,
so argument-scoped DENY/ASK policies couldn't inspect the async tool's
arguments.

Parse target_args into a dict before building the evaluation body, with
a fallback to {} for malformed input. Add a test assertion that verifies
the forwarded arguments are a dict with the correct contents.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* docs(runner): clarify ASK parking behavior in async policy evaluator docstring

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-31 16:33:14 +09:00
Pat Sukprasert 9cc5cbe41b refactor(codex): type native input boundaries (#3669)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:31:03 +00:00
Pat Sukprasert d9ed713321 refactor(runtime): type harness server config (#3666)
* refactor(runtime): type harness server config

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs(runtime): colocate server config rationale

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:18:13 +00:00
Pat Sukprasert 046adb52b9 refactor(server): type runner tunnel route (#3667)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:11:11 +00:00
Tomu Hirata 72d3b38bbd fix(claude-sdk): stop claiming live message queue support (#3484)
* fix(claude-sdk): stop claiming live message queue support

ClaudeSDKExecutor.enqueue_session_message() called query() which queues
a new turn on the SDK's stdin rather than injecting into the active turn.
Returning True from this method caused the adapter to emit
injection.consumed, dropping the runner's buffered copy.  The next user
message would then trigger a turn with an empty buffer, answering the
previous message — producing a permanent one-turn-behind desync.

Fix: return False from both enqueue_session_message and
supports_live_message_queue.  The adapter's existing if-not-accepted
branch retains the message and delivers it as a normal continuation turn
once the active turn ends, preserving in-order delivery.

Closes #3472.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(lint): suppress ARG002 for unused-but-required override params

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(claude-sdk): send all batched steered messages, not just the last

When a user steers multiple messages during a running SDK turn, each is
buffered and the runner collapses them into one continuation turn whose
history ends in several consecutive user messages. On a resumed SDK
session _build_prompt called _extract_latest_user_content, which walks
history in reverse and returns only the FIRST user message it finds — so
the SDK saw just the last steered message and the earlier ones were
silently dropped (they remained in the transcript, making it look like
the second message was "ignored").

Add _extract_trailing_user_content: on resume, collect the whole trailing
run of consecutive user messages (those after the last assistant/tool
message) and concatenate them (blank-line joined for text; merged content
blocks when any message is multimodal). Prior turns stay SDK-cached and
are not replayed.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-31 16:05:43 +09:00
Pat Sukprasert 277eea7166 refactor(runner): type direct MCP manager (#3665)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:04:24 +00:00
Pat Sukprasert 3e774b8686 [models] Remove legacy onboarding wizard (#3626)
* fix(models): resolve supervisor wizard defaults

Replace the legacy multi-agent supervisor wizard's OpenAI and Databricks model pins with provider-catalog suggestions while preserving the free-form model prompt.

Unknown custom endpoints now receive no unrelated vendor default and require an explicit model. Add endpoint-specific coverage and remove both wizard entries from the hardcoded-model baseline.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(onboarding): require a supervisor model

Reject empty supervisor model input before generating an openai-agents spec. Custom endpoints must now provide an explicit model, and known providers fall back to operator input if their catalog has no default.

Keep the user on the model-selection step with a clear validation message and cover both custom-endpoint and empty-catalog retries.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs(onboarding): map supervisor provider branches

Document how the helper's profile, default OpenAI, and custom-endpoint states correspond to the wizard menu. This makes the explicit-input fallback clear when future endpoint choices are added.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:02:20 +00:00
Pat Sukprasert a619e25185 refactor(runner): type resource registry contracts (#3663)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:02:07 +00:00
Pat Sukprasert 795de18ad3 refactor(auth): type OIDC route boundaries (#3664)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 06:56:51 +00:00
Pat Sukprasert c7b02146b4 refactor(runner): type transport wire payloads (#3659)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 06:54:35 +00:00
Ross Sclafani cfea6c1926 fix(tests): keep the untracked-cache worker out of unrelated tests (#3018)
#2976 moved git untracked-cache setup off the runner startup path into a
daemon thread. The worker now shells out to git at an arbitrary moment, so
it can land inside a test that has swapped the process-global
subprocess.run and be recorded as one of that test's own calls.

That is how it failed CI on an unrelated PR: the databricks login test
asserts on the argv it captured and instead saw a stray
`config core.untrackedCache true`.

Stub GitFilesystemRegistry.start for the suite by default, with an
untracked_cache_start fixture for the worker's own tests, and harden the
login recorder so foreign argv reaches the real runner rather than the
capture list.

Signed-off-by: Ross Sclafani <rsclafani@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 06:29:56 +00:00
Pat Sukprasert 36b6515cfd docs(harness): mark Phase 1 complete + record estimate-vs-actual retrospective (#3658)
Phase 1 of the modular native-harness registry refactor landed (10 PRs,
2026-07-28 → 07-31). Bring the design doc in line with what actually shipped:

- Status header, Phase 1 subtotal, effort summary, and bottom line updated from
  forward-looking ('1.1–1.3 in review') to Phase 1 complete / Phase 2 next.
- Ledger: 1.8 (#3648) landed; 1.4 marked descoped (with rationale); the 1.7
  opencode-e2e follow-up (#3656) recorded; per-PR merge dates added.
- Calibration rewritten as a Phase 1 retrospective: estimate (~20–29 eng-days)
  vs. actual (10 PRs / 4 calendar days), the real cost centers (test-shape churn
  + review-caught behavior bugs, enumerated per PR), the correct runner re-scope,
  the two intentional behavior deltas (qwen label, antigravity relay), and the
  recurring uv.lock / full-suite-only-flake operational friction.

Doc-only; no code change.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-31 13:24:41 +07:00
Pat Sukprasert 74066a71a4 [models] Enforce owned static fallback boundary (#3647)
* feat(models): discover Cursor picker models from CLI

Replace the generated Cursor base-model catalog with live cursor-agent models discovery on the bound runner. Normalize compound effort variants and legacy dotted Claude spellings into the base-id namespace used by launch, /model switching, and terminal mirroring.

Route the discovered options through the existing session model-options cache, return retryable failures without blocking Cursor launch, and expose the same live listing to model-catalog callers. Remove the obsolete generator and eleven Cursor hardcode baseline entries.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): preserve valid Cursor picker options

Carry forward round-trip protection without restoring the stale generated denylist. Skip and log reversed Claude ids that the Cursor command parser cannot inject, while retaining CLI-advertised models verified against the current agent.

Keep cached picker options visible during asynchronous refreshes so model or effort changes do not transiently blank Cursor's picker. Cover both the parser guard and refresh behavior with regression tests.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): verify exact Cursor picker matches

Resolve the selected model's display name from the same live CLI catalog that supplies the Web picker, then refuse to press Enter unless Cursor highlights that exact row. This prevents fuzzy matching from silently selecting another model without restoring release-specific denylist entries.

Keep default/current tags unique in catalog order and limit stale-option retention to Cursor refreshes, preserving the existing drop-on-refresh behavior for Codex and other runner-backed pickers.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(models): centralize owned static fallbacks

Move the remaining Claude and Codex release-curated aliases into one fallback module. Record the responsible adapter, catalog provenance, and the concrete discovery gap for every registered fallback.

Carry that metadata through ModelListing and expose it in sys_list_models payloads, while preserving existing model ordering and provider behavior. Move—not expand—the nine lint baseline entries so future fallback edits remain explicit.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(cursor): reuse live model metadata when switching

Cache Cursor model display names when the runner serves the live picker catalog and pass the selected label into the TUI bridge. This avoids spawning cursor-agent models a second time for the same selection while retaining live discovery as a cold-cache fallback.

Also require the highlighted picker row to match the complete normalized display label or one of its suffixed variants, preventing similarly prefixed models from being accepted. Clear the cache with the existing session and agent lifecycle caches and cover cached, cold-cache, and fuzzy-match behavior.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(models): key fallbacks by provider constants

Use the canonical subscription and CLI-config kind constants as static fallback registry keys. This removes a silent coupling where renaming a provider kind could otherwise turn an owned fallback into an empty listing.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* lint(models): enforce owned fallback boundary

Allow unavoidable static model aliases only when AST analysis proves they are confined to complete StaticModelFallback records in the central model_fallbacks module. Require literal owner, provenance, and discovery-gap metadata, and reject fallback tuples reused outside those records.

Remove the nine centralized fallback rows from the count-based baseline while retaining the temporary baseline for independent migrations that have not landed yet. Add focused positive and bypass-resistance tests and document the structural exception.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(models): scan the owned fallback registry

Run the structural hardcode scanner against the production model_fallbacks module, proving the real stacked records satisfy the owned fallback boundary without count-based allowances.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(models): require the fallback registry

Make the production-registry lint assertion fail if model_fallbacks.py is missing instead of passing vacuously. Clarify that only module-level literal tuples qualify for the structural exemption so nested aliases intentionally fail closed.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* Update Codex fallback aliases

Replace the stale Codex fallback entries with the current GPT 5.6 Sol, Luna, and Terra aliases. Keep the change scoped to Codex; no Gemini fallback is introduced.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 06:24:13 +00:00
Pat Sukprasert e662555092 feat(web): select Codex model before launch (#3556)
* feat(web): select Codex model before launch

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix codex databricks default model label

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 06:21:56 +00:00
Pat Sukprasert c947b655cc refactor(codex): type app-server boundaries (#3655)
* refactor(codex): type app-server boundaries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(codex): preserve empty hook results

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 06:04:55 +00:00
Andrew Peltekci 28ed2fe68e fix(deploy): wire the project store into the Docker entrypoint (#3400)
Creating a project against a container-deployed server failed with 405.
create_app mounts the projects router only when a project store is wired,
and the Docker entrypoint built every other store but never this one — so
POST /v1/projects was not a route at all and fell through to the SPA
catch-all (GET-only), which answers 405. The CLI server path already wires
it, so the same build worked under `omnigent server start` and failed in
the container.

Construct SqlAlchemyProjectStore from the resolved database URL and pass it
to create_app, mirroring the other stores.

Signed-off-by: apeltekci <andrew@peltekci.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 06:03:18 +00:00
Pat Sukprasert 0a06151fc8 refactor(runner): type tool schema boundaries (#3657)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 06:00:39 +00:00
Pat Sukprasert 9a3c90594f refactor(harness): fork_history + shell-tool capability axes; derive gating (PR 1.8) (#3648)
Final Phase-1 PR of the modular native-harness registry refactor: move
registry-parallel enumerations onto HarnessCapabilities.

- Add a fork_history axis (ForkHistory enum: none/rebuild/preamble) to
  HarnessCapabilities, declared per harness in _BUILTIN_CAPABILITIES. Derive the
  server's two fork-history gating frozensets in _sessions/common.py from it
  instead of hand-listing. The derivation emits each canonical id plus its
  reversed native-<key> spelling, because native-claude/native-codex/native-cursor
  are valid ids canonicalize_harness passes through unchanged and the read sites
  match on the canonicalized id (guarded by the existing reversed-spelling fork
  test) — so the derived sets are a superset of the prior literals.
- Add optional shell_tool_name / shell_tool_prompt fields carrying the harness
  bench's shell-tool provocation; delete the bench's hardcoded
  _NATIVE_TOOL_PROVOCATION table and read the fields off capabilities in
  native_vendor() (byte-identical (tool_name, prompt) per harness).
- Delete the dead _HARNESS_MODULES literal in runtime/harnesses/__init__.py
  (~120 lines, overwritten unconditionally by harness_modules() next line).
- Extend the drift-guard tests in test_harness_capabilities.py.

Scope kept tight to the doc's mandate: sets that would need new NativeCodingAgent
identity fields (_ANTIGRAVITY_FAMILY_HARNESSES, _PROVIDER_RESOLUTION_HARNESS,
*_NATIVE_TERMINAL_ROLE) are left as-is; noted as follow-ups.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-31 06:00:17 +00:00
Pat Sukprasert 892c401dd8 [models] Centralize owned static fallbacks (#3632)
* feat(models): discover Cursor picker models from CLI

Replace the generated Cursor base-model catalog with live cursor-agent models discovery on the bound runner. Normalize compound effort variants and legacy dotted Claude spellings into the base-id namespace used by launch, /model switching, and terminal mirroring.

Route the discovered options through the existing session model-options cache, return retryable failures without blocking Cursor launch, and expose the same live listing to model-catalog callers. Remove the obsolete generator and eleven Cursor hardcode baseline entries.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): preserve valid Cursor picker options

Carry forward round-trip protection without restoring the stale generated denylist. Skip and log reversed Claude ids that the Cursor command parser cannot inject, while retaining CLI-advertised models verified against the current agent.

Keep cached picker options visible during asynchronous refreshes so model or effort changes do not transiently blank Cursor's picker. Cover both the parser guard and refresh behavior with regression tests.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): verify exact Cursor picker matches

Resolve the selected model's display name from the same live CLI catalog that supplies the Web picker, then refuse to press Enter unless Cursor highlights that exact row. This prevents fuzzy matching from silently selecting another model without restoring release-specific denylist entries.

Keep default/current tags unique in catalog order and limit stale-option retention to Cursor refreshes, preserving the existing drop-on-refresh behavior for Codex and other runner-backed pickers.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(models): centralize owned static fallbacks

Move the remaining Claude and Codex release-curated aliases into one fallback module. Record the responsible adapter, catalog provenance, and the concrete discovery gap for every registered fallback.

Carry that metadata through ModelListing and expose it in sys_list_models payloads, while preserving existing model ordering and provider behavior. Move—not expand—the nine lint baseline entries so future fallback edits remain explicit.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(cursor): reuse live model metadata when switching

Cache Cursor model display names when the runner serves the live picker catalog and pass the selected label into the TUI bridge. This avoids spawning cursor-agent models a second time for the same selection while retaining live discovery as a cold-cache fallback.

Also require the highlighted picker row to match the complete normalized display label or one of its suffixed variants, preventing similarly prefixed models from being accepted. Clear the cache with the existing session and agent lifecycle caches and cover cached, cold-cache, and fuzzy-match behavior.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(models): key fallbacks by provider constants

Use the canonical subscription and CLI-config kind constants as static fallback registry keys. This removes a silent coupling where renaming a provider kind could otherwise turn an owned fallback into an empty listing.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* Update Codex fallback aliases

Replace the stale Codex fallback entries with the current GPT 5.6 Sol, Luna, and Terra aliases. Keep the change scoped to Codex; no Gemini fallback is introduced.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 05:59:18 +00:00
Pat Sukprasert 02ed62dce1 test(e2e): fold opencode host e2e onto shared agent-name constant (#3656)
Follow-up to #3599 (PR 1.7). That PR moved the built-in native agent-name
constants into a shared public block in omnigent/native_coding_agents.py and
migrated the claude/codex host e2e tests onto them, but missed the opencode
sibling: test_host_opencode_native_e2e.py still defined a local
_OPENCODE_NATIVE_AGENT_NAME = "opencode-native-ui" literal and asserted a stale
'_ensure_default_opencode_agent did not run' message (that per-harness seeder
was collapsed into _ensure_default_native_agents).

Import the shared OPENCODE_NATIVE_AGENT_NAME constant and update the message so
all three host e2e tests are consistent. Test-only; opt-in e2e (skipped without
OMNIGENT_E2E_OPENCODE_NATIVE=1).

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-31 12:39:39 +07:00
Pat Sukprasert 957ac0789e refactor(harness): registry-driven server seeding loop (PR 1.7) (#3599)
* refactor(harness): registry-driven server seeding loop (PR 1.7)

Collapse the server's built-in native-agent seeding onto the
NativeHarnessProvider seam. The 11 hand-written _ensure_default_<x>_agent
helpers + their 11 _build_<x>_native_bundle partners become two
registry-driven functions in omnigent/server/app.py:

- _build_native_bundle(provider): resolves provider.materialize_agent_spec via
  the seam and runs the shared materialize -> bundle -> tar dance. The
  per-harness `model` arg variance (codex required kw / kiro,opencode default /
  the rest none) is bridged by one inspect.signature check.
- _ensure_default_native_agents(...): loops NATIVE_CODING_AGENTS, resolving the
  provider by key and seeding each content-aware via _ensure_builtin_agent.

debby / polly / _ensure_extra_builtin_agents stay hand-written. Removed the now
-dead _<X>_NATIVE_AGENT_NAME constants and the *_NATIVE_CODING_AGENT imports.
Net server/app.py -455/+146.

Redeploy safety: builtin_agent_id(name) is a pure hash of the agent name, and
the names (NativeCodingAgent.agent_name) and bundle bytes are unchanged, so
seeded ids and bundles stay byte-identical (verified: sha256 of
_build_native_bundle output matches the pre-loop named builders across all
model-arg variants). New tests freeze the 11 expected ids and assert the loop
covers every native agent. Updated test_builtin_bundles / test_app to the
generic builder; fixed stale symbol refs in two e2e tests and a scheduled-tasks
integration test.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test(server): cover the registry-driven native seeding paths (PR 1.7)

The seeding-loop collapse removed ~330 lines that were only exercised
transitively by e2e suites; add direct unit coverage so the new generic path is
fully covered and the coverage gate recovers:

- Parametrize the native bundle-builder tests over EVERY native agent (was a
  4-agent sample), so each harness's _materialize_* + bundle path is covered
  directly, across both model-arg shapes.
- Cover the two defensive guards in _build_native_bundle /
  _ensure_default_native_agents (missing materialize hook, missing provider row).
- Add an end-to-end seed test asserting all 11 native agents register under
  their stable builtin_agent_id with a retrievable bundle.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* docs(server): note the model-axis limit of the native seed signature bridge

Address Polly non-blocking note: the inspect.signature bridge in
_build_native_bundle understands only the `model` kwarg; a future harness
whose materializer needs a different required kwarg fails loud at seed time
rather than routing. Comment so the next author knows.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* refactor(harness): aggregate built-in native agent-name constants (PR 1.7)

The seeding-loop collapse deleted the 11 private _<X>_NATIVE_AGENT_NAME
constants from server/app.py (the loop uses agent.agent_name directly), which
pushed callers that need one specific built-in onto magic-string literals
("claude-native-ui", "qwen-native-ui", ...) in the tests.

Restore them as PUBLIC constants in omnigent/native_coding_agents.py — the
module that already indexes the registry rows — so seeding and tests share one
named, registry-derived source of truth instead of re-deriving the literal:

- Add CLAUDE_NATIVE_AGENT_NAME ... KIMI_NATIVE_AGENT_NAME (each = the row's
  agent_name) to native_coding_agents.
- Point the server + scheduled-tasks tests at the shared constants (drop the
  bare "qwen-native-ui" / "antigravity-native-ui" / "claude-native-ui" strings).
- Fold the two host e2e tests' own local _CLAUDE/_CODEX_NATIVE_AGENT_NAME
  literals onto the shared constants too.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-31 05:30:08 +00:00
Daniel Lok 164c4d3422 perf(conversation-store): speed up list_items DB query (#3638)
Two localized optimizations to SqlAlchemyConversationStore.list_items,
which backs GET /v1/sessions/{id}/items (the web chat transcript read).

- Scope the after/before cursor subqueries to conversation_id so they
  land on the (workspace_id, conversation_id, id) primary key as point
  lookups. Without it, (workspace_id, id) leads no index and each
  paginated page degraded to a workspace-wide scan.
- load_only the seven columns _to_item reads, dropping the wide
  search_text Text column that this read path never touches. On
  Postgres search_text is TOAST-ed, so omitting it skips a detoast and
  roughly halves the bytes pulled per row on a chatty conversation.

Scoping the cursor to the conversation also fixes a latent correctness
edge: a cursor id from another conversation previously resolved its
position workspace-wide and applied it as a cutoff; it now yields an
empty page, guarded by a new test.

Co-authored-by: Isaac
2026-07-31 13:29:44 +08:00
Pat Sukprasert a75a54679b refactor(opencode): type client wire payloads (#3650)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 05:16:02 +00:00
Pat Sukprasert 4aae769560 refactor: type model catalog boundaries (#3652)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 05:09:11 +00:00
Pat Sukprasert f3355fad46 refactor: type accounts auth boundaries (#3653)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 05:06:42 +00:00
hari 9dd538af2e fix(cli-auth): write the session-JWT file privately instead of chmod-ing it after (#3441)
* fix(cli-auth): write the session-JWT file privately instead of chmod-ing it after

_store_entry's docstring already promised the file is written "with user-only
read/write permissions (0o600) - the file may hold session JWTs, which are
sensitive". The implementation did not deliver that:

    path.parent.mkdir(parents=True, exist_ok=True)
    ...
    path.write_text(json.dumps(data, indent=2))
    os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)

write_text creates a missing file at the process umask, so on the very first
login - exactly when a session JWT is first persisted - the token sat on disk
readable by every local user until the chmod landed. Measured:

    dir  mode after mkdir     : 0o755
    file mode after write_text: 0o644   <- JWT is on disk at this mode
    file mode after chmod     : 0o600

The parent ~/.omnigent was also left world-traversable, and clear_token
rewrote the same file with no chmod of its own, relying on the mode of a file
it may not have created.

Routes both writers through _write_tokens_file, mirroring the pattern already
used in claude_native_bridge._atomic_write_user_json: a tempfile beside the
target (created owner-only by tempfile before any bytes are written), fsync,
chmod, then os.replace. The directory is created 0o700.

The rename also fixes a robustness bug: write_text truncated in place, so a
write that failed partway left a truncated file, and the JSONDecodeError
handler in _store_entry treats that as {} - silently discarding every stored
token for every server. The temp is discarded on failure and the previous file
is left intact.

Tests: tests/test_cli_auth_token_file_mode.py. Three of the eight fail on the
previous code (the on-disk window, the directory mode, and token loss on a
failed write); the rest pin the final mode, round-tripping, trailing-slash
normalisation and selective clearing.

Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>

* style: satisfy ruff format

Pre-commit's ruff-format hook flagged the skipif decorator in the new test
module; it fits on one line.

Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>

* refactor(cli-auth): hoist state-dir hardening into _write_tokens_file

Move the 0o700 mkdir + chmod from _store_entry into _write_tokens_file
so every writer routes through it. Previously only _store_entry
hardened the directory, so a clear_token-only interaction left a
pre-existing world-traversable (0o755) ~/.omnigent untightened. Adds a
regression test pinning that clear_token now hardens the dir.

Co-authored-by: Isaac

---------

Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 05:04:37 +00:00
Pat Sukprasert e16056b2f0 [models] Discover Cursor picker models from CLI (#3624)
* feat(models): discover Cursor picker models from CLI

Replace the generated Cursor base-model catalog with live cursor-agent models discovery on the bound runner. Normalize compound effort variants and legacy dotted Claude spellings into the base-id namespace used by launch, /model switching, and terminal mirroring.

Route the discovered options through the existing session model-options cache, return retryable failures without blocking Cursor launch, and expose the same live listing to model-catalog callers. Remove the obsolete generator and eleven Cursor hardcode baseline entries.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): preserve valid Cursor picker options

Carry forward round-trip protection without restoring the stale generated denylist. Skip and log reversed Claude ids that the Cursor command parser cannot inject, while retaining CLI-advertised models verified against the current agent.

Keep cached picker options visible during asynchronous refreshes so model or effort changes do not transiently blank Cursor's picker. Cover both the parser guard and refresh behavior with regression tests.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): verify exact Cursor picker matches

Resolve the selected model's display name from the same live CLI catalog that supplies the Web picker, then refuse to press Enter unless Cursor highlights that exact row. This prevents fuzzy matching from silently selecting another model without restoring release-specific denylist entries.

Keep default/current tags unique in catalog order and limit stale-option retention to Cursor refreshes, preserving the existing drop-on-refresh behavior for Codex and other runner-backed pickers.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(cursor): reuse live model metadata when switching

Cache Cursor model display names when the runner serves the live picker catalog and pass the selected label into the TUI bridge. This avoids spawning cursor-agent models a second time for the same selection while retaining live discovery as a cold-cache fallback.

Also require the highlighted picker row to match the complete normalized display label or one of its suffixed variants, preventing similarly prefixed models from being accepted. Clear the cache with the existing session and agent lifecycle caches and cover cached, cold-cache, and fuzzy-match behavior.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(cursor): drop unused parser binding

Keep the model-option setdefault call for deduplication without assigning its return value before the later result loop. This addresses the code-quality finding without changing parser behavior.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(cursor): handle missing CLI during model switch

Catch click.ClickException while refreshing a cold Cursor model catalog so a missing cursor-agent executable becomes the existing handled RuntimeError instead of escaping the runner endpoint as a 500.

Add bridge-level regression coverage for the preserved exception cause. The focused Cursor/native-event suite passes 122 tests and full pre-commit passes.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 12:02:08 +07:00
Pat Sukprasert 0bc1cbe992 refactor(opencode): type runtime boundaries (#3651)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:53:51 +00:00
Tomu Hirata ef8761ba2b feat(telemetry): emit PolicyRegisteredEvent and PolicyDeletedEvent (#3637)
* feat(telemetry): emit PolicyRegisteredEvent and PolicyDeletedEvent

Add two new telemetry events that fire on policy create/delete for
both session-level and admin-level policies:

- PolicyRegisteredEvent: fired after a successful POST to
  /v1/sessions/{id}/policies or /v1/policies. Records handler,
  policy_type, scope ("session" or "admin"), session_id, and
  anon_user_id so we can see which handlers are being registered and
  at what scope.

- PolicyDeletedEvent: fired after a successful DELETE. Looks up the
  existing policy first so the handler is available; silently skips
  emission when the policy was already absent (idempotent delete).

Both events follow the existing try/except BLE001 fire-and-forget
pattern used by SessionStoppedEvent and SessionDeletedEvent.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(policy-store): return deleted Policy from delete/delete_default

Previously delete() and delete_default() returned bool, causing a
second PK lookup in the route layer to retrieve the handler before
emitting telemetry. Changing the return type to Policy | None
eliminates that extra round-trip: the store already loads the row to
perform the delete, so we can return the entity at no additional cost.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(telemetry): drop handler from PolicyDeletedEvent, revert store changes

handler required a pre-fetch before delete to avoid an extra DB
round-trip, which meant changing the store layer. Dropping the field
keeps PolicyDeletedEvent simple and the store interface unchanged.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-31 13:47:54 +09:00
Pat Sukprasert a85a059bf9 refactor(workspace): type filesystem payloads (#3640)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:40:39 +00:00
Pat Sukprasert 4db5551f83 refactor(opencode): type forwarder events (#3649)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:40:13 +00:00
Pat Sukprasert 2525bdff8f refactor(pi): type native provider config (#3645)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:37:05 +00:00
Pat Sukprasert d6051e6a1a refactor(host): type frame payloads (#3646)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:35:44 +00:00
Pat Sukprasert 48e6623245 refactor(pi): type native resume records (#3643)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:27:16 +00:00
Pat Sukprasert 7bd2069e09 refactor(config): type harness startup overrides (#3642)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 04:24:32 +00:00
Pat Sukprasert 1ab69347c1 refactor(policies): type dynamic policy boundaries (#3639)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 03:43:17 +00:00
Pat Sukprasert 1725c2e9d4 refactor(llms): close package typing gaps (#3636)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 03:42:13 +00:00
Pat Sukprasert ca4007b19d refactor(telemetry): type config and wire records (#3635)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 02:57:39 +00:00
Pat Sukprasert bb6086ce92 refactor(python): type remaining call boundaries (#3634)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 02:43:16 +00:00
Pat Sukprasert e366a2bb6b refactor(cli): type late-bound helper proxies (#3633)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 02:38:23 +00:00
Pat Sukprasert f3d28c8e71 fix(host): guard orphan reaper against missing os.WNOHANG on Windows (#3627)
The host orphan reaper's waitpid fallback uses os.WNOHANG and
os.waitpid(-1, ...), neither of which exists/works on native Windows.
Windows also has no child reparenting to a subreaper, so there is
nothing to reap. The periodic sweep swallowed the resulting
AttributeError, but the final drain in run()'s finally block runs
unguarded and would crash shutdown.

Return early with 0 when os.WNOHANG is absent, matching the reaper's
own "non-Linux is a no-op" contract.

Co-authored-by: Isaac
2026-07-31 02:11:38 +00:00
Pat Sukprasert c6c874b927 fix(llms): select Anthropic thinking mode from capabilities (#3529)
* fix(anthropic): select thinking mode from model capabilities

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(anthropic): harden model metadata caching

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* docs(anthropic): clarify cache partition HMAC

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(anthropic): strengthen cache key derivation

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(anthropic): bound metadata lookup latency

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* perf(anthropic): avoid blocking cache partitioning

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(anthropic): surface metadata fallback risk

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 09:01:30 +07:00
Pat Sukprasert 795ee49db5 ci(web): enforce high-signal lint baseline (#3628)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:59:58 +00:00
Pat Sukprasert 95f9e89a3b refactor(harness): DI-seam runner interrupt/stop — collapse 16 handlers (PR 1.6) (#3568)
* refactor(harness): DI-seam runner interrupt/stop — collapse 16 handlers (PR 1.6)

Route the runner's native interrupt / stop dispatch through a
dependency-injected NativeInterruptRunner instead of 16 per-harness closures
plus two hardcoded `if _harness == "<x>-native"` chains in the /events handler.
Mirrors the CodexGoalRunner DI precedent (omnigent/runner/codex/goal.py):
app-scope state (AP client, resource registry, event publisher, sub-agent wake
plumbing, codex bridge-state resolver) is injected at construction, typed via
Protocol.

- New omnigent/runner/native/interrupt.py: the 9 uniform interrupt and 7
  uniform stop handlers collapse to two descriptor-driven methods
  (_UNIFORM_INTERRUPT / _UNIFORM_STOP); claude interrupt (bridge-id) and codex
  interrupt (MCP-startup + turn/interrupt) keep dedicated methods, moved
  verbatim. interrupt()/stop() return None for handler-less harnesses so the
  caller falls through to the in-process cancel.
- app.py: the two dispatch chains become one runner.interrupt()/.stop() call +
  fall-through; the 16 closures are deleted (net app.py -470). Local
  `from omnigent.<x>_native_bridge import` stays at call time so bridge-module
  monkeypatches keep resolving (no test repoints).
- 12 new unit tests for NativeInterruptRunner.
- Doc: add 1.6 ledger row (gap-fill deferred); flip stale 1.5c row to landed.

Scope: migration-only, behavior-preserving. The antigravity/opencode coverage
gap (no interrupt/stop handler; they fall through to _cancel_inprocess_turn) is
left unchanged and pinned by a no-handler test; wiring agy interrupt_turn() /
opencode client.abort() is a deferred follow-up.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* docs(runner): fix uniform interrupt/stop harness counts in interrupt.py

Address Polly non-blocking doc nit: the module comments said 'nine uniform
interrupt' and 'seven uniform stop', but _UNIFORM_INTERRUPT has seven entries
and _UNIFORM_STOP six (claude/codex interrupt and claude stop are special-cased;
codex/pi alias stop to interrupt). Clarify uniform-vs-total counts. Doc-only.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-31 01:56:46 +00:00
Pat Sukprasert fcdadc6fc8 refactor(web): standardize object type definitions (#3618)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:37:19 +00:00
Pat Sukprasert 1e07ebc2ef refactor(web): mark file hook as type-only (#3623)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:36:52 +00:00
Pat Sukprasert 71ac4dc59a fix(web): throw structured bulk mutation errors (#3573)
* fix(web): throw structured bulk mutation errors

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(web): narrow bulk mutation errors

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:33:34 +00:00
Pat Sukprasert 64a51170fd test(web): standardize array type syntax (#3621)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:32:32 +00:00
Pat Sukprasert 4d50c2b3a1 refactor(web): standardize production array types (#3619)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:29:42 +00:00
Pat Sukprasert a114a34c96 refactor(web): use function property signatures (#3617)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:22:30 +00:00
Pat Sukprasert 2980774c86 [models] Route Pi wire APIs from catalog metadata (#3572)
* refactor(pi): route wire APIs from catalog metadata

Replace Pi's release-specific GPT Chat Completions allowlist with normalized Unity Catalog model-service wire metadata shared by native and inner Pi execution.

Thread generic-provider wire configuration through the harness, resolve dedicated AI Gateway URLs back to their workspace API origin, and avoid probing non-Databricks providers. When discovery is unavailable, route unknown GPT models to Responses while retaining the documented system-model compatibility fallback.

Cover Chat, Responses, dedicated-gateway, generic-provider, alias, outage-cache, and Responses-only catalog behavior. Verified 275 focused Pi/catalog tests, isolated runtime spawn-env tests, live production UC metadata, and repository-wide pre-commit.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(pi): hoist model routing imports

Move the catalog, gateway, subprocess, and compatibility imports used by Pi routing to module scope so dependencies are explicit and consistently initialized.

Extract the shared Pi model compatibility predicates into a small leaf module to avoid introducing a model_catalog/pi_native_credentials import cycle. Update tests to patch the module-bound credential resolver.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:07:12 +07:00
Khoi Nguyen 8f6f232982 feature/bugfix(agy): Add agy (Google Antigravity CLI) as a 7th polly sub-agent + fix four native-harness defects (supersedes #2992) (#3499)
* Add agy (Google Antigravity CLI) as a 7th polly sub-agent

polly's roster now includes agy alongside claude_code, codex, opencode,
cursor, hermes, and pi. agy drives the antigravity-native harness
(Gemini-native, own Google account auth via ~/.gemini; does not run
Claude/GPT-family models) and follows the same
IMPLEMENT/REVIEW/EXPLORE contract as the other worktree-scoped
implementers, with gate_pushes: false so it can open its own PRs.

Updates the roster count, preflight check, trigger phrases, and
cross-vendor review/cancellation lists in config.yaml; the
investigate/fanout/cross-review skills' vendor lists; and the
structural e2e test assertions (roster tuple, harness family map,
policy-argument count) to match.

Signed-off-by: Imraul Emmaka <ikemmaka@ualr.edu>

* fix(antigravity-native): re-deliver turns agy rejects while verifying the account

agy's TUI composer mounts ~3s after launch, but its account-eligibility
check is not settled until ~7-9s. A turn submitted inside that window is
consumed by agy — the draft leaves the composer, so the submit verifies —
and answered with "We're finishing verifying your account eligibility"
instead of starting a cascade. Nothing retried, so the turn was silently
lost and the terminal sat idle.

Detect the notice after a submit and re-deliver until agy takes the turn,
bounded by 90s. The running-turn marker is checked first so a notice still
rendered from a prior attempt can never re-send a turn that already landed,
and the probe fails open so a future agy that renames its running footer
keeps delivering rather than retrying.

Programmatic first turns — a polly sub-agent dispatch — land in that window
on every launch; interactive users usually type slowly enough to miss it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>

* fix(antigravity-native): treat agy's collapsed-paste placeholder as a rendered draft

agy replaces a paste carrying many line breaks with a single
`[Pasted text #N +M lines]` row instead of echoing the text into the
composer. The threshold is line-count based (~13+ line breaks); total
length does not matter, so a long single-line message still renders
verbatim while a multi-line one never does.

The render gate looks for the message's needle in the composer, which a
collapsed paste can never contain, so delivery raised "agy did not render
the pasted message in its input box before submit" while the draft was in
fact sitting there. Sub-agent task prompts are exactly this shape, so a
polly dispatch failed on its first turn every time; the single-line
follow-up prompts it sent next happened to render verbatim and worked,
which made it look like a startup race.

Recognise the placeholder as draft content in _draft_in_input_region so
both the render gate and the submit verification key off it appearing and
then leaving the composer — the submit stays verified rather than blind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>

* fix(antigravity-native): bind the TUI injector to an explicit bridge dir

The interaction bridge's default TUI injector resolved the bridge directory
from HARNESS_ANTIGRAVITY_NATIVE_BRIDGE_DIR, on the assumption (stated in its
docstring) that "the reader/CLI both run with it set". That is stale: the
reader now runs as a task INSIDE the runner process, which never carries that
variable — it is set only for the harness subprocess by
build_antigravity_native_spawn_env.

So every web approval failed with "HARNESS_ANTIGRAVITY_NATIVE_BRIDGE_DIR is
required" — 100% of the time, not intermittently. The RPC delivery flipped
agy's backend step, but agy's own permission prompt was never dismissed, so
the terminal did not advance and the next typed turn risked landing in the
stale prompt's buffer.

Add tui_injector_for(bridge_dir) and have the reader — which is handed its
bridge_dir — use it. _inject_via_tui stays for callers that genuinely run
with the harness env, with its constraint now spelled out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>

* fix(antigravity-native): close a turn agy reports finished (quiescence backstop)

Turn completion was inferred purely by pattern-matching step types.
_is_turn_close_step has already accreted three special cases — clean text
close, ERROR planner, degenerate DONE — and its own docstring explains that
missing one leaves turn_active stuck True forever: the spinner never clears
and the NEXT turn cannot re-open RUNNING either. Every agy step type it does
not know about is a permanently stranded session, and that list only grows.

agy already publishes the answer. Every GetAllCascadeTrajectories summary
carries a per-cascade CASCADE_RUN_STATUS, which appeared in this codebase
exactly once — in a docstring example — and was never read, even though the
rotation detector already fetches those summaries on every scan.

Use it as a BACKSTOP: when agy reports the bound cascade idle on two
consecutive scans while Omnigent still believes a turn is open, close it. The
step-based close stays the fast path; this only catches what it missed. Being
reconciliation rather than edge detection, it is idempotent and self-healing —
a missed, unknown, or reordered step now costs one detector interval instead
of stranding the session.

Verified against agy 1.1.8 that the status reports RUNNING both while working
and for the entire time a permission gate is parked (75s observed), so the
backstop cannot close a turn that is waiting on a human. Two consecutive ticks
are required so the gap between delivering a turn and agy starting it is not
mistaken for the end of one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>

* fix(antigravity-native): avoid duplicate verification retries

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Imraul Emmaka <ikemmaka@ualr.edu>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Imraul Emmaka <ikemmaka@ualr.edu>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:06:54 +00:00
Pat Sukprasert cd66b027ed test(web): use explicit module type imports (#3615)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:06:09 +07:00
Pat Sukprasert e7d07b2fb9 style(web): separate imports from module setup (#3616)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 08:05:47 +07:00
Pat Sukprasert 62547f7447 refactor(web): remove type-only import side effects (#3614)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:02:59 +00:00
Pat Sukprasert 7ac3f5aa4c refactor(web): consolidate duplicate imports (#3613)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:02:31 +00:00
Pat Sukprasert 3f829a45d8 refactor(web): infer default parameter types (#3612)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 01:01:19 +00:00
Pat Sukprasert 5de2d1c846 refactor(web): standardize generic constructors (#3611)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:58:28 +00:00
Pat Sukprasert d1d03e5406 refactor(electron): modernize updater property checks (#3610)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:57:43 +00:00
Pat Sukprasert 460f3ca1e9 refactor(electron): document swallowed detach races (#3608)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:49:48 +00:00
Pat Sukprasert 53d17044bc refactor(web): break terminal hook import cycle (#3607)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:46:50 +00:00
Pat Sukprasert c353ec0036 refactor(web): avoid dynamic pending stash deletion (#3606)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:46:13 +00:00
Pat Sukprasert 86d7890451 refactor(web): remove dynamic object deletions (#3605)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:42:08 +00:00
Pat Sukprasert 9f898a4aa6 test(web): type sidebar project session fixtures (#3604)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:40:43 +00:00
Pat Sukprasert e9c6432a11 refactor(web): separate ignored stream event cases (#3603)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:39:27 +00:00
Pat Sukprasert b2f2f5bd90 refactor(web): avoid reassigning node view parameter (#3602)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:35:39 +00:00
Pat Sukprasert 22ffc5fc05 refactor(web): split websocket handler cleanup (#3609)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 07:34:02 +07:00
Pat Sukprasert aa0d79d78d refactor(web): remove redundant React child handling (#3601)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:33:35 +00:00
Pat Sukprasert 9c226367a0 ci(web): enforce cleaned lint rules (#3600)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-31 00:28:07 +00:00
Sabhya Chhabria 6935fce648 Add force override for chat imports (#3576)
* Add force override for chat imports

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

* Fix CI checks for import force

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

---------

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
2026-07-30 16:07:44 -07:00
Yuan Tang 18fcf67d7c feat(web): add zoom controls to subagent graph panel (#3583)
* feat(web): add zoom controls to subagent graph panel

Add zoom in/out and fit-to-view buttons to the subagent graph panel
using ReactFlow's useReactFlow hook. Widen the zoom range from
0.3–1.5x to 0.1–3x so users can zoom in closer to read small nodes
or zoom out further for large graphs.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* style(web): fix prettier formatting for zoom control buttons

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-30 13:35:18 -07:00
Yuan Tang 6ff2620fbc fix(openshell): pass workspace to SandboxClient lifecycle methods (#3524)
* fix(openshell): pass workspace to SandboxClient lifecycle methods

The openshell SDK >=0.0.86 added a required `workspace` keyword argument
to `SandboxClient.create()`, `get()`, `delete()`, and `wait_ready()`.
Omnigent never passed it, so `sandbox create --provider openshell`
crashed with `TypeError: SandboxClient.create() missing 1 required
keyword-only argument: 'workspace'`.

Thread a workspace through _OpenShellClient and OpenShellSandboxLauncher,
resolved from: explicit constructor arg (YAML `sandbox.openshell.workspace`),
then `$OMNIGENT_OPENSHELL_WORKSPACE` env var, then "default".

Fixes #3513

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix: bump openshell floor to >=0.0.88 and close test gaps

The `workspace` kwarg landed in openshell 0.0.88, not 0.0.86 — 0.0.86
still has the old signature and would crash with `got an unexpected
keyword argument 'workspace'`. Bump the floor accordingly.

Also record the workspace reaching the fake SDK and assert it in both
the _OpenShellClient and managed_hosts tests.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* chore: strip index-dependent size fields from uv.lock

pypi.org's index serves wheel/sdist sizes while proxy indexes may not,
so re-locks were flipping ~2,900 'size = N' lines back and forth. The
sizeless form is canonical on main; this keeps the diff to the real
dependency changes.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-30 20:00:56 +00:00
Corey Zumar 2f10ed0747 fix(web): keep the session config gear usable while the session is asleep (#3584)
* fix(web): keep the session config gear usable while the session is asleep

The gear required liveness === "online", so an asleep session couldn't
change model/effort even though PATCH /v1/sessions persists overrides
and the next wake applies them. Gate the gear like the composer (inert
only for read-only viewers and unreachable sessions) and make the
native model catalog survive runner death so the picker stays filled:

- relay exit / refresh_state with no runner now mark the per-session
  catalog stale instead of deleting it; snapshots keep serving it
- a stale catalog is re-fetched in the background once a live runner
  is bound again, and replaced on success
- an asleep claude-native session with a cold cache (server restart)
  refills from its host over the host tunnel - the same pre-launch
  source the new-session picker uses

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(e2e): scope the mermaid preview assertion to the diagram svg

The rendered Streamdown mermaid block carries chrome icon svgs (zoom /
copy controls) next to the diagram, so the strict single-svg locator
fails with "resolved to 3 elements" on every run since #3498 merged.
Target the diagram svg via mermaid's aria-roledescription stamp, which
also makes the assertion check the diagram itself rather than any svg.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-30 11:32:55 -07:00
hari b5462b5487 chore(inner): collapse declared_passthrough duplication, document deny-by-default env (#3564)
Follow-up to the non-blocking review notes on #3479.

- codex_executor consumes agent_env.declared_passthrough instead of keeping
  its own copy. It already imports agent_env, so the reason the duplicate
  existed no longer applies. Test repointed at the shared helper.
- POLICIES.md now explains that agent CLIs get a deny-by-default environment
  and what env_passthrough is for. The migration note only ever lived in a PR
  description, so the two cases that bite -- a generic ACP agent with no vendor
  family, and a goose authenticated by an ambient provider key rather than
  gateway routing -- were undocumented.

Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
2026-07-30 10:50:44 -07:00
Dhruv Gupta 6b3ff8af35 chore(lint): strip wheel/sdist size fields in the uv.lock normalizer (#3579)
pypi.org's simple index serves a size for every file while proxy
indexes may not, so each re-lock added or stripped 'size = N' across
~2,900 lines depending on which index resolved it. Make the sizeless
form canonical (the hash is the integrity check): the fixer now drops
size fields and --check flags them, so re-locks from either side
converge on one form.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-30 10:48:01 -07:00
Yuan Tang 240634a947 fix(web): make subagent graph view nodes clickable (#3395)
ReactFlow's pan-on-drag behavior was intercepting pointer events on
graph nodes, preventing the existing <Link> wrapper from navigating.
Adding the `nopan nodrag` utility classes tells ReactFlow to leave
those events alone so clicks reach the router link.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-30 17:01:49 +00:00
Corey Zumar 84310be6cf fix(web): fork dialog presents worktree sessions as repo + worktree and validates the directory before cloning (#3521)
* fix(web): fork dialog presents worktree sessions as repo + worktree and validates the directory before cloning

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): address review — query-param-safe URL join and accurate 404 message in checkHostDirectory

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): label the base-branch input in the fork dialog

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* refactor(web): drop the fork dialog's base-branch input, auto-base new worktrees on the source branch

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): cover worktree-source fork prefill, directory pre-flight, and bind wire shape

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): match the slim session snapshot URL in the worktree fork stub

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): don't press Escape in the fork dialog (it closes the Radix dialog)

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): accept bare session ids in the fork navigation assertion

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-30 10:01:28 -07:00
FromTheRain eda909fd81 feat(kubernetes): add secret_mounts to the managed-sandbox provider (#3280)
Mirror the pvc_mounts config knob for Kubernetes Secrets: project a
pre-created Secret as a read-only file volume on the runner's host
container. A Secret volume (no subPath) is refreshed in place by the
kubelet, so a long-lived runner picks up a rotated credential without a
restart — unlike envFrom, which is frozen at container start.

- server: parse/validate sandbox.kubernetes.secret_mounts at config load
  (DNS-1123 name, absolute/normalized/non-reserved path, intra-list and
  pvc<->secret path-collision checks), failing loud at startup
- onboarding: add the secret volume + host-container-only volumeMount in
  build_pod_manifest (optional=False, defaultMode 0440), threaded through
  the launcher
- tests mirror the pvc_mounts coverage

Signed-off-by: bdchatham <bdchatham@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 16:34:52 +00:00
Pat Sukprasert 249f1eb6a8 docs(deploy): recommend HTTP/2 proxy to avoid multi-window UI stalls (#3555)
Each open session in the web UI holds a long-lived event-stream HTTP
response. Over HTTP/1.1 browsers cap concurrent connections at ~6 per
origin, so opening several windows/tabs against a raw :8000 deploy fills
the pool with held-open streams and every other request stalls — the UI
appears frozen across all windows while the server is idle.

The bundled Caddy overlay and every managed platform already terminate
TLS with HTTP/2, which multiplexes the streams and dissolves the cap;
the gap was only that nothing told operators this proxy is also the fix.
Document it in the deploy README ("Serving") and point to it from the
Caddyfile. Docs-only; no server behavior change.

Co-authored-by: Isaac
2026-07-30 16:08:31 +00:00
Pat Sukprasert c889a07894 refactor(web): remove redundant JSX fragments (#3575)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 16:08:04 +00:00
Pat Sukprasert 882d87a477 refactor(electron): simplify fallback window lookup (#3574)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 16:03:29 +00:00
Pat Sukprasert e46667cc52 refactor(web): avoid Promise executor return values (#3571)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 15:48:31 +00:00
Thomas Garnier e957f1b762 feat(sandbox): make recursive dotfile hiding opt-in and add explicit mask_paths (#3519)
* feat(sandbox): make recursive dotfile hiding opt-in, add mask_paths

The sandbox hid every dotfile under the working directory by walking the
whole tree. On medium-to-large projects that walk is slow and routinely
trips the entry cap, and it masks far more than the secrets it targets.

Make the recursive scan opt-in and add a way to hide specific paths:

- cwd_hidden_scan_recursive (default false) scans only the top level of
  the cwd and each read_paths root (including $HOME when it is a granted
  read path). The top-level dotfiles that hold most secrets (.git, .env,
  .aws, .ssh, ...) are still masked, but the walker no longer descends the
  whole tree. Set it true for untrusted trees where a deeply nested
  credential file would be an unacceptable leak.
- mask_paths hides a named file or folder regardless of a leading dot,
  resolved like read_paths (~ expanded, relative to cwd, no $VAR). Files
  are masked as an empty file, folders as an empty view, on top of the
  dotfile mask in every mode.

Both backends enforce the new fields: linux_bwrap binds /dev/null for
files and a tmpfs for folders; darwin_seatbelt emits literal/subpath deny
rules. Behavior change: with the non-recursive default, dotfiles nested
below the first level are now readable unless recursion is turned on.

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>

* docs(sandbox): note is_dir symlink behavior for mask_paths

Clarify that the explicit mask_paths classification uses is_dir(), which
follows symlinks — unlike the dotfile walker's follow_symlinks=False — and
that seatbelt emits a harmless literal deny for a missing entry where bwrap
drops it on the re-stat.

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>

---------

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
2026-07-30 08:08:18 -07:00
Daniel Lok a315dd4779 perf(web): render conversations before the full history window loads (#3228)
* perf(web): render conversations before the full history window loads

Opening /c/<id> blocked first paint on fetchInitialHistoryWindow, which
pages backward (up to MAX_INITIAL_PAGES serial round-trips) until the last
two user prompts are on screen. On a real deployment each page is ~1s, so a
long tool-heavy last turn could stall the transcript for several seconds.

Fetch only the first page in the blocking bind, render immediately, then
page the rest of the window in the background behind a top-of-history
spinner. The previous-prompt heuristic is unchanged — just no longer on the
critical path.

- Extract the window-complete boundary into initialWindowComplete() and
  reuse it in both fetchInitialHistoryWindow and the new backfill.
- bindStream fetches one page; backfillInitialWindow continues the same
  paging loop after commit, holding loadingMoreHistory so scroll-up/rail
  loaders don't double-fetch, generation-guarded like loadMoreHistory.
- New loadingInitialWindow flag drives a "Loading earlier messages…"
  spinner above the oldest bubble.

Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

*  perf(web): Unify initial history loading

- Build the prompt-boundary and viewport-fill window through one post-render loader
- Make the turn rail lazy and remove its eager 200-item history fetch

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

*  test(web): Cover lazy history loading

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* perf(web): pin the latest turn to the top with a trailing spacer

Add a LatestTurnSpacer as the last child of the message flow that pins the
newest turn's anchor to the top of the viewport (the newest real user prompt,
or the newest assistant text output when a page deep in a tool chain has no
prompt yet), letting the reply grow below it — the ChatGPT/Claude "question at
top" feel.

As a side effect the spacer keeps the transcript taller than its scroll
container whenever content sits above the anchor, so older history stays
reachable by scroll-up. That makes HistoryAutoLoader's viewport-fill fetch loop
redundant: it now pages only to the previous-prompt boundary (still capped by
initialWindowComplete), and the resize-driven re-fill and spinner-height
measurement are removed.

Spacer height = clientHeight − (anchor→content-bottom) − top gap, clamped to
≥ 0: it shrinks as the reply streams (its own top is fixed by the content
above, not by its height, so scrollHeight stays constant and stick-to-bottom
keeps the anchor pinned) and collapses to 0 once the reply exceeds the
viewport, restoring normal bottom-following.

Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* test(e2e-ui): regenerate visual baselines

* fix(web): keep loading history near the top

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* test(e2e-ui): regenerate visual baselines

* fix(web): preload history sooner near the top

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* refactor(web): show history skeleton for every page

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* fix(web): stabilize scroll during history prepends

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* style(web): loosen history skeleton spacing

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* style(web): use compact history loading indicator

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* fix(web): avoid latest turn spacer flicker

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* fix(web): observe initial history scroll adjustment

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* fix(web): bind history loading to live scroller

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>

* 🐛 fix(web): freeze spacer to loaded turn

---------

Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-30 21:19:11 +08:00
Pat Sukprasert e3508f0d34 refactor(web): remove dead initial assignments (#3554)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 12:12:33 +00:00
Anthony Ivan 36f2bb02e1 feat(web): render Mermaid in markdown previews (#3498)
* feat(web): render Mermaid in markdown previews

Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>

* fix(web): harden Mermaid markdown preview

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 19:09:52 +07:00
Pat Sukprasert 558973157c fix(web): keep user bubble hooks unconditional (#3553)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 12:09:00 +00:00
Pat Sukprasert 9617f8ade4 ci(web): reject TypeScript lint warnings (#3552)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 12:03:31 +00:00
Pat Sukprasert ca9c3a9a44 refactor(models): resolve context windows from catalogs (#3551)
Replace the stale exact Qwen context-window registry with metadata from the shared MLflow provider catalog. Keep only the self-describing Anthropic [1m] marker and the conservative 128K offline fallback.

Reuse the onboarding catalog cache for both context sizing and pricing, preserve cache pricing fields in ModelInfo, and support provider-qualified ids, OpenRouter vendor namespaces, and Databricks aliases without release-specific model mappings.

Ratchet the hardcoded-model baseline and document the migration behavior. Cover exact, family, namespace, ambiguity, cache, encoded-metadata, and offline resolution paths.

Tests: 59 focused provider/context-window tests; 110 model-catalog, compaction, and session-override tests; changed-file pre-commit; repository-wide pre-commit except the pre-existing stale routing_pb2.py binding; live MLflow lookup smoke test.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:54:32 +00:00
Pat Sukprasert a5e48d3a39 refactor(harness): registry-driven terminal-ensure — collapse 11 attach arms (PR 1.5c) (#3543)
Collapse the terminal-ensure / attach path in create_session_terminal —
11 hardcoded `if terminal_name == "<x>" and session_key == "main"` arms —
behind a single generic `_ensure_native_terminal(...)` shell dispatched
through the NativeHarnessProvider seam. The attach-path sibling of the 1.5b
launch shell (#3500/#3501); reuses the `_launch_<x>` adapters and
NativeLaunchContext. codex/antigravity supply an ownership predicate; codex
supplies a `finalize` for its one-shot policy notice — both run under the
per-session ensure lock, matching the inline arms.

- New shell in runner/native/orchestration.py (view-based existence check,
  returns JSONResponse: 200 / 500 / 409), exported from runner/native.
- app.py: 11 arms (~450 lines) -> one collect-then-dispatch block.
- Repoint the HTTP attach-path claude/codex auto_create monkeypatch targets
  to the orchestration module (the seam resolves the adapter there).
- 8 new unit tests for the shell.
- Doc: add 1.5c ledger row; flip stale 1.5b-i/ii rows to landed.

Behavior-preserving: qwen error label -> "Qwen Code" (display_name, as 1.5b-i);
antigravity now wires ensure_comment_relay via the base ctx (the landed
_launch_antigravity adapter already passed it).

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-30 18:49:06 +07:00
Pat Sukprasert fd04bd99c6 fix(web): remove dangling underscore names (#3549)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:42:06 +00:00
Pat Sukprasert 494c85e2d0 fix(web): resolve await-in-loop warnings (#3548)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:35:56 +00:00
Pat Sukprasert d7446ebcbe refactor(models): route wire compatibility from catalogs (#3547)
Normalize Databricks Unity Catalog supported_api_types into the provider-neutral ModelWireAPI vocabulary and retain those facts while converting runner catalogs into the id-only routing-client shape.

Replace the exact Pi model exclusion table with a catalog-backed Claude wire check. Pi now keeps Responses-capable GPT models on its supported Responses path, while endpoints explicitly lacking Anthropic Messages are redirected to claude-sdk. Missing metadata from older runners remains unknown and does not trigger a redirect.

Ratchet six retired hardcode allowances and update the migration plan.

Tests: 104 catalog and smart-routing tests; 7 Pi Responses/provider tests; changed-file pre-commit suite. The repository-wide pre-commit run passed every relevant hook and only reported the pre-existing stale routing_pb2.py baseline.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 18:30:57 +07:00
Pat Sukprasert fa72205b8c fix(web): clear one-off correctness warnings (#3546)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:25:30 +00:00
Pat Sukprasert 818104c4f0 fix(web): stabilize React render inputs (#3545)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:23:09 +00:00
Pat Sukprasert eda564bedb docs(models): delegate Kimi example default (#3457)
Remove the release-specific model from the Kimi launcher example so an unoverridden session uses the default already configured in the Kimi CLI.

Document the ownership boundary, assert that the spawn environment omits HARNESS_KIMI_MODEL when no model is declared, and ratchet the retired lint allowance.

Tests: 12 Kimi spawn-environment tests; structural example load; staged pre-commit including YAML and hardcoded-model checks.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:20:28 +00:00
Pat Sukprasert 78013d4daa fix(web): use stable React list keys (#3544)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:14:07 +00:00
Pat Sukprasert 90d70e6875 fix(web): clear no-shadow lint warnings (#3540)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:10:14 +00:00
Pat Sukprasert 9fcaefcfb6 [models] Discover onboarding defaults (#3456)
* feat(models): discover ad-hoc CLI default

Resolve the model for minimal harness-less agent YAMLs from the Databricks OpenAI-family catalog during bundle materialization instead of baking a release-specific endpoint into the CLI.

Preserve --model and OMNIGENT_MODEL precedence, and fail with explicit configuration guidance when discovery is unavailable. Ratchet the removed pin from the hardcoded-model allowlist and document the new behavior.

Tests: 111 discovery-disabled CLI tests; staged pre-commit including the hardcoded-model lint.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(models): pin YAML model precedence

Add a direct regression test showing that an executor.model declared in agent YAML remains authoritative over OMNIGENT_MODEL and catalog discovery.

Fail the test if catalog resolution runs, so future changes cannot silently turn an explicit YAML model into a fallback lookup.

Tests: 114 CLI chat tests; targeted pre-commit checks.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* feat(models): discover onboarding defaults

Select provider setup defaults from the live catalog after filtering specialty modalities, using stable family preferences for broadly accessible Anthropic and OpenRouter choices instead of release-specific model pins.

When discovery is unavailable, leave onboarding unpinned so the user supplies an explicit model. Add deterministic catalog fixtures for interactive CLI coverage, ratchet three lint allowances, and document the migration.

Tests: 147 onboarding and configure-models tests; staged pre-commit including the hardcoded-model lint.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): require intended OpenRouter family

Keep OpenRouter onboarding defaults within the catalog's Kimi family. If discovery returns no compatible family member, require the user to enter a gateway model instead of silently selecting a newer proprietary entry.

Correct the setup comments to match Click's prompt behavior: blank input accepts a discovered default, while an unavailable default requires an explicit value.

Tests: 86 provider and resolver tests passed. Targeted pre-commit passed for all modified files.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(models): pin offline runtime failure

Cover Anthropic and OpenAI runtime fallback when neither the agent nor provider config names a model and catalog discovery returns no data. Both paths must fail closed with guidance to configure an explicit model or retry discovery.

Document that removing source pins affects shared runtime defaults in addition to onboarding prompts, and clarify that required-family policy tokens use case-insensitive substring matching.

Tests: 88 focused runtime, provider, and resolver tests passed. A broader 153-test run reached 152 passes plus one unrelated host-credential leak in the existing Claude fallback test. Targeted pre-commit passed.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 11:01:00 +00:00
Pat Sukprasert c6dc8d6e4a fix(web): use named Tiptap imports (#3542)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 10:59:08 +00:00
Pat Sukprasert 1a873f658a fix(web): clean up TypeScript errors (#3538)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 10:32:54 +00:00
Jason Brashear 295715ff3f test: make sandbox-cwd assertions portable across macOS firmlinks (#3517)
* test: make sandbox-cwd assertions portable across macOS firmlinks

_resolve_sandbox_cwd ends in Path.resolve(), and macOS routes the test's
literal paths through firmlinks (/home via the automounter, /tmp ->
/private/tmp), so the literal-string assertions fail on any macOS dev
box while Linux CI stays green. Compare against the same resolution
instead; on Linux both sides are identical strings.

Signed-off-by: webdevtodayjason <jason@webdevtoday.com>

* test: tidy sandbox cwd portability assertions

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: webdevtodayjason <jason@webdevtoday.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 10:23:01 +00:00
Pat Sukprasert b3dbd9ba6e [models] Discover ad-hoc CLI defaults (#3455)
* feat(models): discover ad-hoc CLI default

Resolve the model for minimal harness-less agent YAMLs from the Databricks OpenAI-family catalog during bundle materialization instead of baking a release-specific endpoint into the CLI.

Preserve --model and OMNIGENT_MODEL precedence, and fail with explicit configuration guidance when discovery is unavailable. Ratchet the removed pin from the hardcoded-model allowlist and document the new behavior.

Tests: 111 discovery-disabled CLI tests; staged pre-commit including the hardcoded-model lint.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(models): pin YAML model precedence

Add a direct regression test showing that an executor.model declared in agent YAML remains authoritative over OMNIGENT_MODEL and catalog discovery.

Fail the test if catalog resolution runs, so future changes cannot silently turn an explicit YAML model into a fallback lookup.

Tests: 114 CLI chat tests; targeted pre-commit checks.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(e2e): pin sessions mock model

Give the sessions-default REPL fixture an explicit mock-server model so the test exercises session routing rather than ad-hoc model discovery.\n\nThe E2E workflow intentionally disables catalog lookup. After ad-hoc defaults moved to catalog resolution, the model-less fixture exited before the REPL opened. Other approval fixtures in this file already pin the same mock-compatible model.\n\nTest: OMNIGENT_DISABLE_CATALOG_LOOKUP=1 OMNIGENT_SKIP_WEB_UI=true uv run --frozen pytest -q tests/e2e/test_repl_sessions_approval_e2e.py::test_sessions_default_flag_works --tb=short\nTest: pre-commit run --files tests/e2e/test_repl_sessions_approval_e2e.py

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 16:59:44 +07:00
Pat Sukprasert 01293d6de7 fix(runtime): clarify shared authorship semantics (#3527)
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-30 09:39:20 +00:00
Pat Sukprasert cefed908db refactor(harness): registry-driven native launch — special arms + turn-path, consolidated dispatch (PR 1.5b-ii) (#3501)
Second half of the runner launch seam, completing 1.5b. Routes the 3 special
arms and the turn-path opencode cold-boot through the seam, and consolidates all
11 create-session legs into one dispatch. Behavior-preserving.

- orchestration: extend the shell _launch_native_terminal with pre_launch (an
  async (has_terminal) -> PreLaunchResult callback run inside the lock, so the
  has_terminal-dependent rebuild/transfer/needs checks see the same state the
  inline arms did), build_context (lazy full-context enrichment for claude's
  bundle_dir/agent_name/skills + closures and codex's bundle, run only on
  create), and reraise (turn-path opencode converts a launch failure to a 503
  instead of publishing a start-error event).
- app.py: replace the 11 per-harness create-session legs with a single
  collect-then-dispatch block — each leg only assigns its lock dict, context,
  and optional pre_launch/build_context/resolve_agent_spec, then one
  _launch_native_terminal call runs them. The 3 special arms (claude rebuild+
  transfer, codex needs-check, antigravity payload+transfer) supply their
  has_terminal-gated pre_launch; claude/codex supply build_context (codex keeps
  the outer spec_entry as agent_spec). Turn-path opencode uses reraise=True.
- Preserve terminal_ready: only claude populated it in the create-session
  response, so only claude's dispatch result is captured back (the consolidation
  fixes a regression where 1.5b-ii's first cut dropped it).
- Tests: repoint the app-level _auto_create_<x>_terminal monkeypatches that now
  route through the seam — claude create-session (events_lifecycle 603/688,
  session_resources 2198) and the create-session auto-create guard tests
  (terminals_autocreate: claude + antigravity) — to the orchestration symbol the
  adapter calls. Add shell unit coverage for build_context (enrich-only-on-create)
  and reraise. The terminal-attach/route patches (1.5c path) are untouched.

Net app.py reduction continues; the 11-arm launch chain is gone. Pre-existing
codex gateway-env failures in events_lifecycle are unchanged (codex arm behavior
preserved; those tests are unrelated app-server/gateway artifacts).

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-30 16:36:17 +07:00
Pat Sukprasert 55b9ad0376 ci: enforce TypeScript lint checks (#3504)
* ci: enforce TypeScript lint checks

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* ci: run TypeScript lint through pre-commit

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-30 16:31:25 +07:00
Andrew Demczuk 6e611a687d fix(harnesses): resolve native CLI launch via the readiness ladder, not bare shutil.which (#3341) (#3535)
The seven native resolvers (pi, hermes, kimi, cursor, goose, kiro, qwen) looked up their CLI with a bare shutil.which, while readiness and the SDK executors resolve through resolve_cli_binary's fallback ladder (the nvm/npm/homebrew bin dirs the daemon's frozen PATH omits). A CLI installed only in a ladder dir passes the readiness badge but fails at launch. Route the resolvers through resolve_cli_binary so the badge and the launch agree.

resolve_cli_binary gains a `which` hook so the resolvers keep their existing test seam; the fallback ladder always uses the real filesystem.

Signed-off-by: Andrew Demczuk <andrew.demczuk@gmail.com>
2026-07-30 09:25:37 +00:00
hari 8a65a72627 fix(inner): stop agent-CLI subprocesses inheriting unrelated host secrets (#3479)
* fix(inner): stop agent-CLI subprocesses inheriting unrelated host secrets

Closes #3445.

pi and codex filtered os.environ before spawning their vendor CLI; goose,
kimi, qwen, acp and hermes did not, so every host secret - cloud tokens, other
providers' API keys - reached those processes, sandboxed or not. hermes was
worst: the no-HERMES_HOME branch passed env=None, which inherits everything.

Implements the decision on the issue.

  agent_env.clean_agent_env(allow_prefixes, allow_exact, deny_exact,
                            extra_allowed, source)

The model is not "no credentials ever". It is a shared safe base (HOME, PATH,
proxy, locale, tmp, XDG, the omnigent-session marker), plus the harness's own
config/provider families, plus whatever the spec declared in
os_env.sandbox.env_passthrough.

Per-harness families, matching the table on the issue:

  qwen    QWEN_, OPENAI_, DASHSCOPE_
  goose   GOOSE_
  kimi    KIMI_, MOONSHOT_        (keeps its documented ambient auth)
  acp     none - base + env_passthrough only, the agent is arbitrary
  hermes  HERMES_                 (see below)

pi and codex become thin calls. Their sets are preserved exactly, including
codex's OPENAI_API_KEY deny; verified by diffing the new output against the
original inlined logic over a synthetic environment - identical, with and
without passthrough. USER/LOGNAME/SHELL/TZ stay per-harness rather than
entering the shared base, because pi passes them and codex does not and this
refactor must not widen codex's set.

hermes prefix family: HERMES_ only, and deliberately not DATABRICKS_. Hermes
authenticates from files, not the environment - hermes_native_bridge copies
~/.hermes/auth.json and ~/.hermes/.env into the per-session HERMES_HOME
(hermes_native_bridge.py:386-394). HOME still passes, so nothing breaks, and
the credential family this change exists to contain stays contained.

Also restores the launcher's env-prune defense: the sandboxed paths bake
tuple(env.keys()) into with_spawn_env_allowlist, so a full-environ env made
that allowlist a no-op.

Tests: tests/test_agent_spawn_env_canary.py - parametrized over all seven
harnesses, planting nine credential-family canaries and asserting none
survive, plus that each still gets a usable environment, that a harness sees
its own family and not a sibling's, that kimi keeps ambient KIMI_/MOONSHOT_,
that env_passthrough works as the migration path, and that deny_exact beats a
matching prefix. 21 cases.

Executor suites: 967 passed, 13 skipped, 0 failed.

Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>

* fix(inner): point the spawn-env canary at the real executors

Addresses review on #3479.

- Extract _build_spawn_env() on qwen/goose/acp/hermes, matching kimi's
  existing shape, and parametrize the canary over the real builders with
  secrets planted in a monkeypatched environ. The prefix table was a hand
  copy, so a harness reverting to os.environ.copy() kept the suite green;
  it now fails, which is what the module docstring already claimed.
- Add NODE_EXTRA_CA_CERTS to BASE_ALLOW_EXACT. Node honours it where
  SSL_CERT_FILE is ignored, so without it a corporate-CA user upgrading
  loses TLS on every Node harness without a NODE_ family of its own.
- Warn in acp_executor._ensure_initialized when the handshake fails or the
  child dies first, naming os_env.sandbox.env_passthrough as the likely fix.

Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>

---------

Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
2026-07-30 06:29:36 +00:00
Andrew Reid ee5ddb9659 fix(sessions): validate policy-evaluate payloads per phase instead of failing open (#3418)
The policy evaluate endpoint is a BLOCKING hook: a harness waits on its
allow/deny before running a tool. Its payload rules were applied from a chain
of conditionals, and a rule in a branch only ever reaches whichever phase
lands in that branch. Three rules now come from one per-phase schema, and all
three run for every phase.

What was getting through:

- `event.data` was accepted as an object, a string or absent, then normalized
  with `or {}`. For a tool phase that means the gate evaluated as though the
  caller had sent nothing: every tool-name-scoped policy skipped, and the hook
  answering allow. Tool and LLM phases now require an object; a bare string is
  a legitimate wire form only on the prompt phase, and an absent payload is
  malformed everywhere, since every first-party producer sends one.
- A tool-scoped gate needs a tool name, and only one spelling was accepted.
  Producers differ: claude-native and the in-process tool dispatch send
  `request_data.name`, the OpenCode plugin sends the tool in `event.target`.
  Requiring the first rejected the second with a 400 — and that plugin turns
  any non-2xx into ALLOW, so a stricter guard silently disabled every OpenCode
  TOOL_RESULT policy rather than tightening it. Any declared source now
  satisfies the rule, and the resolved name is written onto the container the
  engine reads, so those policies gate instead of merely passing validation.
- `event.context` must be an object when present. An earlier revision of this
  message described only two rules while the diff carried three.

`event.type` is also checked before being used as a dict key: an unhashable
value raised inside the lookup and surfaced as a 500 rather than a 400.

The three rules above were previously three independent structures (which
wire types are accepted; which phases need an object payload; where a tool
name may come from), each keyed by phase and each read with a permissive
`.get(phase, default)` fallback. The comment on them already said "one schema
per phase" — the code didn't enforce it: a phase added to the first structure
alone was silently accepted, validated as loosely as possible, and given no
tool-name rule at all, because the other two structures simply had no entry
for it and their lookups defaulted rather than erred. They're now one
NamedTuple per wire type with no default values on any field, so a new entry
cannot be added without deciding both properties at once, and the only
`.get()` left is the outer wire-type lookup, which 400s on a miss instead of
falling back to anything.

The test table enumerates each phase and non-object-data vector and
cross-multiplies them, rather than hand-listing every case — kept in sync
with the production schema by hand, since that schema lives inside a
route-registration closure and isn't something a test module can import. It
asserts the structured error code rather than the status alone, and now
includes a non-empty list alongside the empty one: both are simply
non-dict, but hand-listing only the empty list is coincidentally falsy in a
way a narrower, wrong fix (special-casing falsy values) would have passed.
Five mutations kill it: accepting object-or-string-or-absent everywhere,
requiring a single tool-name spelling, dropping the context rule, validating
the alternative spelling without normalizing it (caught because the oracle
asserts a tool-scoped DENY, not a 200), and giving one phase's schema entry a
wrongly permissive `data_must_be_object`.

A pre-existing test's docstring also claimed OpenCode's plugin sends REQUEST
data as a bare string; it now sends `{"text": ...}` like every other
first-party producer. Reworded to describe why the bare-string form is still
accepted (older/third-party compatibility) without attributing it to
OpenCode's current behaviour.

Signed-off-by: Andrew Reid <andrew@reid.ee>
2026-07-29 23:19:20 -07:00
Harry Yao 48a1cb33a9 claude: always inject CLAUDE_CODE_USE_GATEWAY into ucode subprocess (#3483)
Unconditionally set CLAUDE_CODE_USE_GATEWAY=1 in the Databricks ucode
subprocess env and stop setting CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS on
that path. Gateway-aware mode keeps tool search on so MCP schemas load on
demand, so the betas-disable knob is no longer needed here.

Update test_ucode_config_for_profile_reads_allowlisted_claude_state to
expect CLAUDE_CODE_USE_GATEWAY=1 in the ucode env instead of the removed
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS flag.


Co-authored-by: Isaac

Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
Co-authored-by: Bryan Qiu <bryan.qiu@databricks.com>
Co-authored-by: Harry Yao <harry.yao@databricks.com>
2026-07-29 20:37:50 -07:00
Manfred Calvo cc39c4eac1 fix(tunnel): give websocket clients a verifying SSL context (certifi/OS-trust fallback) (#1731)
* fix(tunnel): give host/runner websocket tunnels a verifying SSL context

On interpreters whose OpenSSL default cert path is uninitialized (python.org
macOS framework builds before Install Certificates.command, and
python-build-standalone interpreters used by uv), ssl.create_default_context()
loads zero trust roots, so the host and runner wss:// tunnels failed with
CERTIFICATE_VERIFY_FAILED and looped on reconnect.

Add omnigent/tls.py (resolve_ca_file + cached client_ssl_context) that resolves
a CA bundle OS-trust-store-first with a certifi fallback, and pass that context
to both tunnel websockets.connect calls for wss:// (ws:// stays ssl=None).
egress/ca.py:_system_ca_bundle now shares resolve_ca_file; certifi is promoted
to an explicit dependency.

Closes #1730

Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>

* fix(claude-native): pass a verifying SSL context to wss:// terminal-attach

_websocket_connect opened wss:// terminal-attach connections (the scheme
terminal_attach_url produces from an https workspace base_url) with a bare
default SSL context, so claude-native attach to a remote workspace hit the same
empty-trust-store failure fixed for the tunnels. Route it through
client_ssl_context() for wss:// (ws:// stays ssl=None). Also realign a
ws_tunnel test with the databricks_request_headers rename from main.

Closes #1730

Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>

* chore(deps): record certifi in uv.lock

pyproject.toml promoted certifi to an explicit dependency; add it to the
omnigent package's dependencies and requires-dist in uv.lock so
"uv sync --locked" passes in CI. certifi was already resolved transitively,
so its package entry (with hashes) is unchanged — this only records the
direct dependency edge.

Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>

---------

Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-30 12:32:37 +09:00
Tomu Hirata e6f3ec420b fix(fork): prefetch agent harness so custom agents appear in fork picker (#3523)
Session-discovered agents start with harness=null (filled lazily on hover
via prefetchAvailableAgentDetails). The fork picker filters candidates with
forkTargetCarriesHistory(a.harness), which returns false for null, so
custom agents were silently excluded from the fork agent dropdown even
though they appear fine in the new-session picker.

Fix: call prefetchAvailableAgentDetails for all agents when ForkSessionForm
mounts (same pattern NewChatDialog uses on dropdown open). The helper is a
no-op for agents whose harness is already known, so re-running on agents
list change is safe.

Adds a test that verifies prefetch is called for a session-discovered agent
(harness=null, sessionId set) on mount.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-30 12:29:20 +09:00
Yuan Tang bdd9d5e658 feat(web): group archived sessions by date (#3394)
* feat(web): group archived sessions by date

The archived sessions list in the settings page was a flat
chronological list that became hard to scan. Group sessions under
date headers (Today, Yesterday, Previous 7 days, Previous 30 days,
or month/year for older entries) for easier browsing.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(web): use DST-safe date arithmetic and add grouping tests

Use calendar-based setDate() instead of fixed millisecond offsets for
computing date boundaries in the archived sessions grouping, avoiding
mis-bucketing around DST transitions. Add a Vitest test with a pinned
system clock that verifies all five date group headers render correctly.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(web): share now across grouping, fix test locale/timezone flakiness

- Capture a single `now` in the groupedArchived memo and pass it to
  every dateGroupLabel call, avoiding redundant Date construction and
  a rare date-rollover inconsistency during iteration.
- Use local-time Date constructors in the test so bucket boundaries
  match dateGroupLabel's local-time arithmetic in any timezone.
- Derive the expected month/year label via toLocaleDateString so the
  assertion passes under non-English locales.
- Wrap assertions in try/finally so fake timers are always restored.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-30 02:37:54 +00:00
Yuan Tang eb2d441e66 feat(policy): add detect_loop builtin contextual policy to catch agent retry loops (#3158)
* feat(policy): add detect_loop builtin to catch agent retry loops

The #1 token-waste pattern is an agent retrying the exact same failing
tool call. max_tool_calls_per_session counts total calls but cannot
detect repeated ones. detect_loop tracks recent (tool_name, args_hash)
tuples in session_state and ASKs when the same call repeats N times
within a configurable sliding window, letting the user break the loop.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* address review: use full SHA-256 digest, add e2e tests

- Remove [:16] truncation from _args_hash to use the full 64-char
  hex digest, avoiding false-positive collisions from 64-bit space.
- Add YAML → PolicyEngine e2e tests exercising the full roundtrip:
  repeated calls trigger ASK, diverse calls pass, window eviction
  works, and non-tool_call phases are unaffected.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* address review: guard params, fix docstring, move e2e test

- Clamp window and threshold to minimum 1 so zero/negative values
  cannot cause unbounded state growth or always-ASK behavior.
- Add minimum: 1 constraints to both params in the registry schema.
- Fix docstring to describe actual persisted state shape (list of
  SHA-256 hex digests, not tuples).
- Move e2e test from tests/runtime/policies/ to tests/e2e/ per
  repo convention.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-30 02:32:43 +00:00
Pat Sukprasert 49f9d82219 feat(sessions): Delegate approval authority (#3446)
- Add an owner-controlled approval capability independent of access level
- Allow delegated editors to resolve privileged actions using owner execution identity
- Expose Edit + approve in the sharing dialog with explicit credential warning

Refs #2150

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-30 09:29:22 +07:00
Yuan Tang e6d939491e feat(policies): add detect_thrashing builtin contextual policy (#3160)
* feat(policies): add detect_thrashing builtin context policy

Agents that hit repeated tool errors burn tokens without making
progress.  Add a new builtin contextual policy that tracks
tool-result outcomes in a rolling window and fires when the agent
appears stuck — either via consecutive errors or a high error rate
within the window.

Two independent triggers (both configurable, both independently
disableable):
- consecutive_threshold (default 5): fires after N straight errors
- window_error_rate (default 0.8): fires when ≥80% of the last
  N results (window, default 10) are errors

Error detection is heuristic (common prefixes like "Error:",
"Traceback", "Permission denied", "fatal:", and JSON {"error": ...}
payloads).  No server LLM required, unlike detect_task_switch.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(policies): address review feedback for detect_thrashing

- Fix docstring: "exceeds" → "reaches or exceeds" to match the >= check
- Rename misleading test names (test_below_consecutive_threshold_allows
  was actually at-threshold; test_window_rate_allows_below_threshold was
  at-threshold)
- Retain max(window, consecutive_threshold) history entries so the
  consecutive check still works when window < consecutive_threshold
- Rate check now computes over the last `window` entries (not the full
  retained history), and reports window size in the reason message
- Add integration test exercising state accumulation across evaluate
  calls through the real policy engine

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(policies): harden detect_thrashing against edge cases

- Validate session_state history as list[int] before use; reset to
  empty on corruption instead of raising TypeError.
- Guard against window=0 by using effective_window = max(window, 1)
  to prevent division by zero in the rate check.
- Use dataclasses.replace in the integration test to preserve all
  original RuntimeCaps fields instead of reconstructing with only
  execution_timeout.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(policies): add minimum/maximum constraints to detect_thrashing schema

Add validation bounds to the registry params_schema so invalid config
values fail fast: consecutive_threshold >= 0, window >= 1,
window_error_rate in [0.0, 1.0].

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(policies): use Phase enum in detect_thrashing integration test

Use Phase.TOOL_RESULT instead of the bare string "tool_result" in the
PhaseSelector construction, consistent with other integration tests.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-30 02:09:21 +00:00
Daniel Lok a14c88efd1 fix(web): clear deleted pinned sessions from the sidebar's Pinned section (#3492)
* fix(web): clear deleted pinned sessions from the sidebar's Pinned section

The Pinned section reads a sibling ["pinned-conversations"] cache that the
delete mutations' prefix-matched ["conversations"] sweep deliberately skips
(nesting it under that prefix breaks the pin-toggle's cache patch). That
isolation is by design, but it means the delete handlers must drop the row
from the pinned cache explicitly — which they didn't. So deleting a pinned
session removed it from the flat list but left it lingering in the Pinned
section until a full reload.

Mirror the unpin removal in all three delete paths (single-delete onSuccess,
bulk-delete onSuccess, and bulk-delete onError's partial-success branch),
patching the pinned cache in place rather than invalidating for the same
search-reindex-lag reason the list is patched in place.

Co-authored-by: Isaac

* fix(web): keep the sidebar row height stable during delete

The in-flight "Deleting…" status row that replaces an interactive
conversation row used `text-sm py-2` with no height constraint, while
the interactive row uses `sidebar-compact-text h-7 py-0.5`. So starting
a delete didn't just recolor the row — it grew taller and changed font
size, shifting the surrounding list.

Match the deleting row's box metrics to the interactive row (h-7,
sidebar-compact-text font size, otto-sm radius) so the swap only changes
color/opacity.

Co-authored-by: Isaac

* fix(web): keep the sidebar row size stable when editing the title

The inline rename row rendered a `text-sm` (14px) input inside a wrapper
whose `py-1` + `size-7` buttons summed to ~36px, while the interactive
row is `h-7` (28px) with the 13px `sidebar-compact-text` font. So
double-clicking to rename made the row grow taller and bump the font
size, an input visibly larger than the row it replaced.

Match the edit row's box metrics to the interactive row (h-7,
sidebar-compact-text, otto-sm radius) and drop the buttons to icon-xs
(24px) so they sit inside the 28px row, leaving only the muted edit
background to signal the mode.

Co-authored-by: Isaac

* test(e2e): guard pinned-session delete clears the Pinned section

Adds a browser e2e that pins a session (while sitting on `/`, so it isn't
the active chat) and deletes it, asserting the "Pinned" section unmounts
in place — no reload.

Two harness details are load-bearing, and getting them wrong yields a
test that passes even against the buggy build:

- Delete a NON-active pinned session (page on `/`). Deleting the open
  session navigates away and refetches; an active session also gets a
  WS `removed`-frame reconcile. Either clears the row regardless of the
  cache bug.
- Assert the "Pinned" SECTION disappears, not the row's href. While the
  delete is in flight the row swaps to a hrefless "Deleting…" status row,
  so an href-count assertion flickers to 0 during that transient and
  passes spuriously; the section stays mounted until the pinned cache is
  actually empty.

Verified it fails (~3s) against a build with the pinned-cache delete
patch removed, and passes with it.

Co-authored-by: Isaac
2026-07-30 09:41:37 +08:00
Corey Zumar c5448dc8f3 feat(web): semantic tool-run fold labels in chat view (#3518)
Collapsed tool runs in the chat transcript now read like the native
CLIs' step summaries ("Ran 1 shell command, read 2 files", "Listed 1
directory") instead of the generic "See N steps". The label is derived
from the folded calls' tool names and arguments in formatToolRunLabel:

- categories: shell / list / read / edit / search, covering omnigent
  sys_* tools plus the native harness names (Claude Code Bash/Read/...,
  Codex shell/apply_patch, pi & opencode lowercase bash/read/edit/...)
- shell commands that are a bare ls / cat recategorize as directory
  listings / file reads, matching the vendor TUIs; codex's login-shell
  wrapper (/bin/bash -lc '...') is unwrapped first
- runs of only unrecognized tools fall back to "Called N tools"
- per-step titles added for the native harness tools (Bash prefers the
  model-written description, codex shell shows the unwrapped command)

The fold now labels only its own (hidden) contents; the whole-run
count plumbing is gone since the label no longer double-counts the
visible streaming tail.

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-29 16:47:23 -07:00
Corey Zumar e7a163ee7e fix(web): show startup spinner when a send relaunches a disconnected runner (#3514)
* fix(web): show startup spinner when a send relaunches a disconnected runner

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): show a sidebar starting spinner while a session is booting

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-29 15:26:24 -07:00
Corey Zumar c0eca53546 fix(web): sidebar rename targets the wrong session when the list reorders mid-interaction (#3515)
* fix(web): don't rename the wrong session when the sidebar reorders mid-double-click

Double-click rename fired on whichever row received the dblclick event.
Browsers pair the two clicks of a double-click by pointer position and
timing, not element identity, so when the list reordered between the
clicks (an updated_at bump pushing rows around under the cursor) the
second click and dblclick landed on the row that slid into place and
opened rename on it — committing the typed title to a session the user
never aimed at.

Track the last two clicks each row receives and enter rename only when
the row saw both clicks of the pair; a dblclick preceded by a single
recent click means the double-click started on a different row and is
ignored.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): freeze sidebar order under the pointer so single-click actions hit the aimed row

The double-click guard can't help single-event interactions: a right-click
(or kebab click) that lands just after a background updated_at bump opens
the context menu of whichever row slid under the cursor — the menus are
visually identical, so the user renames (or archives, deletes, stops) a
session they never aimed at.

Fix it upstream of any one interaction: while the pointer is inside the
conversation list, pin every row's sort key at its first-seen value so
rows cannot move under the cursor at all. Keys accumulate lazily in
sortByUpdatedAtDesc (covering project folders and pages loaded mid-hover)
and clear when the pointer leaves, snapping the order back to reality.
The active row's frozen key captures its ActiveChatOverride value so
dropping the override mid-hover (clicking another row) can't move it
between the clicks of a double-click either.

Also rebuild the element tree per rerenderSidebar call in the row-actions
test harness — re-rendering the identical element let React bail out
without re-invoking the sidebar, silently ignoring mid-test data swaps.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): hold sidebar order while a rename edit is open, not just while hovered

The order freeze keyed off pointer position alone, but the pointer
naturally drifts out of the sidebar while typing a new title — the hold
released mid-edit and background updated_at churn resumed shuffling rows
around the open input. Moving the edit row's DOM node also blurs the
input, committing a half-typed title.

Rows now report an in-progress inline rename through RowEditHoldContext,
and ConversationList keeps the sort-key freeze active while the pointer
is inside the list OR any rename edit is open. The frozen-key map clears
only once neither hold remains, so the order snaps back on commit/cancel
(or pointer-leave with no edit open).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): engage the rename-edit order hold before paint

A passive effect reports the hold after paint, leaving a one-frame
window — when rename starts with the pointer already outside the list
(context-menu portal) — where a background updated_at reorder could
move and blur the just-mounted input. useLayoutEffect closes the gap.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-29 15:02:45 -07:00
Corey Zumar 97385e4760 fix(runner): retry tunnel login redirects instead of exiting on ever-connected runners (#3511)
* fix(runner): retry tunnel login redirects instead of exiting on ever-connected runners

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(runner): gate on_reconnect catch-up scan on an accepted upgrade

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* chore: revert unintended uv.lock registry churn

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-29 14:20:47 -07:00
Corey Zumar e94d5957dc fix(terminals): make web-terminal scrolling work for mouse-tracking TUIs (mode replay + trackpad wheel) (#3510)
* fix(web): make trackpad wheel scrolling work in the terminal view

xterm's built-in wheel-to-mouse-report conversion damps sub-50px pixel
deltas by 0.3x and emits at most one report per DOM event, so macOS
trackpad scrolling over a mouse-tracking TUI (Claude Code, tmux mouse on)
barely moves. Replace it with a custom wheel handler that accumulates
deltas at face value and emits one SGR report per whole line, deferring
to xterm's native handling when the pane program isn't tracking the
mouse (e.g. a plain shell on the control transport).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(terminals): replay pane screen/input modes in the control-mode attach seed

capture-pane records cell contents only, so a TUI that entered the
alternate screen and enabled mouse tracking before the web client
attached (OpenCode, vim — anything that sets modes once at startup)
left the browser xterm believing no tracking was active: wheel events
sent nothing and the terminal view could not scroll until the program
happened to re-toggle its modes. Reconstruct the modes from tmux's pane
flags and replay them around the seed — alt screen before the content
so it never pollutes primary scrollback, mouse tracking/encoding and
DECCKM after the cursor restore.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): pin wheel-to-SGR-report forwarding for mouse-tracking shells

A program in a user shell enables any-motion + SGR mouse tracking and
records its stdin; a slow trackpad-sized wheel gesture over the xterm
must land >=3 wheel-up reports. xterm's damped built-in conversion
yields <=1, so this fails without the accumulating wheel handler
(verified against an unfixed UI build).

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(terminals): harden seed metadata parsing and quote e2e log path

Address review: pad missing/empty tmux mode-flag fields so a flags
anomaly costs only the optional mode replay, never the cursor and
alt-screen state; quote the wheel-log path typed into the e2e shell.

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e_ui): type-annotate the wheel test's tmp_path fixture

Signed-off-by: dbczumar <corey.zumar@databricks.com>

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-07-29 14:19:41 -07:00
Bryan Qiu 5ac7f7cdb8 fix(web): preserve file browser scroll position across session switches (#3490)
* fix(web): preserve file browser scroll position across session switches

The Files panel's scroll container never tracked its position, so
switching conversations collapsed the list to a loading state and
clamped scrollTop back to 0 with nothing to restore it.

Cache scrollTop per conversation (and per Changed/All view) in a
module-level map — the same pattern FolderTree uses for expanded
paths — restoring it once the view's data is ready, and gating saves
on having restored first so the loading-state clamp can't overwrite
the cached value.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(web): survive the loading clamp when restoring file browser scroll

The first cut restored scrollTop once when isLoading turned false — but
the files queries are disabled (not loading) until the environment query
resolves, so the restore fired against the short placeholder, clamped to
0, and the clamp's scroll event overwrote the cached position.

Gate on data presence instead, re-assert the target via an
animation-frame loop until the container can hold it (or its height
stops changing), and keep saving off until the restore settles.
Also re-sync FolderTree's expanded-paths state from its cache when the
conversation changes without a remount — previously the tree kept the
prior conversation's expanded set, which also skewed content height at
restore time.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(web): keep the open file's scroll position across session switches

The app remembers which file is open per session and re-opens it in the
viewer on switch-back — at the top. The earlier fix only covered the
Files panel list, so what users actually saw (the open file's content)
still reset.

Extract the clamp-surviving restore logic into a shared useScrollRestore
hook (FilesPanel now consumes it) and wire persistence into every viewer
surface, keyed per conversation + path: the Monaco code editor and diff
viewer (via their scroll APIs), the FileViewer content area, the
markdown/notebook previews, and the TipTap markdown editor.

Verified end-to-end in a real browser: Playwright tests scroll, switch
sessions via the sidebar, switch back, and assert the offset returns —
for both the file tree and an open markdown file.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

* fix(web): harden scroll restore against async content growth and Monaco clamps

The restore loop gave up as soon as the container's height held still
for one frame — but previews grow in bursts (async syntax highlighting,
image decode, lazy notebook cells), so a single stall stranded the
reader at the top. Replace the giveup with a 1.5s deadline that keeps
re-asserting the saved offset, and settle immediately on wheel/touch/
pointer input so the user is never fought for the scrollbar.

The Monaco surfaces saved onDidScrollChange offsets unconditionally, so
a not-yet-laid-out editor's clamp-to-0 event could permanently overwrite
the cached position. A shared attachEditorScrollRestore helper now
suppresses saves and re-asserts the target until it's reached, the user
scrolls, or the budget expires — the same contract as the DOM hook.

Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>

---------

Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
2026-07-29 13:28:11 -07:00
Tomu Hirata 4d23ed7814 fix(native): route policy-hook evaluation through runner relay (#3489)
CI / gate (push) Failing after 1s
Lint / gate (push) Failing after 1s
web Tests / gate (push) Failing after 2s
Doc sync / Classify and draft docs (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
Lint / Version lockstep check (push) Has been cancelled
Sync OpenAPI to site / Open sync PR on omnigent-site (push) Has been cancelled
UI Preview / notify (push) Has been cancelled
UI Preview / build (push) Has been cancelled
OSS Scorecard / Scorecard analysis (push) Has been cancelled
UI Preview / cleanup (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
web Tests / web test (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
Windows (native) / Windows smoke + unit (push) Has been cancelled
Lint / Pre-commit checks (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
UI Preview / deploy (push) Has been cancelled
Native hook subprocesses (codex, claude, kimi, hermes, cursor) and the pi/opencode
JS extensions have been POSTing directly to the Omnigent server with a baked
30-minute bearer token. After expiry, every hook invocation pays ~1.7s for
credential re-discovery. The relay approach eliminates this class of failure
entirely by removing the server bearer from hook configs.

Changes:

relay handler (claude_native_bridge.py):
  Add POST /policies/evaluate to the tool relay HTTP server. The relay
  authenticates callers with its existing non-expiring local token and
  proxies to the Omnigent server using asyncio.run_coroutine_threadsafe
  with the runner's refresh-capable server_client (86400s timeout to
  match ASK gate long-polls). session_id is written into tool_relay.json
  so hook subprocesses can identify the session without a separate config.

runner/app.py:
  Pass server_client and session_id to start_tool_relay so the relay can
  serve the /policies/evaluate proxy endpoint.

native_policy_hook.py:
  Add read_relay_policy_config(bridge_dir) helper that reads tool_relay.json
  and returns (relay_url, relay_token, session_id), and relay_policy_evaluate_url.
  Add _RELAY_URL_ENV / _RELAY_TOKEN_ENV constants for env-var harnesses.

hook subprocesses (codex, claude, kimi):
  Read tool_relay.json first via read_relay_policy_config; fall back to
  direct server call (policy_hook.json / permission_hook.json) when the
  relay is not yet up. Remove _PersistingReauth from codex_native_hook.

hermes/cursor hook subprocesses:
  Check _OMNIGENT_RELAY_URL / _OMNIGENT_RELAY_TOKEN env vars; fall back to
  existing _OMNIGENT_AUTH_HEADERS path when absent.

hermes_native_bridge.py:
  Add inject_relay_into_policy_hook which rewrites omnigent-policy-hook.sh
  with relay env vars after ensure_comment_relay runs.

orchestration.py:
  Wire ensure_comment_relay into _auto_create_pi_terminal (new param) and
  inject relay coords into pi config.json and hermes wrapper script after
  relay starts. Wire ensure_comment_relay into opencode policy_env via
  OMNIGENT_RELAY_FILE. Remove _policy_hook_auth_loop and related refresh
  machinery (_register/_unregister_policy_hook_auth, _POLICY_HOOK_AUTH_SESSIONS).

pi extension JS:
  Add relayCredentials() that re-reads config.json for relayUrl/relayToken
  on each call; evalNativePolicyHttp prefers relay URL and token over direct
  server call.

opencode plugin JS:
  Add relayCredentials() that re-reads OMNIGENT_RELAY_FILE (tool_relay.json)
  on each call; evaluate() prefers relay over direct server call.

pi_native_bridge.py:
  Add inject_relay_into_config to write relayUrl/relayToken into config.json.

All harnesses keep a direct-server fallback so sessions started before the
relay is up (first-call race) continue to work. The relay path is taken on
every subsequent call once tool_relay.json is written.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-29 15:08:43 +00:00
Pat Sukprasert b28a5701c7 [models] Discover Kiro picker models from CLI (#3452)
* feat(models): discover Kiro picker catalog

Replace the curated Kiro model picker table with the CLI's JSON model listing so newly released, renamed, or retired Kiro models no longer require an Omnigent source update.

Run discovery on the bound runner, expose it through a dedicated model-options endpoint, and reuse the server's asynchronous single-flight cache so snapshots never block on the CLI process.

Preserve Kiro-provided default, description, context-window, and credit-rate metadata in picker rows, remove four hardcode allowances, and document the discovery boundary.

Tests: 115 Kiro, runner lifecycle, and server snapshot tests; staged pre-commit run; manual validation against kiro-cli 2.10.0 output.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* test(kiro): cover picker discovery failures

Exercise the runner endpoint's retryable 503 path when Kiro CLI model discovery fails so the server keeps its picker cache cold instead of treating failure as an empty successful catalog.

Extend the session snapshot round-trip to verify provider descriptions and rate units survive NativeModelOption's extra-field wire schema alongside context windows and rate multipliers.

Tests: 116 Kiro, runner lifecycle, and snapshot tests passed. Live kiro-cli 2.15.1 discovery returned nine models with auto as the sole default. Targeted pre-commit passed.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(kiro): narrow discovered picker contract

Remove the unused kiro_base_model_options compatibility alias because its old pure lookup contract became a blocking CLI subprocess and no production caller remains.

Stop emitting isCurrent for Kiro because the CLI discovery response does not provide current-session state and the Web picker derives the selected row from model_override.

Cover missing and mismatched CLI defaults in the discovery mapper and verify that the Web picker falls back to its Default sentinel, leaving Kiro responsible for choosing the actual default. Refresh the Kiro picker E2E fixture and wording to match live discovery.

Tests: 118 focused Kiro/runner/snapshot tests; 4,705 Web tests; targeted pre-commit checks.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-29 13:43:14 +00:00
Pat Sukprasert 202cf66bd7 refactor(harness): registry-driven native launch dispatch — scaffolding + uniform arms (PR 1.5b-i) (#3500)
First half of the runner launch seam. Wires the provider's auto_create_terminal
field (declared since 1.1, never dispatched) and collapses the 8 uniform
create-session launch arms in runner/app.py onto it. Behavior-preserving.

- orchestration: add NativeLaunchContext (flat dataclass of the inputs the 11
  builders may need, incl. claude's closures), PreLaunchResult (skip /
  force_recreate / needs_terminal for the special arms in 1.5b-ii), 11 thin
  _launch_<x>(ctx) adapters that unpack the context and call the unchanged
  _auto_create_<x>_terminal builder with that harness's exact kwarg subset, and
  the shared shell _launch_native_terminal(harness, ctx, *, ensure_locks,
  pre_launch=None, resolve_agent_spec=None). The shell runs the lock /
  existence-check / pending+error-event mechanics every arm shared and resolves
  the adapter via resolve_hook(provider, "auto_create_terminal").
- Option A (adapters, builders unchanged) keeps the 21 direct-call builder tests
  intact. agent_spec is resolved lazily via resolve_agent_spec inside the create
  block, preserving each arm's error semantics (pi unwrapped; cursor/opencode/
  kimi swallow OmnigentError via _resolve_session_agent_spec_or_none; the rest
  pass no resolver).
- harness_plugins: repoint auto_create_terminal to omnigent.runner.native:_launch_<key>.
- app.py: the 8 uniform arms (pi, cursor, kiro, opencode, goose, hermes, qwen,
  kimi) become one _launch_native_terminal call each, picking the per-harness
  lock dict (kept app-scope so session cleanup can pop by name). Net -256 lines.
- qwen's launch-error label is now "Qwen Code" (uniform display_name) vs the
  former lowercase "qwen" — cosmetic; no test asserted the literal.

Deferred to 1.5b-ii: the 3 special arms (claude/codex/antigravity) and the
turn-path opencode cold-boot, which still use the direct builders.

Tests: unit-cover each adapter's kwarg subset and the shell's branches
(create / existing-skip / force-recreate teardown / skip+needs_terminal /
start-error event / lazy-spec-only-on-create / non-native None). The workflow-
init HTTP suite exercises the real launch path for the uniform arms and stays
green. Pre-existing codex gateway-env failures in events_lifecycle are unchanged
(verified identical on clean main; codex arm untouched here).

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 13:25:42 +00:00
Pat Sukprasert fa96f946ef feat(sessions): Add shared-message attribution (#3422)
- Preserve trusted authorship across history, buffered turns, and native harnesses
- Label model-visible messages while keeping owner credentials authoritative

Refs #2150

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 12:47:01 +00:00
Pat Sukprasert 676b5ef2e2 feat(models): route from discovered catalogs (#3450)
Remove the release-specific smart-routing model table and require the runner worker catalog for routing candidates. When discovery is unavailable, leave the harness on its provider-resolved default instead of selecting a stale fallback.

Order catalog candidates by normalized provider-relative cost tiers while preserving catalog order as the tie-breaker, and express the built-in judge rubric through stable fast, balanced, and powerful intents rather than vendor model-name tiers.

Apply the same discovery-only rule to sys_advise_models, ratchet eight hardcode allowances, and document the remaining wire-compatibility exclusions as a separate migration boundary.

Tests: 67 focused routing/session tests; staged pre-commit run.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-29 19:15:28 +07:00
Pat Sukprasert 2814871d67 [models] Resolve runtime defaults from catalogs (#3448)
* feat(models): resolve runtime defaults from catalogs

Adapt MLflow provider listings into normalized resolver candidates with tri-state capability metadata, context windows, provider-relative cost tiers, and deterministic family filtering.

Replace release-specific defaults across workflow ucode routing, SDK executors, Databricks execution, and Claude/Codex/Pi/OpenCode native launch paths. Explicit request, spec, ucode, and provider-configured models continue to win; unresolved defaults now use the active provider catalog and fail clearly when discovery has no compatible model.

Improve model-version sorting so provider prefixes, dates, endpoint sizes, and unrelated numeric families do not distort catalog order. Ratchet ten obsolete hardcode allowances and document the runtime migration boundary.

Tests: 168 catalog/workflow tests; 519 executor tests; 449 native tests; staged pre-commit run.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): honor overrides before defaults

Apply per-session and CLI model overrides to the effective executor spec before spawn-environment builders attempt provider default resolution. This keeps explicit request values authoritative when catalog lookup is unavailable.

Preserve an explicit OMNIGENT_MODEL value when --harness selects the runtime, and allow model-only E2E overrides when the YAML owns harness selection. Add deterministic fixture models to unrelated tests so catalog-disabled CI does not depend on discovery.

Tests: 8 catalog-disabled CI regressions; 284 broader CLI/runtime/runner tests; staged pre-commit including the hardcoded-model lint.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): preserve catalog default policy

Route default-intent catalog resolution through the existing general-purpose selection policy after family filtering. This retains specialty-model exclusion and provider pins while leaving non-default intents on metadata ranking.

Require dynamically discovered Databricks defaults to use gateway-routable databricks-prefixed ids, report actionable catalog misses to direct executor callers, and model context capacity from max input tokens rather than input plus output budgets.

Add regression coverage for constrained defaults, lagging provider pins, OpenAI specialty variants, Databricks Claude/OpenAI routing, and context-window normalization.

Tests: 85 focused catalog/provider tests passed; 150 broader tests produced 149 passes plus the documented ambient Claude-login failure. Live Databricks catalog verification found 14 Claude and 16 OpenAI entries, all gateway-prefixed. Pre-commit passed all relevant hooks; repository-wide web-prettier and stale routing protobuf checks remain baseline failures.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): offload catalog discovery

Run cold catalog resolution on the existing dedicated thread helper from async Codex, Databricks, Open Responses, OpenAI Agents, and Pi turn paths. Model-less first turns can now wait for remote discovery without blocking the shared event loop for the catalog timeout.

Keep explicit and configured model precedence synchronous and unchanged. Make Pi's internal model resolver async so its Databricks fallback follows the same non-blocking boundary.

Add a regression that verifies catalog discovery executes outside the event-loop thread and update Pi resolver tests for the async contract.

Tests: 405 affected executor tests passed. Targeted pre-commit passed, including formatting, Ruff, and the hardcoded-model lint.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* fix(models): offload Claude catalog lookup

Move the remaining Claude SDK Databricks catalog fallback onto the dedicated thread helper so a cold remote lookup cannot block the async turn loop.

Restore direct Pi coverage tying a catalog-selected Databricks default to dynamic models.json registration. This preserves the prior unknown-model invariant even when the selected gateway id is newer than Pi's curated static entries.

Tests: 251 Claude SDK and Pi executor tests passed with the documented macOS path-canonicalization test deselected. Targeted pre-commit passed, including Ruff and the hardcoded-model lint.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-29 09:30:31 +00:00
Pat Sukprasert 5555c88944 refactor(harness): route native spawn-env through the provider seam (PR 1.5a) (#3495)
* refactor(harness): route native spawn-env through the provider seam (PR 1.5a)

Collapse the two near-identical 11-arm native spawn-env dispatch chains in
runner/app.py (create-session ~2567 and dispatch ~6092) onto the provider seam.
Each block becomes one guarded call to a registry-driven helper; net -171 lines
in app.py. Behavior-preserving — every native harness produces the identical
spawn env before/after.

- harness_plugins: populate `spawn_env_builder` on all 11 built-in providers
  (uniform `omnigent.<key>_native_bridge:build_<key>_native_spawn_env`) and add
  a `bridge_id_label_key` field, set to `omnigent.<key>_native.bridge_id` for
  the three label-based harnesses (codex/opencode/antigravity). The label key is
  derived (not imported) to keep harness_plugins import-light; a test pins the
  derivation against the real bridge constants.
- runner/native/orchestration: add `_resolve_native_spawn_env(harness, session_id,
  *, server_client, optional_labels)`. It resolves `provider.spawn_env_builder`
  and handles the three shapes — bare (session id only), label (bridge id from
  `bridge_id_label_key`), and two named specials: claude (bridge id via the
  runner helper with a server-side fallback) and hermes (writes its policy-hook
  config before building). Returns None for non-native harnesses so the caller
  keeps its SDK spawn env. Re-exported via runner/native/__init__.
- runner/app: both blocks now call the helper; the per-harness bridge imports and
  label-key reads are gone.

The two special-cases (claude/hermes) stay named branches in the helper rather
than fully data-driven provider fields — their only consumers are single call
sites, and 1.5b's NativeLaunchContext will reshape the right calling convention.

Tests: extend the provider-paths-resolve + required-hooks tests to cover
spawn_env_builder; pin bridge_id_label_key against the real constants; add
`_resolve_native_spawn_env` unit coverage for all four shapes + the non-native
None path. The existing workflow_init codex-bundle-dir spawn-env test (the
end-to-end behavior-preservation proof) stays green unchanged.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test(harness): hoist spawn-env test imports to module level

Move the per-test `_resolve_native_spawn_env` and
`CODEX_NATIVE_BRIDGE_ID_LABEL_KEY` imports (added in 1.5a) up to the module
import block. No behavior change; test-only cleanup.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 09:26:49 +00:00
Zeyi (Rice) Fan c38e174f1a fix(web): resolve jest-dom matcher types under pnpm via packageExtensions (#3494)
## Related issue

N/A

## Summary

- `@testing-library/jest-dom` never declares `vitest` as a (peer) dependency.
  Under pnpm's store layout, jest-dom's `declare module "vitest"` matcher-type
  augmentation can't resolve `vitest`, so it silently fails to merge and `tsc`
  loses every DOM matcher (`toBeInTheDocument`, `toHaveClass`, …) — even though
  they register fine at runtime. See vitest-dev/vitest#10411.
- Declare the missing peer via pnpm `packageExtensions` so pnpm links `vitest`
  into jest-dom's scope and the augmentation resolves. This is a root-cause fix
  at the dependency layer — no hand-written type shim needed.
- Note: `type-check` still has unrelated pre-existing errors and is not yet
  gated in CI; this fix only removes the jest-dom matcher category.

## Test Plan

- `pnpm install --frozen-lockfile --filter web` — lockfile stays consistent.
- `pnpm --filter web run type-check` — jest-dom matcher errors drop from 1589
  to 0 (remaining errors are unrelated and pre-existing).
- `pnpm --filter web run test` (e.g. `src/shell/WorkspacePanel.test.tsx`) —
  15/15 pass under Node 22; runtime is unaffected.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Verified by comparing `pnpm --filter web run type-check` jest-dom error counts
(1589 → 0) and running the existing vitest suite (unaffected). The change is
dependency-resolution config only, with no new runtime code to test.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-29 01:24:38 -07:00
Harry Su c62bfc2fe7 docs(policies): document static YAML registration for cel_policy (#3462)
The module docstring only showed the session policy REST API, implying
CEL policies can't be declared statically. Both static paths work and
are now shown: config.yaml policies (handler + factory_params, parsed
by omnigent.inner.loader) and bundled agent specs (guardrails.policies
with a function {path, arguments} mapping, parsed by
omnigent.spec.parser — which does not read factory_params). Verified
both forms against their parsers.

Signed-off-by: Harry Su <tiecheng.su@robinhood.com>
2026-07-29 06:30:35 +00:00
Tomu Hirata 031924b26a fix(codex-native): replace hook-trust carry machinery with --dangerously-bypass-hook-trust (#3477)
* fix: add codex_cli_version to fake app-servers in tests; fix ruff format

- Add codex_cli_version = None to all _FakeCodexAppServer classes so they
  satisfy the new attribute read in the orchestration bypass_hook_trust gate
- Collapse the multiline boolean in orchestration to satisfy ruff format

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: symlink hooks.json into private CODEX_HOME so user hooks fire

hooks.json was never symlinked, so user hooks declared there were silently
ignored in private sessions. Add it to _CODEX_HOME_GLOBAL_INSTRUCTION_FILES
so it's symlinked in full sessions but skipped in minimal_config (title
worker) mode. Trust is no longer a concern since --dangerously-bypass-hook-trust
is passed to runner-owned TUI sessions.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: merge user hooks.json into policy hooks file instead of clobbering symlink

_write_codex_policy_hooks_file was using os.replace() which destroyed the
hooks.json symlink created by _populate_codex_home_config, silently dropping
all user hooks. Now when the path is a symlink, we read the user's hooks,
merge them after the policy hooks for each event (plus any user-only events),
remove the symlink, and write the merged payload as a regular file.

User hooks from ~/.codex/hooks.json now fire in private sessions.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* style: collapse _merge_user_hooks signature to one line (ruff format)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-29 06:30:16 +00:00
Tomu Hirata c52da1dbcc feat(model-discovery): add max_results and parent params to UC model-services request (#3478)
Scopes the listing to system.ai models only via the parent filter and
raises the result cap to 1000, matching the recommended API call at
/ajax-api/2.1/unity-catalog/model-services?max_results=1000&parent=schemas%2Fsystem.ai.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-29 15:09:39 +09:00
Daniel Lok 1a96952a7f fix(web): scale conversation sidebar text with the font-size setting (#3480)
* fix(web): scale conversation sidebar text with the font-size setting

The sidebar's compact text was pinned to a fixed `--sidebar-font-size:
13px`, so the Appearance font-size setting only moved the surrounding
rem-based padding while the text stayed at 13px. Express the variable in
`rem` (0.8125rem = 13px at the 16px default) so it rides the root
font-size, which already folds in `--ui-font-scale` and the mobile bump.

Drop the explicit `line-height` on `.sidebar-compact-text`: single-line
rows use fixed height + flex centering (line-height inert), and the two
line-clamped previews now inherit the root's unitless 1.5, which scales
with the text for free.

Co-authored-by: Isaac

* test(e2e-ui): regenerate visual baselines

---------

Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-29 14:07:08 +08:00
Pat Sukprasert 33a06cc0a4 [models] Add intent resolver contracts (#3443)
* feat(models): add intent resolver contracts

Define stable model intents and provider-neutral metadata for capabilities, context windows, cost tiers, and wire APIs. Capability support is tri-state so incomplete provider listings cannot be mistaken for positive support.

Add deterministic resolution precedence for explicit choices, configured defaults, live catalogs, and documented static fallbacks. Catalog order remains the tie-breaker, while provider-specific preference policies can override ranking without changing callers.

Expose normalized metadata through model catalog entries and payloads without changing any executor or routing defaults in this slice.

Tests: 108 focused resolver, catalog, and smart-routing tests; staged pre-commit hooks.

Part of #3426

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(models): keep resolver intents caller-backed

Limit the public model intent vocabulary to default, fast, balanced, and powerful because those are the only purposes represented by current callers.

Express tool use, image generation, structured output, and similar requirements through explicit capabilities instead of speculative intent-to-capability mappings. Remove the unused large-context ranking path and update resolver tests and migration guidance accordingly.

Tests: uv run --no-sync pytest -q tests/test_model_resolver.py tests/test_model_catalog.py tests/server/test_smart_routing.py; pre-commit run
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* refactor(models): complete wire API contract

Cover every model-endpoint request shape implemented by the provider adapters by adding Bedrock Converse and naming Gemini generateContent explicitly. Keep native CLI and ACP transports outside the model wire protocol vocabulary.

Clarify that explicit model overrides bypass compatibility constraints, intent tiers are best-effort ranking preferences, and uncatalogued explicit resolutions have unknown family and metadata. Add regression coverage for those semantics and for the complete wire API vocabulary.

Tests: uv run --no-sync pytest -q tests/test_model_resolver.py tests/test_model_catalog.py tests/server/test_smart_routing.py tests/llms/test_openai_adapter.py tests/llms/test_anthropic_adapter.py tests/llms/test_gemini_adapter.py tests/llms/test_vertex_adapter.py tests/llms/test_bedrock_adapter.py tests/llms/test_databricks_adapter.py; pre-commit run
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-29 05:18:34 +00:00
Pat Sukprasert 1c23bf77db docs(harness): revise Phase 1 estimates from runner exploration (#3357)
* docs(harness): revise Phase 1 estimates from runner exploration

Reading the runner dispatch surface (not guessing) changed the shape of the
remaining work, so update the proposal's estimates and plan:

- Split PR 1.5 into a serial runner sub-stack: 1.5a spawn-env (bounded, the
  first measurement), 1.5b launch (the epicenter — _auto_create_<x>_terminal
  has 11 divergent signatures, so the seam passes a NativeLaunchContext to a
  uniform provider.auto_create_terminal(ctx) adapter with pre_launch hooks,
  not a single positional call), 1.5c terminal-route.
- Re-scope 1.6 interrupt/stop upward (Med -> Med-High, 2d -> 3-4d): every
  handler closes over app-scope state (server_client, resource_registry,
  _publish_event, module dicts), so extraction needs a DI context, not a move.
- Revise totals: Phase 1 ~17-25 -> ~20-29 eng-days; overall ~26-37 -> ~29-41
  across ~12 -> ~14 PRs; critical path rewritten to the serial runner chain.
- Add a Calibration subsection recording the learning from 1.1-1.3 (additive
  PRs come in under estimate; the real cost is test-shape churn; the runner is
  the back-loaded risk) and settle the "signature uniformity" open question
  with the confirmed finding.

Docs-only; no code paths affected.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* docs(harness): record harness-bench compatibility with native plugins

The harness bench's selection + driver layer is already registry-driven:
manifest.py auto-adds every NATIVE_TUI capability as a BenchProfile and the
NativeTuiDriver is selected generically, so a community native plugin
enumerates and gets a profile with zero bench edits. Record the two remaining
gaps and where they close:

- Provisioning needs registry-driven agent seeding — closed for free by PR 1.7
  (the native driver provisions against a pre-seeded <harness>-ui agent).
- Tool-call probe metadata is hardcoded (_NATIVE_TOOL_PROVOCATION) — fold
  optional shell_tool_name / shell_tool_prompt capability fields into PR 1.8 so
  the probe reads off the registry; until then those probes skip (non-fatal).

Add a "Harness bench compatibility" subsection, extend 1.8's scope with the
tool-probe fields, and give 2.4 a benchable acceptance criterion (the example
plugin runs `python -m tests.harness_bench --harness <plugin> --live` green).
No new phase or standalone bench-migration PR.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 12:08:50 +07:00
Pat Sukprasert 14886486d5 refactor(harness): route native resume through the provider seam (PR 1.3) (#3314)
* refactor(harness): route native resume through the provider seam (PR 1.3)

Collapse the two hand-written native-resume dispatch chains onto
native_dispatch.resolve_hook_for_key(key, "run_native"):

- resume_dispatch._dispatch_wrapper: 10 `if native_agent.key == "<x>"` arms →
  one resolved call.
- chat._redirect_native_resume_if_needed: 6 arms + the 6
  _run_<x>_native_resume_redirect helpers → one resolved call that derives the
  redirect notice from the agent row (wrapper_name == agent.harness,
  native_command == agent.key, both verified equal to the old literals) and
  passes auto_open_conversation. Deletes the helpers.

Behavior change (intended fix): routing through the seam covers all 11 natives,
closing two latent coverage gaps that double-posted each user turn (the exact
hazard the cursor/kimi docstrings warned about):
- chat redirect covered only 6 of 11 — goose/hermes/antigravity/qwen/opencode
  resumes fell through to the Omnigent REPL.
- resume_dispatch covered only 10 of 11 — opencode fell through the same way.
No test pinned either old fall-through; added a chat goose regression test, a
chat unknown-wrapper → False test, and a resume_dispatch opencode test.

Also:
- native_dispatch.resolve is no longer cached — dispatch happens once per
  resume/launch/seed, import_module already caches the module, and caching the
  resolved attribute silently defeats monkeypatch.setattr("...:run_x", ...),
  which the resume/CLI tests rely on. Dropped reset_resolve_cache_for_tests.
- Normalize the cli.py _NativeTerminalDispatchSpec launch table to
  args_param="extra_args" (finishing 1.2's spelling migration into the launch
  hub) and update the tests that captured the old <x>_args kwarg.

Net -261 lines. Full resume/chat/cli/native suites green; new-failure delta vs.
the clean tree is zero.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* docs(harness): record PR 1.3 in the progress ledger

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 04:43:37 +00:00
Pat Sukprasert d9bc1a3040 🔒 fix(sessions): Restrict approvals to owners (#3416)
- Gate both approval event and resolve URL paths at owner access
- Prevent shared editors from authorizing tools using owner credentials

Refs #2150

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 10:44:25 +07:00
Pat Sukprasert 6a32587bfe refactor(harness): normalize native launcher pass-through args (PR 1.2) (#3244)
* refactor(harness): normalize native launcher pass-through args (PR 1.2)

The 11 run_<x>_native launchers each spelled their pass-through arg
differently (claude_args, pi_args, ...). The provider seam needs one uniform
spelling to call them generically. Introduce extra_args as that spelling and
keep <x>_args as a back-compat alias.

- Add native_terminal.normalize_extra_args(): reconciles extra_args vs the
  legacy <x>_args alias — extra_args wins, the legacy alias emits a
  DeprecationWarning (removal targeted for 0.9.0), neither yields ().
- Give all 11 run_<x>_native entry points a keyword-only extra_args and make
  <x>_args an optional deprecated alias, normalizing at the top of each body
  so the deep internals keep using the existing local variable unchanged.
- Migrate the internal callers (resume_dispatch ×10, chat resume-redirect ×6,
  cli_native ×11) to extra_args so nothing in core trips the new warning; the
  alias exists purely for external back-compat.
- Tests: unit-cover the four normalize_extra_args branches. Existing native
  tests that still call <x>_args= now double as back-compat coverage.

No behavior change: with default warning filters the full native + hub suite
is green (verified the failure set is byte-identical to the clean tree; the
handful of red tests are pre-existing gateway-env artifacts unrelated to this
change).

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* docs(harness): record PR 1.2 in the progress ledger

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-29 03:22:09 +00:00
Dhruv Gupta 210adf0dad fix(ci): exclude dev/pre tags from the backcompat version matrix (#3473)
The scheduled server-compat matrix builds its default version set from
all tags, filtering only rcN. Dev/pre tags are snapshots of main, so
main-vs-them cells add no compat signal, and under the 256-job matrix
cap they evict the oldest final releases — the coverage the workflow
exists for. A stray v0.4.0.dev0 tag is already in the live matrix
today, and a nightly prerelease lane would add ~25 such tags a month.
Explicit VERSIONS dispatch overrides still accept prerelease tags.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-29 01:19:21 +00:00
Andrew Peltekci 11c0fef152 fix(repl): don't report an Omnigent credential for ACP-backed sessions (#3431)
* fix(repl): don't report an Omnigent credential for ACP-backed sessions

acp / acp:<slug> / goose / qwen aren't in _HARNESS_FAMILY, so
default_provider_for_harness treats them as unmapped and falls through to
the configured anthropic/openai default. describe_active_credential then
hands back that provider's default_model and credential source, and both
the /model readout and the startup header render it as the active model.

But an ACP agent carries its own auth and picks its own model — the
executor only forwards a model at session/new when send_model_in_session_new
is set. So `omnigent run --harness acp:<agent>` confidently names a model
and an API key the session never touches.

Declines these harnesses at the resolver rather than the readout, so the
startup header stops fabricating too. The predicate reads the declared
capability record (ACP_SUBPROCESS + OWN_AUTH) instead of a hardcoded list,
so community ACP plugins are covered without further edits.

Signed-off-by: apeltekci <andrew@peltekci.com>

* fix(repl): scope the own-auth credential decline to acp/goose and keep overrides visible

The own-auth predicate wrongly included qwen: a harness mapped in
_HARNESS_FAMILY is provider-routed at spawn (_build_qwen_spawn_env injects
the configured openai-family default via
configure_agent_harness_with_provider, and QwenExecutor exports
OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL into the qwen subprocess —
see test_qwen_uses_openai_global_default), so its readout naming that
provider was truthful, and declining it fabricated "own auth" in the other
direction. The decline now applies only to unmapped ACP_SUBPROCESS +
OWN_AUTH harnesses (acp/acp:<slug>, goose, unmapped community ACP plugins).
The predicate is public now, so the REPL stops importing a private name,
and the manual acp:<slug> split is gone (canonicalize_harness already folds
it).

The own-auth readout also no longer claims an Omnigent-side /model override
does not reach the agent — model_env_keys() covers acp and goose, the
process manager respawns on a model change, and goose applies the override
as GOOSE_MODEL — and a live override is shown instead of hidden.

Tests: the resolver-level case now uses a key-kind openai default, the kind
the unmapped fallback actually fabricated (a subscription default was
already declined before the fix, so the previous case pinned nothing), and
new cases pin override visibility and qwen's provider-routed readout.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: apeltekci <andrew@peltekci.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-29 00:56:07 +00:00
Nikhil Chakre 2e6cde9303 fix(accounts): enforce the last-admin invariant atomically on delete (#3304)
DELETE /auth/users/{user_id} checked whether another admin existed and
deleted the target in two separate, unlocked transactions. Two
concurrent deletes of two different admins could each observe the
other as the remaining admin, both pass, and both apply, leaving
the deploy with zero admins and no in-app recovery path.

Lock the current admin set before counting it (BEGIN IMMEDIATE on
SQLite, SELECT ... FOR UPDATE on other dialects) so the check and
the delete happen in one transaction. A concurrent delete of a
different admin now blocks until the first commits and re-observes
the up-to-date count instead of a stale one.

Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
2026-07-29 00:07:08 +00:00
Corey Zumar badd76a75a fix(web): align sidebar primary nav icons on one column (#3468)
* fix(web): align sidebar primary nav icons on one column

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* fix(web): correct stale gap-1 reference in nav comment

Signed-off-by: dbczumar <corey.zumar@databricks.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: dbczumar <corey.zumar@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-28 16:41:37 -07:00
Zeyi (Rice) Fan e1f409245b feat(android): target Android 16 (API level 36) (#3470)
## Related issue

N/A

## Summary

Bump the Android module's `compileSdk` and `targetSdk` from 35 to 36 to meet
Google Play's requirement that apps target API level 36 by August 30, 2026.
This required updating the full Android toolchain:

- AGP 8.6.1 → 9.1.1 (AGP 9 has built-in Kotlin support)
- Gradle wrapper 8.9 → 9.3.1
- Gradle Play Publisher 3.12.1 → 4.0.0
- AndroidX dependencies to versions compatible with compileSdk 36 (e.g.,
  `androidx.core` 1.18.0, `androidx.activity` 1.12.4, `androidx.webkit` 1.15.0)
- Robolectric 4.14.1 → 4.16.1

The `org.jetbrains.kotlin.android` plugin is no longer applied because AGP 9
bundles Kotlin compilation support. Build-script helper tasks that previously
used the Gradle `exec { }` DSL were switched to `ProcessBuilder` to stay
compatible with the new Kotlin/Gradle DSL scope, and `android.sdkDirectory`
was replaced with `androidComponents.sdkComponents.sdkDirectory`.

## Test Plan

Ran the full local Android build pipeline:

```bash
cd web/android
./gradlew :app:assembleDebug :app:lintDebug
./gradlew :app:bundleRelease
./gradlew :app:assembleDebugAndroidTest
```

All completed successfully and produced a debug APK, release AAB, and androidTest
APK with zero lint errors.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Verified by running `:app:assembleDebug`, `:app:lintDebug`, `:app:bundleRelease`,
and `:app:assembleDebugAndroidTest` locally. The existing CI `android-bundle.yml`
workflow uses the Gradle wrapper and JDK 17, both compatible with the updated
toolchain.

## Changelog

Android app now targets Android 16 (API 36) to stay compliant with Google Play's
latest target API level policy.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-28 23:32:04 +00:00
Zeyi (Rice) Fan 2d17ee7b9b fix(build): migrate setup.py web UI build from npm to pnpm (#3467)
The repo migrated to a pnpm workspace (pnpm-workspace.yaml and
pnpm-lock.yaml at the root, packageManager: pnpm@11.15.1) but
setup.py's _build_web_ui still shelled out to 'npm install' / 'npm
run build' from inside web/. That path looked for a package-lock.json
that doesn't exist there (the lockfile is pnpm-lock.yaml at the
workspace root), so npm re-resolved from package.json alone and
hard-failed on the @lobehub/fluent-emoji@4.1.0 peer range
(react@^19 vs the pinned react@18.2.0) with ERESOLVE.

Migrate _build_web_ui to pnpm, matching deploy/databricks/build.sh
and the CI workflows (.github/workflows/e2e-ui.yml):

- Resolve pnpm via shutil.which('pnpm'), falling back to
  'corepack pnpm' (corepack ships with Node 22+ and auto-pins the
  version from package.json's packageManager field).
- Run from the workspace root (cwd=root), not web/, so pnpm uses
  the committed pnpm-lock.yaml.
- 'pnpm install --frozen-lockfile --filter web' then
  'pnpm --filter web run build' — exactly the CI commands.
  --frozen-lockfile guarantees the build is reproducible and
  resolves @lobehub/fluent-emoji against react@18.3.1 under the
  workspace's strictPeerDependencies: false, avoiding the peer
  conflict that broke npm.

Also enforce the Node.js 22 LTS floor up front via a new
_require_node_22 helper that fails fast with a dedicated, actionable
message if 'node' is missing or reports < 22 — instead of failing
deep inside the toolchain with an opaque error.

All existing skip/force env vars are preserved:
OMNIGENT_SKIP_WEB_UI=true (opt out), OMNIGENT_BUILD_WEB_UI=1
(force rebuild), skip-when-bundle-exists, skip-when-web-absent.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-28 15:52:36 -07:00
Zeyi (Rice) Fan 815cdbef43 refactor(sandboxes): split host-launch contract from exec transport (#3337)
## Related issue

N/A

## Summary

- Split `SandboxLauncher` into a layered hierarchy: `SandboxLifecycle`
  (lifecycle + capabilities), `SandboxExecTransport` (run/put/stream/exec),
  `SandboxHostLauncher` (abstract start_host), and `ExecModelHostLauncher`
  (default start_host + run_background + materialize_workspace).
- `SandboxLauncher` is now a backward-compat alias for `ExecModelHostLauncher`.
- Migrated Kubernetes to inherit `SandboxHostLauncher` directly — it no
  longer needs a fake `run()` that raises; the entrypoint-as-host model
  (Pod boots running the host) has no exec transport at all.
- All 8 providers now declare an explicit `capabilities` property instead
  of relying on class-var derivation.
- Updated the registry's `isinstance` guard to check `SandboxLifecycle`
  (the common base) so both exec-model and entrypoint-as-host providers pass.
- Updated the Kubernetes test that asserted `run()` raises to assert the
  method does not exist instead.

## Test Plan

```bash
uv run pytest tests/onboarding/sandboxes tests/sandbox tests/cli/test_cli.py tests/server -k managed -q
pre-commit run --files <all changed files>
```

All 780 selected tests pass and pre-commit is clean.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Existing provider and CLI tests pass unchanged, confirming backward
compatibility. The Kubernetes test was updated to reflect that `run()` no
longer exists on the launcher. The registry test was updated for the
`SandboxLifecycle` guard message.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-28 14:51:55 -07:00
Dhruv Gupta e70f46578c fix(cli): resolve conversation ids pasted with stray punctuation in omni resume (#3465)
A conversation id pasted with surrounding punctuation (e.g. a trailing
period) crashed `omni resume` with a raw StatementError traceback from
the local store's Uuid16 bind. Strip the punctuation a paste drags
along — none of it can be part of a valid id — and resume the id the
argument contains, canonicalized to bare hex so downstream consumers
never see a legacy spelling. Error only when no valid id remains.

Co-authored-by: Isaac

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-28 21:40:44 +00:00
Zeyi (Rice) Fan 43f58d74cc feat(android): instrumented screenshot capture with Gradle-managed servers (#3389)
Add a `./gradlew recordScreenshots` task that captures four real-WebView
screenshots of the Android shell on a device/emulator, with zero manual
setup — Gradle starts and stops both the Vite dev server and an isolated
omnigent backend automatically.

Screens captured (app/build/screenshots/):
  - server_select.png — native ConnectActivity (server-entry screen)
  - home.png           — SPA landing page (sidebar closed)
  - session_list.png   — SPA home with sidebar drawer open (?sidebar=open)
  - session.png        — session/chat page with a real seeded user message

How it works:
  - startBackendServer: launches `omnigent server` in a throwaway mktemp
    data dir (OMNIGENT_DATA_DIR/CONFIG_HOME/DATABASE_URI isolated from
    ~/.omnigent, no-auth on loopback), pre-registers examples/kimi_hello.yaml.
  - seedDemoSession: POST /v1/sessions with an initial user message so the
    session screenshot has real content.
  - startWebDevServer: launches `node vite --host 127.0.0.1 --port 5173`
    directly (avoids spawning npm/pnpm whose grandchild is hard to kill),
    reuses an existing server if present. Vite proxies /v1 to the backend.
  - Per screen: pm clear + pre-grant POST_NOTIFICATIONS, then drive the real
    ConnectActivity → MainActivity flow via UI Automator (am instrument, not
    AGP's connectedDebugAndroidTest which auto-uninstalls and deletes the
    screenshot before we can pull), then adb pull the PNG.
  - stopWebDevServer / stopBackendServer: tear down both + clean temp dir.

The test (ScreenshotTest.kt) is pure UI Automator (out-of-process, black-box):
it launches the app from the launcher, types the server URL (base + route
path) into ConnectActivity, taps Connect, waits for the floating switch pill
as the "shell is up" signal, then captures via UiDevice.takeScreenshot. The
session-list screen uses the ?sidebar=open query param (AppShell reads it on
mount to open the conversation drawer) since uiautomator can't see inside the
WebView to tap the toggle button.

Dependencies added (pinned to the AGP 8.6 / compileSdk 35 toolchain):
  androidx.test:runner 1.6.2, :rules 1.6.1, ext:junit 1.2.1
  androidx.test.espresso:espresso-core 3.6.1
  androidx.test.uiautomator:uiautomator 2.4.0
Also sets testInstrumentationRunner = AndroidJUnitRunner.

Usage:
  ANDROID_SERIAL=emulator-5554 ./gradlew recordScreenshots
  open app/build/screenshots/*.png

Requires an emulator or unlocked device. The backend/Vite are fully managed
— no separate terminals needed.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-28 14:12:01 -07:00
Dhruv Gupta ef8423b3ab fix(release): harden finalize, cut, and homebrew against the 0.7.0-cycle failure modes (#3381)
* fix(release): harden finalize, cut, and homebrew against the 0.7.0-cycle failure modes

- finalize: docs sweep is advisory (never blocks publish), untagged drafts
  are rebound automatically, tag input is normalized
- release: bump-main gates in shell so CLI-dispatched boolean inputs cannot
  silently skip the post-release main bump
- update-homebrew: defer inside PyPI's 24h --uploaded-prior-to window and
  add a nightly catch-up that no-ops when the formula is current
- uv.lock: gitpython 3.1.50 -> 3.1.55 (clears 8 OSV advisories that tripped
  the Security Scan on every lock-touching PR)

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* fix(images): serialize image builds and raise the build timeout to 120m

At the v0.7.0 cut the rc1 (21:51) and final (21:57) tag builds ran
concurrently under SHA-keyed concurrency, raced each other's layer cache
cold, and the final build died on the 60m job timeout — no v0.7.0 or
latest images until a manual re-run a day later. A single serialized
group lets the later build reuse the earlier one's layers; 120m gives a
genuinely cold build headroom.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-28 14:04:50 -07:00
Zeyi (Rice) Fan 239e9cd36b chore(pnpm): migrate editors/vscode and deploy/cloudflare to the root workspace (#3390)
N/A

This is the final npm -> pnpm migration step for the OSS repo.

- Adds `editors/vscode` and `deploy/cloudflare` to `pnpm-workspace.yaml` so
  they use the root `packageManager: pnpm@11.15.1` and the shared
  `pnpm-lock.yaml`.
- Removes the per-package `package-lock.json` files and deletes the now-obsolete
  `scripts/normalize_package_lock_registry.py` hook/script.
- Merges the three remaining categories of build-script approvals into
  `pnpm-workspace.yaml` (`@vscode/vsce-sign`, `esbuild`, `keytar`, `sharp`,
  `workerd`) so `pnpm install` works at the workspace root.
- Migrates VS Code and release workflows to `setup-pnpm`:
  - `.github/workflows/vscode-extension-release.yml`
  - `.github/workflows/vscode-release-pr.yml`
  - `.github/workflows/release-omnigent.yml`
- Updates the lockfile regen workflows to refresh `pnpm-lock.yaml` instead of
  the old web-only `package-lock.json`:
  - `.github/workflows/oss-regenerate-and-smoke.yml`
  - `.github/workflows/oss-regen-on-comment.yml`
- Updates `editors/vscode/README.md`, `editors/vscode/PUBLISHING.md`, and
  `deploy/cloudflare/README.md` to reference pnpm commands.
- Removes the deprecated `.github/actions/setup-node` composite action.

- `pnpm install --frozen-lockfile --filter omnigent-vscode` passes locally.
- `pnpm install --frozen-lockfile --filter omnigent-cloudflare` passes locally.
- `uv run pre-commit run --all-files` passes (after dropping the package-lock
  registry hook).
- Inspected remaining `npm install` occurrences in workflows; the only survivors
  are transient agent CLI installs (`@anthropic-ai/claude-code`,
  `@openai/codex`) that are intentionally not tracked in the lockfile.

N/A

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

Verified the new workspace packages install from the frozen pnpm lockfile and
that the pnpm-only lockfile regen scripts produce a valid lock. The VS Code
workflow commands were checked against the package names/filters from
`pnpm-workspace.yaml`.

N/A

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-28 13:44:52 -07:00
Thomas Garnier eeb750c85e feat(sandbox): bind /proc in bwrap on Lakebox hosts (#3258)
The linux_bwrap sandbox mounts a fresh procfs under --unshare-pid, but a
Lakebox microVM masks /proc so that mount returns EPERM and the sandbox
fails to start. That blocked linux_bwrap — and the L7 egress management
built on top of it — on the Lakebox backend.

Bind the existing /proc instead of mounting a fresh one, but only on
outer sandbox backends known to be safe for it (allow-list: lakebox).
The backend is read from OMNIGENT_HOST_SANDBOX_BACKEND when set, else
autodetected via the /run/lakebox marker. Everywhere else the fresh-proc
mount and its fail-closed behavior stay unchanged.

Binding /proc exposes the outer process list and world-readable per-proc
files (cmdline/comm/stat/status). The retained user namespace still
blocks ptrace-gated files (environ/mem/maps/fd) and --unshare-pid still
contains signalling, so the leak is acceptable on a single-tenant
Lakebox microVM.

Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
2026-07-28 12:11:53 -07:00
Andrew Peltekci fa11e1ccf7 fix(kimi): report terminal status so a parent orchestrator is woken (#3166)
The kimi forwarder mirrored wire content but never posted an
external_session_status edge — the only native forwarder that didn't
(claude/codex/opencode/cursor all do). A kimi sub-agent therefore finished,
delivered its answer to the transcript, and left the parent waiting on it
forever: _mark_subagent_terminal_and_wake was never reached, so no result
ever landed in the parent's inbox.

kimi's wire has no turn.end row; its agent loop steps while step.end carries
finishReason 'tool_use' and stops on 'end_turn' (1:1 with turn.prompt across
every recorded session). Map that edge to external_session_status: idle,
carrying the turn's final assistant text — the runner delivers an empty
result when an idle edge forwards none.

Signed-off-by: apeltekci <andrew@peltekci.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:03:08 +00:00
Pat Sukprasert c41d40454e feat(harness): add NativeHarnessProvider seam foundation (PR 1.1) (#3239)
* feat(harness): add NativeHarnessProvider seam foundation (PR 1.1)

First, additive step of Phase 1 of the modular native-harness registry
(designs/harness-modular-registry-proposal.md). Introduces the behavior
side-channel that later PRs will dispatch through; no hub is rewired yet, so
this changes no runtime behavior.

- Add `NativeHarnessProvider` (frozen dataclass of dotted import-path strings
  for a native harness's lifecycle hooks) and the `native_providers` field on
  `HarnessContribution`, plus `native_providers()` / `native_provider_for_key()`
  accessors.
- Populate 11 built-in provider rows uniformly from the `omnigent.<key>_native`
  module layout (`run_<key>_native`, `_materialize_<key>_agent_spec`, and the
  `_auto_create_<key>_terminal` builder re-exported from `omnigent.runner.native`).
  Hooks that are still runner closures / inline dispatch (interrupt, stop,
  spawn-env, bridge-dir) stay None until those hubs migrate onto the seam.
- Add `omnigent/native_dispatch.py`: a lazy, per-path-cached resolver over the
  existing `load_object`, with `resolve` / `resolve_hook` / `resolve_hook_for_key`
  so hubs resolve a hook instead of branching on `key == "<x>"`. Import hygiene
  preserved — provider rows hold strings; only the resolver imports the target
  modules, and only at dispatch time.
- Tests: provider rows cover every native agent 1:1, required hooks are set, and
  every populated built-in path actually resolves to a callable (guards against
  a typo'd path or renamed symbol); resolver colon/dot forms, caching, and
  unset-hook / unknown-key None paths.

The validator still rejects community native metadata (Phase 2 flips it).

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* docs(harness): add implementation-progress ledger (PR 1.1)

Add an append-only "Implementation progress" ledger to the modular-registry
proposal so each PR in the stack records its own status without editing the
plan tables (which would conflict across the 1.1→1.2→1.3 stack on every
rebase). Seed it with 1.1 (#3239, in review).

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-28 14:31:06 +00:00
Pat Sukprasert c7d7cedb91 [runner] Preserve sub-agent wake attribution (#3409)
* fix: preserve sub-agent wake attribution

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix: harden runner event attribution

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix: retry child dispatch without stale attribution

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix: validate subagent send before actor lookup

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: expect forwarded created_by field

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: avoid escape closing codex config modal

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-28 14:28:17 +00:00
Pat Sukprasert 341652d6f8 [lint] Block hardcoded model pins (#3425)
* 🔧 chore(lint): Block hardcoded model pins

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* 🔧 chore(lint): Tighten model baseline guard

- Reject duplicate path/model rows so baseline allowances cannot silently accumulate.
- Document heuristic false-negative and multiline-config gaps, plus the bounded full-scan tradeoff.
- Add focused coverage for duplicate baseline validation.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

* 🔧 chore(lint): Guard model scan configuration

- Cross-check the pre-commit trigger against the scanner's tracked roots, extensions, exclusions, and allowlist path to prevent silent drift.
- Share the source-extension set across path discovery and scanning.
- Report malformed allowlist counts with consistent path and line context; cover both review cases with focused tests.

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-28 14:22:40 +00:00
Pat Sukprasert e093f56d82 fix(codex): persist permission mode across host resume (#3411)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-28 13:25:11 +00:00
Tomu Hirata 627c8ee59e fix(ui): sub-agent sessions never show reconnect modal when runner dies (#3414)
* fix(ui): sub-agent sessions never show reconnect modal when runner dies

A sub-agent session with a dead runner classified as local_stranded,
which disabled the composer and showed the CLI reconnect modal — a
flow designed for top-level host-bound sessions. Sub-agents have no
host binding and can't be relaunched from a CLI command; they recover
via their parent's live runner (server-side heal, #3151).

- Add kind field ("default" | "sub_agent") to Session type and
  map it from the wire in sessionFromWire
- Thread kind through LivenessRow and livenessRowFromSession
- Add row 7a in useSessionLiveness: sub_agent with dead runner →
  runner_asleep (composer open) instead of local_stranded

Fixes #3413

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

# Conflicts:
#	web/src/hooks/useSessionLiveness.ts

* fixup: add kind and backgroundTaskCount to sessionsApi test fixture

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* test(e2e_ui): sub-agent dead runner keeps composer open, no reconnect modal

Regression test for #3413: a sub-agent session with a dead runner was
classified as local_stranded, showing the CLI reconnect modal and
disabling the composer. After the fix (kind=="sub_agent" → runner_asleep)
the composer stays enabled and the "Agent disconnected" banner is absent.

Creates a real child session (parent_session_id set → kind="sub_agent"),
patches the browser's health poll to report runner offline, and asserts
the composer is usable and no reconnect banner appears.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: splice kind from session snapshot into livenessRow when sidebar conv present

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: expose kind in SessionResponse so the UI can detect sub_agent sessions

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: don't re-initialize session on heal — parent runner already hosts the child

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: re-init session for native sub-agents, skip for SDK sub-agents

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: regenerate openapi.json for kind field in SessionResponse

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: update heal docstring + add SDK sub-agent no-init test

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-28 21:53:08 +09:00
Tomu Hirata d16ad3c7c0 fix(server): heal sub-agent stale runner_id on direct message-send (#3151)
* 🐛 fix(server): heal sub-agent stale runner_id on message-send

A sub-agent copies its parent's runner_id at creation and is never
repointed when the parent's runner is relaunched. The message-send path
returned a permanent 503 for any sub-agent whose runner had
idle-timed-out, even while the parent's replacement runner was healthy
(host_id is None short-circuits all existing relaunch paths).

- Extract _heal_subagent_runner_binding_via_parent from
  _recover_subagent_status_forward_via_parent: walks the ancestor chain
  (immediate parent → root), waits for the live runner tunnel, calls
  replace_runner_id on the child, returns the live client
- Wire the heal into the message-send path after the managed-launch
  rendezvous, guarded to kind=="sub_agent"; sets
  _runner_needs_session_init=True so the child's harness is initialized
  on the healed runner before dispatch
- Refactor _recover_subagent_status_forward_via_parent to delegate
  binding repair to the shared helper (no behavior change for the
  status-forward path)
- Add regression tests: heal succeeds, no-live-ancestor preserves 503,
  top-level sessions not treated as recoverable children

Fixes #3067

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

# Conflicts:
#	omnigent/server/routes/sessions.py

* fixup: rebase onto main, apply heal to routes_events.py, fix lint

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fixup: fix test payload format and monkeypatch targets for routes_events

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-28 18:20:34 +09:00
Pat Sukprasert b7ab0ba548 test: stabilize codex model metadata e2e (#3410)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-28 16:07:25 +07:00
Pat Sukprasert fe55ad2cf2 Import OpenClaw acpx agents during setup (#3354)
* Import OpenClaw acpx agents during setup

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

*  feat(cli): Add one-shot OpenClaw launch

- Resolve one registered agent into a temporary ACP launcher
- Keep user config unchanged and fail clearly on unknown agents

Refs #3351

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* 🐛 fix(openclaw): Harden config bridge imports

- Parse wrapped configs with a real JSON5 implementation
- Deduplicate mirrored registries and preserve slug collisions
- Reject malformed ephemeral ACP payloads with clear errors

Refs #3351

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* 🐛 fix(openclaw): Handle invalid config sources

- Treat filesystem and parser recursion failures as soft discovery errors
- Preserve valid sibling agents when one entry has malformed args
- Quote executable paths so ACP argv parsing handles spaces

Refs #3351

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

*  feat(openclaw): Let users choose import source

Always show the OpenClaw import action during setup, offer detected registries or a user-selected file, and reject unrelated files without changing Omnigent config.

Refs #3351

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* 🐛 fix(openclaw): Unify registry parsing

Parse both acpx and wrapped OpenClaw registries as JSON5 regardless of discovery path, and document why the setup status-width floor must follow available terminal space.

Refs #3351

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-28 08:48:19 +00:00
Pat Sukprasert 624411a334 fix(ci): install CLIs under RUNNER_TEMP so a repo-root package.json can't hoist them (#3412)
The AI-agent workflows install the Claude Code / Codex CLIs with a bare
`npm install` after `cd`-ing into a workspace subdir (`.cc-cli` / `.codex-cli`)
that has no package.json of its own. npm then walks up to the nearest ancestor
package.json to resolve the project root.

Once a repo-root package.json was added, that ancestor became the repo root, so
the install landed in `${GITHUB_WORKSPACE}/node_modules` instead of the subdir.
The follow-up `node node_modules/@anthropic-ai/claude-code/install.cjs` (run from
the empty subdir) then failed with MODULE_NOT_FOUND, breaking Polly review,
issue/security triage, doc-sync, and the run-omnigent-agent action. The
`added 2 packages` line (claude-code has zero deps) was the tell that npm had
reconciled the root tree rather than an isolated install.

Install into `${RUNNER_TEMP}/omnigent-{cc,codex}-cli` instead — outside the
checked-out tree, so no ancestor package.json can ever capture the install. This
matches the pattern e2e-ui.yml and flake-stress-ui.yml already use.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-28 15:19:49 +07:00
Tomu Hirata d0450f8d7e ci: bump @anthropic-ai/claude-code 2.1.170→2.1.212 (#3404)
* ci: bump @anthropic-ai/claude-code 2.1.170→2.1.212

v2.1.170 has a corrupted npm cache entry on GitHub Actions runners
causing install.cjs to be missing after `npm install`. Bumping to the
current stable (2.1.212) forces a fresh fetch and clears the bad entry.

Also bumps the ci-deps/package.json pin (was 2.1.163) and the
run-omnigent-agent action default to keep everything consistent.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* ci: update pnpm-lock.yaml for claude-code 2.1.212

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-28 16:47:38 +09:00
Tomu Hirata 2cff2cea11 fix(codex-native): share plugins/cache into per-session homes (#3401)
Codex materializes its versioned plugin store (openai-curated templates,
browser, presentations, ...) into $CODEX_HOME/plugins/cache on session
start. Because codex-native points CODEX_HOME at a private per-session
home, codex re-materializes ~44 MB of identical plugin data into every
session — the dominant on-disk cost once the upstream logs_2.sqlite TRACE
bloat (openai/codex#28224) is fixed in codex >= 0.142.0.

Symlink plugins/cache from the shared source home into each private home,
mirroring the existing skills-symlink pattern. The cache is content-
addressed read-only reference data (verified byte-identical to the shared
copy), so unlike config.toml it needs no per-session isolation. Skipped in
minimal (title-sidecar) mode, which runs no plugins. Best-effort: a symlink
failure logs and lets codex repopulate its own copy.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-28 16:04:26 +09:00
Pat Sukprasert 23465441d5 feat(setup): Add Antigravity sign-in (#3391)
- Launch bare agy for Google OAuth and verify with agy models\n- Keep Gemini API-key setup available alongside native sign-in

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-28 13:44:36 +07:00
Zeyi (Rice) Fan 31183fbe92 chore(pnpm): approve build scripts for ci-deps dependencies (#3386)
Running a full workspace install without filters complained about ignored
build scripts for @anthropic-ai/claude-code, @google/genai, and protobufjs.
These come from the .github/ci-deps package and are legitimate; approving
them lets Scope: all 4 workspace projects
Already up to date
Done in 194ms using pnpm v11.15.1 / undefined
[ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL] Command "dev" not found at the workspace root run scripts
instead of erroring.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-27 17:37:55 -07:00
Sabhya Chhabria 3e139dab57 fix(claude-native): never launch a bare family alias a gateway rejects (#3378)
* fix(claude-native): never launch a bare family alias a gateway rejects

A family alias (opus/sonnet/haiku/fable) selected on a provider config
whose tier has no ANTHROPIC_DEFAULT_*_MODEL pin is canonicalized by
Claude Code to an Anthropic id (e.g. claude-opus-4-8) that gateways
404, failing session start with "There's an issue with the selected
model". Resolve unpinned aliases to the provider's default model in
resolve_claude_native_model_selection, which launch, sticky handoff,
and /model injection all route through.

Also stop offering the static subscription alias rows to provider
configs with no pins: the picker now lists the one model the config is
known to route.

Co-authored-by: Isaac
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

* refactor: trim the unpinned-alias fix to its minimal form

Shorten the resolver docstring and the pin-less catalog fallback, drop
e2e assertions already implied by the single-row count, and fold the
three alias-passthrough regression tests into one.

Co-authored-by: Isaac
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

* fix(claude-native): scope alias remap to endpoints that reject canonical ids

Review feedback on the unpinned-alias guard:

- Only rewrite an unpinned family alias when the config routes through a
  gateway/Bedrock endpoint; the Anthropic API (api.anthropic.com or no
  endpoint override) resolves aliases natively, so API-key providers keep
  their alias routing and the static picker catalog.
- Respect managed-settings tier pins: Claude Code applies them to the
  spawned process, so a managed pin means the alias still routes.
- The runner's /model handler now resolves the session launch config
  instead of reading the in-memory cache, so alias resolution survives a
  runner restart (cold cache previously skipped the remap).

Co-authored-by: Isaac
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

---------

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
2026-07-27 17:29:07 -07:00
Zeyi (Rice) Fan 355556dff4 chore(ci): migrate .github/ci-deps to pnpm and update docs for pnpm dev workflow (#3379)
- Add .github/ci-deps to the root pnpm workspace so it uses the shared
  pnpm lockfile and install machinery.
- Regenerate pnpm-lock.yaml entries for the e2e-ci-deps package.
- Replace npm install --ignore-scripts in ci.yml and flake-stress-e2e.yml with
  pnpm install --frozen-lockfile --ignore-scripts --filter e2e-ci-deps.
- Update electron-build.yml to use setup-pnpm and filter installs for web and
  web/electron.
- Update omnidev source so the local dev supervisor installs and runs Vite
  with pnpm.
- Update developer docs (README.md, CONTRIBUTING.md, web/README.md,
  web/electron/README.md, dev/omnidev/README.md, tests/e2e_ui visual/README.md
  and COVERAGE_GAPS.md) to reference pnpm commands.
- Add a minimal root package.json with packageManager: pnpm@11.15.1 and remove
  the explicit version from .github/actions/setup-pnpm so CI uses the same
  source of truth.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-27 16:58:33 -07:00
Elliot Sun 38cc498b75 fix(onboarding): detect agy settings.json as login fallback on macOS (#3289)
* fix(onboarding): detect agy settings.json as login fallback on macOS

On macOS, agy 1.1.7+ stores OAuth credentials in Keychain and writes
only ~/.gemini/antigravity-cli/settings.json (no oauth_creds.json).
The existing gemini_auth_has_credential() missed this and falsely
reported 'harness antigravity-native is not configured'.

Accept the existence of settings.json as a fallback signal when no
token files are found. This is safe because the caller
(resolve_native_antigravity_launch) uses it only for an informational
warning — agy always re-drives OAuth on first run regardless.

- Update gemini_auth_has_credential() with settings.json fallback
- Update docstrings to document the third detection path
- Update warning message in antigravity_native_launch.py
- Add unit test for settings.json-only detection
- Fix _GEMINI_DIR isolation in existing test

Signed-off-by: ElliotSun <elros1109@gmail.com>

* fix(onboarding): prove agy login via CLI, not settings.json existence

The macOS lockout this fixes is real: agy 1.1.7+ keeps OAuth in the
Keychain and writes no token file, so the file-only check reported
antigravity-native as unconfigured and connect.py refused to spawn a
runner for a user who was in fact signed in.

Accepting the bare existence of ~/.gemini/antigravity-cli/settings.json
as the fallback signal does not work, because omnigent creates that file
itself: the CLI launch path calls ensure_agy_feedback_survey_disabled
under the real home before agy starts, and build_agy_launch emits no HOME
override. One `omni antigravity` run therefore satisfied the credential
gate forever, on every platform — turning a hard launch gate into a
no-op and letting a runner spawn that dies on its first turn. That is
worst on headless hosts, where agy's OAuth prompt has no TTY.

Ask the CLI instead. `agy models` exits 0 only when signed in and reads
the credential wherever agy stored it, Keychain included, so nothing
omnigent writes can satisfy it. This mirrors ambient._claude_login_detected,
which already solves the identical Keychain split for Claude Code, and
reuses the probe harness_install already wires as the gemini family's
status command.

The fallback is gated on macOS: Linux writes a real token file, so its
absence is a true negative there and the fallback would only add a
subprocess while weakening a signal that works. Failures — missing
binary, non-zero exit, timeout, unreadable home — all read as False,
because readiness must never raise.

Content inspection of settings.json was the alternative considered. It
was rejected as unverifiable from here: no key in that file is known to
mark a completed sign-in on 1.1.7, so keying on one risks reintroducing
the very lockout being fixed.

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

* docs(skills): note agy's macOS Keychain credential in the e2e pre-flight

The pre-flight tells the reader agy's token lives under ~/.gemini, which
leaves a Mac developer on agy 1.1.7+ hunting for a file that is never
written. Name the Keychain case and the `agy models` fallback that
gemini_login_detected() now uses there.

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

---------

Signed-off-by: ElliotSun <elros1109@gmail.com>
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com>
2026-07-27 16:49:59 -07:00
samarmstrong 08056475b0 fix(cursor-native): auto-accept lingering tool gates under --yolo (#2338)
* fix(cursor-native): auto-accept lingering tool gates under --yolo

cursor-agent's Run Everything mode still sometimes leaves pendingToolCall
markers long enough for Omnigent to mirror ApprovalCards and stall a
piloted parent. When the session launched with --yolo/--force/-f, accept
those tool gates in-pane instead of parking a web card; AskQuestion still
surfaces as deliberate human input.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Sam Armstrong <armstrongflg@gmail.com>

* fix(cursor-native): satisfy ruff format and PIE810 on yolo args

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Sam Armstrong <armstrongflg@gmail.com>

* fix(cursor-native): make yolo auto-accept bounded and fail-closed

Auto-answering a tool-approval gate is a safety boundary, so the accept path
now refuses to act on anything it cannot confirm, and always has a way out.

The accept was previously a blind keystroke loop: it never checked that a
prompt was on screen, recorded a send to a dead pane as a success, and had no
attempt cap or fallback. A gate that `y` does not clear therefore degraded from
a visible stall into a literal `y` typed into cursor's composer every two
seconds for the life of the session, with no card ever surfaced.

The accept key now goes out only while `capture_cursor_pane` shows cursor's
parenthesised accept hint, at most three times, and at most once per poll pass
(cursor renders one prompt at a time). A dead pane, a send tmux rejects, or a
gate still pending after the budget all fall back to the same ApprovalCard the
non-yolo path shows, so the worst case is the visible stall we have today.
Because a call accepted this way is never seen by a human, the INFO line now
carries an argument preview: it is the only record Omnigent approved the call.

`cursor_launch_args_enable_yolo` was failing open in the same spirit —
`--yolo=false` and `--force=false` both read as enabled, because only the
presence of the `=` form was checked. Explicit off-values are now honoured, and
a bare `--` ends the flag scan so a `-f` in the prompt text that follows is
text rather than a request to bypass approvals.

Tests cover the bounded retry, the fallback to a card, an idle pane, a dead
pane, an undelivered keystroke, an explicit non-yolo session, and the
off-value / end-of-flags argv cases. The design doc gains a section on the
fail-closed contract and drops its claim that Omnigent never sends a verdict of
its own initiative; its stale `Code:` pointer at the runner wiring is refreshed
to where that wiring now lives.

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

* fix(cursor-native): re-apply yolo wiring where auto-create now lives

`_auto_create_cursor_terminal` moved out of `omnigent/runner/app.py` into
`omnigent/runner/native/orchestration.py`, which left `app.py` a re-export
shell and this branch's wiring hunk applying to code that no longer runs.
Derive `auto_accept_approvals` from `launch_config.terminal_launch_args` at the
live call site instead.

This kwarg is the only thing that turns the in-pane auto-accept on, and it is
one line inside a large function, so a future move can drop it and leave the
feature inert with the whole suite green. Pin it: the auto-create harness now
captures the elicitation supervisor's kwargs, and a parametrized test asserts
the derived stance for `--yolo`, `--force`, `-f`, `--yolo=false`,
`--auto-review`, and no args. Deleting the kwarg fails all six.

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

---------

Signed-off-by: Sam Armstrong <armstrongflg@gmail.com>
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com>
2026-07-27 16:48:10 -07:00
Yi Lyu 2f39f04e1f fix(codex-native): surface launch routing in the thread-startup-timeout error (#2745) (#2843)
* fix(codex-native): surface launch routing in the thread-startup-timeout error (#2745)

Signed-off-by: Yi Lyu <isabellalyu1130@gmail.com>

* Fix checks

Signed-off-by: Yi Lyu <isabellalyu1130@gmail.com>

---------

Signed-off-by: Yi Lyu <isabellalyu1130@gmail.com>
2026-07-27 23:42:24 +00:00
Zeyi (Rice) Fan 7367654b47 fix(android): resolve adb from SDK dir in runDebug task (#3376)
The runDebug, listDevices, and reverseProxy Exec tasks called
`commandLine("adb", ...)`, relying on adb being on PATH. The Gradle
daemon is long-lived and may have been started from an environment
whose PATH doesn't include platform-tools (e.g. homebrew's
android-commandlinetools), so the spawn fails with
"A problem occurred starting process 'command 'adb''" — even though
AGP's own installDebug succeeds because it resolves adb from the
SDK directory internally.

Resolve adb from android.sdkDirectory instead, mirroring AGP, so
the custom launch tasks are independent of the daemon's PATH.
2026-07-27 23:30:24 +00:00
Harry Su 4c3fcdb4f6 docs(DBSPEC): remove stale DBOS/tasks references (#2329)
* docs(DBSPEC): remove stale DBOS/tasks references

The tasks table and DBOS were removed (migration
b9c1d2e3f4a5_drop_tasks_table), but DBSPEC.md still described the
old DBOS-backed workflow design: the tasks table schema, the
try_deliver/close_inbox steering handshake, and the TaskStore
method mapping. Updated the doc to match current state — turn
state now lives in-memory in the runner (_active_turns,
_session_message_buffers), and conversation_items.response_id is
just an app-generated grouping id with no backing table.

Also added the created_by column to conversation_items, which
existed in code but was missing from the doc.

Signed-off-by: Harry Su <tiecheng.su@robinhood.com>

* docs(DBSPEC): correct FK section — no DB-enforced FKs, cleanup is explicit app code

Addresses the blocking review: the previous revision claimed an ON DELETE
CASCADE FK on conversation_items.conversation_id, but
p1a2b3c4d5e6_remove_all_fks dropped every FK (Rule R032) and
delete_conversation cleans up children before parent explicitly. Also
precision-fix response_id as harness- or app-generated per review.

Signed-off-by: Harry Su <tiecheng.su@robinhood.com>

* docs(DBSPEC): correct table count, deletion order, and position allocator

The accuracy pass left five claims that don't match the code:

- The opening line said four tables in the default schema. There are 17
  in `db_models.py`, and none sets an explicit schema — the same doc names
  labels, comments, and policies as tables a hundred lines later. Scope the
  sentence to the four tables this doc covers and point at the models as the
  full list.
- `delete_conversation` was described as deleting comments and policies
  before the conversation rows. It uses two transactions: the AP one drops
  FTS rows, items, labels, and the conversation rows; a second best-effort
  transaction then cleans up comments, policies, session permissions,
  conversation metadata, and session-scoped agents *after* the conversation
  is gone. The doc also omitted three of those tables and hid the
  best-effort tradeoff the method's own docstring calls out.
- "Turn state is not persisted to this schema at all" was overstated. The
  authoritative state is in-memory, but `persist_live_status` mirrors
  `live_status` / `pending_elicitation_count` onto
  `omnigent_conversation_metadata` so any replica can render session status.
- The "Delete agent" row documented cancelling in-flight turns for the
  agent's live sessions. No such mechanism exists: `AgentStore.delete` is a
  bare row delete with no production caller and no HTTP route, and
  session-scoped agent rows are removed by `delete_conversation`.
- The position allocator no longer runs `SELECT MAX(position) + 1`.
  `append()` reads and advances the `conversations.next_position` counter
  under `_lock_conversation`, keeping allocation O(1); the `MAX(position)`
  scan survives only as a one-time backfill for pre-counter conversations.

Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>

---------

Signed-off-by: Harry Su <tiecheng.su@robinhood.com>
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com>
2026-07-27 16:24:17 -07:00
Zeyi (Rice) Fan dc97ade9f5 chore(web): migrate web and electron to root pnpm workspace (#3328)
- Define a root pnpm-workspace.yaml with web/ and web/electron/ packages.
- Move npm overrides from web/package.json into workspace overrides, using a
  shared catalog: for react, react-dom, and shiki.
- Preserve 7-day dependency cooldown via settings.minimumReleaseAge: 10080.
- Delete web/package-lock.json and web/electron/package-lock.json; add the
  generated root pnpm-lock.yaml.
- Update web/electron/package.json scripts to use pnpm --filter web run build:overlay.
- Remove web/.npmrc and web/electron/.npmrc; no committed .npmrc (CI forces the
  public registry via env var).
- Add .github/actions/setup-pnpm so all workflows can share a pinned pnpm
  11.15.1 + Node setup.
- Convert lint.yml and web-tests.yml to pnpm; update ui-snapshot and e2e-ui
  workflows.
- Update the web-prettier pre-commit hook to run web/node_modules/.bin/prettier
  directly when present.
- Update justfile to prefer pnpm for Electron recipes and lockfile normalization.
- Ensure remaining npm-based workflows (editors/vscode/, .github/ci-deps/,
  deploy/cloudflare/) are untouched and continue to work.
- Add pdfjs-dist worker URL import so Vite emits the worker asset under pnpm's
  hoisted node_modules layout.
- Force shiki and its first-party packages into a single build chunk to avoid a
  Cyclic top-level import that produced a 'flatMap' runtime error in Monaco.
- Pin build-tool versions to the legacy npm lockfile (vite 8.1.0, tailwindcss
  4.3.1, jiti 2.7.0, lightningcss 1.32.0, postcss 8.5.15) so bundler behavior
  stays consistent with the pre-migration builds.
- Update tests/e2e_ui/test_pwa_build.py to omit the now-incorrect -- separator
  when forwarding --outDir to pnpm run build:embed.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-27 16:22:30 -07:00
Edwin He 1f66a0914b fix(native): apply routed model with the message, not a racing event (#3257)
On a claude-native session with intelligent routing on, the routed model
was selected but the user's first message was silently dropped — the model
switched, no error surfaced, but no turn ran.

The server issued TWO unsynchronized writes to the same tmux pane: a
standalone model_change event (which typed /model <routed> into the pane)
AND, separately, the user's message (typed in via inject_user_message).
These raced. The message keystrokes landed mid-switch, inject_user_message
never saw its draft, hit its submit-blind fallback, and returned without
error. Model applied, message gone.

Fix: remove the second writer by folding the switch into the message turn,
mirroring how the SDK/pi path already applies the routed model as one
operation.
- Executor (ClaudeNativeExecutor.run_turn): the routed model already
  arrives in ExecutorConfig.model and was being discarded. It is now
  applied: when config.model differs from the pane's model, type /model
  then inject the message — both under the existing _inject_lock, in
  order, exactly once. inject_user_message's prompt-ready gate + verified
  submit then guarantee delivery. _applied_model is seeded lazily from
  read_launch_model so turn 1's routed pick is compared against the spawn
  model rather than blindly re-issued.
- Server (_sessions/orchestration.py): the routed model rides in-band on
  the message (model_override, an extra field the harness MessageEvent
  forwards into ExecutorConfig.model), and the separate racing model_change
  POST is dropped. The manual composer /model picker path (PATCH ->
  model_change) is untouched.

Adds three executor tests: /model precedes the message in order under one
lock; no /model without a routed model; no /model when already on the
routed model. The ordering test fails against the prior discard-config
behavior.

Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
2026-07-27 23:22:15 +00:00
omnigent-ci[bot] 326bd5939f Bump version to 0.8.0.dev0 (#3377)
* Bump version to 0.8.0.dev0

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(release): keep uv.lock at main's shape, stamp workspace versions only

The bump workflow's full relock rewrites every entry with new-uv metadata
churn; restoring main's lock and stamping just the workspace versions keeps
the PR reviewable. Workspace package blocks verified identical to the
relocked version.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-27 23:20:42 +00:00
Sabhya Chhabria 19ca227bc7 feat(polly): launch supported children in goal mode (#3362)
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
2026-07-27 16:00:19 -07:00
SatoTaiga 50aa69420b fix(codex): attribute per-model usage for turns with no pinned model (#3287)
* fix(codex): attribute per-model usage for turns with no pinned model

codex_executor's TurnComplete.usage never carried a "model" field, unlike
every other relay executor (claude-sdk, cursor, copilot, openai-agents,
pi). For a codex-harness agent that pins no llm.model (e.g. Debby's
gpt head, which deliberately defers to the harness/provider default),
_accumulate_session_usage's model-resolution fallback chain had nothing
to resolve to, so the turn's flat token/cost totals still accumulated
but session_usage.by_model silently never got an entry for it.

Stamp the turn's resolved model (already in scope as run_turn's `model`
argument) onto the usage dict extracted from tokenUsage/updated, mirroring
claude_sdk_executor's observed_model pattern.

Signed-off-by: Sato Taiga <antfgxgres@gmail.com>

* test(sessions): add regression test for codex per-model usage attribution

Exercises the real _accumulate_session_usage and GET /v1/sessions/{id}
API against a codex-harness agent with no pinned llm.model (Debby's gpt
head's exact shape): a usage delta with no "model" key still accumulates
the flat total but leaves by_model empty (the bug), while one carrying
"model" (as codex_executor.py now stamps it) gets a by_model entry that
also surfaces through the session snapshot the web UI's cost panel reads.

Signed-off-by: Sato Taiga <antfgxgres@gmail.com>

---------

Signed-off-by: Sato Taiga <antfgxgres@gmail.com>
2026-07-27 15:38:08 -07:00
omnigent-ci[bot] 40ad8b73ee docs(changelog): record v0.7.0 (#3373)
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-27 15:12:23 -07:00
Zeyi (Rice) Fan 92b1e10e53 feat(onboarding): enforce supported CLI version ranges for native harnesses (#3335)
N/A

- Added `min_version` and `max_version_exclusive` to `HarnessInstallSpec` and made `harness_cli_installed` probe `--version` when bounds are declared, so setup and dispatch fail loud for outdated CLIs.
- Implemented generic `--version` parsing + PEP 440 comparison with date-version normalization so Cursor and Hermes calendar-version strings compare correctly.
- Wired code- and changelog-derived version floors for all CLI-backed native harnesses (e.g. Claude >=2.1.161, Codex >=0.137.0, Cursor >=2026.06.02, Kimi >=1.47.0, Hermes >=2026.06.05).
- Updated the CLI setup overview and install prompt to show "Needs upgrade" and the detected/declared versions instead of claiming a present-but-outdated CLI is "not installed".
- Added the `version-too-low` readiness reason and surfaced it in the web UI badge/notice; also made Cursor native auth-aware so it now reports `needs-auth` when installed but not logged in.
- Fixed the readiness-layer lookup so `version-too-low` correctly surfaces for all native harnesses that declare a version floor (Claude, Cursor, OpenCode, Kiro, etc.) instead of falling back to `binary-missing`.
- Preserved the existing `antigravity-native` credential gate: an installed `agy` CLI without a stored Gemini credential still reports not-ready.
- Added E2E UI coverage for the new `version-too-low` warning and updated readiness unit tests for version-bound and credential-bound behavior.

```bash
uv run pytest tests/onboarding/test_harness_install.py \
              tests/onboarding/test_harness_readiness.py \
              tests/cli/test_configure_models.py \
              tests/test_codex_native.py -q

npm run --silent test -- --run src/lib/harnessSetup.test.ts src/shell/NewChatDialog.test.tsx
```

N/A — the change is mostly backend/UX copy; no new visual components.

- [x] Feature
- [ ] Bug fix
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [x] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

Manual verification: ran targeted backend/web test suites after each change and confirmed `omnigent setup`/`harness_cli_installed` now report “installed (vX) but not supported” rather than “missing” for outdated CLIs.

Native harness setup now checks the installed CLI version and prompts to upgrade if it is too old.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-27 14:22:06 -07:00
David O'Keeffe 7048f7a38b fix(hermes): introspect state.db schema to survive cross-version column drift (#2774)
Signed-off-by: David O'Keeffe <dgokeeffe@users.noreply.github.com>
Co-authored-by: David O'Keeffe <dgokeeffe@users.noreply.github.com>
2026-07-27 20:55:17 +00:00
nhsdb 2c5d50b68b egress proxy: trust loose capath CAs, not just the cafile bundle (#3264)
The MITM egress proxy verifies upstream TLS against the system trust
store built by _system_ca_bundle(). It read only the consolidated
cafile (get_default_verify_paths().cafile/openssl_cafile) and ignored
the capath directory. Corporate MDM / IT-managed roots are commonly
installed as loose files under capath (with hashed symlinks) rather than
merged into the cafile, so they were missing from the proxy's trust
store. Any upstream host whose chain relies on such a root then failed
verification (e.g. a corp-intercepted github.com returned 502 from the
proxy) even though the host's own tools trusted it.

Read capath too: concatenate the loose PEM certs from the capath
directory onto the cafile bundle (dedup by resolved path, skip non-PEM
entries), keeping the certifi fallback when neither yields any certs.

Added tests: a CA present only as a loose capath file lands in the
bundle, and non-PEM files in capath are skipped.

Signed-off-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
Co-authored-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
2026-07-27 13:34:51 -07:00
nhsdb 3d58649e77 bwrap sandbox: bind /etc/alternatives so update-alternatives tools resolve (#3263)
Tools invoked by generic name (awk, python3, editor, pager, ...) resolve
through /usr/bin/<name> -> /etc/alternatives/<name> -> real binary. The real
binaries already live under the mounted /usr, but /etc/alternatives was not
bound, so the intermediate symlink node was missing inside the jail and the
lookup failed with 'command not found'.

Bind /etc/alternatives read-only in the default _DEFAULT_ETC_DIRS list,
alongside the existing /etc/ssl and /etc/ca-certificates dir binds. It is a
directory of symlinks (no secrets); read-only means the mapping cannot be
repointed, and every target is a binary already exposed under /usr, so this
grants no new capability -- it only restores standard name resolution.

Linux (bwrap) backend only; darwin_seatbelt is unaffected by this mechanism.

Signed-off-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
Co-authored-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
2026-07-27 20:29:27 +00:00
Anthony Ivan 638df430be fix(codex-native): keep task plans out of chat (#3249)
CI / gate (push) Failing after 1s
Doc sync / Classify and draft docs (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
2026-07-27 10:57:57 -07:00
Jakub Majorek 09a035ebb3 🐛 fix(usage): attribute native per-model cost by delta, not cumulative total (#3223)
Native harnesses (claude-native / codex-native) report a cumulative
SESSION total, not a per-model split. `_persist_native_cumulative_usage`
SET each active model's `by_model` bucket to the whole running total, so a
session that switched models mid-run double-counted the shared baseline:
the previous model kept its last cumulative snapshot while the new model
was set to the full total, and summing the buckets exceeded the session
total (e.g. total $11.91 but opus $10.80 + sonnet $11.91).

Attribute only each report's growth (new - old) to the currently-active
model instead, mirroring the relay path's per-model delta accumulation.
Per-model token and cost buckets now hold each model's own usage and sum
to the flat session total across model switches. Deltas are clamped >= 0
so a lowered / rebased report never claws usage back out of a bucket (the
flat totals are likewise monotonic-clamped).

Read-only reporting (`omni usage`, the web session sidebar) needs no
change — it reads `by_model` verbatim, so corrected data flows through.
Existing sessions keep their already-stored buckets; this corrects
attribution for turns recorded after it ships (not backfillable).

Co-authored-by: Isaac

Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>
2026-07-27 16:00:46 +00:00
Cathy Yin 7dbdb821d3 feat(web): add a harness credential from the New Chat setup dialog (M3 frontend) (#3090)
* feat(web): add a harness credential from the New Chat setup dialog (M3 frontend)

Frontend for Setup From the Web UI — turn a yellow needs-setup harness
green from the browser (Claude/Codex/Pi) via an inline equal-weight auth
form (adopt / subscription signpost / API key / gateway), plus the setup
dialog UX cleanups. Gated behind the existing harness_install_enabled cap.

Rebased onto latest main (the M3 backend #3088 is now upstream, so only
web/ + follow-up backend fixes remain) and folded in the Polly review
notes: stable option keys, clear secret fields on save, and a note that
default_model/wire_api are backend-accepted but reserved for a follow-up.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(web): scope useHosts refocus-refetch to the setup flow (Polly review)

staleTime:0 + refetchOnWindowFocus was app-wide across ~8 useHosts
consumers, bumping /v1/hosts volume on every refocus. Make it an opt-in
refetchOnFocus flag; only the setup dialogs (NewChatDialog, HarnessSetupDialog)
that need live readiness recovery pass it. Others keep the 30s stale window.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(web): guard the credential form against double-submit + close test gaps

Address Pat's review:
- Gate both form onSubmit handlers on !busy so hitting Enter in the field
  during an in-flight save can't re-POST the secret (the Save button was
  already disabled, but the keyboard path wasn't guarded).
- Add a double-submit-guard test, plus direct hook tests for
  useStoreCredential (path/body split, JSON detail + non-JSON error parse,
  cache patch + detect invalidation) and useDetectedCredentials
  (GET/parse, empty-body fallback, enabled/host gating).

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(web): let Pi adopt an openai-family credential too (Polly review)

Pi consumes both anthropic and openai and the daemon adopts a detected
credential under its OWN family, so a host with only $OPENAI_API_KEY could
back Pi — but the adopt filter scoped to Pi's single write-default family
(anthropic), hiding that affordance. Add harnessCredentialAdoptFamilies
(Pi -> both families) and filter the adopt row on it; the paste/gateway
paths and the cross-family guard for Claude/Codex are unchanged.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

---------

Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
2026-07-27 17:33:29 +07:00
Yi Lyu c1acaf885f fix(policies): scan text attachments for PII at the request gate (#2927)
Signed-off-by: Yi Lyu <isabellalyu1130@gmail.com>
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-27 10:19:38 +00:00
Jackson Zheng cd23178ad4 Polish sidebar header spacing (#3346)
* Polish sidebar header spacing

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* test(ui-snapshot): update visual baselines

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

---------

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
2026-07-27 10:15:48 +00:00
Tomu Hirata 0e0bc901b5 fix(codex-native): carry hook trust across private CODEX_HOME copy (#3343)
* fix(codex-native): carry hook trust across private CODEX_HOME copy

When codex-native provisions a per-session private CODEX_HOME and copies
config.toml into it, the [hooks.state] keys inside the copy still reference
the global ~/.codex/ paths. Codex keys trust records by the absolute path of
the hooks file, so every key misses and Codex opens an interactive "Hooks need
review" prompt on every launch. Headless sub-agents can never answer it, so
the app-server never emits thread/started and the run dies on the 15s timeout.

Fix: two changes to _populate_codex_home_config:

1. Symlink hooks.json from the global home into the private home (alongside
   auth.json). This makes the user's hooks reachable at the private path.

2. After copying config.toml, rewrite [hooks.state.*] key path prefixes from
   source_dir to target_dir. The hash values are left untouched, so trust is
   neither widened nor weakened — it is only carried across the copy that
   Omnigent itself performs.

Fixes #3268.

Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: gate hooks.json symlink on not minimal_config; drop redundant re import

The minimal_config path rebuilds config.toml from scratch with only
model_provider/model_providers/profiles — no [hooks.state] entries.
Symlinking hooks.json there with no trust state re-introduces the
interactive trust prompt for the title worker. Gate the symlink (and
the trust-key rewrite that gives it meaning) on not minimal_config.

Also remove the redundant `import re as _re` inside
_retarget_codex_hook_trust_keys; re is already imported at module level.

Addresses Polly review feedback on #3343.

Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(codex-native): flush accepted hook trust back to global config on close

When a user accepts the hook-trust prompt inside a session, Codex writes
[hooks.state] entries into the per-session private config.toml — but those
are discarded when the session ends because the private CODEX_HOME is
ephemeral. So the prompt reappears on every launch.

Fix: in CodexNativeAppServer.close(), call _merge_codex_hook_trust_back to
read [hooks.state] from the private config.toml, translate the path keys
from the private home back to the global ~/.codex/ prefix, and upsert them
into ~/.codex/config.toml atomically. The next session's _populate_codex_home_config
copies the global config (now with the trust entries), and
_retarget_codex_hook_trust_keys translates the paths forward to the new
private home — so Codex sees the hooks as already trusted and skips the prompt.

The write is best-effort: any failure is logged as a warning rather than
raised, since the session has already ended.

Fixes #3268.

Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: assign tmp before try block to avoid unbound variable warning

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-27 10:08:54 +00:00
Tomu Hirata 54d8e61c01 fix(claude-sdk): surface ResultMessage is_error as ExecutorError, not assistant text (#3342)
* fix(claude-sdk): surface ResultMessage is_error as ExecutorError, not assistant text

When the Claude SDK reports a harness-level failure (e.g. an expired
login or unauthenticated session), the terminal ResultMessage carries
is_error=True and the failure text in result. The executor was ignoring
is_error and assigning result directly to response_text, so the error
appeared in the conversation as though the model had said it — with no
error item, no harness attribution, and no log line.

Fix: check is_error before touching response_text. When true, set
terminal_error (the existing path that yields ExecutorError and returns)
and log an error line naming the agent. When false, the existing
response_text assignment runs unchanged.

Also add is_error to _ResultMessageObj so the Protocol matches the
SDK's actual shape (it was only declared on _ToolResultBlockObj before).

Closes #3282

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(claude-sdk): use getattr for is_error, handle null result, add unit test

Address Polly review feedback on #3342:

- Use getattr(result_msg, 'is_error', None) instead of direct attribute
  access so that existing test doubles that only set session_id/result
  don't raise AttributeError (matching the sibling getattr calls for
  session_id and usage in the same block).

- When is_error=True but result is None/empty, fall back to a generic
  'claude-sdk harness error' message rather than silently dropping the
  failure.

- Add test_result_message_is_error_yields_executor_error: verifies that
  a ResultMessage with is_error=True is routed to ExecutorError and does
  not appear in TurnComplete.response.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(test): wrap long assertion string to satisfy ruff E501

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-27 19:01:10 +09:00
Pat Sukprasert fdeac467eb fix(web): stop short links collapsing table columns in chat markdown (#3350)
* fix(web): stop short links collapsing table columns in chat markdown

Streamdown styles links with `wrap-anywhere` (overflow-wrap: anywhere),
which also drops the element's min-content width to a single character.
Inside its `table-layout: auto` table that let a link-only column be
squeezed to ~2ch, so a short link like "#3090" stacked one or two
characters per line while the prose columns took all the width.

Narrow links inside table cells to `break-word`: overlong URLs still
soft-wrap, but min-content stays at the longest unbreakable run so the
column can no longer be squeezed below it. Prose links keep `anywhere`.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test(e2e-ui): guard markdown table link column width in the browser

The CSS fix for the collapsing "PR #" column is only observable with a
layout engine, so the vitest companion can pin the rule and its selector
scoping but not the width. This adds the browser-side half: a seeded
assistant message renders the table shape that triggered the bug — a
link-only `#` column, wide prose columns, and a full-URL column — and
asserts the short link stays on one line box, its cell is at least as
wide as the link, and a long URL still soft-wraps inside its cell.

Verified against the pre-fix stylesheet: `#3090` stacks across 5 line
boxes without the `overflow-wrap: break-word` narrowing, 1 with it.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-27 09:58:46 +00:00
Anthony Ivan 3f357d0f0e fix(openai-agents): honor explicit Databricks profiles (#3288)
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
2026-07-27 09:42:23 +00:00
Tomu Hirata ee2b14a35a fix(seatbelt): resolve two-hop proxy symlink when detecting CPython install root (#3344)
* fix(seatbelt): resolve two-hop proxy symlink when detecting CPython install root

When a Python interpreter is installed via `uv tool install`, the
executable is a two-layer symlink:

  ~/.local/share/uv/tools/<pkg>/bin/python  →  (proxy)
      ~/.local/share/uv/python/cpython-3.12.X-.../bin/python3.12

The literal proxy path grandparent (`tools/<pkg>/`) has no CPython
`lib/python*` markers, so `_interpreter_install_root` returned None.
`_add_topmost` then raised OSError before ever checking the resolved
path, causing every session to fail with:

  darwin_seatbelt: helper interpreter at '.../uv/tools/omnigent/bin/python'
  resolves under the unsafe ancestor '/Users'; ...

Fix: in `_add_topmost`, when the literal path yields no install root,
resolve it one level and retry `_interpreter_install_root` on the
resolved path before giving up. The resolved CPython install root
(which does carry the canonical markers) is then granted as the narrow
subpath, matching the existing behaviour for direct uv-python installs.

Also update the OSError message to say 'CPython install root' and note
that both the literal and resolved path were tried, and fix the
matching assertion in the existing test.

Fixes #3237.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(seatbelt): grant pi_dir and $TMPDIR write root so sandboxed pi can boot

Two follow-up fixes found by running `omnigent run --harness pi` with
darwin_seatbelt enabled end-to-end:

1. with_additional_read_roots silently dropped pi_dir

   When the spec declares no read_paths, resolve_sandbox returns
   read_roots=None (meaning 'no spec-supplied grants').
   with_additional_read_roots bailed early on None, so the pi node_modules
   dir granted by _try_sandbox_pi was never added to the policy. Result:
   pi failed with 'Cannot find package .../pi-ai/index.js' because the
   seatbelt profile had no subpath rule for the nvm install tree.

   Fix: treat None as an empty list rather than 'already unrestricted' —
   the caller is explicitly widening the policy and must be honoured even
   when the spec has no grants of its own.

2. PI_CODING_AGENT_DIR was created under $TMPDIR, which wasn't granted

   _try_sandbox_pi granted /tmp as a write root, but on macOS $TMPDIR is
   /var/folders/.../T/ (not /tmp). PI_CODING_AGENT_DIR is created with
   tempfile.mkdtemp() which uses $TMPDIR, so pi got EPERM trying to write
   its extension/settings. Fix: also grant tempfile.gettempdir() alongside
   /tmp.

With all three fixes (two-hop symlink detection, read-roots None handling,
TMPDIR grant) `omnigent run /tmp/pi-sandbox-bundle --harness pi` boots and
completes a full turn end-to-end under darwin_seatbelt.

Fixes #3237.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-27 18:41:53 +09:00
Tomu Hirata 505b4f821a fix(pi): migrate Pi to Databricks v2 gateway endpoints (#3307)
* fix: route kimi and inkling through Responses API via system.ai.* ids

Kimi and inkling never send finish_reason in /chat/completions streaming
responses, causing Pi to throw 'Stream ended without finish_reason'.

These models work correctly via the Responses API at /ai-gateway/codex/v1
using their system.ai.* model ids (system.ai.kimi-k2-7-code,
system.ai.inkling).

- Add system.ai.kimi-k2-7-code and system.ai.inkling to
  _DATABRICKS_RESPONSES_MODELS in the executor
- Add _DATABRICKS_TO_SYSTEM_AI mapping in pi_native_credentials so live
  endpoint fetch translates databricks-* ids to system.ai.* and routes
  them to the gpt_responses bucket (openai-responses at /ai-gateway/codex/v1)
- Update _pi_needs_responses_api to treat system.ai.* models as responses
- Update _pi_provider_for_model to route system.ai.* to databricks-openai

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(review): restore substring reasoning fallback and fix run-path translation

Addresses Polly's review of #3307:

1. Restore 'kimi'/'inkling' to substring reasoning check in _fetch_pi_model_lists
   so unmapped variants (renamed/versioned endpoints not in _DATABRICKS_TO_SYSTEM_AI)
   still get reasoning:true — preventing silent regression.

2. Translate databricks-* model ids to system.ai.* in the executor run path
   (_build_env_and_dir) so model_override='databricks-kimi-k2-7-code' correctly
   routes to the databricks-openai (Responses API) provider, not databricks-completions.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: move GLM to Responses API via system.ai.glm-5-2

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: move Qwen3 to Responses API via system.ai.* ids

Qwen3 returns array content with tool calls via /chat/completions causing
[object Object] errors. system.ai.qwen3-next-80b-a3b-instruct and
system.ai.qwen35-122b-a10b work correctly via the Responses API.

Also removes qwen3 from _unsupported_in_pi since it's now handled.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor: replace hardcoded system.ai map with keyword-based detection

- Replace _DATABRICKS_TO_SYSTEM_AI exact-id dict with _databricks_to_system_ai()
  function that detects by keyword (kimi, inkling, glm-5, qwen3, qwen35) and
  derives system.ai.* id by stripping 'databricks-' prefix. Handles future model
  variants automatically without needing to update an exact-id map.

- Apply the same swap in model_catalog._fetch_databricks_listing so sys_list_models
  returns system.ai.* ids directly, letting the LLM use the correct id immediately.

- Use specific fragments (glm-5 not glm) to avoid false-positives like
  zai-org-glm-4-7 which has no system.ai.* alias.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(review): fix _ensure_rpc selector translation; revert GLM to completions path

Addresses Polly's blocking issues:

1. Normalize model id to system.ai.* at the top of _ensure_rpc so that both
   models.json and the provider/model selector see the same id. Previously only
   _build_env_and_dir translated the id but _ensure_rpc still built the selector
   from the untranslated databricks-* id, causing 'Model not found' in Pi.

2. Revert GLM (databricks-glm-5-2) back to the completions path. GLM works fine
   via /chat/completions with finish_reason=true — moving it to the Responses API
   was unnecessary and undocumented. Removed from _SYSTEM_AI_MODEL_KEYWORDS and
   _DATABRICKS_RESPONSES_MODELS.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor: use Unity Catalog model-services API for Pi model discovery

Replace /api/2.0/serving-endpoints with /api/2.1/unity-catalog/model-services
which returns system.ai.* model ids directly with supported_api_types metadata.

Benefits:
- No databricks-* → system.ai.* translation needed
- Authoritative API capability info: models with 'openai/v1/responses' in
  supported_api_types go to the Responses provider; others to completions
- Embeddings excluded cleanly via has_embedding check
- sys_list_models returns system.ai.* ids directly via _fetch_databricks_uc_listing

Also add _ensure_rpc id normalization so databricks-* model_override values
are translated before building the provider/model selector.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: route all system.ai.* models through AI Gateway (omnigent-openai)

system.ai.* ids are not valid at /serving-endpoints — they only work
via the AI Gateway at /ai-gateway/codex/v1. Previously, system.ai.*
models without openai/v1/responses in UC metadata (kimi, inkling,
qwen3) were routed to omnigent-completions at /serving-endpoints,
causing 404 errors.

Route all system.ai.* models to omnigent-openai regardless of UC
supported_api_types.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(test): update test to expect all system.ai.* models in gpt_responses

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi-native): surface Pi model errors as visible error items in web UI

When Pi's API call fails (e.g. 404 for unknown model id, 400 for
unsupported API type), the extension was silently returning from
message_end with no output, leaving users with an empty turn.

Post an external_conversation_item of type 'error' when message.stopReason
is 'error', so the error appears in the web UI chat.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(tests): update model_catalog tests for Unity Catalog API format

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: revert Qwen3 from responses API - Pi sends fields that Qwen3 rejects

/ai-gateway/codex/v1/responses rejects Pi's standard Responses API fields
(parallel_tool_calls, temperature:null, top_p:null) for Qwen3, causing 400.
Route Qwen3 back to omnigent-completions until either:
- Pi adds compat flags to suppress these fields for non-standard providers
- The upstream array-content fix (earendil-works/pi#7062) lands to fix [object Object]

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: restore Qwen3 to Responses API path via system.ai.*

Pi only sends store:false in requests - the earlier 400 was from a stale
session before the routing fix. Confirmed minimal Pi request works fine
for Qwen3 via /ai-gateway/codex/v1/responses.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(review): scope UC listing to pi path only; fix test fixtures

Polly's review correctly identified that using _fetch_databricks_uc_listing
for all Databricks providers leaks system.ai.* ids to non-pi harnesses
(claude-sdk, codex, openai-agents) that only understand databricks-* ids.

Revert model_catalog.py to use _fetch_databricks_listing (serving-endpoints)
for sys_list_models. _fetch_databricks_uc_listing remains available but is
only used internally by pi_native_credentials._fetch_pi_model_lists.

Also fix test_model_catalog.py fixtures to use the correct serving-endpoints
payload shape (databricks-* ids) rather than the UC model-services shape
(system.ai.* ids) which the non-pi listing never emits.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* test(model_catalog): update pi tests for UC model-services API

Pi harnesses now call `/api/2.1/unity-catalog/model-services` and return
`system.ai.*` model ids instead of `databricks-*` ids. Update the test
fixtures and expected ids to match:

- `_databricks_transport`: now serves both the serving-endpoints page
  (non-pi) and a UC model-services page (pi harness calls).
- `test_databricks_listing_filters_to_chat_llms`: expect `system.ai.*`
  ids and matching family assertions.
- `test_databricks_listing_skips_explicitly_non_ready_endpoints`: rewrite
  to use UC format (UC has no per-service readiness flag).
- `test_listing_failure_reported_and_not_cached`: switch to codex-native
  harness to test generic failure/retry without UC routing complexity.
- `pi-everything` parametrize: update expected ids to `system.ai.*`.
- `model_catalog.py`: add TTL cache for UC listings (same `_listing_cache`
  with a `"uc:"` prefixed key) so pi harness calls cache-hit correctly.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(model_catalog): fix ruff RUF005 and E501 lint errors

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(test_model_catalog): shorten docstring to fix E501

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi_executor): scope system.ai.* Responses-API routing to kimi/inkling/qwen only

system.ai.claude-* and system.ai.meta-llama-* ids should route to their own
providers (Anthropic surface and completions respectively), not the Responses
API. Previously _pi_needs_responses_api returned True for *all* system.ai.*
ids, which would have routed llama to the Responses endpoint.

Fix: check _SYSTEM_AI_MODEL_KEYWORDS in the system.ai.* branch so only kimi,
inkling, and qwen3 variants return True. Claude is already caught upstream by
the "claude" substring check in _pi_provider_for_model.

Also update stale docstrings in _needs_responses_api and _unsupported_in_pi
that still mentioned qwen3 as excluded (it was re-enabled via the Responses API).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(pi): route GLM via Responses API (system.ai.* ids)

GLM has the same finish_reason issue as Kimi/inkling on /chat/completions.
Route it through the AI Gateway Responses API by adding "glm-" to
_SYSTEM_AI_MODEL_KEYWORDS (uses "glm-" not bare "glm" to avoid matching
"zai-org-glm-4-7" which has no system.ai.* alias).

- Remove GLM from _PI_REASONING_MODEL_FRAGMENTS (reasoning:true is a
  completions-path flag; not needed for Responses API).
- Remove GLM from the reasoning:true assignment in _fetch_pi_model_lists.
- Update test: kimi no longer gets reasoning:true (Responses API path).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): remove gpt-oss from _unsupported_in_pi; it routes via Responses API

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): exclude all Gemini models from Pi, not just gemini-2-5

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): exclude only gemini-2-5 from Pi; other Gemini models use completions

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): drop redundant qwen35 keyword; qwen3 already matches qwen35 ids

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(pi): remove _databricks_to_system_ai; catalog always returns system.ai.* for pi

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): remove reasoning:true from kimi/inkling static model entries

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(pi): route Gemini via /ai-gateway/mlflow/v1/chat/completions using system.ai.* ids

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): fix _unsupported_in_pi to only exclude gemini-2-5; gemini-3+ route via mlflow

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(pi): remove static kimi/inkling/qwen3 entries from _DATABRICKS_RESPONSES_MODELS

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): route system.ai.* llama/other models to mlflow gateway; rename provider to databricks-mlflow

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): use generic base URL for non-Databricks providers (OpenAI API key, LiteLLM)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi): address Polly review — fix 4-tuple annotation, system.ai.gpt routing, gpt-oss exclusion, UC listing filter

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(model_override): strip system.ai.* prefix for vendor-direct providers (OpenAI key, etc.)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-27 17:48:41 +09:00
Serena Ruan 2a60e17d89 fix(pi-native): surface unresolved Databricks credentials instead of a silent dead session (#3336)
A native Pi session routed through a Databricks gateway whose OAuth token
can't be resolved (expired refresh token) launched fine but every message
silently failed to reach the model — no reply, no error. `_databricks_pi_provider`
caught all failures in one try/except and still returned a provider whose
`!databricks auth token` apiKey fails at request time; because pi-native
dispatches turns fire-and-forget, the failure never round-tripped back as an
Omnigent error.

Split credential resolution from the (benign) model-list fetch so a genuine
auth failure carries a `credential_warning`. At terminal auto-create, surface
that warning as an `error` item via `external_conversation_item`: it renders as
the web UI's distinct error banner (not a misleading assistant bubble),
persists across reload, is a non-content item type so it never enters the next
turn's context, and posts without queuing an agent turn (safe on a session
whose model is unreachable).

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-27 15:43:32 +08:00
Abdullah Said 3c64d66aa5 feat(catalog): add claude-opus-5 to curated claude subscription models (#3275)
Claude Opus 5 (released 2026-07-24) was missing from the curated
_SUBSCRIPTION_STATIC_MODELS["claude"] list. Verified empirically against
Claude Code 2.1.220: 'claude-opus-5' -> is_error:false; the dated form
'claude-opus-5-20260724' and a 'claude-opus-5-fast' variant both return
is_error:true, so neither is added.

Placement follows the existing convention: tiers descend
fable -> opus -> sonnet -> haiku, newest version first within a family
(matching claude-sonnet-5 ahead of claude-sonnet-4-6), so opus-5 slots
between fable-5 and opus-4-8.

The web mirror (web/src/lib/claudeNativeModels.ts) needs no change: it
lists version-agnostic aliases ('opus' resolves to the latest Opus) by
design, not pinned ids.

Signed-off-by: Abdullah Said <abdullahsaid89@gmail.com>
Co-authored-by: omnigent <noreply@omnigent.ai>
2026-07-27 14:29:13 +07:00
Serena Ruan 5e62d0e44b ci(ui-snapshot): make the visual-baseline gate merge-blocking + regenerate baselines (#3338)
* ci(ui-snapshot): make the visual-baseline gate merge-blocking

The UI Snapshot visual-regression check was advisory ([non-blocking]) and
not in the required-checks set, so a UI change could land without
regenerating the committed baselines — which is how the baselines drifted
stale on main (every PR since #3311 fails the gate identically).

Register it as a required merge gate:
- Drop the "[non-blocking]" suffix from the job name.
- Add "UI Snapshot (visual baselines)" to REQUIRED and ALLOW_SKIP in
  merge-ready/required.sh, plus a workflow_for mapping. It's safe as a
  required check: a PR touching no render input skips the render via the
  `detect` job's `if` gate, and an if-skipped job reports success — so
  non-UI PRs satisfy the check instead of sitting pending. ALLOW_SKIP +
  workflow_for let the gate tell that genuine skip from a still-pending run.
- Add "UI Snapshot" to merge-ready.yml's workflow_run triggers so the gate
  re-evaluates when the snapshot workflow completes.
- Update the visual README's merge-blocking section.

This PR edits ui-snapshot.yml (a render input), so the gate runs here and
fails on the stale baselines; the `update-ui-snapshot` label regenerates
them onto this branch to turn it green.

Co-authored-by: Isaac

* test(e2e-ui): regenerate visual baselines

---------

Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-27 15:19:33 +08:00
Serena Ruan 2de2d3f888 fix(sessions): fall back to localStorage when pinning against an old server (#3332)
* fix(sessions): fall back to localStorage when pinning against an old server

A pin created in the new UI while the server is still pre-upgrade was lost
on the server upgrade. The pin toggle PATCHes `omnigent.pinned`; an old
server (no per-user pin concept) stores it as a bare label, but the
upgraded server's read path (`_labels_for_viewer`) drops every bare/
`omnigent.pinned.*` key and only surfaces the caller's own
`omnigent.pinned.<user>` key — so the bare-key pin silently vanishes. The
localStorage→server migration couldn't recover it either, since that pin
was never in localStorage.

This complements the earlier migration-gate fix (which protected pins made
*before* the UI upgrade). Now the toggle also checks `filterHonored`: when
the server can't store pins, it writes the pin to localStorage (the same
store the pre-upgrade UI used) instead of PATCHing a doomed bare key. The
pin renders immediately (sidebar unions localStorage pins) and later
migrates through `useMigrateLocalPinsToServer` like any pre-upgrade pin.
Once the server can store pins, the toggle uses the server as before.

- Move the legacy-pin localStorage helpers from Sidebar.tsx to the leaf
  sidebarNav module (+ a single-id `setLegacyPinnedConversationId`) so the
  toggle hook can use them without an import cycle.
- Tests: unit coverage for the toggle's old-server fallback (pin/unpin to
  localStorage, no PATCH; normal PATCH path once honored), and an
  end-to-end case in the backwards-compat suite that pins DURING the
  UI-before-server window and asserts it survives the server upgrade.

Co-authored-by: Isaac

* fix(sessions): surface local-write failures in the old-server pin fallback

Addresses a review note: the old-server pin toggle's localStorage write is
the pin's only persistence, but it went through the best-effort
`writeLegacyPinnedConversationIds`, which swallows write errors (e.g.
storage quota exceeded). So a failed write let the mutation report success
and the optimistic patch show the pin, while it silently vanished on reload
— with no rollback.

Split out a throwing `...OrThrow` raw write. The old-server fallback
(`setLegacyPinnedConversationId`) now uses it, so a failed write rejects the
mutation → `onError` rolls back the optimistic patch and the UI honestly
shows the pin didn't take, matching the server PATCH path. The migration's
best-effort write is unchanged (a failed write there just retries next load).

Test: the fallback rolls back the optimistic pin when the local write throws.

Co-authored-by: Isaac
2026-07-27 14:57:37 +08:00
Zeyi (Rice) Fan a7ef194c4f refactor(sandboxes): introduce contribution-based provider registry (#3330)
## Related issue

N/A

## Summary

- Add `omnigent/onboarding/sandboxes/types.py` with shared dataclasses
  (`SandboxCapabilities`, `SandboxSpec`, `SandboxInfo`, `HostContext`) and the
  new `SandboxError` exception hierarchy.
- Add `omnigent/onboarding/sandboxes/registry.py` with a contribution-based
  provider registry that mirrors `omnigent/harness_plugins.py`: built-in
  providers are declared as a `SandboxProviderContribution`, community
  packages register via the `omnigent.sandbox_providers` entrypoint group, and
  broken plugins are recorded in `load_errors` without breaking core startup.
- Add `omnigent/community/sandbox/__init__.py` as a namespace package so
  third-party providers can ship code under `omnigent.community.sandbox.*`.
- Validation enforces that community provider code lives under the community
  namespace, rejects name collisions, and checks metadata consistency.
- Add a `capabilities` property to `SandboxLauncher` that derives feature flags
  from existing class variables and overridden transport methods.
- Migrate CLI and managed-host call sites from direct class-var reads
  (`supports_cli_bootstrap`, `can_resume`, `supports_local_port_forward`) to
  the new `capabilities` object.
- Add unit tests for types, registry behavior, validation, and entrypoint
  discovery.

No provider implementations were changed; this is purely a surface-layer
refactor toward a pluggable sandbox provider interface.

## Test Plan

```bash
uv run pytest tests/onboarding/sandboxes tests/sandbox tests/cli/test_cli.py tests/server -k managed -q
pre-commit run --files omnigent/onboarding/sandboxes/types.py omnigent/onboarding/sandboxes/registry.py omnigent/onboarding/sandboxes/base.py omnigent/onboarding/sandboxes/__init__.py omnigent/onboarding/sandboxes/bootstrap.py omnigent/community/sandbox/__init__.py omnigent/cli_sandbox.py omnigent/server/managed_hosts.py tests/onboarding/sandboxes/test_types.py tests/onboarding/sandboxes/test_registry.py
```

All 779 selected tests pass and the targeted pre-commit hooks pass.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

New unit tests in `tests/onboarding/sandboxes/test_types.py` and
`tests/onboarding/sandboxes/test_registry.py` exercise the registry,
contribution validation, types, and capabilities derivation. Existing
provider and CLI tests pass unchanged, confirming backward compatibility.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-27 06:44:36 +00:00
Rahul Ravindranathan c1bedeaabd feat(automations): model + reasoning-effort selectors on automations (#3331)
* feat(scheduled): add Model + Reasoning-effort pickers to the task dialog

The scheduled-task create/edit dialog previously omitted model and effort,
sending only agent_id so tasks always ran with the agent's configured
defaults. Add lightweight Model + Reasoning-effort controls, gated by the
selected agent's capability exactly like the interactive New Chat dialog:
they render only for native coding agents that carry the model/effort
surface (Claude Code) and are hidden for agents without it (Codex, plain
SDK agents, etc.).

- New scheduled-local ModelEffortFields component reuses the shared option
  lists (CLAUDE_NATIVE_MODELS + the version-agnostic aliases, and
  CLAUDE_NATIVE_EFFORTS) rather than importing the 26-prop
  HarnessConfigModal, which is bound to smart-routing / cost-control /
  per-turn model loading and disproportionate for a saved task. When a host
  is pinned it uses that host's live model options; with none pinned (the
  common case) it falls back to the static Claude aliases.
- Hoist CLAUDE_NATIVE_EFFORTS into the shared HarnessConfigControls module
  so both dialogs share one source of truth.
- Wire modelOverride + reasoningEffort through create and update (both
  already round-tripped by scheduledTasksApi.ts — no client/API change).
  Unselected ("Default") omits the field on create so the fire path uses
  the agent's defaults; on edit, Default sends null to clear a prior
  override. Edit mode prefills both controls from the loaded task.

No permission/approval/cursor mode picker and no new API field: this is a
pure frontend change.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* test(automations): e2e for model + effort selectors

Extend tests/e2e_ui/scheduled/test_scheduled_tasks_page.py with UI journeys
for the model + reasoning-effort selectors added to the scheduled-task
create/edit dialog:

- controls visible + default to "Default" for a capability-gated agent
  (Claude Code)
- controls hidden (with the "uses defaults" hint) for a non-capable agent
  (seeded Codex task, asserted via the edit dialog)
- create persists a concrete Model + Effort pick (asserted via the REST API)
- create with both controls left on Default persists null overrides
- edit prefills the controls from a seeded task's stored overrides

LLM-free like the sibling tests: exercises only the dialog, REST, and the
rendered row. Uses Playwright expect() auto-waiting, no sleeps.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-26 23:36:49 -07:00
Zeyi (Rice) Fan dbfc7565e5 feat(web): remove sidebar font-size control and add appearance reset dialog (#3326)
## Related issue
N/A

## Summary
- Remove the dedicated Settings → Appearance → Sidebar → Font size card and the `lib/sidebarFontPreferences` module, since users should use the global Interface font size control instead.
- Clear the legacy `omnigent:sidebar-font-size` localStorage key on app boot so anyone who previously changed the sidebar font size falls back to the default 13px.
- Add a "Reset to defaults" button at the bottom of the Appearance section that opens a confirmation dialog and resets all appearance choices: mode, terminal theme, color palette/custom theme, workspace panel default, hide-unconfigured-harnesses toggle, and interface/code font size and family.

## Test Plan
- Updated unit tests in `web/src/pages/SettingsPage.test.tsx` covering the reset flow and the absence of the sidebar font size control.
- Added a Playwright E2E test in `tests/e2e_ui/sessions/test_appearance_reset.py` to verify the sidebar card is gone and the reset dialog restores defaults.
- To verify locally after installing web dependencies:
  - `cd web && npm run type-check`
  - `npx vitest run src/pages/SettingsPage.test.tsx`
  - `pytest tests/e2e_ui/sessions/test_appearance_reset.py`

## Demo
N/A — UI change; a screen recording of the reset confirmation dialog is recommended before merge.

## Type of change
- [ ] Bug fix
- [ ] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [x] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes
local web dependencies are not installed in this environment, so the local type-check and vitest runs could not be executed. CI will run the web test suite on the PR branch.

## Changelog
Settings → Appearance no longer has a separate Sidebar font size control, and a new "Reset to defaults" button restores every appearance preference after confirmation.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-26 23:16:30 -07:00
Jackson Zheng 8b3856fefa Align sidebar project icons (#3317)
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
2026-07-26 23:07:06 -07:00
Serena Ruan a86fba610d feat(projects): name the project in the new-session hero, drop the tray chip (#3327)
* feat(projects): name the project in the new-session hero, drop the tray chip

When starting a session from within a project (a `?project=` landing), the
composer used to show the project as a pill in the footer tray while the hero
kept its generic "What should we do?" prompt. Move that context into the hero
instead: the heading shows the project name and Otto's eyes are swapped for the
same folder icon the sidebar uses for a project. The footer project chip
(`LandingProjectPicker`) is removed — filing on create still uses the same
`selectedProject` state, just without the redundant chip.

The folder icon renders in a fixed-height (`h-18`) box matching Otto so the
vertically-centered composer doesn't shift when toggling between the plain and
in-project landings.

Co-authored-by: Isaac

* fix(projects): clamp long project name in the new-session hero

A 100-char project name (the server-side cap) rendered at text-3xl overflowed
the centered container: the icon+heading flex row sized to its content with no
width bound, so the h1's min-w-0/line-clamp had nothing to act against. Give the
row w-full and keep the heading min-w-0 + line-clamp-2 + break-words so a long
name wraps to two lines and ellipsizes instead of overflowing. Add a test
asserting the clamp class contract on a 100-char name.

Co-authored-by: Isaac
2026-07-27 13:41:37 +08:00
Andrew Peltekci c4df88c712 fix(crash-handler): stop same-second crash reports overwriting each other (#3173)
The same-second filename collision was disambiguated by pid alone. A pid is
only unique across processes — a process that crashed more than twice within
one second reused its own pid, so every report after the first collision was
written to the same path and silently destroyed its predecessor. Saving five
reports in one second left two files on disk with three crash reports lost,
with rotation held wide enough that nothing should have been pruned.

Keep counting past the pid-suffixed name until the path is free.

test_save_report_writes_and_rotates encoded the bug: it asserted all five
returned paths still existed while rotation kept only two, which could only
hold when the collision collapsed them onto two names. It now asserts the
newest report survives its own rotation pass, and a new test pins the
no-overwrite guarantee with rotation held wide.

Signed-off-by: apeltekci <andrew@peltekci.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:36:43 +00:00
Anthony Ivan 96b2f6c97b docs: recommend omnidev for worktree testing (#3277)
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
2026-07-27 05:34:56 +00:00
Serena Ruan 9835d09c1f fix(web): use lucide files icon for Files workspace tab (#3329)
Swap the Files right-rail tab glyph from FilePenLineIcon (pen-on-page) to
FilesIcon (stacked pages) to better convey the panel's contents.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-27 13:34:07 +08:00
Serena Ruan 1dd0c49e71 fix(sessions): don't wipe local pins when UI upgrades before server (#3323)
* fix(sessions): don't wipe local pins when UI upgrades before server

The one-time localStorage→server pin migration (#3189) trusted an
ambiguous success signal. A pre-upgrade server silently ignores the
unknown `?pinned=true` param and returns the normal (unfiltered) session
page, so the UI saw ~100 "server pins", computed an empty to-migrate set,
and cleared localStorage without ever writing a pin. After the server was
upgraded, its per-user key filter found nothing and every pin read as
unpinned — the reported data loss for UI-before-server upgrades.

Fix, entirely client-side:

- `fetchPinnedConversations` now returns `{ conversations, filterHonored }`.
  It keeps only rows actually carrying the `omnigent.pinned` label and
  reports `filterHonored: false` when the server returned unpinned rows —
  the tell-tale of an old server that ignored the filter.
- The migration is gated on `filterHonored`: it stays inert (localStorage
  untouched) against an old server and re-runs after the eventual upgrade.
  A legacy id is dropped only after its write is confirmed.
- Pinned membership is the union of the server's pins and any leftover
  localStorage pins, so a not-yet-migrated pin keeps rendering instead of
  vanishing during the UI-before-server window.

Tests: new filter-honored detection cases, a migration-gate suite, and an
end-to-end backwards-compat test that drives the real hooks across an
old→new server upgrade and asserts the pin is never lost.

Co-authored-by: Isaac

* docs(sessions): address Polly review notes on pin migration

- Document the empty-page ambiguity in `filterHonored` and why it's safe
  (an old empty page means a zero-session account; the migration PATCH to a
  deleted session 404s and the pin is retained, not lost).
- Note the window-scoped caveat that a legacy-only pin outside the loaded
  paginated window may not render a row until loaded.
- Add a regression test: a failed (404) migration write keeps the legacy
  pin in localStorage for retry.

Co-authored-by: Isaac
2026-07-27 13:20:50 +08:00
Rahul Ravindranathan f85452e4f3 feat(automations): relative next-run label + card rows (#3324)
* feat(automations): absolute next-run time + card rows

Change 1: the Automations list now shows the next run as an absolute
wall-clock time ("Next run Tomorrow at 8:00 AM" / "Today at 2:30 PM" /
"Jul 26, 8:00 AM") instead of a relative delta ("in 15h"). Adds
formatNextRunAtAbsolute() in scheduleText.ts, which only FORMATS the
server-authoritative next_run_at (rendered in the task timezone,
Today/Tomorrow bucketed in that same zone) and never recomputes which
instant is next on the client. The old relative formatNextRunAt() is
kept intact.

Change 2: each ScheduledTaskRow now renders as a card (rounded-xl
border bg-card, internal padding), and TasksPage stacks them with a
gap. All existing behavior and data-testids preserved; paused rows are
not dimmed.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* feat(automations): relative full-word next-run label

Reverses the earlier absolute-time next-run display back to a
server-sourced relative delta in full words ("Next run in 3 hours",
"Next run in 8 mins", "Next run in 2 days") per user feedback.

formatNextRunAt now emits full-word, pluralized buckets ('soon' /
'in N min(s)' / 'in N hour(s)' / 'in N day(s)'); the delta is still
computed only from the server's authoritative next_run_at, so the
"no client countdown" rule is unaffected. Removes the now-dead
formatNextRunAtAbsolute and its private helpers (safeFormat,
civilDayInZone). Card-row styling is unchanged.

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(automations): live-tick the relative next-run label

The relative next-run label was frozen at its first-render `now` and
only refreshed on remount. A shared 30s useNow() clock (a module-level
singleton via useSyncExternalStore) now drives live re-renders, so the
delta counts down while the page stays open. TasksPage owns the one
ticker and passes `now` to each row, keeping the row a pure function of
props.

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(automations): round next-run label to nearest unit

Flooring understated the relative next-run label near a unit boundary:
a task 1h49m away read "in 1 hour". formatNextRunAt now rounds to the
nearest minute/hour/day and promotes on carry (each threshold tests the
already-rounded value), so 1h49m reads "in 2 hours" and a delta that
rounds up to a full unit shows "in 1 hour"/"in 1 day" rather than
"in 60 mins"/"in 24 hours".

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* test(automations): e2e for live-ticking next-run countdown

Adds a Playwright test to the scheduled-tasks page suite proving the
relative next-run label re-renders on its own as time passes (the shared
useNow() ticker), with no navigation. Uses clock mocking for determinism:
pins the browser clock 40 min before the server's next_run_at, asserts
"Next run in 40 mins", fast-forwards 35 min past many 30s ticks, then
asserts the same row updated to "Next run in 5 mins". LLM-free.

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-26 22:01:01 -07:00
Zeyi (Rice) Fan a4a73ffe7f feat(cli): include auth override env var in non-loopback bind warning (#3320)
## Related issue

N/A

## Summary

- When `omnigent server` binds a non-loopback interface, it auto-enables accounts (login) mode and prints a warning.
- The warning now explicitly names `OMNIGENT_AUTH_ENABLED=0` as the override to keep single-user mode.
- Kept the warning to the canonical env var; removed any mention of the deprecated alias.
- Improved the rendered indentation so the override sentence starts on its own line.

## Test Plan

- `uv run ruff check omnigent/cli.py`
- `uv run pytest tests/cli/test_bind_auth_defaults.py -q`

Both pass.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

The existing `tests/cli/test_bind_auth_defaults.py` already exercises the non-loopback auto-enable path and the explicit `OMNIGENT_AUTH_ENABLED=0` override. This change only updates the warning copy.

## Changelog

`omnigent server` now tells users they can set `OMNIGENT_AUTH_ENABLED=0` to override automatic multi-user mode when binding to a non-loopback interface.
2026-07-26 20:44:11 -07:00
Zeyi (Rice) Fan f2b2f80948 refactor(server)!: remove deprecated OMNIGENT_ACCOUNTS_ENABLED env alias (#3322)
## Related issue

N/A

## Summary

- Remove the long-deprecated `OMNIGENT_ACCOUNTS_ENABLED` environment-variable alias for the multi-user auth enable switch. The canonical name `OMNIGENT_AUTH_ENABLED` has existed since the repository was open-sourced.
- Strip the alias logic from `omnigent/server/auth.py::_auth_enabled()`, the explicit-auth check in `omnigent/cli.py::_apply_bind_auth_defaults()`, and the runner env-propagation allowlist in `omnigent/host/connect.py`.
- Delete the tests that exercised the alias and the obsolete comment in `tests/conftest.py`.

## Test Plan

- `uv run ruff check omnigent/server/auth.py omnigent/cli.py omnigent/host/connect.py tests/conftest.py tests/cli/test_bind_auth_defaults.py tests/server/test_accounts.py tests/e2e/test_local_server_lifecycle_e2e.py` passed.
- `uv run pytest tests/cli/test_bind_auth_defaults.py tests/server/test_accounts.py -q --no-header` passed (95 items).
- `uv run pytest tests/server/test_accounts.py::test_resolve_auth_source_defaults_to_header tests/server/test_accounts.py::test_resolve_auth_source_opt_in_selects_accounts tests/server/test_accounts.py::test_factory_defaults_to_header_when_env_unset tests/cli/test_bind_auth_defaults.py -q --no-header` passed (15 items).
- Verified no remaining references with `grep -R "OMNIGENT_ACCOUNTS_ENABLED" . --exclude-dir=.git --exclude-dir=.venv`.

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [x] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Removed the tests that specifically covered the deprecated alias; remaining tests continue to validate `OMNIGENT_AUTH_ENABLED` behavior. The refactor does not change the `OMNIGENT_AUTH_ENABLED=1 | =0` semantics.

## Changelog

[Breaking] The deprecated `OMNIGENT_ACCOUNTS_ENABLED` environment variable has been removed; use `OMNIGENT_AUTH_ENABLED` instead.

BREAKING CHANGE: Users and deploys still setting `OMNIGENT_ACCOUNTS_ENABLED` must rename the variable to `OMNIGENT_AUTH_ENABLED` before upgrading; the old name is no longer read or propagated.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-27 03:35:35 +00:00
Serena Ruan c53c631bae docs(projects): update PRD status — Phase 2 done, Phase 3 & 4 postponed (#3321)
Bring the Projects PRD implementation-status section in line with what has
shipped and what remains:

- Mark backend `config` hardening (size bound + non-dict coercion) as done —
  both already landed in the project store.
- Move the completed Benchmark (#3094) and Phase 2 (project defaults) items out
  of TODO into their own "Done" sections.
- Correct a stale claim that the new-session prefill machine still reads the
  `omni_project` label — it was collapsed to config-only in Phase 2. The one
  remaining UI label reader (the Settings archived-project picker) is folded
  into the Phase 4 retire-label-path step instead.
- Postpone Phase 3 (memory & context) and Phase 4 (label consolidation) with
  distinct triggers: Phase 3 waits for customer demand; Phase 4 waits until
  telemetry shows most clients have migrated to a version that writes
  `project_id`.

Co-authored-by: Isaac
2026-07-27 11:25:21 +08:00
Zeyi (Rice) Fan 5169c918c6 fix(claude-native): escape unsupported Claude Code slash commands (#3319)
## Related issue
N/A

## Summary
- Updated `inject_user_message()` in `omnigent/claude_native_bridge.py` so user messages that start with a Claude Code UI-only/unsupported slash command (`/help`, `/exit`, `/quit`, `/doctor`, `/cost`, etc.) are escaped before being pasted into the TUI.
- Escaping inserts an invisible zero-width no-break space before the leading `/`, causing Claude Code to treat the input as regular user text while the user still sees their slash.
- Supported slash commands (`/clear`, `/compact`, `/effort`, `/model`, `/ultrareview`, `/branch`, `/fork`) and unknown skill commands pass through unchanged.

## Test Plan
- Added parametrized unit test for `_escape_unsupported_slash_command`.
- Added payload test verifying `/help` gets the escape prefix and `/clear` does not.
- Ran targeted injection tests and pre-commit:
  - `uv run pytest tests/test_claude_native_bridge.py::test_escape_unsupported_slash_command tests/test_claude_native_bridge.py::test_inject_user_message_escapes_unsupported_slash_command_payload -q`
  - `uv run pytest tests/test_claude_native_bridge.py -k "inject_user_message" -q`
  - `uv run ruff check omnigent/claude_native_bridge.py tests/test_claude_native_bridge.py`
  - `uv run ruff format omnigent/claude_native_bridge.py tests/test_claude_native_bridge.py --check`
  - `uv run pre-commit run --files omnigent/claude_native_bridge.py tests/test_claude_native_bridge.py`

## Demo
N/A

## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes
N/A — new unit tests directly cover the escaping decision and the payload path.

## Changelog
Unsupported Claude Code slash commands are now escaped and sent as regular user messages instead of leaving the native terminal in an undriveable state.
2026-07-27 03:16:44 +00:00
Serena Ruan 35a83b6be6 fix(projects): guard settings inputs during config load; correct worktree doc (#3316)
* fix(projects): disable settings inputs during config load; correct worktree doc

Two non-blocking follow-ups from the PR #3221 review:

- Gate the worktree toggle, workspace Browse trigger, and path input on
  `isLoading`, matching the host Select. Previously an edit made in the load
  window would be clobbered by the seeding effect once the fetch settled.
- Rewrite the `use_worktree` docstring to match the opt-in implementation
  (only `true` is written; `false` is never stored and treated as unset).

Co-authored-by: Isaac

* docs(projects): mark backend config hardening as done in PRD

The two #3108 config-hardening follow-ups (size bound + non-dict coercion)
already landed in the project store; move them from "deferred" to a  bullet
so the PRD status matches the code.

Co-authored-by: Isaac
2026-07-27 10:48:46 +08:00
Serena Ruan 77ed2a2c83 feat(projects): project settings editor + config-driven composer prefill (Phase 2) (#3221)
* feat(projects): project settings editor + config-driven composer prefill (Phase 2)

Add a "Project settings" dialog to set a project's stored session defaults
(host, working directory, agent, opt-in random worktree) and wire the new-chat
composer to prefill from that stored config, retiring the newest-session
inference so stored config is the single source of truth.

- ProjectSettingsDialog: edit + persist config {host_id, workspace, agent_id,
  use_worktree}; worktrees opt-in (default OFF, store true when on). Reuses the
  composer's host/agent pickers and filesystem browser.
- projectPrefill: collapse to config-only seeding; unset fields fall through to
  the composer's generic defaults. Honor a stored sandbox default via
  selectSandbox (gated on managed sandboxes). Remove useNewestProjectSession.
- Extract the nested-dropdown dismiss guard into a dependency-free module shared
  by the settings and scheduled-task dialogs.

Co-authored-by: Isaac

* fix(projects): repair CI — Sidebar test mocks, e2e rewrites, retire inference e2e

- Add useProjectConfig/useUpdateProjectConfig to all 10 Sidebar test mocks
  (Sidebar now mounts ProjectSettingsDialog, which calls them).
- Rewrite the settings-dialog e2e to create the project via POST /v1/projects
  instead of the flaky row-kebab move-to-project flow.
- Fix the composer-prefill e2e to stub GET /v1/sessions/projects (bare array),
  the real endpoint useProjects hits.
- Remove test_start_session_project_prefill — it exercised the newest-session
  inference path this PR retired; config-driven prefill replaces its coverage.

Co-authored-by: Isaac

* fix(projects): address review — no data-loss on failed config load; fresh prefill after save

Blocking issues from the PR review:

1. Data loss: saving the settings dialog after a failed config GET sent `{}`,
   which the server reads as "clear stored defaults". Now `useProjectConfig`'s
   isError is surfaced; a first-class project whose config failed to load blocks
   Save (with a notice), the seed effect skips a blank draft, and onSubmit bails.

2. Stale prefill after save: useUpdateProjectConfig only invalidated, so the
   composer's one-shot prefill could latch onto a stale cached config (30s
   staleTime) and drop just-saved defaults. It now setQueryData's the fresh
   config and upserts the projects list (so a promoted label-only folder
   resolves to its new id immediately).

Tests: dialog load-error blocks Save; hook seeds config + upserts list on
success; useProjectConfig disabled on null id and surfaces isError.

Co-authored-by: Isaac
2026-07-27 10:14:46 +08:00
Anthony Ivan 6c42dfe26b feat(Policy): Make dangerous shell command gating configurable, fix UI-created global policies getting skipped by default (#3297)
* Make dangerous shell command gating configurable

Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>

* Clarify dangerous shell policy settings

Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>

---------

Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
2026-07-27 02:12:21 +00:00
Zeyi (Rice) Fan 96935e03b4 fix(web): center sidebar header buttons and soften session row hover (#3311)
* fix(web): center sidebar header buttons and soften session row hover

## Related issue
N/A

## Summary
- Vertically center section header action buttons (Projects `+`, Sessions kebab, etc.) with their titles by using `top-1/2 -translate-y-1/2` instead of `top-0.5`.
- Remove the 1 px lift on session row hover (`motion-safe:hover:-translate-y-px`) so rows stay visually anchored.
- Calm the hover flash by dropping the bouncy Otto-token transition on rows and reducing the global `--sidebar-hover` tint from 5% to 3%. Rows now use the same plain `transition-colors` pattern as the rest of the sidebar hover surfaces.
- Make `SIDEBAR_ACTIVE_HIGHLIGHT` also specify `:hover` styles so active items (current page, selected session, drop target) keep their active background on hover instead of switching to the hover tint.

## Test Plan
- `cd web && npm install && npm run dev`
- Hover over Projects/Sessions headers and confirm action buttons are vertically centered with the title text.
- Hover over active items (e.g., current page in the top nav, selected session row, current Inbox) and confirm the background stays in the active state and does not flash.
- Hover over inactive session rows and confirm the row no longer shifts up and the background highlight is subtler.

## Demo
Subtle hover/positioning polish. Verify by hovering items in the sidebar — buttons align with title baselines, rows stay still on hover, and active items don't flash.

## Type of change
- [x] Bug fix
- [x] UI / frontend change
- [ ] Feature
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes
Visually verified by inspecting the relevant Tailwind classes and CSS variables. No test coverage changes; the existing `Sidebar.projectHeaderChevron.test.tsx` covers header layout, and the hover behavior is primarily CSS.

## Changelog
Sidebar header action buttons are now vertically centered with section titles. Session row hover is smoother, and active items no longer flash when hovered.

* test(e2e-ui): regenerate visual baselines

---------

Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-27 01:56:48 +00:00
Zeyi (Rice) Fan 6a0e09ed42 refactor(theme): drive shell night mode from selected theme source (#3309)
## Related issue

N/A

## Summary

- Replace the web-resolved-theme bridge with a single cross-shell contract, `setThemeSource(theme)`, so the web app only reports the user's chosen theme source and each shell drives its own OS-level dark mode.
- Android: `MainActivity` now extends `AppCompatActivity`; `OmnigentBridgeListener` maps `setColorScheme` to `AppCompatDelegate.setDefaultNightMode`; system-bar icon contrast is derived from `resources.configuration.uiMode`. Removes `ResolvedColorScheme.kt`, the root-class MutationObserver, and the top-level navigation reset on init.
- iOS: Add a `ThemeSource` enum and `ThemeController` singleton inside the existing `OmnigentWebView.swift` target file to avoid `.pbxproj` edits; wire `setColorScheme` through the JS bridge and apply it via `.preferredColorScheme(...)` and `window.overrideUserInterfaceStyle`.
- Web: Update `nativeBridge.setThemeSource`, remove the `omnigent-native-ready` queue, and update `ThemeProvider`/`nativeBridge` unit tests.
- Android and web unit tests are updated to match the new contract.

## Test Plan

- iOS: `cd web/ios && xcodebuild -project Omnigent.xcodeproj -scheme Omnigent -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -configuration Debug -only-testing:OmnigentTests test`
- Android: `cd web/android && ./gradlew :app:testDebugUnitTest`
- Web: `cd web && npm install && npm run type-check && npm run test -- ThemeProvider.test.tsx nativeBridge.test.ts`

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Manual verification completed for iOS: the app builds and `OmnigentTests` passes in the iPhone 17 Pro simulator. Android and web test suites were not run end-to-end in this session, but the affected unit tests were updated in the same change.
2026-07-26 18:53:49 -07:00
Zeyi (Rice) Fan ba241c3592 feat(dev): add justfile and mobile simulator lanes (#3310)
## Related issue

N/A

## Summary

- Add a top-level `justfile` that groups common local dev tasks (`run-ios`, `run-android`, `dev`, `electron-dev`, `lint`, `normalize-locks`, etc.) with hidden `_ensure-*` / `_check-*` prerequisites.
- Add an iOS `simulator` Fastlane lane that builds the Debug .app, installs it on an already-created iOS Simulator, and launches it.
- Add Android Gradle tasks (`runDebug`, `reverseProxy`) for launching the debug APK and running `adb reverse`.
- Fix the Fastlane `xcodebuild` invocation to use camel-case `derivedDataPath` so the built `.app` is written where the lane expects it.
- Export `FASTLANE_SKIP_UPDATE_CHECK=1` in the justfile.
- Document the new `justfile` recipes concisely in `AGENTS.md`.

## Test Plan

- `just --list` shows grouped recipes.
- `just run-ios` built/launched the iOS app in the iPhone 17 Pro Simulator.
- `pre-commit` passes on the touched files.

## Demo

N/A

## Type of change

- [x] Feature
- [ ] Bug fix
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Verified manually by running `just run-ios` and watching the Omnigent app launch in the iOS Simulator.

## Changelog

Add a top-level `justfile` with recipes for launching the iOS Simulator, running the Android debug build, starting the omnigent dev pod, and running pre-commit/lockfile normalization.
2026-07-27 01:20:34 +00:00
Bryan Li d287e7c903 feat(web): 3D model preview for STL / 3MF / OBJ files (#3007)
* feat(web): 3D model preview for STL / 3MF / OBJ files

Selecting an .stl / .3mf / .obj file in the Files browser now renders an
interactive WebGL preview (orbit/zoom/pan) instead of the "Preview not
available for binary files" placeholder.

- Add `isModelFile()` to codeViewerHelpers (MIME-first, extension fallback),
  scoped to exactly STL/3MF/OBJ.
- New lazy-loaded `ModelViewer` component (three.js STLLoader/3MFLoader/
  OBJLoader) with camera + OrbitControls, lighting, auto-fit, loading/error
  states, and full scene teardown on unmount.
- Dispatch models before the binary-rejection branch in CodeViewer; treat
  them like images in FileViewer (diff/source-mode suppressed).
- three.js pinned at 0.185.1 and code-split into its own chunk so it stays
  out of the main bundle.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(web): address model-viewer review — unified resolver, recovery, teardown

Resolve the four blocking issues from cross-vendor review of the 3D model
preview:

1. Unified format interface: add one shared `getModelFormat(path, contentType)`
   resolver (MIME-first, extension fallback) used by BOTH `isModelFile`
   dispatch and `ModelViewer`'s loader selection, so a MIME-matched file with
   an unknown extension parses via the correct loader instead of erroring.
   `isModelFile` is now `getModelFormat(...) !== null`.
2. Error state no longer unmounts the canvas: the container is always mounted
   and the error is an overlay on top, keeping the ref alive so an
   invalid→valid prop change recovers.
3. Single idempotent `teardownScene()` called from both the init failure path
   and the effect cleanup, so a partial init (renderer/controls/context/RAF)
   can't leak on failure.
4. Empty/degenerate models (e.g. comment-only OBJ) are validated for a
   non-empty, finite bounding box before fitting; invalid bounds route to the
   error UI instead of a blank canvas.

Adds ModelViewer.test.tsx (MIME-only loader selection, malformed/empty/NaN →
error, invalid→valid recovery, failure-path + unmount teardown) and
getModelFormat unit tests.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* feat(web): theme-aware 3D model preview (light/dark)

ModelViewer previously hardcoded a neutral STL material, fixed light
intensities, and a transparent canvas, so the 3D preview ignored the app
theme. Make it theme-aware off the SAME next-themes source Monaco and the
terminal use (`useTheme().resolvedTheme`), so it tracks light/dark and
updates live when the user toggles the theme with a model open.

- Add a pure `modelViewerTheme(resolved)` map in codeViewerHelpers (mirrors
  `resolvedThemeToMonaco`): background clear color, STL default material, and
  ambient/key light intensities per mode — brighter lights in dark so the
  mesh stays legible. Shared across STL/3MF/OBJ in the one unified pipeline.
- ModelViewer seeds the scene from the active mode and keeps light/material
  handles on its resource bag so a theme toggle recolors the live scene in
  place (clear color + intensities + STL color) with no reload/reparse.
- Drop the transparent (alpha) canvas in favor of a theme-derived opaque
  background so the preview sits flush with the panel in both themes.
- Tests: three theme-awareness cases (light build, dark build, live toggle
  without rebuild) mirroring the next-themes mock pattern in
  MonacoCodeEditor.test.tsx, plus modelViewerTheme unit tests.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* test(web): 3MF MIME-only dispatch + prune package-lock churn

Add MIME-only 3MF coverage mirroring the existing STL/OBJ tests: a file
with an absent/unrecognized extension but a `model/3mf` content type must
resolve to the 3MF loader in ModelViewer and route to <ModelViewer> in
CodeViewer, exercising the shared getModelFormat() resolver.

Regenerate web/package-lock.json so the diff vs origin/main is limited to
the `three` dependency subtree — dropping unrelated resolved-URL
normalization churn from an earlier regen.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(web): dispose material textures in ModelViewer teardown

disposeObject() freed each mesh's geometry and material but not the
textures the material references (map, normalMap, roughnessMap, …), so a
textured 3MF leaked its GPU textures every time the viewer unmounted.
three.js frees neither the material nor its textures automatically.

Add disposeMaterial(), which disposes every texture slot on a material
(detected via the three.js `isTexture` flag, robust to multiple three
copies) before disposing the material itself. Extend the ModelViewer
teardown unit test with a textured-material mesh and assert its textures
are released on unmount.

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* test(e2e): cover 3D model preview in the Files browser

Add a Playwright e2e test that seeds an ASCII STL and an OBJ file, opens
each in the Files browser, and asserts the ModelViewer mounts: the
`3D preview of …` canvas host renders a <canvas>, the "Unable to render
3D model" overlay never shows (so parsing and WebGL both succeeded), and
the flow does NOT fall through to the binary placeholder or a source
view. STL exercises MIME-based routing (application/vnd.ms-pki.stl); OBJ
exercises the extension fallback. Seeded via the filesystem PUT endpoint
(no agent run), mirroring the existing image/pdf rendering e2e tests.

This satisfies the E2E UI Required gate for the model-preview feature.

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(web): resolve the 3D-viewer deps from the public npm registry

The three.js stack this PR added (three, @types/three,
@dimforge/rapier3d-compat, @tweenjs/tween.js, @types/stats.js,
@types/webxr, fflate, meshoptimizer) was locked with `resolved` URLs
pointing at an internal mirror (npm-proxy.dev.databricks.com), while the
rest of package-lock.json resolves from registry.npmjs.org. Public CI
can't reach that mirror, so `npm ci` timed out fetching
three-0.185.1.tgz (ETIMEDOUT) and failed the install-dependent checks.

Repoint just those eight `resolved` URLs to the canonical
registry.npmjs.org form. Integrity hashes are unchanged (the mirror
served identical tarballs), so this only changes where the tarballs are
fetched from, not what is installed. `npm ci --legacy-peer-deps` now
succeeds from a clean node_modules, and `npm install --package-lock-only
--legacy-peer-deps` produces no further diff, so the lockfile-up-to-date
gate stays green.

Signed-off-by: Bryan Li <bryan.li@gmail.com>

---------

Signed-off-by: Bryan Li <bryan.li@gmail.com>
2026-07-26 18:15:42 -07:00
Bryan Li bf5b3c3a61 fix(android): honor system dark mode (#3006)
* fix(android): honor system dark mode

Signed-off-by: Bryan Li <15131870+btli@users.noreply.github.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(android): sync system bar contrast

Signed-off-by: Bryan Li <15131870+btli@users.noreply.github.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(android): harden resolved theme sync

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* refactor(android): decode theme at bridge boundary

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* test(android): tighten theme bridge coverage

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(android): drop WebView algorithmic darkening

Algorithmic darkening inverts the SPA when the user forces light mode
while the OS is dark: the page's root color-scheme is then 'light', so
WebView treats it as dark-unaware and darkens it algorithmically,
leaving dark status-bar icons over a darkened page. With targetSdk >= 33
the DayNight host theme alone makes prefers-color-scheme track the OS,
so the darkening flag added nothing for the system-mode path and only
broke the forced-light path. Verified on an API 34 emulator across the
OS-light/dark x app-System/Light/Dark matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(web): keep Electron on the selected theme, not the resolved one

Reporting only resolvedTheme regressed Electron system mode: an explicit
Light selection under a light OS changes no resolved value, so no report
fired and themeSource stayed 'system' — the shell chrome then flipped
dark with the OS while the app was forced light. Report the resolved
scheme first (Android system-bar contrast) and follow with 'system'
while that is the selection: Electron keeps the last report, so it
tracks the OS in system mode and pins to explicit selections, including
ones that leave resolvedTheme unchanged. Android drops 'system' at the
bridge, so its behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix: route native themes by consumer

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(android): harden system bar theme sync

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* test(android): clean up theme bridge state

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(android): install theme bridge at document start

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(android): resync system bars on live theme changes

Signed-off-by: Bryan Li <bryan.li@gmail.com>

* style(android): format theme test

Signed-off-by: Bryan Li <bryan.li@gmail.com>

---------

Signed-off-by: Bryan Li <15131870+btli@users.noreply.github.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 17:10:16 -07:00
Zeyi (Rice) Fan 6d32e8fdbd ci(release): skip GitHub releases for rc/dev/pre tags (#2962)
github-release.yml fired on every v[0-9]* tag push and created an
unpublished DRAFT release for rc/dev/alpha/beta tags. Nothing downstream
depended on those drafts — draft-release-notes.yml already skips rc,
finalize-release.yml refuses rc, and the Docker/homebrew/changelog
workflows fire on the tag push / release:published directly. The drafts
just accumulated (and rehearsal rcs had to be gh-release-deleted during
cleanup).

Add a guard that skips the draft-release job for rcN/devN/preN tags
(trailing digit required so a substring like 'dev' in a mistyped tag can't
trip it). Drop the now-dead alpha/beta arms — this repo only cuts rc
pre-releases — and align the same rc/dev/pre pattern + comments across
the other release-adjacent workflows for consistency. Update release.yml's
Next-steps text and RELEASING.md accordingly.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-26 15:59:43 -07:00
Zeyi (Rice) Fan b0cabcb336 fix(ios): block cross-origin redirects in the post-consent workspace probe (#3115)
## Related issue

Closes #[F-CR-7]

## Summary

- After a user consents to an unknown server (deep link) or types a server URL, `WorkspaceURLExpander.expandIfNeeded` issued a HEAD probe via `URLSession.shared` with no redirect policy, so the consented host could 3xx-redirect the probe to a different origin — including a local-network service — breaking the consent alert's promise that the app only talks to the host the user approved.
- The probe now defaults to a dedicated `URLSession` backed by `SameOriginRedirectHandler`, a `URLSessionTaskDelegate` that follows only same-origin redirects (scheme + host + port match) and blocks any cross-origin redirect by returning `nil` from `willPerformHTTPRedirection`.
- As defense in depth, `expandIfNeeded` additionally verifies `response.url`'s origin matches the approved origin, so a cross-origin response is never trusted even if a caller supplies a bare session without the redirect delegate.
- Rebased onto #3179 (F-CR-6) and deduped: removed my `--omnigent-deep-link` test hook (subsumed by #3179's `--omnigent-open-url` / `--omnigent-reset-state` seam), and consolidated the two `MockHTTPServer` copies into one shared file compiled into both test targets.

## Test Plan

- Unit: `WorkspaceURLExpanderTests.testRejectsResponseFromDifferentOrigin` returns a `server: databricks` 200 whose `url` is a different origin and asserts the URL is left unchanged.
- Integration (simulator, real local HTTP network): `WorkspaceURLExpanderRedirectTests.testBlocksCrossOriginRedirect` / `testFollowsSameOriginRedirect` assert a cross-origin redirect is blocked (response stays 302 on the approved port) and a same-origin redirect is followed. Confirmed meaningful: the cross-origin test fails when the delegate is reverted to follow-all-redirects (the vulnerable behavior).
- UI (simulator): `RedirectConsentUITests.testDeepLinkConsentOpensApprovedServer` drives the deep-link consent flow via #3179's `--omnigent-open-url` + `--omnigent-reset-state` seam and asserts the alert appears, "Open" loads the approved server's WebView.
- Ran on iPhone 17 simulator: all 8 expander/redirect tests + the UI smoke test + all 26 F-CR-6 deep-link tests pass; full project builds.
- Note: the UI test cannot exercise the redirect itself — a localhost deep link infers `http`, and the probe is https-only, so the probe never fires for loopback. The redirect policy is verified over a real local network by the integration test instead.

## Demo

N/A

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [x] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [x] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Manual verification: built and ran the new unit, integration, and UI tests on the iPhone 17 simulator; confirmed all pass and that the integration test fails against the vulnerable (follow-all-redirects) baseline, proving it is a meaningful regression test. Also ran all F-CR-6 tests after the dedup to confirm no regression from #3179's shared seam.

## Changelog

The iOS app no longer follows cross-origin redirects when probing a newly approved server for the Databricks workspace mount, so a consented host can't redirect the probe to a different origin.
2026-07-26 15:57:44 -07:00
Rahul Ravindranathan 61fd72350e feat(automations): rename Scheduled Tasks UI to Automations (UI only) (#3260)
CI / Pytest (runtime-core) (push) Has been cancelled
Lint / Version lockstep check (push) Has been cancelled
Doc sync / Classify and draft docs (push) Has been cancelled
Lint / Pre-commit checks (push) Has been cancelled
OSS Scorecard / Scorecard analysis (push) Has been cancelled
UI Preview / notify (push) Has been cancelled
UI Preview / build (push) Has been cancelled
UI Preview / cleanup (push) Has been cancelled
Windows (native) / Windows smoke + unit (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
web Tests / npm test (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
UI Preview / deploy (push) Has been cancelled
CI / gate (push) Failing after 1s
Lint / gate (push) Failing after 1s
web Tests / gate (push) Failing after 1s
* feat(automations): rename Scheduled Tasks UI to Automations (display copy only)

UI-facing name is now 'Automations'; internal name (DB/CRUD/API/components/
comments/route) remains 'scheduled task'. Changes limited to user-visible
display strings in 5 source files + 2 test files.

Changed:
- TasksPage.tsx: h1, search placeholder, load error, loading text, empty states
- Sidebar.tsx: nav label "Scheduled" → "Automations"
- CommandPalette.tsx: "Go to Scheduled tasks" → "Go to Automations"
- CreateScheduledTaskDialog.tsx: dialog titles + error messages
- Test assertions updated to match new copy

No component names, file names, types, data-testids, routes, or backend
paths were altered.

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* test(e2e-ui): regenerate visual baselines for Automations rename

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* docs(scheduled-tasks): document Automations (UI) vs scheduled-task (internal) naming

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* test(ui-snapshot): regenerate visual baselines after main merge

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-25 11:02:53 -07:00
Pat Sukprasert 4788a77d54 test: stabilize two known flakes (dictation close, agent-info popover) (#3224)
Lint / gate (push) Failing after 1s
web Tests / gate (push) Failing after 0s
CI / gate (push) Failing after 1s
Doc sync / Classify and draft docs (push) Has been cancelled
OSS Scorecard / Scorecard analysis (push) Has been cancelled
Lint / Pre-commit checks (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
UI Preview / notify (push) Has been cancelled
UI Preview / build (push) Has been cancelled
UI Preview / cleanup (push) Has been cancelled
Windows (native) / Windows smoke + unit (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
Lint / Version lockstep check (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
web Tests / npm test (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
UI Preview / deploy (push) Has been cancelled
* test: stabilize two known flakes (dictation close, agent-info popover)

Two load-timing flakes that recur across PRs:

- Pytest (server-rest) test_dictation.py::test_stream_closes_take_on_
  abrupt_disconnect: on an abrupt disconnect the route offloads
  handle.close() to a thread. During teardown the loop's thread-pool
  executor may already be shutting down, so the offload raises and the
  old contextlib.suppress swallowed it — the take (and, for the remote
  engine, a worker slot) leaks. Fall back to a direct close() on the
  loop; it's a quick non-blocking free for every engine.

- E2E UI test_agent_info_popover.py: _open_popover single-clicked the
  trigger, but the button hover-opens on the click's own pointer arrival
  and the click's Radix toggle can flip it back shut past the
  HOVER_CLICK_GRACE_MS window under load, so the panel never mounts.
  Confirm the panel opened and retry the click from a closed state.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: stabilize scheduled-tasks time-picker flake

test_scheduled_task_create_edit_modal_and_time_picker had two coupled
races in the time-picker step (6/10 failures reproduced, no artificial
load needed):

- The picker is a Radix popover nested in the create-task dialog. The
  dialog's focus management can fire an interaction-outside that closes
  it the instant it mounts, so the minute cells unmount between the
  visibility check and the click (element-not-found / click timeout).
- Selecting a minute leaves the popover open, and an open floating-ui
  popover keeps recomputing its position — so the submit button (and,
  later, the edit-phase time input) stays perpetually "not stable" and
  detaches mid-click.

Extract a _pick_minute() helper that opens from a known-closed state and
retries until the cell is present, then dismisses the picker via a
click-outside (not Escape, which would bubble to the Radix Dialog and
close it) and waits for it to unmount so the layout settles before
submit. 0/12 clean + 0/8 under load after the fix.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-25 18:17:13 +07:00
Pat Sukprasert 7a73bc30a7 ci: clear stale waiting labels after author activity (#3242)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-25 17:24:54 +07:00
Jackson Zheng 0b4153548f Prevent inline base64 from leaking into replay context (#3267)
* fix: redact base64 from compaction history

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* Harden inline base64 redaction

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

---------

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
2026-07-25 00:04:14 -07:00
Tomu Hirata 4fb449187b fix(web): hide Smart Routing from native terminal sessions (#3259)
The in-session "Configure" model dropdown offered a "Smart Routing" option on
native terminal sessions (Claude Code, Codex, Pi, …). It's meaningless there:
a native CLI bakes its model into the launch argv once and can't per-turn
route, so picking it did nothing useful.

Add isNativeTerminalSession() (mirrors the server's
_native_coding_agent_for_session: native by omnigent.wrapper label OR resolved
harness) and exclude such sessions from costRoutingEligible in ChatPage, so the
Smart Routing option no longer appears in their Model dropdown. Brain-harness
sessions (claude-sdk / codex / pi, and the polly orchestrator) keep it.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-25 03:30:17 +00:00
Jackson Zheng 568d620da2 Polish sidebar session row layout (#3208) 2026-07-24 19:28:34 -07:00
Rahul Ravindranathan 039fa67089 feat(scheduled tasks): Run now, relative next-run, and Tasks-list row polish (#3218)
* feat(scheduled tasks): add windowed latest-run-status store query

Add ScheduledTaskStore.list_latest_run_status_for_tasks(ids) -> {id: status},
a single row_number()-windowed query (scheduled_at DESC, id DESC — same order
as list_runs) returning each task's most-recent run status. Powers the Tasks
list completion badge in one query instead of N per-row /runs fetches, and is
correct under overlapping run-now runs (unlike a denormalized last_run_status
column). Tasks with no runs are absent from the map.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* feat(scheduled tasks): run-now endpoint + status/next-run serializer fields

Backend for three Tasks-list run controls:

- last_run_status: _to_response now carries the task's most-recent run status
  (from the windowed store query), populated on list/get/patch. Force-fail of
  stale orphans runs BEFORE the status read so a dead run reports failed, not a
  stuck running.
- next_run_at: _to_response carries the live scheduler's authoritative next-fire
  ISO timestamp (scheduler.next_run_at) on list/get/create/patch — server-
  sourced, never client-recomputed (paused/unarmed → null).
- POST /v1/scheduled-tasks/{id}/run: an immediate manual fire that REUSES the
  shared fire path via build_run_now (same _run_fire_for_task body, dispatch/
  preflight seams, and in-flight overlap guard as the scheduler). Paused tasks
  are runnable (manual override); fire-and-forget → 202 Accepted. 409 when a
  fire is already in flight, 404 for a non-owned task, 503 when the scheduler
  subsystem is not running. Wired via app.state.scheduled_task_run_now.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* feat(scheduled tasks): status pill, run-now menu, next-run on rows

Wire the three run controls into the Tasks list UI:

- last_run_status → a completion pill on each row (Failed/Skipped/Running/
  Queued). Succeeded and never-run render NO pill (success is not noise);
  Failed is destructive, Skipped muted — matching the Paused pill styling.
- next_run_at → "Next: <time>" on the schedule subline, formatted in the
  task timezone via a new formatNextRunAt() that only FORMATS the server's
  ISO value (never client-recomputes; paused/unarmed → nothing).
- Run now → a "⋯ menu" item + useRunScheduledTaskNow mutation (POST
  /{id}/run) that invalidates the list + that task's runs so the pill
  updates. Runnable for paused tasks; row busy-disables while in flight.

scheduledTasksApi gains lastRunStatus + nextRunAt (interface + wire map)
and runScheduledTaskNow(). Unit tests: pill per status, no-pill cases,
next-run formatting (tz + calendar-day boundary), run-now mutation wiring.
e2e: new run-controls journey (Run now → recorded run + pill flips);
existing schedule-line assertions relaxed to to_contain_text now that the
server next-run renders on the same line.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* style(scheduled tasks): bump task title size/weight on rows

Make the scheduled-task row title slightly larger and bolder: text-sm →
text-base and font-semibold → font-bold. Subline, pills, and spacing are
unchanged. Updates the one TasksPage sort-order test that located the title
by its .font-semibold class to .font-bold.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Revert "style(scheduled tasks): bump task title size/weight on rows"

This reverts commit e0195ce3c4977abdaeba5316426029f808edeef6.

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* style(scheduled tasks): title 15px, metadata 13px on rows

Trim the row title to exactly 15px and the metadata subline to exactly 13px
using arbitrary-px classes (text-[15px] / text-[13px]) — the app root scales
rem ~1.125×, so the standard text-sm/text-xs would render 15.75/13.5px and
can't hit the exact target. Weights unchanged: title font-semibold (600),
subline no weight class (inherits 400). Pills, spacing, next-run text, and the
⋯ menu are untouched.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* style(scheduled tasks): lighten metadata subline on rows

Soften the row metadata subline one notch to a lighter gray via an opacity
step on the same theme token: text-muted-foreground → text-muted-foreground/80.
Theme-aware (works in light + dark), size unchanged (13px), and the next-run
<span> keeps inheriting the same color (no own color class). Title, pills,
spacing, and the ⋯ menu are untouched.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* style(scheduled tasks): tighten row spacing 2px, remove run-status pill

- Row vertical padding py-3 → py-[11px]: trims each side 1px so the gap
  between adjacent rows drops from 24px to 22px (the list is flex-col with no
  gap, so the row padding is the whole inter-row spacing).
- Remove the last-run status pill (Failed/Skipped/Running/Queued) entirely per
  design: drop the render block, the RUN_STATUS_PILL map, the statusPill local,
  and the now-unused ScheduledTaskRunStatus import. The Paused pill is kept
  as-is. The lastRunStatus API/store field is left in place (harmless data;
  only the visual is removed). Subline, next-run text, and the ⋯ menu unchanged.

Drops the per-status pill test cases in ScheduledTaskRow.test.tsx (that UI is
gone); keeps the paused-pill, next-run, and run-now tests.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* style(scheduled tasks): relative "Next run in Xh" on rows

Switch the row next-run display from an absolute label ("Next: Today 9:00 AM")
to a compact relative delta ("Next run in 15h" / "in 6d" / "soon"):

- formatNextRunAt now returns a delta (nextRunAt − now): <60m → "in Xm" (min
  "in 1m"), <24h → "in Xh", else "in Xd", all floored; a delta below 1 min
  (imminent / clock skew) → "soon"; null/unparseable iso → null. The `timezone`
  param is dropped (a pure delta needs no zone) — call site + useMemo deps
  updated. This only formats HOW FAR AWAY the server's authoritative next_run_at
  is; it never recomputes WHICH instant is next on the client, so the old
  "no client-recomputed countdown" rule still holds.
- Row prefix "Next: " → "Next run " so it reads "Next run in 15h".

Tests: rewrote the formatNextRunAt unit tests for the relative buckets +
boundaries + "soon" + null; updated the row test to the "Next run in …" prefix;
reconciled the e2e (the old count==0 "Next run" guard flips to positively
asserting the server-derived relative label — its real intent, no client
recompute, is unchanged).

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* style(scheduled tasks): darken row hover background

Bump the full-row hover tint one notch: hover:bg-muted/50 → hover:bg-muted/70
(same theme-aware `muted` token, higher opacity). The color-mix stays
`var(--muted) N% transparent`, so in light the effective tint goes ~2.9% → 4.1%
black and in dark the alpha goes 0.5 → 0.7 — visibly stronger but still subtle.
Comment updated to match. Nothing else changes.

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-24 18:20:45 -07:00
Jackson Zheng 397aeeb293 Support background titles for native Codex (#3199) 2026-07-24 17:59:02 -07:00
Jackson Zheng 9a354b6700 fix(claude-native): durably persist compaction boundary on resume/replay (#3118) 2026-07-24 17:40:58 -07:00
Thomas Garnier 3b9d8d55a4 Add databricks_cli secretless credential proxy type (#3080)
Adds a 'databricks_cli' credential_proxy type so sandboxed tools can use
the Databricks CLI without the real OAuth/PAT token ever entering the
sandbox. The operator lists which ~/.databrickscfg profiles to proxy;
each is materialized into the sandbox as a placeholder-only .databrickscfg
(oa_cred_* token), and the L7 egress proxy swaps the placeholder for the
real token on the way out.

- Refreshing token provider (DatabricksProfileTokenProvider) re-mints
  short-lived OAuth tokens via the databricks SDK for long sessions;
  CredentialRewriteRule gains an optional secret_provider and the proxy
  resolves secrets per-swap (offloaded via run_in_executor).
- Placeholder-only files are materialized into the sandbox scratch dir
  and pointed at via DATABRICKS_CONFIG_FILE / DATABRICKS_CONFIG_PROFILE.
- Requires the 'databricks' extra and linux_bwrap (the Go CLI ignores
  SSL_CERT_FILE on macOS, so darwin_seatbelt is rejected at parse time).
- Egress stays operator-listed: the workspace host must be named in
  egress_rules, consistent with the other credential_proxy types.

Signed-off-by: mxatone <mxatone@gmail.com>
2026-07-24 17:00:52 -07:00
Zeyi (Rice) Fan e1a3fdb82f chore(release): bump omnigent-slack to 0.7.0.dev0 and add it to the lockstep version cycle (#3207)
## Related issue

N/A

## Summary

- Bring `omnigent-slack` into the lockstep release cycle (now four packages, not three): its `[project].version` was stuck at `0.1.0` while the rest of the repo moved to `0.7.0.dev0`, so the extra pin and lockfile drifted.
- Pin `omnigent-slack==0.7.0.dev0` in the root `slack` optional-dependency extra, mirroring the existing `omnigent-client==` / `omnigent-ui-sdk==` sibling pins so a published `omnigent[slack]` always pairs with the matching `omnigent-slack` release.
- Teach `scripts/update_versions.py` (the engine behind `.github/workflows/bump-version.yml`) about the 4th package: rewrite the slack `[project].version` and the extra `==` pin on every bump, and scan `[project.optional-dependencies]` (not just `[project.dependencies]`) when verifying sibling pins. Regenerate `uv.lock`.

## Test Plan

- `uv run --no-project --python 3.12 --with packaging python scripts/update_versions.py check` → prints `0.7.0.dev0` (all four packages agree, all sibling `==` pins present).
- `uv lock` → "Updated omnigent-slack v0.1.0 -> v0.7.0.dev0".
- `uv run ... python -m pytest tests/scripts/test_update_versions.py` → 13 passed (updated the test fixture + assertions for the 4th package).
- `tests/test_version.py::test_version_matches_pyproject` still passes (root pyproject == `omnigent/version.py` at `0.7.0.dev0`).

## Demo

N/A

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Updated `tests/scripts/test_update_versions.py` to include `integrations/slack/pyproject.toml` in the `repo_copy` fixture and adjusted the lockstep assertions (5 changed files, 4 `9.9.9` occurrences in root pyproject, 1 in slack). Verified the full suite (13 tests) passes. Also ran `update_versions.py check` and `uv lock` manually to confirm lockstep + lockfile consistency.
2026-07-24 14:54:07 -07:00
Dhruv Gupta 86463e6129 docs(contributing): add Developer Certificate of Origin language and DCO file (#3252) 2026-07-24 20:27:37 +00:00
Cathy Yin 76281b9438 feat(onboarding): write a harness provider credential from the UI (M3 backend) (#3088)
CI / gate (push) Failing after 6s
Doc sync / Classify and draft docs (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
* feat(onboarding): report the installed-but-unconfigured harness state

Add the credential axis to the picker-facing readiness map so Claude, Codex,
and Pi report the yellow "installed but no credential" state — the signal the
web setup dialog needs to know when to offer an "Add credential" action
(the M2 keystone of Setup-From-the-UI). Purely additive: the values
("needs-auth" / "binary-missing") already exist in HarnessAvailability, and
the launch gate (harness_is_configured) stays binary-only, so a not-yet-authed
harness is never blocked from launching.

- New _family_provider_configured(): whether an omnigent-managed provider
  (API key / gateway) serves the harness's family, reading the same config
  omni setup's overview does. Subscription-kind is excluded (that lives in the
  CLI's own login, judged by harness_cli_logged_in). Local, side-effect free,
  never raises (fails to "no credential").
- Claude: ready when a provider is configured OR the CLI subscription login is
  present (was CLI-login only — an API-key-only user wrongly showed yellow).
  Checks config first to avoid the status subprocess on the common path.
- Pi: gains the axis it lacked entirely (was binary-only → always green once
  installed). No CLI login, so binary + provider: installed-but-no-provider is
  now "needs-auth".

Codex already had this (unchanged, the template). Qwen/OpenCode env-auth
unchanged.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* feat(onboarding): write a harness provider credential from the UI

Second PR of Setup-From-the-UI (M3, security-sensitive). Adds the path that
turns a yellow "installed but not configured" harness green from the browser
for Claude / Codex / Pi, host-agnostic (local or remote), reusing the
credential-write logic omni setup already uses.

Design: the server is an authz'd pass-through. It validates ownership + the
UI-auth allowlist and forwards the secret over the (TLS) tunnel; the host DAEMON
does the write on the runner. The server never persists the secret, and the
frame's secret_value field is redaction-named so it never lands on a telemetry
span. Gated behind OMNIGENT_HARNESS_INSTALL_ENABLED (default off) exactly like
the install route (404 when disabled).

- New non-interactive core omnigent/onboarding/harness_auth.py: store a key /
  gateway (secret → keychain, else ~/.omnigent/secrets.json; a providers: entry
  referencing keychain:<name>, never the raw key), adopt an existing host env
  var by reference (env:<VAR>, value never read), and detect adoptable env
  credentials (non-secret descriptors only). First provider on a family becomes
  the default; unsupported families/kinds are refused.
- New host.store_secret / _result frame pair; host daemon handler resolves the
  harness→family, calls the core, and re-reports readiness so the badge flips
  without a reconnect. Pi maps to its preferred anthropic family.
- New route POST /v1/hosts/{id}/harnesses/{harness}/credential (owner-scoped,
  allowlisted, flag-gated) + registry pending_secret_writes plumbing + tunnel
  result resolution.
- Regenerated openapi.json.

Tests: core unit tests (incl. the no-raw-secret-in-config invariant), frame
round-trip + telemetry-redaction, host-handler unit tests, and a full route
integration test over a fake tunnel (ownership, flag-off, allowlist, failure
mapping). 243 pass across the affected suites.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* feat(onboarding): detect adoptable credentials on the host (adopt flow)

Adds the read side of the adopt flow: a host.detect_credentials frame pair +
GET /v1/hosts/{id}/credentials/detected that reports the credentials already
present on the host as NON-secret descriptors (family + source label + env var
name), so the UI can offer a one-click "adopt" instead of asking the user to
paste a key they already have. The value is never read or sent — adopt writes
an env:<VAR> reference via the existing store_secret path.

Owner-scoped + flag-gated like the credential-write route. Decode drops
malformed entries so a garbled payload can't inject a non-string field the UI
would trust. Adds frame round-trip (+ malformed-drop), host-handler, and route
integration (+ flag-off) tests; regenerated openapi.json.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(onboarding): tighten the credential route + adopt guard (Polly review)

Two review fixes on the credential-write path:

- The route gated on ui_installable_harnesses(), which includes the env-auth
  opencode/qwen — the host handler then rejected them, turning a client/allowlist
  problem into a confusing 502. Add ui_credential_configurable_harnesses() (the
  Claude/Codex/Pi families the host can actually write) and gate on it, so
  opencode/qwen get a clean 400 with no frame forwarded.
- adopt_env_credential now refuses an env var that isn't set on the host —
  adopting an unset var would persist a provider entry that resolves to nothing
  at the first turn. (Runs on the runner, so os.environ is the host's env.)

Tests: opencode/qwen added to the 400-rejection parametrize; an unset-env-var
adopt rejection case.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(server): serialize concurrent credential writes to one host (Polly review)

Polly non-blocking note: unlike the install route (which coalesces via
inflight_installs), the credential route had no guard against overlapping
writes. The daemon's write is a non-atomic load→merge→save of config.yaml
(twice — entry, then default), so two writes to one host in quick succession
(a double-click, or key + gateway) could interleave and clobber a sibling
providers: entry.

Add a per-connection credential_write_lock held around the store-secret
round-trip so writes to one host serialize. A gateway/local host still
processes different hosts concurrently (the lock is per HostConnection).
Adds an integration test that holds the first reply and asserts the second
frame only reaches the host after the first completes.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(onboarding): make Pi's auth step UI-authable and trackable

Pi's setup_steps auth descriptor was still the M1 shape (action="setup",
command="omnigent setup", status_key=None). Two consequences surfaced in
manual testing: (1) status_key=None made the step "unknown", so the setup
dialog dropped it and wrongly showed "Pi is ready" with no action even though
readiness reported needs-auth; (2) even rendered it was a CLI signpost, not
the credential form.

Pi is UI-authable now (PR A gave it the needs-auth readiness axis; the UI has
the credential form), so its auth step becomes action="auth" (opens the inline
form, keyed on kind=="auth"), command=None (Pi has no subscription CLI login),
status_key="authed" (trackable, so it's not dropped and the dialog reflects
the real state). Qwen stays the untracked env-auth signpost (not UI-authable).
Updates the pi test and adds a qwen-stays-signpost test.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* chore: use the `omni` CLI alias (omni setup) in setup guidance

Rename user-facing "omnigent setup" → "omni setup" in the harness setup-step
descriptors, the setup hint, and their doc-comments. `omni` is the installed
console entry point (pyproject: omni = omnigent.cli:main) and is already used
elsewhere in the codebase, so the shorter alias is correct and consistent.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* test: fix CI drift on the M3 backend branch (omni setup + auth action)

Two "Pytest (misc)" failures on this branch were stale test expectations, not
product bugs:

- tests/host/test_connect.py asserted the unconfigured-launch error names
  "omnigent setup", but the earlier `omni` CLI-alias rename made the runtime
  message say "omni setup". Update the positive assertion and the cursor
  test's negative assertion (which guards that Cursor points at its own
  installer, not the generic setup command) to the new spelling.
- tests/test_harness_capabilities.py restricted setup-step actions to
  ("install", "command", "setup"), but Pi's UI-authable step uses action
  "auth" (added when Pi's credential step became a form). Add "auth" to the
  allowed set; codex's own two-step assertion is unchanged.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* test: harden the install-flow e2e against a slow picker render

test_install_button_installs_missing_harness opened the agent picker and
immediately clicked the Codex row, but the picker mounts its rows only after
the /v1/agents fetch resolves. Under CI load that render lags, and a menu
opened before the data lands can render empty or re-close on the update — so
the bare open-then-click flaked with a 30s click timeout, the Codex row never
becoming actionable (seen across two different shards). Open the picker, wait
for the Codex row and re-open if the menu flapped, then click. No product
change; passes locally unchanged (the retry is a no-op on the fast path).

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* test: settle agent data before opening the picker in the install e2e

The install-flow e2e flaked (30s click timeout on the Codex row, then on the
picker trigger via an overlay pointer-interception when reopened). Root cause:
the picker opened before the /v1/agents fetch settled, racing the menu-open
against a re-render. Wait for the composer's "Set up Codex" notice (rendered
only once the Codex agent + its unconfigured host state load) BEFORE opening the
picker, then open once and click. Passes locally repeatedly.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* test: stop driving the agent picker in the install e2e (kill the flake)

The picker interaction was redundant — the single seeded Codex agent is already
auto-selected, so the composer's "Set up Codex" notice is present without
opening the dropdown. Driving the picker only added a menu-open-vs-async-render
race that flaked under CI load. Wait for the notice directly (generous 60s) and
click it to open the setup dialog.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* test: wait for network idle before asserting the setup notice (install e2e)

The "Set up Codex" notice depends on two async fetches re-rendering the
composer (/v1/agents auto-selecting the agent, /v1/hosts marking its harness
unconfigured). On loaded CI runners that chain lagged past the timeout and the
assertion raced the still-loading landing screen. Wait for network idle and the
host chip (readiness present) before asserting the notice.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* test: drop networkidle wait in install e2e (WS keeps network busy)

wait_for_load_state("networkidle") never fires in this app — the shell holds a
long-lived sessions/updates WebSocket, so the network is never idle. That wait
just burned its timeout and then raced the still-loading landing screen (the
"Set up Codex" notice was intermittently absent on CI). Replace it with plain
element waits (host chip, then the notice) at a generous 60s, matching every
other e2e_ui test. Passes locally repeatedly.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(host): adopt an env credential under its own family, not the harness's

Review (isaac, MAJOR): pi consumes both the anthropic and openai families, so
the UI can offer an OpenAI env var (e.g. $OPENAI_API_KEY) as adoptable for pi.
`_handle_store_secret` derived the family solely from the harness (pi→anthropic)
and passed that to `adopt_env_credential`, so adopting that var wrote an
anthropic-family provider whose api_key_ref is env:OPENAI_API_KEY — mis-routed
to the anthropic endpoint, failing at run time. For the adopt kind, look the env
var up in the host's detected credentials and use its OWN detected family
(falling back to the harness family if absent). Adds a pi-adopts-OpenAI
regression test.

Also carry the install-flow e2e fix onto this branch: explicitly select Codex
in the picker and stub the /v1/sessions?kind=any agent scan so the seeded-DB
agents don't leak in and leave Claude Code selected (a CI-only flake).

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* fix(host): harden the UI credential-write path (review feedback)

Addresses Polly's blocking finding + hardening notes and Pat's nits on the
store_secret/adopt path:

- BLOCKING (adopt boundary): the daemon's adopt handler fell back to the
  harness-derived family when an env var wasn't detected, and adopt_env_credential
  only checked the var was *set*. An owner hitting the raw API could name any set
  env var (a DB password, an unrelated secret) and have it persisted as a provider
  credential sent to the vendor endpoint. Now the handler refuses an env_var that
  isn't in detect_adoptable_credentials() (no fallback) — enforcing server-side the
  same "only adopt what was detected" restriction the UI presents.
- secrets.py: create the file-backend secrets.json 0600 atomically via
  os.open(O_CREAT, 0o600) instead of open()+chmod-after, which briefly left a
  freshly-created file group/world-readable. Now network-triggerable, so worth
  closing. Fixes the stale "0600 from the start" comment.
- adopt_env_credential: presence-only env check (`in os.environ`, not `.get`) so
  the "never reads the value" contract stays literally true.
- gateway base_url: reject a non-http(s) scheme at write time rather than writing
  a malformed provider entry that fails opaquely at the first turn.
- connect.py: hoist the harness_auth / provider_config imports to module top
  (no circular import) to match the sibling onboarding imports.

Adds regression tests: adopt refuses an undetected env var, gateway rejects a
non-http base_url, and secrets.json is 0600 even under a permissive umask.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

---------

Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
2026-07-24 17:48:43 +07:00
dependabot[bot] 983c93c6ec chore(deps-dev): bump fast-uri from 3.1.2 to 3.1.4 in /editors/vscode (#3035)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.4.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 10:21:49 +00:00
Tomu Hirata 8b2276c529 fix(docker): wire llm/policies/routing into Docker entrypoint RuntimeCaps (#3222)
The Docker entrypoint's build_app() was constructing RuntimeCaps()
bare, so the llm:, policies:, and routing: blocks in a docker
deployment's config.yaml were silently ignored. This meant:

- Builtin policies that read event["llm_client"] (e.g.
  deny_trivial_to_expensive_model) would always see None and abstain.
- default_policies declared under policies: would never fire.
- LLM-based and external routing clients were never built.

Mirror the logic from cli.py: parse_server_llm / parse_default_policies
/ routing client construction are now applied before RuntimeCaps is
passed to init_runtime, putting docker deployments on par with the
CLI-started server.

Fixes #3159

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-24 10:14:59 +00:00
Serena Ruan e491999a14 fix(sessions): strip per-user pin keys from child-session summaries (#3214)
Follow-up to #3189. Every session serialization path collapses per-user
`omnigent.pinned.<user>` keys via `_labels_for_viewer` except
`_child_session_summary_from_conversation`, which passed `conv.labels` through
raw. Child sessions aren't pinnable today (the pin affordance lives only on
top-level sidebar rows), so this is a latent gap rather than a live leak — but
if a shared child were ever pinned, its summary would expose another viewer's
pin key.

- Strip any `omnigent.pinned.<user>` key from a child summary's labels. No
  collapse-to-canonical: there's no pin to surface, just the defensive strip.
- Test: a child carrying two users' pin keys yields a summary with no pin key,
  while unrelated labels survive.
- Correct the stale `useMigrateLocalPinsToServer` docstring: the migration
  patches the pinned-list cache (like `useTogglePinnedConversation`), it does
  not invalidate the pinned query.


Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-24 17:36:07 +08:00
Tomu Hirata 1674f686fe fix(pi-executor): add supportsUsageInStreaming:false to databricks-completions (#3203)
* fix(pi-executor): add supportsUsageInStreaming:false to databricks-completions

Gemini, Qwen, inkling, and other non-OpenAI models in the databricks-completions
provider reject stream_options (which Pi sends with include_usage:true by default)
with 400 'unknown field'. Add supportsUsageInStreaming:false to suppress it,
matching what pi_native_credentials.py already does for omnigent-completions.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi-executor): use openai-responses for newer GPT models (gpt-5-5, gpt-5-6-*)

Newer GPT models reject function tool calls via /chat/completions with 400.
The Databricks Responses API (/ai-gateway/codex/v1/responses) now supports
tool-result chaining on subsequent turns (previously it did not).

- Add databricks-openai provider using openai-responses at /ai-gateway/codex/v1
  for gpt-5-5, gpt-5-6-*, gpt-5-3-codex (matches pi_native_credentials routing)
- Keep databricks provider (openai-completions at /serving-endpoints) for
  older GPT models (gpt-5-4, gpt-5-4-mini) that work fine with /chat/completions
- Add _pi_needs_responses_api() helper mirroring pi_native_credentials
- Update _pi_provider_for_model() to route to databricks-openai when needed

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi-executor): add kimi to reasoning model fragments

kimi-k2-7-code streams output on reasoning_content channel like GLM/DeepSeek.
Without reasoning:true in the model entry Pi ignores reasoning_content and
sees an empty stream, throwing 'Stream ended without finish_reason'.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(test): update kimi model entry to expect reasoning:true flag

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi-native): add reasoning:true to kimi/glm/deepseek model entries

These models stream output on reasoning_content channel. Pi's openai-completions
parser requires reasoning:true on the model entry to consume that channel;
without it the stream has no content and the turn ends with
'Stream ended without finish_reason'.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(pi-native): exclude qwen3 from completions provider

qwen3 models return content as a typed array [{type:'reasoning',...},{type:'text',...}]
when tools are present, causing Pi's streaming handler to produce [object Object].
Same root cause as gpt-oss; same fix.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix: add inkling to reasoning model fragments and LLM detection

Both kimi and inkling stream output on reasoning_content channel with
content=null. Added inkling to _PI_REASONING_MODEL_FRAGMENTS (executor),
reasoning:true model entry condition (pi-native), and LLM name detection
tokens so it appears in the model list.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor: use allowlist for GPT completions-compatible models

Instead of a denylist of specific model ids that need the Responses API,
maintain an allowlist of GPT models known to work with /chat/completions.
Any GPT model not in the allowlist defaults to Responses API — safer
for new models not yet explicitly tested.

The executor's _pi_needs_responses_api now delegates to the same
implementation in pi_native_credentials for a single source of truth.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-24 18:29:07 +09:00
Tomu Hirata 0c20a59ca6 feat(smart-routing): enable routing from config, drop OMNIGENT_SMART_ROUTING (#3215)
Smart routing was gated behind an OMNIGENT_SMART_ROUTING=1 opt-in on top of the
routing/llm config. The env is redundant: build the routing client whenever the
config supplies one — a server llm: block (built-in judge) or a
routing.provider=external block (external routes:select service). Remove the env
gate in cli.py and refresh the stale references in app.py, advise_models.py, and
web capabilities.ts. Server smart_routing_enabled already keyed on the resolved
client, so the /v1/info signal is unchanged.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-24 09:24:52 +00:00
Pat Sukprasert c326937443 docs(harness): break Phases 1 & 2 into a PR-by-PR breakdown (#3217)
The modular-registry proposal described the phases as thin numbered lists.
Turn them into a concrete, verified implementation plan reviewers can cost:

- Add a "Current state (verified 2026-07-24)" subsection grounding the plan
  in the tree at main (59e6b70e): data model ready but no native_providers
  field; run_<x>_native already near-uniform (only claude/codex/antigravity/
  opencode carry extra kwargs); coverage uneven across hubs (resume 10,
  chat-redirect 6, interrupt 9, stop 7); dead _HARNESS_MODULES literal still
  present; harness_catalog() emits no native-agent rows.
- Phase 1 (core-only seam): 8 PRs (1.1–1.8) in a table with scope, key files,
  dependencies, risk, and estimates. 1.1 provider model + resolver is the
  additive foundation; 1.5 runner launch/terminal-route is the risk center.
- Phase 2 (community + web): 4 PRs (2.1–2.4).
- Add an effort summary: ~26–37 engineer-days across ~12 PRs, critical path
  1.1 → 1.2 → 1.5 → 2.2 → 2.3. Refresh the Bottom line to match.

Docs-only; no code paths affected.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-24 16:22:56 +07:00
dependabot[bot] 85fba59e72 chore(deps-dev): bump js-yaml from 4.2.0 to 4.3.0 in /editors/vscode (#2942)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 09:11:17 +00:00
Serena Ruan 8fdce5e6d9 fix(web): remove "Create new project" from the project picker menu (#3210)
* fix(web): remove "Create new project" from the project picker menu

Projects are created via the + icon next to the Projects header in the
sidebar, so the picker's own "Create new project" row was a redundant,
second entry point. Drop it (and the inline new-project input it toggled)
from ProjectPickerMenu, leaving search, the project list, and "Remove
from <project>".

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e_ui): file sessions via the + button after dropping picker create

The project picker no longer offers an inline "Create new project" row, so
the e2e helpers that drove that flow broke. Rewrite `_move_to_new_project`
to create the empty project from the Projects-header + button first, then
file the session via the kebab picker by name.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-24 17:01:01 +08:00
Serena Ruan 20ec819049 feat(sessions): persist pinned sessions server-side (per-user) (#3189)
* feat(sessions): persist pinned sessions server-side as a per-user label

Pins were client-only (localStorage), so they didn't follow a user across
devices. Move them to a server-side per-user session label so a pin persists
and stays per-user even on shared sessions.

- Store: `omnigent.pinned.<user_id>` label (value = epoch-ms pin time);
  `pinned_label_key()` hashes over-long user ids to fit the 128-char key
  column. `list_conversations(pinned=True, pinned_owner=…)` filters to the
  caller's own key.
- Route: `GET /v1/sessions?pinned=true` enumerates the caller's pins
  (independent of the loaded window); PATCH rewrites the client's canonical
  `omnigent.pinned` to the caller's per-user key, and `_labels_for_viewer`
  collapses it back on read so the per-user dimension never crosses the API
  and no viewer sees another user's pin key.
- Write-integrity: reject any client-supplied suffixed `omnigent.pinned.<user>`
  key so a caller can't pin/unpin for someone else.
- Forks drop per-user pin keys by prefix (a clone must not inherit pins).
- Web: server-authoritative `usePinnedConversations` + optimistic
  `useTogglePinnedConversation`; Pinned section ordered by pin timestamp;
  one-time localStorage->server migration that retains pins whose write failed.
- Guard `relativeTime`/`absoluteTime` against non-finite input (no more "NaNy").

Co-authored-by: Isaac

* test(e2e-ui): drive visual-snapshot pins via ?pinned=true, not localStorage

The populated-sidebar visual baseline seeded the pinned session in localStorage,
but pins are now server-authoritative (GET /v1/sessions?pinned=true). Under the
new model the localStorage seed is ignored and the bare-list stub answered the
pinned query too, so every row rendered as pinned → baseline mismatch (the
non-blocking UI Snapshot job).

- Split a `?pinned=true` route out from the bare-list regex (which now also
  excludes `pinned=`, mirroring the existing `project=` exclusion) and return
  just the pinned row, carrying the canonical `omnigent.pinned` label.
- Drop the `omnigent:pinned-conversation-ids` localStorage seed.
- Apply the same fix to the pinned-project flyout baseline (it passed only by
  luck — its bare-list stub happened to return exactly the one pinned row) and
  give its row the pin label so it's explicit, not incidental.

Co-authored-by: Isaac

* fix(sessions): let read-only collaborators pin a shared session

Pinning moved server-side (per-user `omnigent.pinned.<user>` label) but the
session PATCH gated all label writes at LEVEL_EDIT, so a read-only collaborator
on a shared session could no longer pin it — a regression from the localStorage
model, which had no permission check.

- Gate a pin-only PATCH (labels == {omnigent.pinned}, no other field) at
  LEVEL_READ: pinning is a personal per-viewer preference, not an edit to the
  session, so anyone who can SEE it may pin it. Any other field keeps the
  edit/owner requirement. Unpin ("" value) is still pin-only, so it downgrades
  too. The `?pinned=true` list is already scoped `accessible_by`, so a shared
  pin surfaces on "Shared with me".
- Tests: a LEVEL_READ grantee can pin AND unpin a shared session; the downgrade
  stays narrow (a non-pin label, or a pin bundled with one, still 403s).
- Rework the access-tier comment to match the if/elif/else (READ / OWNER / EDIT).

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: Isaac

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-24 17:00:18 +08:00
Pat Sukprasert 5a84c85a39 docs(harness): sync Phase 0 completion in modular-registry proposal (#3212)
PRs #3148 (extract native terminal orchestration) and #3149 (split the
native app-session test monolith) landed the two remaining Phase 0 file
splits. Update the proposal to reflect reality:

- §1 runner hub: app.py is now ~10.1k lines (was ~20.1k) plus the new
  omnigent/runner/native/orchestration.py (~6.5k); drop the stale absolute
  line-number anchors and clarify that the dispatch arms and interrupt/stop
  closures stayed in app.py while the builders/mirrors moved out.
- Phase 0: mark both runner/app.py and the test monolith Done, noting the
  single-orchestration.py outcome (vs the proposed three-way split) and the
  nine concern-scoped test modules + shared conftest.py.
- Risk section: re-anchor the forwarder registry to _AUTO_FORWARDER_TASKS in
  its new home and note the risk now shifts to Phase 1.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-24 15:36:53 +07:00
Tomu Hirata 59e6b70ea1 refactor(server): split sessions.py into 8 domain sub-modules (#3194)
* refactor(server): split sessions.py into domain sub-modules

sessions.py (7799 lines) is split into 8 focused route files under
_sessions/:

  routes_core.py       — CRUD, list, WS updates, fork, switch-agent
  routes_hooks.py      — /hooks/* and /policies/evaluate
  routes_items.py      — /items and /child_sessions
  routes_resources.py  — /resources/* (terminals, files, environments)
  routes_browser.py    — /browser/*
  routes_elicitations.py — /elicitations/*
  routes_events.py     — /events, /stream, DELETE /sessions/{id}
  routes_permissions.py — /permissions/*, /owner
  routes_agent.py      — /agent, /agent/contents, /mcp

Each file exports a register_X_routes(router, ...) function.
create_sessions_router() becomes a thin delegator (~533 lines).

helpers.py gains proxy wrappers for _same_provider_family,
_agent_is_native, _agent_carries_native_fork_history,
_presentation_labels_for_agent, and _reset_runner_resources_after_switch
so existing test monkeypatches on sessions.<name> continue to work.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* refactor(server): move sessions/ route sub-modules out of _sessions/

Convert sessions.py to a sessions/ package and move the 8 routes_*.py
files from _sessions/ into sessions/, so the public package layout is:

  routes/sessions/__init__.py  (facade, formerly sessions.py)
  routes/sessions/routes_core.py
  routes/sessions/routes_hooks.py
  routes/sessions/routes_items.py
  routes/sessions/routes_resources.py
  routes/sessions/routes_browser.py
  routes/sessions/routes_elicitations.py
  routes/sessions/routes_events.py
  routes/sessions/routes_permissions.py
  routes/sessions/routes_agent.py

_sessions/ retains only the private internals (common, helpers,
orchestration) that do not need public names.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(server): use facade indirection for session_stream and get_agent_cache consistently

routes_browser, routes_events, and routes_hooks were still calling
session_stream.publish() and get_agent_cache() via the direct module
binding. Apply the same facade-indirection pattern already used in
routes_core so all call sites are consistent and test monkeypatches on
sessions.session_stream / sessions.get_agent_cache are honored.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(server): fix lint, _to_agent_object closure, and pyproject.toml exemptions

- Move _policy_type, _policy_description, _to_agent_object from inside
  register_permissions_routes closure to module-level in routes_permissions.py
  so routes_agent.py can import them directly. Fixes NameError crash on
  GET /sessions/{id}/agent in server-approvals tests and E2E tests.

- Add missing 'return router' at end of register_permissions_routes (was
  missing after the closure reorganization).

- Import the three helpers explicitly in routes_agent.py.

- Update pyproject.toml per-file-ignores to cover sessions/*.py and
  sessions/__init__.py with the same exemptions the original sessions.py
  had (ARG001, ARG002, BLE001, E501, F401, F403, F405) so pre-commit
  ruff passes.

- Run ruff format on all sessions/ sub-modules.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(server): fix all proxy/monkeypatch misses and restore noqa directives

Route sub-modules were importing _X_impl directly instead of using the
facade proxy, causing monkeypatch(sessions, '_X', ...) to have no effect.
Fix by removing all '_X_impl as _X' imports from routes_*.py — the star-
imports from _sessions.helpers and _sessions.orchestration already bring
in the correct facade-delegating proxies.

Additional fixes:
- Access _SESSION_UPDATES_MAX_WATCHED, _SESSION_UPDATES_RESCAN_INTERVAL_S,
  _SESSION_UPDATES_HEARTBEAT_INTERVAL_S through the facade in routes_core.py
  so monkeypatch(sessions_routes, '_SESSION_UPDATES_*', N) works.
- Use _load_agent_spec_for_session proxy (not impl) in routes_resources.py.
- Access get_caps() through facade in routes_hooks.py evaluate_policy so
  monkeypatch('omnigent.server.routes.sessions.get_caps', ...) fires.
- Restore noqa: BLE001 and F401 directives in _sessions/helpers.py and
  _sessions/orchestration.py that were stripped by the RUF100 auto-fix.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(server): delete old sessions.py, fix remaining facade proxy misses

- Delete omnigent/server/routes/sessions.py (the file was rm'd in a prior
  commit but never staged; CI was still linting it and seeing F403/F405).

- Route _HOST_BOUND_RUNNER_CONNECT_GRACE_S through the facade in
  routes_events.py (3 call sites) so monkeypatch(sessions_module,
  '_HOST_BOUND_RUNNER_CONNECT_GRACE_S', ...) is honored.

- Route _recover_subagent_status_forward_via_parent through facade
  in routes_events.py.

- Route _registered_runner_id through facade in routes_core.py.

- Route _BROWSER_ACTION_AWAIT_S through facade in routes_browser.py.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(server): route patchable names in routes_hooks.py through facade

All five hook handlers and evaluate_policy use module-level timeout
constants (_CLAUDE_NATIVE_PERMISSION_HOOK_TIMEOUT_S etc.) and auth
helpers (_get_user_id, get_caps, get_agent_cache) that tests monkeypatch
on the sessions facade module. Access them through _sf (the facade) at
call time so monkeypatch(sessions_route, '_CLAUDE_NATIVE_PERMISSION_HOOK_TIMEOUT_S', 0.1)
and monkeypatch('omnigent.server.routes.sessions.get_caps', ...) are honored.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-24 08:24:25 +00:00
Kunyu Chen d8da36d081 Simplify env variables for Slack integration on Databricks apps (#3206)
Simplify env variables for Slack integration on Databricks apps
2026-07-24 00:22:39 -07:00
Rahul Ravindranathan 5972254fda feat(scheduled tasks): edit flow + text time inputs (#3186)
CI / gate (push) Failing after 2s
CI / Coverage report (push) Has been cancelled
Doc sync / Classify and draft docs (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
* feat(scheduled tasks): edit tasks

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* feat(scheduled tasks): use text time input

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* feat(scheduled tasks): add compact time picker

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* feat(scheduled tasks): refine task dialog layout

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): time picker wheel-scroll + column widths

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): make host field full width

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): forward Input ref so time field stops reformatting while typing

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): show all minutes and normalize field text

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): prevent edit-modal footer buttons from being clipped

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): hourly minute field placeholder 0, digits-only, clamp 59

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* test(scheduled tasks): e2e_ui coverage for create/edit modal + time picker

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* fix(scheduled tasks): make nextRunAtMs O(1) so the Tasks page loads instantly

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-23 23:46:16 -07:00
dependabot[bot] 32dbb3159d build(deps-dev): bump esbuild from 0.21.5 to 0.28.1 in /editors/vscode (#3190)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.21.5 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 06:36:53 +00:00
Pat Sukprasert 513711cec0 feat(ci): add /rerun comment command to re-run failed CI without a push (#3195)
Re-running CI today means pushing an empty commit or rebasing, which fires a
push event and dismisses existing approvals (branch protection keeps
dismiss-stale-reviews on to block approve-then-swap). A `/rerun` comment
re-runs failed jobs on the existing head SHA instead -- no new commit, so
approvals survive.

Authorized to the PR author or a write-access commenter. Only re-runs the
mock-LLM `pull_request` test suites; the merge gates and Polly AI Review are
left alone. Single file (no privileged relay) because issue_comment gets a
writable base-repo token even for fork PRs.

Co-authored-by: Isaac

Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-24 13:36:39 +07:00
dependabot[bot] a28643e6d8 chore(deps): bump mcp from 1.27.2 to 1.28.1 (#2731)
Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.27.2 to 1.28.1.
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.27.2...v1.28.1)

---
updated-dependencies:
- dependency-name: mcp
  dependency-version: 1.28.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 05:53:46 +00:00
Tomu Hirata c847f5aefb fix(benchmark-pr): use marker-based comment upsert instead of --edit-last (#3197)
--edit-last edits the most recent PR comment regardless of author or
content, so it was overwriting the UI preview comment when both workflows
ran on the same PR. Switch to the same find-by-marker + PATCH approach
used by ui-preview.yml so each workflow manages its own comment.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-24 05:49:48 +00:00
dependabot[bot] 96b467d149 chore(deps): bump js-yaml (#2943)
Bumps the electron-security group with 1 update in the /web/electron directory: [js-yaml](https://github.com/nodeca/js-yaml).


Updates `js-yaml` from 4.2.0 to 4.3.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: direct:production
  dependency-group: electron-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 12:38:48 +07:00
dependabot[bot] 0ceee06155 chore(deps): bump pillow from 12.2.0 to 12.3.0 (#2940)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.2.0 to 12.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.2.0...12.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 05:31:36 +00:00
dependabot[bot] a3a6c1e3c7 chore(deps): bump pyasn1 from 0.6.3 to 0.6.4 (#3036)
Bumps [pyasn1](https://github.com/pyasn1/pyasn1) from 0.6.3 to 0.6.4.
- [Release notes](https://github.com/pyasn1/pyasn1/releases)
- [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst)
- [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.3...v0.6.4)

---
updated-dependencies:
- dependency-name: pyasn1
  dependency-version: 0.6.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-24 05:09:11 +00:00
Jackson Zheng 5f98a88b57 Enable background session titles by default (#3191)
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
2026-07-23 21:53:40 -07:00
Pat Sukprasert 3df3843e18 ci: add waiting-on-author PR hygiene (#3183)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-24 04:21:34 +00:00
dependabot[bot] 12adae2846 build(deps-dev): bump brace-expansion in /editors/vscode (#3174)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.6 to 5.0.8.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.8)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 04:15:46 +00:00
dependabot[bot] 4214d4b5fc build(deps-dev): bump vitest from 1.6.1 to 3.2.6 in /editors/vscode (#3176)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.6.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 3.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 04:13:15 +00:00
Sabhya Chhabria f3bf3d8a51 [polly] Add Codex goal mode (#3181)
*  feat(polly): Add Codex goal mode

Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>

* 🐛 fix(codex): Preserve history for fresh goals

Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>

---------

Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
2026-07-23 21:12:15 -07:00
Jackson Zheng 829c17942c Polish workspace pane layout (#3122)
* Polish workspace pane layout

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* test(ui-snapshot): update chat baseline

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* feat(web): add workspace tab tooltips

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* test(e2e): cover workspace tab tooltips

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* test(ui): update merged chat snapshot

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* test(ui): refresh merged chat snapshot

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* fix(web): stabilize right-pane e2e coverage

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

* fix(web): stabilize remaining e2e flows

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>

---------

Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
2026-07-23 20:11:08 -07:00
Serena Ruan 9151aa9b99 fix(web): make session rename optimistic so the new name shows instantly (#3185)
* fix(web): make session rename optimistic so the new name shows instantly

Renaming a session left the stale name in the sidebar for the duration
of the PATCH round-trip: all cache patching happened in the mutation's
onSuccess, so the row only repainted once the server responded.

Move the cache overlay into onMutate so the new title paints on the next
frame, snapshot the old title for rollback, and restore it in onError.
onSuccess still reconciles with the server-confirmed title + updated_at
and keeps the deliberate no-refetch behavior (an immediate GET races the
search-index reindex). Also patch the ["project-sessions", name] caches
that project folders render from — the flat ["conversations"] overlay
never touched them, so a filed session's row stayed stale until the WS
reconcile.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): cancel in-flight list queries before optimistic rename overlay

Close the in-flight-reconcile clobber race flagged in review: an
already-running GET /v1/sessions reconcile poll (or a WS-triggered
fetch) could resolve after onMutate and overwrite the optimistic title
with the stale search-indexed name. Cancel the ["conversations"] and
["project-sessions"] queries in onMutate before overlaying so no
in-flight fetch can win.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-24 11:09:00 +08:00
Tomu Hirata 3ba4318f17 feat(telemetry): log agent_name for polly and debby in SessionCreatedEvent (#3152)
Add an opt-in agent_name field to SessionCreatedEvent. Only polly and
debby are populated — all other agent names are withheld to avoid leaking
user-defined agent names in telemetry.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-24 11:42:45 +09:00
Yuan Tang dbcd72831f feat(sandbox): support OMNIGENT_CONTAINER_RUNTIME env var for container runtime selection (#2949)
* feat(sandbox): support OMNIGENT_CONTAINER_RUNTIME env var for container runtime selection

Allow switching the container runtime (e.g. from docker to podman) via the
OMNIGENT_CONTAINER_RUNTIME environment variable instead of requiring per-agent
YAML configuration. The per-agent container_runtime key still takes precedence
over the env var.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(test): add missing top-level `Any` import in test_local.py

Ruff flagged F821 (undefined name) because `Any` was used in a
runtime dict annotation but only imported inside a nested function.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(test): read version dynamically in crash handler test

The test hardcoded "0.6.0.dev0" which breaks when the installed
version diverges from the source (e.g. after a version bump).
Read omnigent.version.VERSION at runtime instead.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* Revert "fix(test): read version dynamically in crash handler test"

This reverts commit 53855f5c10e3573e9d1ddbfd2afb0bd76abbc91e.

* fix: address review comments on container runtime PR

- Make container_runtime field explicitly Optional to avoid misleading
  type annotation and unnecessary type-ignore
- Update parser docstring to mention OMNIGENT_CONTAINER_RUNTIME as an
  additional default source
- Update shell script header comment to say "container runtime" instead
  of "Docker"

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(test): add autouse fixture to clear OMNIGENT_CONTAINER_RUNTIME

Prevents the host environment from leaking into tests that assume
the default runtime is "docker".

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* style: add missing blank line before autouse fixture

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix: address additional review comments on container runtime PR

- Rename _ALLOWED_RUNTIMES to ALLOWED_RUNTIMES (public API used
  cross-module by the parser)
- Reject container_runtime: null in YAML instead of silently falling
  back to the env var default
- Add test for container_runtime: null rejection

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-23 22:22:27 -04:00
Yuan Tang 8344c18420 fix(runner): reconnect dead-but-registered native terminals before turn (#2951)
* fix(runner): reconnect dead-but-registered native terminals before turn

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
2026-07-24 02:00:36 +00:00
Cathy Yin 1241a38e40 feat(onboarding): report the installed-but-unconfigured harness state (M2 readiness parity) (#3072)
* feat(onboarding): report the installed-but-unconfigured harness state

Add the credential axis to the picker-facing readiness map so Claude, Codex,
and Pi report the yellow "installed but no credential" state — the signal the
web setup dialog needs to know when to offer an "Add credential" action
(the M2 keystone of Setup-From-the-UI). Purely additive: the values
("needs-auth" / "binary-missing") already exist in HarnessAvailability, and
the launch gate (harness_is_configured) stays binary-only, so a not-yet-authed
harness is never blocked from launching.

- New _family_provider_configured(): whether an omnigent-managed provider
  (API key / gateway) serves the harness's family, reading the same config
  omni setup's overview does. Subscription-kind is excluded (that lives in the
  CLI's own login, judged by harness_cli_logged_in). Local, side-effect free,
  never raises (fails to "no credential").
- Claude: ready when a provider is configured OR the CLI subscription login is
  present (was CLI-login only — an API-key-only user wrongly showed yellow).
  Checks config first to avoid the status subprocess on the common path.
- Pi: gains the axis it lacked entirely (was binary-only → always green once
  installed). No CLI login, so binary + provider: installed-but-no-provider is
  now "needs-auth".

Codex already had this (unchanged, the template). Qwen/OpenCode env-auth
unchanged.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* docs(onboarding): clarify _family_provider_configured checks entry presence

Polly review nit: the helper returns True when a non-subscription default
provider *entry* exists, not when its secret actually resolves — an entry
pointing at an unset env:/keychain ref still reads configured (matching the
secret-blind omnigent setup overview). Reword the docstring from "usable
credential" to "a default provider entry is present" and note the
secret-blind behavior + why it's safe (launch gate is binary-only; signal
only moves toward green). No behavior change.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

* refactor(onboarding): address review nits on readiness detection

- Hoist the provider_config import in `_family_provider_configured` to the
  module top (no circular import); update the test monkeypatch targets to the
  now-module-bound name.
- Drop the internal milestone label from a test docstring.

Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>

---------

Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
2026-07-24 08:03:49 +07:00
Bryan Li 4bc38b96d4 feat(sandbox): operator-configured PVC mounts for Kubernetes runners (+ fix global YAML bool-resolver leak) (#2435)
* feat(sandbox): parse and validate sandbox.kubernetes.pvc_mounts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* feat(sandbox): add pvc_mounts volumes to the runner Pod manifest

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* feat(sandbox): thread pvc_mounts through the kubernetes launcher

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* docs(deploy): document sandbox.kubernetes.pvc_mounts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* feat(sandbox): fail loud on unknown sandbox.kubernetes keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* test(sandbox): lock in pvc_mounts collision-order, null read_only, and claim-reuse semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* test(sandbox): pin the reserved-mount HOME prefix to the launcher's _HOME_DIR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* refactor(sandbox): reuse shared validators in the pvc_mounts parser

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(sandbox): close pvc_mounts reserved-path gaps from review

Reject mount_paths with exactly two leading slashes — POSIX normpath
preserves them so '//home/omnigent' passed both validation gates while
the kernel collapses '//' to '/' at mount time, shadowing HOME. Add
/opt to the reserved prefixes: the host image's omnigent venv lives at
/opt/venv and was shadowable. Both cases now covered in the fail-loud
parametrization.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>

* fix(sandbox): reject pvc_mounts paths that mount over reserved prefixes

The reserved-path check only caught mount_paths at or under a reserved
prefix, so an ancestor like /home or /var passed validation while
mounting over the HOME emptyDir mountpoint or the Secret projections.
Reject ancestors too, and reserve /run, /var/run, and /var/lock in full
so the Debian image's /var/run -> /run and /var/lock -> /run/lock
symlinks can't alias around the lexical check.

Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>

---------

Signed-off-by: Bryan Li <bryan.li@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 07:47:01 +07:00
Zeyi (Rice) Fan 7b835ed767 Add kunyuchen to maintainer list (#3172)
Adds kunyuchen to the canonical maintainer list in .github/MAINTAINER so they can approve PRs and participate in maintainer-gated workflows.
2026-07-23 17:31:21 -07:00
Zeyi (Rice) Fan d2fdafce1b fix(ios): block smuggled query/fragment separators in omnigent:// deep links (#3179)
## Related issue

Closes F-CR-6

## Summary

- `DeepLink.parse` validated the `/c/<id>` segment with only `!contains("/")`, but Foundation's `URL.path` is percent-DECODED — so `omnigent://host/c/id%3Fview=terminal` exposes `?` as a literal in the path and smuggles a query (and `%23` a fragment, `%2e%2e` a `..`, `%00` a control char) past the intended "/c/<id> only" shape. Added a denylist that rejects `?`, `#`, `/`, `.`, `%`, and control chars in the decoded id, so an encoded separator that `URL.path` decoded into one of those is dropped.
- The denylist deliberately does NOT assume the id's exact format (the server emits 32-hex uuids today, but the SPA's `/c/:id` route accepts any non-slash segment); the SPA stays the authority on id validity, and a future id scheme (ULID, nanoid, base64) won't be silently rejected. Benign non-canonical ids like `conv_abc` are accepted; only structure-smuggling is blocked.
- Documented the custom-scheme hijack risk in `DeepLink.swift`: iOS doesn't verify single ownership of `omnigent://`, so a co-installed app can read the link's host + id (metadata disclosure). For managed Databricks domains that can serve an `apple-app-site-association`, prefer verified Universal Links; the custom scheme is retained for BYO/OSS servers that can't host AASA, with the interception risk documented.

## Test Plan

- Unit tests (`OmnigentTests/DeepLinkTests`): 19 cases, all pass — including `testRejectsSmuggledQueryViaEncodedQuestionMark` (`%3F`→`?`), `testRejectsSmuggledFragmentViaEncodedHash` (`%23`→`#`), `testRejectsEncodedDotAndDotDot` (`%2e%2e`), `testRejectsControlCharacters` (`%00`/`%0A`/`%7F`), `testRejectsMalformedPercentEscape` (`%zz`), and `testAcceptsBenignNonCanonicalIds` (`conv_abc`/`x`/`not-a-uuid` are accepted — no smuggled structure).
- UI tests (`OmnigentUITests`): 6 cases via a DEBUG-only `--omnigent-open-url` launch-argument seam that routes the link through the real `handleDeepLink`/`DeepLink.parse` (XCUITest can't reliably deliver custom-scheme URLs on this toolchain). `testValidDeepLinkShowsConsent` (valid link → consent alert), `testBenignNonCanonicalIdIsAccepted` (`conv_abc` → consent alert), and rejection tests for smuggled `?`/`#`/`..`/control-char (no alert). A `--omnigent-reset-state` flag wipes persisted server state so each case starts with no known server. All pass on the iOS simulator.
- Manual simulator verification: drove `xcrun simctl openurl` against the running app with `OMNIGENT_DEEPLINK_TRACE` set; NSLog trace confirmed `ACCEPTED` for the valid link and `REJECTED` for all 5 smuggling/malformed links (smuggled `?`/`#`, `..`, control char, non-id).

## Demo

N/A

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [x] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [x] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Manually verified end-to-end on the iOS simulator: launched the app with `OMNIGENT_DEEPLINK_TRACE=1` and sent six real `omnigent://` links via `xcrun simctl openurl`. The NSLog trace showed `ACCEPTED` for the valid link and `REJECTED` for all smuggling/malformed links, proving the fix through the real `DeepLink.parse` → `handleDeepLink` path. The DEBUG-only `--omnigent-open-url` / `--omnigent-reset-state` launch-argument seam and `OMNIGENT_DEEPLINK_TRACE` NSLog logging are compiled out of Release builds (gated by `#if DEBUG`), so there is no production behavior change from the test infrastructure.
2026-07-23 17:30:33 -07:00
Enes Yilmaz 66d253eacc fix(cli): normalize Azure Databricks custom-URL workspaces to their canonical host (#2870)
* fix(cli): normalize Azure Databricks custom-URL workspaces to their canonical host

omni login (and omni host) failed for Azure Databricks workspaces with a custom
(vanity) URL like https://mydomain.azuredatabricks.net/?o=<workspace_id>: the
vanity edge 303-redirects the unauthenticated probe to /login instead of
answering, so _databricks_workspace_login_target does not recognize the
Databricks posture and login fails. The canonical host
adb-{workspace_id}.{workspace_id % 20}.azuredatabricks.net does answer, and the
?o=<workspace_id> selector already carries the id.

Rewrite the custom host to the canonical adb- form in _resolve_server_url (the
shared normalization every --server entry point uses, so omni host is covered
too). Only *.azuredatabricks.net hosts that are not already the adb- form and
carry a numeric ?o= are touched; AWS/GCP hosts, canonical URLs, and URLs without
a selector are left unchanged.

Closes #2781

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>

* fix(cli): probe before adopting the canonical Azure Databricks host

The custom-URL fix landed the canonical adb- host rewrite unconditionally in
_resolve_server_url, so a wrong synthesis could strand the user on a host they
never typed, and the unit tests only re-asserted the implementation's own
arithmetic (123 % 20 == 3), which would pass under any modulus.

Try the URL as the user gave it first. Only when that fails to resolve, and only
for an Azure vanity workspace URL carrying a numeric ?o=, synthesize the
canonical host, probe it, and adopt it if it answers. A dead synthesis now falls
back to the user's URL instead of replacing it.

The shard rule remains an observed regularity rather than a documented contract
(Microsoft calls the segment a random number and treats properties.workspaceUrl
from the ARM API as authoritative), so the probe keeps it off the load-bearing
path. Docstrings say so plainly.

Also:
- _canonicalize_azure_databricks_url is now _canonical_azure_databricks_url and
  returns None to decline, so a caller can tell "not applicable" from "no change".
- Guard the selector with isascii() as well as isdecimal(): str.isdecimal()
  accepts non-ASCII digits that int() also parses, which synthesized a
  nonsensical host.
- _probe_root reduces a URL the way _workspace_api_server_url does before
  probing. Without it the comparison against the expansion's result never
  matched (it drops the ?o= selector first, and that selector is what makes a
  URL a candidate), and the new probe requested /?o=123/v1/me.
- Replace the tautological shard assertions with five real observed
  workspace/host pairs, and drive the resolver tests through the real expansion
  with only httpx scripted, since a stubbed expander cannot catch the above.

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>

* docs(cli): drop issue-number refs from Azure canonical-host comments

The repo's comment convention says code comments should describe the
scenario, not reference issue/PR numbers. Remove the (#2781) tags from
the _canonical_azure_databricks_url / _resolve_server_url docstrings and
the vanity-URL fallback test; the surrounding prose already explains the
Azure vanity-host case without needing the external link.

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>

---------

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
2026-07-24 00:30:22 +00:00
Sunny Yang 78b37f20de fix(runner): resolve and re-materialize file attachments on remote-runner history reload (#2085)
* fix(runner): resolve and re-materialize file attachments on remote-runner history reload

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>

* fix(runner): seed the native-session compaction anchor; tolerate malformed file metadata

Native-harness sessions skip the history reload entirely, which also
skipped seeding the last server item ID that harness compaction
persistence anchors on — compactions then silently stopped persisting.
Session create now fetches just the newest item ID (newest-first, single
item, no attachment downloads) for native harnesses.

A 200 metadata response with an unparseable body no longer aborts
attachment resolution: both resolvers (the runner's message-content
resolver and the claude-native transcript rebuild) fall back to the
content response's Content-Type for the media-type hint.

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>

* refactor(attachments): centralize file_id resolution and reference-line emission in native_attachments

The transcript rebuild and the runner each carried a full copy of the
file_id fetch-and-inline pipeline, and nine native executors repeated
the same materialize-or-marker block. Both now live in
native_attachments: resolve_file_id_block() serves the runner and the
transcript rebuild, attachment_reference_line() serves the executors,
and ATTACHMENT_MARKER_STRIP_PATTERN replaces four hand-copied forwarder
regexes. Materialized filenames are sanitized the same way as marker
names so a bracketed filename cannot break the marker consumers, and
the resume dedupe short-circuits on file size before reading bytes.

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>

* fix(attachments): replay resolved history attachments as structured content

Cold-started claude-sdk sessions flattened prior turns into a text
prefix, so a resolved historical image reached the model as the marker
[image: name, media_type, N base64 chars]. The bytes never arrived, which
leaves the #882 symptom in place for that harness: the model describes an
attachment it cannot see.

Prior-turn attachments now replay as real Anthropic image/document blocks
via the existing converter, interleaved in transcript order. Text-only
history still takes the plain-string path and renders byte-identically,
unresolved attachments keep their existing marker, and base64 still never
enters prompt text.

Materialization also derives its collision suffix from a content hash
rather than a random one, so a history carrying two distinct uploads of
the same filename keeps one file per payload instead of gaining a copy on
every transcript rebuild.

The two tests that asserted the compact-placeholder shape are replaced by
cold-reload tests: that shape is the behavior being corrected, but the
invariant those tests protected (no base64 in prompt text) is asserted
against the prompt's text blocks.

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>

* refactor(attachments): collapse duplicated prompt-shape branches

The structured and plain-text arms of _build_prompt returned the same
value whenever the latest message was multimodal, and re-scanned the
block list to decide which arm to take. Coalescing already leaves an
all-text history as one block, so the block count answers that.

Materialization's second identity check was a no-op guarding a write
that produces the same bytes, so the collision path flattens to one
branch.

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>

* refactor(tests): keep runner conftest identical to upstream

Move the file-server fake's items/failure/malformed-meta behaviors out of
the shared _FakeFileServerClient into local subclasses in the one file
that uses them, so conftest.py stays in sync with upstream and per-test
modes stay next to their tests.

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>

---------

Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-24 07:23:56 +07:00
Zeyi (Rice) Fan 7738df6fb3 fix(electron): guarantee the desktop quits after before-quit cleanup (#2972)
CI / gate (push) Failing after 1s
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
Doc sync / Classify and draft docs (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
The before-quit handler defers the quit until serverManager.shutdown()
finishes, then re-issues app.quit() as the *only* way the quit ever
proceeds. Re-issuing app.quit() after before-quit's preventDefault() is a
known intermittently-unreliable Electron behavior (electron/electron#4994,
#33643, #39094); when it no-ops, or shutdown hangs (a stuck
'omnigent server stop'), the app stays up with its window still open —
matching 'sometimes the app is still running and refuses to quit'.

- Hard safety cap: app.exit(0) after quitCleanupTimeoutMs (unref'd) if
  graceful cleanup + the re-issued quit haven't terminated. Normal cleanup
  (<6s) completes well under the 10s cap; it only trips when stuck.
- Evaluate resolvedCliPath() inside an async IIFE so a future throw becomes
  a rejection caught by .catch, never stranding the quit.
- Install fallback: when quitAndInstallIfPending() returns true but
  quitAndInstall() doesn't actually quit (staged update gone), a short
  app.exit(0) fallback still quits.
- unref() the periodic update-check setInterval so it can't keep the event
  loop alive at quit.

Adds two regression tests (install-fallback and cleanup-cap) via an
injectable setQuitTimeouts; harness exposes setTimeout/clearTimeout/app.exit.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-23 16:47:59 -07:00
Rahul Ravindranathan cc94a9c5e6 feat(scheduled tasks): list page + sidebar nav (#3112)
* Add scheduled tasks page

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Remove scheduled page phase labels from comments

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Remove scheduled Omnigent stub wiring

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Shorten scheduled nav comment

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Remove scheduled tab styling comment

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Clean up scheduled task suggestions

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* test(e2e-ui): update visual baselines

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-23 16:35:37 -07:00
Dhruv Gupta 131db6276b fix(codex-native): launch on the spec's declared model, not the provider default (#3175)
The codex-native launch read the spec model only from
executor.config["model"], a key the single-file agent loader never
populates, so a custom agent's declared model: was silently replaced by
the provider default. Read the canonical executor.model first — the same
field the in-process harness and the claude/cursor native launches
consume — and keep config["model"] as a fallback for bundle specs that
pin the model inside the harness config block.

Co-authored-by: Isaac
2026-07-23 23:16:50 +00:00
Dhruv Gupta af3d18ba16 fix(loader): reject the bundle type:/config: nesting in single-file executor blocks (#3178)
* fix(loader): reject the bundle type:/config: nesting in single-file executor blocks

A single-file agent YAML written with the bundle config.yaml shape
(executor: {type: omnigent, config: {harness: ...}}) loaded without
complaint: the unknown keys were silently dropped, the declared harness
with them, and a different harness was inferred from the model prefix —
databricks-gpt-* landing on openai-agents instead of the declared
codex-native, with no diagnostics. Reject exactly type:/config: with an
error that shows the flat spelling. Other extra executor keys
(use_responses, extra, ...) keep loading — the compat loader reads them
from the raw YAML.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

* test: spell e2e fixture executors flat instead of the bundle config: nesting

Six runtime-generated single-file agent YAMLs in the e2e/e2e_ui/server
fixtures nested the harness under executor.config — the exact trap the
loader now rejects. They only worked because the dropped harness was
re-inferred from the gpt-* model prefix as the same openai-agents value.
Spell them flat so the declared harness actually flows. The two
spec_version bundle specs (approval agent, elicitation supervisor) keep
the nesting — config.harness is the correct spelling on the strict
parser path.

Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>

---------

Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
2026-07-23 16:14:11 -07:00
Sabhya Chhabria 53c0125e84 [polly] Add Claude SDK goal mode (#3084)
*  feat(polly): Add Claude SDK goal mode

- Reuse the composer Goal control for top-level Polly sessions on claude-sdk
- Send the completion condition as a native /goal command without server APIs
- Cover command dispatch, validation, read-only state, and harness gating

Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>

*  test(e2e-ui): Cover Polly Claude goal flow

Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>

---------

Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
2026-07-23 16:01:43 -07:00
Kunyu Chen c8828ed62d Enhance device auth to require user login (#3156)
* Update device auth scheme to require a recent login to reduce phishing attack risks
* Device grant ui tests
2026-07-23 14:43:43 -07:00
Rahul Ravindranathan 0085334e94 feat(scheduled tasks): create-task dialog (#3123)
* feat(scheduled tasks): manual create dialog (2/3)

Stack 2 of 3 for the Scheduled Tasks page (UI-1). Builds on the data
layer (1/3). The dialog isn't mounted anywhere yet, so it type-checks
standalone.

- CreateScheduledTaskDialog.tsx: manual create form wired to POST
  /v1/scheduled-tasks. Reuses the shared AgentHarnessPicker (exported from
  NewChatDialog) with "needs setup" badges via a fallback online host;
  seed-on-open prefill (cleared on close, no stale leak); backdrop-click
  dismiss with the guard scoped to the nested-Select case only.
- ScheduleFields.tsx: frequency/time/weekday schedule builder.
- Label.tsx: small shared form label.
- CreateWithOmnigentDialog.tsx: TODO(UI-2) stub.
- NewChatDialog.tsx: export AgentHarnessPicker + add optional
  onOpenChange / content+trigger class / contentAlign props (backward
  compatible for the interactive composer).

Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Fix scheduled task dialog defaults and picker

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Shorten scheduled task hourly comment

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Remove scheduled task phase labels from comments

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Remove scheduled dialog follow-up label comment

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

* Remove deferred scheduled Omnigent stub

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>

---------

Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
2026-07-23 14:38:40 -07:00
Pranav Setlur 34656aa806 fix(host): forward global config to the background local server (#2935)
The background server spawned by bare `omni` (`_spawn_local_server`)
launched `omnigent.cli server` without `--config`, so the server's
loader returned an empty config and never read `~/.omnigent/config.yaml`.
Its `llm:` (and `policies:`) block was invisible to the detached server,
so self-hosted smart routing silently stayed off (`sys_advise_models` ->
`router_on: false`; `/v1/info` -> `smart_routing_enabled: false`).

Forward `--config <global_config_path()>` when the file exists. Same bug
class as #2386/#2763 (Docker entrypoint dropped `policies:`); this is the
local-spawn instance.

Co-authored-by: Isaac

Signed-off-by: Pranav Setlur <psetlur@gmail.com>
2026-07-23 14:07:44 -07:00
Kunyu Chen 8285b58940 refactor(cli): replace omni integration slack start with omni integration slack --background (#3153) 2026-07-23 20:14:25 +00:00
Zeyi (Rice) Fan db11081516 fix(runner): patch heartbeat cadence on the app module (#3163)
PR #3148 extracted _session_labels_for_runner_spawn into
omnigent.runner.native.orchestration, but _SESSION_STREAM_HEARTBEAT_S
and the stream loop that reads it remained in omnigent.runner.app.

test_session_stream_emits_heartbeat_on_idle located the module to patch
via _session_labels_for_runner_spawn.__module__, which now resolves to
omnigent.runner.native.orchestration — a module that has no
_SESSION_STREAM_HEARTBEAT_S attribute — so the test raised
AttributeError and failed CI on main.

Patch omnigent.runner.app directly, which is where the heartbeat cadence
constant and its consumer actually live.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-07-23 13:02:46 -07:00
Harry Yao d18f7b95f5 claude-native: respect CLAUDE_CODE_USE_GATEWAY=1 for tool search (#3161)
When the launching process sets CLAUDE_CODE_USE_GATEWAY=1, that
gateway-aware mode keeps tool search enabled so MCP schemas load on
demand. Setting CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS alongside it
would override that mode, disabling all betas and inflating startup
token usage.

Only set CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS when gateway-aware
mode was NOT selected.

Ported from databricks-eng/universe#2298829.

Co-authored-by: harry-yao_data <harry.yao@databricks.com>
2026-07-23 12:36:02 -07:00
Aravind Segu 56d1db68af Add overridable item-data serialization seams to SqlAlchemyConversationStore (#3126)
Introduce no-op extension points on the conversation store so a subclass
can transform conversation_items.data and control search_text, without
changing OSS behavior:

- _encode_item_data(data_json): identity by default; append's data write is
  routed through it so a subclass may compress or encrypt the payload.
- _decode_item_data_batch(stored_list): identity by default; the read paths
  (list_items, list_latest_message_items_for_conversations, the FTS-ranked
  read) decode a whole page of rows through it before building entities, and
  _to_item now takes the already-decoded data. Making the read seam a batch
  (not a per-row hook) lets a subclass decode a page in one pass — e.g. a
  single bulk decrypt — instead of once per row.
- _item_search_text(item): extracts the search text as before by default;
  may return None to skip persisting search_text (and its FTS row) on a
  schema that omits the column.

Every default preserves current behavior exactly: the column stays plaintext
Text, and search/FTS are unchanged. This lets a downstream store (Databricks'
MySQL-homed conversation store) envelope-encrypt item payloads at the column
boundary while reusing append/list_items unchanged.

Co-authored-by: Isaac

Signed-off-by: aravind-segu <aravind.segu@databricks.com>
2026-07-23 10:00:25 -07:00
Pat Sukprasert afe6b3ba11 [tests] Split native app session tests by concern (#3149)
* test: split native app session tests

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: address native session split review feedback

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: address follow-up lint feedback

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: clarify native session test scopes

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* test: update native session helper imports

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-23 16:51:11 +00:00
Pat Sukprasert a979ec97d7 [runner] Extract native terminal orchestration (#3148)
* refactor(runner): extract native terminal orchestration

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(runner): limit native compatibility syncing

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-23 16:19:00 +00:00
Sai Asish Y 750c395a50 docs(deploy): correct docker admin bootstrap flow (no auto-generated password) (#2840)
* docs(deploy): correct docker admin bootstrap flow (no auto-generated password)

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>

* docs(deploy): correct remaining generated-password and /data-persistence claims

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>

* docs(deploy): scrub generated-password flow from remaining platform guides

The Docker docs were corrected earlier, but fly / railway / render / modal /
hf-spaces still told operators to read a generated admin password out of the
logs / /data/admin-credentials — a flow that no longer exists (bootstrap never
auto-generates a password; the first admin is claimed via the web Create-admin
form or a pre-seeded OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD).

- Rewrite the first-admin step in each guide to the real flow, and drop the
  fake "Created initial admin ... password: <generated>" log block.
- Add a first-visitor security note (unauthenticated /auth/setup while no
  password-bearing account exists) to every public-facing guide; fold it into
  hf-spaces' "make the Space Public" step where the exposure is most direct.
- render: correct the disk bullet (hashes live in Postgres, not on /data) and
  the render.yaml comment that called the anchor path a password file.

Co-authored-by: Isaac <isaac@example.com>
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Isaac <isaac@example.com>
2026-07-23 16:11:36 +00:00
Tomu Hirata 538494ff73 feat(cli): add omnigent session import (inverse of session export) (#3141)
CI / gate (push) Failing after 2s
Lint / gate (push) Failing after 1s
Lint / Pre-commit checks (push) Has been cancelled
Lint / Version lockstep check (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
Doc sync / Classify and draft docs (push) Has been cancelled
OSS Scorecard / Scorecard analysis (push) Has been cancelled
Windows (native) / Windows smoke + unit (push) Has been cancelled
* feat(cli): add `omnigent session import` (inverse of session export)

`session export` writes a portable JSONL but there was no way to load it
back — inspecting a shared/exported session meant hand-writing items into
the store. Add `session import` to close the round-trip: it reads the
session_meta + item lines and recreates the conversation on the target
server as a new session (fresh id each time) via POST /v1/sessions with
the history passed as initial_items.

Details:
- De-aliases the `model` serialization alias back to `agent` per item and
  validates each with parse_item_data() client-side before the request.
- Agent binding: reuse the exported agent_id when it exists on the target
  server; else fall back to the built-in native agent for the export's
  harness (mirrors /v1/imports); else fail with a clear message.
- Creates history-only (host_type=external, no host_id) so no runner
  launches. Carries over title/workspace/harness/model/effort overrides.

Known limitation (documented in --help): the server seeds initial_items
under a single synthetic response_id, so exact per-turn grouping is not
preserved. Fine for viewing/debugging; a follow-up server route could
preserve it if needed.

Verified end-to-end: imported the real 260-item export, re-exported, and
diffed — identical item counts and types, agent bound, model<->agent
alias round-trips.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(cli): scope agent→model de-alias to alias-bearing item types

Polly review caught that the import de-alias applied `model`→`agent` to
every item type, corrupting the two types where `model` is a genuine
field: `compaction.model` (silently dropped) and `routing_decision.model`
(required + collides with its own `agent` field → hard import failure for
any smart-routed session).

Derive the alias-bearing types from the data-model field definitions
(serialization_alias == "model") so the reverse map only fires for
message/function_call/reasoning/slash_command and can't drift. Add
regression tests for compaction and routing_decision.

Also address non-blocking review notes:
- Wrap non-404 create errors in a clean ClickException instead of a raw
  httpx traceback.
- Document created_by re-attribution in --help alongside the response_id
  caveat.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-23 14:25:36 +00:00
Anas Khan 2561d54ee5 fix(hermes): mirror native reasoning to the web conversation (#1645)
The hermes-native forwarder's messages SELECT omitted the reasoning
columns Hermes persists, so thinking shown in the TUI never reached the
web conversation. Read reasoning_content/reasoning and emit a one-shot
external_output_reasoning_delta before the assistant message (started=True),
matching the codex- and opencode-native transient reasoning contract. The
structured codex_reasoning_items column is left alone.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-23 13:29:56 +00:00
Enes Yilmaz 414b404ee5 fix(codex): fall back to the runner workspace when no explicit cwd is set (#3015)
The codex harness wrap read only HARNESS_CODEX_CWD, so when the spawn env
omits that var the executor fell through to os.getcwd(). Seven sibling
harnesses (acp, claude-sdk, goose, hermes, kimi, pi, qwen) already fall
back to OMNIGENT_RUNNER_WORKSPACE first.

tests/runtime/test_spawn_env_cwd.py::test_builder_omits_cwd_when_none
documents that the builder omits the CWD var precisely so the harness can
apply its own OMNIGENT_RUNNER_WORKSPACE fallback. codex is in that test's
builder list but never held up the harness half of the contract.

Every current caller threads a cwd, so this changes no observed behavior
today. It closes the contract gap and covers a caller that omits it.

Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
2026-07-23 22:25:30 +09:00
Jakub Majorek c3facacd57 feat(cli): add omni usage cost report (#2787)
*  feat(cli): add `omni usage` cost report

Summarize LLM spend across a user's sessions: rolling 24h / 7d / 30d
cost totals plus a per-session breakdown of model and cost.

- server: `GET /v1/usage` aggregates each top-level session's subtree
  usage (via `load_session_usage`), scoped to the caller, bucketing
  cost by last-activity time; normalizes the primary model per session.
- cli: `omni usage` (`--limit`, `--server`, `--json`) renders the
  report through the shared `omnigent.inner.ui` palette.

Co-authored-by: Isaac
Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>

*  feat(usage): address review — separate router, per-model breakdown, daily-rollup windows

Addresses the four review comments on the `omni usage` cost report:

1. Move the report to its own user-scoped router (omnigent/server/routes/
   usage.py) instead of the session-scoped sessions router.
2. Rename the schema UsageSession -> SessionUsage.
3. Show a per-model cost breakdown per session, mirroring the web session
   sidebar: authoritative session total on the id line, each model's
   recorded cost beneath (shown faithfully, not forced to sum). Single-model
   sessions stay on one line.
4. Source the cost summary (Today / Last 7 days / Last 30 days / All time)
   from the per-user daily-cost rollup (user_daily_cost) via a new
   sum_daily_cost range read, so windows reflect when spend occurred rather
   than a session's last-activity time. Labels relabeled to calendar-day
   truthful wording.

Regenerates openapi.json; updates unit + e2e tests.

Co-authored-by: Isaac
Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>

---------

Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>
2026-07-23 21:58:47 +09:00
Serena Ruan e4c895c7e6 test(ui-snapshot): add sidebar pinned-project flyout baseline (#3140)
* test(ui-snapshot): add sidebar pinned-project flyout baseline

The populated-sidebar baseline covers every sidebar row type but not the
hover flyout that surfaces a pinned session's originating project — the
card is portalled and only mounts on hover, so a restyle of it (recently
aligned to a compact HoverCard: clamped title + folder icon + project
name) sails through that gate.

Add a visual test that hovers a pinned, project-owned row and captures
`PinnedProjectFlyoutContent`. Mirrors the populated-sidebar fixture's
determinism (pinned clock, silenced updates socket, seeded localStorage);
the flyout's 150ms openDelay fires under set_fixed_time since only Date.now
is pinned, so a plain hover opens it.

Baseline PNG intentionally omitted — generated in CI's pinned image via the
`update-ui-snapshot` label so it matches the gate byte-for-byte.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-23 19:47:30 +08:00
Bryan Chua a3d6be1221 fix(codex): normalize deprecated ultra/max reasoning effort to xhigh (#2697)
The ChatGPT desktop app writes model_reasoning_effort = "ultra" into
~/.codex/config.toml; the codex CLI forwards it as the retired "max"
wire value, which the OpenAI Responses API rejects with
invalid_value: 'max' (its ladder tops out at xhigh). Because the codex
harness copies the user config verbatim into every per-session
CODEX_HOME, every codex turn fails on such machines — including debby's
gpt sub-agents.

Two-part fix:
- validate_effort() coerces a deprecated alias (ultra/max -> xhigh) when
  the raw value is unsupported but the canonical one is. Providers that
  genuinely support max (Anthropic) are unaffected. This also stops the
  server rejecting external_reasoning_effort_change events from
  ChatGPT-app-configured codex terminals that report effort ultra.
- _populate_codex_home_config() normalizes a deprecated top-level
  model_reasoning_effort in the session's private config.toml copy;
  keys inside tables and supported values are left untouched, and the
  user's real ~/.codex/config.toml is never modified.

_normalize_copied_codex_effort() now tracks array bracket depth so a
top-level multiline array's continuation lines (which can themselves
start with "[") are never mistaken for a table header — otherwise a
still-top-level model_reasoning_effort key after such an array would be
skipped. Also updates the two reasoning-effort-validation tests that
asserted "max" was rejected outright: since max/ultra now coerce to
xhigh for codex and the OpenAI Agents SDK, those tests now assert the
coercion instead.

Fixes #2696

Signed-off-by: Bryan Chua <me@bryanchua.com>
2026-07-23 11:34:31 +00:00
Tomu Hirata 83f17cc646 fix(runtime): strip base64 image data from stored history on replay (#3133)
* fix(runtime): strip base64 image data from stored history on replay

The native-ingest strip only helps images read *after* that fix landed.
Sessions already in the conversation store still hold full base64 images
in their function_call_output items, so they keep overflowing the context
window on resume — replaying the stored output as prompt text wedges
compaction (loads over-window history to summarize, fails "prompt is too
long", writes no boundary, re-overflows).

Strip inline base64 image blocks at the replay boundary in
history_to_input_items, where every harness's stored history is converted
to LLM input. This fixes already-stored large-image sessions without a
store migration. A base64 image tool result (JSON list of
{"type":"image","source":{"type":"base64",...}} blocks) is rewritten to a
"[<media> image omitted from history …]" placeholder that points back at
the originating tool call so the image stays recoverable on demand.
Plain-text and non-image JSON outputs (the common case) pass through
unchanged via a cheap guard before any JSON parse.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(runtime): strip base64 from truncated (invalid-JSON) image outputs

Testing against the real wedged session's export revealed the JSON-only
strip was a no-op on exactly the data that matters: stored image outputs
are clipped at the conversation-store 245760B cap, leaving the base64
string unterminated, so json.loads raises and the original (base64-laden)
output was returned unchanged.

Add a linear regex fallback that rewrites an image source block in place
when the output is not parseable JSON. The pattern uses fixed optional
key groups and a base64-alphabet char class disjoint from the quote
terminator, so it cannot backtrack catastrophically against a
multi-hundred-KB payload (an earlier lazy-quantifier attempt hung).

Verified on the real 3440987444542977 export: all 4 truncated image
items strip, 982,448 -> 832 chars (99.92%), sub-ms. New test covers the
truncated/invalid-JSON shape.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(claude-native): strip truncated base64 images on cold resume

Native Claude Code resumes from its own local transcript, which the
wrapper rebuilds from Omnigent items before `claude --resume`. Intact
image tool results are intentionally rehydrated into real image blocks
(cheap ~1.5K tokens). But an output clipped at the conversation-store
byte cap holds corrupt/partial base64 that no longer parses: rehydration
fails, so the raw ~250K-char string was sent as tool_result text AND
stashed in toolUseResult — re-overflowing the resumed context and
wedging compaction (the exact native failure users hit).

Collapse only that truncated/unparseable-image case to a recoverable
placeholder before building the record, so both the tool_result content
and the toolUseResult metadata stay small. Intact images still resume as
images.

Verified on the real 3440987444542977 export: full transcript rebuild
drops from 1,549,700 to 563,994 chars with zero base64 leak, while a
valid image still rehydrates to an image block.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-23 20:02:18 +09:00
Tomu Hirata 4e509ea248 feat(llms): merge extra_headers and log upstream 4xx bodies (#3138)
Merge caller-supplied headers threaded through connection_params so MAS
can route CP serving-endpoint calls through the Barnacle forward proxy
(host + s2s auth headers). Also log the upstream error body on 4xx/5xx
for both non-streaming and streaming requests, which raise_for_status()
otherwise omits — essential for debugging CP serving/gateway failures.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-23 10:58:40 +00:00
Serena Ruan b2f38ea334 docs(web): drop stale migration comments from the composer config code (#3137)
The in-session config gear PR left comments that narrated the change
(a now-deleted IntelligentModelControl reference, "moved OUT of the picker
trigger", "no longer a standalone toggle", "old/pre-gear picker") and named
a "picker trigger"/"Agent picker" that no longer exists. Rewrite them to
describe current behavior — where the Smart Routing toggle, harness label,
and model/effort label live — per the repo's "describe the scenario, not
the change history" guidance.

Comment-only; no behavior change.

Co-authored-by: Isaac

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-23 18:55:23 +08:00
Tomu Hirata 2fdb72bdb2 fix(auto-harness): post-merge fixes for auto harness routing (#3093)
* feat(databricks-adapter): use SDK Config for OAuth token refresh

Cache a databricks.sdk.config.Config per profile and call authenticate()
on every request so OAuth tokens are refreshed transparently instead of
expiring after ~1 hour. Falls back to resolve_databricks_workspace when
the SDK is unavailable.

This addresses the v1 limitation documented in credentials/databricks.py.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): hide per-turn Smart Routing toggle when Auto harness is selected

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(new-chat): hide Smart Routing checkbox in favour of Auto harness

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(auto-harness): propagate routing error to UI via routing card

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(smart-routing): route harness+model for child sessions via sys_session_send

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(smart-routing): force auto-harness for sub-agents when parent routing is on

When the parent session has smart routing enabled, a sub-agent created via
sys_session_send is now routed regardless of the harness/model the
orchestrator chose — the server forces the "auto" sentinel at child-session
create time, ignoring the tool call's agent/model args. The first-message
routing path then picks both harness and model.

Skips native-terminal wrapper labeling for forced-auto children so the
harness isn't prematurely fixed (routing may pick a non-native SDK harness);
the child takes the SDK routing path where auto-resolution runs.

Only applies to omnigent-executor agents (auto needs a swappable brain
harness); non-omnigent children keep the orchestrator's choice.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): persist cost_control=on for Auto sessions, hide composer routing toggle

- New-chat create body sends cost_control_mode_override="on" when harness=auto
  so the persisted state matches the routing that always runs for auto sessions.
- Hide the per-turn composer routing icon entirely — it's superseded by the
  Auto harness (routes at session start), and its "off" state was misleading.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): exclude databricks-claude-haiku-4-5 from pi routing candidates

pi routes Claude models through the Anthropic Messages gateway, whose request
path adds an eager_input_streaming field the Databricks serving endpoint
rejects with a 400 when tools are present. Filter the model out of pi's
candidate list in route_session_harness (both live-catalog and static paths)
so Claude work routes to claude-sdk instead. Keeps pi's GPT models.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): prevent double-routing on forced-auto child sessions

The auto-harness resolution block and the per-turn routing block both called
route_session_harness on a forced-auto child's first message (parent routing
on + harness_override="auto"), causing two judge calls, two routing cards, and
a possible harness/model mismatch between the two picks. Track whether the auto
block routed this turn and skip the per-turn block when it did. Also fixes the
failure-path card duplication (auto emits an applied=False card, then no longer
falls through to a second card).

Cleanup: except (ImportError, Exception) -> except Exception in the databricks
adapter (Exception already subsumes ImportError).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(auto-harness): mirror routing card into parent session for sub-agents

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): map live-catalog worker names to harness ids for routing

The live runner catalog (fetch_runner_models) keys rows by worker name —
sub-agent names like "claude_code" plus "self" — not by harness id. So
route_session_harness found no matches for _AUTO_ROUTING_HARNESSES and
returned "No routable harnesses are available", especially for child
(sub-agent) sessions.

Normalize worker names to harness ids via _WORKER_NAME_TO_HARNESS
(claude_code -> claude-sdk, codex, pi), and fall back to the static
infer_models table when the live catalog yields no routable candidates
(e.g. a catalog with only an unrecognized "self" worker).

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(ci): remove dead _ROUTABLE_HARNESSES and effectiveHarness (noUnusedLocals)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* test: update child-session routing test for forced-auto (route_session_harness)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* test: remove dead Smart Routing dialog tests (superseded by Auto harness)

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): exclude gpt-5.5/5.6 reasoning models from pi routing

pi routes GPT models through the openai-completions (/chat/completions) path.
Databricks applies a default reasoning_effort for the gpt-5.5/5.6 reasoning
models there and rejects tool calls with "Function tools with reasoning_effort
are not supported for gpt-5.5 ... use /v1/responses or set reasoning_effort to
'none'." pi's provider can't send that override, so every tool turn 400s.

Exclude databricks-gpt-5-5, -5-5-pro, and the -5-6 family from pi's routing
candidates (same pattern as pi+claude-haiku). The gpt-5.4 family works on pi
and stays; codex serves gpt-5.5+ via the Responses API natively.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): redirect incompatible router verdicts off pi

Some external routers ignore the filtered candidate set we send and still
return an excluded (harness, model) pair — e.g. pi + gpt-5-5. Since we can't
stop the router choosing it, post-process the verdict: redirect a Claude model
on pi to claude-sdk and a gpt-5.5/5.6 reasoning model on pi to codex (which
serves them via the Responses API). The chosen model is preserved; only the
harness is corrected to one that can actually run it with tools.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* style: ruff format test_sessions_model_override

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): order codex before pi so GPT models default to codex

_AUTO_ROUTING_HARNESSES order is both the candidate-set insertion order and
the tiebreak when a model is served by multiple harnesses (the external
router's id-only fallback and our own model-ownership fallback both pick the
first harness owning the model). With pi before codex, a GPT model with no/
ambiguous harness resolved to pi — whose openai-completions path 400s on
gpt-5.5+ reasoning models with tools. Reorder to codex, pi so GPT defaults to
codex (Responses API, handles reasoning+tools).

Complements _redirect_incompatible_pick, which handles the separate case of a
router returning an explicit pi+gpt-5.5 pair despite our filtered candidates.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): stop filtering candidates; router requires full model set

The external task_v0 router enforces a required model set (e.g. must include
gpt-5-6-luna) and returns 400 "task_v0 requires [...] models" when any is
missing. Our _filter_excluded_models pruning stripped gpt-5.5/5.6 and Claude
models from pi's candidates, making the required set incomplete and 400-ing
every route call.

Send the full candidate set unfiltered and rely solely on
_redirect_incompatible_pick to correct an incompatible (harness, model)
verdict after the router responds. Removes the now-unused _filter_excluded_models.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): emit routing card after input.consumed so it renders

The auto-harness routing card (success and failure) was published to the live
SSE stream at resolution time — before the runner forward and before
input.consumed. The user-message bubble hadn't been delivered yet, so the
reducer dropped/misordered the card and it never appeared live (only on
reload). Defer the card emission to after input.consumed, matching the
per-turn routing path's ordering. Now the "router unavailable" failure card
shows in the UI.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): refresh external router OAuth token per call

ExternalRoutingClient captured its bearer once at server startup (from the
routing profile), so after ~1h the token expired and the router 401'd
("Credential was not sent or was of an unsupported type"), which surfaced as
"router returned no verdict". Pass the Databricks profile through and mint a
fresh bearer per route() call via the SDK Config (same OAuth-refresh pattern
as the DatabricksAdapter fix). An explicit api_key still uses a static bearer.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* feat(auto-harness): surface the router's actual error in the failure card

The auto-harness failure card showed a generic "router returned no verdict".
ExternalRoutingClient swallowed the real reason (401, task_v0 required-model-set,
etc.) — only logging it. Record it on client.last_error and have
route_session_harness surface it, so the UI card reads e.g. "Routing
unavailable: router returned HTTP 401: Credential was not sent or was of an
unsupported type". _router_error_detail unwraps the gateway's nested JSON
error envelope to a clean message.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(auto-harness): route sub-agents against the parent's catalog

A sub-agent's own runner catalog is "self"-only (it's a leaf spec with no
sub-agents), so _WORKER_NAME_TO_HARNESS didn't recognize it and routing fell
back to the small static infer_models lists — a different, incomplete candidate
set than the top agent sees (which broke the external router's required-model
check, e.g. missing glm-5-2/gpt-5-6-luna).

Add catalog_session_id to route_session_harness and pass the parent session id
for sub-agent routing (parent + child share a runner). The parent's catalog
enumerates the full spawnable-worker map (claude_code/codex/pi with complete
model lists), so a sub-agent now routes against the same stable candidate set
as the orchestrator — regardless that we route both harness and model for it.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* test(routing): assert external client defers profile auth to per-call

_build_external_routing_client no longer resolves a Databricks profile
token at build time — the client mints a fresh bearer per request (OAuth
refresh) so it survives ~1h token expiry. Update the test to assert the
profile is threaded through (no eager resolve, no static _auth) instead
of the old build-time resolution contract.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-23 10:46:20 +00:00
Serena Ruan d641eb79f9 fix(web): align sidebar session flyout and row padding (#3124)
* fix(web): align sidebar session flyout and row padding

The session hover flyout and the sidebar rows were visually inconsistent
with the pinned-project flyout and project folder rows:

- The plain session tooltip used a wide card (w-72, bg-card-solid) while
  the pinned-project flyout used a compact HoverCard look. Restyle the
  tooltip to mirror it (w-64, bg-popover, clamped title, muted metadata).
- Both flyout titles used rem-based `text-sm`, which scaled with the UI
  font-size setting and rendered larger than the fixed-px sidebar rows.
  Size both to `sidebar-compact-text` so they match the row name exactly.
- Session rows used `w-[calc(100%+1rem)]`, bleeding ~8px past the right
  edge so their highlight didn't align with the project/folder rows.
  Switch to `w-full` and shift the trailing pin/kebab controls inward
  (right-[30px] / right-1) so they stay inside the row edge.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): drop reserved scrollbar gutter so sidebar rows sit flush right

The sidebar scroll container reserved a stable scrollbar gutter
(`scrollbar-gutter: stable`), which on overlay-scrollbar platforms
(macOS) leaves ~15px of empty space on the right of every row. That made
rows look uncentered — 8px inset on the left vs. 8px + 15px on the right —
and misaligned the project-folder header actions with the session-row
controls. It's also why session rows previously used `w-[calc(100%+1rem)]`
to paint over the gutter (the workaround this series already removed).

Drop the reserved gutter so the right inset collapses to the same 8px
`px-2` as the left. On overlay scrollbars there's no layout shift; the
rows and folder-header actions now line up on both edges.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): match project-folder header controls to compact session kebab

The project-folder header pencil + kebab used `icon-sm` (size-7, 28px)
while the session-row kebab uses `icon-xs` (size-6, 24px). Both anchor at
`right-1` with a centered `size-3.5` glyph, so the 4px width difference
put their glyph centers in different columns — the folder ⋯ sat ~2px left
of the row ⋯ and read as misaligned.

Drop the folder-header controls to `icon-xs` so they share the compact
size (and glyph column) with the session-row kebab.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): match folder-header icon spacing to session row

The folder-header pencil + kebab sat in a gapless flex, while the session
row's pin↔kebab pair has a 2px (right-1 vs right-[30px]) gap. That put the
folder pencil 2px right of the session pin, so the leading-icon columns
didn't line up across row types.

Add `gap-0.5` to the folder-actions flex so the pencil lands in the same
column as the session pin; the kebabs already share the trailing column.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): shrink Projects group-header controls to compact icon

The "New project", "Expand all", and "Collapse to previous" controls in
the Projects group header were still `icon-sm` (size-7, 28px) while every
other right-gutter control — folder-row and session-row pin/kebab — is now
`icon-xs` (size-6, 24px). The larger buttons broke the shared icon column.

Drop all three to `icon-xs` so the whole sidebar right-gutter shares one
compact size.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* refactor(web): share one flex container for sidebar row trailing controls

The session row's pin + kebab were two separately absolute-positioned
buttons, so their spacing was hand-tuned per button and drifted from the
project-folder header actions at non-default font scales. Wrap both in a
single `absolute right-1 flex items-center gap-0.5` container — the same
pattern the folder header already uses — so the spacing is defined once
and stays aligned across every right-gutter control at any scale. Also add
the matching gap-0.5 to the Projects group-header controls.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

* fix(web): reserve scrollbar gutter symmetrically instead of removing it

Removing `scrollbar-gutter: stable` fixed the right-edge asymmetry on
macOS overlay scrollbars but reintroduced horizontal reflow on classic-
scrollbar platforms (Windows/Linux) when the scrollbar appears/disappears.

Use `stable both-edges` instead: the gutter is reserved symmetrically on
both sides, so rows stay centered against the left `px-2` inset and never
reflow — a no-op on overlay scrollbars, correct on classic ones.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(e2e-ui): regenerate visual baselines

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
2026-07-23 18:30:43 +08:00
Pat Sukprasert d681baf83b docs(harness): sync Phase 0 split status in modular-registry proposal (#3136)
The Phase 0 section listed pre-split line counts and framed the cli.py and
sessions.py extractions as to-do, but both have shipped. Update it to reflect
actual state: correct the counts, mark cli.py (#3047) and sessions.py (#3097)
done, and leave runner/app.py and test_app_sessions_native.py as the two
remaining >10k files (which can proceed in parallel). Move chat.py to a
deferred bucket since it is already under the 10k target.

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-23 17:12:33 +07:00
Daniel Lok 50e6bd85d0 test(runner-init): guard fork-history directives survive the reconnect envelope (#3125)
* test(runner-init): guard fork-history directives survive the reconnect envelope

Adds an integration test across the exact seam that regressed in #2793 and
was fixed in #3116: a forked claude-native session's fork directives
(carry-history, source-external-session) must survive from the store's
by-runner-id reconnect lookup into the session-init envelope the runner
reads to decide whether to clone/rebuild the vendor transcript.

Unlike the existing envelope tests (which hand-build an envelope with the
label already present) and the store unit test (which checks one method in
isolation), this drives the real store end to end — create a native source
with a captured external_session_id + workspace, fork it with
carry_history_into_native, bind it to a runner, then run
list_conversations_by_runner_id -> build_runner_session_init_payload ->
parse -> _claude_launch_metadata_from_envelope and assert the fork
directives land as launch metadata. It fails if any layer on that path
stops carrying labels (verified: reverting #3116's hydration makes it fail
with an empty label set).

Runs in CI (no vendor Claude login), unlike the opt-in
tests/e2e/test_host_claude_native_fork_e2e.py that would otherwise be the
only coverage of this path — which is why the original regression slipped
through.

Co-authored-by: Isaac

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: repair test docstring indentation broken by suggested edit

A GitHub-suggested "Potential fix for pull request finding" commit
(b48c50b3) rewrote the test docstring flush-left, leaving the function
with no indented body -> IndentationError, which failed ruff-format,
ruff-check, and pytest collection (server-rest).

Restore a properly-indented docstring and switch the em-dashes/arrows in
comments to ASCII so the file is unambiguously parseable everywhere. Test
behavior is unchanged: still passes with #3116's label hydration and fails
without it (verified by reverting the fix).

Co-authored-by: Isaac

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-23 10:07:11 +00:00
Pat Sukprasert 06f52ea0f7 refactor(server): split sessions route into facade + impl package (#3097)
* refactor(server): split sessions route into facade + impl package

The sessions route had grown to ~15k lines in a single file, well past
the 10k-line ceiling we want for maintainability and ahead of the
native-harness pluggability work that will touch this module heavily.

Split it into a facade over an implementation package:

- sessions.py (7.7k) stays the public entry point, keeps
  create_sessions_router, and re-exports the impl modules via `import *`.
- _sessions/common.py, helpers.py, orchestration.py hold the
  implementation, layered common -> helpers -> orchestration, each
  star-importing the ones below it.

No behavior change. Symbols that tests patch on the facade are exposed
through call-time proxies that delegate back to the facade, so a
`monkeypatch.setattr(sessions_mod, ...)` is honored no matter which impl
module resolves the name. F403/F405 are waived for these files in
pyproject since star re-export is the point of the facade.

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(sessions): honor facade monkeypatch across _sessions impl modules

The facade/_sessions split re-exports symbols via `import *`, so each impl
module holds its own binding of every name. A test's
`monkeypatch.setattr(sessions, "_kick_managed_wake", ...)` rebound only the
facade attribute; sibling impl callers kept their stale star-import binding and
ran the real path, breaking managed-wake and compact single-flight tests.

Route the patched symbols (`_kick_managed_wake`, `_compact_lock`) through
call-time facade proxies with the real body renamed `*_impl`, and add explicit
facade override imports so the patch is honored no matter which module resolves
the name.

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* refactor(sessions): route impl-module get_agent_cache/session_stream through facade proxy

Drop the function-local `from omnigent.runtime import get_agent_cache`
and `from omnigent.runtime import session_stream` imports in the impl
modules. Those locals shadowed the module-level facade-delegating
proxies (bound via the `# noqa: F401` import block from
`_sessions.common`), so a `monkeypatch.setattr` on the facade was not
honored at those call sites.

Removing the shadowing imports lets the already-bound module-level
proxies resolve the names, keeping facade patches effective while
behaving identically when unpatched (the proxy forwards to the real
runtime symbol). Addresses Copilot review on the sessions split.

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(sessions): repair cross-module seams from the facade split

The _sessions split moved code behind an explicit __all__ per impl module
and a star-import facade, which introduced three latent seams:

- _validated_harness_override_executor_type was omitted from
  helpers.__all__, so the harness_override == "auto" gate in
  orchestration (which sees it only via star-import) hit NameError at
  session creation. Add it to __all__.

- _query_host_runner_status read _HOST_RUNNER_STATUS_TIMEOUT_S off its
  own star-import binding, so a facade-level monkeypatch was dropped.
  Read the constant off the facade module instead; strengthen the
  timeout test to assert the wait actually bails early.

- _wait_for_managed_runner_tunnel and _run_managed_wake read
  _HOST_RELAUNCH_RUNNER_CONNECT_TIMEOUT_S bare; qualify both through the
  facade for the same reason.

Add test_sessions_facade_exports.py to pin these re-export seams so a
dropped __all__ entry or un-re-exported constant fails at import time.

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(sessions): restore call-time get_agent_cache import in resolvers

The split dropped the call-time `from omnigent.runtime import
get_agent_cache` local import from the four harness/model resolver
functions. Without it the name resolved to the module-level facade
proxy, which forwards to a snapshot binding taken at import time, so a
test patching `omnigent.runtime.get_agent_cache` was no longer honored
and the call hit the real uninitialized runtime.

Restore the local import in _resolve_llm_model, _resolve_harness_impl,
_validated_harness_override, and _validated_harness_override_executor_type
to match pre-split behavior.

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-07-23 16:54:23 +07:00
Serena Ruan 9bbb4eeb99 feat(web): in-session composer config gear modal (#3111)
* feat(web): in-session composer config gear modal

Bring the new-session gear-config affordance (#3050) into the in-session
composer. A gear icon left of the send button shows the session's live
run-config on hover and opens a config modal on click, consolidating the
mid-session switchable knobs — Model, Effort, and Smart Routing — behind one
control. Permission/approval/cursor modes stay launch-time only and are
intentionally absent.

What changed:

- New ComposerConfigGear + SessionConfigModal: draft Model/Effort/Smart Routing
  and apply on Save (Cancel discards), mirroring HarnessConfigModal. Save
  commits SEQUENTIALLY (awaiting each PATCH) because claude-native applies
  model/effort by typing separate /model and /effort slash commands into its
  terminal — firing them concurrently interleaves the injections into one bad
  line. Unchanged knobs are skipped.
- The <Model> <Effort> control is now a read-only status label, not a dropdown
  (the gear owns config); bare /model opens the modal. The label reads "Smart
  Routing" when routing is on, and falls back to the harness identity
  ("Polly (Pi)") for SDK/bundle agents that surface no model/effort.
- Harness identity moved out of the status-line tray into the gear tooltip.
- The gear is soft-disabled (aria-disabled + click guard, tooltip preserved)
  when the session isn't live, since a config PATCH can't wake a sleeping
  runner and those states never load the model catalog.
- Extracted ConfigRow / DescribedSelect / MODEL_SELECT_* sentinels from
  NewChatDialog into web/src/components/HarnessConfigControls.tsx for reuse.
- Removed the standalone IntelligentModelControl and its per-turn verdict chip;
  Smart Routing now folds into the Claude Model dropdown (a Switch for other
  routable agents).

Smart Routing eligibility is unchanged (same isCostRoutingSession gate the
prior control used); a KNOWN GAP note documents that the in-session gate is
stricter than the new-session dialog's routable-harness rule, to be aligned in
a follow-up.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): restore host/context tray + fold Smart Routing into Codex model dropdown

Two follow-up fixes on the in-session composer gear modal:

- Restore the composer status-line tray (host badge + context ring) for
  host-bound sessions that have no worktree branch and no context ring yet
  (e.g. codex). Removing the harness label from the tray also dropped it from
  the render guard, which had been the de-facto "always render for a bound
  session" trigger — so the whole shelf vanished. Gate on a `showHostBadge`
  (host-bound + non-sub-agent) signal instead. Fixes the failing
  test_host_badge / test_hosts_changed_push e2e specs.
- Fold Smart Routing into the Model dropdown for ANY agent that has one
  (Claude and Codex), not just Claude. Previously Codex got both a standalone
  Smart Routing switch AND a Model dropdown whose selected value could become
  the routing sentinel with no matching option (empty trigger). The rule is now
  "has a Model dropdown" (showModels): fold in when it does, standalone Switch
  only for routable agents without one (e.g. Polly).

Both covered by regression tests (host-bound tray renders with no branch/ring;
Codex folds routing into its dropdown with no standalone switch). Verified the
previously-failing host-badge e2e specs and the gear-modal e2e specs pass
locally.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(ui-snapshot): update visual baselines for composer gear modal

The composer now shows a read-only model/effort label + config gear (and
the harness label moved into the gear tooltip), which changes the chat
conversation render. Regenerate the three drifting visual baselines from
the PR's CI-rendered artifact (byte-identical to the pinned Playwright
image the UI Snapshot gate compares against) so the gate passes.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* refactor(web): drop orphaned IntelligentModelControl + verdict exports

This PR relocated the standalone Smart Routing control into the composer
gear modal and removed its only app-code usage, leaving
IntelligentModelControl, parseCostRoutingVerdict, CostRoutingVerdict,
verdictRelativeTime, ModelTierPill, and COST_CONTROL_PLAN_LABEL with no
remaining consumers (only their own tests). Delete them and their tests.

Keep the still-used exports: isCostRoutingSession (ChatPage eligibility
gate), CostControlMode (NewChatDialog), and shortModelName (StatusBlocks
+ SmartRoutingCard). Fix the stale {@link ModelTierPill} JSDoc reference
in SmartRoutingCard.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(ui-snapshot): exercise the composer config gear in the chat baseline

The chat visual-snapshot fixture served a bare session (no omnigent.wrapper
label, no model_options), so modelPickerKind was null and the composer's
config gear + read-only model/effort label never rendered — the baseline
couldn't guard them. Patch the mocked session into a claude-native wrapper
(labels + harness + llm_model + model_options, mirroring the model-picker
e2e), and wait for the gear + model/effort label before capture, so the
baseline now covers the new composer surface.

The committed [linux] baseline PNG is regenerated separately from the CI
render (no Docker locally); verified on a throwaway [darwin] render that the
gear + "Sonnet 5" label now appear.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* test(ui-snapshot): regenerate chat baseline capturing the composer gear

Adopt the CI-rendered [linux] baseline (byte-identical to the pinned
Playwright image the gate compares against) now that the fixture renders
a claude-native session: the composer shows the config gear + "Sonnet 5"
model/effort label.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

* fix(web): don't re-pin a leaked sticky on routing-off; use effort sentinel

Two non-blocking review notes:

- Routing-off on a no-dropdown routable agent (e.g. Polly) entered the
  model-commit branch and could setModel(resolvedModelId) where
  resolvedModelId resolves the leftover cross-session sticky
  (sessionModelOverride ?? selectedModel) — pinning a model the user never
  chose. Gate the routing-off re-pin on showModels: only agents with a Model
  dropdown re-pin; no-dropdown agents clear via setModel(null).
- The Effort select reused MODEL_SELECT_DEFAULT as its "none" sentinel;
  switch to the purpose-built EFFORT_SELECT_NONE for consistency with the
  new-session dialog.

Adds a regression test proving a leaked "gpt-5.5" sticky is not pinned when
turning routing off on an SDK/bundle agent.

Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

---------

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
2026-07-23 17:54:10 +08:00
Tomu Hirata 55a3884872 fix(claude-native): strip base64 image data from tool-result history (#3113)
* fix(claude-native): strip base64 image data from tool-result history

Reading an image file via Claude Code's Read tool returns the image as
a list of {"type":"image","source":{"type":"base64",...}} blocks. The
transcript mirror serialized that content verbatim into the stored
function_call_output, so a single image cost ~245KB (~70K+ tokens) of
literal text. On resume the native harness replays these items as prompt
text, and a handful of image reads overflows even a 1M context window —
which then wedges compaction (it must load the same over-window history
to summarize, fails with "prompt is too long", writes no compaction
boundary, and re-overflows on the next resume). The base64 is useless to
the model as text anyway.

Strip inline base64 image blocks to a "[image omitted from history]"
placeholder before serializing the tool-result output. Observed on a
real wedged session: 245,080 -> 55 chars per image (99.98% reduction),
eliminating the ~281K-token replay overrun.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

* fix(claude-native): make stripped-image placeholder recoverable

The base64-strip placeholder was a dead "[image omitted from history]"
marker. Since a stripped image always comes from a tool call (e.g. Read
of a file path) that is preserved intact right before the output, the
agent can view the image again by re-running that call. Name the media
type and say so in the placeholder, so the image is recoverable on
demand rather than appearing silently lost.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>

---------

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-23 18:31:06 +09:00
Anthony Ivan 09b9f00c76 fix(pi): show intermediate reasoning (#2979)
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com>
Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com>
2026-07-23 17:56:19 +09:00
Tomu Hirata a30cc35063 fix(llms): flatten list-shaped content in non-streaming converter (#3109)
Non-streaming chat_response_to_response stored message.content raw, so
for Claude via Databricks (and Kimi, etc.) — which return content as a
list of typed blocks — OutputText.text became a list instead of a str.
This broke prompt_policy (fail-closed DENY on .strip() of a list) and
any non-streaming consumer of databricks-claude-* models.

Reuse the existing _extract_delta_content helper (already used by the
streaming path) to flatten list-of-blocks content into a string; it
returns the plain string unchanged for existing providers.

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
2026-07-23 17:48:32 +09:00
Daniel Lok 1e914403e2 fix(store): hydrate labels in list_conversations_by_runner_id (#3116)
Forked claude-native (and other native) sessions launched the vendor
TUI with no prior conversation history, even though the fork copied the
history into the store (the web UI showed it). The runner never received
the fork directives that drive transcript seeding.

Root cause: list_conversations_by_runner_id built its Conversation
entities without fetching labels, so they carried labels={}. The runner
reconnect path (_on_runner_connect) sources conversations from this
lookup and builds the session-init envelope from conversation.labels;
with an empty label set the fork directives (omnigent.fork.carry_history,
omnigent.fork.source_external_session_id) were dropped in transit. The
init-envelope initializer then caches and shares that label-less envelope
with the first-turn path, so even the label-hydrated get_conversation
result was never used for the envelope. The runner saw no fork labels,
skipped the clone/rebuild branches, and launched the TUI fresh.

This dropped labels for every consumer of the reconnect path, not just
claude-native forks — any label-driven behavior on reconnect (codex / pi
/ qwen fork history, presentation ui/wrapper labels) was equally
affected and is fixed by the same hydration.

Fix: fetch labels via the existing batched _fetch_labels_bulk inside the
same _conv_session and thread them into _to_conversation. One extra
query, no N+1, correct under the split-DB topology (labels live in the
conversation DB).

Co-authored-by: Isaac
2026-07-23 08:13:49 +00:00
Aravind Segu 1370a31247 Add injectable-conversation-id seams to create_session_with_agent and fork_conversation (#3106)
`create_conversation` already accepts an optional `conversation_id` (falling back
to `generate_conversation_id()` when omitted). This extends the same capability to
the other two session-creating methods via protected `_..._with_id` seams:

- `create_session_with_agent(...)` -> `_create_session_with_agent_with_id(conversation_id, ...)`
- `fork_conversation(...)` -> `_fork_conversation_with_id(conversation_id, ...)`

The public methods stay unchanged thin wrappers that pass `generate_conversation_id()`,
and the `ConversationStore` ABC is untouched, so this is a behavior-preserving refactor
for all existing callers. It lets a subclass mint the id externally and inject it as the
row id (e.g. a store that keys conversations by an identity-service node id) — which
`create_conversation` already permits but these two methods did not.

Co-authored-by: Isaac

Signed-off-by: aravind-segu <aravind.segu@databricks.com>
2026-07-23 01:07:00 -07:00
1320 changed files with 183338 additions and 100941 deletions
@@ -71,8 +71,10 @@ Three transports, easy to confuse:
agy models # exits 0 and lists models only when signed in; else 'Please sign in'
```
`False` / non-zero → run `agy` once and sign in. agy's token lives under
`~/.gemini` (`oauth_creds.json` on macOS, `antigravity-cli/antigravity-oauth-token`
on Linux).
`~/.gemini` (`oauth_creds.json` on macOS through 1.0.10,
`antigravity-cli/antigravity-oauth-token` on Linux); agy 1.1.7+ on macOS
writes no token file and keeps the credential in the Keychain, which is why
`gemini_login_detected()` falls back to `agy models` there.
4. **`tmux` is on PATH.** The agy terminal is a runner-owned tmux pane; the CLI
attaches to it and the executor drives it via `tmux send-keys`
(`_preflight_local_tools` hard-fails without tmux).
@@ -115,6 +115,19 @@ All capabilities are **required** for a complete harness integration:
- [ ] Unit tests cover tool bridging, auth, model routing
- [ ] Mock LLM tests cover the happy path without real API calls
### Shortcut: ACP CLI harnesses are one catalog row
If the vendor CLI speaks the Agent Client Protocol on stdio (the
`goose acp` / `qwen --acp` family), do NOT write a new inner module, registry
entries, or a spawn-env builder. Add one row to `ACP_CLI_HARNESSES` in
`omnigent/acp_cli_harnesses.py` (label, binary, ACP argv, aliases, install
hint or npm package, vendor login command) plus docs. Validity, module
routing, picker label, capabilities, install spec, readiness, setup steps,
spawn env, and the live e2e-matrix exclusion all derive from the row;
`tests/test_acp_cli_harnesses.py` asserts the wiring per row automatically.
These rows run through `omnigent/inner/acp_harness.py` and `AcpExecutor`, own
their auth and model selection, and reject `/model` overrides up front.
---
## Part 2 — Native harnesses
+80
View File
@@ -0,0 +1,80 @@
---
name: run-load-test
description: Run the Omnigent load test and produce a results file explaining the latencies. Load when the user wants to load-test / stress-test / benchmark Omnigent under concurrency ("load test omnigent", "stress test the server", "how many hosts/sessions/turns can it handle", "load test real agent turns / conversations", "run a load test"). The test makes each simulated user a real omnigent host that creates host-bound sessions and drives real multi-turn conversations with a mocked LLM; it boots its own local stack (dev/loadtest/run.py). Gather inputs, run it, then read the generated summary.md and explain the latency distribution (avg/median/p95/p99, throughput, failures). NOT for single-request latency micro-benchmarks (that is dev/benchmarks/).
---
# Run the Omnigent load test
Drives `dev/loadtest/` end to end: collect inputs → run → read `summary.md`
explain the latencies. **Each Locust user is a real `omnigent host`** that
registers over the host tunnel, creates host-bound sessions, and drives **real
multi-turn conversations** — every turn is a genuine post→idle loop through the
host's runner, with the **LLM mocked** (zero latency) so the numbers are
Omnigent's own overhead. `-u N` scales the number of hosts.
It **boots its own local stack** (server + mock LLM), so there is no server to
point at, and it runs **from a repo checkout** only. For single-request latency
micro-benchmarks (not concurrency), that is a different tool: `dev/benchmarks/`.
## 1. Ensure deps (repo checkout)
```bash
pip install -e '.[loadtest,dev,agents-sdk]' # or: uv sync --extra loadtest --extra dev --extra agents-sdk
```
Run with that same interpreter (e.g. `.venv/bin/python`), from the repo root.
## 2. Gather inputs
Ask the user (AskUserQuestion when several are unknown); all have defaults.
| Input | Flag | Default | Notes |
|---|---|---|---|
| Hosts | `--users` | 4 | Concurrent hosts (N) — the main scale knob. |
| Spawn rate | `--spawn-rate` | 1 | Hosts started per second. |
| Run time | `--run-time` | 120s | `40s` / `5m` / `1h`. |
| Sessions/host | `--sessions-per-user` | 2 | Host-bound sessions each host drives. |
| Turns/session | `--turns-per-session` | 4 | Turns per session — history grows across them. |
| Reply length | `--reply-words` | 60 | Words in the mocked (streamed) reply per turn. |
**Capacity caveat — say this to the user if they ask for large N:** turns run on
real host + runner subprocesses, so N hosts × M sessions = N×M runner processes
on *this* box. It is capacity-limited by design (real turns, not faked). Start at
`--users 2 --sessions-per-user 1 --turns-per-session 2 --run-time 40s` to confirm
the stack boots (~10-30s), then ramp to a few dozen hosts at most. At high N the
load box saturates before the server (Locust warns about CPU).
## 3. Run
```bash
python dev/loadtest/run.py \
--users <N> --spawn-rate <R> --run-time <T> \
--sessions-per-user <S> --turns-per-session <TU>
```
It boots the stack, prints the server URL + registered agent, runs Locust, and
writes `dev/loadtest/results/omnigent_load_test-<timestamp>/`.
## 4. Read and explain
`Read` the `summary.md` and relay it. Focus on:
- **Outcome / failures** first. Exit 0 + 0 failures = PASS. Non-zero failures are
the headline — check `console.log` and, for a host that failed to register,
the per-host `results/.../host-workspaces/<name>/host.log`. At high N, failures
usually mean the *load box* saturated, not the server.
- **turn** — the headline latency: one full post→idle agent turn on a host's
runner (mocked LLM), so it is Omnigent's per-turn overhead. It **grows across a
conversation** as history accumulates, so a rising p95/p99 with larger
`--turns-per-session` is expected and is the interesting signal.
- **host online** — host tunnel registration cost; **session create** — the
host-bound create; **Ops/s** — aggregate throughput at this concurrency.
If failures appeared or the tail looks high, suggest a concrete next step (lower
N if the load box is saturated, raise `--turns-per-session` to study history
growth, lengthen `--run-time` for steady state, or check server logs/metrics).
## Notes
- Scenario file: `dev/loadtest/omnigent_load_test.py`; driver + report:
`dev/loadtest/run.py`. Full reference: `dev/loadtest/README.md`.
+1 -1
View File
@@ -1,7 +1,7 @@
name: Feature Request
description: Suggest a new feature or improvement
title: "[Feature] "
labels: ["enhancement", "needs-triage"]
labels: ["Feature", "needs-triage"]
body:
- type: textarea
id: problem
+2
View File
@@ -10,6 +10,7 @@ dhruv0811
Edwinhe03
fanzeyi
kerryspchang
kunyuchen
lisancao
mahesh-venkatachalam
mateiz
@@ -23,3 +24,4 @@ TomeHirata
xq-yin
hzub
zhengwin
ajayalfred
@@ -145,8 +145,6 @@ runs:
OMNIGENT_CLAUDE_SDK_NO_SANDBOX: ${{ inputs.harness == 'claude-sdk' && '1' || '' }}
PYTEST_PROGRESS_LOG_DIR: ${{ github.workspace }}/artifacts/progress-${{ inputs.harness }}
OMNIGENT_TOKEN_USAGE_JSON: ${{ github.workspace }}/artifacts/tokens-${{ inputs.harness }}.json
OMNIGENT_TEST_MODEL_SPREAD: "1"
OMNIGENT_TEST_MODEL_POOL_GPT: "databricks-gpt-5-5,databricks-gpt-5-4-mini"
run: |
set -euo pipefail
mkdir -p artifacts "$INTEGRATION_TMP_BASE"
+14 -5
View File
@@ -7,6 +7,9 @@ description: >-
after this action returns — the only secret here is the model key.
inputs:
model:
description: Provider-configured model id.
required: true
workdir:
description: >-
Repo checkout dir relative to the workspace (`.` when checked out at the
@@ -36,7 +39,7 @@ inputs:
claude-code-version:
description: "@anthropic-ai/claude-code npm version to install."
required: false
default: 2.1.170
default: 2.1.212
runs:
using: composite
@@ -64,18 +67,24 @@ runs:
CLAUDE_CODE_VERSION: ${{ inputs.claude-code-version }}
run: |
set -euo pipefail
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli"
cd "${GITHUB_WORKSPACE}/.cc-cli"
# Install outside the checked-out tree: a repo-root package.json would
# otherwise capture this bare `npm install` and hoist it there, leaving
# this dir's node_modules empty.
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR"
cd "$CC_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Write Omnigent provider config
shell: bash
env:
GATEWAY_BASE_URL: ${{ inputs.gateway-base-url }}
OMNIGENT_AGENT_MODEL: ${{ inputs.model }}
run: |
set -euo pipefail
: "${OMNIGENT_AGENT_MODEL:?Set the action model input}"
mkdir -p "$HOME/.omnigent"
python3 -c "
import pathlib, os, json
@@ -85,7 +94,7 @@ runs:
'anthropic': {
'base_url': gw + '/anthropic',
'api_key_ref': 'env:LLM_API_KEY',
'models': {'default': 'databricks-claude-opus-4-8'},
'models': {'default': os.environ['OMNIGENT_AGENT_MODEL']},
}}}}
pathlib.Path.home().joinpath('.omnigent', 'config.yaml').write_text(json.dumps(cfg, indent=2))
"
-37
View File
@@ -1,37 +0,0 @@
name: "setup-node"
description: "Set up Node and pin npm, with npm dependency caching keyed on the web lockfile."
# Single source of truth for the JS toolchain across CI. Pins npm to the
# EXACT version that regenerates the lockfile in oss-regenerate-and-smoke.yml
# (npm 11.12.1); without this, jobs use whatever npm Node 20 bundles
# (npm 10.x) and the `package-lock.json` freshness gate in lint.yml would
# flake on version-skew churn (dev/extraneous flags, metadata). Keep this
# version in lockstep with the regen workflow so generation and
# verification never diverge.
inputs:
node-version:
description: "Node version to use."
default: "20"
required: false
cache:
description: "Package-manager cache to enable (passed to actions/setup-node)."
default: "npm"
required: false
cache-dependency-path:
description: "Lockfile path used as the cache key."
default: "web/package-lock.json"
required: false
runs:
using: "composite"
steps:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.cache }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- name: Pin npm
shell: bash
run: npm install -g npm@11.12.1
+21
View File
@@ -0,0 +1,21 @@
name: "setup-pnpm"
description: "Set up Node + pnpm for the web workspace"
inputs:
node-version:
description: "Node version to use."
default: "22"
required: false
runs:
using: composite
steps:
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
standalone: true
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ inputs.node-version }}
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
@@ -83,6 +83,23 @@ prompt: |
Full Changelog: <copy the exact `Full Changelog:` line from the input, verbatim>
<!-- /RELEASE_POST -->
Then, AFTER the closing `<!-- /RELEASE_POST -->` marker, emit a machine-readable
map of which PRs back each feature section you wrote, so the workflow can build
a per-feature demo-video reference table for the reviewer. Emit it between its
own markers, as a JSON array in the SAME ORDER as your numbered sections — one
object per section, `title` matching the section's title text exactly (without
the `N. ` prefix), `pr_refs` the numbers from the `(#123, #456)` refs on the
release-body bullets you folded into that feature (integers, no `#`). Include
ONLY features you wrote up; omit bullets/PRs you dropped. This block is metadata,
NOT part of the post — never put PR numbers back into the RELEASE_POST prose.
<!-- RELEASE_POST_PRS -->
[
{"title": "<Feature 1 title>", "pr_refs": [123, 456]},
{"title": "<Feature 2 title>", "pr_refs": [789]}
]
<!-- /RELEASE_POST_PRS -->
## The MLflow 3.14.0 style (match this)
- CURATE, don't mirror. Pick only the ~4-6 OUTSTANDING, headline features and
give each its own numbered section. DROP minor features, small tweaks, and
@@ -113,7 +130,9 @@ prompt: |
placeholder immediately under EACH feature heading:
`![TODO: add a demo screenshot or GIF for "<feature title>"](TODO)`
Use the literal token `TODO` so a reviewer can grep for it. Never fabricate a
real-looking image path.
real-looking image path. (The workflow adds a table of the release's feature
PRs and their existing demo videos to the PR description, so a reviewer can drop
an already-recorded clip into these placeholders — you do not reference it.)
## Docs links (link to the most specific real page/section, or omit the line)
The "## Available docs pages and sections" input lists every real docs URL and
+34 -66
View File
@@ -97,13 +97,12 @@
"owners": [
"dhruv0811",
"TomeHirata",
"SabhyaC26",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -117,10 +116,10 @@
"dhruv0811",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -134,10 +133,10 @@
"dhruv0811",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -150,13 +149,12 @@
"owners": [
"dhruv0811",
"TomeHirata",
"SabhyaC26",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -167,7 +165,6 @@
"omnigent/onboarding/"
],
"owners": [
"SabhyaC26",
"dhruv0811",
"fanzeyi"
]
@@ -195,10 +192,7 @@
],
"owners": [
"TomeHirata",
"SabhyaC26",
"bbqiu"
],
"owners_paused": [
"bbqiu",
"dbczumar"
]
},
@@ -211,8 +205,7 @@
],
"owners": [
"dhruv0811",
"PattaraS",
"SabhyaC26"
"PattaraS"
]
},
{
@@ -225,9 +218,7 @@
"owners": [
"fanzeyi",
"dhruv0811",
"bbqiu"
],
"owners_paused": [
"bbqiu",
"dbczumar"
]
},
@@ -239,10 +230,7 @@
"omnigent/sandbox/"
],
"owners": [
"SabhyaC26",
"fanzeyi"
],
"owners_paused": [
"fanzeyi",
"dbczumar"
]
},
@@ -255,13 +243,12 @@
],
"owners": [
"bbqiu",
"aravind-segu",
"fanzeyi",
"dhruv0811",
"SabhyaC26"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -273,16 +260,15 @@
],
"owners": [
"bbqiu",
"aravind-segu",
"fanzeyi",
"dhruv0811",
"SabhyaC26",
"serena-ruan",
"daniellok-db",
"TomeHirata"
"TomeHirata",
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -295,12 +281,11 @@
"owners": [
"fanzeyi",
"dhruv0811",
"aravind-segu",
"bbqiu",
"SabhyaC26"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -313,13 +298,12 @@
"owners": [
"dhruv0811",
"TomeHirata",
"SabhyaC26",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -345,9 +329,7 @@
"dhruv0811",
"fanzeyi",
"serena-ruan",
"daniellok-db"
],
"owners_paused": [
"daniellok-db",
"dbczumar"
]
},
@@ -374,9 +356,6 @@
"owners": [
"dhruv0811",
"PattaraS",
"SabhyaC26"
],
"owners_paused": [
"dbczumar"
]
},
@@ -390,13 +369,12 @@
"owners": [
"dhruv0811",
"fanzeyi",
"SabhyaC26",
"TomeHirata",
"bbqiu",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -410,13 +388,12 @@
"owners": [
"dhruv0811",
"TomeHirata",
"SabhyaC26",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -432,13 +409,12 @@
"owners": [
"dhruv0811",
"TomeHirata",
"SabhyaC26",
"bbqiu",
"fanzeyi",
"aravind-segu"
"dbczumar"
],
"owners_paused": [
"dbczumar"
"aravind-segu"
]
},
{
@@ -450,10 +426,7 @@
"omnigent/cursor_native"
],
"owners": [
"SabhyaC26",
"dhruv0811"
],
"owners_paused": [
"dhruv0811",
"dbczumar"
]
},
@@ -468,8 +441,8 @@
"omnigent/onboarding/gemini_auth.py"
],
"owners": [
"SabhyaC26",
"TomeHirata"
"TomeHirata",
"PattaraS"
]
},
{
@@ -496,7 +469,6 @@
],
"owners": [
"dhruv0811",
"SabhyaC26",
"TomeHirata"
]
},
@@ -509,9 +481,11 @@
"omnigent/kimi_native"
],
"owners": [
"aravind-segu",
"dhruv0811",
"fanzeyi"
],
"owners_paused": [
"aravind-segu"
]
},
{
@@ -524,7 +498,6 @@
],
"owners": [
"PattaraS",
"SabhyaC26",
"TomeHirata",
"dhruv0811"
]
@@ -542,9 +515,6 @@
"dhruv0811",
"PattaraS",
"TomeHirata",
"SabhyaC26"
],
"owners_paused": [
"dbczumar"
]
},
@@ -557,7 +527,6 @@
"omnigent/pi_native"
],
"owners": [
"SabhyaC26",
"TomeHirata",
"dhruv0811"
]
@@ -585,7 +554,6 @@
"omnigent/onboarding/copilot_auth.py"
],
"owners": [
"SabhyaC26",
"PattaraS",
"TomeHirata",
"dhruv0811"
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"description": "Pinned npm CLIs the e2e workflow installs (claude-code, codex, pi).",
"dependencies": {
"@anthropic-ai/claude-code": "2.1.163",
"@anthropic-ai/claude-code": "2.1.212",
"@earendil-works/pi-coding-agent": "0.79.0",
"@openai/codex": "0.139.0"
}
+1 -1
View File
@@ -174,7 +174,7 @@ def render_section(tag: str, date: str, results: list[HarvestResult]) -> str:
# Multi-section draft for the GitHub Release body: the Type-of-change tags collapse
# into the sections the release coordinator curates by hand (see RELEASING.md /
# into the sections the release coordinator curates by hand (see the maintainer release runbook /
# the release-notes-drafter agent). This is the deterministic scaffold — the AI
# drafter refines it, and it is also the fallback when the LLM is unavailable.
# Values are "Type of change" checkbox labels (see _md.TYPE_TAGS).
@@ -3,7 +3,7 @@
# `e2e_matrix` and `integration_matrix`.
#
# We test `main` (the checked-out code = client + tests, always) against each
# non-rc release tag AT OR ABOVE the backcompat floor (MIN_VERSION, default
# final (non-prerelease) release tag AT OR ABOVE the backcompat floor (MIN_VERSION, default
# 0.2.0 — the first release with the mock-LLM e2e infra; see below), on BOTH
# axes — and ONLY those cells:
# (server=main, runner=<release>) — new server vs a previously-shipped runner
@@ -17,8 +17,9 @@
#
# Env in:
# VERSIONS optional comma-separated override of the version set used for
# BOTH axes (e.g. "main,v0.2.0"). Empty = main + all non-rc tags.
# Blank entries are dropped and surrounding whitespace trimmed.
# BOTH axes (e.g. "main,v0.2.0"). Empty = main + all final
# (non-prerelease) tags. Blank entries are dropped and
# surrounding whitespace trimmed.
# NUM_SHARDS e2e shard count per cell (default 4).
# Out (GITHUB_OUTPUT):
# e2e_matrix={"include":[{"server":..,"runner":..,"shard_id":..,"num_shards":..}, ...]}
@@ -61,9 +62,12 @@ if [ -n "${VERSIONS:-}" ]; then
IFS=',' read -ra raw <<<"$VERSIONS"
else
raw=("main")
# `[^a-z]rc[0-9]` so we drop vX.Y.ZrcN without over-excluding tags that merely
# contain the substring "rc" (e.g. a hypothetical "...march").
while IFS= read -r tag; do raw+=("$tag"); done < <(git tag --sort=-v:refname | grep -viE '(^|[^a-z])rc[0-9]')
# Drop pre-release tags (vX.Y.ZrcN / .devN / preN — same trio github-release.yml
# skips): they are snapshots of main, so main-vs-them is not a compat signal, and
# under the 256-job cap they would evict the oldest FINAL releases from coverage.
# `[^a-z]` guards against over-excluding tags that merely contain the substring
# (e.g. a hypothetical "...march"). An explicit VERSIONS override still accepts them.
while IFS= read -r tag; do raw+=("$tag"); done < <(git tag --sort=-v:refname | grep -viE '(^|[^a-z])(rc|dev|pre)[0-9]')
fi
# Trim whitespace, drop blanks, reject invalid tokens, drop below-floor releases.
@@ -105,7 +109,7 @@ done
# The integration suite runs a single openai-agents leg in mock mode (matches
# integration-matrix.sh); the model name is unused under the mock LLM.
integ_harness="openai-agents"
integ_model="databricks-gpt-5-4-mini"
integ_model="mock-model"
integ_workers="4"
e2e_items=()
+1 -1
View File
@@ -34,7 +34,7 @@ fi
read -r -d '' matrix <<'JSON' || true
{"include":[
{"name":"openai-agents","harness":"openai-agents","model":"databricks-gpt-5-4-mini","workers":4}
{"name":"openai-agents","harness":"openai-agents","model":"mock-model","workers":4}
]}
JSON
# Collapse to one line so the GITHUB_OUTPUT key=value contract holds.
+243 -13
View File
@@ -10,9 +10,12 @@ Resolution: `uv pip compile` computes the exact transitive closure of
`omnigent[<extras>]==<version>` for each target platform (macOS arm + intel by
default — the brew tap's `brew test-bot` matrix). The per-platform closures are
unioned; for each package we then fetch the sdist URL + sha256 from the PyPI JSON
API and emit a `resource` stanza. Packages with no sdist (e.g. `cel-expr-python`,
which is Bazel-built and has no PyPI sdist) are skipped — omnigent degrades
gracefully without them, matching the hand-tuned formula.
API and emit a `resource` stanza. Every package in the closure must publish an
sdist: the formula builds each resource from source, so one dropped for lack of
an sdist ships a venv missing that dependency, which surfaces as an ImportError
(or a silently disabled feature) at runtime rather than a red build. A missing
sdist is therefore a hard error; `--allow-no-sdist NAME` waives it for a package
omnigent genuinely works without.
Excluded from `resource` generation (provided by the brewed Python environment,
NOT built as virtualenv resources — keep in sync with the template's
@@ -69,6 +72,66 @@ BREWED_EXCLUSIONS = {
# omnigent is the stable `url` itself, so it's never a resource.
SELF_EXCLUSIONS = {"omnigent"}
# Packages pinned to an upstream platform wheel instead of the sdist, emitted as
# an arch-conditional `resource` (the template's install block pip-installs any
# `.whl` resource from its cached download).
#
# google-re2 (required by cel-python, which backs CEL policy evaluation) has an
# sdist that cannot be built here: its setup.py shells out to `bazel` whenever
# GITHUB_ACTIONS is set — always true under `brew test-bot` — and the non-bazel
# path needs re2 + abseil + pybind11 headers and C++17, which it never requests.
# The upstream macOS wheels statically link re2 and abseil, so they need no build
# toolchain and no brewed `abseil` (whose ABI breaks on most releases, which
# would force a formula `revision` bump every time it moved).
WHEEL_REQUIRED = {"google-re2"}
# Compiled extensions we PREFER to take as an upstream wheel, falling back to the
# sdist when no compatible wheel exists (e.g. right after a python@X.Y bump,
# before upstream publishes cpXY wheels). Building these is the bulk of the
# formula's cost -- grpcio alone dwarfs everything else on a 3-core bottle
# builder -- and every wheel here has enough Mach-O header padding for Homebrew
# to rewrite its install name during keg relocation.
#
# jiter, tiktoken and watchfiles are deliberately NOT here: their wheels are
# maturin-built with no install-name padding, so relocation dies with "Failed
# changing dylib ID" (omnigent issue #866). They are built from source with
# -headerpad_max_install_names instead, which is how every bottled release up to
# 0.6.0 shipped them. Verify with:
# install_name_tool -id <long Cellar path> <extracted .so>
PREFER_WHEEL = {
"argon2-cffi-bindings",
"grpcio",
"httptools",
"markupsafe",
"protobuf",
"pyyaml",
"regex",
"uvloop",
"zstandard",
}
# Packages pinned to the PURE-PYTHON (`py3-none-any`) wheel on purpose.
#
# pendulum is the awkward case: its maturin wheel cannot be relocated (see
# above), and its sdist does not link against python 3.14 -- pyo3 leaves
# _Py_NoneStruct/_Py_Dealloc/_Py_TrueStruct undefined and the arm64 link fails.
# Its pure-Python wheel ships no extension module at all, so there is nothing to
# relocate and nothing to build. Only cel-python pulls it in, for CEL timestamp
# arithmetic, so the slower implementation is not on any hot path.
PURE_WHEEL = {"pendulum"}
# uv target platform -> (Homebrew arch block, wheel platform-tag arch suffix).
_ARCH_BLOCKS = {
"aarch64-apple-darwin": ("on_arm", "arm64"),
"x86_64-apple-darwin": ("on_intel", "x86_64"),
}
# name-version[-build]-pytag-abitag-platformtag.whl (PEP 427).
_WHEEL_RE = re.compile(
r"^(?P<name>.+?)-(?P<version>[^-]+?)(?:-(?P<build>\d[^-]*))?"
r"-(?P<py>[^-]+)-(?P<abi>[^-]+)-(?P<plat>[^-]+)\.whl$"
)
_PLACEHOLDERS = (
"__OMNIGENT_URL__",
"__OMNIGENT_SHA256__",
@@ -124,6 +187,73 @@ def pick_sdist(files: list[dict]) -> tuple[str, str] | None:
return f["url"], f["digests"]["sha256"]
def _abi_compatible(py: str, abi: str, python_tag: str) -> bool:
"""Is a wheel's (pytag, abitag) usable by CPython `python_tag` (e.g. cp314)?
Accepts the exact CPython tag, a stable-ABI (`abi3`) wheel built for that
version or older, and pure-Python `py3-none`. Free-threaded builds (`cp314t`)
are excluded: the brewed python is not free-threaded, and equality on the abi
tag keeps them out.
"""
if abi == python_tag:
return True
if abi == "abi3" and py.startswith("cp") and py[2:].isdigit():
return int(py[2:]) <= int(python_tag[2:])
return py == "py3" and abi == "none"
def _wheel_arches(plat: str) -> tuple[frozenset[str], tuple[int, int]] | None:
"""Arches a macOS wheel platform tag covers, plus its deployment target."""
if plat == "any":
return frozenset({"arm64", "x86_64"}), (0, 0)
m = re.match(r"macosx_(\d+)_(\d+)_(arm64|x86_64|universal2|intel)$", plat)
if not m:
return None
arches = {
"arm64": {"arm64"},
"x86_64": {"x86_64"},
"intel": {"x86_64"},
"universal2": {"arm64", "x86_64"},
}[m.group(3)]
return frozenset(arches), (int(m.group(1)), int(m.group(2)))
def pick_macos_wheels(
files: list[dict], python_tag: str, arches: list[str]
) -> dict[str, tuple[str, str]] | None:
"""Best macOS wheel per arch: {arch: (url, sha256)}, or None if any is missing.
Ranked by (native before pure-Python, then lowest deployment target). A wheel
built for an older `macosx_<major>_<minor>` minimum installs on every newer
macOS the tap builds for while the reverse is not true. Pure-Python
`py3-none-any` wheels sort last on purpose: when a package ships both (e.g.
protobuf, pendulum) the `any` wheel is the slow fallback implementation, and
it would otherwise always win by having no deployment target at all.
A `universal2` (or `any`) wheel satisfies both arches with one file, which the
caller renders as a single unconditional url.
"""
best: dict[str, tuple[tuple[int, int, int], str, str]] = {}
for f in files:
if f.get("packagetype") != "bdist_wheel":
continue
m = _WHEEL_RE.match(f["filename"])
if not m or not _abi_compatible(m.group("py"), m.group("abi"), python_tag):
continue
covered = _wheel_arches(m.group("plat"))
if not covered:
continue
covered_arches, target = covered
pure = 1 if m.group("abi") == "none" else 0
rank = (pure, *target)
for arch in arches:
if arch in covered_arches and (arch not in best or rank < best[arch][0]):
best[arch] = (rank, f["url"], f["digests"]["sha256"])
if any(arch not in best for arch in arches):
return None
return {arch: (url, sha) for arch, (_, url, sha) in best.items()}
def rewrite_url(url: str, rewrites: list[tuple[str, str]]) -> str:
"""Apply `from -> to` substitutions to a download URL, in order.
@@ -145,6 +275,25 @@ def resource_stanza(name: str, url: str, sha256: str, indent: int = 2) -> str:
return f'{pad}resource "{name}" do\n{pad} url "{url}"\n{pad} sha256 "{sha256}"\n{pad}end'
def wheel_resource_stanza(name: str, per_arch: list[tuple[str, str, str]], indent: int = 2) -> str:
"""An arch-conditional `resource` stanza: one `on_arm`/`on_intel` block each.
`per_arch` is [(brew_block, url, sha256), ...]. `Resource` includes
`OnSystem::MacOSAndLinux`, so these blocks are valid inside a resource.
"""
pad = " " * indent
lines = [f'{pad}resource "{name}" do']
for block, url, sha256 in per_arch:
lines += [
f"{pad} {block} do",
f'{pad} url "{url}"',
f'{pad} sha256 "{sha256}"',
f"{pad} end",
]
lines.append(f"{pad}end")
return "\n".join(lines)
def resolve_closure(
version: str,
platforms: list[str],
@@ -253,6 +402,7 @@ def generate(
index_url: str,
uv: str,
exclude: set[str],
allow_no_sdist: set[str] | None = None,
api_base: str = PYPI_JSON_API,
url_rewrites: list[tuple[str, str]] | None = None,
) -> str:
@@ -290,24 +440,96 @@ def generate(
# a sdist resource stanza. `exclude` is the caller-supplied set (CLI --exclude);
# it augments the built-in brewed set and the always-excluded self package.
excluded = BREWED_EXCLUSIONS | exclude | SELF_EXCLUSIONS
resources: list[tuple[str, str, str]] = []
waived = allow_no_sdist or set()
python_tag = "cp" + python_version.replace(".", "")
resources: list[tuple[str, str]] = []
missing_sdist: list[str] = []
for name, ver in sorted(closure.items()):
if name in excluded:
continue
files = pypi_release_files(name, ver, api_base)
sdist = pick_sdist(files)
if not sdist:
# No sdist (e.g. cel-expr-python, Bazel-built) — skip. omnigent
# degrades gracefully without it, matching the hand-tuned formula.
print(
f"::warning::{name}=={ver} has no sdist on PyPI — skipping (no resource).",
file=sys.stderr,
# Wheel-pinned packages. One `universal2`/`abi3` wheel usually covers both
# arches, so emit a plain url and only fall back to on_arm/on_intel blocks
# when upstream ships separate per-arch wheels.
# Deliberate pure-Python wheel: no extension module, nothing to relocate.
if name in PURE_WHEEL:
pure = next((f for f in files if f["filename"].endswith("-py3-none-any.whl")), None)
if not pure:
raise RuntimeError(
f"{name}=={ver} publishes no py3-none-any wheel, but it is in "
f"PURE_WHEEL because neither its platform wheel nor its sdist "
f"is usable here. Re-check the comment on PURE_WHEEL."
)
resources.append(
(
name,
resource_stanza(
name, rewrite_url(pure["url"], rewrites), pure["digests"]["sha256"]
),
)
)
continue
resources.append((name, rewrite_url(sdist[0], rewrites), sdist[1]))
if name in WHEEL_REQUIRED or name in PREFER_WHEEL:
wheels = pick_macos_wheels(files, python_tag, [_ARCH_BLOCKS[p][1] for p in platforms])
if wheels is None:
if name in WHEEL_REQUIRED:
raise RuntimeError(
f"{name}=={ver} has no macOS wheel for {python_tag} on every "
f"target arch. It is in WHEEL_REQUIRED because its sdist is "
f"unbuildable here, so upstream must publish one or the "
f"dependency has to go."
)
# PREFER_WHEEL is best-effort: fall through and build the sdist.
print(
f"::warning::{name}=={ver} has no macOS wheel for {python_tag} on "
f"every target arch — falling back to a source build (slow).",
file=sys.stderr,
)
elif len({url for url, _ in wheels.values()}) == 1:
url, sha = next(iter(wheels.values()))
resources.append((name, resource_stanza(name, rewrite_url(url, rewrites), sha)))
continue
else:
per_arch = [
(
_ARCH_BLOCKS[p][0],
rewrite_url(wheels[_ARCH_BLOCKS[p][1]][0], rewrites),
wheels[_ARCH_BLOCKS[p][1]][1],
)
for p in platforms
]
resources.append((name, wheel_resource_stanza(name, per_arch)))
continue
sdist = pick_sdist(files)
if not sdist:
# Wheel-only dependency: Homebrew can't build it as a resource.
# Dropping it silently yields a formula that installs green and is
# missing an import, so fail unless the caller waived it.
if name in waived:
print(
f"::warning::{name}=={ver} has no sdist on PyPI — waived, no resource.",
file=sys.stderr,
)
continue
missing_sdist.append(f"{name}=={ver}")
continue
resources.append((name, resource_stanza(name, rewrite_url(sdist[0], rewrites), sdist[1])))
if missing_sdist:
raise RuntimeError(
"no sdist on PyPI for: "
+ ", ".join(missing_sdist)
+ "\nHomebrew builds every resource from source, so these would be "
"absent from the installed venv. Drop the dependency, move it to an "
"extra that isn't bundled (see DEFAULT_EXTRAS), or pass "
"--allow-no-sdist <name> if omnigent works without it."
)
# No trailing newline: the template's blank lines frame the resource block.
resources_str = "\n".join(resource_stanza(n, u, s) for n, u, s in resources)
resources_str = "\n".join(stanza for _, stanza in resources)
return render_template(template, stable_url, stable_sha, resources_str)
@@ -385,6 +607,13 @@ def main(argv: list[str]) -> int:
help="Package name to exclude from resources (repeatable; "
"added to the built-in brewed set).",
)
ap.add_argument(
"--allow-no-sdist",
action="append",
default=None,
help="Package allowed to have no PyPI sdist (repeatable). Without this, a "
"wheel-only dependency fails the run instead of vanishing from the formula.",
)
ap.add_argument("--uv", default="uv", help="uv binary path.")
args = ap.parse_args(argv)
@@ -408,6 +637,7 @@ def main(argv: list[str]) -> int:
index_url=index_url,
uv=args.uv,
exclude={normalize_name(n) for n in (args.exclude or [])},
allow_no_sdist={normalize_name(n) for n in (args.allow_no_sdist or [])},
api_base=api_base,
url_rewrites=url_rewrites,
)
+28 -12
View File
@@ -5,7 +5,8 @@
# spliced into this file via three placeholders that live ONLY in the class body
# below — keep them out of this comment or the splicer will mangle it:
# * the stable `url` / `sha256` lines -> the released omnigent sdist on PyPI
# * the per-dependency `resource` stanzas (one per PyPI sdist in the closure)
# * the per-dependency `resource` stanzas (one per package in the closure: the
# PyPI sdist, or a pinned wheel for WHEEL_REQUIRED / PREFER_WHEEL)
#
# Edit the hand-tuned STRUCTURAL parts here (desc, depends_on, install, test).
# Edit the dependency set in omnigent-ai/omnigent's `pyproject.toml`
@@ -27,7 +28,10 @@ class Omnigent < Formula
sha256 "__OMNIGENT_SHA256__"
license "Apache-2.0"
# The Rust toolchain builds jiter and watchfiles from source.
# Most compiled extensions come from upstream wheels (see PREFER_WHEEL in
# generate_formula.py). jiter, tiktoken and watchfiles still build here, because
# their maturin wheels have no Mach-O install-name padding and Homebrew cannot
# relocate them -- hence the Rust toolchain and the RUSTFLAGS below.
depends_on "pkgconf" => :build
depends_on "rust" => :build
# certifi, cryptography, pydantic (which bundles pydantic-core), and rpds-py
@@ -49,18 +53,25 @@ __RESOURCES__
def install
venv = virtualenv_create(libexec, "python3.14")
# The Rust extensions (jiter, watchfiles) must leave Mach-O header padding so
# Homebrew can rewrite their install names to the Cellar path during
# relocation (macOS only; the flag breaks Linux ld).
# jiter, tiktoken and watchfiles are the only Rust builds left. Their
# extensions must leave Mach-O header padding so Homebrew can rewrite install
# names to the Cellar path during relocation (macOS only; the flag breaks
# Linux ld). Everything else compiled is a prebuilt wheel.
ENV.append_to_rustflags "-C link-args=-Wl,-headerpad_max_install_names" if OS.mac?
# argon2-cffi-bindings' sdist ships an unprocessed .git_archival.txt that the
# (build-isolated, latest) setuptools-scm parses instead of falling back to
# PKG-INFO, so version detection fails. Pin the version it should report.
ENV["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_ARGON2_CFFI_BINDINGS"] =
resource("argon2-cffi-bindings").version.to_s
venv.pip_install resources
# Pure-Python resources are sdists Homebrew builds in place. Every other
# compiled extension is pinned to an upstream wheel (WHEEL_REQUIRED /
# PREFER_WHEEL in generate_formula.py), which is what keeps this formula out of
# cc/rustc on a 3-core bottle builder. Homebrew only auto-installs
# `py3-none-any` wheels, so copy each platform wheel's cached download back to
# its real filename and pip-install the file directly.
wheels, sdists = resources.partition { |r| r.url.end_with?(".whl") }
venv.pip_install sdists
wheels.each do |r|
whl = buildpath/r.url.split("/").last
cp r.cached_download, whl
venv.pip_install whl
end
venv.pip_install_and_link buildpath
@@ -79,5 +90,10 @@ __RESOURCES__
# provided by Homebrew formulae and imported from the brewed python through
# the virtualenv's system site-packages; confirm they resolve in the venv.
system libexec/"bin/python", "-c", "import certifi, cryptography, pydantic, rpds"
# celpy imports re2 at module scope and omnigent imports celpy behind a
# try/except, so a google-re2 that failed to build disables inline policies
# silently instead of failing. Import both so the gap is caught at build time.
system libexec/"bin/python", "-c", "import re2, celpy"
end
end
+3
View File
@@ -33,6 +33,7 @@ REQUIRED=(
"E2E UI Tests (shard 0/3)"
"E2E UI Tests (shard 1/3)"
"E2E UI Tests (shard 2/3)"
"UI Snapshot (visual baselines)"
"Integration (claude-sdk)"
"Integration (openai-agents)"
"Integration (codex)"
@@ -63,6 +64,7 @@ ALLOW_SKIP=(
"E2E UI Tests (shard 0/3)"
"E2E UI Tests (shard 1/3)"
"E2E UI Tests (shard 2/3)"
"UI Snapshot (visual baselines)"
"Integration (claude-sdk)"
"Integration (openai-agents)"
"Integration (codex)"
@@ -80,6 +82,7 @@ workflow_for() {
"Pytest ("*) echo "CI" ;;
"E2E Tests (shard "*) echo "E2E Tests" ;;
"E2E UI Tests (shard "*) echo "E2E UI Tests" ;;
"UI Snapshot (visual baselines)") echo "UI Snapshot" ;;
"Integration ("*) echo "Integration Tests" ;;
*) echo "" ;;
esac
-2
View File
@@ -21,9 +21,7 @@
"Dhruv Gupta": { "slack_id": "U0A76097E1F", "tz": "America/Los_Angeles" },
"Edwin He": { "slack_id": "U077B1V6WQJ", "tz": "America/Los_Angeles" },
"Pat Sukprasert": { "slack_id": "U05HRKWFY81", "tz": "Asia/Singapore" },
"Sabhya Chhabria": { "slack_id": "U07A1KQDXAB", "tz": "America/Los_Angeles" },
"Serena Ruan": { "slack_id": "U0571L5KNLR", "tz": "Asia/Singapore" },
"Shivam Mittal": { "slack_id": "U09FZKX9S6B", "tz": "America/Los_Angeles" },
"Tomu Hirata": { "slack_id": "U07TX4PR5MZ", "tz": "Asia/Singapore" },
"Zeyi (Rice) Fan": { "slack_id": "U09L5HT4CH0", "tz": "America/Los_Angeles" }
}
+50 -66
View File
@@ -12,69 +12,13 @@
"name -> slack_id + timezone mapping)."
],
"schedule": [
{
"date": "2026-07-14",
"name": "Edwin He"
},
{
"date": "2026-07-15",
"name": "Pat Sukprasert"
},
{
"date": "2026-07-16",
"name": "Sabhya Chhabria"
},
{
"date": "2026-07-17",
"name": "Serena Ruan"
},
{
"date": "2026-07-20",
"name": "Shivam Mittal"
},
{
"date": "2026-07-21",
"name": "Tomu Hirata"
},
{
"date": "2026-07-22",
"name": "Zeyi (Rice) Fan"
},
{
"date": "2026-07-23",
"name": "Aravind Segu"
},
{
"date": "2026-07-24",
"name": "Bryan Qiu"
},
{
"date": "2026-07-27",
"name": "Daniel Lok"
},
{
"date": "2026-07-28",
"name": "Dhruv Gupta"
},
{
"date": "2026-07-29",
"name": "Edwin He"
},
{
"date": "2026-07-30",
"name": "Pat Sukprasert"
},
{
"date": "2026-07-31",
"name": "Sabhya Chhabria"
},
{
"date": "2026-08-03",
"name": "Serena Ruan"
},
{
"date": "2026-08-04",
"name": "Shivam Mittal"
"name": "Aravind Segu"
},
{
"date": "2026-08-05",
@@ -110,7 +54,7 @@
},
{
"date": "2026-08-17",
"name": "Sabhya Chhabria"
"name": "Bryan Qiu"
},
{
"date": "2026-08-18",
@@ -118,7 +62,7 @@
},
{
"date": "2026-08-19",
"name": "Shivam Mittal"
"name": "Daniel Lok"
},
{
"date": "2026-08-20",
@@ -154,7 +98,7 @@
},
{
"date": "2026-09-01",
"name": "Sabhya Chhabria"
"name": "Dhruv Gupta"
},
{
"date": "2026-09-02",
@@ -162,7 +106,7 @@
},
{
"date": "2026-09-03",
"name": "Shivam Mittal"
"name": "Edwin He"
},
{
"date": "2026-09-04",
@@ -198,7 +142,7 @@
},
{
"date": "2026-09-16",
"name": "Sabhya Chhabria"
"name": "Tomu Hirata"
},
{
"date": "2026-09-17",
@@ -206,7 +150,7 @@
},
{
"date": "2026-09-18",
"name": "Shivam Mittal"
"name": "Zeyi (Rice) Fan"
},
{
"date": "2026-09-21",
@@ -242,7 +186,7 @@
},
{
"date": "2026-10-01",
"name": "Sabhya Chhabria"
"name": "Aravind Segu"
},
{
"date": "2026-10-02",
@@ -250,7 +194,7 @@
},
{
"date": "2026-10-05",
"name": "Shivam Mittal"
"name": "Bryan Qiu"
},
{
"date": "2026-10-06",
@@ -286,7 +230,47 @@
},
{
"date": "2026-10-16",
"name": "Sabhya Chhabria"
"name": "Daniel Lok"
},
{
"date": "2026-10-19",
"name": "Dhruv Gupta"
},
{
"date": "2026-10-20",
"name": "Edwin He"
},
{
"date": "2026-10-21",
"name": "Pat Sukprasert"
},
{
"date": "2026-10-22",
"name": "Serena Ruan"
},
{
"date": "2026-10-23",
"name": "Tomu Hirata"
},
{
"date": "2026-10-26",
"name": "Zeyi (Rice) Fan"
},
{
"date": "2026-10-27",
"name": "Aravind Segu"
},
{
"date": "2026-10-28",
"name": "Bryan Qiu"
},
{
"date": "2026-10-29",
"name": "Daniel Lok"
},
{
"date": "2026-10-30",
"name": "Dhruv Gupta"
}
]
}
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Mirror a linked issue's priority label onto the pull request that closes it.
A PR only inherits a priority when it *closes* an issue via a closing keyword
(``closes``/``fixes``/``resolves`` #n); a plain "related to #n" mention never
creates a closing link, so it is ignored. When a PR closes several issues with
different priorities the highest one wins, and stale priority labels left by an
earlier run are dropped. Pure stdlib so it runs without an install and the
label logic is unit-tested directly.
"""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
CANONICAL_REPO = "omnigent-ai/omnigent"
# Priority labels from most to least urgent; the earliest match wins.
PRIORITY_ORDER = ("P0-critical", "P1-high", "P2-medium", "P3-low")
PRIORITY_LABELS = frozenset(PRIORITY_ORDER)
_CLOSING_ISSUES_QUERY = """
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
closingIssuesReferences(first: 50) {
nodes {
number
labels(first: 50) { nodes { name } }
}
}
}
}
}
"""
def desired_priority(closing_issue_labels: list[list[str]]) -> str | None:
"""Highest-priority label across the issues a PR closes, or None."""
present = {label for labels in closing_issue_labels for label in labels}
for priority in PRIORITY_ORDER:
if priority in present:
return priority
return None
def label_changes(current: list[str], desired: str | None) -> tuple[str | None, list[str]]:
"""Return the priority to add (if missing) and stale priorities to remove."""
current_priorities = [label for label in current if label in PRIORITY_LABELS]
to_remove = [label for label in current_priorities if label != desired]
to_add = desired if desired is not None and desired not in current_priorities else None
return to_add, to_remove
class GitHubAPI:
def __init__(self, token: str, repo: str) -> None:
self.token = token
self.repo = repo
self.owner, _, self.name = repo.partition("/")
def _request(self, url: str, body: dict[str, Any] | None, method: str) -> Any:
data = None if body is None else json.dumps(body).encode()
request = urllib.request.Request(
url,
data=data,
method=method,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
"X-GitHub-Api-Version": "2022-11-28",
},
)
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read()
return json.loads(raw.decode()) if raw else None
def closing_issue_labels(self, pull_number: int) -> list[list[str]]:
payload = {
"query": _CLOSING_ISSUES_QUERY,
"variables": {"owner": self.owner, "name": self.name, "number": pull_number},
}
result = self._request("https://api.github.com/graphql", payload, "POST")
if result and result.get("errors"):
raise RuntimeError(f"GraphQL error: {result['errors']}")
# GitHub may return null for data or any intermediate node (e.g. an
# unknown PR number), so treat each missing level as empty.
data = (result or {}).get("data") or {}
repository = data.get("repository") or {}
pull_request = repository.get("pullRequest") or {}
nodes = (pull_request.get("closingIssuesReferences") or {}).get("nodes") or []
return [
[label["name"] for label in (node.get("labels") or {}).get("nodes") or []]
for node in nodes
]
def pull_labels(self, pull_number: int) -> list[str]:
result = self._request(
f"https://api.github.com/repos/{self.repo}/issues/{pull_number}/labels",
None,
"GET",
)
return [label["name"] for label in result or []]
def add_label(self, pull_number: int, label: str) -> None:
self._request(
f"https://api.github.com/repos/{self.repo}/issues/{pull_number}/labels",
{"labels": [label]},
"POST",
)
def remove_label(self, pull_number: int, label: str) -> None:
quoted = urllib.parse.quote(label, safe="")
try:
self._request(
f"https://api.github.com/repos/{self.repo}/issues/{pull_number}/labels/{quoted}",
None,
"DELETE",
)
except urllib.error.HTTPError as error:
if error.code != 404:
raise
def sync_pull(api: GitHubAPI, pull_number: int) -> None:
desired = desired_priority(api.closing_issue_labels(pull_number))
to_add, to_remove = label_changes(api.pull_labels(pull_number), desired)
for label in to_remove:
api.remove_label(pull_number, label)
print(f"Removed stale priority {label} from #{pull_number}.")
if to_add:
api.add_label(pull_number, to_add)
print(f"Applied {to_add} to #{pull_number} from its closing-linked issue(s).")
if not to_add and not to_remove:
print(f"#{pull_number} priority already in sync ({desired or 'none'}).")
def run(repo: str, pull_number: int, api: GitHubAPI) -> None:
if repo != CANONICAL_REPO:
print(f"Skipping {repo}; priority sync only runs for {CANONICAL_REPO}.")
return
sync_pull(api, pull_number)
def main() -> int:
repo = os.environ.get("GITHUB_REPOSITORY", "")
token = os.environ.get("GITHUB_TOKEN")
pull_number = os.environ.get("PR_NUMBER")
if not token:
print("GITHUB_TOKEN is required", file=sys.stderr)
return 1
if not pull_number:
print("PR_NUMBER is required", file=sys.stderr)
return 1
try:
pull_number_int = int(pull_number)
except ValueError:
print(f"PR_NUMBER must be an integer, got {pull_number!r}", file=sys.stderr)
return 1
run(repo, pull_number_int, GitHubAPI(token, repo))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Offline tests for sync_pr_priority.py."""
from __future__ import annotations
import importlib.util
import pathlib
import unittest
SCRIPT_PATH = pathlib.Path(__file__).with_name("sync_pr_priority.py")
SPEC = importlib.util.spec_from_file_location("sync_pr_priority", SCRIPT_PATH)
sync_pr_priority = importlib.util.module_from_spec(SPEC)
assert SPEC and SPEC.loader
SPEC.loader.exec_module(sync_pr_priority)
class FakeAPI:
def __init__(self, *, closing: list[list[str]], current: list[str]) -> None:
self._closing = closing
self._current = current
self.added: list[tuple[int, str]] = []
self.removed: list[tuple[int, str]] = []
def closing_issue_labels(self, pull_number: int) -> list[list[str]]:
assert pull_number
return self._closing
def pull_labels(self, pull_number: int) -> list[str]:
assert pull_number
return self._current
def add_label(self, pull_number: int, label: str) -> None:
self.added.append((pull_number, label))
def remove_label(self, pull_number: int, label: str) -> None:
self.removed.append((pull_number, label))
class DesiredPriorityTest(unittest.TestCase):
def test_no_closing_issue_yields_none(self) -> None:
self.assertIsNone(sync_pr_priority.desired_priority([]))
def test_closing_issue_without_priority_yields_none(self) -> None:
self.assertIsNone(sync_pr_priority.desired_priority([["Bug", "comp:server"]]))
def test_single_priority_is_returned(self) -> None:
self.assertEqual(sync_pr_priority.desired_priority([["P2-medium"]]), "P2-medium")
def test_highest_priority_wins_across_issues(self) -> None:
self.assertEqual(
sync_pr_priority.desired_priority([["P3-low"], ["P1-high"], ["P2-medium"]]),
"P1-high",
)
def test_highest_priority_wins_within_one_issue(self) -> None:
self.assertEqual(
sync_pr_priority.desired_priority([["P0-critical", "P3-low"]]),
"P0-critical",
)
class LabelChangesTest(unittest.TestCase):
def test_adds_missing_priority(self) -> None:
self.assertEqual(sync_pr_priority.label_changes(["Bug"], "P1-high"), ("P1-high", []))
def test_noop_when_already_correct(self) -> None:
self.assertEqual(sync_pr_priority.label_changes(["P1-high", "Bug"], "P1-high"), (None, []))
def test_replaces_stale_priority(self) -> None:
self.assertEqual(
sync_pr_priority.label_changes(["P3-low"], "P1-high"), ("P1-high", ["P3-low"])
)
def test_removes_priority_when_no_longer_desired(self) -> None:
self.assertEqual(
sync_pr_priority.label_changes(["P2-medium"], None), (None, ["P2-medium"])
)
def test_leaves_non_priority_labels_untouched(self) -> None:
self.assertEqual(sync_pr_priority.label_changes(["Bug", "python"], None), (None, []))
class SyncPullTest(unittest.TestCase):
def test_applies_priority_from_closing_issue(self) -> None:
api = FakeAPI(closing=[["P1-high"]], current=["Bug"])
sync_pr_priority.sync_pull(api, 7)
self.assertEqual(api.added, [(7, "P1-high")])
self.assertEqual(api.removed, [])
def test_swaps_stale_priority(self) -> None:
api = FakeAPI(closing=[["P0-critical"]], current=["P2-medium"])
sync_pr_priority.sync_pull(api, 7)
self.assertEqual(api.added, [(7, "P0-critical")])
self.assertEqual(api.removed, [(7, "P2-medium")])
def test_related_only_pr_gets_nothing(self) -> None:
# No closing references -> no priority, and nothing to strip.
api = FakeAPI(closing=[], current=["Bug"])
sync_pr_priority.sync_pull(api, 7)
self.assertEqual(api.added, [])
self.assertEqual(api.removed, [])
class ClosingIssueLabelsParseTest(unittest.TestCase):
"""GraphQL response parsing tolerates null nodes and surfaces errors."""
def _api_returning(self, response: object) -> sync_pr_priority.GitHubAPI:
api = sync_pr_priority.GitHubAPI("token", "owner/name")
def stub_request(*_args: object, **_kwargs: object) -> object:
return response
api._request = stub_request # type: ignore[method-assign]
return api
def test_parses_labels(self) -> None:
response = {
"data": {
"repository": {
"pullRequest": {
"closingIssuesReferences": {
"nodes": [{"labels": {"nodes": [{"name": "P1-high"}]}}]
}
}
}
}
}
self.assertEqual(self._api_returning(response).closing_issue_labels(1), [["P1-high"]])
def test_null_data_yields_empty(self) -> None:
self.assertEqual(self._api_returning({"data": None}).closing_issue_labels(1), [])
def test_null_pull_request_yields_empty(self) -> None:
response = {"data": {"repository": {"pullRequest": None}}}
self.assertEqual(self._api_returning(response).closing_issue_labels(1), [])
def test_errors_raise(self) -> None:
response = {"data": None, "errors": [{"message": "boom"}]}
with self.assertRaises(RuntimeError):
self._api_returning(response).closing_issue_labels(1)
if __name__ == "__main__":
unittest.main()
+318
View File
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
"""Keep the waiting-on-author pull request label actionable."""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import UTC, datetime
from email.message import Message
from typing import Any
LABEL = "waiting-on-author"
WAITING_DAYS = 7
CANONICAL_REPO = "omnigent-ai/omnigent"
MAX_CLOSURES_PER_RUN = 30
def label_names(item: dict[str, Any]) -> list[str]:
return [
label.get("name", label) if isinstance(label, dict) else label
for label in item.get("labels", [])
]
def has_waiting_label(item: dict[str, Any]) -> bool:
return LABEL in label_names(item)
def parse_time(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def days_between(start: str, end: datetime) -> int:
return int((end - parse_time(start)).total_seconds() // 86400)
def latest_waiting_label_at(timeline: list[dict[str, Any]]) -> str | None:
latest: str | None = None
for event in timeline:
if event.get("event") != "labeled" or not event.get("created_at"):
continue
label = event.get("label") or {}
name = label.get("name") if isinstance(label, dict) else label
if name != LABEL:
continue
if latest is None or parse_time(event["created_at"]) > parse_time(latest):
latest = event["created_at"]
return latest
def close_message(label_applied_at: str) -> str:
return "\n".join(
[
f"Closing this PR because it has been labeled `{LABEL}` for "
f"{WAITING_DAYS} days without an author reply or new commit.",
"",
f"The label was last applied on {label_applied_at}. If you are "
"ready to continue, please reopen this PR or open a new one.",
]
)
class GitHubAPI:
def __init__(self, token: str, repo: str):
self.token = token
self.repo = repo
def request(
self, method: str, path: str, body: dict[str, Any] | None = None
) -> tuple[Any, Message]:
data = None if body is None else json.dumps(body).encode()
request = urllib.request.Request(
f"https://api.github.com{path}",
data=data,
method=method,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
"X-GitHub-Api-Version": "2022-11-28",
},
)
with urllib.request.urlopen(request) as response:
raw = response.read()
parsed = json.loads(raw.decode()) if raw else None
return parsed, response.headers
def paginated(self, path: str) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
next_path: str | None = path
while next_path:
page, headers = self.request("GET", next_path)
items.extend(page or [])
next_path = next_link(headers.get("Link", ""))
return items
def get_pull(self, pull_number: int) -> dict[str, Any]:
pull, _ = self.request("GET", f"/repos/{self.repo}/pulls/{pull_number}")
return pull
def remove_label(self, issue_number: int, label: str) -> bool:
quoted = urllib.parse.quote(label, safe="")
try:
self.request("DELETE", f"/repos/{self.repo}/issues/{issue_number}/labels/{quoted}")
except urllib.error.HTTPError as error:
if error.code == 404:
return False
raise
return True
def list_waiting_issues(self) -> list[dict[str, Any]]:
query = urllib.parse.urlencode({"state": "open", "labels": LABEL, "per_page": 100})
return self.paginated(f"/repos/{self.repo}/issues?{query}")
def list_timeline(self, issue_number: int) -> list[dict[str, Any]]:
return self.paginated(f"/repos/{self.repo}/issues/{issue_number}/timeline?per_page=100")
def list_issue_comments(self, issue_number: int) -> list[dict[str, Any]]:
return self.paginated(f"/repos/{self.repo}/issues/{issue_number}/comments?per_page=100")
def list_review_comments(self, pull_number: int) -> list[dict[str, Any]]:
return self.paginated(f"/repos/{self.repo}/pulls/{pull_number}/comments?per_page=100")
def list_reviews(self, pull_number: int) -> list[dict[str, Any]]:
return self.paginated(f"/repos/{self.repo}/pulls/{pull_number}/reviews?per_page=100")
def list_commits(self, pull_number: int) -> list[dict[str, Any]]:
return self.paginated(f"/repos/{self.repo}/pulls/{pull_number}/commits?per_page=100")
def close_pull(self, pull_number: int) -> None:
self.request("PATCH", f"/repos/{self.repo}/pulls/{pull_number}", {"state": "closed"})
def create_comment(self, issue_number: int, body: str) -> None:
self.request("POST", f"/repos/{self.repo}/issues/{issue_number}/comments", {"body": body})
def next_link(link_header: str) -> str | None:
for part in link_header.split(","):
url_part, _, rel_part = part.partition(";")
if 'rel="next"' not in rel_part:
continue
url = url_part.strip()[1:-1]
parsed = urllib.parse.urlparse(url)
return f"{parsed.path}?{parsed.query}"
return None
def remove_waiting_label(api: GitHubAPI, issue_number: int, reason: str) -> bool:
removed = api.remove_label(issue_number, LABEL)
if removed:
print(f"Removed {LABEL} from #{issue_number}: {reason}")
else:
print(f"#{issue_number} no longer has {LABEL}; nothing to remove.")
return removed
def user_login(item: dict[str, Any]) -> str | None:
login = item.get("user", {}).get("login")
return login.lower() if login else None
def is_after(timestamp: str | None, since: str) -> bool:
return bool(timestamp and parse_time(timestamp) > parse_time(since))
def authored_after(items: list[dict[str, Any]], author: str, since: str, key: str) -> bool:
return any(user_login(item) == author and is_after(item.get(key), since) for item in items)
def commit_after(commits: list[dict[str, Any]], since: str) -> bool:
for commit in commits:
authored_at = commit.get("commit", {}).get("author", {}).get("date")
committed_at = commit.get("commit", {}).get("committer", {}).get("date")
if is_after(authored_at, since) or is_after(committed_at, since):
return True
return False
def author_activity_since_label(api: GitHubAPI, pull: dict[str, Any], since: str) -> str | None:
author = pull.get("user", {}).get("login")
if not author:
return None
author = author.lower()
pull_number = pull["number"]
if authored_after(api.list_issue_comments(pull_number), author, since, "created_at"):
return "the author commented"
if authored_after(api.list_review_comments(pull_number), author, since, "created_at"):
return "the author replied to a review comment"
if authored_after(api.list_reviews(pull_number), author, since, "submitted_at"):
return "the author submitted a review response"
if commit_after(api.list_commits(pull_number), since):
return "new commits were pushed"
return None
def clear_on_author_activity(event_name: str, payload: dict[str, Any], api: GitHubAPI) -> bool:
pull_number: int | None = None
actor: str | None = None
reason: str | None = None
author_activity = False
if event_name in {"pull_request", "pull_request_target"} and payload.get("pull_request"):
if payload.get("action") != "synchronize":
return False
pull_number = payload["pull_request"]["number"]
reason = "new commits were pushed"
author_activity = True
elif event_name == "issue_comment" and "pull_request" in payload.get("issue", {}):
pull_number = payload["issue"]["number"]
actor = payload.get("comment", {}).get("user", {}).get("login")
reason = "the author commented"
elif event_name == "pull_request_review_comment" and payload.get("pull_request"):
pull_number = payload["pull_request"]["number"]
actor = payload.get("comment", {}).get("user", {}).get("login")
reason = "the author replied to a review comment"
elif event_name == "pull_request_review" and payload.get("pull_request"):
pull_number = payload["pull_request"]["number"]
actor = payload.get("review", {}).get("user", {}).get("login")
reason = "the author submitted a review response"
else:
return False
if pull_number is None or reason is None:
return False
pull = api.get_pull(pull_number)
if pull.get("state") != "open" or not has_waiting_label(pull):
return False
if not author_activity:
author = pull.get("user", {}).get("login")
author_activity = bool(actor and author and actor.lower() == author.lower())
if not author_activity:
return False
return remove_waiting_label(api, pull_number, reason)
def close_stale_waiting_prs(api: GitHubAPI, now: datetime | None = None) -> int:
now = now or datetime.now(UTC)
closed = 0
for issue in api.list_waiting_issues():
if closed >= MAX_CLOSURES_PER_RUN:
break
if "pull_request" not in issue or not has_waiting_label(issue):
continue
try:
label_applied_at = latest_waiting_label_at(api.list_timeline(issue["number"]))
if label_applied_at is None:
print(
f"::warning::#{issue['number']} has {LABEL} but no label timestamp "
"in the timeline; skipping."
)
continue
pull = api.get_pull(issue["number"])
reason = author_activity_since_label(api, pull, label_applied_at)
if reason:
remove_waiting_label(api, issue["number"], reason)
continue
if days_between(label_applied_at, now) < WAITING_DAYS:
continue
api.close_pull(issue["number"])
api.create_comment(issue["number"], close_message(label_applied_at))
closed += 1
print(f"Closed #{issue['number']}; {LABEL} was applied at {label_applied_at}.")
except Exception as error: # noqa: BLE001 - keep the sweep moving across PRs.
print(f"::warning::Could not close #{issue['number']}: {error}")
print(f"Closed {closed} PR(s) labeled {LABEL}.")
return closed
def run(
event_name: str,
payload: dict[str, Any],
api: GitHubAPI,
repo: str,
now: datetime | None = None,
) -> None:
if repo != CANONICAL_REPO:
print(f"Skipping {repo}; waiting-on-author hygiene only runs for {CANONICAL_REPO}.")
return
if event_name in {"schedule", "workflow_dispatch"}:
close_stale_waiting_prs(api, now=now)
return
clear_on_author_activity(event_name, payload, api)
def load_event_payload() -> dict[str, Any]:
path = os.environ.get("GITHUB_EVENT_PATH")
if not path:
return {}
with open(path, encoding="utf-8") as handle:
return json.load(handle)
def main() -> int:
repo = os.environ.get("GITHUB_REPOSITORY", "")
token = os.environ.get("GITHUB_TOKEN")
if not token:
print("GITHUB_TOKEN is required", file=sys.stderr)
return 1
event_name = os.environ.get("GITHUB_EVENT_NAME", "")
run(event_name, load_event_payload(), GitHubAPI(token, repo), repo)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+235
View File
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""Offline tests for waiting_on_author.py."""
from __future__ import annotations
import importlib.util
import pathlib
import unittest
from datetime import UTC, datetime
from typing import Any
SCRIPT_PATH = pathlib.Path(__file__).with_name("waiting_on_author.py")
SPEC = importlib.util.spec_from_file_location("waiting_on_author", SCRIPT_PATH)
waiting_on_author = importlib.util.module_from_spec(SPEC)
assert SPEC and SPEC.loader
SPEC.loader.exec_module(waiting_on_author)
def pr(
number: int = 12, author: str = "alice", labels: list[str] | None = None, state: str = "open"
) -> dict[str, Any]:
labels = [waiting_on_author.LABEL] if labels is None else labels
return {
"number": number,
"state": state,
"user": {"login": author},
"labels": [{"name": label} for label in labels],
}
def issue(number: int, labels: list[str] | None = None, is_pr: bool = True) -> dict[str, Any]:
labels = [waiting_on_author.LABEL] if labels is None else labels
item: dict[str, Any] = {"number": number, "labels": [{"name": label} for label in labels]}
if is_pr:
item["pull_request"] = {}
return item
def labeled_at(iso: str, label: str | None = None) -> dict[str, Any]:
return {
"event": "labeled",
"label": {"name": label or waiting_on_author.LABEL},
"created_at": iso,
}
class FakeAPI:
def __init__(
self,
*,
pull: dict[str, Any] | None = None,
issues: list[dict[str, Any]] | None = None,
timeline_by_issue: dict[int, list[dict[str, Any]]] | None = None,
issue_comments: dict[int, list[dict[str, Any]]] | None = None,
review_comments: dict[int, list[dict[str, Any]]] | None = None,
reviews: dict[int, list[dict[str, Any]]] | None = None,
commits: dict[int, list[dict[str, Any]]] | None = None,
):
self.pull = pull or pr()
self.issues = issues or []
self.timeline_by_issue = timeline_by_issue or {}
self.issue_comments = issue_comments or {}
self.review_comments = review_comments or {}
self.reviews = reviews or {}
self.commits = commits or {}
self.removed: list[tuple[int, str]] = []
self.closed: list[int] = []
self.comments: list[tuple[int, str]] = []
def get_pull(self, pull_number: int) -> dict[str, Any]:
return self.pull | {"number": pull_number}
def remove_label(self, issue_number: int, label: str) -> bool:
self.removed.append((issue_number, label))
return True
def list_waiting_issues(self) -> list[dict[str, Any]]:
return self.issues
def list_timeline(self, issue_number: int) -> list[dict[str, Any]]:
return self.timeline_by_issue.get(issue_number, [])
def list_issue_comments(self, issue_number: int) -> list[dict[str, Any]]:
return self.issue_comments.get(issue_number, [])
def list_review_comments(self, pull_number: int) -> list[dict[str, Any]]:
return self.review_comments.get(pull_number, [])
def list_reviews(self, pull_number: int) -> list[dict[str, Any]]:
return self.reviews.get(pull_number, [])
def list_commits(self, pull_number: int) -> list[dict[str, Any]]:
return self.commits.get(pull_number, [])
def close_pull(self, pull_number: int) -> None:
self.closed.append(pull_number)
def create_comment(self, issue_number: int, body: str) -> None:
self.comments.append((issue_number, body))
class WaitingOnAuthorTest(unittest.TestCase):
def test_latest_waiting_label_at_uses_latest_matching_label(self) -> None:
self.assertEqual(
waiting_on_author.latest_waiting_label_at(
[
labeled_at("2026-07-01T00:00:00Z"),
labeled_at("2026-07-10T00:00:00Z", "other"),
labeled_at("2026-07-12T00:00:00Z"),
]
),
"2026-07-12T00:00:00Z",
)
def test_author_issue_comment_removes_waiting_label(self) -> None:
api = FakeAPI(pull=pr(author="Alice"))
waiting_on_author.clear_on_author_activity(
"issue_comment",
{"issue": {"number": 12, "pull_request": {}}, "comment": {"user": {"login": "alice"}}},
api,
)
self.assertEqual(api.removed, [(12, waiting_on_author.LABEL)])
def test_author_review_thread_reply_removes_waiting_label(self) -> None:
api = FakeAPI(pull=pr(author="alice"))
waiting_on_author.clear_on_author_activity(
"pull_request_review_comment",
{"pull_request": {"number": 12}, "comment": {"user": {"login": "alice"}}},
api,
)
self.assertEqual(api.removed, [(12, waiting_on_author.LABEL)])
def test_maintainer_comment_keeps_waiting_label(self) -> None:
api = FakeAPI(pull=pr(author="alice"))
waiting_on_author.clear_on_author_activity(
"issue_comment",
{
"issue": {"number": 12, "pull_request": {}},
"comment": {"user": {"login": "maintainer"}},
},
api,
)
self.assertEqual(api.removed, [])
def test_new_commits_remove_waiting_label(self) -> None:
api = FakeAPI(pull=pr(author="alice"))
waiting_on_author.clear_on_author_activity(
"pull_request_target",
{"action": "synchronize", "pull_request": {"number": 12}},
api,
)
self.assertEqual(api.removed, [(12, waiting_on_author.LABEL)])
def test_scheduled_sweep_closes_pr_after_7_days(self) -> None:
api = FakeAPI(
issues=[issue(20)], timeline_by_issue={20: [labeled_at("2026-07-17T00:00:00Z")]}
)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.closed, [20])
self.assertEqual(len(api.comments), 1)
self.assertIn(waiting_on_author.LABEL, api.comments[0][1])
def test_scheduled_sweep_removes_label_after_author_comment(self) -> None:
api = FakeAPI(
pull=pr(number=23, author="alice"),
issues=[issue(23)],
timeline_by_issue={23: [labeled_at("2026-07-01T00:00:00Z")]},
issue_comments={
23: [{"user": {"login": "alice"}, "created_at": "2026-07-20T00:00:00Z"}]
},
)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.removed, [(23, waiting_on_author.LABEL)])
self.assertEqual(api.closed, [])
def test_scheduled_sweep_keeps_label_after_maintainer_comment(self) -> None:
api = FakeAPI(
pull=pr(number=24, author="alice"),
issues=[issue(24)],
timeline_by_issue={24: [labeled_at("2026-07-18T00:00:00Z")]},
issue_comments={
24: [{"user": {"login": "maintainer"}, "created_at": "2026-07-20T00:00:00Z"}]
},
)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.removed, [])
self.assertEqual(api.closed, [])
def test_scheduled_sweep_removes_label_after_new_commit(self) -> None:
api = FakeAPI(
pull=pr(number=25, author="alice"),
issues=[issue(25)],
timeline_by_issue={25: [labeled_at("2026-07-01T00:00:00Z")]},
commits={25: [{"commit": {"author": {"date": "2026-07-20T00:00:00Z"}}}]},
)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.removed, [(25, waiting_on_author.LABEL)])
self.assertEqual(api.closed, [])
def test_scheduled_sweep_ignores_author_comment_before_label(self) -> None:
api = FakeAPI(
pull=pr(number=26, author="alice"),
issues=[issue(26)],
timeline_by_issue={26: [labeled_at("2026-07-17T00:00:00Z")]},
issue_comments={
26: [{"user": {"login": "alice"}, "created_at": "2026-07-10T00:00:00Z"}]
},
)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.removed, [])
self.assertEqual(api.closed, [26])
def test_scheduled_sweep_leaves_6_day_pr_open(self) -> None:
api = FakeAPI(
issues=[issue(21)], timeline_by_issue={21: [labeled_at("2026-07-18T00:00:00Z")]}
)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.closed, [])
self.assertEqual(api.comments, [])
def test_scheduled_sweep_skips_missing_label_timestamp(self) -> None:
api = FakeAPI(issues=[issue(22)], timeline_by_issue={22: []})
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(api.closed, [])
def test_scheduled_sweep_caps_closures_per_run(self) -> None:
issues = [issue(100 + idx) for idx in range(waiting_on_author.MAX_CLOSURES_PER_RUN + 3)]
timeline = {item["number"]: [labeled_at("2026-07-01T00:00:00Z")] for item in issues}
api = FakeAPI(issues=issues, timeline_by_issue=timeline)
waiting_on_author.close_stale_waiting_prs(api, now=datetime(2026, 7, 24, tzinfo=UTC))
self.assertEqual(len(api.closed), waiting_on_author.MAX_CLOSURES_PER_RUN)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -31,7 +31,7 @@ prompt: |
```
{
"type": "bug" | "enhancement" | "documentation" | null,
"type": "bug" | "Feature" | "Docs" | null,
"components": ["comp:server" | "comp:runner" | "comp:repr" | "comp:web-ui" | "comp:tui" | "comp:policies" | "comp:harnesses" | "comp:infra"],
"priority": "P0-critical" | "P1-high" | "P2-medium" | "P3-low" | null,
"needs_info": true | false,
@@ -49,9 +49,9 @@ prompt: |
repro steps for a bug). When `true`, leave type/component/priority as
`null`.
**type** — the issue templates add `bug` or `enhancement` labels
**type** — the issue templates add `bug` or `Feature` labels
automatically; if the existing labels already include one, set the
matching type. Otherwise determine from content. Use `documentation`
matching type. Otherwise determine from content. Use `Docs`
for docs-only issues.
**components** — list of affected subsystems (one or more):
+6 -1
View File
@@ -83,11 +83,16 @@ jobs:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
ROUTER_MODEL: ${{ vars.OMNIGENT_CI_FAST_ANTHROPIC_MODEL }}
run: |
if [ -z "${LLM_API_KEY:-}" ] || [ -z "${GATEWAY_BASE_URL:-}" ]; then
echo "::notice::No LLM credentials; reviewer ranking skipped (load-balanced fallback)."
exit 0
fi
if [ -z "${ROUTER_MODEL:-}" ]; then
echo "::warning::Repository variable OMNIGENT_CI_FAST_ANTHROPIC_MODEL is empty; reviewer ranking skipped (load-balanced fallback)."
exit 0
fi
# Skip maintainer-authored PRs: the assign step (auto-assign-reviewer.js)
# no-ops on them, so ranking them would spend a gateway call whose result
# is discarded. Mirror that step's author-is-maintainer guard here
@@ -141,7 +146,7 @@ jobs:
# /anthropic/messages + x-api-key path 401s / 400s on this gateway.)
url = os.environ["GATEWAY_BASE_URL"].rstrip("/") + "/chat/completions"
payload = json.dumps({
"model": "databricks-claude-sonnet-4-6",
"model": os.environ["ROUTER_MODEL"],
"max_tokens": 512,
"temperature": 0,
"messages": [
+13 -5
View File
@@ -158,12 +158,20 @@ jobs:
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr comment "${{ github.event.pull_request.number }}" \
--edit-last \
--body-file comment_body.md || \
gh pr comment "${{ github.event.pull_request.number }}" \
--body-file comment_body.md
COMMENT_MARKER="<!-- benchmark-pr-comment -->"
COMMENT_ID=$(gh api --paginate \
"repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | startswith(\"$COMMENT_MARKER\")) | .id" \
| head -1)
if [ -n "$COMMENT_ID" ]; then
gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" \
-X PATCH -f body="$(cat comment_body.md)"
else
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file comment_body.md
fi
- name: Upload candidate results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+7 -2
View File
@@ -84,7 +84,12 @@ jobs:
python scripts/update_versions.py "$MODE" --new-version "$NEW_VERSION"
- name: Regenerate lockfile
run: uv lock
run: |
uv lock
# uv may emit non-canonical lockfile fields (e.g. size on file
# entries); normalize like the pre-commit fixer, then hard-verify.
python3 scripts/normalize_uv_lock_registry.py uv.lock || true
python3 scripts/normalize_uv_lock_registry.py --check uv.lock
- name: Verify all locations agree
run: uv run --no-project --python 3.12 --with packaging python scripts/update_versions.py check
@@ -139,6 +144,6 @@ jobs:
--title "Bump version to ${resolved}" \
--body "Automated version bump via \`.github/workflows/bump-version.yml\` (mode: \`${MODE}\`, input: \`${NEW_VERSION}\`).
Rewrote \`[project].version\` and sibling \`==\` pins across all three packages (\`pyproject.toml\`, \`sdks/python-client\`, \`sdks/ui\`), the runtime \`VERSION\` constant in \`omnigent/version.py\`, and regenerated \`uv.lock\`.
Rewrote \`[project].version\` and sibling \`==\` pins across all four packages (\`pyproject.toml\`, \`sdks/python-client\`, \`sdks/ui\`, \`integrations/slack\`), the runtime \`VERSION\` constant in \`omnigent/version.py\`, and regenerated \`uv.lock\`.
Generated by \`scripts/update_versions.py\`. Opened via the omnigent-ci App when configured (CI runs automatically); on the GITHUB_TOKEN fallback, re-open or push to kick CI."
+5 -5
View File
@@ -370,14 +370,14 @@ jobs:
path: .tmp-codex-parity-target/debug/codex-parity-sidecar
key: codex-parity-bin-${{ runner.os }}-${{ steps.rustc.outputs.version }}-${{ hashFiles('tests/codex_parity/sidecar/**') }}
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install codex CLI
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
npm install --ignore-scripts --prefix .github/ci-deps
pnpm install --frozen-lockfile --ignore-scripts --filter e2e-ci-deps
echo "${GITHUB_WORKSPACE}/.github/ci-deps/node_modules/.bin" >> "$GITHUB_PATH"
- name: Cache virtualenv
@@ -3,9 +3,11 @@ name: Discord watch rotation - maintain schedule
# Monthly housekeeping for rotation_schedule.json: prune elapsed dates and
# extend the horizon ~3 months out. Opens a PR rather than pushing to main, so
# the change is reviewable and no write to a protected branch is needed.
# Schedule paused: runs only on manual dispatch for now.
# To resume, restore the `schedule:` block below.
# schedule:
# - cron: "0 8 1 * *" # 08:00 UTC on the 1st of each month
on:
schedule:
- cron: "0 8 1 * *" # 08:00 UTC on the 1st of each month
workflow_dispatch: {} # manual "Run workflow" button
# Needs to push a branch and open a PR; no other write scope.
+6 -5
View File
@@ -1,13 +1,14 @@
name: Discord watch rotation
# Wakes up only at the UTC times that are ~08:00 in an assignee's timezone.
# Schedule paused: the rotation ping only runs on manual dispatch for now.
# To resume, restore the `schedule:` block below.
# schedule:
# - cron: "0 0 * * *" # 08:00 Asia/Singapore (UTC+8, no daylight saving)
# - cron: "0 15 * * *" # 08:00 SF in summer (PDT); 07:00 in winter (PST)
# Note: a single fixed UTC time can't track San Francisco's daylight saving,
# so the SF ping lands at 08:00 in summer (PDT) and 07:00 in winter (PST).
on:
schedule:
- cron: "0 0 * * *" # 08:00 Asia/Singapore (UTC+8, no daylight saving)
- cron: "0 15 * * *" # 08:00 SF in summer (PDT); 07:00 in winter (PST)
workflow_dispatch: {} # manual "Run workflow" button for testing
workflow_dispatch: {} # manual "Run workflow" button
# Only needs to check out the repo; nothing is written back.
permissions:
+10 -4
View File
@@ -254,16 +254,22 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
# Install outside the checked-out tree: a repo-root package.json would
# otherwise capture this bare `npm install` and hoist it there, leaving
# this dir's node_modules empty.
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR" && cd "$CC_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.212
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Write Omnigent provider config
if: steps.plan.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
env:
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
OMNIGENT_AGENT_MODEL: ${{ vars.OMNIGENT_CI_ANTHROPIC_MODEL }}
run: |
: "${OMNIGENT_AGENT_MODEL:?Set OMNIGENT_CI_ANTHROPIC_MODEL}"
mkdir -p "$HOME/.omnigent"
python3 -c "
import pathlib, os, json
@@ -273,7 +279,7 @@ jobs:
'anthropic': {
'base_url': gw + '/anthropic',
'api_key_ref': 'env:LLM_API_KEY',
'models': {'default': 'databricks-claude-opus-4-8'},
'models': {'default': os.environ['OMNIGENT_AGENT_MODEL']},
}}}}
pathlib.Path.home().joinpath('.omnigent', 'config.yaml').write_text(json.dumps(cfg, indent=2))
"
+2 -1
View File
@@ -31,7 +31,8 @@ on:
- 'pyproject.toml'
- 'setup.py'
- 'uv.lock'
- 'web/package-lock.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/docker-build.yml'
permissions:
+3 -2
View File
@@ -89,14 +89,14 @@ jobs:
base="${INPUT_BASE:-}"
proceed=false; dry_run=false
# Does the tag look like a final release (vX.Y.Z, not rc/dev/alpha/beta)?
# Does the tag look like a final release (vX.Y.Z, not a pre-release)?
is_version=true
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) is_version=false ;;
esac
case "$tag" in
*rc*|*dev*|*a[0-9]*|*b[0-9]*) is_version=false ;;
*rc[0-9]*|*dev[0-9]*|*pre[0-9]*) is_version=false ;;
esac
if [ "$EVENT_NAME" = "workflow_run" ]; then
@@ -245,6 +245,7 @@ jobs:
stderr-file: /tmp/draft-stderr.log
gateway-base-url: ${{ secrets.GATEWAY_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
model: ${{ vars.OMNIGENT_CI_ANTHROPIC_MODEL }}
- name: Extract synthesized notes (fall back to mechanical)
if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.available == 'true'
+4 -2
View File
@@ -71,5 +71,7 @@ jobs:
# OpenAI-compatible gateway (same secrets the e2e suites use).
OPENAI_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
OPENAI_API_KEY: ${{ secrets.LLM_API_KEY }}
E2E_UI_JUDGE_MODEL: databricks-gpt-5-4
run: bash .github/scripts/e2e-ui-required/check.sh
E2E_UI_JUDGE_MODEL: ${{ vars.OMNIGENT_CI_E2E_JUDGE_MODEL }}
run: |
: "${E2E_UI_JUDGE_MODEL:?Set OMNIGENT_CI_E2E_JUDGE_MODEL repository variable}"
bash .github/scripts/e2e-ui-required/check.sh
+15 -15
View File
@@ -168,8 +168,8 @@ jobs:
with:
python-version-file: ".python-version"
- name: Set up Node 20
uses: ./.github/actions/setup-node
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
@@ -225,14 +225,11 @@ jobs:
- name: Build web SPA
# Build BEFORE pytest: Vite's emptyOutDir clobbers the static dir,
# so never run it under xdist or alongside the live server.
# --legacy-peer-deps avoids re-resolving the known React 19 peer
# conflict under @emoji-mart/react.
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
cd web
npm ci --legacy-peer-deps --no-audit --no-fund
npm run build
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build
# Native coding-agent harness enablement: the next steps let the
# native render-parity tests boot a real Claude Code / Codex CLI. The
@@ -241,15 +238,17 @@ jobs:
# claude-code 2.1.170, NOT the 2.1.124 in .github/ci-deps: 2.1.124
# doesn't recognise the hook events the native bridge configures and
# shows a blocking startup modal that swallows the first message.
# --ignore-scripts then run install.cjs explicitly (audited: platform
# detect + same-tree hardlink, no network/exec) and put its bin on PATH.
# Runs the package's install script so the platform-specific binary is
# linked into the temporary CLI directory and put on PATH.
# (The install.cjs script was removed in the same version the upstream
# npm package no longer ships it, so lifecycle scripts are required.)
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR" && cd "$CC_CLI_DIR"
npm install --no-audit --no-fund @anthropic-ai/claude-code@2.1.212
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Install Codex CLI
# @openai/codex at the .github/ci-deps pin (same build as e2e.yml's
@@ -259,9 +258,10 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.codex-cli" && cd "${GITHUB_WORKSPACE}/.codex-cli"
CODEX_CLI_DIR="${RUNNER_TEMP}/omnigent-codex-cli"
mkdir -p "$CODEX_CLI_DIR" && cd "$CODEX_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @openai/codex@0.139.0
echo "${GITHUB_WORKSPACE}/.codex-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CODEX_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Run UI e2e tests
# --ui-skip-build: the SPA was built in the previous step.
+9 -30
View File
@@ -55,48 +55,27 @@ jobs:
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up Node
uses: ./.github/actions/setup-node
with:
# Node 22.x per web/electron/README.md ("Prerequisites").
node-version: "22"
cache-dependency-path: web/electron/package-lock.json
- name: Verify lockfile uses public registry
# Fail fast (in seconds, not minutes) if any package-lock.json
# resolved URL points at an internal proxy that public CI runners
# can't reach — e.g. npm-proxy.cloud.databricks.com. Without this
# guard, npm ci silently times out mid-install on Windows/Linux.
# Uses the shared normalize_package_lock_registry.py script (same
# one wired into pre-commit) so CI and local checks stay in sync.
working-directory: web/electron
shell: bash
run: |
python3 ../../scripts/normalize_package_lock_registry.py --check package-lock.json
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install dependencies
working-directory: web/electron
run: npm ci --no-audit --no-fund
# The shell-owned update overlay reuses the web UpdateBanner component; it
# is built from the web app into electron/overlay/ (gitignored) and shipped
# by electron-builder (build.files). The build:<platform> scripts run it
# automatically via their `prebuild:*` hook (see web/electron/package.json)
# — this step only needs to install the web app's deps so that hook works.
- name: Install web deps (for the update overlay build)
working-directory: web
run: npm ci --legacy-peer-deps --no-audit --no-fund
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |-
pnpm install --frozen-lockfile --filter @omnigent/electron
pnpm install --frozen-lockfile --filter web
- name: Build ${{ matrix.platform }} app
working-directory: web/electron
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
# No signing credentials in CI: force an unsigned build instead of
# letting electron-builder fail hunting for a certificate.
CSC_IDENTITY_AUTO_DISCOVERY: "false"
# electron-builder downloads Electron/tooling from GitHub; the token
# lifts the anonymous rate limit that otherwise flakes downloads.
GH_TOKEN: ${{ github.token }}
run: npm run ${{ matrix.build-script }} -- --publish never
run: pnpm run ${{ matrix.build-script }} -- --publish never
# One artifact per platform bundling the COMPLETE electron-updater feed —
# the installer(s), the .blockmap electron-updater needs for differential
+47 -5
View File
@@ -242,6 +242,7 @@ jobs:
stderr-file: ${{ github.workspace }}/scout-stderr.log
gateway-base-url: ${{ secrets.GATEWAY_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
model: ${{ vars.OMNIGENT_CI_ANTHROPIC_MODEL }}
- name: Parse candidates
id: candidates
@@ -360,7 +361,7 @@ jobs:
# markdown code fences aren't shell-evaluated — index and repo come in
# via env (CAND_INDEX / SOURCE_REPO), never interpolated into the body.
CAND_INDEX="$i" python3 -u <<'PYEOF'
import json, os, pathlib, subprocess
import json, os, pathlib, re, subprocess
repo = os.environ["SOURCE_REPO"]
idx = int(os.environ["CAND_INDEX"])
cand = json.load(open("/tmp/candidates.json"))[idx]
@@ -381,6 +382,21 @@ jobs:
mergers, authors = Counter(), Counter()
def _usable(login):
return bool(login) and not login.endswith("[bot]") and login != "omnigent-ci"
# Detect a demo VIDEO already attached to a PR body so the reviewer can
# reuse it instead of re-recording. GitHub renders uploaded videos as bare
# asset links (user-attachments/assets or the older <owner>/<repo>/assets),
# and video files by extension. Images (user-images.githubusercontent /
# ![](...) / .png|.jpg) are NOT counted — the marker asks for a recording.
video_re = re.compile(
r"https?://github\.com/user-attachments/assets/[0-9a-f-]+"
r"|https?://github\.com/[^/\s)]+/[^/\s)]+/assets/\d+/[0-9a-f-]+"
r"|https?://[^\s)]+\.(?:mp4|mov|webm|m4v)\b"
r"|<video\b",
re.IGNORECASE)
def _title_cell(t):
# Keep the table one row per PR: collapse newlines and pipe chars.
return (t or "").replace("|", "\\|").replace("\n", " ").strip()
rows = [] # (number, title, url, has_video) for the reviewer table
for pr in refs:
try:
diff = subprocess.run(
@@ -389,10 +405,11 @@ jobs:
except Exception as e:
diff = f"(diff unavailable: {e})"
parts += [f"### PR #{pr}", f"{fence}diff", diff[:BUDGET], fence]
title, url, has_video = "", f"https://github.com/{repo}/pull/{pr}", False
try:
meta = json.loads(subprocess.run(
["gh", "pr", "view", str(pr), "--repo", repo,
"--json", "mergedBy,author"],
"--json", "title,body,url,mergedBy,author"],
capture_output=True, text=True, timeout=60).stdout or "{}")
mb = (meta.get("mergedBy") or {}).get("login", "")
au = (meta.get("author") or {}).get("login", "")
@@ -400,9 +417,21 @@ jobs:
mergers[mb] += 1
if _usable(au):
authors[au] += 1
title = _title_cell(meta.get("title", ""))
url = meta.get("url") or url
has_video = bool(video_re.search(meta.get("body") or ""))
except Exception as e:
print(f"::notice::Could not read merger/author for PR #{pr}: {e}")
print(f"::notice::Could not read metadata for PR #{pr}: {e}")
rows.append((pr, title, url, has_video))
pathlib.Path(f"/tmp/material_{idx}.txt").write_text("\n".join(parts))
# Reviewer reference table: which contributing PRs already ship a demo
# video (✅, linked) vs. still need one (—). Written even when none have a
# video, so the reviewer always sees the source PRs behind the marker.
table = ["| PR | Title | Demo video? |", "| --- | --- | --- |"]
for pr, title, url, has_video in rows:
cell = f"[✅ video]({url})" if has_video else "—"
table.append(f"| [#{pr}]({url}) | {title} | {cell} |")
pathlib.Path(f"/tmp/demo_table_{idx}.md").write_text("\n".join(table))
# Most-frequent merger wins; ties broken by Counter insertion order (PR
# order). Fall back to the most-frequent author, then empty.
reviewer = (mergers.most_common(1)[0][0] if mergers
@@ -499,6 +528,7 @@ jobs:
# env), so the unsandboxed drafter run above never sees it and it
# can't reach the drafter's scanned stdout.
GATEWAY_BASE_URL='${{ secrets.GATEWAY_BASE_URL }}' \
IMAGE_MODEL='${{ vars.OMNIGENT_CI_IMAGE_MODEL }}' \
IMAGE_PROMPT="$image_prompt" SLUG="$slug" SITE="$SITE" POST="$post" \
python3 -u <<'PYEOF' || echo "::warning::hero image generation failed for ${slug}; leaving heroArt blank"
import base64, json, os, pathlib, re, urllib.request
@@ -511,7 +541,9 @@ jobs:
m = re.match(r"(https?://[^/]+)", gw)
if not m:
raise SystemExit(f"cannot parse gateway host from {gw!r}")
model = os.environ.get("IMAGE_MODEL", "databricks-gemini-3-pro-image")
model = os.environ.get("IMAGE_MODEL", "").strip()
if not model:
raise SystemExit("repository variable OMNIGENT_CI_IMAGE_MODEL is empty")
url = f"{m.group(1)}/serving-endpoints/{model}/invocations"
style = (" Flat vector illustration, dark navy tech background with subtle "
"circuit lines, teal and pink accents, 16:9 wide, no text, no words, "
@@ -660,7 +692,16 @@ jobs:
# the source-repo maintainer isn't an omnigent-site collaborator), so
# it must be written on BOTH the create and force-push-update paths.
summary="$(sed -n '/<!-- BLOG_DRAFT_SUMMARY -->/,$p' "/tmp/drafter_out_${idx}.txt" | tail -n +2 || true)"
body="$(printf 'Drafts a feature-blog post for **%s**, selected by `feature-blog-scout` at the %s release cut.\n\n> **This is a DRAFT.** Before merging, a human must: record the mandatory demo (replace the `DEMO REQUIRED` marker) and do a final voice pass. The hero image and `author: omnigent` byline are auto-generated — review and optionally replace them.\n\n%s\n\nSource release: %s%s\n<sub>Generated by omnigent `.github/workflows/feature-blog.yml`. Review for accuracy before merging.</sub>' "$title" "$TAG" "$summary" "$TAG" "$mention")"
# Reference table of the contributing PRs and whether each already
# ships a demo video (built in the Draft posts step). Reviewers can pull
# an existing recording from a ✅ PR to replace the `DEMO REQUIRED`
# marker instead of re-recording. Omitted if the table wasn't produced.
demo_table=""
if [ -f "/tmp/demo_table_${idx}.md" ]; then
demo_table="$(printf '\n\n### Source PRs — demo videos\nCheck a ✅ PR for a recording you can drop into the `DEMO REQUIRED` marker.\n\n%s\n' "$(cat "/tmp/demo_table_${idx}.md")")"
fi
body="$(printf 'Drafts a feature-blog post for **%s**, selected by `feature-blog-scout` at the %s release cut.\n\n> **This is a DRAFT.** Before merging, a human must: record the mandatory demo (replace the `DEMO REQUIRED` marker) and do a final voice pass. The hero image and `author: omnigent` byline are auto-generated — review and optionally replace them.\n\n%s%s\n\nSource release: %s%s\n<sub>Generated by omnigent `.github/workflows/feature-blog.yml`. Review for accuracy before merging.</sub>' "$title" "$TAG" "$summary" "$demo_table" "$TAG" "$mention")"
existing="$(gh pr list --repo "$SITE_REPO" --head "$branch" --state open --json url --jq '.[].url' | head -n1)"
if [ -n "$existing" ]; then
@@ -722,5 +763,6 @@ jobs:
/tmp/candidates.json
/tmp/drafter_out_*.txt
/tmp/post_*.mdx
/tmp/demo_table_*.md
retention-days: 7
if-no-files-found: ignore
+55 -20
View File
@@ -2,19 +2,22 @@
# run after the prod PyPI publish succeeded and the draft notes are curated
# (designs/RELEASE-AUTOMATION.md).
#
# Deterministic gates first (all fail with actionable links):
# * the tag is a final vX.Y.Z with an unpublished draft release,
# Deterministic gates first (each fails with actionable links):
# * the tag is a final vX.Y.Z (input is normalized: `0.7.0` -> `v0.7.0`)
# with an unpublished draft release; a draft whose tag binding was lost
# to a web-UI edit (tag_name became `untagged-…`) is rebound automatically,
# * PyPI serves all three lockstep packages at the version (never advertise
# a release that isn't installable),
# * the auto/changelog/vX.Y.Z CHANGELOG PR isn't sitting open,
# * the docs sweep: no open PRs against omnigent-site's X.Y-docs staging
# branch (every doc staged this cycle is reviewed + merged/closed).
# * the auto/changelog/vX.Y.Z CHANGELOG PR isn't sitting open.
# The docs sweep (open PRs against omnigent-site's X.Y-docs staging branch) is
# ADVISORY only: it lists what is still unmerged but never blocks the publish —
# docs can land after the release, any time before the docs-publish PR merges.
#
# The publish job binds the `publish-release` environment (one-time setup:
# create it in repo settings with required reviewers). Approving it is the
# human attestation "I reviewed the draft notes". The publish itself uses the
# App token — GITHUB_TOKEN-published releases emit no `release: published`
# event, and publish-changelog.yml + update-homebrew.yml hang off it — and
# event, and publish-changelog.yml + homebrew-tap-pr.yml hang off it — and
# sets make_latest explicitly, which API publishes don't do on their own.
#
# rc tags never finalize: their drafts deliberately stay unpublished.
@@ -64,16 +67,23 @@ jobs:
outputs:
release_id: ${{ steps.draft.outputs.release_id }}
already_published: ${{ steps.draft.outputs.already_published }}
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Require a final vX.Y.Z tag
id: tag
env:
TAG: ${{ inputs.tag }}
RAW_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
# Normalize the input: trim whitespace, add the leading v if omitted
# (`0.7.0` -> `v0.7.0`), so a bare version doesn't fail the dispatch.
TAG="$(printf '%s' "$RAW_TAG" | tr -d '[:space:]')"
case "$TAG" in v*) ;; *) TAG="v${TAG}" ;; esac
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::${TAG} is not a final vX.Y.Z tag — rc/dev/alpha/beta releases never finalize."
echo "::error::${RAW_TAG} is not a final vX.Y.Z tag — rc/dev/pre releases never finalize."
exit 1
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
# Drafts are invisible to read-only tokens and unaddressable by tag
# (the get-by-tag endpoint 404s on drafts) — resolve by listing with the
@@ -93,11 +103,32 @@ jobs:
id: draft
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
match="$(gh api "repos/${GITHUB_REPOSITORY}/releases" --paginate \
--jq 'map(select(.tag_name == env.TAG)) | first // empty')"
if [ -z "$match" ]; then
# Editing a draft in the web UI can silently drop its tag binding
# (tag_name becomes `untagged-…` while the name stays vX.Y.Z; bit
# v0.5.0 and v0.7.0). Recover: match the DRAFT by name and rebind —
# only ever onto a tag that already exists, so publishing can never
# mint a new tag at main.
match="$(gh api "repos/${GITHUB_REPOSITORY}/releases" --paginate \
--jq 'map(select(.draft == true and .name == env.TAG)) | first // empty')"
if [ -n "$match" ]; then
if ! gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq .object.sha >/dev/null 2>&1; then
echo "::error::Draft named ${TAG} exists but the git tag does not — push the tag before finalizing."
exit 1
fi
rebind_id="$(printf '%s' "$match" | jq -r '.id')"
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${rebind_id}" \
-f tag_name="$TAG" > /dev/null
match="$(gh api "repos/${GITHUB_REPOSITORY}/releases/${rebind_id}")"
echo "Rebound draft ${rebind_id} to ${TAG} (tag binding was lost, usually to a web-UI edit)." \
| tee -a "$GITHUB_STEP_SUMMARY"
fi
fi
if [ -z "$match" ]; then
echo "::error::No GitHub release found for ${TAG}. Did the tag push run github-release.yml?"
exit 1
@@ -118,7 +149,7 @@ jobs:
- name: Assert PyPI serves all three packages
if: steps.draft.outputs.already_published != 'true'
env:
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
version="${TAG#v}"
@@ -134,7 +165,7 @@ jobs:
if: steps.draft.outputs.already_published != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
open_pr="$(gh pr list --repo "$GITHUB_REPOSITORY" --head "auto/changelog/${TAG}" \
@@ -145,11 +176,14 @@ jobs:
fi
echo "CHANGELOG PR for ${TAG}: merged or not needed."
- name: Docs sweep — no open PRs against the X.Y-docs staging branch
# Advisory only: docs frequently land after the release. The list tells
# the coordinator what must merge into X.Y-docs before the docs-publish
# PR does — it never blocks the publish itself.
- name: Docs sweep — list open PRs against the X.Y-docs staging branch
if: steps.draft.outputs.already_published != 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TAG: ${{ inputs.tag }}
TAG: ${{ steps.tag.outputs.tag }}
SITE_REPO: ${{ github.repository_owner }}/omnigent-site
run: |
set -euo pipefail
@@ -158,16 +192,17 @@ jobs:
open="$(gh pr list --repo "$SITE_REPO" --base "$docs_branch" --state open \
--json url,title --jq '.[] | "- \(.url) \(.title)"')"
if [ -n "$open" ]; then
count="$(printf '%s\n' "$open" | grep -c .)"
{
echo "## Docs sweep failed for ${TAG}"
echo "## Docs sweep for ${TAG} — ${count} open PR(s) still target \`${docs_branch}\`"
echo ""
echo "Open PRs still target \`${docs_branch}\` on ${SITE_REPO} — review and merge/close them, then re-dispatch:"
echo "Advisory, not blocking. Merge/close these before merging the docs-publish PR:"
echo "$open"
} | tee -a "$GITHUB_STEP_SUMMARY"
echo "::error::Open doc PRs still target ${docs_branch} — see the run summary."
exit 1
echo "::warning::${count} open doc PR(s) still target ${docs_branch} (non-blocking) — see the run summary."
else
echo "Docs sweep clean: no open PRs against ${docs_branch}." | tee -a "$GITHUB_STEP_SUMMARY"
fi
echo "Docs sweep clean: no open PRs against ${docs_branch}." | tee -a "$GITHUB_STEP_SUMMARY"
# Approving this environment attests "I reviewed the curated draft notes".
publish:
@@ -189,7 +224,7 @@ jobs:
- name: Publish the draft as Latest
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TAG: ${{ inputs.tag }}
TAG: ${{ needs.checks.outputs.tag }}
RELEASE_ID: ${{ needs.checks.outputs.release_id }}
run: |
set -euo pipefail
@@ -202,5 +237,5 @@ jobs:
echo ""
echo "The \`release: published\` event now fires (App-token publish):"
echo "- **publish-changelog.yml** opens the omnigent-site release-post PR and the docs-publish PR — review and merge both."
echo "- **update-homebrew.yml** opens the homebrew-tap bump PR — review the resource diff, then apply the \`pr-pull\` label."
echo "- **homebrew-tap-pr.yml** opens the homebrew-tap bump PR — review the resource diff, then apply the \`pr-pull\` label."
} >> "$GITHUB_STEP_SUMMARY"
+12 -8
View File
@@ -238,22 +238,26 @@ jobs:
# executor adapters import at collection time.
run: uv sync --extra all --extra dev
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install binary dependencies
# Mirrors e2e.yml. ripgrep: Grep fallback for inner tests. tmux +
# bubblewrap: the e2e runner runs real agents under the linux_bwrap
# sandbox, which fails loud if `bwrap` is missing. The apparmor
# sysctl mirrors ci.yml (Ubuntu 24.04 blocks unprivileged user
# namespaces, which bwrap's unshare(CLONE_NEWUSER) needs). npm install
# with --ignore-scripts blocks postinstall; the claude-code stub needs
# its audited install.cjs run explicitly (platform detect + same-tree
# hardlink, no network/exec) for claude-sdk harness rows.
working-directory: .github/ci-deps
# namespaces, which bwrap's unshare(CLONE_NEWUSER) needs). pnpm
# install with --ignore-scripts blocks postinstall; the claude-code
# stub needs its audited install.cjs run explicitly (platform detect +
# same-tree hardlink, no network/exec) for claude-sdk harness rows.
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
sudo apt-get update
sudo apt-get install -y ripgrep tmux bubblewrap
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
npm install --ignore-scripts
node node_modules/@anthropic-ai/claude-code/install.cjs
pnpm install --frozen-lockfile --ignore-scripts --filter e2e-ci-deps
node .github/ci-deps/node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.github/ci-deps/node_modules/.bin" >> "$GITHUB_PATH"
- name: Run pytest target
@@ -274,7 +278,7 @@ jobs:
# low-quota gpt-5-4 model, so sustained 429s don't masquerade as
# flakes (mirrors e2e.yml).
OMNIGENT_TEST_MODEL_SPREAD: "1"
OMNIGENT_TEST_MODEL_POOL_GPT: "databricks-gpt-5-5,databricks-gpt-5-4-mini"
OMNIGENT_TEST_MODEL_POOL_GPT: ${{ vars.OMNIGENT_CI_E2E_MODEL_POOL_GPT }}
run: |
mkdir -p artifacts "artifacts/basetemp-${{ matrix.attempt }}"
# --junitxml emits per-test results eagerly so diagnostics survive a
+81 -26
View File
@@ -15,7 +15,11 @@ name: Flake stress (E2E UI)
# but needs the full UI toolchain: a built SPA, Playwright Chromium, and — for
# the native render-parity / Codex goal-mode tests — the Claude Code / Codex
# CLIs and the Rust parity sidecar. This workflow mirrors e2e-ui.yml's setup
# exactly, then runs ONE target N times instead of the sharded full suite.
# exactly (including the dedicated build-sidecar job that compiles the
# codex-parity sidecar ONCE and hands each attempt the prebuilt binary via
# CODEX_PARITY_SIDECAR_BIN — otherwise the fixture's inline cargo build runs on
# every attempt and blows past the per-test timeout on a cold Rust cache), then
# runs ONE target N times instead of the sharded full suite.
#
# Examples:
# gh workflow run flake-stress-ui.yml --ref main \
@@ -140,9 +144,58 @@ jobs:
echo "attempts_json=$ARR" >> "$GITHUB_OUTPUT"
echo "Will run $ATTEMPTS attempts of: $TEST_TARGET extra='$EXTRA_ARGS'"
build-sidecar:
# Build the Codex-parity sidecar ONCE and publish the binary, exactly like
# e2e-ui.yml. The mocked_native_codex_goal_session fixture needs it, but
# compiling it pulls openai/codex's core_test_support (~1100 crates). Done
# lazily inside pytest it runs ~7 min cold — past the per-test --timeout, so
# every attempt would die at fixture setup on a cold Rust cache. Building it
# here once and handing every attempt the ~10MB binary (via the artifact +
# CODEX_PARITY_SIDECAR_BIN below) keeps the sidecar off the attempt's
# critical path. Checks out target_branch so the sidecar matches the code
# under test.
name: build codex-parity sidecar
needs: prep
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.inputs.target_branch }}
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- name: Capture Rust version
id: rustc
run: echo "version=$(rustc --version | tr ' ' '-')" >> "$GITHUB_OUTPUT"
# Same cache key as e2e-ui.yml / ci.yml so a main-populated cache restores:
# the binary is a pure function of sidecar/** + the toolchain, so cache the
# built binary (not the 1.6 GB target dir) and skip the compile on a hit.
- name: Cache parity sidecar binary
id: sidecar-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
with:
path: .tmp-codex-parity-target/debug/codex-parity-sidecar
key: codex-parity-bin-${{ runner.os }}-${{ steps.rustc.outputs.version }}-${{ hashFiles('tests/codex_parity/sidecar/**') }}
- name: Build parity sidecar
if: steps.sidecar-cache.outputs.cache-hit != 'true'
run: |
cargo build \
--manifest-path tests/codex_parity/sidecar/Cargo.toml \
--target-dir .tmp-codex-parity-target
- name: Upload sidecar binary
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: codex-parity-sidecar
path: .tmp-codex-parity-target/debug/codex-parity-sidecar
if-no-files-found: error
retention-days: 1
repro:
name: Attempt ${{ matrix.attempt }}
needs: prep
needs: [prep, build-sidecar]
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
@@ -162,8 +215,8 @@ jobs:
with:
python-version-file: ".python-version"
- name: Set up Node 20
uses: ./.github/actions/setup-node
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
@@ -190,20 +243,17 @@ jobs:
sudo apt-get install -y bubblewrap tmux
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- name: Set up Rust toolchain
# The mocked_native_codex_goal_session fixture builds the Codex parity
# sidecar via `cargo build`; pin the toolchain for a stable cache key
# (mirrors e2e-ui.yml / ci.yml's codex-parity job).
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Download codex-parity sidecar binary
# Prebuilt once by the build-sidecar job. The goal-mode fixture uses this
# (via CODEX_PARITY_SIDECAR_BIN on the pytest step) instead of running a
# multi-minute cargo build on each attempt's critical path.
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
toolchain: stable
name: codex-parity-sidecar
path: .tmp-codex-parity-target/debug
- name: Cache Rust build
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
with:
path: .tmp-codex-parity-target
# Identical key to e2e-ui.yml / ci.yml so a populated cache restores.
key: codex-parity-sidecar-${{ runner.os }}-${{ hashFiles('tests/codex_parity/sidecar/Cargo.lock') }}
- name: Make sidecar binary executable
run: chmod +x .tmp-codex-parity-target/debug/codex-parity-sidecar
- name: Cache Playwright browsers
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -222,20 +272,20 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
cd web
npm ci --legacy-peer-deps --no-audit --no-fund
npm run build
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build
- name: Install Claude Code CLI
# Pinned to match e2e-ui.yml (2.1.170 recognises the native bridge
# hook events). --ignore-scripts then run the audited install.cjs.
# hook events). Runs lifecycle scripts so the platform-specific binary
# is linked and put on PATH.
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR" && cd "$CC_CLI_DIR"
npm install --no-audit --no-fund @anthropic-ai/claude-code@2.1.212
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Install Codex CLI
# @openai/codex pinned to match e2e-ui.yml; goal-mode app-server APIs
@@ -243,9 +293,10 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.codex-cli" && cd "${GITHUB_WORKSPACE}/.codex-cli"
CODEX_CLI_DIR="${RUNNER_TEMP}/omnigent-codex-cli"
mkdir -p "$CODEX_CLI_DIR" && cd "$CODEX_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @openai/codex@0.139.0
echo "${GITHUB_WORKSPACE}/.codex-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CODEX_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Run pytest target
# Inputs validated by prep. Word-splitting on $TEST_TARGET / $EXTRA_ARGS
@@ -258,6 +309,10 @@ jobs:
env:
TEST_TARGET: ${{ github.event.inputs.test_target }}
EXTRA_ARGS: ${{ github.event.inputs.extra_pytest_args }}
# Prebuilt sidecar from build-sidecar; the codex goal-mode fixture uses
# this instead of running cargo build. Absolute path: the fixture
# resolves it as-is, and pytest may run from a different cwd.
CODEX_PARITY_SIDECAR_BIN: ${{ github.workspace }}/.tmp-codex-parity-target/debug/codex-parity-sidecar
run: |
mkdir -p artifacts "artifacts/basetemp-${{ matrix.attempt }}"
# shellcheck disable=SC2086
+30 -15
View File
@@ -1,9 +1,9 @@
# Create a GitHub Release entry (the `…/releases` page) when a version tag is
# pushed. This is METADATA ONLY — it does NOT build or publish any installable
# artifact. PyPI publishing lives in the central secure-release repo
# (databricks/secure-public-registry-releases-eng → `omnigent` workflow), on
# hardened runners with OIDC Trusted Publishing and a mandatory dependency
# scan. Keeping those concerns separate is deliberate (see RELEASING.md):
# artifact. PyPI publishing lives in a Databricks-internal secure-release repo
# (its `omnigent` workflow), on hardened runners with OIDC Trusted Publishing
# and a mandatory dependency scan. Keeping those concerns separate is
# deliberate (see the maintainer release runbook):
#
# * This job runs NO project or third-party code — no build, no `pip
# install`/`npm ci`, no tests. Its only action is SHA-pinned
@@ -28,7 +28,10 @@ on:
push:
tags:
# Version tags only (v0.2.0, v0.2.0rc1, …) — `v[0-9]*` avoids triggering
# on non-release tags like `v-infra-*`.
# on non-release tags like `v-infra-*`. Pre-release tags (rcN / devN /
# preN) still match the glob but are skipped in the job below — no
# GitHub release is created for them; their installable artifacts live
# only on PyPI, and a curated release page is reserved for the final cut.
- "v[0-9]*"
# Least privilege: creating a release requires `contents: write`; nothing here
@@ -43,9 +46,30 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Skip pre-release tags (rcN / devN / preN)
id: tag
env:
TAG: ${{ github.ref_name }}
run: |
# Pre-release tags (rcN / devN / preN) get NO GitHub release — they
# live on PyPI only, and a curated release page is reserved for the
# final cut. The tag glob above still matches them, so gate here.
# A trailing digit is required so a substring like 'dev' or 'pre' in a
# mistyped tag name can't trigger a skip by accident.
case "$TAG" in
*rc[0-9]*|*dev[0-9]*|*pre[0-9]*)
echo "Pre-release tag ${TAG} — not creating a GitHub release (rc/dev/pre releases live on PyPI only)." \
| tee -a "$GITHUB_STEP_SUMMARY"
echo "skip=true" >> "$GITHUB_OUTPUT" ;;
*)
echo "skip=false" >> "$GITHUB_OUTPUT" ;;
esac
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
if: steps.tag.outputs.skip != 'true'
- name: Draft release with a placeholder body
if: steps.tag.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
@@ -57,20 +81,11 @@ jobs:
echo "Release $TAG already exists — skipping." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
# rc / dev / alpha / beta tags are flagged as pre-releases.
pre=""
case "$TAG" in
*rc*|*dev*|*a[0-9]*|*b[0-9]*) pre="--prerelease" ;;
esac
# $pre is intentionally UNQUOTED: it word-splits to nothing when empty,
# and is only ever "" or "--prerelease" (set just above, never from
# external input). Quoting it would pass an empty positional arg.
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--draft \
--verify-tag \
--notes "_Release notes are being drafted automatically — check back shortly._" \
--title "$TAG" \
$pre
--title "$TAG"
echo "Drafted release $TAG — curated notes will be filled in by draft-release-notes.yml; review and publish from the Releases page." \
| tee -a "$GITHUB_STEP_SUMMARY"
+30 -4
View File
@@ -14,13 +14,18 @@ name: Homebrew tap PR
# We trigger on `release: published` (not the tag push) for the same reason as
# publish-changelog.yml: that's the moment the version is installable from PyPI
# — the secure-release repo publishes to PyPI before the GitHub Release goes
# public (see RELEASING.md), so the sdist we pin the formula to actually exists.
# public (see the maintainer release runbook), so the sdist we pin the formula to actually exists.
#
# Cross-repo writes can't use the workflow's own GITHUB_TOKEN (scoped to this
# repo), so we mint a short-lived token from the omnigent-ci GitHub App scoped to
# homebrew-tap — the same App used by publish-changelog.yml / doc-sync.yml. One
# prerequisite: the omnigent-ci App must be installed on omnigent-ai/homebrew-tap
# with contents:write + pull-requests:write.
#
# This is the only workflow that touches the tap. It replaced update-homebrew.yml,
# which regenerated resources with `brew update-python-resources` and asserted on
# hand-maintained stanzas this template no longer emits, so it failed on every
# run while racing this workflow on the same `release: published` event.
on:
release:
@@ -61,14 +66,14 @@ jobs:
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
is_final=true
# Only final vX.Y.Z tags; exclude rc/dev/alpha/beta and the event's
# prerelease flag (homebrew users get stable releases from the tap).
# Only final vX.Y.Z tags; exclude rcN/devN/preN pre-releases and
# the event's prerelease flag (homebrew users get stable releases).
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) is_final=false ;;
esac
case "$tag" in
*rc*|*dev*|*a[0-9]*|*b[0-9]*) is_final=false ;;
*rc[0-9]*|*dev[0-9]*|*pre[0-9]*|*a[0-9]*|*b[0-9]*) is_final=false ;;
esac
if [ "${PRERELEASE}" = "true" ]; then is_final=false; fi
echo "is_final=${is_final}" >> "$GITHUB_OUTPUT"
@@ -86,8 +91,29 @@ jobs:
TAP_REPO: ${{ github.repository_owner }}/homebrew-tap
BRANCH: auto/formula/${{ needs.resolve.outputs.tag }}
steps:
- name: Require admin/maintain role (manual dispatches)
# The release-event path is already gated by the tag checks above; only a
# human dispatch needs an actor-role check, since workflow_dispatch is
# runnable by anyone with write access.
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
role="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${ACTOR}/permission" --jq .role_name)"
case "$role" in
admin|maintain)
echo "Dispatcher ${ACTOR} has role ${role} — authorized." ;;
*)
echo "::error::Release workflows require the admin or maintain role (dispatcher ${ACTOR} has '${role}')."
exit 1 ;;
esac
- name: Checkout omnigent (template + generator)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+100 -25
View File
@@ -13,6 +13,10 @@ name: Issue Triage
# exfiltrate secrets. All GitHub mutations happen in steps the LLM
# cannot influence.
#
# Runs on issue open, and again when someone removes the `needs-info` label —
# the re-triage path reads the reporter's follow-up comments and classifies +
# assigns the issue (re-adding `needs-info` only if it is still too vague).
#
# What the bot does:
# 1. Removes `needs-triage`, adds `triaged`
# 2. Classifies component — one `comp:*` label
@@ -24,12 +28,21 @@ name: Issue Triage
on:
issues:
types: [opened]
# `unlabeled` re-runs triage when someone removes `needs-info` (see the job
# `if:` below) — that removal is the signal the issue now has enough detail
# to classify and assign.
types: [opened, unlabeled]
permissions:
issues: write
contents: read
# One triage run per issue at a time; a newer event supersedes an in-flight one
# (e.g. a re-label right after open won't race with the initial run).
concurrency:
group: issue-triage-${{ github.event.issue.number }}
cancel-in-progress: true
env:
OMNIGENT_SKIP_WEB_UI: "true"
UV_INDEX_URL: https://pypi.org/simple
@@ -39,9 +52,25 @@ jobs:
triage:
runs-on: ubuntu-latest
timeout-minutes: 10
# Skip issues opened by bots to avoid feedback loops.
# Run on:
# - a newly opened issue by a non-bot author (initial triage), OR
# - the `needs-info` label being REMOVED from an open issue (re-triage:
# the removal signals the issue now has enough detail to classify).
# The `unlabeled` path intentionally allows a bot actor: the removal is made
# by the omnigent-ci App (see needs-info-response.yml) whose login ends in
# `[bot]`, and only an App-token/human removal re-triggers at all — this
# workflow's own label edits use the default GITHUB_TOKEN, which never emits
# re-triggering events, so there is no loop to guard against here.
if: >-
!endsWith(github.event.issue.user.login, '[bot]')
(
github.event.action == 'opened' &&
!endsWith(github.event.issue.user.login, '[bot]')
) ||
(
github.event.action == 'unlabeled' &&
github.event.label.name == 'needs-info' &&
github.event.issue.state == 'open'
)
steps:
- name: Check LLM credentials available
id: creds
@@ -107,8 +136,11 @@ jobs:
set -euo pipefail
# Fetch issue metadata to a file — never interpolated into shell.
# `comments` is included so the re-triage path (needs-info removed) can
# see the detail the reporter added in comments, not just the original
# body.
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
--json number,title,body,labels,author \
--json number,title,body,labels,author,comments \
> /tmp/issue.json
# Extract key terms for duplicate search (first 200 chars of title+body).
@@ -179,10 +211,14 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
# Install outside the checked-out tree: a repo-root package.json would
# otherwise capture this bare `npm install` and hoist it there, leaving
# this dir's node_modules empty.
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR" && cd "$CC_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.212
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Set LLM credentials
if: steps.creds.outputs.available == 'true'
@@ -210,7 +246,9 @@ jobs:
if: steps.creds.outputs.available == 'true'
env:
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
OMNIGENT_AGENT_MODEL: ${{ vars.OMNIGENT_CI_FAST_ANTHROPIC_MODEL }}
run: |
: "${OMNIGENT_AGENT_MODEL:?Set OMNIGENT_CI_FAST_ANTHROPIC_MODEL}"
mkdir -p "$HOME/.omnigent"
python3 -c "
import pathlib, os, json
@@ -224,7 +262,7 @@ jobs:
'anthropic': {
'base_url': gw + '/anthropic',
'api_key_ref': 'env:LLM_API_KEY',
'models': {'default': 'databricks-claude-sonnet-4-6'},
'models': {'default': os.environ['OMNIGENT_AGENT_MODEL']},
},
}
}
@@ -252,6 +290,22 @@ jobs:
body = (issue.get("body") or "")[:8192]
labels = [l["name"] for l in issue.get("labels", [])]
# Follow-up comments by the issue author — the reporter often supplies
# the missing detail here, so the re-triage path must read them. Only
# the author's own comments count as clarification (others' comments
# are noise for this purpose and are dropped). Capped to 4 KB total.
author_login = issue.get("author", {}).get("login")
comment_section = "None."
if author_login:
author_comments = [
c.get("body", "")
for c in issue.get("comments", [])
if c.get("author", {}).get("login") == author_login and c.get("body")
]
if author_comments:
joined = "\n\n---\n\n".join(author_comments)[:4096]
comment_section = joined
dupe_section = "None found."
if dupes:
lines = [f"- #{d['number']}: {d['title']}" for d in dupes[:5]]
@@ -269,6 +323,10 @@ jobs:
Body:
{body}
## AUTHOR FOLLOW-UP COMMENTS (UNTRUSTED — later clarification from the reporter)
{comment_section}
## CANDIDATE DUPLICATES
{dupe_section}
@@ -336,6 +394,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
EVENT_ACTION: ${{ github.event.action }}
run: |
set -euo pipefail
@@ -344,7 +403,7 @@ jobs:
# All GitHub mutations are built in Python with proper escaping
# — no eval, no shell interpolation of model output.
python3 <<'PYEOF'
import json, pathlib, sys, shlex
import json, os, pathlib, sys, shlex
raw = pathlib.Path("/tmp/triage_output.txt").read_text()
@@ -369,7 +428,7 @@ jobs:
sys.exit(1)
# Validate fields against allowed values to prevent label injection.
ALLOWED_TYPES = {"bug", "enhancement", "documentation"}
ALLOWED_TYPES = {"bug", "Feature", "Docs"}
# Component labels come from .github/areas.json (single source of truth),
# so the validator can never drift from the area definitions.
ALLOWED_COMPONENTS = set(json.loads(pathlib.Path("/tmp/components.json").read_text()))
@@ -385,12 +444,18 @@ jobs:
dup = None
if result.get("needs_info"):
labels_add.append("needs-info")
if "needs-info" not in existing_labels:
labels_add.append("needs-info")
if "needs-triage" in existing_labels:
labels_remove.append("needs-triage")
# needs-info issues are still triaged — they just need more info.
labels_add.append("triaged")
else:
# No longer needs info. On the re-triage path the label is already
# gone (its removal triggered this run); this is a safety net for
# any case where it lingers.
if "needs-info" in existing_labels:
labels_remove.append("needs-info")
# Type
t = result.get("type")
if t and t in ALLOWED_TYPES:
@@ -413,16 +478,22 @@ jobs:
labels_add.append("help wanted")
# Duplicate — only accept if the issue number is in our
# pre-fetched candidate list (prevents hallucinated refs).
# pre-fetched candidate list (prevents hallucinated refs). Only on
# the initial open: on re-triage we neither re-label nor re-comment
# (the duplicate call was already made at open time), so the label
# and its explanatory comment stay consistent.
dup = result.get("duplicate_of")
candidates = json.loads(
pathlib.Path("/tmp/duplicates.json").read_text()
)
candidate_numbers = {d["number"] for d in candidates}
if dup and isinstance(dup, int) and dup in candidate_numbers:
if (
dup and isinstance(dup, int) and dup in candidate_numbers
and os.environ.get("EVENT_ACTION") == "opened"
):
labels_add.append("duplicate")
else:
dup = None # discard hallucinated duplicate
dup = None # discard hallucinated / re-triage duplicate
if "needs-triage" in existing_labels:
labels_remove.append("needs-triage")
@@ -451,12 +522,12 @@ jobs:
"ranked_owners": ranked_owners,
"duplicate_of": dup if isinstance(dup, int) else None,
"priority": result.get("priority") if result.get("priority") in ALLOWED_PRIORITIES else None,
"needs_info": bool(result.get("needs_info")),
"reasoning": result.get("reasoning", ""),
}
pathlib.Path("/tmp/triage_result.json").write_text(json.dumps(output))
# Build a shell script with properly escaped arguments — no eval.
import os
issue = os.environ["ISSUE_NUMBER"]
repo = os.environ["REPO"]
cmds = []
@@ -470,8 +541,10 @@ jobs:
if labels_add or labels_remove:
cmds.append(" ".join(shlex.quote(a) for a in args))
# Duplicate comment.
if output["duplicate_of"]:
# Duplicate comment — only on the initial open. On the re-triage path
# (needs-info removed) any duplicate note was already posted at open
# time, so we skip it to avoid re-commenting.
if output["duplicate_of"] and os.environ.get("EVENT_ACTION") == "opened":
comment_args = [
"gh", "issue", "comment", issue, "--repo", repo,
"--body", f"Potential duplicate of #{output['duplicate_of']}. React 👎 to contest.",
@@ -503,12 +576,14 @@ jobs:
maintainer_assigned=true
fi
# Otherwise, assign an owner for P0/P1 issues: the least-loaded area
# owner, with LLM rank as a tiebreaker (load primary, rank secondary).
# Symmetric with the PR reviewer path. Skipped if the maintainer-author
# was already assigned above.
priority=$(jq -r '.priority // empty' /tmp/triage_result.json)
if [ "$maintainer_assigned" = "false" ] && { [ "$priority" = "P0-critical" ] || [ "$priority" = "P1-high" ]; }; then
# Otherwise, assign an owner: the least-loaded area owner, with LLM
# rank as a tiebreaker (load primary, rank secondary). Symmetric with
# the PR reviewer path. Skipped if the maintainer-author was already
# assigned above. Every triaged issue gets an owner — the only issues
# left unassigned are needs_info ones (too vague to route until the
# reporter adds detail).
needs_info=$(jq -r '.needs_info // false' /tmp/triage_result.json)
if [ "$maintainer_assigned" = "false" ] && [ "$needs_info" != "true" ]; then
# Open-issue load per candidate (fewest assigned open issues wins ties).
# One trusted query; the LLM never sees GH_TOKEN.
gh issue list --repo "$REPO" --state open --limit 500 \
@@ -520,8 +595,8 @@ jobs:
owners = json.loads(pathlib.Path("/tmp/owners.json").read_text())
# Candidates: the validated ranked owners (LLM preference order). If the
# LLM gave none, fall back to the full owner pool so a P0/P1 is never
# left unassigned — load then picks the least-loaded owner.
# LLM gave none, fall back to the full owner pool so a triaged issue is
# never left unassigned — load then picks the least-loaded owner.
ranked = triage.get("ranked_owners") or []
candidates = ranked if ranked else owners
rank_of = {u: i for i, u in enumerate(ranked)} # unranked -> +inf below
+31 -29
View File
@@ -1,9 +1,10 @@
name: Lint
# Runs the project's pre-commit hooks (ruff, mypy, custom anti-pattern grep
# hooks, etc.) on every non-draft PR and on push to main. Surfaces as the
# `Pre-commit checks` check, a REQUIRED gate entry in merge-ready.yml. Draft PRs
# are skipped; `ready_for_review` refires so the check doesn't strand pending.
# Runs the project's pre-commit hooks (ruff, Pyrefly, TypeScript lint/type-check, custom
# anti-pattern grep hooks, etc.) on every non-draft PR and on push to main.
# Surfaces as the `Pre-commit checks` check, a REQUIRED gate entry in
# merge-ready.yml. Draft PRs are skipped; `ready_for_review` refires so the
# check doesn't strand pending.
on:
pull_request:
@@ -76,35 +77,40 @@ jobs:
# a stale lockfile). Fix locally with `uv lock`.
run: uv sync --locked --extra dev
# Sets up Node 20 and pins npm to the same major that regenerates
# the lockfile in the OSS-regen workflows, so the freshness gate
# below doesn't flake on npm version-skew churn.
- name: Set up Node.js
uses: ./.github/actions/setup-node
# Sets up Node 20 + pnpm, with pnpm dependency caching keyed on the
# workspace lockfile. pnpm is pinned in .github/actions/setup-pnpm.
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install web dependencies
working-directory: web
# Pin the npm registry to the npmjs default.
- name: Install TypeScript dependencies
# Pin the npm registry to the npmjs default; limit installs to packages
# checked here so Electron's large native devDependencies are not fetched.
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: npm ci --legacy-peer-deps
run: pnpm install --frozen-lockfile --filter web --filter omnigent-vscode
# The npm equivalent of the `uv sync --locked` gate above. `npm ci`
# only checks the lockfile is CONSISTENT with package.json; it
# tolerates cosmetic drift (dev/extraneous flags, metadata) that a
# fresh resolution would rewrite. Regenerate the lockfile and fail
# if it differs from the committed one.
- name: Check web/package-lock.json is up to date
working-directory: web
# The pnpm equivalent of the `uv sync --locked` gate above.
# `pnpm install --frozen-lockfile` only checks the lockfile is CONSISTENT
# with package.json; it tolerates cosmetic drift that a fresh resolution
# would rewrite. Regenerate the lockfile and fail if it differs from the
# committed one.
- name: Check pnpm-lock.yaml is up to date
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
npm install --package-lock-only --legacy-peer-deps --no-audit --no-fund
git diff --exit-code package-lock.json || {
echo "::error::web/package-lock.json is out of date. Run 'npm install --package-lock-only --legacy-peer-deps' in web/ and commit the result."
pnpm install --lockfile-only
git diff --exit-code pnpm-lock.yaml || {
echo "::error::pnpm-lock.yaml is out of date. Run 'pnpm install --lockfile-only' at the repo root and commit the result."
exit 1
}
# A `--frozen-lockfile` / lockfile-regen gate can't catch an override that
# pins a version outside a package.json range: overrides outrank
# package.json, so the declared version is silently ignored and the lock
# stays internally consistent for the pin. Check overrides directly.
- name: Check pnpm overrides satisfy declared ranges
run: .venv/bin/python dev/lint/lint_pnpm_override_consistency.py
# ktlint is invoked by the android-ktlint-* pre-commit hooks. The wrapper
# script (web/android/bin/ktlint.sh) exits 0 if ktlint is absent, so we
# install it here before pre-commit runs to ensure the check is enforced.
@@ -122,14 +128,10 @@ jobs:
chmod +x /tmp/ktlint
sudo mv /tmp/ktlint /usr/local/bin/ktlint
- name: Run formatting, lint, and typing checks
- name: Run formatting, lint, and type checks
run: uv run pre-commit run --all-files --show-diff-on-failure
- name: Type-check web
working-directory: web
run: npm run type-check
# The three packages release in lockstep (identical versions + `==` sibling
# The four packages release in lockstep (identical versions + `==` sibling
# pins). Assert agreement on every change so drift from a bad merge or
# cherry-pick — however it happened — is caught before it reaches a release.
version-lockstep:
+1 -1
View File
@@ -27,7 +27,7 @@ on:
pull_request_target:
types: [labeled]
workflow_run:
workflows: [PR Template, CI, Lint, Docker build, E2E UI Tests, E2E Tests, Integration Tests]
workflows: [PR Template, CI, Lint, Docker build, E2E UI Tests, E2E Tests, Integration Tests, UI Snapshot]
types: [completed]
check_run:
types: [completed]
+78
View File
@@ -0,0 +1,78 @@
name: Clear needs-info on author response
# When the issue AUTHOR comments on an issue that carries `needs-info`, remove
# the label — the reporter has (presumably) supplied the missing detail. That
# removal is the signal the rest of the pipeline is built around:
#
# author comments -> this workflow removes `needs-info`
# -> issue-triage.yml's `unlabeled` trigger re-triages
# (reads the follow-up comments, classifies + assigns,
# or re-adds `needs-info` if it is still too vague)
# author never responds -> stale.yml closes the issue after inactivity
#
# CRITICAL: the label MUST be removed with the omnigent-ci App token, not the
# default GITHUB_TOKEN. GitHub does not re-trigger workflows from events made
# by GITHUB_TOKEN, so a default-token removal would NOT fire issue-triage's
# `unlabeled` re-triage. The App token is a distinct actor, so its `unlabeled`
# event does re-trigger. If the App isn't configured, we skip (fail-closed):
# leaving the label is safer than removing it and stranding the issue.
on:
issue_comment:
types: [created]
permissions:
issues: write
concurrency:
group: needs-info-response-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
clear-needs-info:
runs-on: ubuntu-latest
# Only when a NON-bot commenter who IS the issue author comments on an OPEN
# issue (not a PR — issue_comment fires for PRs too) that still carries
# `needs-info`.
if: >-
!endsWith(github.event.sender.login, '[bot]') &&
!github.event.issue.pull_request &&
github.event.issue.state == 'open' &&
github.event.comment.user.login == github.event.issue.user.login &&
contains(github.event.issue.labels.*.name, 'needs-info')
steps:
- name: Mint omnigent-ci App token
id: app-token
if: vars.OMNIGENT_BOT_APP_ID != ''
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.OMNIGENT_BOT_APP_ID }}
private-key: ${{ secrets.OMNIGENT_BOT_APP_KEY }}
- name: Warn when the omnigent-ci App is unconfigured
# The feature no-ops without the App (see above). Surface it so a dormant
# setup is distinguishable from a broken one.
if: steps.app-token.outputs.token == ''
run: echo "::notice::omnigent-ci App not configured; needs-info re-triage is dormant (label left in place)."
- name: Remove needs-info label
# Skip when the App isn't configured: removing with GITHUB_TOKEN would
# not re-trigger re-triage, so the label would just silently vanish.
if: steps.app-token.outputs.token != ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -euo pipefail
# Re-check the live labels before removing: the job gate reads the
# (possibly stale) event payload, and `gh --remove-label` errors on a
# label that is already gone. This keeps the step idempotent under a
# race (e.g. two quick comments, or a concurrent removal).
if gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json labels \
--jq '.labels[].name' | grep -qx needs-info; then
echo "Author responded on #$ISSUE_NUMBER; removing needs-info to re-triage."
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --remove-label needs-info
else
echo "needs-info already cleared on #$ISSUE_NUMBER; nothing to do."
fi
@@ -7,10 +7,13 @@ name: Nightly Failure Monitor
# the maintainer; it comments-and-closes that issue when a later nightly is
# green. A single flake (one red run) is ignored -- the real-LLM legs are
# 429-sensitive -- so only a sustained break pages.
#
# Nightly Release is watched for the same reason: it is fully unattended, so a
# broken cut blocks nobody and consumers just silently stop getting new builds.
on:
workflow_run:
workflows: ["E2E Tests", "E2E UI Tests"]
workflows: ["E2E Tests", "E2E UI Tests", "Nightly Release"]
types: [completed]
permissions:
+259
View File
@@ -0,0 +1,259 @@
# Cut the nightly prerelease build: a stamped, tagged snapshot of main.
#
# Every night (or on manual dispatch) this picks the newest commit on main
# with green CI, stamps the lockstep version to `X.Y.Z.devYYYYMMDD` (today's
# UTC date appended to main's `X.Y.Z.dev0` line), commits that stamp DETACHED
# on top of the base commit, tags it `vX.Y.Z.devYYYYMMDD`, and pushes ONLY the
# tag with the omnigent-ci App token (GITHUB_TOKEN-pushed tags fire no
# workflows; the image build hangs off the tag push).
#
# Deliberately NOT the release.yml flow: no release/vX.Y branch is created,
# bump-version.yml is never dispatched (a nightly must not walk main's
# version), and there is no benchmark gate (benchmark.yml already measures
# main nightly). Downstream is already quiet for dev tags: github-release.yml
# and draft-release-notes.yml skip `*dev[0-9]*` tags, update-check ignores
# dev releases, a default `pip install` never resolves them, and
# oss-publish-images.yml publishes the immutable
# `:vX.Y.Z.devYYYYMMDD` image (only `:latest-rc` follows it, by design).
#
# The datestamp is FIXED-WIDTH (YYYYMMDD, one nightly per UTC day): PEP 440
# compares the dev segment as a plain integer, so a longer stamp would sort
# above every shorter one forever. A same-day re-run finds the tag and no-ops.
#
# Nightlies are NOT published to PyPI. Consumers install straight from the
# tag with uv (scripts/update_nightly.sh resolves and installs the newest
# one), which pins all lockstep packages to the tagged commit.
name: Nightly Release
on:
schedule:
# 04:30 UTC: after the 00:00 UTC scheduled suites drain, well before the
# 07:00 UTC image rebuild (its concurrency is per-SHA, so a tag build
# racing it would cold-race the layer cache).
- cron: "30 4 * * *"
workflow_dispatch:
inputs:
dry_run:
description: "Plan only: print the base commit and version, push nothing."
required: false
type: boolean
default: true
# Nothing here writes with GITHUB_TOKEN; the tag push uses the App token.
permissions:
contents: read
# Share release.yml's group: a nightly cut must never interleave with a real
# release dispatch.
concurrency:
group: release
cancel-in-progress: false
jobs:
plan:
if: github.repository == 'omnigent-ai/omnigent'
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
base_sha: ${{ steps.resolve.outputs.base_sha }}
version: ${{ steps.resolve.outputs.version }}
tag: ${{ steps.resolve.outputs.tag }}
should_cut: ${{ steps.resolve.outputs.should_cut }}
steps:
# Manual dispatches are maintainer-only, same rule as release.yml.
# Scheduled runs have no meaningful actor and skip the check.
- name: Require admin/maintain role (dispatch only)
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
role="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${ACTOR}/permission" --jq .role_name)"
case "$role" in
admin|maintain)
echo "Dispatcher ${ACTOR} has role ${role} — authorized." | tee -a "$GITHUB_STEP_SUMMARY" ;;
*)
echo "::error::Nightly dispatches require the admin or maintain role (dispatcher ${ACTOR} has '${role}')."
exit 1 ;;
esac
- name: Checkout main history and tags
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Resolve base commit, version, and whether to cut
id: resolve
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
DRY_RUN_INPUT: ${{ inputs.dry_run }}
run: |
set -euo pipefail
# Boolean inputs arrive as strings on CLI dispatches and are empty
# on schedule — gate in shell, not in `if:` expressions. A schedule
# fire is always a real run; a dispatch is dry unless explicitly not.
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$DRY_RUN_INPUT" != "false" ]; then
dry_run=true
else
dry_run=false
fi
# Walk main newest-first to the first commit whose CI is complete
# and green. A fixed-hour cron can't demand HEAD be green (the last
# merge of the day is often mid-CI); walking back keeps the nightly
# cadence without ever building a red commit. Check runs from this
# workflow's own runs are excluded (a schedule run parks a pending
# check on the HEAD it triggered from — it must not mask HEAD).
base_sha=""
for sha in $(git rev-list -n 20 origin/main); do
own_runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/nightly-release.yml/runs?head_sha=${sha}&per_page=100" \
--jq '.workflow_runs[].id' 2>/dev/null | paste -sd, -)"
runs="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${sha}/check-runs?per_page=100" \
--paginate --jq '.check_runs[] | [.name, .status, .conclusion // "-", .details_url] | @tsv')"
runs="$(printf '%s' "$runs" | awk -F'\t' -v rel="$own_runs" '
BEGIN { n=split(rel, a, ","); for (i=1; i<=n; i++) if (a[i]!="") own[a[i]]=1 }
{ o=0; for (r in own) if (index($4, "/runs/" r "/")) { o=1; break }
if (!o) print $1 "\t" $2 "\t" $3 }')"
total="$(printf '%s' "$runs" | grep -c . || true)"
pending="$(printf '%s' "$runs" | awk -F'\t' '$2 != "completed"' || true)"
bad="$(printf '%s' "$runs" | awk -F'\t' '$3 ~ /^(failure|timed_out|action_required|startup_failure)$/' || true)"
if [ "$total" -gt 0 ] && [ -z "$bad" ] && [ -z "$pending" ]; then
base_sha="$sha"
break
fi
echo "skipping ${sha}: $([ "$total" -eq 0 ] && echo 'no check runs' || { [ -n "$bad" ] && echo 'failing checks' || echo 'checks still running'; })"
done
should_cut=false
version=""
tag=""
reason=""
if [ -z "$base_sha" ]; then
reason="no commit with completed green CI in the newest 20 on main"
else
# Main must carry the `X.Y.Z.dev0` marker (the repo's versioning
# invariant). The nightly replaces the dev segment with today's
# UTC date; anything else on main is a broken state to fail loudly on.
main_version="$(git show "${base_sha}:pyproject.toml" | sed -n 's/^version = "\(.*\)"$/\1/p' | head -1)"
if ! [[ "$main_version" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.dev0$ ]]; then
echo "::error::main's version at ${base_sha} is '${main_version}', expected X.Y.Z.dev0 — refusing to derive a nightly version."
exit 1
fi
version="${BASH_REMATCH[1]}.dev$(date -u +%Y%m%d)"
tag="v${version}"
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
reason="tag ${tag} already exists (nightly already cut today)"
else
# Skip quiet nights: the newest nightly tag's commit is the
# stamp on top of its base, so parent = the base it built. If
# our base is that commit (or older, after a red-HEAD walk),
# there is nothing new to ship.
last_tag="$(git for-each-ref --sort=-creatordate --format='%(refname:short)' 'refs/tags/v[0-9]*' \
| grep -E '\.dev[0-9]{8}$' | head -1 || true)"
if [ -n "$last_tag" ] && { [ "$base_sha" = "$(git rev-parse "${last_tag}^")" ] \
|| git merge-base --is-ancestor "$base_sha" "$(git rev-parse "${last_tag}^")"; }; then
reason="no new commits on main since ${last_tag}"
elif [ "$dry_run" = "true" ]; then
reason="dry run — would cut ${tag} from ${base_sha}"
else
should_cut=true
fi
fi
fi
{
echo "## Nightly plan"
echo ""
echo "| | |"
echo "| --- | --- |"
echo "| Base commit | \`${base_sha:-—}\` |"
echo "| Version | \`${version:-—}\` |"
echo "| Cutting | ${should_cut} |"
[ -n "$reason" ] && echo "| Reason | ${reason} |"
} >> "$GITHUB_STEP_SUMMARY"
{
echo "base_sha=${base_sha}"
echo "version=${version}"
echo "tag=${tag}"
echo "should_cut=${should_cut}"
} >> "$GITHUB_OUTPUT"
cut:
needs: plan
if: needs.plan.outputs.should_cut == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
env:
# Clean public resolution for `uv lock` — the committed lockfile must
# reference https://pypi.org/simple (never a proxy).
UV_INDEX_URL: https://pypi.org/simple
PIP_INDEX_URL: https://pypi.org/simple
steps:
- name: Mint App token (omnigent)
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.OMNIGENT_BOT_APP_ID }}
private-key: ${{ secrets.OMNIGENT_BOT_APP_KEY }}
owner: ${{ github.repository_owner }}
repositories: omnigent
- name: Checkout base commit
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ needs.plan.outputs.base_sha }}
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
- name: Stamp the lockstep version
env:
VERSION: ${{ needs.plan.outputs.version }}
run: |
set -euo pipefail
uv run --no-project --python 3.12 --with packaging \
python scripts/update_versions.py pre-release --new-version "$VERSION"
uv lock
# uv may emit non-canonical lockfile fields (e.g. size on file
# entries); normalize like the pre-commit fixer, then hard-verify.
python3 scripts/normalize_uv_lock_registry.py uv.lock || true
python3 scripts/normalize_uv_lock_registry.py --check uv.lock
uv run --no-project --python 3.12 --with packaging \
python scripts/update_versions.py check --expect "$VERSION"
- name: Commit, tag, and push the tag
env:
PUSH_TOKEN: ${{ steps.app-token.outputs.token }}
TAG: ${{ needs.plan.outputs.tag }}
run: |
set -euo pipefail
git config user.name "omnigent-ci[bot]"
git config user.email "294685417+omnigent-ci[bot]@users.noreply.github.com"
# Stage everything the stamp touched: update_versions.py owns the
# file set (same staging as release.yml and bump-version.yml).
git add -A
git commit -s -m "nightly: ${TAG}"
git tag "$TAG"
# Tag only — the stamp commit stays off every branch, reachable via
# the tag. The App token push fires the tag-triggered workflows.
push_url="https://x-access-token:${PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push "$push_url" "refs/tags/${TAG}"
echo "Pushed ${TAG} at $(git rev-parse HEAD) (base $(git rev-parse HEAD^))." \
| tee -a "$GITHUB_STEP_SUMMARY"
+8 -5
View File
@@ -50,8 +50,10 @@ permissions:
contents: read
concurrency:
# Key by SHA so back-to-back merges each build; don't cancel mid-push.
group: oss-publish-images-${{ github.sha }}
# One build at a time: rc and final tags land minutes apart at a release cut,
# and built concurrently they race each other's layer cache cold and blow the
# job timeout. Serialized, the later build reuses the earlier one's layers.
group: oss-publish-images
cancel-in-progress: false
jobs:
@@ -67,9 +69,10 @@ jobs:
runs-on: ubuntu-latest
# Multi-arch: the linux/arm64 leg cross-builds under QEMU emulation on this
# amd64 runner, which roughly doubles the host-image build time (emulated
# npm/pip native steps). 30m was tight for two native amd64 builds; give the
# four-variant (server+host × amd64+arm64) build headroom.
timeout-minutes: 60
# npm/pip native steps). A cold-cache four-variant (server+host × amd64+
# arm64) build can exceed 60m, and hitting the timeout loses the release's
# images silently — give it real headroom.
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+24 -28
View File
@@ -1,11 +1,11 @@
# A maintainer comments `/regen` on a PR to regenerate the repo's lockfiles
# (uv.lock + web/package-lock.json) against public PyPI/npm and commit them
# (uv.lock + pnpm-lock.yaml) against public PyPI/npmjs.org and commit them
# ONTO that PR's branch. Use when the PR itself moved a dependency; complements
# oss-regenerate-and-smoke.yml (standalone rolling PR on dispatch).
#
# Two forms:
# /regen re-resolve, preserving existing pins.
# /regen upgrade <pkg> [pkg] additionally force uv to take the newest allowed
# /regen re-resolve BOTH lockfiles, preserving existing pins.
# /regen upgrade <pkg> [pkg] uv.lock ONLY: force uv to take the newest allowed
# version of each named package (uv lock
# --upgrade-package). Use for a transitive pip
# security bump Dependabot can't land on this uv
@@ -164,28 +164,14 @@ jobs:
with:
enable-cache: false
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
# 7-day cooldown comes from uv.toml (`exclude-newer = "P7D"`), recorded as
# a relative span; an env-var cutoff would stamp an absolute date and break
# later `uv sync --locked`. npm's cooldown (web/.npmrc min-release-age=7)
# is only honored by npm >= 11.10.0; node 20 ships npm 10.x which ignores it.
# Pin the EXACT version (not a range) and keep it in lockstep with
# .github/actions/setup-node (npm 11.12.1): this workflow generates the
# lockfile and that action verifies it, so a version gap would fail the
# freshness gate in lint.yml.
- name: Ensure npm honors the dependency cooldown
run: npm install -g npm@11.12.1
# Delete package-lock.json so npm RESOLVES from scratch: min-release-age
# only filters during resolution, and --package-lock-only keeps an existing
# in-range pin without re-applying the cooldown.
# --legacy-peer-deps is REQUIRED and MUST match the flag lint.yml verifies
# with (React 18 runtime vs React 19 peers would otherwise ERESOLVE-fail,
# and a flag mismatch rewrites dev/extraneous flags, failing the gate).
- name: Regenerate lockfiles against public PyPI/npm
# 7-day cooldown comes from uv.toml (`exclude-newer = "P7D"`) and from
# pnpm-workspace.yaml (`minimumReleaseAge: 10080`), recorded as a relative
# span; an env-var cutoff would stamp an absolute date and break later
# `uv sync --locked` / `pnpm install --frozen-lockfile`.
- name: Regenerate lockfiles against public PyPI/npmjs.org
env:
REGEN_MODE: ${{ needs.authorize.outputs.mode }}
UPGRADE_PKGS: ${{ needs.authorize.outputs.pkgs }}
@@ -203,7 +189,17 @@ jobs:
else
uv lock
fi
( cd web && rm -f package-lock.json && npm install --package-lock-only --legacy-peer-deps --no-audit --no-fund )
# uv may emit non-canonical lockfile fields (e.g. size on file
# entries); normalize like the pre-commit fixer, then hard-verify.
python3 scripts/normalize_uv_lock_registry.py uv.lock || true
python3 scripts/normalize_uv_lock_registry.py --check uv.lock
# `/regen upgrade <py pkgs>` is a targeted Python bump: leave the npm
# lockfile alone so the PR diff stays reviewable. Plain `/regen`
# refreshes both lockfiles, as before.
if [ "$REGEN_MODE" != "upgrade" ]; then
rm -f pnpm-lock.yaml
pnpm install --lockfile-only
fi
# Mint the App token only AFTER `uv lock` so untrusted PR build backends
# never see it. Skipped when the App isn't configured (push then falls back
@@ -229,12 +225,12 @@ jobs:
git config user.name "omnigent-ci[bot]"
git config user.email "294685417+omnigent-ci[bot]@users.noreply.github.com"
# --porcelain (not git diff) so first-time UNTRACKED lockfiles count too.
if [ -z "$(git status --porcelain -- uv.lock web/package-lock.json)" ]; then
if [ -z "$(git status --porcelain -- uv.lock pnpm-lock.yaml)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Lockfiles already current — nothing to commit."
exit 0
fi
git add uv.lock web/package-lock.json
git add uv.lock pnpm-lock.yaml
git commit -m "chore(oss): regenerate public lockfiles against public PyPI/npm"
git push "https://x-access-token:${PUSH_TOKEN}@github.com/${REPO}.git" "HEAD:$HEAD_REF"
echo "changed=true" >> "$GITHUB_OUTPUT"
@@ -256,7 +252,7 @@ jobs:
upgraded=" (upgraded: $UPGRADE_PKGS)"
fi
if [ "$CHANGED" = "true" ]; then
base="✅ Regenerated \`uv.lock\`$upgraded + \`web/package-lock.json\` against public PyPI/npm and pushed to this PR."
base="✅ Regenerated \`uv.lock\`$upgraded + \`pnpm-lock.yaml\` against public PyPI/npmjs.org and pushed to this PR."
if [ "$APP_USED" = "true" ]; then
body="$base CI will re-run on the new commit."
else
+65 -41
View File
@@ -1,10 +1,15 @@
# Regenerate the repo's lockfiles against PUBLIC PyPI/npm, then validate via
# Keep the repo's public lockfiles consistent with the manifests, validated via
# a Docker build + CLI smoke. Runs on GitHub-hosted ubuntu-latest so resolution
# sees public registries directly (lockfiles must record public sources, never
# a proxy). Exists because sync PRs land manifest changes without lockfile
# updates and the Dockerfile COPYs web/package-lock.json, so the tree is not
# Docker-buildable until lockfiles are (re)generated here. Runs every 12h (and
# on manual dispatch); opens a PR with any regenerated lockfiles.
# updates and the Dockerfile COPYs pnpm-lock.yaml, so the tree is not
# Docker-buildable until lockfiles are (re)generated here.
#
# Check-first: a consistency check (`uv lock --check` / pnpm `--frozen-lockfile`)
# gates the work. When the lockfiles already satisfy the manifests the job exits
# clean and opens no PR — even if newer in-range versions exist upstream — so
# routine transitive drift never becomes a churn PR. Only a real manifest/lock
# desync triggers regeneration and a PR. Runs every 12h (and on manual dispatch).
name: OSS regenerate lockfiles + smoke
on:
@@ -40,43 +45,62 @@ jobs:
with:
enable-cache: false
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
# 7-day cooldown comes from uv.toml (`exclude-newer = "P7D"`), recorded as
# a relative span; an env-var cutoff would stamp an absolute date and break
# later `uv sync --locked`.
# Check-first: does each lockfile already satisfy its manifests? These
# checks pass on a consistent-but-not-latest lock, so routine in-range
# transitive drift does NOT trigger a regen; only a real manifest/lock
# desync (the case this job exists to catch) flips a flag.
#
# Per-ecosystem flags: gating each regen on its own flag keeps a desync in
# one ecosystem from forcing a from-scratch re-resolve of the other, which
# would reintroduce the in-range transitive churn this job avoids. `any`
# drives the shared token/PR steps below.
- name: Check lockfile consistency
id: check
run: |
drifted_uv=false
drifted_pnpm=false
uv lock --check || drifted_uv=true
# --lockfile-only: verify the lock satisfies the manifests without a
# full node_modules install; --frozen-lockfile fails on any desync.
pnpm install --frozen-lockfile --lockfile-only || drifted_pnpm=true
any=false
if [ "$drifted_uv" = "true" ] || [ "$drifted_pnpm" = "true" ]; then
any=true
fi
echo "drifted_uv=$drifted_uv" >> "$GITHUB_OUTPUT"
echo "drifted_pnpm=$drifted_pnpm" >> "$GITHUB_OUTPUT"
echo "drifted=$any" >> "$GITHUB_OUTPUT"
echo "uv.lock drifted=$drifted_uv, pnpm-lock.yaml drifted=$drifted_pnpm"
# 7-day cooldown comes from uv.toml (`exclude-newer = "P7D"`) and from
# pnpm-workspace.yaml (`minimumReleaseAge: 10080`), recorded as a relative
# span; an env-var cutoff would stamp an absolute date and break later
# `uv sync --locked` / `pnpm install --frozen-lockfile`.
- name: Regenerate uv.lock
if: steps.check.outputs.drifted_uv == 'true'
run: uv lock
# npm's cooldown (web/.npmrc `min-release-age=7`) is only honored by
# npm >= 11.10.0; node 20 ships npm 10.x which silently ignores it.
# Pin the EXACT version (not a range) and keep it in lockstep with
# .github/actions/setup-node: this workflow generates the lockfile and
# that action verifies it, so a version gap would fail the freshness
# gate in lint.yml. 11.12.1 satisfies the >= 11.10.0 cooldown floor.
- name: Ensure npm honors the dependency cooldown
run: npm install -g npm@11.12.1
# Delete the lockfile so npm RESOLVES from scratch: min-release-age only
# filters during resolution, and --package-lock-only keeps an existing
# in-range pin without re-applying the cooldown.
#
# --legacy-peer-deps is REQUIRED: the tree pins React 18 at runtime while
# much of the UI stack (and @types/react) peer-requires React 19, so npm's
# strict resolver would ERESOLVE-fail without it. It MUST match the flag the
# freshness gate in lint.yml verifies with; generating without it resolves
# the peer graph differently and rewrites the dev/devOptional/extraneous
# flags, failing that byte-exact gate.
- name: Regenerate package-lock.json
working-directory: web
# pnpm's cooldown is configured in pnpm-workspace.yaml and respected by
# the workspace root. Delete the lockfile so pnpm RESOLVES from scratch:
# --lockfile-only keeps an existing in-range pin without re-applying the
# cooldown, so we drop it first.
- name: Regenerate pnpm-lock.yaml
if: steps.check.outputs.drifted_pnpm == 'true'
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
rm -f package-lock.json
npm install --package-lock-only --legacy-peer-deps --no-audit --no-fund
rm -f pnpm-lock.yaml
pnpm install --lockfile-only
# Validate BEFORE committing: the Docker build proves the regenerated
# locks + public registries produce a working image.
# Always run, on both paths: when drifted, this validates the freshly
# regenerated locks BEFORE they're committed; when not drifted, it's the
# ongoing 12h proof that the committed locks + public registries still
# build a working image (catches buildability regressions independent of
# manifest state — e.g. a yanked package that still satisfies the lock's
# specifiers, or a Dockerfile change).
- name: Docker build (FE + Python, public registries)
run: docker build -f deploy/docker/Dockerfile -t omnigent-smoke .
@@ -87,7 +111,7 @@ jobs:
# own CI. Skipped when the App isn't configured (falls back to GITHUB_TOKEN).
- name: Mint App token
id: app-token
if: vars.OMNIGENT_BOT_APP_ID != ''
if: steps.check.outputs.drifted == 'true' && vars.OMNIGENT_BOT_APP_ID != ''
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.OMNIGENT_BOT_APP_ID }}
@@ -98,7 +122,7 @@ jobs:
# blocked by the org PR-creation restriction and the PR runs its own CI;
# falls back to GITHUB_TOKEN if the App isn't configured.
- name: Open lockfile-regen PR
if: github.event_name != 'pull_request'
if: steps.check.outputs.drifted == 'true' && github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
@@ -106,15 +130,15 @@ jobs:
git config user.name "omnigent-ci[bot]"
git config user.email "294685417+omnigent-ci[bot]@users.noreply.github.com"
# --porcelain (not git diff) so first-regen UNTRACKED lockfiles count too.
if [ -z "$(git status --porcelain -- uv.lock web/package-lock.json)" ]; then
if [ -z "$(git status --porcelain -- uv.lock pnpm-lock.yaml)" ]; then
echo "Lockfiles already current — nothing to PR."
exit 0
fi
# One rolling branch, force-pushed each run, so regens update a single PR.
BRANCH="automation/oss-lockfile-regen"
git checkout -b "$BRANCH"
git add uv.lock web/package-lock.json
git commit -m "chore(oss): regenerate public lockfiles against public PyPI/npm"
git add uv.lock pnpm-lock.yaml
git commit -m "chore(oss): regenerate public lockfiles against public PyPI/npmjs.org"
git push --force "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$BRANCH"
# An already-open PR just picks up the force-pushed update.
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')" ]; then
@@ -126,7 +150,7 @@ jobs:
# exempts gh from `set -e`, so a non-zero exit hits the else branch.)
if gh pr create --base main --head "$BRANCH" \
--title "chore(oss): regenerate public lockfiles against public PyPI/npm" \
--body "Automated: regenerated uv.lock + web/package-lock.json against public PyPI/npm, validated by a Docker build + omnigent --help smoke (run ${{ github.run_id }}). Merge to keep the public lockfiles current and buildable."; then
--body "Automated: the lockfiles were out of sync with the manifests, so uv.lock + pnpm-lock.yaml were regenerated against public PyPI/npm and validated by a Docker build + omnigent --help smoke (run ${{ github.run_id }}). Merge to keep the public lockfiles consistent and buildable."; then
echo "Opened the regen PR."
else
echo "::warning::Could not open the regen PR automatically (the GITHUB_TOKEN may be disallowed from creating PRs). The branch '$BRANCH' is pushed with the regenerated lockfiles — open the PR by hand:"
+18 -7
View File
@@ -171,19 +171,26 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
# Install outside the checked-out tree: a repo-root package.json would
# otherwise capture this bare `npm install` and hoist it there, leaving
# this dir's node_modules empty.
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR" && cd "$CC_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.212
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Install Codex CLI
if: steps.trigger.outputs.skip != 'true' && steps.creds.outputs.available == 'true'
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.codex-cli" && cd "${GITHUB_WORKSPACE}/.codex-cli"
# Install outside the checked-out tree so a repo-root package.json
# can't capture this bare `npm install` and hoist it away from here.
CODEX_CLI_DIR="${RUNNER_TEMP}/omnigent-codex-cli"
mkdir -p "$CODEX_CLI_DIR" && cd "$CODEX_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @openai/codex@0.139.0
echo "${GITHUB_WORKSPACE}/.codex-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CODEX_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Set LLM credentials
if: steps.trigger.outputs.skip != 'true' && steps.creds.outputs.available == 'true'
@@ -213,7 +220,11 @@ jobs:
if: steps.trigger.outputs.skip != 'true' && steps.creds.outputs.available == 'true'
env:
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
OMNIGENT_ANTHROPIC_MODEL: ${{ vars.OMNIGENT_CI_ANTHROPIC_MODEL }}
OMNIGENT_OPENAI_MODEL: ${{ vars.OMNIGENT_CI_OPENAI_MODEL }}
run: |
: "${OMNIGENT_ANTHROPIC_MODEL:?Set OMNIGENT_CI_ANTHROPIC_MODEL}"
: "${OMNIGENT_OPENAI_MODEL:?Set OMNIGENT_CI_OPENAI_MODEL}"
mkdir -p "$HOME/.omnigent"
# Use python to write the config safely — avoids interpolating
# secrets/URLs into a heredoc where special chars could break YAML.
@@ -231,13 +242,13 @@ jobs:
'anthropic': {
'base_url': gw + '/anthropic',
'api_key_ref': 'env:LLM_API_KEY',
'models': {'default': 'databricks-claude-opus-4-8'},
'models': {'default': os.environ['OMNIGENT_ANTHROPIC_MODEL']},
},
'openai': {
'base_url': host + '/ai-gateway/codex/v1',
'api_key_ref': 'env:LLM_API_KEY',
'wire_api': 'responses',
'models': {'default': 'databricks-gpt-5-5'},
'models': {'default': os.environ['OMNIGENT_OPENAI_MODEL']},
},
}
}
+138 -3
View File
@@ -75,14 +75,14 @@ jobs:
echo "dry_run=${dry_run}" >> "$GITHUB_OUTPUT"
is_final=true
# Only final vX.Y.Z tags; exclude rc/dev/alpha/beta and the
# event's prerelease flag.
# Only final vX.Y.Z tags; exclude rcN/devN/preN pre-releases and
# the event's prerelease flag.
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) is_final=false ;;
esac
case "$tag" in
*rc*|*dev*|*a[0-9]*|*b[0-9]*) is_final=false ;;
*rc[0-9]*|*dev[0-9]*|*pre[0-9]*) is_final=false ;;
esac
if [ "${PRERELEASE}" = "true" ]; then
is_final=false
@@ -264,6 +264,7 @@ jobs:
stderr-file: /tmp/format-stderr.log
gateway-base-url: ${{ secrets.GATEWAY_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
model: ${{ vars.OMNIGENT_CI_ANTHROPIC_MODEL }}
- name: Extract narrative post (fall back to raw body)
if: steps.creds.outputs.available == 'true'
@@ -282,6 +283,130 @@ jobs:
print("::warning::No RELEASE_POST block parsed — publishing the raw release body.")
PYEOF
# Reviewer reference table of the PRs behind each feature and whether each
# already ships a demo video, so a human filling the `TODO` demo placeholders
# can reuse an existing recording instead of re-recording. Grouped to MATCH
# THE POST: when the formatter ran it curated the body down to a few headline
# features and emitted a per-feature RELEASE_POST_PRS map — group by that so
# the table covers only the PRs behind the features that made the post. With
# no map (raw-body fallback), the post keeps every feature, so fall back to
# the raw `## Major new features` / `## Breaking changes` sections. Runs
# unconditionally (helps the fallback too); best-effort, empty table on error.
- name: Build demo-video reference table
id: demotable
continue-on-error: true
working-directory: omnigent
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REPO: ${{ env.SOURCE_REPO }}
run: |
set -euo pipefail
python3 -u <<'PYEOF'
import json, os, pathlib, re, subprocess
repo = os.environ["SOURCE_REPO"]
body = pathlib.Path("/tmp/release_body.md").read_text(encoding="utf-8", errors="replace")
# PR refs from the raw body's FEATURE sections, per section — the set of
# PRs the table is allowed to name (bug fixes are dropped from the post,
# so from the table too), and the fallback grouping when no formatter map.
# Match a feature section by its heading TEXT at any level (the release
# body uses ### here, older ones ##) — startswith, so "Bug fixes &
# hardening" is excluded but "Major new features" matches.
FEATURE = ("major new features", "breaking changes")
ref_re = re.compile(r"#(\d+)")
raw_sections, current, allowed = [], None, set()
for ln in body.splitlines():
h = re.match(r"#{2,}\s+(.*\S)", ln)
if h:
name = h.group(1).strip()
low = name.lower()
current = (name, []) if any(low.startswith(f) for f in FEATURE) else None
if current:
raw_sections.append(current)
continue
if current and ln.lstrip().startswith(("-", "*")):
for n in ref_re.findall(ln):
pr = int(n)
allowed.add(pr)
if pr not in current[1]:
current[1].append(pr)
# Prefer the formatter's curated feature -> PRs map so the table's groups
# match the post's numbered features. The formatter is fed injectable PR
# prose, so validate: keep only pr_refs that appear in the harvested body.
groups = [] # (feature title, [pr, ...])
fmt = pathlib.Path("/tmp/format_out.txt")
if fmt.is_file():
raw = fmt.read_text(encoding="utf-8", errors="replace")
mm = re.search(r"<!--\s*RELEASE_POST_PRS\s*-->(.*?)<!--\s*/RELEASE_POST_PRS\s*-->",
raw, re.DOTALL)
if mm:
try:
for feat in json.loads(mm.group(1).strip()):
if not isinstance(feat, dict):
continue
title = str(feat.get("title", "")).strip()
prs, seen = [], set()
for r in feat.get("pr_refs", []):
try:
n = int(r)
except (TypeError, ValueError):
continue
if n in allowed and n not in seen:
seen.add(n)
prs.append(n)
if title and prs:
groups.append((title, prs))
except json.JSONDecodeError as e:
print(f"::warning::Could not parse RELEASE_POST_PRS — falling back to raw sections: {e}")
if not groups:
print("::notice::No curated feature map — grouping the table by raw release sections.")
groups = [(name, prs) for name, prs in raw_sections if prs]
# Same demo-VIDEO detection as feature-blog.yml: GitHub uploaded-asset
# links, bare video URLs, and <video> tags. Images are NOT counted — the
# placeholder asks for a recording (a screenshot is a weaker fallback).
video_re = re.compile(
r"https?://github\.com/user-attachments/assets/[0-9a-f-]+"
r"|https?://github\.com/[^/\s)]+/[^/\s)]+/assets/\d+/[0-9a-f-]+"
r"|https?://[^\s)]+\.(?:mp4|mov|webm|m4v)\b"
r"|<video\b",
re.IGNORECASE)
def _cell(t):
return (t or "").replace("|", "\\|").replace("\n", " ").strip()
meta_cache = {}
def _meta(pr):
if pr in meta_cache:
return meta_cache[pr]
m = {"title": "", "url": f"https://github.com/{repo}/pull/{pr}", "video": False}
try:
d = json.loads(subprocess.run(
["gh", "pr", "view", str(pr), "--repo", repo,
"--json", "title,body,url"],
capture_output=True, text=True, timeout=60).stdout or "{}")
m["title"] = _cell(d.get("title", ""))
m["url"] = d.get("url") or m["url"]
m["video"] = bool(video_re.search(d.get("body") or ""))
except Exception as e:
print(f"::notice::Could not read metadata for PR #{pr}: {e}")
meta_cache[pr] = m
return m
out, total = [], 0
for name, prs in groups:
out += [f"### {_cell(name)}", "", "| PR | Title | Demo video? |",
"| --- | --- | --- |"]
for pr in prs:
total += 1
m = _meta(pr)
cell = f"[✅ video]({m['url']})" if m["video"] else "—"
out.append(f"| [#{pr}]({m['url']}) | {m['title']} | {cell} |")
out.append("")
pathlib.Path("/tmp/demo_table.md").write_text("\n".join(out).rstrip() + ("\n" if out else ""))
print(f"Built demo table for {total} PR(s) across {len(groups)} feature(s).")
PYEOF
- name: Render the release post to MDX
working-directory: omnigent
run: |
@@ -305,6 +430,10 @@ jobs:
echo '```markdown'; cat /tmp/site_body.md; echo '```'
echo "### Rendered \`app/releases/${VERSION}/page.mdx\`"
echo '```mdx'; cat /tmp/site_page/page.mdx; echo '```'
if [ -s /tmp/demo_table.md ]; then
echo "### Source PRs — demo videos (for the \`TODO\` demo placeholders)"
cat /tmp/demo_table.md
fi
} | tee -a "$GITHUB_STEP_SUMMARY"
echo "Dry-run: no token minted, no PR opened."
@@ -354,6 +483,11 @@ jobs:
exit 0
fi
body="$(printf 'Publishes the **%s** release post at `/releases/%s` — the curated GitHub Release notes reformatted into the site'"'"'s narrative, prose-driven style.\n\nGenerated by omnigent `.github/workflows/publish-changelog.yml`. Edit the GitHub Release, not this file.' "$TAG" "$VERSION")"
# Append the demo-video reference table: which feature PRs already ship a
# recording a reviewer can drop into the post'"'"'s `TODO` demo placeholders.
if [ -s /tmp/demo_table.md ]; then
body="$(printf '%s\n\n### Source PRs — demo videos\nCheck a ✅ PR for a recording to replace a `TODO` demo placeholder in the post.\n\n%s' "$body" "$(cat /tmp/demo_table.md)")"
fi
gh pr create \
--repo "$SITE_REPO" \
--base main \
@@ -435,5 +569,6 @@ jobs:
/tmp/format-stderr.log
/tmp/format_out.txt
/tmp/site_body.md
/tmp/demo_table.md
retention-days: 7
if-no-files-found: ignore
+8 -11
View File
@@ -71,24 +71,21 @@ jobs:
with:
enable-cache: false
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
# 1. Build the web UI FIRST into the package tree, clean. Ordering is
# load-bearing: the wheel packages on-disk files, so the bundle must
# exist before `uv build`. `rm -rf` backstops Vite's emptyOutDir
# against stale bundles; `npm ci` installs the exact locked deps.
# `--legacy-peer-deps` matches how web's lockfile is generated and
# validated everywhere else (lint, e2e-ui, web-tests, the regen
# jobs) — required for the React 19 peer conflict; without it `npm ci`
# rejects the lockfile ("Missing: yaml@1.10.3 from lock file").
# against stale bundles; `pnpm install --frozen-lockfile --filter web`
# installs the exact locked deps for the web workspace package.
- name: Build web UI (clean, fresh)
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
rm -rf omnigent/server/static/web-ui
npm --prefix web ci --legacy-peer-deps
npm --prefix web run build # Vite outDir -> omnigent/server/static/web-ui
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build # Vite outDir -> omnigent/server/static/web-ui
# 2. Tag-driven: the tag must match the version in all three pyprojects
# and the core package's `==` sibling-SDK pins, so the lockstep
+36 -12
View File
@@ -11,7 +11,7 @@
# oss-publish-images.yml) hangs off the tag push.
#
# PyPI publishing does NOT happen here — after this run, dispatch the secure
# release repo on the tag (see RELEASING.md). Everything here is idempotent:
# release repo on the tag (see the maintainer release runbook). Everything here is idempotent:
# re-dispatch with identical inputs after any failure and it converges
# (branch exists -> reused; version stamped -> no new commit; tag at the
# converged commit -> no-op; tag anywhere else -> loud failure).
@@ -103,13 +103,13 @@ jobs:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
# Final X.Y.Z or a PEP 440 pre-release (a/b/rc). No dev/post here.
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]]; then
# Final X.Y.Z or a PEP 440 pre-release (rc). No dev/post/alpha/beta here.
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+)?$ ]]; then
echo "::error::Invalid release version: ${VERSION} (expect 0.6.0 or 0.6.0rc1)"; exit 1
fi
major="${VERSION%%.*}"; rest="${VERSION#*.}"; minor="${rest%%.*}"
prerelease=false
case "$VERSION" in *a[0-9]*|*b[0-9]*|*rc[0-9]*) prerelease=true ;; esac
case "$VERSION" in *rc[0-9]*) prerelease=true ;; esac
{
echo "version=${VERSION}"
echo "tag=v${VERSION}"
@@ -164,7 +164,7 @@ jobs:
echo "Tag ${TAG} already at the converged release commit ${base_sha} — nothing to do." \
| tee -a "$GITHUB_STEP_SUMMARY"
else
echo "::error::Tag ${TAG} already exists at ${tag_sha} (stamped version: ${stamped:-unknown}), which is not the converged branch head ${base_sha}. Delete the tag first if this is recovery (see RELEASING.md)."
echo "::error::Tag ${TAG} already exists at ${tag_sha} (stamped version: ${stamped:-unknown}), which is not the converged branch head ${base_sha}. Delete the tag first if this is recovery (see the maintainer release runbook)."
exit 1
fi
fi
@@ -600,6 +600,10 @@ jobs:
uv run --no-project --python 3.12 --with packaging \
python scripts/update_versions.py pre-release --new-version "$VERSION"
uv lock
# uv may emit non-canonical lockfile fields (e.g. size on file
# entries); normalize like the pre-commit fixer, then hard-verify.
python3 scripts/normalize_uv_lock_registry.py uv.lock || true
python3 scripts/normalize_uv_lock_registry.py --check uv.lock
fi
uv run --no-project --python 3.12 --with packaging \
python scripts/update_versions.py check --expect "$VERSION"
@@ -615,8 +619,10 @@ jobs:
git config user.name "omnigent-ci[bot]"
git config user.email "294685417+omnigent-ci[bot]@users.noreply.github.com"
git add pyproject.toml sdks/python-client/pyproject.toml sdks/ui/pyproject.toml \
omnigent/version.py uv.lock
# Stage everything the stamp touched: update_versions.py owns the
# file set, and a hand-kept path list here goes stale whenever a
# package joins the lockstep (bump-version.yml stages the same way).
git add -A
if git diff --cached --quiet; then
echo "No version changes to commit (already stamped)."
else
@@ -639,15 +645,18 @@ jobs:
{
echo "## Next steps"
echo ""
echo "1. Dispatch the secure-release repo on this tag:"
# Run summaries are world-readable on a public repo, so the
# secure-release repo is a placeholder here; the runbook names it.
echo "1. Dispatch the secure-release repo on this tag. Substitute the repo"
echo " name from the maintainer release runbook, then run:"
echo ' ```'
echo " gh workflow run omnigent.yml --repo databricks/secure-public-registry-releases-eng \\"
echo " gh workflow run omnigent.yml --repo <secure-release-repo> \\"
echo " -f ref=${TAG} -f destination=pypi -f dry-run=true # gates rehearsal"
echo " gh workflow run omnigent.yml --repo databricks/secure-public-registry-releases-eng \\"
echo " gh workflow run omnigent.yml --repo <secure-release-repo> \\"
echo " -f ref=${TAG} -f destination=pypi -f dry-run=false # real publish"
echo ' ```'
if [ "$PRERELEASE" = "true" ]; then
echo "2. Validate the rc from PyPI (see RELEASING.md). The GitHub draft for ${TAG} stays unpublished."
echo "2. Validate the rc from PyPI (see the maintainer release runbook). No GitHub release is created for rc tags (rcs live on PyPI only) — skip straight to the next rc or the final cut."
else
echo "2. Merge the CHANGELOG PR, curate the ${TAG} draft notes, then dispatch finalize-release.yml (tag=${TAG})."
fi
@@ -657,7 +666,6 @@ jobs:
# never re-freezes and doc-sync keeps deriving the right X.Y-docs branch.
bump-main:
needs: [authorize, plan, cut]
if: ${{ !inputs.dry_run && needs.plan.outputs.branch_exists == 'false' }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
@@ -667,8 +675,24 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.plan.outputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
BRANCH_EXISTS: ${{ needs.plan.outputs.branch_exists }}
run: |
set -euo pipefail
# Gate in shell, not a job-level `if`: a CLI/API dispatch delivers
# boolean inputs as the STRING "false", which is truthy in an
# expression, so `!inputs.dry_run` silently skipped this job at the
# v0.7.0 cut. Shell string comparison is dispatch-channel-proof and
# logs its decision instead of vanishing from the run.
if [ "$DRY_RUN" = "true" ]; then
echo "Dry run — not dispatching the main bump." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$BRANCH_EXISTS" != "false" ]; then
echo "Release branch pre-existed (not the first cut of this cycle) — main bump not needed." \
| tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
# A cut below main's current line (a throwaway rehearsal rc, or
# resurrecting an old series for a backport) must not walk main's
# version backwards.
+134
View File
@@ -0,0 +1,134 @@
# A contributor comments `/rerun` on a PR to re-run its failed CI **on the
# existing head commit** -- no empty commit, no rebase, so no push event and
# thus no dismissed approvals (branch protection keeps dismiss-stale-reviews
# on to block approve-then-swap). Use for flaky-test recovery instead of
# pushing a throwaway commit to re-trigger checks.
#
# CI here runs entirely against the in-process mock LLM (no gateway spend), so
# a re-run costs only Actions minutes; `cancel-in-progress` on each suite caps
# concurrent burn. Polly AI Review (the one real-LLM path) is gated by
# maintainer approval elsewhere and is intentionally NOT re-run here.
#
# Authorization: the PR author (so fork contributors can re-run their own PR)
# OR a write-access commenter (OWNER/MEMBER/COLLABORATOR). `issue_comment` runs
# from the base repo, so its token is writable even for fork PRs and is not
# held behind the fork-approval gate -- unlike `pull_request_target`, this
# needs no privileged `workflow_run` relay (cf. rerun-security-gate*.yml).
name: Rerun CI on /rerun comment
on:
issue_comment:
types: [created]
# Read-only at the top level; write scopes live on the job below.
permissions:
contents: read
concurrency:
# One re-run in flight per PR; a second `/rerun` supersedes the first.
group: rerun-ci-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
rerun:
name: Re-run failed CI for the PR head
permissions:
actions: write # gh run rerun
pull-requests: read # resolve the PR head SHA
issues: write # react to the comment + post the result
# PR comment, body starts with `/rerun`, not a bot, in this repo, AND the
# commenter is the PR author or has write access. `issue.user.login` is the
# PR author; `comment.user.login` is the commenter.
if: >-
github.repository == 'omnigent-ai/omnigent'
&& github.event.issue.pull_request != null
&& startsWith(github.event.comment.body, '/rerun')
&& !endsWith(github.actor, '[bot]')
&& (
github.event.comment.author_association == 'OWNER'
|| github.event.comment.author_association == 'MEMBER'
|| github.event.comment.author_association == 'COLLABORATOR'
|| github.event.comment.user.login == github.event.issue.user.login
)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# The job `if` startsWith() also matches `/rerunfoo`; re-validate `/rerun`
# as a command (first non-space token is exactly `/rerun`, optional args).
- name: Validate command
id: cmd
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
if ! grep -qE '^[[:space:]]*/rerun([[:space:]]|$)' <<<"$COMMENT_BODY"; then
echo "::notice::Comment mentions '/rerun' but not as a command; skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Acknowledge
if: steps.cmd.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
gh api "repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
-f content=eyes --silent || true
- name: Re-run failed CI runs for the PR head
if: steps.cmd.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
# Integer from the payload, but sanitised to digits before shell use.
PR_NUMBER: ${{ github.event.issue.number }}
run: |
set -euo pipefail
PR_NUMBER="$(tr -dc '0-9' <<<"$PR_NUMBER")"
[ -n "$PR_NUMBER" ] || { echo "::error::Empty PR number."; exit 1; }
# Resolve the PR's CURRENT head SHA (a push could have superseded any
# SHA recorded at comment time).
SHA="$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.head.sha')"
echo "PR #$PR_NUMBER head $SHA"
# Latest run per workflow for this SHA, restricted to `pull_request`
# events -- this is the test-suite set (CI, Lint, E2E, E2E UI,
# Integration, Docker build, web Tests). It deliberately EXCLUDES the
# merge machinery (Merge Ready, Maintainer Approval, Polly) which run
# on pull_request_target / workflow_run / issue_comment, so `/rerun`
# never re-triggers a gate or the real-LLM review.
mapfile -t FAILED < <(
gh api "repos/$REPO/actions/runs?head_sha=$SHA&per_page=100" --paginate \
--jq '[.workflow_runs[] | select(.event=="pull_request")]
| group_by(.name)
| map(sort_by(.created_at) | last)
| .[] | select(.conclusion=="failure")
| "\(.id)\t\(.name)"'
)
if [ "${#FAILED[@]}" -eq 0 ]; then
echo "No failed pull_request CI runs for $SHA."
gh pr comment "$PR_NUMBER" --repo "$REPO" \
--body "🔁 \`/rerun\`: no failed CI runs on the current head (\`${SHA:0:7}\`) to re-run. If a check is stuck *pending*, it needs a push or a maintainer, not a re-run."
exit 0
fi
RERAN=""
while IFS=$'\t' read -r id name; do
[ -n "$id" ] || continue
echo "• Re-running failed jobs in '$name' (run $id)"
# --failed: re-run only the failed jobs (cheapest path for a flake).
# --repo is REQUIRED: this job has no checkout, so `gh run rerun`
# cannot infer the repo from a git remote and would fail client-side.
if gh run rerun "$id" --repo "$REPO" --failed; then
RERAN="$RERAN"$'\n'"- $name"
else
echo "::warning::Could not re-run '$name' (run $id) -- may be in progress."
RERAN="$RERAN"$'\n'"- $name ⚠️ (skipped: already running or not re-runnable)"
fi
done < <(printf '%s\n' "${FAILED[@]}")
NOTE="The \`Merge Ready\` gate re-evaluates automatically when these complete."
gh pr comment "$PR_NUMBER" --repo "$REPO" \
--body "🔁 \`/rerun\`: re-running failed jobs on \`${SHA:0:7}\`:${RERAN}"$'\n\n'"$NOTE"
+10 -4
View File
@@ -195,10 +195,14 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
mkdir -p "${GITHUB_WORKSPACE}/.cc-cli" && cd "${GITHUB_WORKSPACE}/.cc-cli"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.170
# Install outside the checked-out tree: a repo-root package.json would
# otherwise capture this bare `npm install` and hoist it there, leaving
# this dir's node_modules empty.
CC_CLI_DIR="${RUNNER_TEMP}/omnigent-cc-cli"
mkdir -p "$CC_CLI_DIR" && cd "$CC_CLI_DIR"
npm install --ignore-scripts --no-audit --no-fund @anthropic-ai/claude-code@2.1.212
node node_modules/@anthropic-ai/claude-code/install.cjs
echo "${GITHUB_WORKSPACE}/.cc-cli/node_modules/.bin" >> "$GITHUB_PATH"
echo "$CC_CLI_DIR/node_modules/.bin" >> "$GITHUB_PATH"
- name: Write gateway profile (~/.databrickscfg)
if: steps.creds.outputs.available == 'true'
@@ -222,7 +226,9 @@ jobs:
if: steps.creds.outputs.available == 'true'
env:
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
OMNIGENT_AGENT_MODEL: ${{ vars.OMNIGENT_CI_FAST_ANTHROPIC_MODEL }}
run: |
: "${OMNIGENT_AGENT_MODEL:?Set OMNIGENT_CI_FAST_ANTHROPIC_MODEL}"
mkdir -p "$HOME/.omnigent"
python3 -c "
import pathlib, os, json
@@ -235,7 +241,7 @@ jobs:
'anthropic': {
'base_url': gw + '/anthropic',
'api_key_ref': 'env:LLM_API_KEY',
'models': {'default': 'databricks-claude-sonnet-4-6'},
'models': {'default': os.environ['OMNIGENT_AGENT_MODEL']},
},
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ name: Backwards-Compat
# suites, over the FULL pairwise (server, runner) version matrix.
#
# The version universe is `main` (the checked-out code = client + tests, always)
# plus every non-rc release tag; we cross every server version with every runner
# plus every final (non-prerelease) release tag; we cross every server version with every runner
# version. Each cell pins the server and/or runner subprocess to that build
# (a "main" axis value leaves that component on the checked-out code) while the
# client and tests stay on main. The (main, main) cell is omitted — it pins
@@ -18,13 +18,13 @@ name: Backwards-Compat
#
# Triggers:
# workflow_dispatch manual; optional `versions` CSV overrides the set.
# schedule every 12h; full pairwise over main + all non-rc tags.
# schedule every 12h; full pairwise over main + all final tags.
on:
workflow_dispatch:
inputs:
versions:
description: "Comma-separated version set for BOTH axes (e.g. 'main,v0.2.0'). Empty = main + all non-rc tags."
description: "Comma-separated version set for BOTH axes (e.g. 'main,v0.2.0'). Empty = main + all final (non-prerelease) tags."
required: false
default: ""
schedule:
+118
View File
@@ -0,0 +1,118 @@
name: Sync PR Priority
# Mirrors a linked issue's priority label (P0-P3) onto the PR that closes it.
# Only closing links (closes/fixes/resolves #n) count -- a plain "related to
# #n" mention is ignored. Runs on PR events and re-syncs when an issue's labels
# change. Uses only default-branch code and never checks out or executes PR
# files; it edits labels through the API.
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
- ready_for_review
issues:
types:
- labeled
- unlabeled
workflow_dispatch:
inputs:
pr_number:
description: "PR number to sync"
required: true
permissions:
contents: read
concurrency:
group: sync-pr-priority-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number || github.ref }}
cancel-in-progress: true
jobs:
sync:
# On issue label events, only react when the changed label is a priority
# (P0-P3) label; other label edits (Bug, triaged, ...) never affect sync.
if: >-
github.repository == 'omnigent-ai/omnigent' &&
(github.event_name != 'issues' ||
contains(fromJSON('["P0-critical", "P1-high", "P2-medium", "P3-low"]'), github.event.label.name))
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout default-branch script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: .github/scripts/sync_pr_priority.py
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
# For issue label events, find open PRs that close this issue so we can
# re-sync each of them. For PR / dispatch events we already know the PR.
- name: Resolve PR numbers to sync
id: resolve
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
PR_FROM_PR: ${{ github.event.pull_request.number }}
PR_FROM_DISPATCH: ${{ github.event.inputs.pr_number }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -euo pipefail
if [[ "${EVENT_NAME}" == "issues" ]]; then
# Only relevant when a priority label was (un)set on the issue.
# On lookup failure, warn and treat as no PRs rather than failing
# the whole workflow.
if ! prs=$(
gh api graphql -f query='
query($owner:String!,$name:String!,$number:Int!){
repository(owner:$owner,name:$name){
issue(number:$number){
closedByPullRequestsReferences(first:50, includeClosedPrs:false){
nodes { number }
}
}
}
}' \
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F number="${ISSUE_NUMBER}" \
--jq '.data.repository.issue.closedByPullRequestsReferences.nodes[].number'
); then
echo "::warning::Failed to resolve PRs closing issue #${ISSUE_NUMBER}; skipping sync."
prs=""
fi
elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
prs="${PR_FROM_DISPATCH}"
else
prs="${PR_FROM_PR}"
fi
echo "prs<<EOF" >> "${GITHUB_OUTPUT}"
echo "${prs}" >> "${GITHUB_OUTPUT}"
echo "EOF" >> "${GITHUB_OUTPUT}"
- name: Sync priority labels
if: steps.resolve.outputs.prs != ''
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PRS: ${{ steps.resolve.outputs.prs }}
run: |
set -euo pipefail
while read -r pr; do
[[ -z "${pr}" ]] && continue
echo "Syncing PR #${pr}"
PR_NUMBER="${pr}" python3 .github/scripts/sync_pr_priority.py
done <<< "${PRS}"
+5 -4
View File
@@ -106,7 +106,7 @@ jobs:
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- uses: ./.github/actions/setup-node
- uses: ./.github/actions/setup-pnpm
- name: Build wheels (no UI)
# Build the wheels WITHOUT the SPA so they stay small (Databricks Apps
@@ -120,10 +120,11 @@ jobs:
run: bash deploy/databricks/build.sh
- name: Build UI
working-directory: web
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
npm ci --legacy-peer-deps --no-audit --no-fund
npm run build
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build
- name: Package UI assets
run: |
+14 -5
View File
@@ -80,8 +80,18 @@ jobs:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- name: Set up Node 20
uses: ./.github/actions/setup-node
# The pinned Playwright image doesn't ship Node, so install Node before
# the pnpm action (pnpm/action-setup's self-installer needs a Node binary).
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.15.1
standalone: true
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
@@ -106,9 +116,8 @@ jobs:
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
cd web
npm ci --legacy-peer-deps --no-audit --no-fund
npm run build
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build
- name: Compare the baselines (no --update-snapshots)
# Deliberately NOT --update-snapshots: that rewrites EVERY PNG, churning
+17 -9
View File
@@ -95,7 +95,7 @@ jobs:
if ! files=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[].filename'); then
echo "ui=true" >> "$GITHUB_OUTPUT"; echo "file list unavailable -> render"; exit 0
fi
pattern='^(web/|tests/e2e_ui/visual/|tests/e2e_ui/conftest\.py|\.github/actions/setup-node/|\.github/workflows/ui-snapshot\.yml|pyproject\.toml|uv\.lock)'
pattern='^(web/|tests/e2e_ui/visual/|tests/e2e_ui/conftest\.py|\.github/actions/setup-pnpm/|\.github/workflows/ui-snapshot\.yml|pnpm-lock\.yaml|pnpm-workspace\.yaml|pyproject\.toml|uv\.lock)'
if printf '%s\n' "$files" | grep -qE "$pattern"; then
echo "ui=true" >> "$GITHUB_OUTPUT"
echo "render-affecting files changed:"
@@ -106,7 +106,7 @@ jobs:
fi
ui-snapshot:
name: UI Snapshot (visual baselines) [non-blocking]
name: UI Snapshot (visual baselines)
needs: detect
# Skipped (not failed) when no render input changed -> reports SUCCESS, so a
# non-UI PR neither runs the render nor blocks a required check.
@@ -130,8 +130,18 @@ jobs:
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up Node 20
uses: ./.github/actions/setup-node
# The pinned Playwright image doesn't ship Node, so install Node before
# the pnpm action (pnpm/action-setup's self-installer needs a Node binary).
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.15.1
standalone: true
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
@@ -156,14 +166,12 @@ jobs:
- name: Build web SPA
# Build BEFORE pytest: Vite's emptyOutDir clobbers the static dir, so
# never run it alongside the live server. --legacy-peer-deps avoids
# re-resolving the known React 19 peer conflict under @emoji-mart/react.
# never run it alongside the live server.
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
cd web
npm ci --legacy-peer-deps --no-audit --no-fund
npm run build
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build
- name: Compare (PR) or regenerate (dispatch) the visual snapshots
id: snapshot
-247
View File
@@ -1,247 +0,0 @@
# Open the omnigent-ai/homebrew-tap version-bump PR when a FINAL release is
# published (designs/RELEASE-AUTOMATION.md). This is the missing link that let
# the tap freeze while PyPI moved on: the tap already builds bottles on every
# PR (brew test-bot) and publishes them on the `pr-pull` label — nobody was
# opening the bump PR.
#
# What it does: wait for the new sdist on PyPI, rewrite the formula's
# url/sha256 (dropping any bottle `revision`), regenerate the pinned Python
# resources with `brew update-python-resources`, sanity-check that the
# hand-maintained sections survived, and open the tap PR. A human reviews the
# resource diff and applies `pr-pull`; the tap's own automation bottles and
# merges. The omnigent-desktop cask is `version :latest` and needs nothing.
#
# Pre-releases never reach the tap. The `release: published` trigger fires
# from finalize-release.yml's App-token publish; `workflow_dispatch` covers
# retries and catch-up (e.g. jumping the formula straight to the newest
# version after a missed cycle).
name: Update Homebrew tap
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Final release tag to bump the tap to, e.g. v0.6.0."
required: true
type: string
permissions:
contents: read
concurrency:
group: update-homebrew-${{ github.event.release.tag_name || inputs.tag }}
cancel-in-progress: false
jobs:
resolve:
name: Resolve release tag
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
tag: ${{ steps.r.outputs.tag }}
is_final: ${{ steps.r.outputs.is_final }}
steps:
- name: Resolve tag and finality
id: r
env:
EVENT_TAG: ${{ github.event.release.tag_name }}
PRERELEASE: ${{ github.event.release.prerelease }}
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
tag="${INPUT_TAG:-$EVENT_TAG}"
is_final=true
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) is_final=false ;;
esac
case "$tag" in
*rc*|*dev*|*a[0-9]*|*b[0-9]*) is_final=false ;;
esac
if [ "${PRERELEASE}" = "true" ]; then
is_final=false
fi
{
echo "tag=${tag}"
echo "is_final=${is_final}"
} >> "$GITHUB_OUTPUT"
echo "Resolved tag=${tag} is_final=${is_final}" | tee -a "$GITHUB_STEP_SUMMARY"
bump:
name: Open tap bump PR
needs: resolve
# Canonical repo only; skip cleanly where the App isn't configured. The
# release-event path is already gated by finalize-release's environment
# approval; only manual dispatches need the role check below.
if: >-
needs.resolve.outputs.is_final == 'true' &&
github.repository == 'omnigent-ai/omnigent' &&
vars.OMNIGENT_BOT_APP_ID != ''
# macOS: `brew update-python-resources` evaluates the formula (with its
# on_macos blocks) in a real Homebrew.
runs-on: macos-latest
timeout-minutes: 30
env:
TAG: ${{ needs.resolve.outputs.tag }}
TAP_REPO: ${{ github.repository_owner }}/homebrew-tap
steps:
- name: Require admin/maintain role (manual dispatches)
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
role="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${ACTOR}/permission" --jq .role_name)"
case "$role" in
admin|maintain)
echo "Dispatcher ${ACTOR} has role ${role} — authorized." ;;
*)
echo "::error::Release workflows require the admin or maintain role (dispatcher ${ACTOR} has '${role}')."
exit 1 ;;
esac
- name: Wait for the sdist on PyPI
id: sdist
run: |
set -euo pipefail
version="${TAG#v}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
for _ in $(seq 1 30); do
if json="$(curl -fsS "https://pypi.org/pypi/omnigent/${version}/json" 2>/dev/null)"; then
url="$(printf '%s' "$json" | jq -r '.urls[] | select(.packagetype == "sdist") | .url')"
sha="$(printf '%s' "$json" | jq -r '.urls[] | select(.packagetype == "sdist") | .digests.sha256')"
if [ -n "$url" ] && [ -n "$sha" ]; then
{
echo "url=${url}"
echo "sha=${sha}"
} >> "$GITHUB_OUTPUT"
echo "sdist for ${version}: ${url}"
exit 0
fi
fi
echo "omnigent==${version} not visible on PyPI yet — retrying in 20s…"
sleep 20
done
echo "::error::omnigent==${version} never appeared on PyPI (is the secure-repo publish done?)."
exit 1
- name: Mint App token (homebrew-tap)
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.OMNIGENT_BOT_APP_ID }}
private-key: ${{ secrets.OMNIGENT_BOT_APP_KEY }}
owner: ${{ github.repository_owner }}
repositories: homebrew-tap
- name: Checkout the tap
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ env.TAP_REPO }}
token: ${{ steps.app-token.outputs.token }}
path: tap
persist-credentials: false
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@18fcb8e3e06b4247c676c506750dc95ea7226479 # 2026-07-10
with:
token: ${{ github.token }}
- name: Rewrite the formula's stable url/sha256
working-directory: tap
env:
SDIST_URL: ${{ steps.sdist.outputs.url }}
SDIST_SHA: ${{ steps.sdist.outputs.sha }}
run: |
set -euo pipefail
python3 - <<'PYEOF'
import os, pathlib, re
path = pathlib.Path("Formula/omnigent.rb")
text = path.read_text(encoding="utf-8")
# The formula's own url/sha256 sit at 2-space indent; resource and
# bottle entries are deeper, so first-match at this indent is safe.
text, n_url = re.subn(r'(?m)^ url ".*"$', f' url "{os.environ["SDIST_URL"]}"', text, count=1)
text, n_sha = re.subn(r'(?m)^ sha256 ".*"$', f' sha256 "{os.environ["SDIST_SHA"]}"', text, count=1)
text, _ = re.subn(r'(?m)^ revision \d+\n', "", text, count=1)
assert n_url == 1 and n_sha == 1, f"unexpected formula shape (url={n_url}, sha={n_sha})"
path.write_text(text, encoding="utf-8")
PYEOF
git diff --stat
- name: Regenerate the pinned Python resources
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_INSTALL_FROM_API: "1"
run: |
set -euo pipefail
# Make the checkout visible to brew as the real tap.
tap_root="$(brew --repository)/Library/Taps/omnigent-ai"
mkdir -p "$tap_root"
ln -sfn "${GITHUB_WORKSPACE}/tap" "${tap_root}/homebrew-tap"
# Excluded packages stay hand-maintained in the formula: the brewed
# deps (certifi/cryptography/pydantic/rpds-py and their transitive
# cffi/pycparser) and the platform-conditional google-antigravity
# wheel stanzas.
brew update-python-resources \
--exclude-packages=certifi,cryptography,pydantic,rpds-py,cffi,pycparser,google-antigravity \
omnigent-ai/tap/omnigent
brew style omnigent-ai/tap/omnigent
- name: Assert the hand-maintained sections survived
working-directory: tap
run: |
set -euo pipefail
fail=0
for needle in 'resource "google-antigravity"' 'depends_on "pydantic"' 'depends_on "cryptography"'; do
if ! grep -qF "$needle" Formula/omnigent.rb; then
echo "::error::update-python-resources dropped: ${needle} — fix the formula by hand this cycle."
fail=1
fi
done
[ "$fail" -eq 0 ]
# The lockstep siblings must have moved with the release. Match the
# sdist filename (PEP 503-normalized name + version) in the resource
# url, not a bare version substring.
version="${TAG#v}"
for sib in omnigent-client omnigent-ui-sdk; do
if ! grep -A2 "resource \"${sib}\"" Formula/omnigent.rb | grep -q "${sib//-/_}-${version}"; then
echo "::error::resource ${sib} did not update to ${version}."
exit 1
fi
done
- name: Open or update the tap bump PR
working-directory: tap
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ steps.sdist.outputs.version }}
run: |
set -euo pipefail
if [ -z "$(git status --porcelain -- Formula/omnigent.rb)" ]; then
echo "Formula already at ${VERSION} — nothing to do." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
BRANCH="bump-omnigent-${VERSION}"
git config user.name "omnigent-ci[bot]"
git config user.email "294685417+omnigent-ci[bot]@users.noreply.github.com"
PUSH_URL="https://x-access-token:${GH_TOKEN}@github.com/${TAP_REPO}.git"
git switch -C "$BRANCH"
git add Formula/omnigent.rb
git commit -m "omnigent ${VERSION}"
git push --force "$PUSH_URL" "$BRANCH"
if [ -n "$(gh pr list --repo "$TAP_REPO" --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
echo "Bump PR already open for ${BRANCH} — force-push updated it." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
body="$(printf 'Bumps the omnigent formula to **%s** (new sdist url/sha256, resources regenerated via `brew update-python-resources`).\n\ntest-bot builds the bottles on this PR. Review the resource diff — especially that the extras'"'"' deps survived — then apply the `pr-pull` label to publish bottles and merge.\n\nOpened by omnigent `.github/workflows/update-homebrew.yml`.' "$VERSION")"
gh pr create \
--repo "$TAP_REPO" \
--base main \
--head "$BRANCH" \
--title "omnigent ${VERSION}" \
--body "$body"
echo "Opened tap bump PR for omnigent ${VERSION}." | tee -a "$GITHUB_STEP_SUMMARY"
@@ -9,8 +9,8 @@
# match the input), so the tag and the packaged version can't diverge.
#
# This produces the ARTIFACT ONLY — it does NOT publish to the VS Code
# Marketplace or Open VSX. That runs from the central secure-release repo
# (databricks/secure-public-registry-releases-eng), on hardened runners, where
# Marketplace or Open VSX. That runs from a Databricks-internal secure-release
# repo, on hardened runners, where
# a workflow downloads this `.vsix`, verifies its `.sha256`, scans it, and
# publishes. Keeping the two halves separate is deliberate: this job only
# builds and uploads; the secured half holds the marketplace tokens and scan
@@ -68,15 +68,16 @@ jobs:
with:
ref: release/vscode-v${{ inputs.version }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install, build, and package
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: |
npm ci
npm run build
npm run package
pnpm install --frozen-lockfile --filter omnigent-vscode
pnpm --filter omnigent-vscode run build
pnpm --filter omnigent-vscode run package
- name: Resolve tag and verify package.json version
id: meta
+12 -4
View File
@@ -62,6 +62,9 @@ jobs:
fetch-depth: 0
fetch-tags: true
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Validate version
env:
VERSION: ${{ inputs.version }}
@@ -80,10 +83,10 @@ jobs:
working-directory: editors/vscode
env:
VERSION: ${{ inputs.version }}
# `npm pkg set` edits ONLY package.json (unlike `npm version`, which also
# rewrites package-lock.json). Keeps the release PR to package.json +
# `pnpm pkg set` edits ONLY package.json (unlike `pnpm version`, which
# also rewrites the lockfile). Keeps the release PR to package.json +
# CHANGELOG.md.
run: npm pkg set version="$VERSION"
run: pnpm pkg set version="$VERSION"
- name: Add the CHANGELOG section (placeholder)
working-directory: editors/vscode
@@ -167,6 +170,7 @@ jobs:
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }}
DRAFTER_MODEL: ${{ vars.OMNIGENT_CI_FAST_ANTHROPIC_MODEL }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
@@ -174,6 +178,10 @@ jobs:
echo "::warning::No LLM credentials — keeping the CHANGELOG placeholder."
exit 0
fi
if [ -z "${DRAFTER_MODEL:-}" ]; then
echo "::warning::Repository variable OMNIGENT_CI_FAST_ANTHROPIC_MODEL is empty — keeping the CHANGELOG placeholder."
exit 0
fi
echo "::add-mask::${LLM_API_KEY}"
python3 - "$VERSION" <<'PY'
import json, os, re, pathlib, sys, urllib.request
@@ -201,7 +209,7 @@ jobs:
url = os.environ["GATEWAY_BASE_URL"].rstrip("/") + "/chat/completions"
payload = json.dumps({
"model": "databricks-claude-sonnet-4-6",
"model": os.environ["DRAFTER_MODEL"],
"max_tokens": 1024,
"temperature": 0,
"messages": [
@@ -0,0 +1,31 @@
name: Waiting on Author Test
# Offline unit test for waiting-on-author hygiene. Runs on PR head without
# secrets or network and only when the workflow logic changes.
on:
pull_request:
paths:
- .github/scripts/waiting_on_author.py
- .github/scripts/waiting_on_author_test.py
- .github/workflows/waiting-on-author.yml
- .github/workflows/waiting-on-author-test.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: waiting-on-author-test-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run waiting-on-author unit test
run: python3 .github/scripts/waiting_on_author_test.py
+46
View File
@@ -0,0 +1,46 @@
name: Waiting on Author Hygiene
# Keeps the `waiting-on-author` PR label actionable: author activity clears it,
# and PRs that sit in that state for 7 days are closed. The workflow runs from
# trusted default-branch code and never checks out PR-authored files.
on:
pull_request_target:
types: [synchronize]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
schedule:
- cron: "0 */12 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: waiting-on-author-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: false
jobs:
hygiene:
if: github.repository == 'omnigent-ai/omnigent'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Check out .github
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: .github
persist-credentials: false
- name: Update waiting-on-author state
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 .github/scripts/waiting_on_author.py
+11 -13
View File
@@ -24,14 +24,14 @@ concurrency:
cancel-in-progress: true
jobs:
# Security precondition gate: npm ci/test runs the PR's own install hooks and
# Security precondition gate: pnpm install/test runs the PR's own install hooks and
# test code, so untrusted PRs are held until the scan passes (security-gate.yml).
# Trusted authors and non-PR events pass through.
gate:
uses: ./.github/workflows/security-gate.yml
npm-test:
name: npm test
web-test:
name: web test
needs: gate
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
@@ -41,31 +41,29 @@ jobs:
- name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: ./.github/actions/setup-node
- name: Set up pnpm
uses: ./.github/actions/setup-pnpm
- name: Install dependencies
working-directory: web
# Pin the npm registry to the npmjs default.
# Pin the npm registry to the npmjs default; limit to the web package
# so the Electron package's large native devDependencies are not fetched.
env:
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
run: npm ci --legacy-peer-deps
run: pnpm install --frozen-lockfile --filter web
- name: Check formatting
working-directory: web
run: npm run format:check
run: pnpm --filter web run format:check
- name: Run tests with coverage
working-directory: web
run: npm run test:coverage
run: pnpm --filter web run test:coverage
# Distill the v8 json-summary into a single total.txt, mirroring the
# backend's coverage-report job. ui-code-coverage.yml (privileged
# workflow_run) consumes this artifact and posts the report-only status.
- name: Summarize coverage
if: always()
working-directory: web
run: |
cd web
mkdir -p ui-coverage-summary
if [[ ! -f coverage/coverage-summary.json ]]; then
echo "::warning::No coverage-summary.json; skipping UI coverage report."
+2
View File
@@ -2,6 +2,8 @@
build/
dist/
node_modules/
.pnpm-store/
.pnpm-debug.log*
reviews/
# Generated artifact; never committed.
+39 -18
View File
@@ -1,6 +1,5 @@
# Pre-commit hooks for this repository: ruff (format + check) plus standard
# file-hygiene checks. Heavier validation (typing, lockfile freshness,
# OpenAPI drift) runs in CI rather than as commit-time hooks.
# Pre-commit hooks for this repository: ruff, Pyrefly, project-specific lint,
# and standard file-hygiene checks.
#
# A built web-UI bundle may be committed at this path (vendored, minified
# JS/CSS) — never lint or "fix" it.
@@ -20,6 +19,13 @@ repos:
entry: .venv/bin/python -m ruff check --fix --force-exclude
types: [python]
- id: pyrefly
name: pyrefly
language: system
entry: .venv/bin/pyrefly check
pass_filenames: false
files: ^(omnigent/.*\.pyi?|sdks/python-client/.*\.py|pyproject\.toml|pyrefly\.toml|uv\.lock)$
# Project-specific test-quality lint rules (dev/lint/). Run on test
# files only — the patterns never occur in production code.
- id: no-global-asyncio-patch
@@ -36,16 +42,45 @@ repos:
types: [python]
files: ^tests/
- id: no-hardcoded-models
name: no new hardcoded LLM model ids
language: system
entry: .venv/bin/python dev/lint/lint_no_hardcoded_models.py
pass_filenames: false
files: \.(py|ya?ml|json|toml|sh)$
exclude: (^|/)tests/|^openapi\.json$
- id: web-prettier
name: web prettier
language: system
entry: npm --prefix web exec -- prettier --write
entry: bash -c 'test -x web/node_modules/.bin/prettier && web/node_modules/.bin/prettier --write "$@"' --
files: ^web/.*\.(css|html|js|jsx|json|md|mdx|ts|tsx|yaml|yml)$
# Exclude generated assets: web-ui build output, Xcode asset catalogs,
# and Apple Icon Composer `.icon` bundles (machine-formatted; prettier
# fights the tooling).
exclude: ^(omnigent/server/static/web-ui/assets/|web/.*\.xcassets/|web/.*\.icon/)
- id: web-oxlint
name: web oxlint
language: system
entry: bash -c 'test -x web/node_modules/.bin/oxlint && cd web && node_modules/.bin/oxlint --deny-warnings --report-unused-disable-directives .'
files: ^(web/.*\.[cm]?[jt]sx?|web/\.oxlintrc\.json|web/package\.json|pnpm-lock\.yaml)$
pass_filenames: false
- id: web-tsc
name: web TypeScript type check
language: system
entry: bash -c 'test -x web/node_modules/.bin/tsc && cd web && node_modules/.bin/tsc -b'
files: ^(web/.*\.[cm]?[jt]sx?|web/tsconfig(?:\.[^.]+)?\.json|web/package\.json|pnpm-(?:lock|workspace)\.yaml)$
pass_filenames: false
- id: vscode-tsc
name: VS Code extension TypeScript type check
language: system
entry: bash -c 'test -x editors/vscode/node_modules/.bin/tsc && cd editors/vscode && node_modules/.bin/tsc --noEmit'
files: ^(editors/vscode/.*\.[cm]?[jt]sx?|editors/vscode/tsconfig\.json|editors/vscode/package\.json|pnpm-(?:lock|workspace)\.yaml)$
pass_filenames: false
# Android Kotlin formatting + linting via ktlint (config:
# web/android/.editorconfig). The wrapper no-ops when ktlint is absent,
# so local machines without ktlint installed skip cleanly. CI installs
@@ -105,20 +140,6 @@ repos:
files: ^uv\.lock$
pass_filenames: true
# Local `npm install` rewrites every `resolved` URL in
# package-lock.json to whatever registry is configured on the
# developer's machine (e.g. the Databricks npm proxy via a global
# ~/.npmrc). This OSS repo must always commit the public npm registry
# (registry.npmjs.org), so normalize it back before it lands — a
# proxy URL would make `npm ci` time out on public CI runners. Fixer:
# re-stage if it changes. Mirrors normalize-uv-lock-registry above.
- id: normalize-package-lock-registry
name: normalize package-lock.json registry to npmjs.org
language: system
entry: .venv/bin/python scripts/normalize_package_lock_registry.py
files: ^(web|web/electron|editors/vscode)/package-lock\.json$
pass_filenames: true
# Fail if routing.proto changed without regenerating the committed
# bindings (or vice versa). Verify-only, not a fixer: regen needs
# grpcio-tools, so CI's `uv sync --extra dev` enforces it (like ktlint).
+20
View File
@@ -9,6 +9,17 @@ Run the `pre-commit` hook before committing (`pre-commit run --all-files`, or
let it run on staged files via `git commit`). Fix any issues it reports so the
commit lands clean — CI runs the same checks.
## Local development shortcuts
Use `just` for common tasks; run `just --list` for grouped recipes.
- `just ensure` — install/check prerequisites
- `just run-ios` / `just run-android` — build/run mobile apps
- `just dev` / `just dev-mobile` — start the omnigent dev pod
- `just electron-dev` / `just electron-build` — Electron desktop shell
- `just lint` / `just lint-all` — run pre-commit
- `just normalize-locks` — rewrite lockfile registries to PyPI/npmjs.org
## Pull requests
When you open a pull request, fill in the repo's PR template at
@@ -56,6 +67,15 @@ Keep comments short and focused on the code, not on the change history.
issue numbers, or ticket IDs (e.g. `#1646`, `fixes JIRA-123`); the scenario
should be clear without chasing external links.
## Database query names
Application stores use `make_named_managed_session_maker` and give every
session a stable semantic operation name. The session-level name must describe
the caller's intent rather than repeat SQL syntax; use a nested
`query_name_scope` only when one transaction needs distinct names for important
subqueries. Because the named session covers implicit flush and commit, don't
add an explicit `flush()` only to make a query name observable.
## Framework-owned instructions
Keep runtime lifecycle and metadata instructions separate from portable agent
+468 -4
View File
@@ -5,12 +5,476 @@ generated at release time from each PR's `## Changelog` section, tagged by the
PR's `Type of change` (e.g. `[UI]`); the concise, curated highlights live on the
website under `/releases`.
## [Unreleased]
## [v0.8.1] — 2026-08-03
### Features
- [UI] Reverted the v0.8.0 "Chat/Terminal switcher in the header" change; the
switcher returns to its previous location. (#3931)
- [UI / Feature] Added a Nord color theme (arctic frost-blue palette) to the Appearance settings palette picker.
- [Feature] Per-harness startup command/args overrides via a polymorphic `harness:` key in `config.yaml`. The `harness:` key now accepts a mapping with a `default` plus per-harness `command`/`args` overrides (e.g. `harness: {default: claude-sdk, codex: {command: /usr/local/bin/codex, args: [--config, approval_policy=on-request]}}`). The legacy scalar form (`harness: claude-sdk`) still works and auto-migrates to the mapping form on the next config write. Harness binary-path precedence: `OMNIGENT_<NAME>_PATH` env var > config `harness.<id>.command` > built-in default; `args` follow the same precedence with config `args` as the base and CLI pass-through args appended. The `OMNIGENT_<NAME>_PATH` env var (base id, `-native` suffix stripped) is the canonical per-binary override, standardizing the headless `HARNESS_<NAME>_PATH` and native `OMNIGENT_*_PATH` conventions into one namespaced name; the legacy `HARNESS_<NAME>_PATH` is still read as a deprecated fallback that logs a one-time warning + a CLI startup notice, and is slated for removal in v0.8.0. The pre-existing `omnigent claude --command` flag is deprecated (warns on use, pointing to `OMNIGENT_CLAUDE_PATH`/config) and will be removed in a future release; no other native command gained a `--command` flag — override via env or config.
## [v0.8.0] — 2026-08-03
- [Bug fix] Cursor YOLO sessions no longer stall piloted parents on mirrored tool-approval cards when Cursor leaves a lingering pending gate. (#2338)
- [Feature] codex-native startup-timeout errors now name the resolved provider/model routing (and the login-fallback case) instead of pointing at the runner log (#2843)
- [UI / Bug fix / Feature] Pi can now create task plans and display them in the shared Tasks panel without requiring an optional Pi extension. (#2884)
- [Feature] `detect_loop` builtin policy catches agents stuck retrying the same tool call and prompts for approval to break the loop (#3158)
- [Feature] New `detect_thrashing` builtin policy detects when an agent is stuck in a failure loop and alerts the user to intervene. (#3160)
- [Bug fix] kimi sub-agents now report completion to a parent orchestrator instead of leaving the fan-out waiting forever. (#3166)
- [UI / Bug fix / Chore / Test/CI] Conversations show their newest messages immediately, pin the latest turn below the header, and load older context smoothly near the top. (#3228)
- [Docs / Chore / Breaking] The `run_<x>_native` launchers accept a uniform `extra_args`; the per-harness `<x>_args` keyword is deprecated and will be removed in 0.9.0. (#3244)
- [Bug fix] Intelligent routing no longer drops the first message on a new claude-native session (#3257)
- [Feature] `linux_bwrap` sandboxes and their egress rules now run on the Databricks Lakebox backend. (#3258)
- [Feature] `sandbox.kubernetes.secret_mounts` projects a Secret as a rotation-friendly read-only file volume on the runner (#3280)
- [Bug fix] Session cost panel now shows a per-model breakdown for sub-agents/second heads (e.g. Debby's GPT head, Polly's codex sub-agents) running on an unpinned codex model, instead of folding their usage into the total with no per-model entry. (#3287)
- [Bug fix] Fix `antigravity-native` readiness detection for `agy` CLI installs on macOS where OAuth credentials live in Keychain (#3289)
- [Bug fix] Fixed a race condition where deleting two admins at nearly the same time could leave a deployment with zero admins and no way to recover through the API. (#3304)
- [Docs / Chore] Resuming a goose / hermes / antigravity / qwen / opencode native session now attaches to the live TUI instead of double-posting each message through the Omnigent REPL. (#3314)
- [UI / Feature] Workspace rail now shows files and shells as editor-style tabs, opens shells in-rail instead of replacing the chat, adds a full-screen toggle, and adds a "+" menu for creating shells with a remembered type picker (#3333)
- [UI / Bug fix / Feature / Test/CI] `omnigent setup` can import OpenClaw/acpx coding agents from an auto-detected or user-selected config into the generic ACP harness picker, and `omnigent run --from-openclaw <agent>` can try one without saving it. (#3354)
- [Feature] Polly can launch supported Claude and Codex implementation children in goal mode. (#3362)
- [Bug fix] Claude sessions started with a model alias (e.g. Opus) no longer fail on gateway setups that can't pin the alias — the launch resolves to a routable model id. (#3378)
- [Feature] `omni setup` now supports signing in to Antigravity with Google OAuth through `agy`, alongside Gemini API keys. (#3391)
- [UI / Feature] Archived sessions in Settings are now grouped by date (Today, Yesterday, Previous 7/30 days, month/year) for easier browsing. (#3394)
- [UI / Bug fix] Subagent graph view nodes are now clickable and navigate to the selected session. (#3395)
- [Bug fix] Dedupe codex-native's per-session plugin cache to reclaim disk (#3401)
- [Bug fix / Test/CI] Shared-session sub-agent completion notices are attributed to the collaborator who dispatched the sub-agent. (#3409)
- [Bug fix / Test/CI] N/A — test-only change. (#3410)
- [Bug fix / Test/CI] Codex sessions keep their selected permission mode when resumed on a replacement host. (#3411)
- [Bug fix] Only session owners can approve tools that run with owner credentials in shared sessions. (#3416)
- [Bug fix] Fixed a bug where a malformed or unrecognized tool-call payload on certain harnesses (notably OpenCode) could silently bypass a configured tool-call policy instead of being blocked or asked. (#3418)
- [Bug fix / Feature / Docs / Test/CI] ACP agents can opt out of Omnigents MCP relay with `omnigent_mcp: false`, enabling compatible OpenClaw Gateway ACP registrations. (#3420)
- [Feature / Docs] Shared-session agents can distinguish who wrote each message without changing whose credentials execute the session; operators can hide model-visible author labels with an environment flag. (#3422)
- [Bug fix] `/model` and the startup header no longer name an Omnigent provider and model for ACP-backed sessions, which run on the agent's own auth and model. (#3431)
- [Bug fix] Session tokens saved by `omnigent login` are now created owner-only, so a JWT is never briefly world-readable on first login, and an interrupted write no longer discards every stored token. (#3441)
- [Feature / Docs / Chore / Test/CI] Model selection can now represent provider choices through stable intents and normalized capability metadata. (#3443)
- [UI / Feature] Session owners can grant trusted collaborators permission to approve privileged actions without transferring ownership; ordinary editors can reject but cannot approve. (#3446)
- [Bug fix / Feature / Docs / Chore / Test/CI] Default model selection now follows the active provider catalog instead of release-specific model names baked into runtime harnesses, while retaining general-purpose selection policy and Databricks gateway routability. In cold-cache Databricks environments without GitHub egress, configure `executor.model` or a provider `models.default`. (#3448)
- [Feature / Docs / Chore / Test/CI] Smart routing now chooses only from models discovered on the active runner instead of falling back to release-specific model names. (#3450)
- [Feature / Docs / Chore / Test/CI] The Kiro model picker now follows the models and metadata reported by the installed Kiro CLI. (#3452)
- [Feature / Docs] Minimal `omnigent run` agents now discover their default model instead of using a release-specific built-in endpoint. (#3455)
- [Feature / Docs / Test/CI] Provider setup and unconfigured provider runtimes now select current catalog models instead of release-specific built-in defaults, and fail with explicit configuration guidance when discovery is unavailable. (#3456)
- [Docs / Chore] The Kimi launcher example now respects the default model configured in Kimi Code. (#3457)
- [Bug fix] `omni resume` now accepts a conversation id pasted with stray punctuation (trailing period, quotes, backticks) instead of crashing (#3465)
- [UI / Bug fix] Sidebar New session, Automations, and Inbox icons now sit on the same left column (#3468)
- [Chore] Android app now targets Android 16 (API 36) to stay compliant with Google Play's (#3470)
- [Feature / Test/CI] Nightly prerelease builds: every night a `X.Y.Z.devYYYYMMDD` tag of main is cut automatically; install or update with `scripts/update_nightly.sh` or `omni upgrade --nightly` (#3475)
- [Bug fix / Breaking] Agent CLIs (qwen, goose, kimi, hermes, and generic ACP agents) no longer receive unrelated host credentials such as cloud tokens and other providers' API keys; an agent that authenticates from a variable outside its own family now declares it in `os_env.sandbox.env_passthrough`. (#3479)
- [UI / Bug fix] Conversation sidebar text now scales with the Interface font size setting (#3480)
- [Bug fix] Fixed two `claude-sdk` steering bugs: messages sent during an active turn were answered one turn late (a permanent chat desync), and steering several messages at once dropped all but the last. Steered messages are now buffered correctly and all of them reach the model. (#3484)
- [Bug fix] Fix the first message being silently dropped when a session resumes on a managed sandbox / lakebox (#3488)
- [UI / Bug fix] The workspace file view keeps its scroll position — both the file tree and the open file — when you switch between sessions (#3490)
- [UI / Bug fix] Deleting a pinned session now removes it from the sidebar's Pinned section immediately, and sidebar rows no longer grow or shift while being deleted or renamed (#3492)
- [Chore] Faster initial load — syntax-highlighting language grammars now load on demand instead of all upfront. (#3496)
- [UI / Feature] Markdown file previews render fenced Mermaid diagrams. (#3498)
- [Bug fix / Feature] Antigravity (`agy`) sub-agents now reliably receive their first turn, get their approvals dismissed in the terminal, and stop showing as still-running after they have finished. (#3499)
- [Docs / Chore] qwen-native's terminal-start error label now reads "Qwen Code" (consistent with the other native harnesses) instead of "qwen". (#3500)
- [UI / Bug fix] Terminal view scrolling now works with macOS trackpads and with TUIs that enable mouse tracking at startup (OpenCode, Claude Code) (#3510)
- [Bug fix] Runners now retry login-page redirects with refreshed credentials instead of exiting, so a hosted session survives an expired bearer (e.g. after the machine slept through a token's lifetime) and reconnects on its own. (#3511)
- [UI / Bug fix] The chat view shows the "Starting up…" spinner while a message is waking a disconnected runner, instead of nothing until the runner boots; the sidebar row shows a spinner while a session is starting up (#3514)
- [UI / Bug fix] Renaming a session in the sidebar no longer hits the wrong row when the list reorders — row order holds while the pointer is over the list or a rename is in progress (#3515)
- [UI] Collapsed tool runs in the chat view are labeled by what they did ("Ran 1 shell command, read 2 files") instead of "See N steps" (#3518)
- [Feature] Sandbox dotfile hiding is now top-level only by default (opt into the full-tree walk with `cwd_hidden_scan_recursive: true`), and `mask_paths` hides named files or folders. Untrusted-tree sandboxes that relied on recursive masking should set `cwd_hidden_scan_recursive: true` on upgrade. (#3519)
- [UI / Bug fix] Cloning a session that runs in a git worktree now pre-fills the original repo with the worktree branch (instead of the worktree path as the working directory), and the clone dialog blocks creation when the picked working directory doesn't exist on the host (#3521)
- [Bug fix] `omnigent sandbox create --provider openshell` no longer crashes against openshell SDK >=0.0.86; workspace is configurable via `sandbox.openshell.workspace` or `$OMNIGENT_OPENSHELL_WORKSPACE` (defaults to `"default"`). (#3524)
- [Bug fix] Shared-session agents distinguish speakers without treating claimed roles as authorization. (#3527)
- [Bug fix] Direct `omnigent.llms.Client` Anthropic reasoning requests now select adaptive or fixed-budget thinking from live model capabilities. (#3529)
- [Chore / Test/CI] N/A — internal lint cleanup. (#3545)
- [Bug fix / Chore / Test/CI] N/A — internal lint cleanup. (#3546)
- [Chore / Test/CI] N/A — internal lint cleanup. (#3548)
- [Chore / Test/CI] N/A — internal lint cleanup. (#3549)
- [Bug fix / Docs / Chore] Model context limits now follow live provider catalog maximum-input metadata instead of adding output capacity or relying on stale built-in model IDs. (#3551)
- [Test/CI] N/A — internal CI enforcement only. (#3552)
- [Bug fix] User messages no longer risk a React hook-order crash when changing from a system marker to regular content. (#3553)
- [Chore] N/A — internal code-quality cleanup only. (#3554)
- [UI / Feature / Test/CI] Choose a Codex model before starting a Web UI session (#3556)
- [Chore] N/A — internal code-quality cleanup only. (#3571)
- [Bug fix / Docs / Chore] Pi now routes Databricks models through the API advertised by the live model catalog instead of a release-specific model allowlist. (#3572)
- [Bug fix] Bulk conversation actions now report structured errors when only some items fail. (#3573)
- [Chore] N/A — internal code-quality cleanup only. (#3574)
- [Chore] N/A — internal code-quality cleanup only. (#3575)
- [Feature] `omnigent import --force` replaces a previously imported chat with the latest local transcript. (#3576)
- [UI / Feature] Subagent graph panel now has zoom in, zoom out, and fit-to-view buttons for easier navigation of large agent trees (#3583)
- [UI / Bug fix] Change a session's model and effort from the config gear while the session is asleep — the change is saved immediately and applies when the next message wakes it. (#3584)
- [Feature / Test/CI] Add a Locust WebSocket load test (`dev/loadtest/`) with a one-command runner and result summary (#3591)
- [Feature] Sandbox now hides dotfiles (`.env`, `.aws`, `.ssh`, ...) under `write_paths` roots, not just `cwd` and `read_paths` (#3596)
- [Test/CI] N/A — internal CI enforcement only. (#3600)
- [Feature] GitHub policy blocks destructive operations (deletes) by default across MCP tools, `git push --delete`, and `gh * delete`; opt in with `allow_destructive: true`. (#3622)
- [Feature / Chore] The Cursor model picker now follows the models advertised by your installed Cursor CLI. (#3624)
- [Feature / Docs / Chore] Pi gateway sessions now list the models currently available from the workspace instead of a release-specific bundled menu. (#3629)
- [Docs / Chore] Setup and tool help no longer recommend release-specific model ids. (#3630)
- [Feature / Docs / Chore] Static model catalog responses now identify fallback ownership and advertise the current GPT 5.6 Sol, Luna, Terra, and GPT 5.5 Codex aliases. (#3632)
- [Feature] Catalog-backed model defaults now reuse a validated last-known-good provider catalog during transient upstream outages or empty responses. (#3641)
- [Chore] N/A — internal typing cleanup only. (#3643)
- [Chore] N/A — internal typing cleanup only. (#3645)
- [Chore] N/A — internal typing cleanup only. (#3646)
- [Docs / Chore / Test/CI] Repository lint now permits unavoidable static model aliases only through auditable owned fallback records. (#3647)
- [Chore] N/A — internal typing cleanup only. (#3649)
- [Chore] N/A — internal typing cleanup only. (#3650)
- [Chore] N/A — internal typing cleanup only. (#3651)
- [Chore] N/A — internal type cleanup only. (#3652)
- [Chore] N/A — internal type cleanup only. (#3653)
- [Chore] N/A — internal type-safety refactor. (#3655)
- [Chore] N/A — internal type-safety refactor. (#3657)
- [Chore] N/A — internal type-safety refactor. (#3659)
- [Bug fix] An undeclared sub-agent name on session create is now rejected instead of silently running the child as the parent agent (#3662)
- [Chore] N/A — internal type-safety refactor. (#3663)
- [Chore] N/A — internal type-safety cleanup. (#3664)
- [Chore] N/A — internal type-safety refactor. (#3665)
- [Chore] N/A — internal type-safety cleanup. (#3666)
- [Chore] N/A — internal type-safety cleanup. (#3667)
- [Bug fix] The Codex model picker now hides OpenAI models that the installed Codex CLI cannot run. (#3668)
- [Chore] N/A — internal type-safety cleanup. (#3669)
- [Chore] N/A — internal type-safety cleanup. (#3671)
- [Chore] N/A — internal type-safety cleanup. (#3674)
- [Chore] N/A — internal type-safety cleanup. (#3675)
- [Chore] N/A — internal type-safety cleanup. (#3676)
- [UI / Feature] Redesigned the sidebar's bulk-select bar and added per-section selection: pick sessions from the flat list or from within project folders (#3677)
- [Chore] N/A — internal type-safety cleanup. (#3678)
- [Chore] N/A — internal type-safety cleanup. (#3679)
- [Chore] N/A — internal type-safety cleanup. (#3680)
- [Chore] N/A — internal type-safety cleanup. (#3681)
- [Chore] N/A — internal type-safety cleanup. (#3683)
- [Chore] N/A — internal type-safety cleanup. (#3684)
- [Chore] N/A — internal type-safety cleanup. (#3686)
- [Chore] N/A — internal type-safety cleanup. (#3687)
- [Chore] N/A — internal type-safety cleanup. (#3689)
- [Chore] N/A — internal type-safety cleanup. (#3691)
- [Chore] N/A — internal type-safety cleanup. (#3695)
- [Chore] N/A — internal type-safety cleanup. (#3696)
- [Chore] Malformed tools, retry, and MCP YAML now fails with actionable parser errors instead of leaking untyped values. (#3705)
- [Chore] N/A — internal type-safety cleanup with no user-facing behavior change. (#3706)
- [Chore] N/A — internal ASGI type cleanup with no user-facing behavior change. (#3707)
- [Chore] N/A — internal migration decoding hardening with no supported-input behavior change. (#3708)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3729)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3734)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3735)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3736)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3737)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3739)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3740)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3741)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3742)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3743)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3744)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3745)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3746)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3747)
- [Bug fix / Chore] Managed Islo hosts now receive configured provider gateways during startup. (#3748)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3751)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3752)
- [Bug fix] Antigravity sessions without an explicit model now follow the installed SDK's current default. (#3762)
- [UI / Bug fix] Moving a session into a project updates the sidebar instantly instead of after a multi-second wait (#3784)
- [UI / Feature] `omnigent --help` now groups launch commands under a **Harnesses** section, colorizes the output, hides harnesses whose optional extra isn't installed (with a notice pointing at `omnigent setup`), drops the duplicate `update` alias line, and tidies the command descriptions (#3795)
- [Feature] `omni upgrade` now preserves requested extras for `uv tool` and `pipx` installs, supports `--extra`, `--target-version`, and `--dry-run`, and refuses to auto-upgrade `pip` / `uv pip` installs because those installers don't record extras. (#3796)
- [UI / Chore] `omnigent --help` command descriptions are tidied (harness rows read "Launch <Name> with Omnigent"), and the duplicate `update` alias line is hidden from the listing (#3797)
- [Bug fix] Prevent `list_files` from running without a conversation scope. (#3804)
- [Bug fix] Reject stale harness continuation requests that cannot resolve an agent model. (#3806)
- [Chore] N/A (internal type cleanup) (#3830)
- [Chore] N/A (internal type cleanup) (#3831)
- [Chore] N/A (internal type cleanup) (#3832)
- [Chore] N/A (internal type cleanup) (#3833)
- [Chore] N/A (internal type cleanup) (#3835)
- [Chore] N/A (internal type cleanup) (#3836)
- [Chore] N/A (internal type cleanup) (#3837)
- [Chore] N/A (internal type cleanup) (#3838)
- [UI / Bug fix] New sessions created inside a project appear under that project immediately instead of briefly showing under "Sessions" (#3869)
- [Bug fix / Breaking] The "GitHub Repo & Branch Access" and "Block Working Directory & Worktree (#3888)
- [Feature] claude-native sessions now derive their Working/idle status from Claude Code's own session file for faster, more accurate turn-edge detection (falls back to the terminal watcher on older Claude versions) (#3906)
- [UI] Tightened the sidebar's spacing so nav, sections, and session rows sit on a consistent vertical rhythm (#3908)
- [UI / Bug fix] Sidebar rows no longer stay highlighted in "Select sessions" mode unless explicitly selected (#3912)
- [UI / Feature] Opening a shell on a sleeping session now wakes its runner automatically instead of failing with "no runner available" (#3919)
- [Bug fix] Pi-native sessions now retain Omnigent system tools and the comment relay. (#3920)
- [Chore] N/A — no user-facing behavior change. (#3923)
- [Bug fix] Fixed a leak where native Codex sub-agents left orphaned `codex app-server` processes after idle reaping, TUI exit, runner shutdown, or a hard host/runner death (#3925)
- [Bug fix / Chore] OpenCode-native model options now fall back to the authenticated server catalog when CLI discovery fails. (#3926)
- [UI / Bug fix] The sidebar's My sessions / Shared with me switch stays visible while bulk-selecting sessions (#3927)
- [Feature] `omnigent diagnose` prints a secret-free environment snapshot (CLI/server versions, OS, auth mode) for bug reports (#3928)
- [UI / Bug fix] Hide the empty Projects header menu (⋯) when you have no projects (#3930)
- [UI] Moved the Chat/Terminal switcher for terminal-first sessions from the composer into the session header (#3931)
- [UI / Bug fix / Feature] Recent conversations reopen instantly while catching up without reordering live response output. (#3932)
- [Bug fix] `pip install` / `uv sync` no longer hangs when pnpm is provided by a corepack (#3986)
## [v0.7.0] — 2026-07-27
- [Bug fix] Hermes thinking now appears in mirrored web conversations. (#1645)
- [Bug fix] Image and file attachments now survive session relaunches on remote host runners; attachments that fail to load show a visible marker instead of silently disappearing. (#2085)
- [UI / Feature] Voice dictation in the composer now works in Electron, Firefox, and Chromium via optional server-side transcription (`omnigent[dictation]`) — local models, live streaming partials, audio never leaves your server. (#2093)
- [UI / Bug fix / Test/CI] Hide Claude task completion control messages from conversation history while preserving them for resume context. (#2104)
- [Bug fix / Feature / Docs] Operators can mount pre-created PersistentVolumeClaims (NFS/SMB/SAN) into Kubernetes sandbox runners via `sandbox.kubernetes.pvc_mounts` (read-only by default) (#2435)
- [Bug fix / Test/CI] `/compact` no longer races when multiple compact requests hit the same session at once (#2585)
- [Bug fix / Test/CI] `sys_call_async` / `sys_cancel_async` now consistently use `handle_id` as the cancel identifier. (#2586)
- [Bug fix / Test/CI] Runner idle timeout no longer kills sessions waiting on async tools, timers, or approval prompts (#2588)
- [Bug fix] Misconfigured runner tool policies deny tool calls instead of silently allowing them (#2589)
- [UI / Feature] Slash-command menus now match any part of a command's name, so `/using-superpowers` finds `/superpowers:using-superpowers` (#2655)
- [Bug fix] Host-launched runners now reuse delegated credentials instead of repeating Databricks authentication during startup. (#2762)
- [Feature] Projects are now a first-class entity with a `/v1/projects` CRUD API (create, list, rename, delete) and per-session membership. (#2765)
- [Bug fix] Hermes forwarder introspects state.db columns to survive cross-version schema drift (#2774)
- [Feature] `omni usage` reports your LLM cost for today / the last 7 / 30 days, with a per-session per-model cost breakdown (#2787)
- [Feature / Chore] Native Claude sessions start faster by coalescing runner initialization into one handshake. (#2793)
- [UI / Bug fix / Feature / Docs / Test/CI] Claude-native launch and in-session pickers now share the selected host's live model catalog, including Claude Code's managed routes. (#2831)
- [Bug fix] Managed BoxLite sandboxes remain available after provisioning so agent launches can execute commands reliably. (#2846)
- [Feature] Server-side smart routing can now call an external `routes:select` router via `routing.provider: external`, with provider-agnostic auth (`api_key`) and model-name mapping (`model_prefix`) (#2864)
- [UI] Chat code blocks no longer load the syntax-highlighter engine until the first (#2886)
- [UI / Bug fix] The main chat "Working…" indicator now clears reliably when the session goes idle, instead of occasionally staying lit after a reply completes. (#2900)
- [Bug fix] The performance benchmark harness now records HTTP failures and continues the rest of the suite instead of aborting, and excludes fully-failed runs from the summary averages. (#2917)
- [Feature / Test/CI] Scheduled tasks can now be created without a workspace or a pinned host for non-code work (research, summaries, chat-only, MCP-only); an unset host runs on your live host at fire time, and an unset workspace defaults to the host's home directory. A pinned host is now authorized (existence + ownership) at create time rather than only at fire time. (#2946)
- [Chore / Test/CI] N/A — internal benchmark/dev tooling; no user-facing impact. (#2947)
- [Feature] Set `OMNIGENT_CONTAINER_RUNTIME=podman` to use Podman (or another supported runtime) globally instead of Docker, without editing every agent's YAML. (#2949)
- [Bug fix] Sending a message to a session whose Claude Code terminal crashed no longer (#2951)
- [Bug fix] The desktop app now always quits within a few seconds even if its background cleanup stalls or the OS re-quit is dropped. (#2972)
- [UI / Bug fix] Messages send immediately when a session's only remaining work is a background job, instead of being held in the queue until it finishes (#2974)
- [UI / Feature] Desktop update notifications now appear in a native corner toast that works (#2975)
- [Bug fix / Chore] Runner startup no longer waits several seconds for Git's optional untracked-file cache probe. (#2976)
- [Feature / Test/CI] `omnigent` benchmark harness gains `--network-delay-ms` and per-journey HTTP request counts (#2977)
- [UI / Bug fix] Pi sessions now show reasoning while it streams and after conversation history reloads. (#2979)
- [Feature] The runner log now records why the runner exited (crash traceback, signal, idle timeout, tunnel close, or parent death) (#2985)
- [UI / Feature] Set up a missing agent from the New Chat dialog with a guided, step-by-step checklist (#2987)
- [UI / Bug fix / Feature] HTTP headers can now be set and edited for HTTP MCP servers in the session agent info panel. (#2989)
- Capped unbounded DB list queries in the permission store and reduced session opens in `check_access`/`get_permission_level` from 23 to 1. (#2995)
- Deleting a conversation with many descendants now issues a single FTS DELETE instead of one per descendant. (#2999)
- [UI / Bug fix] Android auto theme and system-bar icons now stay readable with both device themes and explicit in-app theme overrides. (#3006)
- [UI / Feature] 3D model files (STL, 3MF, OBJ) now render an interactive preview in the file browser (#3007)
- [UI / Bug fix] Subagents panel Graph View now shows the same status dot colors as List View (#3009)
- [Feature / Test/CI] Scheduled-task runs now transition to a terminal state (`succeeded`/`failed`) as soon as the dispatched turn finishes, instead of staying `running` forever; run history is readable at `GET /v1/scheduled-tasks/{id}/runs`. (#3014)
- [Feature / Chore] When enabled, new sessions receive concise semantic titles in the background without adding work or latency to the active agent turn. (#3024)
- [Feature] Offload dictation speech-to-text to a remote worker with (#3025)
- [Bug fix / Docs / Test/CI] Codex-native subagents now appear in the Agents panel with their live conversations. (#3028)
- [Bug fix] Credential proxy no longer attaches injected credentials to TRACE/OPTIONS requests, and the egress proxy now honors Max-Forwards as a conformant intermediary. (#3029)
- [Docs] N/A — internal documentation cleanup. (#3031)
- [Feature] Import local Qwen, Kiro, Pi, and Kimi coding chats into Omnigent (#3032)
- [UI / Feature] Press ⌘⌥V (Ctrl+Alt+V) to toggle voice dictation from anywhere; while dictating, Enter keeps the text and Esc discards it (#3044)
- [UI / Feature] Added: "Auto · smart routing" harness option in the new-chat picker — lets the intelligent router pick both harness and model based on the task description (#3045)
- [Feature] Import existing OpenCode chats, including files and tool activity, with `omnigent import` (#3046)
- [Bug fix] Dictation streams now reliably release their worker slot when a browser disconnects abruptly. (#3048)
- [UI] New-session composer moves harness configuration into a gear-icon modal, with a cleaner agent picker (needs-setup and custom agents folded into flyouts) and Smart Routing offered as a model option. (#3050)
- [Bug fix / Feature] The Slack bot can now run against an Omnigent server deployed on Databricks Apps, (#3051)
- [Feature] Sessions can now be filed into first-class projects via `PATCH /v1/sessions/{id}` and listed with `GET /v1/sessions?project=<name>`, which dual-reads first-class membership and legacy project labels. (#3053)
- [Bug fix / Test/CI] Fixed SDK session telemetry always recording `harness: null` in server deployments not started via the CLI. (#3054)
- [Test/CI] N/A — test-only reliability change. (#3056)
- [Bug fix] Databricks OAuth CLI profiles no longer fail with a misleading "malformed profile" error; the message now explains the real fix (install `omnigent[databricks]` or refresh the OAuth session). (#3059)
- [Bug fix] An idle runner shutting down after inactivity no longer shows a scary "disconnected" error — just send a message to wake it back up. (#3060)
- [UI / Feature] The sidebar now uses first-class projects: create empty projects, rename and delete them, and file sessions into them — while existing label-based projects keep working. (#3061)
- [UI / Bug fix] The "Host is offline — click to reconnect" prompt now appears in the composer's host badge instead of a separate banner below the composer (#3062)
- [Test/CI] N/A (test-only change) (#3063)
- [Feature / Docs / Test/CI] New `databricks_cli` credential-proxy type lets sandboxed agents use the Databricks CLI without the real token entering the sandbox (#3080)
- [UI / Feature / Test/CI] Polly sessions running on Claude SDK can start Goal mode from the chat composer. (#3084)
- [UI / Bug fix] The Configure agent modal's footer no longer shows a gray background band behind Cancel/Save (#3089)
- [UI / Feature] The Sidebar is more compact and polished, with clearer status indicators and richer session details on hover. (#3092)
- [UI] Reordered the project-folder header buttons (new-session before the menu), (#3096)
- [Chore / Breaking] `omni server start` is removed; use `omni server --background` to launch the (#3105)
- [Bug fix] `omnigent server --host 0.0.0.0` now enables accounts (login) mode automatically instead of silently 401-ing every request (#3107)
- [Feature] Projects can store default session settings (host, workspace, harness, model, …) via a new `config` field on the projects API. (#3108)
- [Bug fix] Databricks-served Claude models no longer break non-streaming responses (prompt-policy and smart routing) when returning typed content blocks (#3109)
- [Feature] `omnidev omnigent <args…>` runs an omnigent command against the current checkout's pod via `uv run omnigent`, with the pod's isolated env applied (#3110)
- [UI / Feature] Configure a session's model, effort, and smart routing mid-chat from a new gear icon in the composer (#3111)
- [UI / Feature / Test/CI] Add the `/tasks` Scheduled Tasks page with sidebar navigation, task rows, empty states, suggestion chips, create-dialog entry points, and Playwright E2E coverage. (#3112)
- [Bug fix] Reading image files in a Claude Code native session no longer bloats conversation history and breaks resume/compaction on large sessions (#3113)
- [Bug fix / Test/CI] The iOS app no longer follows cross-origin redirects when probing a newly approved server for the Databricks workspace mount, so a consented host can't redirect the probe to a different origin. (#3115)
- [Bug fix] Forked native sessions (Claude Code, Codex, Pi, Qwen) again resume with their prior conversation history. (#3116)
- [UI / Feature / Test/CI] Workspace pane icons now explain themselves on hover, with a cleaner right-side session layout and compact Share action. (#3122)
- [UI / Bug fix / Feature] Add a dialog for creating recurring scheduled agent tasks. (#3123)
- [UI] Sidebar session hover flyouts and rows now align with the project rows — matching flyout style, title size, and right-edge padding. (#3124)
- [Bug fix] Resuming a session with large images stored in history no longer overflows the context window or breaks compaction, on both the SDK and native Claude Code paths (#3133)
- [Feature] `omnigent session import` loads a `session export` JSONL back into a server as a new session (#3141)
- [Feature] Telemetry now records the agent name for Polly and Debby sessions. (#3152)
- [Chore / Breaking] `omni integration slack start` is removed; use `omni integration slack --background` to launch the (#3153)
- [UI / Bug fix / Docs / Chore] Slack device login now requires a fresh password at the consent screen, closing a device-code phishing gap where an already-signed-in user could approve a login by reflex. (#3156)
- [Feature] `omnigent claude` keeps tool search enabled when launched with `CLAUDE_CODE_USE_GATEWAY=1`. (#3161)
- [Test/CI] Fix `test_session_stream_emits_heartbeat_on_idle` after `_session_labels_for_runner_spawn` was extracted into `omnigent.runner.native.orchestration`; patch the heartbeat cadence on `omnigent.runner.app` where it is defined and consumed. (#3163)
- [Bug fix] Crash reports are no longer lost when a process crashes more than once in the same second. (#3173)
- [Bug fix] Custom codex-native agents launch on the model declared in the agent spec (`executor.model`) instead of silently falling back to the provider default (#3175)
- [Bug fix] Single-file agent YAMLs that nest the executor under `type:`/`config:` (the bundle config.yaml shape) now fail at load time with the correct flat spelling, instead of silently running a harness inferred from the model prefix (#3178)
- [UI / Bug fix / Feature] Use native Codex goal mode from Polly's Goal control (#3181)
- [UI / Bug fix] Renaming a session now updates the name in the sidebar instantly instead of after a short delay. (#3185)
- [UI / Bug fix / Feature / Test/CI] Edit scheduled tasks and type exact run times, with a scrollable time picker and a consistent, fully-visible dialog. (#3186)
- [UI / Feature] Pinned sessions now persist server-side per user, so pins follow you across devices and browsers. (#3189)
- [Feature] New sessions now receive concise semantic titles automatically without additional configuration. (#3191)
- [Test/CI] `/rerun` PR comment re-runs failed CI on the current commit without dismissing approvals (#3195)
- [Feature] Native Codex sessions can now receive concise automatic background titles. (#3199)
- [Bug fix] Qwen3, inkling, and other non-OpenAI models now work in the Pi SDK executor harness (#3203)
- [Chore / Breaking] Slack-on-Databricks deploy: renamed `OMNIGENT_SLACK_WEBAUTH_BASE_URL` to `OMNIGENT_SLACK_DATABRICKS_APP_URL` (`--app-url`), removed the `WEBAUTH_PORT` / `DATABRICKS_WORKSPACE_HOST` overrides, and dropped deploy-time `uv lock` in favor of in-container `uv run`. (#3206)
- [UI / Bug fix] Sidebar session titles use the available space cleanly and reveal branch and action details only when needed. (#3208)
- [UI / Bug fix] Removed the redundant "Create new project" option from the sidebar project picker — create projects with the + icon next to Projects (#3210)
- [Feature] Smart routing now activates automatically when a server `llm:` block or an external `routing:` block is configured — no `OMNIGENT_SMART_ROUTING` env var needed (#3215)
- [UI / Feature / Test/CI] Scheduled task rows now show when each task will next run ("Next run in 15h") and a "Run now" action in the ⋯ menu to fire a task immediately, with refreshed row styling. (#3218)
- [UI / Feature] Projects now carry default session settings (host, working directory, agent, optional git worktree) that pre-fill the new-session composer. (#3221)
- [Bug fix] Fixed per-model cost attribution for native harnesses so a session's per-model (#3223)
- [UI / Bug fix] Codex task plans now stay in Tasks instead of being duplicated in chat. (#3249)
- [UI / Bug fix] Smart Routing no longer appears in the model dropdown for native terminal sessions (Claude Code, Codex, Pi), where it had no effect (#3259)
- [Bug fix] Sandboxed agents can now run tools managed by update-alternatives (awk, python3, editor, and similar) on Linux. (#3263)
- [Bug fix] Egress proxy now trusts corporate/MDM CA roots installed under the system `capath` directory, so TLS to hosts behind a corporate MITM works from a sandboxed agent. (#3264)
- [Bug fix] Large historical attachments no longer inflate replay and compaction context as inline base64 text. (#3267)
- [Docs] Contributors can now use `omnidev` as the documented worktree-safe local testing flow. (#3277)
- [Bug fix] Custom OpenAI Agents can use Unity AI Gateway Model Services with fully qualified model names when a Databricks provider or profile is configured. (#3288)
- [UI / Bug fix / Feature] Configure recoverable dangerous shell commands to ask for approval or deny execution, while always blocking catastrophic operations. (#3297)
- [Bug fix / Feature] Pi harness now routes kimi, inkling, GLM, qwen3, Gemini 3+, and Llama through the correct AI Gateway endpoints, fixing "Stream ended without finish_reason" errors and ensuring `system.ai.*` ids are used throughout. (#3307)
- [Feature] Add a top-level `justfile` with recipes for launching the iOS Simulator, running the Android debug build, starting the omnigent dev pod, and running pre-commit/lockfile normalization. (#3310)
- [UI / Bug fix] Sidebar header action buttons are now vertically centered with section titles. Session row hover is smoother, and active items no longer flash when hovered. (#3311)
- [UI / Bug fix] Aligns project folder icons and color with the rest of the sidebar. (#3317)
- [Bug fix] Unsupported Claude Code slash commands are now escaped and sent as regular user messages instead of leaving the native terminal in an undriveable state. (#3319)
- [Feature] `omnigent server` now tells users they can set `OMNIGENT_AUTH_ENABLED=0` to override automatic multi-user mode when binding to a non-loopback interface. (#3320)
- [Chore / Breaking] [Breaking] The deprecated `OMNIGENT_ACCOUNTS_ENABLED` environment variable has been removed; use `OMNIGENT_AUTH_ENABLED` instead. (#3322)
- [UI / Bug fix] Fixed pinned sessions being lost when the web UI was updated before the server. (#3323)
- [UI / Feature] Automations list: tasks now render as cards and show a live-updating relative next-run time ("Next run in 3 hours"). (#3324)
- [UI] Settings → Appearance no longer has a separate Sidebar font size control, and a new "Reset to defaults" button restores every appearance preference after confirmation. (#3326)
- [UI] Starting a session inside a project now names the project in the new-session (#3327)
- [UI] The Files workspace tab now uses a stacked-files icon. (#3329)
- [UI / Feature] Automations: scheduled tasks can now pick a model and reasoning effort in the create/edit dialog (defaults to the agent's settings). (#3331)
- [UI / Bug fix] Fixed sessions pinned in the updated web UI being lost after the server was updated. (#3332)
- [UI / Feature] Native harness setup now checks the installed CLI version and prompts to upgrade if it is too old. Cursor's missing-binary case is normalized to the same structured `binary-missing` signal as the other CLI-backed native harnesses. (#3335)
- [Bug fix] Pi sessions now show a clear error when their Databricks login has expired, instead of silently accepting messages with no reply (#3336)
- [Test/CI] N/A (internal CI change). (#3338)
- [Bug fix] claude-sdk harness now surfaces harness-level failures (expired login, auth error) as structured errors instead of storing them as assistant messages. (#3342)
- [UI] Align the sidebar brand row with the rest of the navigation. (#3346)
- [UI / Bug fix] Short links like `#3090` in chat markdown tables no longer stack one character per line (#3350)
## [v0.6.0] — 2026-07-21
- [Bug fix] `sys_os_shell` commands no longer inherit omnigent's own `PYTHONPATH` entry, so project subprocesses resolve their own installed packages instead of omnigent's. (#1861)
- [Bug fix] Stopping a Goose turn no longer leaves stale ACP session state behind when the subprocess is terminated. (#1928)
- [Bug fix] Cancelling or deleting a session during agent cold start no longer leaks the harness subprocess. (#1982)
- [Bug fix] macOS sign-in with a hardware security key (e.g. YubiKey) works again. (#2036)
- [UI / Bug fix] Tool-call cards keep their live spinner when a superseded response terminates mid-turn (fixes a first-turn "no spinner" on native-terminal harnesses). (#2045)
- [Bug fix] hermes-native tool-call cards now show a live spinner and elapsed timer while a tool runs, including on the first turn. (#2046)
- [Bug fix] Fix blank white page on Safari/iPadOS older than 16.4 caused by regex lookbehinds on the web UI boot path (#2105)
- [UI / Feature / Chore] A project's "new session" pencil now prefills the composer from the project's latest session — host, repo, agent, and a fresh git worktree branch — so starting a new chat in a project is one prompt away. (#2133)
- [UI / Feature] Filter the Archived sessions view by project (#2134)
- [Bug fix / Docs / Test/CI] Polly now verifies pytest test-count discrepancies against collected cases before recording miscount/fabrication claims. (#2140)
- [Bug fix] Evict the cached claude-sdk client when a turn is cancelled so the session recovers on the next turn (#2169)
- [UI / Bug fix] Tapping an Android notification now opens the waiting session (or the inbox). (#2210)
- [Bug fix] Fixed the headless Hermes harness registering no Omnigent MCP server (agent had no builtin tools). (#2216)
- [Feature / Breaking] Ids are now bare 32-character hex (no `conv_`/`ag_`/`host_` prefixes) stored as 16-byte binary; existing prefixed ids in URLs, configs, and clients keep working. (#2228)
- [Bug fix] `sys_list_models` no longer misreports codex `cli-config` providers and cursor workers as having no usable model provider. (#2237)
- [Bug fix] Cursor sessions now run shell tools in the declared workspace directory instead of the runner's working directory (#2244)
- [UI / Bug fix / Feature] Conversation view gains a turn-rail minimap for jumping between messages (#2285)
- [Feature / Docs] `sandbox.host_config` in the server config injects verbatim host `config.yaml` content (e.g. a `providers:` gateway block) into managed sandboxes before the host starts (#2306)
- [Bug fix] `omnigent server` now accepts documented boolean server-config values like `sandbox.kubernetes.in_cluster: false` instead of rejecting them at startup. (#2314)
- [Bug fix] Chat sessions now recover automatically from a brief reverse-proxy 404 during a backend restart, instead of freezing until a manual page refresh. (#2316)
- [UI / Bug fix] Bulk select-all/delete/archive in the sidebar now correctly handles collapsed sections (#2377)
- [Bug fix / Feature] `omnigent run --harness <name>-native` now launches every registered native harness with matching prompt and model behavior. (#2379)
- [Feature / Docs / Test/CI] Harness bench now reports Omnigent MCP relay support separately from vendor-native tool calling. (#2380)
- [Bug fix] Sessions are now stopped server-side before archive or delete, so SDK and API callers get the same stop-first behavior the web UI provides. (#2400)
- [UI / Bug fix / Test/CI] Sidebar session tabs no longer overflow their background on narrow widths. (#2425)
- [Bug fix] Local server startup now works when a system proxy intercepts loopback traffic. (#2433)
- [Bug fix] Native TUI harnesses (opencode, pi, hermes) no longer render multibyte UTF-8 as mojibake on deployments whose environment lacks a UTF-8 `LANG`/`LC_ALL`. (#2440)
- [Bug fix] Claude native sessions now back off rejected cost updates instead of retrying on every poll. (#2453)
- [Feature] Server now collects opt-out usage telemetry for session created, stopped, and deleted events to help understand product usage patterns. (#2457)
- [UI / Bug fix] Confirming a Japanese IME conversion with Enter no longer submits the session rename or new-project name inputs (#2459)
- [Feature] Server `llm:` config accepts an optional `fallback_models:` list; LLM-backed policies now retry alternate models before failing closed. (#2462)
- [Feature] Harden the LLM prompt-classifier policy against prompt injection by spotlighting untrusted content behind an unguessable per-evaluation marker. (#2463)
- [Feature / Docs / Test/CI] Harness bench now reports whether forked sessions retain and replay their source conversation history. (#2472)
- [Bug fix / Test/CI] Harness benchmark live tables now distinguish not-applicable capabilities from skipped probes. (#2475)
- [UI / Bug fix] New sessions respond faster to their first message, and the "Working…" indicator now appears immediately on the first turn instead of staying dark while the runner finishes connecting (#2478)
- [Bug fix] A policy-denied message (e.g. hitting the cost budget) now shows immediately instead of only after a page refresh (#2481)
- [Bug fix / Feature / Docs / Test/CI] Codex sessions using Databricks custom model aliases can surface reasoning summaries, and the harness benchmark now reports observable reasoning support. (#2482)
- [Feature / Docs / Test/CI] The harness benchmark can run selected capability dimensions and pin an exact model directly in each harness argument. (#2485)
- [Bug fix / Test/CI] Polly Cursor workers no longer ask for tool permissions by default (YOLO / auto) (#2493)
- [Feature] Polly defaults to Sonnet 5 for its brain and Claude Code workers, and Cursor Grok 4.5 for Cursor workers (#2500)
- [Bug fix] Polly Cursor workers default to grok-4.5 (#2503)
- [Bug fix] Polly Claude brain and Claude Code workers default to the sonnet alias (#2504)
- [Bug fix] Polly Claude brain and Claude Code workers inherit the provider default model again (#2507)
- [Bug fix] Codex-harness sub-agents no longer crash on macOS when the runner inherits a read-only working directory (e.g. `/` from the desktop app). (#2512)
- [UI / Feature / Test/CI] New chats can start with the Workspace panel collapsed via Appearance settings (#2516)
- [Bug fix] `sys_advise_models` is only offered to agents when intelligent routing is configured on the server. (#2517)
- [UI / Feature / Test/CI] OpenCode-native sessions now show their available models in the web model picker. (#2519)
- [Bug fix] Pi's `/model` command now shows all LLM models available on your Databricks workspace (fetched live from the serving-endpoints API) instead of a hardcoded list (#2525)
- [UI / Feature] The changed-files panel now shows a per-file line-change count (`+N M`) beside each file. (#2526)
- [Bug fix] Telemetry rollout percentages now honor 0% and 100% exactly. (#2528)
- [Bug fix] Pi no longer shows a blocking "Trust project folder?" dialog when launched via Omnigent in a project with `.pi/` settings or extensions (requires Pi 0.79+) (#2529)
- [Feature / Docs / Test/CI] Harness metadata can now declare resume, steering, live queue, image, and compaction support for capability conformance checks. (#2530)
- [UI] Admin Settings pages (Members, Policies, Sharing) now share the same left and top alignment as the rest of Settings. (#2532)
- [Bug fix] Pi's `/model` command now shows GLM, Llama, Qwen, and other non-GPT Databricks models alongside Claude and GPT models (#2534)
- [UI / Bug fix] The Members and Sharing settings pages and the Share buttons are hidden in single-user mode (#2536)
- [Bug fix] Pi's `/model` command now correctly lists all available Databricks models when using the AI Gateway (cli-config) setup — the NXDOMAIN error that caused single-model fallback is fixed (#2540)
- [UI / Feature] Native Pi sessions can now switch model mid-session from the web composer, with the picker scoped to your logged-in models and synced to in-terminal `/model` changes (#2543)
- [UI / Feature] Add an opt-in "Hide unconfigured harnesses" setting that filters the new-chat picker to harnesses set up on the selected host (#2544)
- [Bug fix] Sending a message to an idle native Pi session no longer gets stuck in the "queued" state until you switch tabs (#2545)
- [Feature / Docs / Test/CI] Add `omnigent uninstall` for safely removing the CLI, installer-managed shell/config edits, and optionally local Omnigent state. (#2550)
- [Bug fix] Pi's `/model` command now shows all available Databricks models for ucode / Codex app profile-switched setups (#2552)
- [UI / Feature] Added a Nord color theme (arctic frost-blue palette) to the Appearance settings palette picker. (#2561)
- [Bug fix] Fix `uv tool install` of the `databricks` extra failing to build pyarrow from source on Python 3.14 (#2563)
- [Bug fix] Sessions list no longer does a workspace-wide id pre-fetch for the archived filter, restoring the single indexed query on the sidebar path. (#2568)
- [Bug fix] The CLI now shows a clear error instead of a raw traceback when the server is too slow or unreachable while starting a session. (#2572)
- [Bug fix] Pi harness: Databricks GLM/DeepSeek reasoning models no longer fail with "Stream ended without finish_reason" (#2573)
- [UI / Bug fix] Clicking a pinned session that belongs to a project no longer re-opens the project section after you've collapsed it. (#2583)
- [Bug fix] Claude-native messages that fail to reach Claude (or stall unsent) now surface the failure instead of silently disappearing. (#2591)
- [UI / Bug fix] The file editor now follows Omnigent's selected web and desktop color theme instead of the operating-system theme. (#2594)
- [UI / Feature] Hovering a pinned session now shows its project name in a flyout (#2595)
- [UI] Sidebar rows, section headers, and nav items now line up on a consistent grid (#2596)
- [Bug fix] Queued follow-up messages in the Pi web UI now dispatch immediately when Pi finishes replying (#2597)
- [UI / Bug fix] Pinned-session project flyout no longer opens (and gets stuck over the chat) when tapping a session on mobile (#2599)
- [UI / Feature] Diff viewer gains a "Wrap lines" toggle, and Find / Download / diff toggles now live in a single "⋯" View settings menu (#2600)
- [UI / Bug fix] Mobile session menu opens the project picker in place instead of an off-screen side flyout (#2602)
- [Bug fix] `sys_session_send` now reliably accepts model overrides when spawning named sub-agent sessions with a specific model (#2603)
- [Chore / Breaking] `omnigent[memory]` is renamed to `omnigent[hindsight]`; the Hindsight memory extra is now the canonical name. (#2605)
- [Bug fix] `omnigent host status` now hides the session table by default (use `--sessions` to show it), making the command significantly faster. (#2606)
- [Feature] `omnigent://<hostname>/c/<session_id>` links open that session in the desktop app, reusing an open window on that server in-place (#2607)
- [Bug fix] Fix native Claude sessions overflowing the context limit on reconnect when the conversation contained screenshots (#2609)
- [Feature] Session creation requests from the web UI now send an explicit `X-Omnigent-Client` header, enabling the server to record the exact client surface (web/desktop/ios/android) in telemetry without relying on User-Agent parsing. (#2615)
- [UI / Bug fix] Mobile: the Chat/Terminal toggle no longer reappears over the sidebar when opening a session's kebab menu (#2617)
- [UI] Project sidebar headers show a chevron in place of the folder icon on hover instead of a persistent caret next to the name (#2618)
- [UI / Feature] PDF files now preview inline in the file viewer, with scrollable pages and zoom (#2619)
- [UI / Bug fix] Find in file now toggles the editor's find bar closed on a second click, and its close button no longer shows a stray keyboard-hint tooltip (#2621)
- [UI / Bug fix / Feature] Find in file now works in the markdown editor — highlights and cycles matches, including terms split across bold/italic formatting (#2628)
- [Bug fix] Clicking an Omnigent session URL printed inside the embedded terminal now stays in the web app instead of opening a duplicate browser tab/window. (#2639)
- [Docs] Clarify optional install extras in the quick-start docs. (#2640)
- [Bug fix] `use_responses: false` now correctly selects the Chat Completions API in standard `config.yaml` bundles. (#2641)
- [Bug fix / Feature] Import existing Claude Code or Codex chats with `omni import`. (#2649)
- [UI / Feature] Customize a built-in color theme with shared light/dark accent, tint, contrast, and two-rail sidebar translucency controls. (#2650)
- [UI / Bug fix] Pinned sessions no longer show duplicate rows after upgrading — legacy pin ids are migrated to the new id format automatically. (#2651)
- [UI / Feature] Long-running Codex commands now stream their output into the web tool card while they run. (#2652)
- [UI / Feature] Randomize accent and background tint colors with a playful new dice button. (#2653)
- [Feature] `omnigent://<hostname>/c/<session_id>` links open that session in the iOS app, reusing the open window on that server in-place (#2661)
- [Bug fix] Pi's `/model` command now shows and correctly routes GPT, Kimi, Llama, GLM, and Gemini 3.x models alongside Claude (#2665)
- [UI / Bug fix] The Add Global Policy dialog now shows all available policies, including ones already applied. (#2668)
- [UI / Bug fix] The Add Policy dialog for sessions now shows all available policies, including ones already applied. (#2670)
- [Bug fix] Deleting or archiving a session now also stops any still-running sub-agent children, instead of leaving them running as unreachable orphans. (#2673)
- [UI / Feature] Find in file now works in the markdown and notebook preview — highlights and cycles matches in the rendered document (#2674)
- [Bug fix] Codex sessions through a Databricks gateway profile no longer fail with "Invalid Token" when the environment's `DATABRICKS_HOST` points at a different workspace than the profile (#2675)
- [UI / Feature] The file panel (changed files, browsing, diffs, and file contents) stays viewable when a session's runner is offline but its host is still connected — no need to send a message to wake it. (#2676)
- [UI / Feature] Select text in a PDF preview to add review comments with inline highlight overlays. (#2677)
- [UI / Feature] Codex plans now show up in the Tasks panel, the same way Claude Code todos do (#2678)
- [UI / Bug fix] Token expiration and other harness startup errors now appear immediately in the chat transcript instead of requiring a page reload. (#2681)
- [UI / Feature] Switching back to a recently-opened chat now renders instantly from cache instead of blanking and reloading over the network. (#2688)
- [UI / Bug fix] Fixed the comment box being hidden behind the keyboard when commenting on a file in the iOS app (#2694)
- [Bug fix] Host-bound sessions relaunch immediately after a Stop or host restart instead of waiting out the runner-connect grace. (#2699)
- [Test/CI] Add a `checkout_sha` input to the Benchmark workflow so nightly benchmarks can be run ad-hoc against a specific commit. (#2700)
- [Bug fix / Test/CI] Fresh installs no longer resolve an OpenAI SDK combination that crashes every OpenAI Agents harness turn. (#2713)
- [Feature] `omni import --harness <claude|codex> --last N` can now import up to 50 recent local chats at once. (#2724)
- [UI / Bug fix] Cursor setup now distinguishes CLI readiness from SDK API-key configuration and shows install/login guidance before web chat launch. (#2733)
- [UI] The Gateway option in model setup no longer cites OpenRouter as an example — use the dedicated OpenRouter option instead (#2738)
- [Bug fix] Sandboxed (`darwin_seatbelt`) agents now boot when the helper interpreter or the wrapped CLI is reached through symlinks — uv-installed Pythons and the standalone Claude CLI no longer die with `Operation not permitted`. (#2743)
- [Bug fix] A sandboxed claude-sdk agent no longer dies at connect time when its sandbox can't wrap the Claude CLI — it starts with native tools disabled (file/shell access stays sandboxed via the `sys_os_*` tools) and logs why. (#2749)
- [Feature] `omnigent setup` can now install Hermes directly before configuring its model provider. (#2751)
- [Bug fix] Sandboxed (`darwin_seatbelt`) agents no longer die at startup with "No usable temporary directory" — the private scratch tmpdir is now granted in the sandbox profile before the process is jailed. (#2759)
- [Feature / Docs / Test/CI] Benchmark reports now measure cold restarts of existing sessions whose runner is offline. (#2761)
- [Docs / Chore / Test/CI] Website release posts now use the narrative, prose-driven MLflow style (#2764)
- [Bug fix] opencode-native carries the user config's model default into the synthesized config when no model is pinned (#2775)
- [Feature] New sessions automatically get concise, useful titles generated by the agent already handling the conversation. (#2778)
- [Bug fix] Forking a Claude Code session onto pi (or other CLIs) no longer fails to start with `required_terminal_exited` (#2780)
- [Bug fix] Native Codex and Claude now resolve their CLI from common global install dirs and an `OMNIGENT_CODEX_PATH` / `OMNIGENT_CLAUDE_PATH` override when it isn't on the host daemon's PATH (#2788)
- [Feature] Nightly benchmark report now includes server CPU% and RSS memory usage (mean/min/max) alongside latency metrics. (#2795)
- [Bug fix] The changed-files panel now shows per-file line-change counts even when the list is served by the host (runner offline). (#2802)
- [Bug fix] Harness readiness now finds CLIs installed in common global dirs (nvm/npm/homebrew) that aren't on the host daemon's PATH, matching what the launch resolves (#2805)
- [UI / Bug fix / Feature] Config-file policies (declared via `omni server -c`) now appear in the admin Global Policies page as read-only entries labeled "Config". (#2807)
- [Bug fix] Codex sessions now inherit global `AGENTS.md` instructions. (#2809)
- [UI / Feature] Switching back to a recently-opened chat now renders instantly from cache instead of blanking and reloading over the network. (#2810)
- [Bug fix] Allow the ACP prompt deadline to be configured without changing its default. (#2817)
- [Feature] The Slack bot now handles tool approvals and questions inline — Approve/Deny (#2820)
- [UI / Feature] The Share dialog now shows a QR code so you can open a session in the mobile app by scanning it with your phone. (#2824)
- [Bug fix] Harness setup warnings now clear automatically without reconnecting the host. (#2828)
- [UI / Feature] Android app shows a floating server switcher pill with a dropdown menu for quick server switching (#2829)
- [Feature] Crash reports now show a calm, branded screen with a compact traceback and a one-tap prompt to file a pre-filled GitHub issue (#2841)
- [UI / Bug fix] Parallel sub-agent dispatch now explains when to use distinct task titles instead of accidentally reusing and serializing one child session. (#2860)
- [Bug fix] Fixed a context-window overflow on a live-streamed turn, leaking its harness process instead of ending the turn cleanly. (#2869)
- [Bug fix] Fixed `/model show` (and `list`/`status`/`current`) bricking the session. (#2888)
- [Bug fix] Clicking "Stop session" no longer shows a spurious "runner disconnected" error (#2903)
- [UI / Bug fix] New-session Send button shows a spinner while the session is being created, instead of looking frozen (#2907)
- [Feature / Breaking] Per-harness startup command/args overrides via a polymorphic `harness:` config key + `OMNIGENT_<NAME>_PATH` env-var standardization (legacy `HARNESS_*_PATH` deprecated, slated for v0.8.0 removal) (#2933)
- [Bug fix] `omnigent[memory]` is restored as a backwards-compat alias for `omnigent[hindsight]` (the `memory` extra was dropped by #2605); it will be removed in 0.70. (#2938)
- [Feature] `OMNIGENT_SESSION_RENAME=on` re-enables the automatic first-turn session rename (disabled by default) (#2944)
## [v0.5.1] — 2026-07-10
- [UI] The desktop Browser tab is now hidden on older desktop builds that don't support the embedded browser, instead of showing a tab that does nothing (#2393)
## [v0.5.0] — 2026-07-10
+87 -10
View File
@@ -28,7 +28,9 @@ Install local prerequisites first:
- `bubblewrap` (`bwrap`), **Linux only**, used to OS-sandbox those native
Claude/Codex/Pi terminals (`apt install bubblewrap` on Debian/Ubuntu). macOS
uses the built-in `seatbelt` sandbox and needs nothing extra.
- Node.js 22 LTS or newer with `npm` when working on `web/`.
- Node.js 22 LTS or newer with `pnpm` (install via `corepack enable` or
`npm install -g pnpm`) when working on `web/`.
- A Rust toolchain for the recommended `omnidev` local development supervisor.
```bash
git clone https://github.com/omnigent-ai/omnigent.git
@@ -42,33 +44,91 @@ source .venv/bin/activate # or prefix commands with `uv run`
Common checks:
Pyrefly is the canonical Python type checker for the repository.
```bash
uv run pytest # Python tests (e2e/live skipped by default)
uv run ruff check . && uv run ruff format --check .
uv run --no-sync pyrefly check # Python type checking (core and client SDK)
uv run pre-commit run --all-files
```
When touching `web/`:
```bash
cd web && npm install && npm run lint && npm run build
cd web && pnpm install && pnpm run lint && pnpm run type-check && pnpm run build
```
When touching `editors/vscode/`:
```bash
cd editors/vscode && pnpm install && pnpm run type-check && pnpm run test && pnpm run build
```
## Running locally
To try your changes, start a local server, register your machine as a host,
and run the frontend dev server. Use three separate terminals:
Start with the smallest relevant automated test described in [Tests](#tests).
For full-stack manual testing, use `omnidev`.
### Recommended: worktree-safe testing with `omnidev`
`omnidev` runs the current checkout's server, host, and Vite frontend in one
terminal. Each checkout path, including each worktree, gets isolated state,
configuration, database, artifacts, logs, and automatically allocated ports,
so it can run alongside your normal Omnigent installation and other worktrees.
Install the supervisor once from an up-to-date checkout:
```bash
cargo install --path dev/omnidev --force
```
Then run it from anywhere inside the branch checkout or worktree you want to
test. A fresh worktree needs its own Python environment first:
```bash
cd /path/to/omnigent-worktree
uv sync --extra all --extra dev
omnidev
```
Open the exact `ui` URL displayed in the header; do not assume the Vite port is
`5173`. Python changes under `omnigent/` reload the server and host, while
frontend changes use Vite HMR.
Run CLI commands against the development pod through the passthrough so they
use that checkout and its isolated state instead of a globally installed
`omnigent`:
```bash
omnidev omnigent config show
omnidev omnigent agent list
```
Keep `omnidev` in the foreground and quit with `q` or `Ctrl-C` so it tears down
all three processes. An interactive terminal inside an existing Omnigent
session also works; use `git rev-parse --show-toplevel` to confirm that its
current checkout is the one you intend to test.
See [`dev/omnidev/README.md`](dev/omnidev/README.md) for log controls,
clean-state testing, backend-only and LAN modes, and other options.
### Manual three-terminal fallback
Use the manual flow when you need to run or debug each component separately.
Unlike `omnidev`, it does not isolate state or allocate ports. These commands
assume the default ports are free:
```bash
# Terminal 1: local server on :6767
omnigent server
uv run omnigent server
# Terminal 2: register your machine as a host
omnigent host --server http://localhost:6767
uv run omnigent host --server http://localhost:6767
# Terminal 3: frontend dev server
cd web
npm run dev
pnpm run dev
```
Open the Vite URL from the frontend dev server, usually
@@ -81,7 +141,7 @@ The host URL can also be passed positionally (`omnigent host
http://localhost:6767`). See the [README](README.md) for more on hosts,
harnesses, and credentials.
### Backend-only local development validation
### Disposable backend-only validation
Use this when you want to validate the Python backend and local API server from
a source checkout without building the web UI, configuring provider
@@ -169,7 +229,7 @@ Two cross-cutting suites sit on top of these:
Frontend changes follow the same expectation with a different toolchain:
- Add or update a **colocated Vitest test** — a `*.test.ts`/`*.test.tsx` file
next to the component or module you changed — and run it with `npm test`.
next to the component or module you changed — and run it with `pnpm test`.
- A change to **user-facing UI behaviour** also needs a Playwright test under
`tests/e2e_ui/`. This one is enforced mechanically by the `E2E UI Required`
check, so a UI PR won't merge without a covering test (or a maintainer
@@ -177,10 +237,27 @@ Frontend changes follow the same expectation with a different toolchain:
- Styling/formatting-only changes, copy tweaks with no flow change, and
refactors with no behaviour change are exempt, same as the backend.
## Developer Certificate of Origin
To contribute to this repository, you must sign off your commits to certify
that you have the right to contribute the code and that it complies with the
open source license. If you can certify the contents of the [DCO](DCO), add a
`Signed-off-by` line to each commit message:
```
Signed-off-by: Joe Smith <joe.smith@email.com>
```
Please use your real name — pseudonymous/anonymous contributions are not
accepted. If your `user.name` and `user.email` git configs are set, `git
commit -s` adds the sign-off automatically. The DCO check on every pull
request enforces this, so unsigned commits will block merging.
## Pull requests
- Branch from `main`, keep changes focused, and include tests or docs when relevant.
- Sign off your commits with `git commit -s` (Developer Certificate of Origin).
- Sign off your commits with `git commit -s` (see
[Developer Certificate of Origin](#developer-certificate-of-origin) above).
- Fill in the PR template. For **UI / frontend changes**, check the
"UI / frontend change" box and attach a **video or images** in the `Demo`
section showing the new behaviour, so reviewers can see it without checking
+34
View File
@@ -0,0 +1,34 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
+19 -4
View File
@@ -122,10 +122,10 @@ uv tool install -q --python 3.12 git+https://github.com/omnigent-ai/omnigent.git
- **`uv`** (required). https://docs.astral.sh/uv/getting-started/installation/
The installer offers to set this up for you.
- **`git`** (required).
- **Node.js 22 LTS or newer** with **`npm`**, for the npm-installed coding
harnesses (Claude, Codex, OpenCode, Pi). `omnigent run` installs the
harness CLI you pick.
https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
- **Node.js 22 LTS or newer** with **`npm`** (for the coding-harness CLIs
installed by `omnigent run`) and **`pnpm`** (for the web UI). You can get
both from a single Node install; pnpm is available via
`corepack enable` or `npm install -g pnpm`.
- **Kiro CLI** (optional), for `omnigent kiro`: install with
`curl -fsSL https://cli.kiro.dev/install | bash`, then sign in with Kiro.
Kiro tool approvals stay answerable in the embedded Terminal; supported
@@ -267,6 +267,9 @@ omnigent hermes # Hermes Agent (Nous Research)
omnigent pi # Pi
```
Using OpenClaw? See the [OpenClaw integration guide](docs/openclaw.md) to import
its coding agents or drive a live OpenClaw Gateway session over ACP.
#### 🐙 Polly and 🟠🔵 Debby
Two example agents ship with the repo, and they make good first sessions:
@@ -274,6 +277,7 @@ Two example agents ship with the repo, and they make good first sessions:
```bash
omnigent run examples/polly/
omnigent run examples/debby/
omnigent run examples/deep-research/
# ...or on a different harness (sub-agents keep their own):
omnigent run examples/polly/ --harness <harness>
@@ -291,6 +295,12 @@ side by side. Type `/debate` and the heads critique each other for a few
rounds before converging. (She needs both a Claude and an OpenAI credential;
see step 3.)
**🔎 Deep Research** is a single agent that answers a question with a cited,
cross-checked report. It plans sub-queries, searches the live web and reads
full pages through an MCP search server, and verifies each claim across
independent sources. It's also the simplest example to copy from: one agent
plus one `tools/mcp/*.yaml` server, no sub-agents.
**Prefer the browser?** Start a server and register your machine as a host:
```bash
@@ -414,6 +424,11 @@ and they're in. Signup is invite-only.
omnigent run --fork <session_id>
```
Shared sessions identify model-visible messages with `[account]:` labels by
default. Set `OMNIGENT_SHARED_MESSAGE_ATTRIBUTION_ENABLED=0` to hide those
labels. This does not change stored authors, UI avatars, or who may approve or
run privileged actions.
> [!TIP]
> Want your team to sign in with the logins they already have (**Google,
> GitHub, Okta, Microsoft**)? Set `OMNIGENT_OIDC_ISSUER` plus a client ID
-307
View File
@@ -1,307 +0,0 @@
# Releasing omnigent
omnigent ships **three PyPI packages that version-lock together**:
| Package | What it is |
| --- | --- |
| `omnigent` | core wheel (bundles the `web` web UI) |
| `omnigent-client` | Python client SDK |
| `omnigent-ui-sdk` | terminal UI SDK |
`pip install omnigent==X` must resolve `omnigent-client==X` and
`omnigent-ui-sdk==X`. The pins are **lockstep** (the three packages co-version and
pin each other with `==`), so every release builds and publishes **all three at
one identical version**.
Releases are driven by **workflow dispatches, not by hand** (design:
`designs/RELEASE-AUTOMATION.md`). Every workflow below is idempotent —
re-dispatch with identical inputs after any failure and it converges — and
every dispatch requires the **admin or maintain** role on this repo.
## Where things run
- **Source of truth** (versions, tags, GitHub Releases): **`omnigent-ai/omnigent`**
— use the **OSS GitHub account** (the personal account with push/release rights
on the public repo).
- **Publishing to PyPI**: the central **secure-release repo**
**`databricks/secure-public-registry-releases-eng`**, `omnigent` workflow —
use whichever account has access to that repo. Publishing runs on hardened runner
groups with **OIDC Trusted Publishing (no stored secrets)** and a **mandatory
dependency scan**. This is why we don't publish from `omnigent-ai/omnigent`,
and why the pipeline is two dispatches per phase rather than one.
> The exact account handles — and how to request publish access — live in the
> internal release wiki; this public runbook refers to them only by role.
The legacy `.github/workflows/release-omnigent.yml` in this repo is a
**deprecated manual fallback only** — its tag-push trigger was removed so a tag
never double-publishes. Use the secure repo for real releases.
## Versioning model
- `main` always carries the **next** version with a `.dev0` suffix
(e.g. `0.6.0.dev0`) — never a clean released number. This matches
MLflow / Delta / Unity Catalog and keeps every `main` build PEP 440-ordered as
"ahead of the last release, not yet the next one".
- Releases are cut on **per-minor release branches** (`release/vX.Y.0`) and tagged
there (`vX.Y.Z`, rc tags `vX.Y.ZrcN`); patches (`vX.Y.1`, `vX.Y.2`, …) are
cherry-picked onto the same `release/vX.Y.0`. `main` is never tagged.
- Every release ships as an **rc first** (`0.6.0rc1` → … → `0.6.0`). rcs go to
**real PyPI** as PEP 440 pre-releases — a default `pip install omnigent`
never resolves them, and testers install with exact pins. TestPyPI is no
longer part of the standard flow.
## Docs staging
Because `main` carries the **next** version, the docs generated from merged PRs
describe a release that isn't out yet — so they must **not** deploy to the live
site on merge. Two workflows enforce this by staging onto a **per-minor docs
branch** on `omnigent-site` instead of `main`:
- **`doc-sync.yml`** — drafts prose docs for each merged PR that needs them.
- **`sync-openapi-to-site.yml`** — syncs the API reference (`openapi.json`).
Both derive the branch name from `omnigent/version.py` (`0.6.0.dev0``0.6-docs`)
and create it off site `main` the first time a doc PR lands in the cycle. All docs
for the `0.6` line — including patches — accumulate on `0.6-docs`. Each PR still
gets its own review, but merging one only lands it on the staging branch, not the
live site. At finalize time, the whole batch goes live at once (step 4 below).
---
## Standard flow
### rc phase (example: `0.6.0rc1`)
**1. Cut + tag — dispatch `Release` (`release.yml`), OSS account.**
```bash
gh workflow run release.yml --repo omnigent-ai/omnigent \
-f version=0.6.0rc1 -f dry_run=false
# optional: -f ref=<sha> to cut release/v0.6.0 from a specific commit (rc1 only);
# dry_run defaults to true — run once without -f dry_run to preview the plan.
```
What it does (all idempotent):
- asserts green CI on the base commit (escape hatch: `-f skip_ci_check=true`,
use deliberately — needed for a flaky check, or when the base commit ran no
checks at all, e.g. a cherry-pick that only touched `paths-ignore`d files);
- creates `release/v0.6.0` from `ref` (rc1) or reuses the existing branch head
(rc2+, final, patches — `ref` is ignored then);
- stamps the lockstep version via `scripts/update_versions.py` and regenerates
`uv.lock` with a clean public-PyPI resolution — **never hand-edit `uv.lock`
or run `uv lock` behind a proxy**; the workflow owns this now;
- commits `release: v0.6.0rc1`, tags, and pushes branch + tag with the
omnigent-ci App token, which fires the downstream automation:
`github-release.yml` (draft GH release, pre-release flagged),
`draft-release-notes.yml`, and `oss-publish-images.yml` (Docker);
- on the **first** cut of a cycle (rc1), dispatches `bump-version.yml`
(post-release) — **review and merge the `main → 0.7.0.dev0` bump PR
promptly**, so `doc-sync` keeps staging to the right docs branch.
**2. Publish to PyPI — dispatch the secure repo (EMU account).**
```bash
gh auth switch --user <secure-repo-account>
gh workflow run omnigent.yml --repo databricks/secure-public-registry-releases-eng \
-f ref=v0.6.0rc1 -f destination=pypi -f dry-run=true # gates rehearsal
gh workflow run omnigent.yml --repo databricks/secure-public-registry-releases-eng \
-f ref=v0.6.0rc1 -f destination=pypi -f dry-run=false # real publish
```
The dry run exercises build + dependency scan + the gates (lockstep
version/pins, web-UI-in-wheel, `twine check`, smoke-install) and the OIDC
token exchange without uploading. The real run binds the per-package
Trusted-Publisher environments (may gate on reviewer approval) and re-verifies
that `ref` is exactly the tag and points at the built commit.
**3. Validate from PyPI** (clean venv; exact pins resolve pre-releases;
behind a corporate network, point `--index-url` at your PyPI mirror
instead — this is a manual step on purpose: the secure repo's runners
cannot see a fresh index view, so no CI job can do it):
```bash
python -m venv /tmp/omni-rc && /tmp/omni-rc/bin/pip install \
--index-url https://pypi.org/simple/ \
omnigent==0.6.0rc1 omnigent-client==0.6.0rc1 omnigent-ui-sdk==0.6.0rc1
/tmp/omni-rc/bin/omnigent --version # expect 0.6.0rc1
```
The rc's GitHub draft stays **unpublished** — rc drafts are never published.
Need another candidate? Repeat with `0.6.0rc2` (fixes land on `release/v0.6.0`
first, via cherry-pick PRs or direct pushes; CI runs on `release/v*` pushes).
### Final phase (example: `0.6.0`)
1. **Cut + tag**: `gh workflow run release.yml -f version=0.6.0 -f dry_run=false`
— same as above; builds from the `release/v0.6.0` head.
2. **Publish to PyPI**: same secure-repo dispatches on `ref=v0.6.0`.
3. **Curate**: merge the `CHANGELOG.md` PR that `draft-release-notes.yml`
opened, and review/trim the curated notes in the `v0.6.0` draft on the
Releases page — whatever you leave becomes the website post.
4. **Finalize — dispatch `Finalize release` (`finalize-release.yml`)**:
```bash
gh workflow run finalize-release.yml --repo omnigent-ai/omnigent -f tag=v0.6.0
```
It verifies PyPI serves all three packages, the CHANGELOG PR isn't open,
and the **docs sweep**: no open PRs against `0.6-docs` on `omnigent-site`
(it lists any stragglers — get them reviewed and merged/closed, then
re-dispatch). Then it pauses on the **`publish-release` environment**;
approving it attests "I reviewed the draft notes". It publishes the release
as **Latest**, which fires:
- `publish-changelog.yml` → the site **release-post PR** and the
**`0.6-docs → main` docs-publish PR** — review and merge both;
- `update-homebrew.yml` → the **homebrew-tap bump PR** (new sdist pin +
regenerated resources; test-bot builds the bottles on it) — review the
resource diff, then apply the **`pr-pull`** label to bottle + merge.
### Patch release (example: `0.6.1`)
Cherry-pick the fixes onto `release/v0.6.0` (CI runs on the push), then run the
same flow with `version=0.6.1` — an rc first if the patch warrants one. `main`
does not change for a patch, and a patch never needs a new branch.
---
## One-time setup (repo admin)
- **`publish-release` environment** on `omnigent-ai/omnigent` with required
reviewers = the release managers. Without it the finalize publish job runs
ungated.
- **omnigent-ci App** installed on `omnigent-ai/homebrew-tap` (it already
covers `omnigent` and `omnigent-site`).
- **Tag ruleset** (recommended): restrict `v[0-9]*` create/update/delete to
the omnigent-ci App + admins, so no write-access account can start the
tag-push automation by hand.
## If a publish goes wrong (recovery)
**PyPI releases can't be deleted, only _yanked_**, and a version number once used
can never be reused. So:
- **Any workflow failed mid-run:** fix the cause and **re-dispatch with the
same inputs** — every step converges (branch exists → reused; version
stamped → no new commit; tag at the converged commit → no-op) or fails
loudly (tag elsewhere) rather than duplicating work.
- **Wrong commit tagged, nothing published yet:** delete the tag and draft
(`gh release delete vX.Y.Z`, `git push origin :refs/tags/vX.Y.Z`), then
re-dispatch `release.yml`.
- **rc is bad:** just cut the next rc — rcs are cheap and invisible to
default installs.
- **Prod publish partially succeeded** (e.g. two of three packages uploaded):
**yank** the published version(s) on PyPI (each affected project → *Manage* →
*Releases* → *Yank*) so installs don't resolve a half-published set, then cut
the next version with the fix. Don't try to overwrite — Trusted Publishing /
`twine` rejects re-uploading an existing version.
- Publishing uses **OIDC Trusted Publishing (no stored secrets)**, so a failed
run leaks nothing — fix forward to the next version.
---
## Rehearsing the pipeline (throwaway rc release)
To exercise the whole flow end to end without touching users, release a
deliberately **below-latest** rc on the dead `0.0` line. A below-latest rc is
inert everywhere that matters: the GitHub draft stays unpublished, Docker
publishes only the immutable version image tag (`:latest` / `:latest-rc` only
move for the highest version), the notes/site/homebrew workflows ignore rc
tags, `bump-main` skips itself (the version sorts below main's), and a
PEP 440 pre-release is never resolved by a default `pip install` — on real
PyPI or TestPyPI alike.
**Pick a version that has never touched the destination index.** PyPI
filenames are burned forever — even for yanked releases — so reusing a number
fails the upload with "File already exists". (`0.0.1rc1` itself is spent: it
reserved the PyPI project names in June 2026.) Confirm before starting; a 404
means the version is free:
```bash
curl -fsS https://pypi.org/pypi/omnigent/0.0.1rc2/json # expect 404
```
The examples below use `0.0.1rc2`; substitute the next free number.
1. **Plan (read-only)** — dry run is the default:
```bash
gh workflow run release.yml --repo omnigent-ai/omnigent -f version=0.0.1rc2
```
2. **Execute**: re-run with `-f dry_run=false`. Expect `release/v0.0.0` + tag
`v0.0.1rc2` pushed, the tag firing the draft-release and image workflows,
and CI running on the branch push. If the CI gate rejects main's head
(failing or still-pending checks), that's the gate working — wait, or
re-dispatch with `-f ref=<green sha>` / `-f skip_ci_check=true`.
Cancelled (superseded) runs only warn.
3. **Idempotency**: dispatch the exact same command again — it must no-op
("already at the converged release commit").
4. **Secure-repo publish.** Real PyPI is safe for a below-latest rc and
exercises the full prod path (the tag gate + the per-package reviewer
environments; approve all three) — so rehearse against
`destination=pypi`. `destination=test-pypi` also works, but skips the
prod tag gate and needs TestPyPI Trusted Publishers configured. Then
validate the published rc manually, exactly like a real release (step 3
of the standard flow).
```bash
gh workflow run omnigent.yml --repo databricks/secure-public-registry-releases-eng \
-f ref=v0.0.1rc2 -f destination=pypi -f dry-run=true # gates only
gh workflow run omnigent.yml --repo databricks/secure-public-registry-releases-eng \
-f ref=v0.0.1rc2 -f destination=pypi -f dry-run=false # real publish
```
5. **No-double-publish check** (optional): re-dispatching step 4's second
command must FAIL every leg with "File already exists" — PyPI
immutability doing its job. The publish is deliberately **write-only**:
the release runners cannot read the index, so there is no
already-published skip (a curl probe and twine's `--skip-existing` both
failed live for exactly that reason). A real partial publish is recovered
by yank + next version (see "If a publish goes wrong").
6. **Finalize gates (no side effects)**:
`gh workflow run finalize-release.yml -f tag=v0.0.1rc2` must fail fast
("not a final tag"), and `-f tag=v0.5.1` (any already-published release)
must no-op as already published.
Cleanup — delete everything the rehearsal minted on GitHub:
```bash
gh release delete v0.0.1rc2 --repo omnigent-ai/omnigent --cleanup-tag --yes
gh api -X DELETE 'repos/omnigent-ai/omnigent/git/refs/heads/release/v0.0.0'
```
Optionally delete the rehearsal image versions from GHCR. The PyPI side needs
no cleanup: the rc is invisible to default installs and only the version
number is spent — optionally yank it (*Manage → Releases → Yank*) for
tidiness.
---
## Break-glass appendix (manual fallback)
If the workflows are unavailable, the flow can be driven by hand — but keep two
rules even then:
1. **Never hand-edit `uv.lock` and never run `uv lock` behind a proxy.** Use
`bump-version.yml` (mode `pre-release`, `base_branch=release/vX.Y.0`) to
produce the bump as a PR with a cleanly regenerated lockfile, and merge it.
2. **Push tags from an account, not automation you improvised** — the tag push
must fire `github-release.yml` et al., which a `GITHUB_TOKEN`-authored push
would not.
```bash
gh auth switch --user <oss-account>
git fetch origin && git checkout -b release/v0.6.0 origin/main # rc1 only
gh workflow run bump-version.yml -f mode=pre-release -f new_version=0.6.0rc1 \
-f base_branch=release/v0.6.0 # then merge the PR
git fetch origin && git checkout release/v0.6.0 && git pull
git tag v0.6.0rc1 && git push origin release/v0.6.0 v0.6.0rc1 # explicit tag, NOT --tags
```
Then continue from step 2 of the standard flow (secure-repo dispatches). If the
GH draft wasn't created, `gh release create vX.Y.Z --draft --verify-tag
--title vX.Y.Z` recreates it. To re-run the notes/site halves for an existing
tag, dispatch `draft-release-notes.yml` or `publish-changelog.yml` with the
`tag` input; for the tap, dispatch `update-homebrew.yml`.
-3
View File
@@ -1,3 +0,0 @@
llm:
model: databricks-claude-haiku-4-5
profile: oss
+33
View File
@@ -177,6 +177,39 @@ remote DB.
256 MB default does not, so the Fly config pins a 1 GB machine, and the
Modal app pins `memory=1024` for the same reason.
## Serving: put an HTTP/2 proxy in front for many concurrent views
Each open session in the web UI holds a long-lived streaming HTTP
response (`GET /v1/sessions/{id}/stream`, `text/event-stream`) for as
long as the view is on screen. Over **HTTP/1.1 browsers cap concurrent
connections at ~6 per origin**, and every open stream occupies one of
those slots. Open a handful of windows or tabs against the same server
and the pool fills with held-open streams — then every *other* request
the UI makes (sending a message, the session list, `/health`, auth)
queues behind the cap and never fires. The symptom is the whole UI
appearing to freeze across all windows while the server itself is idle;
in DevTools → Network the stuck requests sit in **Stalled/Queued**, not
"waiting for server".
**Fix: serve over HTTP/2.** HTTP/2 multiplexes every stream over one
connection, so the per-origin cap stops mattering. `uvicorn` (the
server's ASGI server) speaks HTTP/1.1 only, so HTTP/2 comes from a
reverse proxy that terminates TLS in front of it — which most real
deploys already have:
- **The bundled Caddy overlay** (`docker/docker-compose.https.yaml`)
gives you this for free — Caddy negotiates HTTP/2 (and HTTP/3) over
TLS via ALPN with no extra config. See
[`docker/README.md`](docker/README.md#multi-user-mode-oidc).
- **The one-click platforms** (Render, Railway, Fly, Cloudflare) and
managed **Databricks** terminate TLS with HTTP/2 at their edge, so
they're already covered.
The gap is only when you expose the raw `:8000` HTTP/1.1 port directly
to browsers (e.g. `docker compose up` with no proxy, reached over
plain HTTP). That's fine for a single window; put a proxy in front once
you or your team routinely open several.
## Execution model
Omnigent runs in two pieces that talk to each other over a
+1
View File
@@ -83,6 +83,7 @@ sandbox:
boxlite:
image: docker.io/me/omnigent-host:latest # optional, shared; default: official
env: [OPENAI_API_KEY, GIT_TOKEN] # optional, shared; SERVER env var NAMES
disk_size_gb: 100 # optional, shared; default: SDK default
cloud:
endpoint: https://boxlite.example.com:8100 # selects CLOUD mode
```
+2 -2
View File
@@ -68,8 +68,8 @@ browser ───────────────► Worker (src/index.js)
```bash
cd deploy/cloudflare
npm install
npx wrangler login
pnpm install
pnpm exec wrangler login
```
## Deploy
+2 -4
View File
@@ -28,10 +28,8 @@ rm -rf omnigent/server/static/web-ui dist build omnigent.egg-info
if [[ "${SKIP_WEB_UI:-}" != "1" ]]; then
echo "==> Building web SPA into omnigent/server/static/web-ui/"
cd web
npm install
npm run build
cd "${REPO_ROOT}"
pnpm install --frozen-lockfile --filter web
pnpm --filter web run build
else
echo "==> SKIP_WEB_UI=1: skipping web build"
fi
+5
View File
@@ -153,6 +153,9 @@ try:
SqlAlchemyPermissionStore,
)
from omnigent.stores.policy_store.sqlalchemy_store import SqlAlchemyPolicyStore
from omnigent.stores.project_store.sqlalchemy_store import (
SqlAlchemyProjectStore,
)
from omnigent.stores.scheduled_task_store.sqlalchemy_store import (
SqlAlchemyScheduledTaskStore,
)
@@ -182,6 +185,7 @@ try:
file_comment_store = SqlAlchemyCommentStore(DB_URI)
permission_store = SqlAlchemyPermissionStore(DB_URI)
policy_store = SqlAlchemyPolicyStore(DB_URI)
project_store = SqlAlchemyProjectStore(DB_URI)
host_store = HostStore(DB_URI)
scheduled_task_store = SqlAlchemyScheduledTaskStore(DB_URI)
@@ -215,6 +219,7 @@ try:
comment_store=file_comment_store,
permission_store=permission_store,
policy_store=policy_store,
project_store=project_store,
host_store=host_store,
scheduled_task_store=scheduled_task_store,
auth_provider=auth_provider,
+19 -8
View File
@@ -34,13 +34,19 @@ POSTGRES_PASSWORD=change-me-please
# instance.
#
# A) Built-in accounts (DEFAULT — no env needed for laptop testing).
# First boot auto-creates an admin user (named after the OS
# user, falling back to "admin"), prints the password to
# `docker compose logs omnigent`, and saves it to
# /data/admin-credentials on the persistent volume. Admin
# invites teammates via the web UI Members page.
# No credentials are auto-generated. First boot prints a
# "No admin yet" line pointing at the base URL; you create the
# first admin (username + password) via the web Create-admin
# form, or pre-seed it with OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD.
# Admin invites teammates via the web UI Members page.
# For any deploy behind a public domain you MUST set
# OMNIGENT_ACCOUNTS_BASE_URL — see below.
# Security note for public deployments: POST /auth/setup is
# intentionally unauthenticated while no password-bearing account
# exists, so an instance exposed before its operator reaches the
# form can be claimed by the first visitor. Pre-seed
# OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD, or keep the service
# private until setup completes.
#
# B) Native OIDC (for shops with an existing IdP).
# Set the OMNIGENT_OIDC_* vars below (at minimum
@@ -82,9 +88,14 @@ POSTGRES_PASSWORD=change-me-please
# omnigent:8000 container address.
# OMNIGENT_ACCOUNTS_BASE_URL=https://omnigent.example.com
#
# Optional: pre-seed the initial admin password instead of the
# auto-generated one. Useful for headless / CI deploys where
# the operator can't read `docker compose logs`.
# Optional: pre-seed the initial admin password so bootstrap creates
# the first admin directly, instead of waiting for someone to claim it
# through the web Create-admin form. Useful for headless / CI deploys
# where that form can't be reached interactively. Nothing is ever
# auto-generated: without this (and without an OIDC issuer) a fresh
# instance stays in the needs-setup state and prints the setup URL to
# stderr — no password appears in the logs. See the security note under
# section A above for public deployments.
# OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD=
#
# Optional: session/invite/magic TTLs. Defaults shown.
+7
View File
@@ -5,6 +5,13 @@
# over the internal docker network — the omnigent container is no
# longer directly exposed to the host.
#
# Fronting the server with Caddy also gives you HTTP/2 (negotiated over
# TLS via ALPN, no extra config). That matters beyond HTTPS: each open
# session in the web UI holds a long-lived event-stream connection, and
# HTTP/1.1's ~6-connections-per-origin cap makes the UI stall once a
# user opens several windows/tabs. HTTP/2 multiplexes them over one
# connection, so this proxy is the fix. See ../README.md ("Serving").
#
# No ACME email is required: Let's Encrypt allows anonymous account
# registration, so the cert issues without one. If you WANT expiry /
# renewal notices, add a global email option above the site block:
+15 -7
View File
@@ -55,7 +55,7 @@
# Must satisfy pyproject requires-python (>=3.12); 3.11 fails dependency resolution.
ARG PYTHON_VERSION=3.12
ARG NODE_VERSION=20
ARG NODE_VERSION=22
# ── Web UI builder ──────────────────────────────────────
# Builds the web SPA so `docker build` works from a clean checkout —
@@ -70,12 +70,20 @@ ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim AS web-builder
ARG NPM_CONFIG_REGISTRY=
ENV NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY}
WORKDIR /web/web
# Manifests first so the install layer caches across pure source edits.
COPY web/package.json web/package-lock.json ./
RUN npm install --no-audit --no-fund
COPY web/ ./
RUN npm run build
WORKDIR /web
# Workspace manifests + lockfile first so the install layer caches across pure
# source edits. The electron package JSON is included so the root workspace is
# structurally complete, but we --filter web to avoid downloading Electron.
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY web/package.json ./web/
COPY web/electron/package.json ./web/electron/
RUN npm install -g pnpm@11.15.1
RUN pnpm install --frozen-lockfile --filter web
COPY web/ ./web/
RUN pnpm --filter web run build
# ── Python builder (shared: server + host) ──────────────
# Installs the package (and its transitive native-extension deps) into
+15 -6
View File
@@ -13,7 +13,7 @@
# -f deploy/docker/Dockerfile.ubi .
ARG PYTHON_VERSION=3.12
ARG NODE_VERSION=20
ARG NODE_VERSION=22
# ── Web UI builder ──────────────────────────────────────
FROM registry.access.redhat.com/ubi9/nodejs-${NODE_VERSION} AS web-builder
@@ -21,11 +21,20 @@ ARG NPM_CONFIG_REGISTRY=
ENV NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY}
USER 0
WORKDIR /web/web
COPY web/package.json web/package-lock.json ./
RUN npm install --no-audit --no-fund
COPY web/ ./
RUN npm run build
WORKDIR /web
# Workspace manifests + lockfile first so the install layer caches across pure
# source edits. The electron package JSON is included so the root workspace is
# structurally complete, but we --filter web to avoid downloading Electron.
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY web/package.json ./web/
COPY web/electron/package.json ./web/electron/
RUN npm install -g pnpm@11.15.1
RUN pnpm install --frozen-lockfile --filter web
COPY web/ ./web/
RUN pnpm --filter web run build
# ── Python builder (shared: server + host) ──────────────
FROM registry.access.redhat.com/ubi9/python-312 AS builder
+20 -13
View File
@@ -43,40 +43,47 @@ docker compose down -v
Built-in accounts auth: no IdP to register, no proxy to host.
This is the default — `docker compose up -d` brings it up with no
extra env wiring. First boot creates an admin user (named after the
operator's OS user, falling back to `admin` in headless containers)
with a random password that lands in the container logs and on the
persistent volume at `/data/admin-credentials`.
extra env wiring. No credentials are auto-generated. On first boot,
when no admin exists yet and none was pre-seeded, the server creates
nothing and prints:
```
→ No admin yet. Open <base_url> to create the first admin account (choose a username + password).
```
You then open the web UI's **Create admin** form (it appears while no
admin exists) and pick your own username + password.
For any deploy reachable through a public domain, also set the
external URL so invite links resolve correctly:
external URL so the printed link and invite links resolve correctly:
```bash
# Add to .env (bootstrap.sh already minted the cookie secret for you):
OMNIGENT_ACCOUNTS_BASE_URL=https://omnigent.example.com
docker compose up -d
docker compose logs omnigent | grep -A4 "Created initial admin"
docker compose logs omnigent # shows the "No admin yet" line with your base URL
```
Copy the random `password` from the log line into the web UI's
login form, then:
Once you've created the admin and signed in:
- Click your username in the top-right → **Members****Invite member**.
- Share the single-use URL with the teammate; they pick their own
username and password when they redeem it.
- Sign-out lives in the same account menu.
Headless deploy (CI, Cloud Run, etc.) where you can't read the
logs? Pre-seed the password:
Headless deploy (CI, Cloud Run, etc.) where you can't reach the
Create-admin form? Pre-seed the admin password so first boot creates
the admin directly:
```bash
OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD=<your-strong-password>
```
The persistent password file is at `/data/admin-credentials` on
the `artifact-data` volume — survives `docker compose restart`,
deleted by `docker compose down -v`.
`OMNIGENT_ADMIN_CREDENTIALS_PATH` (set to `/data/admin-credentials`
in `docker-compose.yaml`) anchors the persistent state directory on
the `artifact-data` volume — it survives `docker compose restart` and
is deleted by `docker compose down -v`.
## Multi-user mode (OIDC)
+5 -4
View File
@@ -94,8 +94,9 @@ echo
echo "✓ deploy/docker/.env is ready. Next:"
echo " docker compose up -d && docker compose logs omnigent"
echo
echo " Accounts mode is the default — the first-boot admin password"
echo " lands in the logs and in /data/admin-credentials on the"
echo " persistent volume. For any public-domain deploy also set:"
echo " Accounts mode is the default — no credentials are auto-generated."
echo " First boot prints a 'No admin yet' line; open that URL and create"
echo " the first admin (username + password) via the web form. For any"
echo " public-domain deploy also set:"
echo " OMNIGENT_ACCOUNTS_BASE_URL=<your public URL>"
echo " in .env so invite links resolve to the right host."
echo " in .env so that link and invite links resolve to the right host."
+14 -6
View File
@@ -8,9 +8,11 @@
# open http://localhost:8000 # web UI; start a local runner per the prompt
#
# Auth modes (OMNIGENT_AUTH_PROVIDER):
# - accounts (DEFAULT) — built-in accounts, no IdP needed. First
# boot prints the admin password to `docker compose logs` and
# saves it to /data/admin-credentials. Set
# - accounts (DEFAULT) — built-in accounts, no IdP needed. No
# credentials are auto-generated; first boot prints a "No admin
# yet" line and you create the first admin (username + password)
# via the web Create-admin form, or pre-seed it with
# OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD. Set
# OMNIGENT_ACCOUNTS_BASE_URL for any deploy reachable behind
# a public domain (defaults to http://<HOST>:<PORT> otherwise).
# - oidc — bring your own IdP. Set OMNIGENT_OIDC_* vars — see
@@ -60,9 +62,15 @@ services:
ARTIFACT_DIR: /data/artifacts
HOST: 0.0.0.0
PORT: "8000"
# Pin the admin-credentials path to the persistent volume so
# the file survives container restarts. Empty/unset would
# write to /root/.omnigent/ inside the ephemeral container.
# Anchor the server's data dir on the persistent volume so
# file-backed operator config survives container restarts:
# the admin roster (/data/admins) and allowed-domains file
# (/data/allowed_domains), plus artifacts mounted elsewhere
# in the same volume. Account rows and password hashes live in
# PostgreSQL (the postgres-data volume), not here. The server
# resolves its data dir from this path's parent (/data);
# empty/unset would fall back to /root/.omnigent/ inside the
# ephemeral container.
OMNIGENT_ADMIN_CREDENTIALS_PATH: /data/admin-credentials
# ── Auth ─────────────────────────────────────────
+71 -2
View File
@@ -21,7 +21,7 @@ module importable for testing / tooling without a live database.
Configuration is via environment variables:
DATABASE_URL Required. SQLAlchemy URL. Both PaaS-style URLs
(``postgresql://user:pw@host:5432/db``,
(``postgresql://<user>:<password>@host:5432/db``,
``postgres://...``) and the explicit psycopg3
form (``postgresql+psycopg://...``) are accepted;
the prefix is normalized automatically.
@@ -254,6 +254,25 @@ def _select_artifact_store(resolved_config: _ResolvedConfig) -> ArtifactStore:
return LocalArtifactStore(str(resolved_config.artifact_dir))
def _build_local_llm_routing_client(
server_llm: Any, # type: ignore[explicit-any] # LLMConfig | None
) -> Any | None: # type: ignore[explicit-any] # LLMRoutingClient | None
if server_llm is None:
return None
from omnigent.runtime.policies.builder import (
_build_policy_llm_client,
_resolve_server_llm_connection,
)
conn = _resolve_server_llm_connection(server_llm)
policy_client = _build_policy_llm_client(server_llm, conn)
if policy_client is None:
return None
from omnigent.server.smart_routing import LLMRoutingClient
return LLMRoutingClient(policy_client)
def build_app(resolved_config: _ResolvedConfig | None = None) -> _BuiltApp:
"""Resolve config if needed, wire the stores, and build the app.
@@ -290,6 +309,7 @@ def build_app(resolved_config: _ResolvedConfig | None = None) -> _BuiltApp:
SqlAlchemyPermissionStore,
)
from omnigent.stores.policy_store.sqlalchemy_store import SqlAlchemyPolicyStore
from omnigent.stores.project_store.sqlalchemy_store import SqlAlchemyProjectStore
from omnigent.stores.scheduled_task_store.sqlalchemy_store import (
SqlAlchemyScheduledTaskStore,
)
@@ -304,6 +324,7 @@ def build_app(resolved_config: _ResolvedConfig | None = None) -> _BuiltApp:
host_store = HostStore(database_url)
policy_store = SqlAlchemyPolicyStore(database_url)
scheduled_task_store = SqlAlchemyScheduledTaskStore(database_url)
project_store = SqlAlchemyProjectStore(database_url)
# Fail startup loud on a malformed `sandbox:` section (an operator
# typo should not surface as a runtime 502 on the first managed
# session); the startup catch-all below logs it.
@@ -315,9 +336,56 @@ def build_app(resolved_config: _ResolvedConfig | None = None) -> _BuiltApp:
cache_dir=artifact_dir / ".cache",
)
from omnigent.spec import parse_default_policies, parse_server_llm
server_llm = parse_server_llm(cfg.get("llm"))
routing_cfg = cfg.get("routing")
if isinstance(routing_cfg, dict) and routing_cfg.get("provider") == "external":
from omnigent.server.smart_routing import ExternalRoutingClient, _bearer_auth
base_url = (routing_cfg.get("base_url") or "").strip()
router_name = (routing_cfg.get("router_name") or "").strip()
api_key_raw = (routing_cfg.get("api_key") or "").strip()
profile = (routing_cfg.get("profile") or "").strip()
raw_prefixes = routing_cfg.get("model_prefix")
if isinstance(raw_prefixes, str):
raw_prefixes = [raw_prefixes]
model_prefixes = (
[p.strip() for p in raw_prefixes if isinstance(p, str) and p.strip()]
if isinstance(raw_prefixes, list)
else []
)
if base_url and router_name:
auth = None
databricks_profile: str | None = None
if api_key_raw:
from omnigent.spec import expand_env_vars
auth = _bearer_auth(expand_env_vars({"api_key": api_key_raw})["api_key"])
elif profile:
databricks_profile = profile
routing_client = ExternalRoutingClient(
base_url=base_url,
router_name=router_name,
auth=auth,
databricks_profile=databricks_profile,
model_prefixes=model_prefixes,
)
else:
routing_client = None
else:
routing_client = _build_local_llm_routing_client(server_llm)
caps = RuntimeCaps(
default_policies=parse_default_policies(cfg.get("policies")),
llm=server_llm,
routing_client=routing_client,
)
init_runtime(
agent_cache=agent_cache,
caps=RuntimeCaps(),
caps=caps,
agent_store=agent_store,
file_store=file_store,
conversation_store=conversation_store,
@@ -353,6 +421,7 @@ def build_app(resolved_config: _ResolvedConfig | None = None) -> _BuiltApp:
policy_store=policy_store,
host_store=host_store,
scheduled_task_store=scheduled_task_store,
project_store=project_store,
auth_provider=auth_provider,
account_store=account_store,
# Non-secret auth settings from the config file (admins are the
+14 -5
View File
@@ -35,14 +35,23 @@ Then:
1. **Memory**`fly.toml` pins a **1 GB** machine (`[[vm]] memory = "1gb"`).
The server idles around ~275 MB RSS, so Fly's 256 MB default OOM-loops.
Keep it at 1 GB (or `fly scale memory 1024 -a <your-app>` if you changed it).
2. **Admin password** prints once in the first-boot logs:
2. **Create the first admin.** No credentials are auto-generated. First boot
prints a "No admin yet" line pointing at your `*.fly.dev` URL:
```bash
fly logs -a <your-app>
```
Look for `Created initial admin account ... password: <generated>` (also
written to `/data/admin-credentials` on the volume).
3. Open `https://<your-app>.fly.dev`, log in as `admin`. The cookie secret and
base URL (`FLY_APP_NAME` -> `<app>.fly.dev`) are handled automatically.
Open `https://<your-app>.fly.dev` and use the web Create-admin form to pick
your own username + password. For a headless deploy, pre-seed
`OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` (`fly secrets set …`) before first
boot to create the admin directly instead.
3. Log in with the admin you just created. The cookie secret and base URL
(`FLY_APP_NAME` -> `<app>.fly.dev`) are handled automatically.
> **Security note for public deployments:** `POST /auth/setup` is
> unauthenticated while no password-bearing account exists, so an instance
> exposed before you reach the Create-admin form can be claimed by the first
> visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`, or complete setup
> promptly after the deploy goes live.
## Deploy (Fly web-UI Launch)
+10 -4
View File
@@ -35,15 +35,21 @@ files plus two secrets.
| `DATABASE_URL` | variable | `sqlite:////data/artifacts/chat.db` |
| `OMNIGENT_ACCOUNTS_COOKIE_SECRET` | secret | `openssl rand -hex 32` (pin it: ephemeral disk would otherwise drop sessions on restart) |
4. The Space builds + boots. Admin password is in the Space **Logs** on first
boot. The base URL is auto-detected from `SPACE_HOST`, so it needs no manual
set.
4. The Space builds + boots. No admin credential is auto-generated: first boot
prints a "No admin yet" line to the Space **Logs**, and the Space serves a
web Create-admin form where you pick your own username + password. The base
URL is auto-detected from `SPACE_HOST`, so it needs no manual set. To create
the admin directly instead, add `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` as a
Space secret before first boot.
5. **Log in via the direct URL** `https://<user>-<space>.hf.space` in its own
tab — not HF's embedded preview. The session cookie is `SameSite=Lax`, which
browsers won't send inside HF's cross-origin iframe, so logging in from the
embedded view loops back to `/login`. The direct URL is top-level
(same-site), so login sticks. Make the Space **Public** so the direct URL
isn't gated.
isn't gated — but note the Create-admin form is unauthenticated until the
first admin is claimed, so a public Space can be claimed by the first
visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` (step 4) or claim
the admin immediately after it goes public.
## Want persistence / multi-user later?
@@ -138,6 +138,38 @@ writing nothing to disk — use HTTPS repository URLs. Details by provider match
| `resources` | Optional `requests` / `limits` (`cpu` / `memory`) override. |
| `in_cluster` | Optional cluster-config source: `true` (in-cluster SA only), `false` (kubeconfig only), omit (try in-cluster, then kubeconfig). |
| `kubeconfig` | Optional kubeconfig path for the out-of-cluster fallback (env: `OMNIGENT_KUBERNETES_KUBECONFIG`). |
| `pvc_mounts` | Optional pre-created PersistentVolumeClaims mounted into every runner Pod — see [Persistent storage mounts](#persistent-storage-mounts-pvc_mounts). |
## Persistent storage mounts (`pvc_mounts`)
Runner Pods are ephemeral by design — the workspace lives on an `emptyDir` and
dies with the Pod. To expose durable data (datasets, model caches, shared
output directories) mount pre-created PersistentVolumeClaims:
1. Create the PV/PVC **in the runner namespace** (`omnigent-sandboxes`) out of
band — via your GitOps repo, with whatever backend your cluster provides
(NFS/SMB CSI drivers, SAN, cloud disks). Omnigent only references the claim;
it never creates volumes, so the server RBAC stays unchanged.
2. List the claims under `sandbox.kubernetes.pvc_mounts` (see
`sandbox-config.yaml`). Mount paths may not overlap `/home/omnigent`, the
OS directories, or their ancestors (e.g. `/home`, `/var`) — the server
rejects such config at startup.
Caveats:
- **Multiple runners share writable claims concurrently** — use a
`ReadWriteMany`-capable backend (NFS/SMB/CephFS) for anything writable, and
prefer `read_only: true` (the default) everywhere else: a writable shared
mount lets one session's agent read and modify what another session wrote,
and anything written there outlives the Pod and its launch token.
- Runner Pods run as uid/gid 1000660000 with `fsGroup`. NFS `root_squash` and
SMB ownership mapping must permit that identity (export to the uid, or use
CSI mount options like `uid=`/`gid=` for SMB); `fsGroupChangePolicy:
OnRootMismatch` avoids re-chowning large exports on every start.
- `ReadWriteOnce` claims pin all runners to one node — combine with
`node_selector` deliberately, or the second Pod sits `Pending`.
- A mount visible in the Pod is not automatically visible to a harness's own
OS-level sandbox (OmniBox path grants are separate).
To verify `host_config` end to end against a live cluster, run
`python tests/e2e/integrations/deploy/kubernetes/e2e_managed_host_config.py
@@ -45,5 +45,9 @@ data:
# resources: # runner Pod sizing (defaults: 0.5-2 cpu / 1-4Gi)
# requests: {cpu: "500m", memory: "1Gi"}
# limits: {cpu: "2", memory: "4Gi"}
# pvc_mounts: # pre-created PVCs (in the runner namespace) mounted into every runner Pod
# - claim_name: omnigent-datasets
# mount_path: /mnt/datasets
# read_only: true # default true; set false only for claims meant as shared scratch
# in_cluster: true # config source: true=in-cluster SA only, false=kubeconfig only, omit=try both
# kubeconfig: /path/to/config # out-of-cluster kubeconfig (env: OMNIGENT_KUBERNETES_KUBECONFIG)
+12 -9
View File
@@ -50,23 +50,26 @@ the secret and redeploy.
The first boot runs DB migrations over the network (~1 minute on Neon).
**Get the admin password:** the first boot prints it to the app log:
**Create the first admin.** No credentials are auto-generated. First boot
prints a "No admin yet" line pointing at your `*.modal.run` URL:
```bash
modal app logs omnigent
```
```
✓ Created initial admin account (accounts auth provider).
password: <generated>
```
Open that URL and use the web Create-admin form to pick your own username +
password, then invite teammates from **Members** in the web UI.
Log in as the admin and invite teammates from **Members** in the web UI.
> To set a known admin password instead, add
> `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD=<password>` to the
> To create the admin directly instead of claiming it through the web form,
> add `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD=<password>` to the
> `omnigent-deploy` secret before the first deploy.
> **Security note for public deployments:** `POST /auth/setup` is
> unauthenticated while no password-bearing account exists, so an instance
> exposed before you reach the Create-admin form can be claimed by the first
> visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`, or complete setup
> promptly after the deploy goes live.
### Modal-specific caveats
- **2 MiB WebSocket message cap.** Modal's ingress limits WebSocket
+12 -8
View File
@@ -47,14 +47,12 @@ steps below are validated end-to-end:
reference value simply hadn't propagated yet — **redeploy** and it resolves.
(To confirm, the app service should have a `DATABASE_URL` variable
referencing the Postgres service, e.g. `${{Postgres.DATABASE_URL}}`.)
3. **Get the admin password** from the first-boot **Deploy logs** (printed once;
idempotent — later boots don't reprint):
```
✓ Created initial admin account (accounts auth provider).
password: <generated>
```
It's also written to `/data/admin-credentials`.
4. Open the URL, log in as `admin`, invite teammates from **Members**.
3. **Create the first admin.** No credentials are auto-generated. The
first-boot **Deploy logs** print a "No admin yet" line pointing at your
`*.up.railway.app` URL (printed once; idempotent — later boots don't
reprint). Open that URL and use the web Create-admin form to pick your own
username + password.
4. Log in with the admin you just created, invite teammates from **Members**.
> **`HOST` is handled automatically.** Railway injects `HOST=[::]`, which a
> socket bind can't use and which Railway's IPv4 edge can't reach; the
@@ -69,6 +67,12 @@ steps below are validated end-to-end:
> pin a known admin password, set `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`
> before first boot.
> **Security note for public deployments:** `POST /auth/setup` is
> unauthenticated while no password-bearing account exists, so an instance
> exposed before you reach the Create-admin form can be claimed by the first
> visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`, or complete setup
> promptly after the deploy goes live.
## Use your own IdP instead (OIDC)
Prefer GitHub / Google / Okta login over built-in accounts? Switch the provider
+18 -13
View File
@@ -23,8 +23,9 @@ The `render.yaml` blueprint at the repo root defines:
- **omnigent-db** (`basic-256mb` managed Postgres) — `DATABASE_URL` is injected
into the service automatically
- **artifact-data** (10 GB persistent disk) — mounted at `/data` so server
config, first-boot credentials, cookie secrets, and agent artifacts survive
redeploys. Artifacts live under `/data/artifacts`.
config, the auto-minted cookie secret, and agent artifacts survive redeploys.
Artifacts live under `/data/artifacts`. (Account rows and password hashes
live in the managed Postgres, not on the disk.)
## Quickstart (built-in accounts — the default)
@@ -34,18 +35,22 @@ mints its own cookie secret and auto-detects its public URL from Render.
1. Click the Deploy to Render button above → **Apply**. Wait ~35 min for the
image pull + health check.
2. **Get the admin password:** open the service → **Logs** and find the
first-boot block:
```
✓ Created initial admin account (accounts auth provider).
password: <generated>
```
(also written to `/data/admin-credentials` on the disk; printed once).
3. Open your `https://<service>.onrender.com` URL, log in as the admin, and
invite teammates from **Members** in the web UI.
2. **Create the first admin.** No credentials are auto-generated. Open your
`https://<service>.onrender.com` URL — a fresh instance shows a
Create-admin form where you pick your own username + password. (First-boot
**Logs** also print a "No admin yet" line with that URL.)
3. Log in as the admin you just created, and invite teammates from **Members**
in the web UI.
> To set a known admin password instead of the generated one, add
> `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` in the dashboard before first boot.
> To create the admin directly instead of claiming it through the web form
> (e.g. a headless deploy), add `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` in the
> dashboard before first boot.
> **Security note for public deployments:** `POST /auth/setup` is
> unauthenticated while no password-bearing account exists, so an instance
> exposed before you reach the Create-admin form can be claimed by the first
> visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`, or complete setup
> promptly after the deploy goes live.
## Use your own IdP instead (OIDC)
+1 -1
View File
@@ -339,7 +339,7 @@ injection (agent holds a placeholder, proxy swaps the real secret). Mapped under
### 2.G Onboarding, credentials & auth (incl. token refresh) ✅
**First-run setup**`omnigent setup` wizard (`onboarding/wizard.py`): provider picker, **ambient detection**
**First-run setup**`omnigent setup` (`cli.py` / `cli_config.py`): provider picker, **ambient detection**
(`onboarding/ambient.py` scans installed CLIs — Claude.app, Codex, LM Studio), saves `~/.omnigent/config.yaml`.
Databricks profile aliasing reuses same-host profiles to avoid redundant OAuth (`onboarding/setup.py:_alias_profile`).
+56 -50
View File
@@ -46,19 +46,12 @@
The Slack integration (`integrations/slack/`) is a standalone Socket-Mode
process that calls each user's Omnigent server over HTTP + SSE
(`OmnigentClient` / `OmnigentClientPool`). Today it sends **every request
unauthenticated**: the pool is *"one unauthenticated client per server URL"*
(`omnigent.py:337`), and any server with auth enabled returns 401, which the
bot converts into a dead-end *"authentication … isn't supported yet"* setup
error (`omnigent.py:23`, `setup.py:144`).
So the bot only works against auth-disabled servers, and when it does work the
server sees a single shared anonymous identity — it cannot tell one Slack user
from another, cannot scope permissions, and cannot audit who did what.
We want each Slack user's turns to reach the Omnigent server **as that user's
own authenticated identity**, without the Slack process ever handling the
user's Omnigent credentials.
(`OmnigentClient` / `OmnigentClientPool`). Each Slack user's turns must reach
the Omnigent server **as that user's own authenticated identity** — so the
server can scope permissions and audit who did what — **without the Slack
process ever handling the user's Omnigent credentials**. An unauthenticated
client can only reach auth-disabled servers, and would present one shared
anonymous identity the server can't distinguish per user.
## Topology and trust
@@ -85,28 +78,25 @@ Role mapping:
| Resource Owner | The Slack user, authenticating in their browser |
| Out-of-band channel | Slack (delivers the verification link only) |
## What already exists (reused, not rebuilt)
## Shared substrate (reused, not rebuilt)
RFC 8628 primitives are absent (no `device_code` / `user_code` /
`verification_uri` anywhere), but the substrate is all present:
The device grant builds on existing server primitives:
- **Poll-endpoint shape** — `POST /auth/cli-login` + `GET /auth/cli-poll` with
202-pending / 200-done / 410-expired semantics (`routes/auth.py:484`).
- **Atomic single-use token redemption** — `SqlAlchemyAccountStore.redeem_token`
uses `UPDATE … WHERE redeemed_at IS NULL` + rowcount so at most one caller
wins under concurrency (`accounts_store.py:329`). The new grant store copies
this pattern.
wins under concurrency (`accounts_store.py`). The grant store follows the
same pattern.
- **Session JWT minting** — `mint_session_token(user_id, secret, ttl, provider)`
(`oidc.py:53`), HS256 with `sub`/`iat`/`exp`/`provider`.
- **Bearer validation** — `UnifiedAuthProvider._check_cookie` already accepts
(`oidc.py`), HS256 with `sub`/`iat`/`exp`/`provider`.
- **Bearer validation** — `UnifiedAuthProvider._check_cookie` accepts
`Authorization: Bearer <jwt>` and validates the same claim shape
(`auth.py:477`). Delegated access tokens validate through this path unchanged.
- **Browser consent under accounts mode** — the `accounts` provider already
(`auth.py`). Delegated access tokens validate through this path unchanged.
- **Browser consent under accounts mode** — the `accounts` provider
establishes the browser identity via its session cookie; the consent page
runs behind it. (This is why the grant mounts in accounts mode only — see
the mount restriction below.)
- **Open-redirect hardening** — `_sanitize_return_to` (`routes/auth.py:150`) is
reused for the post-login bounce back to the consent page.
- **Open-redirect hardening** — `_sanitize_return_to` (`routes/auth.py`) guards
the post-login bounce back to the consent page.
## Design decisions (agreed)
@@ -120,14 +110,10 @@ RFC 8628 primitives are absent (no `device_code` / `user_code` /
only an authorized client can drive the flow. The **browser** endpoints
(consent GET / approve / deny) are never gated by it — the user's browser
doesn't hold the secret; their trust is the session cookie + Origin check.
Unset ⇒ endpoints stay public (backward compatible).
*History:* the secret was implemented, removed, then reintroduced as
opt-in. It was removed when the Slack client accepted a **user-supplied**
server URL — shipping a shared secret to an arbitrary user-typed host was a
secret-exfiltration/SSRF path. That objection is now gone: the Slack socket
server's target is a **fixed operator config** (`OMNIGENT_SERVER_URL`), not
a user-supplied URL, so the secret only ever travels to the trusted server.
Unset ⇒ endpoints stay public (backward compatible). Shipping the secret to
the Slack client is safe because its target is a **fixed operator config**
(`OMNIGENT_SERVER_URL`), not a user-supplied URL, so the secret only ever
travels to the trusted server.
2. **Refresh tokens** — short-lived access tokens (≤ 1 h) plus a rotating,
revocable refresh token, with a 30-day absolute grant lifetime. The Slack
server refreshes silently; a stolen access token expires quickly and a grant
@@ -149,15 +135,21 @@ RFC 8628 primitives are absent (no `device_code` / `user_code` /
verification_uri_complete, # verification_uri?user_code=XYZ
expires_in: 600, interval: 5 }
3. Slack server shows the verification link in the setup modal (initiator
only). The device_code is NOT included — it never leaves the server
pair; only the user_code (in verification_uri_complete) does.
3. Slack server shows the verification link (verification_uri_complete,
code prefilled for one-click) in the setup modal (initiator only),
plus the user_code so the user can confirm the match. The
device_code is NOT included — it never leaves the server pair.
4. User clicks → Omnigent consent page (verification_uri).
Browser authenticates via the server's accounts provider.
Page shows: "<client_id> is requesting permission to act as YOU
(alice@example.com) on this Omnigent server. [Approve] [Deny]"
plus a warning to approve only a login the user personally started.
The page REQUIRES a login started for THIS flow: if the browser's
session predates the grant (session iat < grant.created_at), it
bounces through the login page with ?reauth=1 — which forces a fresh
password entry even for an already-signed-in user — and returns here.
Once re-authenticated, the page shows: "<client_id> is requesting
permission to act as YOU (alice@example.com) on this Omnigent server.
[Approve] [Deny]" plus a warning to approve only a self-started login.
The forced re-auth means a grant can't be approved by one reflexive
click on a link the user didn't personally start (see threat #2).
5. User approves → the grant is bound to the authenticated identity
(alice@…). client_id is recorded for display/audit only, never as
@@ -197,9 +189,8 @@ cli-ticket flow and never mounts these routes; header mode has no
server-mintable identity — see `create_device_auth_router`, which raises if
constructed for any other source). The `device_grants` table is created
unconditionally by the migration regardless of the flag; only the router
mount is gated. This router also **owns** `mint_delegated_token` and
`DELEGATED_SCOPE` (moved here from `oidc.py`, which retains only
`mint_session_token` / `mint_session_cookie`).
mount is gated. This router **owns** `mint_delegated_token` and
`DELEGATED_SCOPE`.
- `POST /oauth/device/authorize`**public** (rate-limited). Generates a
high-entropy `device_code` (`secrets.token_urlsafe`, stored **hashed**), a
@@ -275,9 +266,8 @@ HS256 shape (so `_check_cookie` accepts them) plus four delegated-only claims:
## Slack-side changes
- **`oauth.py` (new)** — device-authorize → post ephemeral link → poll token
endpoint (respecting `interval` / `slow_down`) → store tokens. Replaces the
`AuthRequiredError` dead-end.
- **`oauth.py`** — device-authorize → post ephemeral link → poll token
endpoint (respecting `interval` / `slow_down`) → store tokens.
- **`omnigent.py`** — attach `Authorization: Bearer` per
`(server_url, slack_user_id)`; on 401, refresh once and retry; on refresh
failure, surface a re-login prompt. `OmnigentClientPool` keys clients by
@@ -285,8 +275,8 @@ HS256 shape (so `_check_cookie` accepts them) plus four delegated-only claims:
- **`store.py`** — new `oauth_tokens` table `(team_id, user_id, server_url)`
access/refresh **encrypted at rest** (key from env / secret manager, never in
the DB). `/omnigent logout``POST /oauth/revoke` + local delete.
- **`setup.py`** — validation uses the user's token; auth-enabled servers become
supported rather than rejected.
- **`setup.py`** — validation uses the user's token, so auth-enabled servers
are supported.
- **`config.py`** — holds the local encryption key for token storage.
## Security analysis
@@ -294,7 +284,7 @@ HS256 shape (so `_check_cookie` accepts them) plus four delegated-only claims:
| # | Threat | Mitigation |
|---|--------|-----------|
| 1 | `device_code` leak → token theft | Never transits Slack or the user — only `verification_uri_complete` (a `user_code`) does. Stored hashed; single-use. |
| 2 | Link misdelivery / phishing another user | Link shown to the initiator only (in their own setup modal). Consent page names the exact Omnigent identity the grant will act as and the requesting `client_id`, and warns to approve only a self-initiated login. |
| 2 | Link misdelivery / phishing another user | Link shown to the initiator only (in their own setup modal). **Consent requires a login started FOR this flow: the consent page rejects a session whose `iat` predates the grant and bounces through the login page with `reauth=1`, forcing a fresh password entry even for an already-signed-in user.** So an attacker-initiated flow can't be approved by a single reflexive click — the victim must deliberately re-enter their password against a screen naming the exact Omnigent identity and requesting `client_id`. The gate is enforced on both the consent GET and the approve POST. |
| 3 | Anyone can initiate/poll (public client) | Cheap `pending` state grants nothing until an authenticated user approves. `POST /oauth/device/authorize` is rate-limited per client IP (10/60s → 429 `slow_down`); short (10 min) `device_code` expiry; `slow_down` enforced server-side on aggressive polling; expired grants purged opportunistically. |
| 4 | Slack SQLite exfiltration → mass impersonation | Tokens **encrypted at rest**; access tokens short-lived (≤ 1 h); refresh tokens revocable. Bounded, centrally killable window. |
| 5 | Compromised Slack server acts as all users (inherent to delegation) | Reduced scope (no admin), short TTL + refresh rotation, per-grant revocation, **absolute grant lifetime (30 d) enforced on refresh** so even an un-revoked grant dies, and an `act`-claim audit trail. |
@@ -315,6 +305,17 @@ token.
When no client secret is configured the endpoints are **public**, so
initiation is open — the defense is layered, not a gate:
- **Forced re-authentication at consent.** Consent requires a login started for
THIS flow: the consent page (and the approve POST) reject a session whose
`iat` predates the grant's `created_at` and bounce through the login page with
`reauth=1`, which forces a fresh password entry even for an already-signed-in
user. This defeats the reflex-approve variant of the attack — a victim handed a
one-click link (even one with the code prefilled) still can't bind the grant
without deliberately re-entering their password against a screen naming the
exact identity and client. (`device_auth.py` `_session_iat` + the
`reauth=1` bounce; `LoginPage.tsx` suppresses its already-signed-in
auto-return under `reauth=1`.) The prefilled one-click link is therefore
retained for convenience — the re-auth step, not code handling, is the gate.
- The consent page prominently **warns** the user to approve only a login they
personally started and to match the code shown by the application.
- The delegated scope excludes admin / user-management endpoints.
@@ -322,6 +323,11 @@ initiation is open — the defense is layered, not a gate:
grant self-expires even if never revoked.
- Initiation is rate-limited per IP; nothing is granted until a real user
authenticates and approves in their own browser.
- **Startup warning.** When the grant is mounted on a multi-user (accounts)
server with `OMNIGENT_DEVICE_CLIENT_SECRET` unset, the server logs a loud
warning at startup that the authorize endpoint is public — nudging the
operator to opt into the secret rather than leaving initiation open unknowingly
(`app.py`, at the device-router mount).
Setting `OMNIGENT_DEVICE_CLIENT_SECRET` closes initiation entirely to
unauthorized callers: without the matching `X-Omnigent-Client-Secret` header,

Some files were not shown because too many files have changed in this diff Show More