Skip to content

remote: incode remote run — run a task on GitHub Actions (refs #23) - #49

Merged
blockmandev merged 3 commits into
mainfrom
feat/remote-run
Oct 22, 2026
Merged

blockmandev merged 3 commits into
mainfrom
feat/remote-run

Conversation

@blockmandev

Copy link
Copy Markdown
Member

Adds incode remote run | list | logs (#23): run a task with incode -t on GitHub Actions instead of this machine, wait for it, and print its result.

How it works

  • run dispatches the repository's incode-remote.yml (workflow_dispatch: task, model, id), finds the run by the id in its title, polls every 10 s for up to --timeout minutes (60 by default; a wait that runs out cancels nothing), then prints result.json from the incode-result artifact plus branch.
  • Exits non-zero when the run failed, the task reported is_error, or the wait ran out. logs <run-id> exits the same way for a completed run.
  • Up to 5 failed reads in a row while polling (a 502, a rate limit, a dropped connection) are reported and asked again rather than ending the wait.

Token safety

  • The GitHub token is read from the standard variables, sent only in a header, only to the API's own origin, and only over https or to this machine.
  • Redirects are followed one hop at a time, so the artifact download's redirect to blob storage is made without the token.

The workflow (.github/workflows/incode-remote.yml.example, inert here; setup in docs/remote-runs.md)

  • Two jobs. task runs the agent with contents: read and persist-credentials: false, and uploads its result and a patch of its changes as data.
  • publish, on a fresh runner with contents: write, applies the patch and pushes incode/remote-<id>. The agent's commands get sudo on a hosted runner, so nothing in its own job can be trusted to push.
  • A patch that holds the model key as written is not pushed.
  • Every input reaches the shell only through environment variables. No PR is opened automatically.

Verified locally

  • cargo fmt --check and cargo clippy --all-targets -D warnings are clean; cargo test passes (lib 2243, bin 59).
  • 13 remote tests run against a loopback fake GitHub, with the artifact behind a 302 to a second host. Each key test was checked with a deliberately broken control.
  • The workflow YAML parses and each step passes bash -n.
  • The task/publish git steps were simulated against a local bare repo:
    • normal case: the agent's own commit, its uncommitted file and a byte-identical binary reached the branch;
    • the task text with $(...) stayed literal;
    • a patch holding the key, and a bad id: nothing pushed.

Not verified: the workflow has not run on GitHub Actions yet; the jq parts were not run locally (no jq on this machine; Ubuntu runners have it). GitHub Enterprise is not supported.

…ult (refs #23)

incode could only run a task on the machine it was started on. A long task
held that machine and its session for as long as it ran, and nothing could
hand it to a runner and collect the outcome later.

`incode remote run "<task>"` dispatches the repository's incode-remote.yml
workflow (workflow_dispatch: task, model, id), finds the run by the id the
workflow titles it with, polls it every 10 s for up to --timeout minutes
(60 by default; a wait that runs out says the run is still going and
cancels nothing), and prints result.json from the run's incode-result
artifact: the `-t --output-format json` result plus `branch`, the
incode/remote-<id> branch the run pushed its changes to. It exits non-zero
when the run did not succeed, the task reported is_error, or the wait ran
out. `list` and `logs <run-id>` show past runs and a run's result.

The token (GH_TOKEN, else GITHUB_TOKEN) travels only in a header, only to
the API's own origin, and only over https or to this machine
(key_may_travel_to). Redirects are followed one hop at a time, so the
artifact download's redirect to blob storage is fetched without it. The
repository is -R or the github.com repository origin names; both are held
to GitHub's name characters so neither can reshape an API path.

The workflow ships as a template (.github/workflows/incode-remote.yml.example,
inert in this repository) that installs and configures incode as the
GitHub Action does, passes every input through environment variables, and
writes the result outside the checkout so it is never committed. A test
holds the template to the names remote.rs relies on and refuses an input
spliced into a script. Setup is in docs/remote-runs.md.

Tests run each path against a loopback fake GitHub API: dispatch body and
headers, discovery by id, polling, the artifact zip, the token kept off the
redirect target, timeout, failed conclusion, remote parsing, missing token.
…d poll (refs #23)

The workflow ran the agent and pushed its branch in one job. A hosted
runner gives the agent's commands sudo, and actions/checkout left the
job's contents:write GITHUB_TOKEN in .git/config, so a prompt-injected
task could push anywhere in the repository, or rewrite git so the later
push step did. The template now has two jobs: `task` runs incode with
contents:read and persist-credentials:false and uploads its result and a
patch of everything it changed (against $GITHUB_SHA, so its own commits
count) as data; `publish`, on a fresh runner with contents:write, applies
the patch and pushes incode/remote-<id>. A patch that fails to apply, or
that holds the model key as written, is not pushed, and the result says so
(is_error, publish_error) instead of reporting branch null as if nothing
changed.

`incode remote run` ended an hour-long wait on the first failed read of
the run: the `?` on get_run turned one GitHub 502, rate limit or dropped
connection into a failure. Up to POLL_MISSES_TOLERATED (5) failed reads
in a row are now reported and asked again.

`incode remote logs` exited 0 for a run that failed, so a script that
dispatched with --no-wait could not tell failure from success; it now
exits non-zero when a completed run did not succeed or its task reported
an error, as `run` does.

Tests: the template test holds the split (incode only in `task`, no
contents:write there, no persisted credentials); a fake that answers 502
to the first polls; logs' exit for failed and is_error runs. Each was
checked against a deliberately broken control.
The Windows CI checkout has core.autocrlf on, so the workflow template
that include_str! reads there has CRLF line endings and the template test
split it on "\n  publish:\n", which never matched: "the template has a
publish job" failed on windows-latest only. The test now finds the job by
its line and checks the template both as LF and as CRLF, so a run on an
LF checkout reproduces the Windows case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant