Skip to content

harness: releasing a held call with resolve { action: "execute" } runs the ORIGINAL arguments, skipping hook mutations #506

Description

@paulefde

Summary

When a call held by a pre-trigger hook is released with
harness::function::resolve { action: "execute" }, the function executes with
the original arguments from the model's tool call. Argument mutations from
the hook chain are not applied on the release path — neither mutations made by
hooks that ran before the holding hook, nor mutations from hooks positioned
after it.

Observed live on harness 1.3.1 (engine 0.21.4, macOS arm64), with both
orderings tested explicitly.

Reproduction

Setup: two pre-trigger hooks bound to demo::write:

  • an approval gate (returns hold for this function),
  • a scope injector that mutates args (continue + args with __scope added).

Case A — injector at priority 20, gate at priority 10 (injector runs first):

  1. Turn calls demo::write; injector mutates args; gate holds.
  2. harness::function::resolve { action: "execute" }.
  3. demo::write executes without the injector's mutation — the pre-hold
    mutation is lost.

Case B — gate first, injector after:

  1. Gate holds before the injector runs.
  2. Release with action: "execute".
  3. The remaining hook chain does not (observably) re-apply mutations: the
    function again executes with the original arguments.

In both cases the function body received the raw model-supplied args, so a
function that fails closed on a missing hook-injected field (as ours does)
errors after an approved release.

Expected

Either of these would be workable, but the current behavior is neither:

  • release re-dispatches through the remaining hook pipeline with mutations
    applied (matching the docs' framing that resolve "resumes through the
    remaining hook pipeline"), or
  • release executes the checkpointed call as mutated up to the hold point.

Why it matters

Any tenancy/scoping model where hooks inject validated context into tool calls
breaks on the approval path: exactly the calls important enough to need
approval are the ones that lose their injected scope. Combined with the holding
hook's own mutations being discarded (#505), there is currently no
way to get hook-injected arguments into a held-then-released call.

Workaround we use

We stopped releasing with action: "execute" for confirmed writes: our backend
performs the confirmed write itself (with full context) and resolves the held
call with action: "deliver" + resultJson, so the parked run resumes on a
result we produced. This works well, but it means the "release and let the
harness execute" model is effectively unusable with hook-injected arguments.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions