Skip to content

[POC] (do not review )feat(minimald): GitHub-integrated sessions (spec 10) - #996

Draft
norrietaylor wants to merge 36 commits into
mainfrom
claude/minimald-github-prd-r6ew53
Draft

[POC] (do not review )feat(minimald): GitHub-integrated sessions (spec 10)#996
norrietaylor wants to merge 36 commits into
mainfrom
claude/minimald-github-prd-r6ew53

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 28, 2026

Copy link
Copy Markdown
Member

POC: PR is up to utilize CI to run some validation

Spec 10 — GitHub-integrated minimald sessions

Implements the PRD at docs/specs/10-spec-github-sessions/. Daemon-held GitHub App
auth (device flow), a mediated min git/min api facade so no token enters the
sandbox
, repo pre-priming at activation, scope consent + reuse-or-mint, and a
client-driven PR-on-exit prompt.

Draft: opened to run CI on a real toolchain. The development environment had no
assembler, so minimald (via aws-lc-sys) could not be compiled there — this PR exists
to let CI build and verify the daemon half.

Verified locally (96 tests green)

The whole crates/github engine: domain types, grant store (0600/atomic), device flow,
REST client, leak-proof gitops (env-only credential injection + option-injection guard),
refresh state machine (single-flight, write-ahead rotation), and the test-support mock
GitHub. Plus the minimald-rpc wire contracts.

Written, awaiting CI verification

Daemon GithubService wiring, the authorize() choke point, the auth RPC handlers, the
min github login|status|logout CLI, and the git% facade rework — which isolates the
token-bearing git leg from all sandbox-controlled config (daemon git runs only in a
daemon-authored bare mirror; object transfer via objects/info/alternates + plain-file
ref writes). Facade exfil PoC tests (core.fsmonitor, core.sshCommand, filter.*.process,
[include]) are committed but have never executed locally — CI is their first real run.

Not yet implemented (follow-up)

api% facade verb, in-sandbox min git helper, push/PR + prime RPC handlers, activate
consent/prime wiring, PR-on-exit, redaction + no-token proof harnesses, integration/e2e
suites, docs. Deferred by design: MCP server proper, on_destroy executor, egress
enforcement, NG1–NG6.

🤖 Generated with Claude Code

Note

Add GitHub-integrated sessions with device-flow auth and git facade to minimald

  • Introduces a new github crate with OAuth device-flow, token refresh, scoped grants, REST client, and git-over-HTTP support, backed by on-disk credential storage with secret-safe types.
  • Adds min github login/status/logout CLI subcommands that drive device-flow auth via the daemon over SSH RPC, polling with a terminal spinner until completion.
  • Registers five new SSH RPCs in minimald (GithubBeginLogin, GithubPollLogin, GithubStatus, GithubListAuths, GithubLogout) dispatched through a shared GithubService built at daemon startup.
  • Adds a git% verb to SessionChannel that proxies sandbox git operations to the GitHub facade when a SessionGithub is wired in, and returns a clear error otherwise.
  • Extends mfile to parse an optional [session.github] block with per-repo specs and scope overrides, validated at load time.
  • Adds a github-mock binary (test-support feature) that runs an in-process mock OAuth + smart-HTTP git server, announces its base URL on stdout, and is used by integration tests.
  • Changes min attach to spawn ssh as a child process and exit() with its status rather than exec()-ing into it; the RPC connection is closed before handing off the terminal and SIGINT/SIGQUIT are ignored in the parent while waiting.
  • Risk: daemon startup now fails if GITHUB_* environment overrides contain malformed URLs; absence of a client ID is non-fatal.

Macroscope summarized ec57571.

claude and others added 30 commits July 20, 2026 19:26
Define user requirements for making GitHub development easy in minimald
sessions: branch-ready activation (checkout-or-create, server-side clone
and adopt-local modes), explicit in-session push, and an opt-in
prompt-to-open-PR on exit.

Ground the design in a public GitHub App with user-to-server OAuth device
flow (real-user attribution, short-lived repo-scoped refreshable tokens)
and map each requirement to an existing extension seam in the codebase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G6K2UrYewEyXTQCHkTm53
Make explicit that the device-flow client is the min CLI (public client,
client_id only), while minimald performs no OAuth and holds no App private
key in the single-user MVP; distinguish device-flow login from installing
the App on a repo. Note that a hosted model drives the same device grant
from the hosted side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G6K2UrYewEyXTQCHkTm53
Invert the auth model: minimald is the GitHub App device-flow client and
holds the token; the token never enters the sandbox. Add a min git facade
(and same facade for GitHub MCP) that proxies scoped operations back to
minimald, so access is mediated and dies when the sandbox exits — preferred
over injecting a token or a man-in-the-middle egress proxy.

Also: support multi-repo pre-priming as a task-spec field; set default
scopes to contents/PRs/issues r/w with workflows excluded; add the
spec-declared-else-prompt scope rule, show-but-not-selectable consent, and
a reuse-or-mint prompt on subsequent sandboxes; treat local as the
anonymous user and record multiple-minimalds-per-host as the scaling axis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G6K2UrYewEyXTQCHkTm53
Add a new dependency-light `github` crate holding the pure, I/O-free
domain types shared by minimald and the min client for spec 10
(GitHub-integrated sessions). Default features carry no reqwest/HTTP so
mfile/minimal can depend on it cheaply; a `client` feature is added
later.

Contents:
- RepoSpec/BranchSpec: strict owner/repo[@Branch[:base]] parse+display,
  base-without-branch made unrepresentable.
- Scope/ScopeSet: typed permission model with the R5.1 defaults; the
  `workflows` permission is unrepresentable by construction (NG6) and a
  render-for-consent form (R5.3).
- SecretString: sole token carrier; redacts in Debug/Display and
  zeroizes on drop, with no serde derive so raw-String tokens are a
  review-rejectable pattern (R6).
- GithubConfig: client_id + oauth/api/git bases with env-override
  constructors (the mock-test seam); unset client_id fails closed with
  NotConfigured (R1.1).
