fabro/.fabro/workflows/implement-issue/workflow.toml
fabro-sh-0530[bot] e40dc7d9ad
Move GitHub token permissions to [run.integrations.github.permissions] (#215)
## Summary

Token scopes describe what *a run* is authorized to do, not server
identity. Today they live under
`[server.integrations.github.permissions]`, which can't be overridden by
`workflow.toml` / `project.toml` (server keys are stripped from
per-workflow layers) — so projects and workflows can't tighten or relax
permissions despite the docs already advertising a per-run config. This
PR moves them under `[run.integrations.github.permissions]`, where the
standard layer-merge (workflow > project > user > defaults) Just Works.
Greenfield, no migration shim.

## What changed

- **New layer/resolved types** in `fabro-config` and `fabro-types`:
`RunIntegrationsLayer`, `RunIntegrationsGithubLayer`, and resolved
counterparts. `permissions` becomes a flat `HashMap<String,
InterpString>` post-resolve; empty = no token requested.
- **Server schema**: `permissions` removed from `GithubIntegrationLayer`
/ `GithubIntegrationSettings`. `deny_unknown_fields` rejects the stale
path.
- **Bundled `workflow.toml` parsing** (`run_manifest.rs`): now goes
through `SettingsLayer` via the new `parse_run_layer_from_settings_toml`
helper, so stale `[server.integrations.github.permissions]` errors
instead of being silently dropped by the old `toml::Table` lift-out.
- **Consumers updated**: server preflight, run launch path, and the CLI
worker (`runner.rs`) all read run-level permissions. CLI worker
previously hardcoded `HashMap::new()` — runs launched via the local CLI
path were getting no `GITHUB_TOKEN` regardless of TOML.
- **Shared helpers** on `RunIntegrationsGithubSettings`:
`is_token_requested()` and `resolve_permissions(lookup)` so server and
CLI don't drift.
- **OpenAPI + TS client** regenerated; new `RunIntegrationsSettings` /
`RunIntegrationsGithubSettings` schemas added, `permissions` removed
from `GithubIntegrationSettings`.
- **Repo workflows + docs** rewritten to the new path. Docs gain a
security-model note (boundary = installation grants; no Fabro-side cap).

## Key design decision: hand-rolled `Combine` for
`RunIntegrationsGithubLayer`

`ReplaceMap`'s "empty inherits from below" semantics (`maps.rs:76-80`)
are wrong here — we want `permissions = {}` in a higher layer to act as
an explicit clear. So the layer field is `Option<HashMap<...>>` with
hand-rolled `Combine`:

| Higher layer | Lower layer | Result |
|---|---|---|
| `None` | anything | lower (inherit) |
| `Some(map)` | anything | `Some(map)` (full replace, including
`Some({})` = clear) |

Not derived: the blanket `Option<T: Combine>` impl would recurse into
the inner `HashMap` and reintroduce empty-fallback. Documented inline in
`layers/run.rs`.

`InterpString` is preserved through resolve and only flattened to
`String` at the start-services boundary, matching the existing pattern.

### Plan Summary

- New `[run.integrations.github.permissions]` layer + resolved types;
remove from server side.
- Hand-rolled `Combine` so empty-wins-as-clear; no change to
`ReplaceMap` semantics for other consumers.
- Strict `SettingsLayer` parse for bundled `workflow.toml` so stale
schema errors loudly.
- Both server and CLI worker paths read run-level permissions via shared
helpers.
- OpenAPI + TS client regenerated; parity test added.
- Repo workflow TOMLs and `integrations/github.mdx` rewritten.


### Fabro Details

<details>
<summary>Ran 0 stages in 61m 23s for $53.41</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **61m 23s** | **$53.41** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:33:31 -04:00

5 lines
91 B
TOML

_version = 1
[run.integrations.github.permissions]
pull_requests = "read"
issues = "read"