Skip to content

feat(start-sdk,start-os)!: let a service run an action that takes input - #4062

Merged
dr-bonez merged 2 commits into
masterfrom
fix/sdk-getinput-prefill
Sep 23, 2026
Merged

dr-bonez merged 2 commits into
masterfrom
fix/sdk-getinput-prefill

Conversation

@helix-a

@helix-a helix-a commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

What changes

A service can run an action that takes input — its own, or another service's that access admits. Before this, both failed:

  • The SDK's Action.run validates against the form getInput built under the same event id (prevInputSpec[effects.eventId]).
  • On the service path, effects.action.getInput and effects.action.run are separate effect calls, and each runs under a fresh procedure_id. That id is #[serde(default)], skipped in the TypeScript type, and a new random Guid per call.
  • So every run threw getActionInput has not been called for EventID ….

The fix uses the handshake the user's CLI already has (start-cli package action run --event-id):

  • start-core (Rust): the effect's RunActionParams takes an optional event_id. When present, the run executes under it instead of the per-call id. The EffectsRunActionParams binding and the start-container action run man page are regenerated.
  • start-core (TS): Effects.action.run gains eventId?. Effects.action.getInput gains prefill?, which the generated GetActionInputParams and StartOS already accepted but the hand-written type omitted.
  • sdk.action.run (breaking, in the unreleased 3.0.0):
    • input is now a function from the opened form (spec, plus the value from the action's prefill function) to the input.
    • The helper calls getInput, applies the function, and runs under that form's eventId.
    • It gains packageId, which was commented out, so it could only target the service's own actions, and prefill.
    • A plain input value is no longer accepted: it could not answer a form.
    • An action without input runs without opening one.
  • Docs: the packaging book's actions page gains "Running Another Service's Action", and the access section points at it. The 3.0.0 changelog gets one ### Changed entry.

Why

Start9Labs/tor-startos#39 makes Tor's Add/Delete Onion Service access: 'public' so a service can manage its own addresses. Start9Labs/bitcoin-core-startos#309 is the first consumer: after retiring the port its StartOS 0.3.5 version bound, Bitcoin re-attaches that version's peer .onion to its current binding. Nothing in the tree ran an action with input from a service before, which is how the event-id gap went unnoticed. #4045 notes the path was not exercised on a box.

For the reviewer to decide

startosTypeValidation.test.ts checks one direction only: each generated params type must be assignable to the Effects parameter. A binding field missing from Effects still passes, which is how getInput's prefill went unnoticed. I have not made the check two-way here, since it may surface other effects that already differ.

Verification

  • cargo check -p start-core and make start-core-format-check pass.
  • make start-core-ts-bindings and make manpages change only EffectsRunActionParams.ts and start-container-action-run.1.
  • start-core: tsc passes, and make test passes 17 suites. The new runAction.test.ts drives a real Action through effects that give each call a fresh event id, as StartOS does. It checks three things: the helper answers the form it opened; a run naming no opened form is still refused; and an action without input skips the form.
  • start-sdk: make bundle, make check and make test (131 tests) pass. container-runtime's npm run check passes against the rebuilt bundle.
  • Prettier 3.8.3, the repo pin, passes on every changed file. The packaging book builds with mdBook v0.5.2.
  • Not exercised yet on a box. I am testing it through Bitcoin → Tor on a VM built from this branch.

StartOS accepts a prefill on the effect (GetActionInputParams carries it,
and the container runtime forwards the options object whole), but the
hand-written Effects type omitted it. A service reading another package's
action form for a particular target, such as Tor's Add Onion Service for
one of its own interfaces, could not say which target without a cast.

startosTypeValidation did not catch it: it checks that each generated
params type is assignable to the Effects parameter, and a binding with an
extra field still is.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
…form it opened

A service could not run an action that takes input, its own or another's.
The SDK's run validates against the form getInput built under the same
event id, and each effect call runs under a fresh procedure id, so the run
found no form and threw "getActionInput has not been called".

effects.action.run now takes the eventId getInput returned and runs under
it, the handshake `start-cli package action run --event-id` already uses.
sdk.action.run is built on it: `input` becomes a function from the opened
form to the input, and the helper opens the form, applies the function and
runs under that form's event id. It also gains `packageId`, to run another
service's action that `access` admits, and `prefill`. A plain `input` value
is no longer accepted; it could not reach a form.

The binding and the start-container man page are regenerated. The actions
page gains a section on running another service's action, and the
unreleased 3.0.0 changelog entry replaces the prefill-only one.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
@helix-a helix-a changed the title fix(start-sdk): let a service pass prefill to effects.action.getInput feat(start-sdk,start-os)!: let a service run an action that takes input Sep 23, 2026
@dr-bonez
dr-bonez merged commit 304926d into master Sep 23, 2026
45 of 46 checks passed
@dr-bonez
dr-bonez deleted the fix/sdk-getinput-prefill branch September 23, 2026 19:02
helix-a added a commit that referenced this pull request Sep 23, 2026
The previous commit flattened `EventId` into the effect params with
`#[arg(skip)]`, which removed `start-container action run --event-id`
(added in #4062). Without it every in-container CLI call gets a fresh id,
so `run` can never answer the form `get-input` opened.

`EventId` is now a clap arg group: `--event-id` on both `get-input` and
`run`, flattened after `--package-id`. Its field is an `Option<Guid>` so
neither clap nor the man pages carry a random default, and `EventId::or_new`
resolves it for the handlers. Its comment is a plain `//`: as a doc comment,
clap took it as the about text of every command that flattens it, replacing
the `get-input` and `run` descriptions.

Against master the man pages differ only by `--event-id` on `get-input`.
A new test parses `--event-id` for both commands and checks it survives
serialization to the request.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
dr-bonez pushed a commit that referenced this pull request Sep 24, 2026
…r's eventId (#4069)

* fix(start-os,start-sdk): run effect-initiated actions under the caller's eventId

The container runtime tags every effect call with the calling procedure's
event id under `eventId`, but the action effects declared the field as
`procedure_id`, which serde reads from `procedureId`. So every
effect-initiated get-input, run and task lookup reached a service's actor
under a fresh random id. ConcurrentActor skips a message's conflicts with a
running handler of the same id, so those calls lost that re-entrancy, and a
getInput and the run answering it landed under different ids. That is why
#4062 added an explicit `eventId` to effects.action.run, and why it still
failed on a box: the runtime overwrote it with the caller's procedure id.

The three effect param structs now flatten one `EventId` (prelude.rs), the
single owner of the envelope field, and the service code names that id
`event_id` all the way to the runtime's `execute`. A form and the run
answering it now share the caller's procedure id with nothing passed, so
#4062's explicit `eventId` on effects.action.run and in sdk.action.run is
removed. The unused `EventId` binding goes with the old struct.

A unit test deserializes each params struct from an envelope and checks the
id arrives.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5

* fix(start-os): keep --event-id on the in-container action CLI

The previous commit flattened `EventId` into the effect params with
`#[arg(skip)]`, which removed `start-container action run --event-id`
(added in #4062). Without it every in-container CLI call gets a fresh id,
so `run` can never answer the form `get-input` opened.

`EventId` is now a clap arg group: `--event-id` on both `get-input` and
`run`, flattened after `--package-id`. Its field is an `Option<Guid>` so
neither clap nor the man pages carry a random default, and `EventId::or_new`
resolves it for the handlers. Its comment is a plain `//`: as a doc comment,
clap took it as the about text of every command that flattens it, replacing
the `get-input` and `run` descriptions.

Against master the man pages differ only by `--event-id` on `get-input`.
A new test parses `--event-id` for both commands and checks it survives
serialization to the request.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5

* fix(start-os): take --event-id on action run only

A CLI command takes an event id when it continues an event an earlier call
started: `run` answers the form `get-input` opened. `get-input` starts the
event and returns its id, so the flag has nothing to name there. This
matches `start-cli package action`, where only `run` has `--event-id`.
get-input keeps reading the envelope id; the man pages match master again.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5

* docs(start-core): state the EventId comment plainly

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants