Commit Graph

91 Commits

Author SHA1 Message Date
roboomp 78ef6805f3 fix(sdk): thread response model into after_provider_response context
ExtensionRunner.emitAfterProviderResponse accepted the response model but
discarded it, calling createContext() with no model. Response-scoped hooks
therefore saw the primary session model in ctx.model and ctx.models.current()
even when the response came from a cross-provider side request, so an extension
that revokes a credential on an HTTP 402 could target the wrong provider.

Call createContext(model) to match emitBeforeProviderRequest, plus a regression
test asserting both fields expose the response model.

Fixes #8955
2026-08-19 09:44:22 +00:00
can1357 9913c58ec1 Merge branch 'main' into pr-8052 2026-08-17 11:00:58 +03:00
can1357 f474b43880 chore(changelog): normalized changelogs after merged fixes 2026-08-16 02:59:03 +02:00
can1357 5b24971163 fix(extensions): charged custom dialog setup to timeout 2026-08-16 02:45:04 +02:00
can1357 06793c72bb Merge PR #8441: fix(extensions): pause tool-call timeout during human dialogs (@Seljuke)
# Conflicts:
#	packages/coding-agent/test/extensions-runner.test.ts
2026-08-16 02:45:04 +02:00
roboomp 1fc05fc635 fix(tui): waited for wire-aliased edit previews
Matched model-scheduled tool calls against canonical and custom wire names so approval waits for the rendered edit diff.

Covered canonical and apply_patch alias approval ordering.

Fixes #8607
2026-08-15 03:32:17 +00:00
Larry Gordon 6e4334c003 feat(extensions): broker denied file writes and deletes
A host that runs omp inside an OS sandbox can grant a path mid-session but cannot
apply that grant to an in-process write: `write` and `edit` do their I/O in the
agent process, so an out-of-workspace write fails and stays failed until the
process restarts under a wider profile.

Nothing available today closes that. A `tool_call` handler can block and a
`tool_result` handler can rewrite content, but neither can re-run a tool.
`ctx.invokeTool` delegates execution, but the delegated native tool runs in the
same process under the same restrictions. And the failure lands AT the write
syscall - after the tool computed the final content, before it returned - so the
bytes are gone with the throw, and reconstructing them means reimplementing
`edit`'s hashline protocol and the snapshot bookkeeping.

The byte-write that `write`, `edit` and `apply_patch` perform on an ordinary file
path already funnels through one two-line primitive
(`file ? file.write(content) : Bun.write(dst, content)`) at four call sites.
Routing that primitive through `writeFileWithFallback` gives an embedder a single
seam to intercept a permission-denied write: the native tool still records its own
snapshot under the real destination path once a handler reports success, so a
follow-up hashline `edit` on that path keeps working.

Only a permission boundary diverts - `EPERM`/`EACCES`/`EROFS`. Two cases needed
more than that:

- `Bun.write` creates missing parents itself, and when that `mkdir` is the denied
  operation it reports the subsequent `open()`'s `ENOENT` instead of the denial -
  making a sandboxed write into a new out-of-tree directory indistinguishable from
  an ordinary bad path. Redoing the `mkdir` explicitly recovers the real errno, and
  because it runs through the same enforcement path as the write it also sees
  kernel-level denials (Seatbelt, LSM) that a `stat`/`access` probe reports as
  writable. If no handler takes the write, the original `ENOENT` is still what
  propagates, with the recovered denial attached as its `cause`.
- `apply_patch` creates the parent as a separate step before writing, so a denial
  there threw before the seam was ever reached. That `mkdir` now tolerates a
  permission denial when a fallback is registered, letting the write report it.

A denial reached through a SYMLINK is never brokered. The in-process write follows
the link, so the kernel denied the link's TARGET, but a handler receives `dst` and
a privileged helper opening it with ordinary follow semantics would land the bytes
wherever the link points. That also defeats the obvious helper-side defence, since
a prefix allowlist passes when the link sits inside the allowed root while its
target does not. omp cannot vouch for the destination, so it refuses rather than
hand the ambiguity to a privileged writer - the same answer `confineToWorkspace`
already gives an unresolvable link.

Removing a file is a different primitive, so it gets its own seam
(`deleteFileWithFallback`, `registerFileDeleteFallback`) covering `edit`'s `REM`,
a hashline `MV`'s source unlink, and `apply_patch`'s delete op. Two differences
from the write path: `ENOENT` is never diverted, since nothing is created on the
way to an unlink and `REM` needs it to become a not-found error; and the seam
refuses a target it can confirm is a directory, because `unlink` on a directory
reports `EPERM` on Darwin and is otherwise indistinguishable from a sandbox
denial. That check cannot always run - a sandbox denying the unlink usually denies
the target's metadata too - so the request carries `confirmedFile`, and a handler
is required to use a plain unlink rather than resolving or recursing.

