Skip to content

feat(actions): typed merge/ready/CI/PR/push actions gated by multi_bot switches - #6

Merged
lukisch merged 2 commits into
feat/multibot-resourcesfrom
feat/multibot-typed-actions
Oct 17, 2026
Merged

lukisch merged 2 commits into
feat/multibot-resourcesfrom
feat/multibot-typed-actions

Conversation

@ellmos-multi-bot

Copy link
Copy Markdown
Collaborator

Problem: the core knows three write actions (issue.add_labels, issue.comment, api.request). Merging, marking a draft ready, triggering CI and opening fix PRs therefore stay impossible even when an instance sets multi_bot.merge_enabled and the policy switches. Scoping api.request by endpoint prefix is far too coarse for merges (repos/<owner>/ allows every write in every repository), draft-ready exists only in GraphQL, and a branch push is not an API call at all.

Change:

  • Six typed actions in typed_actions.py: pr.merge, pr.ready, actions.dispatch, actions.rerun, pr.create, git.push_branch. Each re-reads the target before writing, binds to the exact reviewed sha where that applies, and reads the result back (the readback goes into the receipt).
    • pr.merge: open, exact head, default base (not stacked), not a draft, mergeable_state == "clean" (so an admin token cannot bypass required checks), configured merge account; PUT …/merge with sha; merged read back.
    • pr.ready: exact draft head, default base, GraphQL markPullRequestReadyForReview, readback.
    • actions.rerun: completed run of the exact head and not from a fork. actions.dispatch: active workflows only.
    • pr.create / git.push_branch: bot branch prefix (typed_actions.push_branch_prefix, default bot/), never the default branch, no force or + refspec, origin must be the target repository, ls-remote readback.
  • The kinds are in the default ceiling but never in enabled. Each kind also needs its multi_bot switch (merge_enabled, policy.draft_*, policy.trigger_missing_ci, policy.open_fix_prs), so a permission alone opens nothing. The MULTI-BOT run contract reports effective_enabled.
  • New CLI command act <kind> --params '{…}' that uses the existing preview → --apply --expect-plan contract.
  • The policy prompt routes merges, ready, CI triggers and fix PRs through these actions. A job that waits for a runner (for example a self-hosted Mac runner) counts as HOLD, never green.
  • .gitattributes pins the prompt and JSON resources to LF, so the bound policy hash no longer depends on core.autocrlf.