- attrs codec: github.grant_id/repos/scopes through SessionConfig.attrs
  (R7.1); facade verb constants; actionable non-secret error taxonomy
  (R8.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the `exec()` process replacement at the end of the attach path
with spawn + wait, so control returns to the client after the session
shell exits — the seam client-driven post-attach flows hook into.

Observable behavior is preserved:

- The child's exit code is propagated verbatim via `std::process::exit`
  (signal death maps to the shell's 128+N convention, the same $? an
  exec()'d ssh produced).
- `-tt` PTY semantics are unchanged, and the interactive path now fails
  fast when stdin is not a TTY instead of blocking forever in `ssh -tt`
  (ports the #956 guard, which this branch's base predates).
- SIGINT/SIGQUIT are ignored in the waiting parent for exactly as long
  as the child runs — the POSIX system(3) discipline — so Ctrl-C reaches
  the foreground ssh / in-session process, and the saved dispositions
  are restored once the child exits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One JSON file per grant under an injected directory, mirroring the
minimald Store actor's on-disk pattern. Enforces 0700/0600 permissions
(re-asserted on every open, correcting any drift), atomic temp+rename
writes so a crash or error never leaves a partial grant file, and
supports multiple coexisting grants -- the substrate reuse-or-mint
(R1.3/R6.4) needs. Token fields are typed SecretString and named with
`token` in the key so the diagnostics redaction denylist masks them
for free (R6.2); GrantStore::list returns a token-free GrantSummary so
metadata can be enumerated without ever reading token material back
out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend mfile::Session with an optional github field carrying
[[session.github.repos]] entries (repo, branch, optional base) plus a
session-wide and per-repo scopes override (spec 10: R2.1, R2.2, R5.2,
R7.1). Fields store raw strings; validating accessors
(GithubRepo::repo_spec/scope_set, SessionGithub::scope_set) delegate
to the github crate's RepoSpec/ScopeSet parsing so there is exactly
one grammar, and the workflows scope is rejected there (NG6). Old
minimal.toml files without the key keep parsing unchanged (NG7), and
the github block is documented as client-consumed at activate time
rather than materialized into the project's loadout composable, which
is also why Session::is_empty ignores it.

Note: this commit could not be verified with cargo test/clippy in
this sandbox — cargo build -p mfile has been consistently
OOM-killed (signal 9) while compiling the pre-existing, unrelated
nickel-lang-parser dependency (pulled in transitively via args/common,
present before this change) across 35+ attempts under multiple
profile/flag configurations, with no successful build of that
dependency observed anywhere in the shared target directory. This
appears to be a shared-environment resource constraint rather than a
defect in this diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the spec-10 GitHub session RPC types following the existing
OneshotSshRpc pattern: GithubBeginLogin, GithubPollLogin, GithubStatus,
GithubListAuths, GithubLogout, GithubPrimeRepos, GithubPush,
GithubCreatePr, and GetSessionGitState. Repos and scopes cross the wire
as plain "owner/repo@branch:base" / "contents:write" strings so this
crate gains no new dependency; parsing lives at the edges in the github
crate. Every type is #[non_exhaustive] with per-field #[serde(default)]
for old-shape and unknown-field tolerance, and no request or response
type can carry token material.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
crates/mfile/Cargo.toml gained github.workspace = true in 2d5edd8, but
Cargo.lock was never regenerated alongside it, leaving the lockfile's
mfile dependency list out of sync with the manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Behind the `test-support` feature, add a `testing` module: a hand-rolled
tokio HTTP/1.1 mock GitHub that adds no new runtime dependency. It serves
the OAuth device flow (scriptable pending/slow_down/expired/denied plus
refresh-token rotation, strict-rotation, and invalid_grant), the REST
surface (`GET /user`, repo default branch, App-installation lookup with
not-installed -> 404 per R1.5, and pull-request list/create with
existing-PR-by-head detection per R4.5), and an auth-enforcing git
smart-HTTP endpoint backed by `git http-backend` that rejects
unauthenticated fetches and serves local bare fixture repos -- the surface
that later proves a credential helper actually fires.

Every request is captured for test assertions with the Authorization header
redacted from Debug and kept out of the header list, so token material never
leaks through the capture log. Add `src/bin/github-mock.rs`
(required-features = test-support) so an out-of-process e2e script can point
a real git/minimald at the same mock via the MINIMALD_GITHUB_*_BASE_URL
overrides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces DeviceFlowClient (start_device_flow / poll / fetch_user /
login) implementing the GitHub App device flow: POST
login/device/code, poll login/oauth/access_token honoring
authorization_pending/slow_down/expired_token per RFC 8628, then GET
/user to assemble a complete Grant via assemble_grant, ready for
GrantStore::save.

Errors map into the crate-wide Error taxonomy (Request/Decode for
transport and shape failures, NotConfigured for a client_id GitHub
does not recognize, NeedsReauth for an expired or declined
authorization) rather than introducing a parallel error type.

The device code is carried in a SecretString like an access/refresh
token, since possessing it is enough to complete the login; no public
type in this module renders token material through Debug or Display,
which mock-backed tests assert directly alongside happy-path,
slow_down backoff, pending-loop, and expiry coverage.

Adds `form` to the crate's existing optional reqwest dependency (pure
Rust, no new C/asm compile step) for the flow's URL-encoded POST
bodies, and threads `dep:tokio` into the `client` feature for the
poll loop's backoff sleep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds RestClient<T: TokenProvider>, a typed REST surface over
GithubConfig::api_base: get_user, repo_installation (mapping a 404 to
Error::AppNotInstalled with the caller-supplied install URL, spec
R1.5), repo_default_branch, list_pulls(head=owner:branch) for
existing-PR detection (spec R4.5), create_pull, and get_pull.

TokenProvider is defined here (device_flow.rs does not define one) and
comes with a StaticToken impl so the client is unit-testable without a
live grant store. Every non-2xx response maps into the shared Error
taxonomy: 401 universally becomes NeedsReauth (spec R8.1); 404 becomes
an endpoint-specific variant (AppNotInstalled, RepoNotFound,
PullNotFound); anything else becomes UnexpectedStatus carrying only
GitHub's own `message` (never the raw body). Extends error.rs with
these new variants (Request, Decode, UnexpectedStatus, RepoNotFound,
PullNotFound), additive to the existing taxonomy.

The `client` feature's reqwest dependency deliberately requests no TLS
backend feature: both workspace-available providers (`ring`,
`aws-lc-rs`) compile a C/asm crypto core via `cc`, which this build
environment cannot do (no working assembler). It rides on Cargo's
per-binary feature unification with whichever sibling crate enables a
TLS feature in the real daemon binary; the mock-backed tests here only
ever talk plain HTTP.

Mock tests (crate's in-process MockGithub, test-support feature) cover
installation present/absent with the install URL, existing-PR
detection by head, the create-pull request payload shape, and
401/404/422 error mapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a gitops module that drives git against a session's working tree on
the daemon-held token's behalf. Model the hardening on crates/checkouts
(the --no-optional-locks + core.hooksPath=/dev/null prefix) but keep the
token out of argv, the remote URL, and any on-disk git config: origin
stays the clean https URL and credentials are injected env-only through
an inline, one-shot credential.helper (installed via
GIT_CONFIG_COUNT/KEY/VALUE) whose password is read from a per-child env
var. The helper list is reset first so an inherited host helper cannot
splice in the wrong user's token. Pin protocol.allow to the remote's
scheme, set GIT_TERMINAL_PROMPT=0, and stream all child output through a
scrubber that masks the live token bytes.

Cover clone (temp-sibling + atomic rename so a failed clone leaves
nothing behind, R2.6), fetch/pull/push, checkout-or-create (create from
base with a clean error when the base is absent; no implicit push, R2.5),
current-branch, default-branch via ls-remote --symref, and
ahead-of-upstream detection. Tests exercise file:// fixtures and the
auth-enforcing smart-HTTP mock (push/fetch succeed only with the helper
wired), assert zero token bytes on disk and in the spawned argv, and
confirm the scrubber masks a token echoed by a scripted command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Declare pub mod gitops so daemon-side callers can reach the authenticated
git working-tree API added in the preceding commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formatting only (import ordering and line wrapping); no behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GrantManager is the sole access-token accessor: token_for() refreshes
near-expiry (<5 min) tokens transparently under a per-grant single-flight
mutex, persists the rotated refresh+access pair (atomic write, file and
directory fsync) before any caller sees the new token, and turns
invalid_grant into a sticky, persisted needs_reauth state with the
actionable "run 'min github login'" error (spec R1.2). Transient
network/5xx failures get a bounded retry and never invalidate the grant.
GrantTokenProvider implements rest::TokenProvider, and with_reauth_retry
gives REST callers retry-once-on-401 semantics via refresh_now's
grace-limited forced rotation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compose the store, GrantManager, REST client, gitops, and device-flow
building blocks from the `github` crate into one `GithubService`,
constructed at daemon startup from GithubConfig::from_env() with its
grant store rooted at minimal_state_dir()/github/grants, and hang it
off ServerStateHandle so every github-RPC handler and session facade
channel reaches the same single-flight GrantManager. An unconfigured
daemon (no MINIMALD_GITHUB_CLIENT_ID) still constructs the service
fine; every op is expected to fail closed via ensure_configured().

Scaffold crates/minimald/src/github/ with rpcs.rs, authz.rs, prime.rs,
push_pr.rs, and facade.rs stubs, pre-registered in mod.rs, so the five
downstream daemon tasks land in disjoint files. Record the R8.1
span-name conventions (github.auth/refresh/prime/facade/push/pr;
repo/branch/grant_id fields only, never tokens) as doc comments on
GithubService.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the daemon-authz seam: typed read/write of the github.*
attrs on SessionConfig.attrs/Record.attrs via the github crate's
GithubAttrs codec, and authorize(&Record, repo, perm) -> Result<GrantId,
AuthzError> as the single choke point every authenticated GitHub
operation must call. It denies fail-closed when no grant is bound, the
target repo is not in the session's declared allow-list, or the
required scope/level is not in the resolved scope set (R5.4), and
documents the "caller must hold a live-session handle" contract so a
destroyed session's id authorizes nothing.

Exhaustive allow/deny matrix tests cover the happy path, unbound
session, undeclared repo, missing scope per permission, write
satisfying a read requirement, malformed attrs, and that deny messages
carry no token-shaped material.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the spec-10 R3 mediated-git facade to the session command channel:
a `git%<argv>` arm on the SessionChannel dispatch that parses a
fail-closed argv allowlist (push/pull/fetch/status/remote -v, clone of
declared repos only; every option, path, and unknown subcommand is
rejected — the gate is a security boundary with dedicated negative
tests), re-reads the live session record, authorizes each operation
through the github::authz choke point, obtains the token from the
shared GrantManager, and runs github::gitops in the session's working
directory, streaming scrubbed output back as msg: lines.

Origin URLs are verified against the daemon-derived remote before any
authenticated operation, as defense-in-depth against sandbox-side
.git/config rewrites (residual TOCTOU documented; the race-free fix
belongs in gitops' runtime config injection). Sessions without GitHub
attrs — and channels with no facade wired, which is every session
until the activation wiring lands — are refused with "this session
has no GitHub authentication". Facade access dies with the sandbox:
the channel actor is aborted on Env drop and a destroyed session's
record read fails, so nothing authorizes after destroy (R3.5/R6.3).

Existing channel verbs are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The origin check only compared the first `remote.origin.url` line, so a
sandbox could keep that url byte-identical while adding a `pushurl`, a
second `url`, or a `url.<x>.insteadOf`/`pushInsteadOf` rewrite in the same
sandbox-writable `.git/config` and steer a token-bearing `git push|fetch|
pull` at an attacker host, leaking the grant's scoped token (spec G5/R6.1).

Replace the first-line match with a whole-config scan (parse_origin_config)
that fails closed unless there is exactly one canonical `origin` url and no
`pushurl`, extra `url`, or `insteadOf`/`pushInsteadOf` rewrite anywhere.
Header parsing tolerates whitespace and the legacy `[remote.origin]` form so
a noncanonical-but-valid header cannot smuggle a redirect past the scan.
Also refuse a dash-leading current branch before `git push` could parse it
as an option (argv-injection hardening; the branch name is not secret).

Add facade unit tests for each redirect vector and channel regression tests
that plant a pushurl/extra-url/insteadOf in a primed clone and assert the
push is refused before it can reach the attacker remote.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Eae46AffqM8TQ2v4bDxDDk
The first cut of the git% facade ran the credentialed push/fetch/pull
inside the sandbox-writable working tree and tried to vet the tree's
.git/config origin by hand-parsing it. That is unfixable by parsing:
git honours [include]/[includeIf] (arbitrary attacker-authored config
pulled in only at runtime), credential.helper, http.<url>.extraHeader,
url.<base>.insteadOf/pushInsteadOf and remote.origin.url/pushurl, any of
which redirects the token-bearing connection or splices the token into a
header — and a check-then-use scan races the sandbox rewriting the file.

Close the hole by architecture, not inspection: the token-bearing git
never runs in, and never reads the config of, the sandbox working tree.
Every push/pull/fetch is split into a token-free local leg and a
credentialed network leg run against a daemon-authored clean bare mirror
under the session's own state dir (a sibling of the sandbox-mounted
workspace, reachable by nothing inside the sandbox):

- push: transfer the branch worktree -> mirror over a local,
  network-incapable leg (protocol pinned to file, global/system config
  disabled, no token), then push mirror -> the daemon-derived canonical
  URL via github::gitops, which injects the token env-only and reads only
  the mirror's daemon-authored config.
- fetch/pull: gitops fetches canonical -> mirror with the token, then the
  worktree is updated from the mirror over the local, token-free leg.

The canonical URL always comes from the declared repo (authz), never
from sandbox input; the worktree's own origin/insteadOf/include is never
consulted on the token leg. Delete the .git/config-verification code
(verify_origin/parse_origin_config/…) and its now-moot OriginMismatch.

The fail-closed argv allowlist and its negative tests are unchanged.
Add dedicated exfil tests: a worktree config planting [include],
[includeIf], credential.helper, http.extraHeader, url.insteadOf,
pushInsteadOf, a redirected remote.origin.url and a pushurl each fails to
send the token anywhere but the canonical remote (asserted against sink
remotes + token-on-disk scans), plus fetch/pull channel coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The clone arm exempted itself from the mirror discipline: after the
clone finalized into the sandbox-writable workspace, the declared-branch
checkout ran token-bearing ls-remote/fetch with cwd inside the fresh
tree, resolving `origin` from its .git/config. Once the tree lands in
the workspace that config is sandbox-writable, so a racing sandbox
process could rewrite remote.origin.url (or plant insteadOf/includeIf/
extraHeader directives) in the window between the clone's rename and
the checkout's first network op and redirect the token leg to an
attacker host — the same TOCTOU class the mirror redesign closed for
push/fetch/pull.

Clone now runs the full discipline: the credentialed leg fetches
canonical -> mirror (and resolves the default branch via ls-remote run
in the mirror, reading only daemon-authored config); the worktree is
then built from the mirror over token-free, network-incapable local
legs inside a temp directory under the daemon-private mirror root and
renamed into the workspace only once complete. No git process of the
operation — token-bearing or not — ever touches the tree after it
becomes sandbox-reachable, and a pre-existing destination is refused
outright. Branch names that would reach git as bare arguments are
refused when dash-leading (argv injection, defense in depth).

Also documents the launcher-owed half of the invariant: activation
wiring must never bind the workspace's host parent (which holds
.gh-mirror) into the sandbox namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The credentialed gitops leg pinned the transport protocol and reset any
inherited credential helper, but still let a spawned git read the
operator's global/system config — so the "reads only daemon-authored
config" invariant held only because the mirror directory and the host
environment were clean, not by construction. An inherited
url.<base>.insteadOf or url-scoped helper in ~/.gitconfig could still
influence where a token-bearing child connects.

Pin GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM to /dev/null on every
child, matching the token-free local legs in the minimald facade: a git
spawned here now reads only the repo-local config of the
daemon-controlled directory it runs in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a `min github` command group modeled on the existing `Session`
subcommand: `login` starts the device flow, shows the verification
URL/code, and polls GithubPollLogin with a Ctrl-C-able spinner until
approval or expiry (R1.1); `status` renders identity, token
validity/expiry, per-repo App-installation state with guidance URLs,
and the per-session repo/branch/scope table (R1.4/R1.5/R8.2); `logout`
forgets a stored grant behind confirm(). All three are thin
oneshot_rpc callers into the already-committed GithubBeginLogin/
GithubPollLogin/GithubStatus/GithubListAuths/GithubLogout wire types —
minimald remains the only thing that ever holds a token.

All logic lives in the new crates/minimal/src/github.rs; lib.rs only
gains the `Command::Github` variant and its dispatch arm, keeping the
shared-file footprint minimal. minimal now depends on `github` (default
features: pure domain types, no reqwest) to validate --repo via
RepoSpec and render session scopes via ScopeSet::render_for_consent,
and on `indicatif` (already linked in via `ot`) for the login spinner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the GithubBeginLogin/GithubPollLogin/GithubStatus/
GithubListAuths/GithubLogout oneshot RPC handlers (spec R1.1-R1.4, R8.2).

- crates/minimald/src/github/rpcs.rs: begin_login starts the device flow
  and parks the poll/fetch-user/persist sequence in a background task
  keyed by an opaque login_id, so poll_login never blocks on GitHub and
  can answer Pending promptly on every call. status resolves the active
  grant (a requested session's bound grant, or the sole stored grant),
  reports identity/token validity via a live token_for + GET /user check,
  per-repo App-installation state with install-guidance URLs, and the
  session's repo/branch/scope table decoded from Record attrs.
  list_auths returns grant metadata only (no token can reach it, by
  construction of GrantSummary). logout deletes the local grant
  fail-closed on an empty id; no revoke endpoint exists yet in the
  github crate's REST client, so that half is left as a documented
  follow-up. Every handler runs under a github.auth tracing span.
- crates/minimald/src/rpc.rs: adds the five RPCs to the allowlist and
  dispatch match, and establishes a contiguous "GitHub" block that later
  prime/push/PR/facade RPCs append siblings to; the channel-framing
  serve_github_* wrappers stay here per this file's existing convention,
  delegating decision logic to crate::github::rpcs.

Adds a hand-rolled in-process mock GitHub HTTP responder in rpcs.rs's
own test module (github::testing::MockGithub is gated behind a feature
minimald does not currently enable) to exercise a real login -> poll ->
status -> list-auths -> logout round trip over the actual RPC framing,
including a not-installed-repo/install-URL assertion and a grep-clean
check that no response ever serializes token bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Harden the credentialed gitops leg against two residual vectors found in
adversarial review:

* Set GIT_CONFIG_NOSYSTEM=1 alongside the existing
  GIT_CONFIG_GLOBAL/SYSTEM=/dev/null redirects, so the system config is
  denied to every daemon-run git child by construction even on builds that
  ignore the path override.
* Refuse a leading-dash branch/base name (reject_option_like) and separate
  it from the options with `--` in push/checkout/ls-remote argv, closing an
  option-injection where a branch from `rev-parse`/remote output
  (e.g. `git push --set-upstream origin <branch>`) could be parsed as a git
  flag.

Adds negatives asserting the guard fires before any git spawns and that
GIT_CONFIG_NOSYSTEM is pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rework the git% facade to establish the invariant the adversarial gate
required: no privileged (daemon-run) git process may read sandbox-writable
git config. The prior "token-free local leg" still ran git in — or made a
daemon git open upload-pack against — the session worktree, where a hostile
repo-local .git/config (core.fsmonitor, core.sshCommand, filter.*.process,
hooks, [include]/[includeIf]) executes arbitrary code as the daemon, which
then reads the token from the mirror. A config-text scanner cannot fix this
(git honours includes at file precedence).

Now the daemon runs git only in the daemon-authored bare mirror
(GIT_CONFIG_GLOBAL=/dev/null, GIT_CONFIG_NOSYSTEM=1, canonical URL explicit,
token env-only). Object/ref movement between the worktree and the mirror
never exposes worktree config to a daemon git:

* Push gives the mirror read-only object access to the worktree via
  objects/info/alternates (objects are inert/content-addressed) and writes
  the branch tip as a plain mirror ref, then pushes mirror -> canonical.
* Fetch packs the canonical heads in the mirror and copies the .pack/.idx
  into the worktree object store as plain files, writing
  refs/remotes/origin/* as plain files.
* Pull fetches, verifies a fast-forward in the mirror (merge-base
  --is-ancestor over the alternate), then advances the branch/tracking refs
  as plain files.
* Status/branch/tip come from parsing HEAD/refs/packed-refs as plain files;
  ahead/behind from a mirror-side rev-list over the alternate.

Every OID read from a worktree ref is validated as a hex sha and every
branch name is check_safe_branch-validated before it becomes a path
component or git argument. Adds exfil PoC tests planting core.fsmonitor,
core.sshCommand, filter.*.process and an [include] that sets one of those:
each fails to execute daemon code (a sink script never runs) and the push
still reaches only the canonical remote with no token on disk. Clone keeps
its daemon-private-temp discipline unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…b-prd-r6ew53

# Conflicts:
#	Cargo.lock
#	crates/minimal/Cargo.toml
#	crates/minimal/src/lib.rs
#	crates/minimald-rpc/src/lib.rs
#	crates/minimald/Cargo.toml
#	crates/minimald/src/env.rs
#	crates/minimald/src/rpc.rs
The main-side cmd_default tail-called attach_to_session expecting
Result<()>, but the spec-10 attach rework returns ExitStatus; consume
it via std::process::exit(attach_exit_code(..)) like cmd_attach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions

The initial merge resolution unioned two sections that shared a factored-out
trailing brace run, leaving an unbalanced delimiter that broke parsing (and
thus the whole workspace build). Redo it as a proper 3-way merge so both the
spec-10 GitHub RPC types and main's DiagBundle section are kept intact, and
apply rustfmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@norrietaylor
norrietaylor marked this pull request as ready for review July 28, 2026 16:39
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cc1b230-2cea-45ac-8658-ae3433bd10bc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@norrietaylor norrietaylor reopened this Jul 28, 2026
@norrietaylor
norrietaylor marked this pull request as draft July 28, 2026 16:50
@norrietaylor norrietaylor changed the title feat(minimald): GitHub-integrated sessions (spec 10) [POC] (do not review )feat(minimald): GitHub-integrated sessions (spec 10) Jul 28, 2026
norrietaylor and others added 6 commits July 28, 2026 18:07
…macro

The merge routed the spec-10 GitHub handlers through main's serve! macro,
which wraps futures in served() requiring Result<(), ConnectionError>; the
handlers returned () (they predate the macro). Return the handle_channel
result directly so served() reports the outcome, matching every other handler
(fixes E0271 across build/clippy/tests/artifacts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ody assign

main made Cli::command an Option<Command>; the spec-10 CLI parse tests still
matched Command::Github(..) directly (6x E0308, breaking clippy+tests lib-test
builds). Wrap in Some(..). Also drop a never-read want_body assignment and its
now-needless mut in the mock HTTP reader (unused_assignments/unused_mut).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…b-prd-r6ew53

# Conflicts:
#	crates/minimal/src/lib.rs
#	crates/minimald/src/env.rs
`assert_no_token_on_disk` is only called from the `#[cfg(feature =
"test-support")] mod mock` tests, so a default-feature build of the test
target sees it as dead code and `clippy -D warnings` rejects it. Gate the
helper on the same feature as its only callers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch adds `crates/github`, taking the workspace to 30 crates; the
map still said 29 and omitted the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

3 participants