The two registries are deliberately separate. A write handler brokers `content` to
`dst`, so a delete request reaching it with no content invites brokering an empty
write and truncating the file it was asked to remove.

With nothing registered both seams are inert: the primitives run exactly as
before, a failure rethrows from the same place, and no extra syscalls are
performed.

Scope is deliberately narrow. Archive-member and SQLite writes are unchanged -
neither is a byte-write to a path, so brokering them needs a different request
shape - along with the ACP bridge's `writeTextFile`, the `lsp` tool's own
workspace-edit and formatter writes, and directory removal.
2026-08-14 08:52:34 -07:00
Seljuke fd28acf5a1 fix(extensions): harden dialog timeouts 2026-08-13 20:39:12 +02:00
can1357 b279db1790 test: refactored test suites to eliminate time-based sleeps and polling loops
- Replaced time-based sleeps and polling loops with event-driven promise resolvers and fake timers across agent and tool tests.
- Migrated test suites to share in-memory auth storage and fixtures using lifecycle hooks.
- Updated catalog model definitions, metadata, and configurations.
2026-08-13 19:32:22 +02:00
Seljuke 7e384fbc4f fix(extensions): pause tool-call timeout during human dialogs 2026-08-13 18:03:01 +02:00
roboomp 7463803c95 fix(extensions): populated runtime mode in context
Expose the Pi-compatible tui, rpc, json, or print host mode to every extension context and cover mode transitions in the runner regression suite.

Fixes #8419
2026-08-13 08:51:05 +00:00
can1357 6b4823181b test: cleaned test suites and documented filtering guidelines
- Remove redundant definedness, null, and type checks across test suites in multiple packages.
- Clean up unused assertions, metadata tests, and obsolete test cases.
- Add good versus bad test filter guidelines and requirements to project documentation.
2026-08-13 08:28:42 +02:00
can1357 47b282ff9b Merge PR #8069: fix(extensions): register lifecycle tools (@mrexodia) 2026-08-11 15:24:18 +02:00
can1357 f3a3073a3d Merge PR #7959: fix(tui): show edit previews before approval (@roboomp) 2026-08-11 15:18:16 +02:00
Fatih Al-Aziz d194f2d76c fix(tui): honor transformed mode attachments 2026-08-11 15:52:58 +07:00
Duncan Ogilvie 0cf54f428c fix(extensions): preserve mutation queue ownership 2026-08-10 01:40:18 +02:00
Duncan Ogilvie c5c686ca11 fix(extensions): drain tool-call registrations 2026-08-10 00:24:11 +02:00
Duncan Ogilvie 0abb4a9529 fix(extensions): preserve late tool invariants 2026-08-09 23:51:39 +02:00
roboomp ee2f10764c fix(tui): waited for edit previews before approval
- Gated model-issued approval dialogs on the TUI tool preview lifecycle.
- Finalized edit arguments before waiting for the asynchronous diff.
- Added regression coverage for both the preview gate and approval ordering.

Fixes #7957
2026-08-07 22:58:57 +00:00
can1357 bc39ffa265 feat: introduced omptype validation package and migrated workspace dependencies
- Introduce `@oh-my-pi/omptype` as a new ArkType-compatible schema validation package featuring a lazy JIT runtime, JSON Schema emission, and compatibility adapters.
- Replace `arktype` across workspace packages and test utilities with `@oh-my-pi/omptype`.
- Add benchmark suites, tests, and documentation for the new validation engine and adapters.
- Update workspace build, test runner, and release configurations to include the new package.
2026-08-03 21:56:48 +02:00
can1357 f7480294b4 fix(agent): track rewritten xdev approval tiers 2026-08-02 20:57:06 +02:00
can1357 f76ce86966 Merge PR #6840: feat(extensions): add ctx.invokeTool for native built-in delegation (@psyrendust) 2026-07-31 20:17:32 +02:00
Kenneth Hoff ae08bd57cb fix(coding-agent): keep ExtensionContext.cwd live across /move
ExtensionRunner cached cwd from its constructor argument, which is set
once at session start. /move (SessionManager.moveTo) relocates the
active session's directory, but ExtensionRunner never re-read it, so
every ExtensionContext built afterwards (tool calls, hooks, slash
commands) kept reporting the pre-move directory for the rest of the
session -- observed while building an extension that tracks the
session's git worktree via ctx.cwd.

Turn cwd into a getter over this.sessionManager.getCwd() instead of a
constructor-time snapshot. Session-scoped, not the process-global
project directory: the interactive /move handler happens to also
chdir the process (command-controller.ts -> applyCwdChange ->
setProjectDir), but moveTo() itself never touches that global, so a
programmatic AgentSession.moveSession()/SessionManager.moveTo() call,
a collab guest adopting a host's session cwd without chdir'ing, or an
SDK/ACP session opened via createAgentSession({ cwd }) with a cwd that
differs from the process's own would all still observe a stale
ctx.cwd under a getProjectDir()-based getter. Reading the runner's own
sessionManager -- already held for other purposes -- covers every one
of these instead of just the single-session interactive case.

The constructor parameter is kept (renamed _initialCwd, documented as
ignored) so the two existing call sites don't need touching.

Added a regression test constructing a real ExtensionRunner over an
in-memory SessionManager, relocating it via SessionManager.moveTo(),
and asserting both runner.cwd and createContext().cwd observe the new
directory.
2026-07-31 15:13:37 +02:00
Larry Gordon e8d9f15e9d fix(extensions): inherit the wrapper call's abort and progress channels
A bare ctx.invokeTool(params) passed undefined for both signal and onUpdate,
so a wrapper that simply delegates did not stop the native tool when the
outer call was aborted, and native progress updates were dropped unless every
wrapper forwarded them by hand.

createContext now takes the delegation wiring as one named object and binds
the wrapper's own signal and onUpdate as defaults for the delegated call, with
explicit invokeTool options still taking precedence. Grouping toolName, depth,
context, signal, and onUpdate together also keeps the signature readable now
that delegation carries five inputs.

Tests: the delegated native call receives the outer signal and onUpdate,
explicit options override them, invokeTool is absent when no native built-in
of that name exists, and recursion stays bounded per call chain.
2026-07-30 10:35:12 -07:00
can1357 857b70fe99 Merge remote-tracking branch 'refs/remotes/pr/6535' into prep/6535
# Conflicts:
#	packages/coding-agent/src/extensibility/extensions/runner.ts
2026-07-30 01:42:24 +02:00
can1357 da6d11de0e feat(agent): introduced prepareToolCall phase supporting argument replacement
- Added a prepareToolCall phase to the agent loop running before tool scheduling for validation and hooks.
- Updated BeforeToolCallContext and result types to support argument replacement instead of in-place mutation.
- Updated coding-agent extension handling and runner to track emitted tool calls and re-evaluate approvals on input revisions.
- Added comprehensive test coverage for argument replacement, concurrency resolution, and schema validation.
2026-07-27 22:55:20 +02:00
can1357 46707e3e36 Merge PR #6681: feat(extensions): let tool_call handlers revise tool input (@psyrendust) 2026-07-27 22:27:09 +02:00
roboomp eb40353a95 fix(extensions): preserved prototype-backed UI methods
Replaced the scoped UI object spread with a delegating proxy that binds inherited methods to the original context while overriding only abort-capable dialogs.

Extended watchdog coverage with a prototype-backed notification method matching RPC UI contexts.
2026-07-27 13:44:33 +00:00
roboomp 4176a6c799 fix(extensions): registered abort before handler start
Attached the session-stop abort listener and rechecked cancellation before invoking extension work, preventing synchronous ctx.abort() calls from being missed.

Added deterministic coverage for a handler that aborts and then waits on non-UI work.
2026-07-27 13:37:05 +00:00
roboomp 5e6f12b278 fix(extensions): cancelled timed-out handler dialogs
Forwarded confirmation dialog options in the interactive TUI and scoped extension UI dialogs to each handler watchdog signal.

Added regressions for direct confirmation cancellation and fail-closed tool-call timeout cleanup.

Fixes #6805
2026-07-27 13:28:24 +00:00
Larry Gordon d683cf90e7 fix(extensions): resolve tool approval against the revised tool input
Follow-up to the earlier approval re-check, which missed the
prompt-to-prompt case: if the original and revised inputs both resolve
to `prompt`, a handler could swap in different prompt-gated args that ran
under approval granted for the original.

Emit the `tool_call` event before the approval gate instead of after, so
the gate resolves policy and shows the interactive prompt against the
input that actually executes. The user always approves what runs, and
deny/allow-to-prompt/prompt-to-prompt transitions are all covered by one
gate rather than special-cased. A `deny` on the original input still
short-circuits before the runner is touched, so an already-denied tool
never emits `tool_call`.

