6f0257dbc7
* feat: add Kiro native CLI harness Signed-off-by: Michael Gardner <gardnmi@gmail.com> * fix(kiro): avoid ambient env in tmux attach Signed-off-by: Michael Gardner <gardnmi@users.noreply.github.com> * fix: restore uv.lock pypi.org sources (drop accidental databricks-proxy re-lock) A local `uv run` during the merge re-locked uv.lock against this machine's Databricks-internal pypi proxy, flipping every package source URL. Kiro changes no dependencies and pyproject.toml is unchanged vs main, so restore main's uv.lock verbatim (pypi.org sources). Only registry URLs differed — no version or hash changes. Co-authored-by: Isaac * test(e2e-ui): add native-kiro render-parity suite (E2E UI Required gate) The E2E UI Required gate flagged that #899 changes the agent-picker/session UI (adds Kiro) without a tests/e2e_ui/** test. Add test_native_kiro_render_parity.py mirroring the cursor/goose siblings — composer-IN parity, a TUI-originated turn surfacing OUT, and no duplicate rendering — plus the native_kiro_session fixture. Skip-gated on kiro-cli + tmux, so it skips in CI (no Kiro account provisioned) exactly like the goose/cursor suites, and runs for real where Kiro is signed in. Verified: collects + skips cleanly (kiro-cli absent); ruff clean. Co-authored-by: Isaac * fix: restore ap-web/package-lock.json npmjs.org sources (drop databricks npm-proxy) Same root cause as the uv.lock fix: an npm command during round-1 merge re-resolved one dependency (yaml-1.10.3) against this machine's Databricks-internal npm proxy (npm-proxy.cloud.databricks.com), which CI (pinned to registry.npmjs.org) can't reach -> 'npm ci' ETIMEDOUT. ap-web/package.json is unchanged vs main and Kiro adds no npm dependency, so restore main's package-lock.json verbatim (clean npmjs.org sources). Co-authored-by: Isaac * test(e2e): exclude kiro-native from the live-harness matrix coverage check test_run_harness_live_matrix_covers_registered_coding_harnesses asserts every registered coding harness is either in the live no-AGENT e2e matrix or explicitly excluded. kiro-native is a terminal-first TUI launched via `omni kiro` (tmux pane + bridge dir), not `omnigent run --harness kiro-native`, so — like goose-native / qwen-native / cursor-native — it can't run in this matrix. Add it to the exclusion set with the matching rationale; its coverage is the kiro-native bridge/executor/ forwarder unit tests + the test_native_kiro_render_parity e2e_ui suite. Co-authored-by: Isaac * test(ap-web): set isNativeWrapper in /compact composer menu tests #1139 gated "/compact" behind isNativeWrapper (hidden for non-native harnesses), but the three slash-menu-UX tests that assert "/compact" tops/appears in the suggestions still rendered a non-native composer, so they now fail on main (and on every PR that merges main). Render those three with isNativeWrapper:true so "/compact" is offered, restoring the built-in ordering the tests pin. Test-only; no behavior change. Fixes the inherited ChatPage.composer.test.tsx red on this PR. Co-authored-by: Isaac * test(kiro): cover kiro_native launcher helpers (raise coverage 43%→70%) The kiro-native launcher (omnigent/kiro_native.py) was the largest coverage gap on this PR: its CLI/daemon orchestration is only exercised by the live render-parity e2e, which skips in CI when kiro-cli is absent. Add focused unit tests (with a fake httpx client) for the unit-testable surface: executable resolution, launch-argv assembly, terminal-payload decoding, tmux attach gating, startup-progress forwarding, preflight, resume-id resolution, and the create/fetch/ ensure/find/wait session helpers (success + error branches). Lifts kiro_native.py from 43% to 70%; remaining misses are the daemon-driven async orchestration covered by runner/e2e paths. Co-authored-by: Isaac * test(kiro): rename test env var to avoid exfil-scan false positive The CI exfil scanner flags any added file containing a secret-named source (regex `[A-Z0-9]+_SECRET\b`) together with a network sink. The tmux-allowlist test used `OMNIGENT_SECRET` purely as a non-allowlisted sample var, which matched the secret regex and — combined with the fake httpx client's .post()/.get() in the same file — tripped the "secret-named source + network sink" block. Rename it to a neutral `OMNIGENT_UNLISTED_VAR`; the test's intent (filtering non-allowlisted keys) is unchanged. Co-authored-by: Isaac --------- Signed-off-by: Michael Gardner <gardnmi@gmail.com> Signed-off-by: Michael Gardner <gardnmi@users.noreply.github.com> Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""Executor that bridges Omnigent web-chat turns into the native Kiro TUI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
from collections.abc import AsyncIterator
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from omnigent.inner.executor import (
|
|
Executor,
|
|
ExecutorConfig,
|
|
ExecutorError,
|
|
ExecutorEvent,
|
|
Message,
|
|
ToolSpec,
|
|
TurnComplete,
|
|
)
|
|
from omnigent.kiro_native_bridge import KIRO_NATIVE_BRIDGE_DIR_ENV_VAR, inject_user_message
|
|
|
|
|
|
class KiroNativeExecutor(Executor):
|
|
"""Harness-side executor for ``omnigent kiro`` web-UI turns."""
|
|
|
|
def __init__(self, bridge_dir: Path | None = None) -> None:
|
|
self._bridge_dir = bridge_dir or _bridge_dir_from_env()
|
|
self._inject_lock = asyncio.Lock()
|
|
|
|
def supports_streaming(self) -> bool:
|
|
""":returns: ``False`` — output is shown by the embedded terminal."""
|
|
return False
|
|
|
|
def supports_live_message_queue(self) -> bool:
|
|
""":returns: ``True`` — messages can be injected mid-turn."""
|
|
return True
|
|
|
|
async def enqueue_session_message(self, session_key: str, content: Any) -> bool:
|
|
"""Inject a live steering message into the Kiro terminal."""
|
|
del session_key
|
|
text = _content_to_text(content, self._bridge_dir)
|
|
if not text:
|
|
return False
|
|
try:
|
|
async with self._inject_lock:
|
|
await asyncio.to_thread(inject_user_message, self._bridge_dir, content=text)
|
|
except RuntimeError:
|
|
return False
|
|
return True
|
|
|
|
async def run_turn(
|
|
self,
|
|
messages: list[Message],
|
|
tools: list[ToolSpec],
|
|
system_prompt: str,
|
|
config: ExecutorConfig | None = None,
|
|
) -> AsyncIterator[ExecutorEvent]:
|
|
"""Inject the latest web-UI user message into the Kiro TUI pane."""
|
|
del tools, system_prompt, config
|
|
text = _latest_user_text(messages, self._bridge_dir)
|
|
if not text:
|
|
yield ExecutorError(message="kiro native turn had no user text to send")
|
|
return
|
|
try:
|
|
async with self._inject_lock:
|
|
await asyncio.to_thread(inject_user_message, self._bridge_dir, content=text)
|
|
except RuntimeError as exc:
|
|
yield ExecutorError(message=str(exc))
|
|
return
|
|
yield TurnComplete(response=None)
|
|
|
|
|
|
def _bridge_dir_from_env() -> Path:
|
|
"""Resolve the kiro-native bridge dir from the harness spawn env."""
|
|
raw = os.environ.get(KIRO_NATIVE_BRIDGE_DIR_ENV_VAR, "").strip()
|
|
if not raw:
|
|
raise RuntimeError(
|
|
f"{KIRO_NATIVE_BRIDGE_DIR_ENV_VAR} is required for the kiro-native harness"
|
|
)
|
|
return Path(raw)
|
|
|
|
|
|
def _latest_user_text(messages: list[Message], bridge_dir: Path) -> str:
|
|
"""Return the latest user message's text."""
|
|
for message in reversed(messages):
|
|
if message.get("role") == "user":
|
|
return _content_to_text(message.get("content"), bridge_dir)
|
|
return ""
|
|
|
|
|
|
def _content_to_text(content: Any, bridge_dir: Path) -> str:
|
|
"""Normalize executor content into text the Kiro TUI receives."""
|
|
if isinstance(content, str):
|
|
return content
|
|
if isinstance(content, list):
|
|
from omnigent.inner.native_attachments import materialize_attachment
|
|
|
|
attachment_lines: list[str] = []
|
|
text_parts: list[str] = []
|
|
for block in content:
|
|
if not isinstance(block, dict):
|
|
continue
|
|
block_type = block.get("type", "")
|
|
if block_type in ("input_text", "text"):
|
|
text = block.get("text")
|
|
if isinstance(text, str):
|
|
text_parts.append(text)
|
|
elif block_type in ("input_image", "input_file"):
|
|
path = materialize_attachment(block, bridge_dir)
|
|
if path is not None:
|
|
attachment_lines.append(f"[Attached: {path}]")
|
|
return "\n\n".join(attachment_lines + text_parts)
|
|
return ""
|