d3b0ec080d175ab4c8f45523b16be4cfec00a356
429 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d3b0ec080d |
fix(pi): also redact equals-joined system-prompt argv form
Harden _redact_argv_for_log so a future refactor that switches to the equals-joined flag form (--append-system-prompt=<secret> / --system-prompt=<secret>) does not leak the system prompt into the PiExecutor spawn debug log. The two-token form was already handled; this adds the inline-value form, keeping the flag name visible and replacing the value with a length-only placeholder. Adds unit tests for the equals-joined form and the two-token --system-prompt form. Co-authored-by: Isaac |
||
|
|
e8f176d282 |
test(pi): cover system prompt redaction through run_turn
Add a full PiExecutor.run_turn regression test so F92 is covered at the executor boundary: Pi still receives the system prompt in argv, but the debug spawn log only includes the redacted length placeholder. |
||
|
|
f6f16db4b2 |
fix(pi): redact system prompt from PiExecutor spawn debug log (F92)
The debug log line at PiExecutor spawn time joined the full argv, leaking the entire --append-system-prompt value into logs. Redact the system-prompt value to a length-only placeholder ([system prompt N chars]) while keeping all other flags visible for debugging. Adds tests asserting the redaction helper hides the prompt and that the spawn debug log line never contains a known test prompt string. |
||
|
|
5af8cd40b2 |
perf(conversation-store): maintain next_position counter to drop per-append MAX(position) aggregate (#696)
append() computed the next item position by running `SELECT coalesce(max(position), -1)` over conversation_items on every call. This replaces that with a maintained `next_position` counter on the conversations row: append() reads it, allocates contiguous positions, and advances it under the existing `_lock_conversation` serialization — O(1), one fewer query per write, and collision-free. - New nullable `conversations.next_position` column (Alembic n1a2b3c4d5e6) plus a model-level default of 0 for new rows. - Backwards compatible: rows created before the column read NULL; append() falls back to a one-time MAX(position) scan and persists the counter, so the next append is aggregate-free. - fork_conversation seeds the clone's counter from the number of copied (re-densified) items, so the first append on a fork is collision-free. The MAX aggregate is an index lookup on the SQL backends (unique index on (conversation_id, position)); the counter still removes the per-append round-trip and scales to backends where the same position allocation is a full scan. Tests (tests/stores/test_conversation_store.py): counter allocation/advance across batch shapes; counter-not-scan (advance past max, next item lands at the counter); NULL-counter scan fallback for 0/1/3 pre-existing items; full and truncated fork seeding; and a long-session contiguity check. Full tests/stores/ suite passes (395). Co-authored-by: Isaac |
||
|
|
276c725616 | chore(desktop): update icon and release v0.1.1 (#77) | ||
|
|
17feeedcf5 |
Move Cursor above Pi in session composer (#702)
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com> |
||
|
|
1b2ff5328a |
fix(policies): block ASK gates until a human answers, not a short client timeout (#626)
* fix(policies): default ASK approval timeout to 1 day, not 30s
An ASK policy is a human-in-the-loop gate, but DEFAULT_ASK_TIMEOUT was
30s. When a user didn't answer within 30s the server failed closed
(DENY) with no input and the web card flipped to the neutral "Resolved
elsewhere" pill -- looking like a silent auto-resolve. This bit the
session_cost_budget warning-threshold ASK in particular: it re-fires on
every request/tool_call until approved (the approved-checkpoint
state_update lands only on accept), so each one timed out in turn.
Every other wait-for-a-human budget in the native path is already
86400 (1 day): the PermissionRequest / evaluate-policy hook long-polls
and their server-side mirrors. The design intent (see sessions.py and
polly's config) is that everything waits a day and the policy
ask_timeout is the real cap -- so a 30s default was the lone outlier
that capped first. Align the default with the rest of the system.
Headless/unattended agents that want a fast fail-closed still override
per-policy via PolicySpec.ask_timeout or spec-wide via
GuardrailsSpec.ask_timeout (polly already does).
* fix(policies): block ASK gates until a human answers, not a short client timeout
An ASK approval is a human-in-the-loop checkpoint, but several client-side
timeouts on the delivery paths capped the wait far below the deciding
policy's ask_timeout. So the approval card auto-resolved (DENY) — or, on
the sub-agent wake path, retried into duplicate cards — before any human
could answer. The deciding policy's ask_timeout must be the single real
cap; every layer that merely waits for the human is pinned above it.
Source:
- spec: DEFAULT_ASK_TIMEOUT -> INT_MAX (effectively infinite, ~68y).
- native plumbing (claude/codex hooks + server-side mirrors): every
wait-for-a-human budget -> INT_MAX so no layer caps the wait first.
- runner deliverers that PARK behind the gate now wait for the verdict
instead of severing it, extracted to a named _ASK_GATE_DELIVERY_TIMEOUT
(INT_MAX read, fast 30s connect): the policy-eval + sub-agent
wake-notice POSTs (runner/app.py) and the message-send POSTs
(runner/tool_dispatch.py); plus pending_approvals._DEFAULT_WAIT_SECONDS
(was 120s -> auto-refuse) -> INT_MAX.
- SDK round-trip gate (_scaffold): -> INT_MAX and fail CLOSED (DENY) on the
now-unreachable expiry instead of fail-open (ALLOW).
Tests:
- tests/test_ask_timeout_infinite.py: drift-guard pinning every ASK timeout
(policy default, native plumbing + lockstep ordering, SDK, runner
delivery constants) to INT_MAX.
- tests/runner/test_pending_approvals.py: behavioral test that the gate
keeps blocking on the default budget and only a real verdict releases it.
- updated scaffold fail-closed + claude-bridge hook-timeout assertions.
* fix(policies): scope ASK-gate fix to 1 day, not infinite
Per review: 1 day (DEFAULT_ASK_TIMEOUT) is enough; no need for an effectively
infinite budget. The native plumbing was ALREADY 1 day before this work — the
bug was only that several runner→server delivery clients sat BELOW it. So:
- Revert the "infinite" (INT_MAX) churn on the native plumbing, DEFAULT_ASK_TIMEOUT,
and the server-side park mirrors back to main's existing 1-day values (those
files now have no net change).
- Keep only the real fix: bump the sub-1-day delivery budgets up to the 1-day
ASK budget so they wait for the verdict instead of severing the parked gate:
* pending_approvals._DEFAULT_WAIT_SECONDS 120s -> 86400
* runner.app _ASK_GATE_DELIVERY_TIMEOUT (policy-eval + wake POST) 30s -> 86400 read
* runner.tool_dispatch _ASK_GATE_DELIVERY_TIMEOUT (message sends) 30s -> 86400 read
* _scaffold._POLICY_EVAL_TIMEOUT_S 35s -> 86400 (main's phase-aware fail
open/closed fallback kept)
connect stays fast (30s).
Tests: rename drift-guard to tests/test_ask_timeout.py, assert the delivery
budgets == 1 day and never undercut DEFAULT_ASK_TIMEOUT; behavioral test in
test_pending_approvals.py unchanged in intent (gate blocks until verdict).
|
||
|
|
16a742e614 |
fix(cost): attribute claude-native cost into the per-model TOKEN USAGE view (#625)
The session "Token usage" panel (sourced from `usage_by_model`) and the "Session cost" badge (sourced from the flat `total_cost_usd`) are both summed over the conversation subtree, and the schema promises the per-model costs sum to the session total. They diverged badly for any session containing a claude-native (sub-)agent. Root cause: the relay and codex-native paths carry token counts, so `_persist_native_cumulative_usage` resolves a model and attributes the cost to `by_model`. claude-native instead forwards Claude Code's statusLine total (S) as a *cost-only* broadcast with no token counts, so `has_tokens` was false, the model was never resolved, and the per-model attribution block was skipped. The cost landed in the flat `total_cost_usd` (and the Session-cost badge) but never in `by_model`, so the per-model panel undercounted the session total by every native agent's spend. Fix (source-level, preserving model identity): - forwarder: tag the cost payload with the active model captured by the statusLine wrapper (already written to context.json), sent only when the display cost (S) advances. - server: resolve the model on a cost-bearing broadcast too, not just a token-bearing one, with priority `data["model"]` -> `conv.model_override` (the forwarder mirrors /model switches there) -> agent spec, mirroring the relay path. The existing attribution block then records the cost under the model (token buckets stay absent, as claude-native reports none). This restores the documented invariant (sum of per-model costs == session total) for native sessions. Widening `_post_external_session_usage`'s `usage` param to a covariant `Mapping` also resolves a pre-existing type error. Tests: cost-only attributes to the event's model; cost-only falls back to model_override; policy-only posts skip attribution; the forwarder tags a display-cost advance with the model and omits it on policy-only re-posts. |
||
|
|
032c8d015c |
feat(cursor): enforce PHASE_TOOL_CALL via preToolUse hook for all native tools (#667)
* feat(cursor): enforce PHASE_TOOL_CALL via preToolUse hook for all native tools Write .cursor/hooks.json at session startup with a preToolUse hook that calls the Omnigent server's policy evaluation endpoint before any Cursor native tool executes. This catches tools that execute silently (results embedded in assistant text without tool_call events) which the stream-based policy gate cannot see. Co-authored-by: Isaac * fix(cursor): use conversation_id from CLI args for preToolUse hook The hooks.json was baked with the executor's internal session_key (a bare UUID) instead of the server's conversation_id (conv_ prefix), causing the hook script's policy evaluation call to 404 and silently fail open. Now reads --conversation-id from sys.argv, matching the canonical ID the process_manager passes to the harness subprocess. Co-authored-by: Isaac * fix(cursor): use wrapper shell script for preToolUse hook command The Cursor SDK hook executor runs commands directly (not via a shell), so inline `env VAR=val cmd` silently fails. Write a wrapper shell script (.cursor/omnigent-hook.sh) that exports the env vars and execs the Python hook, and point hooks.json at the wrapper. Also resolve cwd to absolute path so hooks.json lands in the correct workspace directory. Co-authored-by: Isaac * fix(cursor): register Cursor native tool name `Shell` in ask_on_os_tools policy Cursor's native terminal tool is called `Shell` (not `Bash` like Claude/Codex), so the ask_on_os_tools policy didn't match it and silently allowed all cursor native shell commands. Co-authored-by: Isaac * fix: lint formatting Co-authored-by: Isaac |
||
|
|
5cc9125179 |
test(cursor): add cursor-native e2e + e2e_ui render-parity tests (#691)
Adds end-to-end coverage for the cursor-native (terminal-first) harness introduced in #551, mirroring the existing claude/codex native suites. CLI e2e (tests/e2e/test_cursor_native_cli_e2e.py): - smoke: drive `omnigent cursor` as a subprocess, inject a turn through the server (web-UI path), assert the marker comes back as an assistant item. - launch-cwd: cursor-agent reads a file that exists only in the launch cwd (proves cwd resolution + built-in Read tool), sibling of the codex test. UI render-parity e2e (tests/e2e_ui/messages/test_native_cursor_render_parity.py + native_cursor_session fixture in tests/e2e_ui/conftest.py): - composer parity (IN), a TUI-typed turn surfacing in the web UI (OUT), and no-duplicate-render — the three properties the codex/claude suites pin. Both are gated to skip unless `cursor-agent` + `tmux` are on PATH and a Cursor login is present (CURSOR_API_KEY or `cursor-agent login`), so CI stays green: unlike claude/codex, cursor-agent has no Databricks-gateway path (it speaks Cursor's proprietary aiserver.v1 protocol with a Cursor account credential), so it can't reuse the AI Gateway token CI already has. The fixture launches the TUI with `-f` so the unattended tmux pane never blocks on trust/approval prompts. Two cursor-only TUI-driving fixes vs codex: a settle-pause before Enter (the composer debounces input) and staying on the Terminal view until the forwarder mirrors the turn (switching tears down the xterm WS before the Enter commits). Verified locally (cursor-agent logged in): CLI tests pass; render-parity passes stably (~44s). Co-authored-by: Isaac |
||
|
|
6da4d7512f |
feat(cli): add --command flag to omni claude for custom wrappers (#484)
Expose the existing `command` parameter of `run_claude_native` on the CLI so that users whose environment provides a drop-in wrapper around the Claude Code CLI (one that injects auth or environment variables before delegating to `claude`) can use it without patching the tool. omni claude --command my-claude-wrapper --server https://... When --command is omitted the behaviour is unchanged: the executable defaults to `claude`. Co-authored-by: Noritaka Sekiyama Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
bcc5b4bd3e |
test: un-quarantine 5 stale-green 'empty-output' tests; re-triage 4 as sub-agent result-delivery (#669)
* test: un-quarantine 5 stale-green 'empty-output' tests; re-triage 4 as sub-agent result-delivery The openai-agents-empty-output cluster cited issue #2707, which does not exist in the repo — a stale bulk-quarantine. Flake-stress on main (run 27761358025, 20x, --no-skip-known) re-triaged all 9: Un-quarantined (0/20 failures): - test_steering.py::test_steering_acknowledged - test_steering.py::test_steering_during_multi_tool_iterations (both mock-LLM — they never touch the gateway, so the "empty-output on the gateway" reason was never valid; also verified 2/2 locally) - test_coder_subagent.py::test_coder_spawns_reviewer_and_collects - test_openai_coder_client_tools.py::test_openai_coder_lists_files_with_client_tools - test_agent_update.py::test_update_agent_zero_downtime Kept quarantined, re-characterized (the failure is NOT empty-output): - the 3 test_sub_agent_phase3_e2e tests fail ~consistently on a sub-agent result-delivery race — the parent turn replies before the spawned sub-agent's result is drained back ("still waiting for the researcher sub-agent to complete"). - test_subagent_completion_auto_wakes_idle_parent: same autowake/drain family, low-rate flake (2/20). Moved these 4 to a new `subagent-result-delivery` cluster and repointed the dead #2707 issue ref to the #532 umbrella. The empty-output cluster is now empty. * test: point the 4 subagent-result-delivery quarantines at the new tracking issue #682 Files the focused issue for the sub-agent result-delivery race (parent turn finalizes before the child result is drained; the async_work_complete end-of-turn await is specced but unimplemented — shared surface with #663). Repoints the 4 entries from the #532 umbrella to #682. |
||
|
|
526703bc53 |
feat(cursor): add cursor-native harness (cursor-agent acp over stdio) (#551)
* feat(cursor): add cursor-native harness (cursor-agent acp over stdio)
Adds a `cursor-native` harness that drives the official Cursor CLI's Agent
Client Protocol server (`cursor-agent acp`) over stdio JSON-RPC — the
codex-native model, but stdio instead of a WebSocket. This is the core slice:
session create + prompt + streamed `session/update` mapped to ExecutorEvents.
Unlike the SDK `cursor` harness, auth is the ambient `cursor-agent login`
($HOME/.cursor) — no CURSOR_API_KEY. Despite the "native" name it behaves like
the SDK harness (streaming, runner replays history), so it is intentionally NOT
in NATIVE_HARNESSES.
- omnigent/inner/cursor_acp_client.py: async stdio JSON-RPC client for
`cursor-agent acp` (initialize / session.new / session.load / session.prompt /
session.cancel; handles agent->client request_permission + fs/* requests).
- omnigent/inner/cursor_native_executor.py: CursorNativeExecutor — streaming
executor; maps agent_message_chunk/agent_thought_chunk/tool_call(_update) to
Text/Reasoning/ToolCall events.
- omnigent/inner/cursor_native_harness.py: create_app() wrap.
- Registration: _HARNESS_MODULES, OMNIGENT_HARNESSES, runner spawn-env dispatch
+ _build_cursor_native_spawn_env.
- tests/inner/test_cursor_native_executor.py: unit tests for update mapping,
prompt building, capability flags, ACP request handlers, registration.
Deferred to follow-ups: MCP host-tool relay, session/request_permission ->
policy bridge, resume via session/load, per-session $HOME isolation, model pin.
Verified end-to-end locally:
omnigent run hello_world.yaml --harness cursor-native -p "..." -> streamed reply, exit 0.
Co-authored-by: Isaac
* fix(cursor): harden cursor-native ACP client + add deterministic client tests
Bug-bash follow-ups on the cursor-native (ACP) harness (8/8 live e2e scenarios
pass; an adversarial review surfaced the P0/P1s below).
cursor_acp_client.py:
- P0: answer agent->client requests (session/request_permission, fs/*) on a
separate task instead of awaiting the reply inline in the read loop. Replying
inline parks the reader in stdin.drain() while not draining stdout — if the
agent's stdout pipe is full it can't read our reply, a deadlock. Now the reader
keeps draining; close() cancels+awaits the request tasks.
- A failed reply-send (broken pipe / dead proc) is suppressed so it can't kill
the reader task as an unretrieved exception.
- close() now awaits the cancelled reader/stderr tasks (deterministic cleanup,
no "Task was destroyed but pending" warnings).
- prompt() pops its _prompt_session entry in a finally (no leak on early close).
- _dispatch guards a None message id.
cursor_native_executor.py:
- P0: on first-turn start failure, close the local client directly. It was not
yet stored in self._sessions, so close_session() popped nothing and the
cursor-agent acp subprocess + reader tasks orphaned.
- P1: derive is_first_turn from has_sent_prompt (not just session existence), and
build the prompt before spawning so an empty turn is a cheap no-op and never
drops first-turn system-prompt semantics.
P1 (model-override table sync): remove cursor-native from _HARNESS_MODEL_ENV_KEY
and stop threading HARNESS_CURSOR_NATIVE_MODEL. cursor-agent acp uses its
configured default and the executor ignores a model pin, so cursor-native is now
consistently absent from all three tables (incl. _SDK_MODEL_OVERRIDE_HARNESSES).
tests/inner/test_cursor_acp_client.py: deterministic tests driving the real
client against a stdlib-only fake ACP server — streaming, multi-turn isolation,
JSON-RPC error -> CursorAcpError, the agent permission round-trip (no deadlock),
EOF mid-turn, and subprocess cleanup. No cursor-agent/network needed.
Verified: 27 cursor-native unit tests pass; 299 existing tests across the edited
modules (spawn-env, model-override, aliases, cursor executor/harness, runner
dispatch) pass; ruff clean.
Co-authored-by: Isaac
* feat(cursor): omnigent cursor launches the Cursor TUI in an omnigent terminal
Branch B, Stage 1: adds the `omnigent cursor` verb that launches cursor-agent's
interactive TUI inside an omnigent-runner-owned tmux terminal and attaches the
local TTY — the cursor analog of `omnigent codex` / `omnigent pi`.
Mirrors the pi-native template (simplest TUI launcher; no app-server, no
forwarder): create/resume session -> daemon runner bind -> POST ensure terminal
{terminal: "cursor"} -> runner spawns `cursor-agent` in tmux -> direct tmux
attach. Auth is the ambient `cursor-agent login` ($HOME inherited), so no API
key and no extension bridge.
- omnigent/cursor_native.py: run_cursor_native + the daemon/terminal/attach flow.
- omnigent/cli.py: `omnigent cursor` verb (+ _CLICK_SUBCOMMANDS).
- omnigent/runner/app.py: _auto_create_cursor_terminal (launch cursor-agent TUI),
create_session dispatch, ensure-native-terminal route, ensure-lock, cleanup.
- registration: _wrapper_labels (CURSOR_NATIVE_WRAPPER_VALUE), native_coding_agents
(CURSOR_NATIVE_CODING_AGENT — UI-visible), harness_aliases (NATIVE_HARNESSES),
resource_registry (CURSOR_NATIVE_TERMINAL_ROLE), resume_dispatch.
cursor-native is now a terminal-native harness (in NATIVE_HARNESSES), so the
runner treats it like the other native TUIs. Flipped the Branch-A test that
asserted otherwise.
Verified live: `omnigent cursor --server <local>` creates the session, the runner
launches `cursor-agent` in tmux (`terminal_cursor_main` running, status bar wired
to the conversation link), and the CLI attaches (only fails to attach in a
non-TTY shell). 77 unit/registry tests pass; ruff clean.
Stage 2 (follow-up): mirror the TUI conversation to the web UI (read cursor's
store/hooks) + inject web-UI messages into the running TUI.
Co-authored-by: Isaac
* feat(cursor): bridge web-UI chat to the running Cursor TUI via tmux injection
Branch B, Stage 2 (the bidirectional bridge): web-UI messages now inject into the
running cursor-agent TUI instead of a separate side-session, so the web chat box
and the TUI are connected. Since the web UI embeds the same tmux pane, a message
sent from the web appears in the TUI (local terminal + embedded web terminal),
and TUI activity shows in the web embedded terminal.
This replaces the Branch-A ACP executor (which spun up a separate `cursor-agent
acp` session the user never saw) with the claude/pi-native tmux-injection model:
- omnigent/cursor_native_bridge.py (new): per-session bridge dir + tmux.json;
inject_user_message (clear draft -> bracketed paste via load-buffer/paste-buffer
-> Enter, multi-line safe; accepts the first-run "Trust this workspace" modal);
build_cursor_native_spawn_env.
- omnigent/inner/cursor_native_executor.py: rewritten to inject the latest web-UI
message into the TUI pane (supports_streaming=False; live steering).
- omnigent/runner/app.py: _auto_create_cursor_terminal writes tmux.json after
launch; cursor-native spawn-env now carries the bridge dir (mirrors pi-native);
dropped the stale Branch-A spawn-env dispatch.
- Removed the now-superseded ACP client + its test; rewrote the executor test for
the injection model (content extraction, paste-payload encoding, bridge
round-trip, registration).
Verified live: `omnigent cursor --server <local>` launches the TUI; POSTing a
web-UI message to the session injects it into the pane ("→ WEBUI_INJECT_BANANA"
appears in the live Cursor TUI). 16 unit tests pass; ruff clean.
Follow-up: structured chat-bubble mirror (cursor's chat store is content-addressed
SQLite, not a tailable transcript) — the embedded terminal already shows output.
Co-authored-by: Isaac
* fix(cursor): wire Stop/interrupt, status badge, robust injection + attachments
Addresses the audited P1 control-plane no-ops + injection robustness (all verified
live against a real cursor-agent on a test server):
- Stop session no-op (audit P1): cursor-native had no branch in the runner's
stop_session dispatch, so the Stop button never killed the pane (terminal +
cursor-agent leaked). Added cursor_native_bridge.kill_session + a
_handle_cursor_native_stop handler (kill tmux session, tear down terminal
resource, publish idle, reclaim sub-agent entry) — mirrors claude-native.
- Interrupt no-op (audit P1): added cursor_native_bridge.inject_interrupt
(sends Escape — verified to stop a cursor turn) + _handle_cursor_native_interrupt,
wired into the interrupt dispatch. Stop button now cancels the in-flight turn.
- Working-status badge stuck (audit P1): added CURSOR_NATIVE_TERMINAL_ROLE to the
PTY watcher's emit_status set (cursor has no forwarder, so the watcher is its
only status source — like pi/claude).
- Dead-terminal silent message loss (my live finding): inject_user_message now
fast-fails with a clear error if the tmux session is gone, instead of polling a
dead pane for the full 30s and dropping the message silently.
- Probabilistic dropped message (audit P1): wait for the pasted text to render in
the pane before sending Enter (avoids the Enter being folded into the paste as a
newline), instead of a fixed sleep + blind Enter.
- Trust-modal keystroke spam (audit P2): the 'a' accept is now one-shot.
- Dropped attachments (my live finding): the executor's _content_to_text now
materializes input_image/input_file to disk and references them by path so
cursor-agent can read them, instead of silently discarding non-text content.
Verified live: normal/leading-slash/multiline injection land; Escape interrupts a
running turn; kill_session kills the pane; dead-pane injection raises in ~0s (was
30s + silent loss). 17 unit tests pass; ruff clean.
Co-authored-by: Isaac
* feat(cursor): register cursor-native in the ap-web frontend (icon, picker, branding)
Fixes the audited frontend-registry cluster (the root cause of cursor-native
sessions rendering wrong / not appearing as a first-class agent):
- ap-web/src/lib/nativeCodingAgents.ts: add the cursor entry (key/agentName/
harness/wrapperLabel/displayName Cursor/iconKind cursor/sortRank 40), widen
NativeCodingAgentIconKind to include 'cursor', and add the native-cursor alias.
This is the single root fix — isNativeWrapper, nativeDisplayNameForAgent, sort
rank, slash/model gating, and branding all key off this registry.
- CursorIcon.tsx (lobehub Cursor glyph) + cursor branches in AgentCard.tsx and
SubagentsPanel.tsx (both icon sites) + the SDK 'cursor' harness fallback.
- sidebarNav.ts: add 'cursor' to ConversationIconKind so getConversationIconKind
stays type-sound now that the registry emits iconKind 'cursor'.
- NewChatDialog.tsx: add cursor-native-ui to BUILTIN_AGENTS and 'Cursor' to
AGENT_DISPLAY_ORDER so a cursor agent groups with the built-ins (not last,
fallback-iconed, in the custom group).
- test mocks (test-setup.ts global + AgentCard.test.tsx) + new cursor icon-
selection cases.
forkHarness.ts intentionally left unchanged: cursor cannot carry fork history
(no resume-by-id), so it stays out of the history-carrying fork path — the
matching backend honesty fix follows. Type-check clean; 138 frontend tests pass.
Co-authored-by: Isaac
* feat(cursor): seed cursor-native as a default agent + document tool-policy non-coverage
- Seed cursor-native-ui as a built-in agent on server startup (_ensure_default_
cursor_agent + _build_cursor_native_bundle, mirroring claude/codex/pi). Without
this, cursor only appeared in GET /v1/agents after the `omnigent cursor` CLI
first registered it, so a stock deployment's picker never showed it. Verified:
a fresh server now lists cursor-native-ui.
- Document in the harness that Omnigent's PreToolUse/PostToolUse tool policies do
NOT apply to cursor-native (cursor-agent gates tools with its own in-TUI
approval), so operators don't assume deny-policies constrain a cursor session.
Co-authored-by: Isaac
* fix(cursor-native): mirror TUI conversation back to the web UI
The cursor-native harness only injected web→TUI; nothing mirrored the
running cursor-agent TUI's conversation back into the Omnigent session,
so the chat view stayed empty and the spinner dropped the instant a
message was sent. Four reported symptoms, one root cause (no forwarder)
plus a status-edge bug:
1. Working spinner vanished — run_turn returns TurnComplete immediately
after the tmux paste, and cursor-native was absent from the
_publish_turn_status suppression set, so the turn-lifecycle idle raced
ahead of and clobbered the PTY watcher's running. Add cursor-native to
the suppression set (parity with claude/pi); the PTY watcher is now the
sole status source.
2. Session title stuck at "Cursor" — title seeds only when an
external_conversation_item is persisted; the forwarder now posts the
first user message, seeding it.
3. No assistant output in the web conversation — fixed by the forwarder.
4. TUI-typed follow-ups never appeared in the web UI — fixed by the
forwarder.
New omnigent/cursor_native_forwarder.py polls cursor's content-addressed
SQLite chat store (~/.cursor/chats/<md5(cwd)>/<chat-id>/store.db),
reading role-bearing JSON blobs in rowid order (= conversation order) and
posting user (unwrapped <user_query>) and assistant text as
external_conversation_item events. Store discovery is by md5(cwd) + newest
chat created since launch, with a cross-workspace fallback; dedup is an
O(1) high-water rowid persisted to the bridge dir; a supervisor restarts
on crash with bounded backoff. The store MUST be opened mode=ro (not
immutable=1) — a live chat keeps its data in the -wal sidecar, which
immutable=1 ignores. Wired into _auto_create_cursor_terminal (host-spawned
sessions have no CLI to start it) and cancelled on session stop.
Verified end-to-end against a real cursor-agent: spinner tracks the TUI,
title populates, assistant replies and TUI-typed follow-ups both mirror to
the web conversation.
Co-authored-by: Isaac
* fix(cursor-native): harden forwarder discovery, state, and remote-deploy URL
Follow-up to the TUI→web forwarder, addressing issues found by an adversarial
multi-agent audit of the cursor-native flow (verified against the live server +
a headless-browser bug-bash). The headline TUI→web mirroring already works
end-to-end (user + assistant render live, spinner tracks the TUI, title seeds);
these are correctness/robustness fixes around it:
- Require RUNNER_SERVER_URL instead of silently defaulting to localhost:6767
(matches codex's _required_runner_env). The default made every mirror POST
miss on a remote deploy, leaving the web conversation empty.
- Canonicalize the workspace with os.path.realpath before launch + discovery so
the cursor TUI's cwd and the forwarder hash the SAME md5(cwd) — a symlink /
trailing-slash mismatch would hide the chat store.
- Make store discovery cross-talk-safe: bind the exact md5(cwd) dir, and fall
back to other workspace dirs ONLY when exactly one chat qualifies. Two
candidates (concurrent same-cwd sessions, or an unrelated workspace) now
return None and retry rather than risk mirroring the wrong conversation.
- Clear the persisted forward cursor when the terminal is re-created
(clear_cursor_bridge_state, mirrors codex's clear_bridge_state) so a stale
store_path/last_rowid can't make the new forwarder resume the wrong chat.
- Surface (log) state-write failures instead of silently swallowing them; the
in-memory cursor still prevents within-process re-posting.
- Strip the executor's injected "[Attached: <path>]" markers from mirrored user
text so bridge paths don't leak into web-UI bubbles.
- Forwarder Authorization now rides solely on the refresh-capable auth (no
static header snapshot that would expire mid-session).
Audit findings deliberately NOT changed, with rationale: per-blob response_id is
fine (itemsToBlocks renders per-item in arrival order, not grouped by
response_id — confirmed live); cursor tool-call mirroring is a separate feature
(tool calls live in binary protobuf blobs, not the JSON message blobs); the
shared native sub-agent-completion path and shared terminal idle markers were
left untouched to avoid regressing claude/codex/pi.
Tests: 3 new unit tests (ambiguous-discovery → None, attachment-marker strip,
state clear); all 22 cursor-forwarder tests pass.
Co-authored-by: Isaac
* fix(cursor): register cursor pane in AGENT_TERMINAL_IDS
The cursor-native agent's terminal pane has id ``terminal_cursor_main``
(``terminal_{terminal_name}_{session_key}`` with ``terminal_name="cursor"``),
but it was missing from the frontend ``AGENT_TERMINAL_IDS`` allowlist. That
made ``isShellView`` treat the agent's own terminal as a user shell, hiding
the Chat/Terminal toggle pill in Terminal view and stranding the user with
only the close affordance. The pane also leaked into the Shells inventory.
Add ``terminal_cursor_main`` to the set (mirroring the existing tui/claude/
codex/pi entries) and add regression tests in ``isAgentTerminalKey`` and
``inventoryTerminals`` matching the pi cases.
Co-authored-by: Isaac
* test(cursor): exclude cursor-native from gateway e2e harness matrix
cursor-native now lands in OMNIGENT_HARNESSES ∩ _HARNESS_MODULES, so
test_run_harness_live_matrix_covers_registered_coding_harnesses expected a
live HARNESS_PROBES row for it and failed. cursor-native can't round-trip
this gateway-backed matrix for the union of the existing exclusions: like
the *-native harnesses it needs a bridge dir + runner-managed tmux pane (set
up by ``omnigent cursor``, not ``omnigent run --harness cursor-native``), and
like ``cursor`` it drives cursor-agent against Cursor's own backend. Its live
coverage is the gated row in test_per_harness_cursor.py.
Co-authored-by: Isaac
* docs(cursor): correct stale cursor-native harness-registry comment
The registry comment still described the pre-pivot design (Cursor ACP server
over stdio, streaming executor, "intentionally absent from NATIVE_HARNESSES").
The shipped harness drives the resident cursor-agent TUI via tmux injection
and IS in NATIVE_HARNESSES. Align the comment with the implementation.
Co-authored-by: Isaac
---------
Co-authored-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: Serena Ruan <82044803+serena-ruan@users.noreply.github.com>
|
||
|
|
3df92a6adc |
revert: remove FORK_NEVER_SKIP from required.sh (#681)
Reverts the IS_FORK / FORK_NEVER_SKIP changes that made e2e checks non-skippable for fork PRs in evaluate-checks.sh. The merge gate (compute-gate.sh) already blocks fork PRs without approval, making the ALLOW_SKIP override redundant. Co-authored-by: Isaac |
||
|
|
969a9368b2 |
fix(e2e): tolerate slow REPL teardown in clean_exit (#680)
The pexpect clean_exit helper raised pexpect.TIMEOUT when neither Ctrl+D nor the /quit fallback produced EOF within the exit timeout, failing tests whose functional assertions had already passed. On a loaded xdist worker the REPL shutdown (session-log write, task cancellation, app.exit()) occasionally exceeds the timeout — especially for workflows that leave parked tasks behind, e.g. test_run_omnigent_rate_limit_approval_round_trip. clean_exit is a teardown helper run as the last step of ~25 e2e tests, so a slow shutdown handshake should not fail an otherwise green run. Force-kill the child on the final fallback timeout instead of raising. Verified with 5x pytest-repeat runs of the rate-limit-approval test: 5 passed, 0 flakes. Co-authored-by: Isaac |
||
|
|
ac13810669 |
feat: wire MLflow tracing end-to-end through omnigent run (#638)
* feat: wire MLflow tracing end-to-end through omnigent run
Enable MLflow tracing from `omnigent run` by propagating OTEL/MLflow
env vars through the daemon→server→runner→harness process chain and
wiring TracingContext into ExecutorAdapter.run_turn().
Changes:
- cli.py: add MLFLOW_/OTEL_ to _LOCAL_DAEMON_ENV_PREFIXES
- host/connect.py: add MLFLOW_/OTEL_ to _RUNNER_ENV_ALLOWLIST_PREFIXES
- runner/_entry.py: call telemetry.init() in the runner process
- harnesses/_runner.py: call telemetry.init() in the harness subprocess
- harnesses/_executor_adapter.py: create TracingContext per session,
emit agent/tool spans per turn, flush OTel provider and finalize
trace status via MLflow PATCH API on turn completion
- runtime/telemetry.py: call enable_tracing() in init(), support
short hex response IDs (24-char → zero-padded to 32-char)
Co-authored-by: Isaac
* fix: update telemetry test for zero-padded short hex IDs
trace_id_from_response_id now zero-pads short hex suffixes (e.g.
24-char harness-allocated IDs) instead of raising ValueError.
Update the test to match and add a test for the too-long case.
Co-authored-by: Isaac
* fix(ci): use sentinel + robust fallback for preamble stripping
Address Polly review feedback:
- Prompt now asks the model to emit <!-- POLLY_REVIEW_START -->
sentinel; stripping anchors on it deterministically
- Fallback heuristic covers #{1,6} headings (not just #{1,3})
- Anchors `---` to standalone lines to avoid matching table separators
Co-authored-by: Isaac
* Revert "fix(ci): use sentinel + robust fallback for preamble stripping"
This reverts commit
|
||
|
|
408a18bee6 |
test: re-home 2 client-side-tool /v1/responses e2e tests to mock-LLM sessions layer (#532) (#664)
The POST /v1/responses route was removed; two quarantined e2e tests
in the async-dispatch-inbox-sse cluster were client-side tool
round-trips that 405 as written. Re-home their invariants at the
mock-LLM sessions-API integration layer (the test_d6_* /
test_client_tools.py idiom):
- test_client_side_tool_inline_sse_carries_action_required:
the inline function_call SSE output_item.done parks as
status="action_required" and the posted function_call_output
round-trips into the reply.
- test_request_supplied_client_tool_result_reaches_model:
a request-supplied client tool routes through the client-side
dispatch branch (not the unknown-server-side-tool envelope) and
the posted result reaches the model verbatim.
Removes the two obsolete e2e files and their known_failures.yaml
entries. The remaining 11 async-dispatch-inbox-sse entries depend on
the sessions-native sys_call_async / sys_read_inbox dispatch surface
(dispatch_async raises NotImplementedError; no async_tool_results on
/v1/sessions/{id}/events) and stay quarantined pending product work.
Co-authored-by: Isaac
|
||
|
|
e3c80c02b5 |
fix(cursor): enable delta stream so TurnEndedUpdate usage arrives (#653)
* fix(cursor): enable delta stream so TurnEndedUpdate usage arrives The Cursor backend only sends interaction updates (including TurnEndedUpdate with token usage) when the request includes enableDeltas: true — set by passing SendOptions(on_delta=...) to agent.send(). Without it, no interaction_update events arrive in the stream and cost tracking silently produces nothing. Also adds cacheReadTokens / cacheWriteTokens (the actual field names the Cursor backend sends) to the normalization lookup. Co-authored-by: Isaac * refactor(cursor_executor): streamline agent.send call for improved readability Consolidated the parameters of the agent.send method into a single line for better clarity and maintainability. This change enhances the readability of the code without altering its functionality. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> --------- Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
aacc6bc374 |
test: remove dead web_search async-dispatch e2e (feature removed with DBOS layer) (#661)
test_web_search_async_dispatch_e2e.py asserts that web_search dispatches asynchronously for non-OpenAI models (a function_call + async_work_complete drain). That path was deleted with the durability (DBOS) layer: WebSearchTool.is_async() now returns False for every backend, so the test exercises a code path that no longer exists and can never pass. The surviving sync behavior is covered by unit tests in tests/tools/builtins/test_web_search.py — notably test_non_openai_mode_is_sync_in_sessions_native_mode (pins is_async()==False) plus the per-backend invoke tests (perplexity/google/nimble). Removes: - the e2e test file, - its sole fixture agent tests/resources/agents/web-search-test/, - the now-stale "covered by name elsewhere" allowlist entry in test_examples_coverage_sync.py, - the quarantine entry in known_failures.yaml. The other 14 /v1/responses async-dispatch quarantines stay put: unlike this one they test invariants not yet re-homed to the sessions API, so deleting them would drop coverage — they need re-homing, not removal. |
||
|
|
769b6ceb5c |
fix(host): tolerate non-JSON daemon-status responses so the REPL never crashes on startup (#660)
The host + runner status polls (GET /v1/hosts/{id}, GET /v1/runners/{id}/status)
expect JSON, but a server reached over --server that does not mount the host
router (API-only deployment, or a misconfigured server) lets these paths fall
through to the SPA HTML5-history fallback, which answers 200 text/html with
index.html. Calling resp.json() on that raised an opaque json.JSONDecodeError
that crashed `omnigent run` before the REPL ever became ready.
Add a _json_body helper that decodes the status body and treats any non-JSON /
non-dict 200 as "no status yet", so the wait loops keep polling and ultimately
fail with the actionable timeout message instead of an opaque decode error.
Applied at all 5 status-decode call sites (host wait, runner online check,
runner wait, daemon reuse snapshot).
Adds deterministic unit coverage (200-text/html-then-online + always-html) for
both wait loops and the single-shot runner_is_online check.
|
||
|
|
c2201d4d03 |
test(repl): un-quarantine 4 stale-green REPL tests (#648)
Swept into the "Nightly bulk" / force-merge quarantines; pass now that the shared pexpect harness (tests/e2e/omnigent/_pexpect_harness.py) is matured and the openai-agents base_url routing bug is fixed (#629 + #645). Verified 30/30 in CI flake-stress: - test_repl_session_lifecycle.py::test_repl_full_session_lifecycle - test_repl_session_lifecycle.py::test_repl_reasoning_effort_threads_through - test_run_omnigent_coding_supervisor.py::test_run_omnigent_coding_supervisor_interactive_enters_repl - test_run_omnigent_rate_limit_approval.py::test_run_omnigent_rate_limit_approval_round_trip NOT un-quarantining test_repl_local_mode_launches_runner_subprocess: it passes locally (macOS) but fails 0/30 in CI with "No runner subprocess found under <pid>" — the test asserts the runner is a direct process-tree child, which doesn't hold in CI's container/daemon model. Its reason is updated to record that; it stays quarantined pending a CI-robust runner-detection fix (tied to the daemon-lifecycle work). Co-authored-by: Isaac |
||
|
|
d8bbb42eaf |
fix(claude-native): hold assistant commit until its streamed deltas forward (#493)
* fix(claude-native): hold assistant commit until its streamed deltas forward The transcript JSONL and message_deltas.jsonl have independent writers (Claude's session loop vs the per-chunk MessageDisplay hook), so a chunk can be forwarded AFTER the message's committed item — inverting the deltas-before-done order every downstream layer assumes and building a second live preview (the transient duplicate bubble). Fix at the forwarder, the one place that sees both files: hold the assistant message item until a complete (final-seen) forwarded delta stream byte-equals its text, or a ~2s timeout. This forces deltas-before-commit so no chunk lands after the commit. Matching on complete byte-equal text (not prefix) keeps identical-text messages interchangeable and avoids prefix mis-identification; the hold only delays the commit, never suppresses a preview, so the failure direction is safe. Tests cover: a non-final chunk arriving after the commit (held until the true final), final-seen-but-incomplete (byte-equal required), identical content consume-once, the timeout release, no-deltas-file (never held), and a break-the-feature guard (no hold -> commit before final delta). Co-authored-by: Isaac * docs(claude-native): tighten deltas-before-done hold comments Condense the verbose comments and docstrings added for the assistant-item delta-hold fix in the forwarder and its tests. Comment-only; no behavior change. The 7 hold tests still pass locally. Co-authored-by: Isaac |
||
|
|
aa6452afb9 |
feat(chat): reveal "Jump to top" pill on scroll-up (#658)
The pill previously surfaced only when hovering the top ~140px band of the conversation. Now an upward scroll also reveals it, then it fades back out ~2s after scrolling settles — making it reachable without hunting for the hover band. Adds unit coverage (reveal on scroll-up + auto-hide, no reveal on scroll-down) and an e2e_ui journey (scroll up surfaces the pill, then it auto-hides). Co-authored-by: Isaac |
||
|
|
95301c9352 |
docs: add omnigent bot identities & attribution runbook (#650)
Documents the two distinct attribution identities that shipped: - polly sub-agent commits co-sign as 'omnigent <noreply@omnigent.ai>' (local git commits, not Actions runs) - omnigent-ci[bot] GitHub App for CI-minted work: lockfile-regen commits/PRs and automated PR-review comments (polly-review.yml) Captures the one-time org-admin App setup (App ID 4082516, bot user id 294685417, OMNIGENT_BOT_APP_ID/_KEY config) that isn't otherwise recorded in the repo, and notes the old OSS_REGEN_APP_* App + config are retired. Co-authored-by: omnigent <noreply@omnigent.ai> |
||
|
|
bb6bcf590f |
fix(ci): pass --repo to gh run rerun in the security-gate relay (#655)
`gh run rerun` resolves its target repo from -R/--repo, the GH_REPO env
var, or the local git remote -- in that order. The relay job has no
`actions/checkout` and sets only REPO (not GH_REPO), so the call fell
through to the git-remote path and died CLIENT-SIDE before reaching
GitHub:
failed to determine base repo: failed to run git:
fatal: not a git repository (or any of the parent directories): .git
That error was swallowed by `|| echo "::warning::..."`, so the relay
looked like it ran but never actually re-ran anything -- silently
stranding the gate-bearing workflows that have no `labeled` trigger of
their own (Lint, Integration, E2E UI, ap-web Tests, Polly AI Review) on
both #556 and #644. The script's other `gh api "repos/$REPO/..."` calls
work because the repo is in the URL path, not resolved.
Pass `--repo "$REPO"` ($REPO = github.repository = the base repo, where
these run ids resolve -- fork-PR `pull_request` runs live base-side).
One line; the relay's design is otherwise correct.
Co-authored-by: Isaac
|
||
|
|
f45209e44a |
feat(cursor): evaluate PHASE_TOOL_CALL policy for native tools (#643)
* feat(cursor): evaluate PHASE_TOOL_CALL policy for native tools Cursor's native tools (bash, file editing, etc.) previously bypassed all tool-call policies. Now when a non-bridged tool call is observed in the stream, the executor evaluates PHASE_TOOL_CALL and cancels the run on DENY. Bridged (MCP-wrapped) tools are skipped since they're already gated server-side via the dispatch bridge. Co-authored-by: Isaac * fix(cursor): fix lint formatting and strengthen policy test assertions Address Polly review: fix any test fixture typos, assert ToolCallRequest is observed in the bridged-skip test, assert event ordering in the DENY test, and fix line-length formatting. Co-authored-by: Isaac |
||
|
|
42a6ce5815 |
fix(ci): strip sub-agent preamble from Polly review comments (#646)
* fix(ci): strip sub-agent preamble from Polly review comments
Sub-agents (e.g. Codex) sometimes leak coordination narration
("I've dispatched the codex reviewer…") before the structured
review output. Post-process the output to trim everything before
the first markdown heading or horizontal rule.
Co-authored-by: Isaac
* fix(ci): use sentinel + robust fallback for preamble stripping
Address Polly review feedback:
- Prompt now asks the model to emit <!-- POLLY_REVIEW_START -->
sentinel; stripping anchors on it deterministically
- Fallback heuristic covers #{1,6} headings (not just #{1,3})
- Anchors `---` to standalone lines to avoid matching table separators
Co-authored-by: Isaac
|
||
|
|
cd91a621a2 |
fix(antigravity): accept new 'AQ' Google API key prefix in setup (#640)
* fix(antigravity): accept new 'AQ' Google API key prefix in setup New Google API keys start with 'AQ' instead of the legacy 'AIza', which triggered a spurious "doesn't start with 'AIza'. Store it anyway?" prompt during `omni setup`. Broaden the soft prefix check to accept both prefixes. Co-authored-by: Isaac * style: ruff format antigravity key prefix hint Co-authored-by: Isaac * chore: revert accidental uv.lock / package-lock.json drift Co-authored-by: Isaac |
||
|
|
ad5e9cc534 |
test: migrate REPL approval e2e tests to mock LLM (#641)
* test: migrate 6 REPL approval tests to mock LLM, skip 8 complex ones 6 tests (single approval, refusal, two-turn, approve-always, label-driven approve/refuse) now run fully against the mock LLM server. 8 tests that require tool-call/subagent/output-phase mock support not yet available in REPL pexpect mode are guarded with `if using_mock_llm: pytest.skip(...)` so they only run with a real LLM key. Co-authored-by: Isaac * test: remove dead mock setup code from 8 skipped REPL approval tests These tests skip under mock LLM, so the _configure_mock_* calls after pytest.skip() were unreachable dead code. Remove those calls and the now-unused mock_llm_server_url parameter from each test signature. Co-authored-by: Isaac |
||
|
|
dcce5caa39 |
fix(openai-agents): honor ambient OPENAI_BASE_URL on spec api_key path (#645)
A baked executor.auth api_key is frequently a gateway PAT (detected from OPENAI_API_KEY). When its companion base_url is dropped on the daemon -> runner -> harness propagation chain (the spec-auth bake omits base_url when OPENAI_BASE_URL is absent at materialization time; a reused local daemon may predate the env var), the executor's api_key branch set base_url=None and routed the gateway token to api.openai.com -> 401. Fall back to the ambient OPENAI_BASE_URL (which the runner/harness inherit) when no base_url override reached us, so the gateway target is present on every turn. A genuine OpenAI key with no gateway anywhere still defaults to api.openai.com (base_url=None). Co-authored-by: Isaac |
||
|
|
a7ae6bb7f7 |
ci: gate fork e2e on maintainer approval, make blocking (#636)
* ci: gate fork e2e on maintainer approval instead of label, make blocking
Replace the `e2e-approved` label gate with maintainer PR approval for
triggering e2e on fork PRs. The merge gate now blocks until e2e passes
after approval, instead of allowing fork PRs to merge with skipped e2e.
Co-authored-by: Isaac
* ci: make e2e/integration checks non-skippable for fork PRs
Add FORK_NEVER_SKIP list to required.sh so that is_allow_skip returns
false for e2e/integration checks when IS_FORK=true. This closes the
edge case where a fork PR could merge with e2e never having run (e.g.
if the mirror failed after approval). Pytest shards remain skippable
for fork PRs since they don't require secrets.
Co-authored-by: Isaac
* ci: address Polly review — cleanup on revocation, fork guard, relay scope
B1: Delete the stale mirror branch when should-mirror returns false on
workflow_dispatch (approval revoked / changes requested). Extend the
review relay to fire on all non-COMMENTED review states so dismissals
and changes-requested also trigger re-evaluation.
B2: The relay now fires on all decisive review states (not just
approved). The mirror workflow re-evaluates via should-mirror.sh and
either mirrors (approved) or cleans up (revoked).
B3: Add fork guard for workflow_dispatch in the mirror job — resolve
the PR and skip early for same-repo PRs.
Co-authored-by: Isaac
* ci: keep e2e-approved label as alternative gate alongside approval
The fork e2e mirror gate now accepts either condition:
1. Maintainer PR approval (primary flow), OR
2. e2e-approved label applied by a maintainer (escape hatch for
running e2e without approving for merge)
Co-authored-by: Isaac
|
||
|
|
65058d3fba | feat(ci): post Polly AI review as omnigent-ci[bot] (#642) | ||
|
|
faf67f4e34 |
ci(merge-ready): pin gate scripts to main, never the PR head (#639)
* ci(merge-ready): pin gate scripts to main, never the PR head The "Check out scripts" step had no `ref:`, so on the `pull_request` (automerge) event it checked out `refs/pull/N/merge` and on `check_suite` the suite head SHA -- i.e. the PR's own copy of `.github/scripts/merge-ready/required.sh` and `evaluate-checks.sh`. `required.sh` is a generated file replaced wholesale on each sync, so a PR branched before E2E was added to REQUIRED carried a stale list: labeling it `automerge` evaluated the gate from the PR's old script and merged it without E2E required. It is also a privilege escalation -- a same-repo PR could edit its own gate scripts and self-merge under the job's contents:write + auto-merge permissions. Pin the checkout to `ref: main` so Merge Ready always evaluates with main's gate logic regardless of trigger, matching fork-e2e-mirror.yml's "trusted; never the PR head" pattern. Co-authored-by: Isaac * ci(merge-ready): trim comment to one line |
||
|
|
8f21bdd5fd |
fix(ci): make skip-security-scan waiver label-only and fix rerun race (#637)
* fix(ci): make skip-security-scan waiver label-only and fix rerun race The skip-security-scan waiver required BOTH the label AND a maintainer approval (should-scan.sh). When those two events arrived apart (as on #556, 8 min apart), the approval fired a premature relay while the scan still failed, leaving gate runs in-progress; the decisive label-triggered relay then hit `gh run rerun` on those in-flight runs, which GitHub rejects ("could not re-run"), stranding stale failing checks (Lint, Integration, E2E UI). The approval half added no real authority: applying the label already requires Triage permission, held only by write/admin collaborators, so a fork author can never self-waive. Make the waiver label-only. - should-scan.sh: replace skip_label_effective() (label + maintainer approval/author) with has_skip_label() (label presence only). Still fails closed on missing token/repo/PR. author_is_maintainer (private- membership author trust) is unchanged. - security-scan.yml: drop the pull_request_review trigger; re-run on labeled/unlabeled only. Update the on-failure waiver message. - rerun-security-gate.yml: drop the pull_request_review trigger; gate the record job on the skip label only. - rerun-security-gate-run.yml: add a race guard -- wait for the head SHA's Security Scan check to complete and only re-run gate workflows once it has passed, so the relay never churns in-progress runs. Co-authored-by: Isaac * fix(ci): raise rerun-gate job timeout above the race-guard wait budget The race guard can wait up to ~6 min for the Security Scan to settle, but the job timeout was 5 min, so a slow scan could cancel the job before it reached the rerun loop -- stranding the very gate re-runs the guard exists to issue. Bump timeout-minutes to 10 to cover the wait plus download/rerun. Co-authored-by: Isaac * fix(ci): address PR review — single-call race guard, accurate triage wording - rerun-security-gate-run.yml: fetch scan status+conclusion in ONE check-runs call (was two, a TOCTOU on which run is 'latest'); sort by monotonic id instead of started_at; document the >6-min scan timeout as a known gap. - should-scan.sh: reword 'write/admin' to 'Triage (or higher)' and frame the 'can already push' claim as an accepted repo-policy risk, not a GitHub guarantee; fix the waiver reason string accordingly. Co-authored-by: Isaac |
||
|
|
612e6db792 |
ci: use pull_request_target in merge-ready so it always runs from main
A PR cannot modify the gate logic by editing merge-ready.yml since pull_request_target always runs the workflow file from the base branch. Co-authored-by: Isaac |
||
|
|
f04df131bb |
feat(cursor): implement cost/usage tracking for cursor harness (#635)
* feat(cursor): implement cost/usage tracking for cursor harness The cursor SDK exposes token usage via TurnEndedUpdate interaction updates, but the executor was iterating run.messages() which only yields SDKMessage objects—skipping interaction updates entirely. Switch to run.events() to capture TurnEndedUpdate.usage, normalize it to the standard Omnigent usage dict, and pipe it through _notify_usage_from_dict and TurnComplete. Co-authored-by: Isaac * fix(cursor): use None-checks in usage normalization to handle zero-valued fields Addresses Polly review feedback: the `or`-chain conflated zero with missing, duplicate cache-key loop had last-writer-wins, and `if val:` dropped legitimate zero entries. Co-authored-by: Isaac |
||
|
|
7049fe5f60 |
fix(providers): ambient OPENAI_API_KEY detection honors OPENAI_BASE_URL (#629)
An ambient OPENAI_API_KEY detection was synthesized into an 'openai' provider hardcoded to https://api.openai.com/v1, ignoring a companion OPENAI_BASE_URL. For an openai-agents agent whose OPENAI_API_KEY is a Databricks gateway token (the daemon-spawned runner's ambient creds), every LLM call routed to api.openai.com and 401'd with invalid_api_key. Honor OPENAI_BASE_URL for the openai-family canonical vendor, matching the interactive wizard, non-interactive onboarding, and provider_selection._read_credentials_from_env. Scoped to the openai vendor (third-party OpenAI-compatible endpoints keep their own base_url). Co-authored-by: Isaac |
||
|
|
b0418c0723 |
fix(antigravity): stamp model on usage dict for cost pricing (#634)
The antigravity executor's _extract_usage() did not include the "model" key in the usage dict, so the scaffold created Usage(model=None) and the cost pricing pipeline could not look up Gemini pricing from the MLflow catalog — total_cost_usd stayed at 0 for all antigravity turns. Stamp usage["model"] = model after extraction, matching the pattern used by the claude-sdk and openai-agents-sdk executors. Co-authored-by: Isaac |
||
|
|
fa6191ce22 |
fix(tests): align debby cross-vendor test with codex harness migration (#633)
The GPT head in examples/debby was switched from openai-agents to codex (to avoid the unpinned-model Databricks fallback), but the test still asserted openai-agents. Co-authored-by: Isaac |
||
|
|
3608e767d3 |
test(codex): add real CLI parity harness (#556)
* test(codex): add real CLI parity harness * docs(codex): explain parity sidecar architecture * docs(codex): explain sidecar cargo patches * test(codex): use git dependency for parity fixtures * test(codex): clarify parity regression cases * test(codex): keep regressions in parity harness * refactor: remove dual-mode branch from test_sharing_permissions Always use inline agent + mock LLM — no using_mock_llm branching. The mock server always runs, migrated tests always use it. Co-authored-by: Isaac * lint Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * lint Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci: add codex parity tests to CI workflow Run the real-Codex/mock-Responses parity tests (tests/codex_parity/) as a dedicated CI job. The job installs the Rust toolchain to build the WireMock sidecar and the codex CLI from ci-deps, then runs pytest with --codex-parity. Also excludes tests/codex_parity from the misc catch-all shard to avoid redundant skip collection. Co-authored-by: Isaac * fix(ci): pin rust-toolchain action to commit SHA The repo requires all actions to be pinned to full-length commit SHAs. Co-authored-by: Isaac * fix(ci): correct setup-node action SHA Co-authored-by: Isaac * fix(ci): pre-build parity sidecar before running tests The cargo build was happening inside the pytest session-scoped fixture, which timed out on first run. Move the build to a dedicated CI step so it runs outside the test timeout and benefits from the Rust cache. Co-authored-by: Isaac --------- Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
9ee4c76f12 |
test: migrate claude-coder sandbox e2e tests to mock LLM (#623)
* test: migrate claude-coder sandbox e2e tests to mock LLM Replace real LLM calls with mock LLM server for all 5 sandbox isolation tests. Each test registers an inline claude-sdk agent backed by the mock server, configures it to issue specific tool calls (Read/Write/Glob/Edit targeting paths outside the workspace), and asserts the sandbox blocks them. Co-authored-by: Isaac * fix: address Polly review — stronger write-blocked assertion, URL docs - Add secondary assertion on tool results for write-blocked test: verify the sandbox hook actually fired and returned an error, not just that the file doesn't exist (which could pass if the mock response was never consumed). - Add explicit comment explaining raw URL convention for claude-sdk (Anthropic SDK appends /v1/messages, vs OpenAI /v1/responses). - Add "no API key needed" note to module docstring. Co-authored-by: Isaac |
||
|
|
8dda0b44a5 | test(debby): guard packaged resource sync (#631) | ||
|
|
14de04cd24 | Fix stale D6 fan-out docstring pointer (#627) | ||
|
|
19c846db77 |
fix(debby): run the GPT head on codex so it doesn't fall back to Databricks (#179) (#180)
Debby's GPT head was pinned to the openai-agents harness with no model. In omnigent/inner/openai_agents_sdk_executor.py the client builder treats an unpinned model as a Databricks model (`is_databricks_model = model is None`), so with no OPENAI_API_KEY/OPENAI_BASE_URL in the environment it skips the fail-loud guard and falls back to ambient Databricks credentials — routing the "GPT" head through the Databricks gateway instead of OpenAI. Switch the GPT head to the codex harness: codex is GPT-only, uses OpenAI's native auth, and has no unpinned-model Databricks fallback (a directly-supplied gateway with no model fails loud rather than silently defaulting to databricks-*). Debby already requires an OpenAI credential, so the codex head resolves to OpenAI/GPT. - examples/debby: GPT head harness openai-agents -> codex; refresh the stale comments and orchestrator prompt that named openai-agents. - omnigent/resources/examples/debby: keep the packaged copy (used by server seeding) byte-identical. - tests/cli/test_chat.py: the bundle-materialization test expected gpt=openai-agents; update to codex. - tests/spec/test_debby_example.py: add a parse-only regression guard that the GPT head is codex and pins no Databricks model/auth. Signed-off-by: Arya Buddha <40647186+AryaBuddha@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
657d57e4ad |
Fix Render persistent data disk mount (#573)
* Fix Render persistent data disk mount * Fix ap-web package lock sync * Revert ap-web package lock change |
||
|
|
7b7144ff47 |
Rehome D6 parallel coverage to mock sessions (#592)
* Rehome parallel D6 coverage to mock sessions * test: reset mock LLM around parallel rehome tests * test: harden parallel fan-out test * test: skip mock-only parallel fan-out tests outside mock mode |
||
|
|
eb1817ea57 |
feat(onboarding): auto-detect Claude on Vertex AI via GCP ADC env vars (#606)
* feat(onboarding): auto-detect Claude on Vertex AI via GCP ADC env vars Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix lint * fix lint Update test for vertex-claude detection with missing vars. --------- Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
0755e8fc5b |
fix(sandbox): harden OpenShell launcher: background contract, channel cleanup, observability (#591)
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
2ada11e84b |
fix(claude-native): surface terminal startup errors in failure messages (#187)
When Claude Code crashes on startup (e.g. its API client receives an HTML auth/proxy page and throws "JSON Parse error: Unrecognized token '<'"), its input prompt never renders. The readiness gate then timed out with a generic "Claude Code terminal did not become ready within 30.0s (input prompt never rendered)" RuntimeError in the web UI error banner — while the actual cause was visible only in the terminal pane. Capture the tmux pane one last time on timeout and append its tail to the error so Claude Code's own output surfaces in the UI. Also harden the forwarder's Sessions-API calls: four sites did raise_for_status() then a bare resp.json(), which raises an opaque JSONDecodeError (and a silent supervisor restart loop) when the same expired-OAuth/proxy layer returns a 200 HTML body. Route them through a _parse_json_response helper that re-raises with the content type and a body snippet. Adds 7 unit tests. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1d897ca0fd |
fix(harnesses): fail closed for TOOL_CALL when native policy hook can't reach the server (#579)
* fix(harnesses): fail closed for TOOL_CALL when native policy hook can't reach the server Fixes #536. For native harnesses (claude-native, codex-native) the PreToolUse/ PostToolUse hook subprocess is the entire policy-governance layer: it gates Bash/Write/Edit, the native Skill tool, and connector-native mcp__* tools by POSTing to /v1/sessions/{id}/policies/evaluate. Every error/edge path returned exit 0 with no stdout — "no opinion" — so any condition that prevented a well-formed verdict (server unreachable, non-2xx, empty body, malformed JSON) silently disabled all DENY/ASK enforcement. A transient AP outage turned a blocked tool into an allowed one, with only a stderr line. P0 bypass. Make the hooks' default phase-aware, mirroring the runner-side fix in PR #163. Once a session is known to be governed (active session id + configured ap_server_url) and the evaluate round-trip cannot yield a usable verdict, a PreToolUse (PHASE_TOOL_CALL) call fails CLOSED with a deny — the authoritative, only-enforcement-point gate — while UserPromptSubmit (advisory request gate) and PostToolUse (the tool already ran) keep failing OPEN. Pre-evaluation short-circuits that mean the session simply isn't governed (no session, no ap_server_url, unparseable payload, relay-gated mcp__omnigent__* tools) still emit "no opinion" so non-Omnigent sessions are never blocked. The client timeout is intentionally left unchanged: the long timeout backs the server-side ASK long-poll, and shortening it would break ASK and reintroduce a fail-open. A hung server still blocks (the safe direction) rather than failing open. Changes: - native_policy_hook.py: new shared fail_closed_hook_output() helper. - claude_native_hook.py / codex_native_hook.py: the HTTP-error, empty-body, and malformed-response branches now fail closed for the tool-call gate instead of returning no opinion. - Tests: unit coverage for the helper plus integration tests asserting PreToolUse denies across connect-error/non-2xx/empty/malformed while PostToolUse and UserPromptSubmit stay fail-open. * test/fix(harnesses): address Polly review — clearer non-2xx log, shared test helper, unknown-event guard Non-blocking follow-ups from the Polly AI review on PR #579: - Log non-2xx responses distinctly from connection errors. Both native hooks now catch httpx.HTTPStatusError before the broad httpx.HTTPError branch and log the status code, so a real AP outage (e.g. 503) is distinguishable from an unreachable server in production diagnostics. Behavior is unchanged — both still fail closed for the tool-call gate. - Deduplicate the failing-client test stub into tests/native_hook_helpers.make_failing_client, imported by both the claude- and codex-native hook test modules, so the four failure modes can't drift. - Add an explicit unknown-event test for fail_closed_hook_output ("SomeNewEvent" -> None) documenting the fail-open-for-unknowns contract. |