Tests: the approval prompt reflects the revised input, `tool_call` fires
before `tool_approval_requested`, plus the existing deny/computer/
multi-handler cases.
2026-07-26 00:30:38 -07:00
Larry Gordon 8916e7de9f fix(extensions): re-check approval on revised tool input and skip computer calls in hook wrapper
Addresses PR review feedback.

- Re-gate approval (P1): the extension wrapper's approval/safety gate resolved
  against the original `params`, but execution ran with the overridden input, so
  a handler could rewrite approved args into ones a deny/critical policy would
  have blocked. After an override, re-resolve the policy on the revised input and
  block a revision that newly resolves to `deny` (or, outside yolo, newly requires
  a prompt) instead of running it unapproved.
- Computer skip in hook wrapper (P2): the hook wrapper applied the override to
  `computer` tool calls, contradicting the documented contract; it now skips them
  like the extension wrapper does.

Docs: the shared-events contract note is now accurate for both wrappers and
documents the approval re-check. Tests: added the re-gate (blocks-deny,
allows-benign), multi-handler last-wins, and hook computer-skip cases.
2026-07-26 00:09:50 -07:00
Larry Gordon ed457a9d4f feat(extensions): let tool_call handlers revise tool input
A `tool_call` handler (extension or hook) could previously only block a
tool. It can now also return `input` to replace the arguments the tool
executes with, so a handler can normalize or rewrite a built-in's input
without reimplementing the tool.

The returned object is the raw execution input passed to the tool's
`execute` (the handler owns its correctness), not the normalized
`event.input` view, which may carry derived gate-only fields (e.g.
hashline `edit` `path`/`paths`) that are not real parameters. It is
ignored when `block` is set, and not applied to `computer` tool calls
whose event input is a synthetic actions view rather than the real
params. When multiple handlers set `input`, the last one wins.

Honored in both the extension and hook tool wrappers; documented in
docs/extensions.md, docs/hooks.md, and docs/skills/authoring-hooks.md.
2026-07-25 23:16:04 -07:00
Anthony "Asterisk" Ambuehl 29625f08c2 feat(mcp): add mcp_notification extension event + multi-listener API
Convert MCPManager's dangling single-slot setOnNotification callback into
a multi-listener API and expose server-initiated MCP notifications as an
extension event so extensions can bridge push-capable MCP servers (e.g.
peer messaging, ticket nudges) into session behavior.

API changes:
- Removed: MCPManager.setOnNotification(handler) — single-slot, zero callers
- Added:   MCPManager.addNotificationListener(listener): () => void
           Multi-listener with per-listener error isolation, returns unsub.
- Added:   'mcp_notification' extension event
           Payload: { server: string; method: string; params: unknown }

Wired in sdk.ts: one listener bridges to extensionRunner.emitMcpNotification,
captured under postmortem for teardown.

Tests: 3 new (multi-listener fanout, error isolation, unsubscribe),
fixture pattern matches neighboring mcp tests. bun check passes (biome +
tsgo).

Docs: extensions.md (new MCP notifications subsection with bridging
example), mcp-runtime-lifecycle.md (Server-initiated notifications
section), CHANGELOG.
2026-07-24 13:36:03 -07:00
roboomp 9506548b21 fix(session): cancelled in-flight stop handlers
Raced session_stop handlers against the active settle signal and discarded cancellation without timeout errors.

Added runner and AgentSession regressions for pre-dispatch and in-flight aborts, timeout preservation, and stale continuation suppression.

Fixes #6489
2026-07-24 07:29:39 +00:00
Alex TYRODE a2044703b0 test(extensions): write service-tier fixture asynchronously 2026-07-23 22:21:43 +00:00
Alex TYRODE 7626effec5 fix(extensions): preserve service-tier host compatibility 2026-07-23 22:14:31 +00:00
Alex TYRODE e0928070c2 feat(extensions): expose session service tiers 2026-07-23 21:49:20 +00:00
roboomp bf232ca062 fix(extensions): scoped provider hooks to request model
Passed the per-request model through SDK payload callbacks and ExtensionRunner context creation.

Added regression coverage for Codex-primary and Anthropic-request contexts.

Fixes #6006
2026-07-18 16:52:15 +00:00
roboomp 8a61515819 fix(extensions): isolated self-scheduled callbacks from killing the session
Extension-scheduled setInterval/setTimeout/detached callbacks ran outside
the handler-dispatch try/catch, so a throw surfaced as a process-level
uncaughtException and the global postmortem handler tore down the whole
session instead of isolating the misbehaving extension.