Stacking: based on feat/multibot-resources (#5) plus the previously unpushed commit 539b0d0 (policy switches), which this change depends on. Merge only after #5.

Tests (Windows, Python 3.12, outside OneDrive): ruff check . and ruff format --check . clean; pytest -q → 190 passed, 2 skipped, 5 failed. The 5 failures are the existing tests/test_multibot_windows.py starter tests, which also fail on the base branch on Windows (the namespace stub is shadowed by the installed package; see the review on #5). CI runs only on Ubuntu, where they are skipped. 16 new tests in tests/test_typed_actions.py cover: default denial, switch gating, static parameter checks, sha-bound merge with readback, refusal on moved head / blocked / unknown / stacked / draft / closed / foreign identity / missing readback, GraphQL ready, fork/foreign-head/running rerun refusal, inactive workflow, PR base, push without force and with origin check, default-branch protection, effective_enabled, CLI preview → apply.
Live read-only check: GhCliTransport.request("POST", "graphql", …) with a read query works through gh api. A preview against a real instance layer correctly denies pr.merge (not enabled there).

Not included: activating the actions in any instance configuration. An independent review is required before that (author: this session).

Ticket: T-20261017-353754430

🤖 Generated with Claude Code

https://claude.ai/code/session_01C4ZiDnWpjFvgsu8AjdPgbN

Lukas Geiger and others added 2 commits October 17, 2026 18:54
… and publishing switchable

multi_bot.policy carries validated switches into the run request and the packaged
policy documents them. Public defaults stay conservative (model_class independence,
unknown author holds, no draft readying, no CI trigger, no publishing, no fix PRs);
an instance opts in per key. Unknown keys or wrong types fail closed before launch.
The switches loosen no hold, lock, exclude_writes or permission-ceiling rule.

Two tests in test_multibot_resources.py already fail on this branch before the change
under Windows checkouts (line endings, path comparison); not touched here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013G793TMH12ptWX1f6PYcZU
…t switches

api.request can only be scoped by endpoint prefix; "repos/<owner>/" would
allow every write in every repository, and draft-ready needs GraphQL. Add six
narrow actions instead: pr.merge (fresh read, exact sha, default base, not a
draft, mergeable_state clean, configured merge account, sha-bound PUT, merged
readback), pr.ready (GraphQL markPullRequestReadyForReview at the exact draft
head), actions.dispatch (active workflow only), actions.rerun (completed,
non-fork run of the exact head), pr.create (bot-prefixed branch onto the
default branch) and git.push_branch (no force, bot prefix, never the default
branch, ls-remote readback).

They are in the default ceiling but never enabled by default, and each also
needs its multi_bot switch, so a permission alone opens nothing. The MULTI-BOT
run contract now reports effective_enabled. New `act` command for one action
through the existing preview/apply/expect-plan contract. The policy prompt
routes merges, ready, CI triggers and fix PRs through these actions and treats
a job waiting for a (self-hosted) runner as HOLD, never green.

Prompt and JSON resources are pinned to LF so the bound policy hash no longer
depends on core.autocrlf.

Ticket: T-20261017-353754430

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4ZiDnWpjFvgsu8AjdPgbN
@lukisch
lukisch merged commit 52b5410 into feat/multibot-resources Oct 17, 2026
lukisch pushed a commit that referenced this pull request Oct 17, 2026
…t switches (#6) (#7)

* feat(multibot): make review independence, draft readiness, CI trigger and publishing switchable

multi_bot.policy carries validated switches into the run request and the packaged
policy documents them. Public defaults stay conservative (model_class independence,
unknown author holds, no draft readying, no CI trigger, no publishing, no fix PRs);
an instance opts in per key. Unknown keys or wrong types fail closed before launch.
The switches loosen no hold, lock, exclude_writes or permission-ceiling rule.

Two tests in test_multibot_resources.py already fail on this branch before the change
under Windows checkouts (line endings, path comparison); not touched here.


Claude-Session: https://claude.ai/code/session_013G793TMH12ptWX1f6PYcZU

* feat(actions): typed merge/ready/CI/PR/push actions gated by multi_bot switches

api.request can only be scoped by endpoint prefix; "repos/<owner>/" would
allow every write in every repository, and draft-ready needs GraphQL. Add six
narrow actions instead: pr.merge (fresh read, exact sha, default base, not a
draft, mergeable_state clean, configured merge account, sha-bound PUT, merged
readback), pr.ready (GraphQL markPullRequestReadyForReview at the exact draft
head), actions.dispatch (active workflow only), actions.rerun (completed,
non-fork run of the exact head), pr.create (bot-prefixed branch onto the
default branch) and git.push_branch (no force, bot prefix, never the default
branch, ls-remote readback).

They are in the default ceiling but never enabled by default, and each also
needs its multi_bot switch, so a permission alone opens nothing. The MULTI-BOT
run contract now reports effective_enabled. New `act` command for one action
through the existing preview/apply/expect-plan contract. The policy prompt
routes merges, ready, CI triggers and fix PRs through these actions and treats
a job waiting for a (self-hosted) runner as HOLD, never green.

Prompt and JSON resources are pinned to LF so the bound policy hash no longer
depends on core.autocrlf.

Ticket: T-20261017-353754430


Claude-Session: https://claude.ai/code/session_01C4ZiDnWpjFvgsu8AjdPgbN

---------

Co-authored-by: Lukas Geiger <lukas@um-bruch.org>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

2 participants