- Added ManagedTimers backing sanctioned ctx.setInterval/setTimeout/clearTimer:
  callbacks run with handler-dispatch isolation (throw/rejection logged and
  routed through onError), handles are unref'd, and all are cleared on
  session_shutdown.
- Wired the helpers into ExtensionRunner.createContext and the runner-less
  command-context fallback; onSession now inherits the runner context.
- Documented in-process no-isolation behavior and the managed timers in
  docs/extensions.md and docs/skills/authoring-extensions.md.

Fixes #5664
2026-07-16 07:17:10 +00:00
can1357 1e1569d58e Merge PR #5465: fix(extensions): let tool_result rewrite thrown failure content (@roboomp) 2026-07-14 22:58:48 +02:00
roboomp 5303c3852e fix(extensions): let tool_result rewrite thrown failure content
ExtensionToolWrapper caught a thrown tool exception, emitted tool_result
with the modifiable result, then rethrew the original executionError whenever
the effective error state stayed true. This discarded any replacement content
or details a handler returned, so an extension could only surface modified
content by returning isError: false, which wrongly converted the failure into
a success.

Return the (possibly modified) result carrying isError instead of rethrowing.
The agent loop already honors AgentToolResult.isError (coerceToolResult) and
surfaces it as a tool error on the wire, so replacement failure content now
reaches the model while the call remains an error. No-modification, error->success, and success->error paths keep their existing semantics.

Fixes #5302
2026-07-14 18:01:02 +00:00
can1357 4b5c32a092 Merge PR #4950: fix(mcp): resolve local image paths for tool calls (@roboomp) 2026-07-14 18:45:22 +02:00
roboomp 3ebcb36901 fix(mcp): threaded local roots through startup tools
- Copied localProtocolOptions through SDK-created custom tool contexts so startup MCP tools resolve '/data/workspaces/can1357__oh-my-pi__4946/.omp-session/2026-07-09T16-06-43-993Z_019f47a1-a619-7000-9062-5f5d863afa45/local' against the active session.
- Exposed localProtocolOptions on extension contexts and covered the runner propagation path.

Fixes #4946
2026-07-09 17:17:27 +00:00
can1357 c944870566 fix(coding-agent): implemented pi's ui.addAutocompleteProvider API
Extensions calling ctx.ui.addAutocompleteProvider (e.g. @ff-labs/pi-fff)
crashed at load with 'TypeError: ... is not a function' because omp's
ExtensionAPI.ui omitted pi's autocomplete-provider API; the throw also
aborted the rest of a try/catch-guarded session_start init.

ExtensionUIContext now declares addAutocompleteProvider(factory).
Interactive mode stacks each factory on the built-in editor provider in
registration order, re-applies the stack on every slash-command refresh,
and skips throwing/malformed factories; RPC, ACP, and headless contexts
accept the factory as a no-op, matching upstream pi's RPC behavior.

Fixes #4919
2026-07-09 18:27:23 +02:00
roboomp 6c56d39dc5 fix(extensions): bound tool_call handlers by extensionHandlerTimeoutMs
emitToolCall awaited each extension handler directly (runner.ts:704-706),
bypassing the #runHandlerWithTimeout wrapper every other subscribed event
routes through. A tool_call handler that never resolves parked
ExtensionToolWrapper.execute indefinitely, freezing tool dispatch even
though the symmetric emitToolResult path has always been timeout-protected.

Race each tool_call handler against Bun.sleep(extensionHandlerTimeoutMs)
inline (the shared wrapper swallows errors, and this callsite is
fail-closed). On timeout: emit an ExtensionError with event: 'tool_call',
log a warning, and return { block: true, reason: 'Extension <path>
timed out after <ms>ms' } — symmetric with the existing per-handler error
branch. Fail-closed is the correct policy for a pre-execution gate: an
unresponsive extension MUST NOT be silent consent to run the tool.

Fixes #3948
2026-07-01 01:03:37 +00:00
can1357 9b3c193dfd Merge PR #1681: fix(coding-agent): expose hashline edit path to extensions (@roboomp)
# Conflicts:
#	packages/coding-agent/test/extensions-runner.test.ts
2026-06-24 18:42:35 +02:00
can1357 a98230addc chore: update changelog & tests 2026-06-19 16:06:18 +02:00
ben bd14fed678 fix(coding-agent): finalize session stop lifecycle 2026-06-17 12:26:52 +02:00
ben c93774f892 fix(coding-agent): implement session stop hook semantics 2026-06-17 12:26:52 +02:00