Skip to content

fix: bounded IPv4-first connect, observable refresh, honest scope model - #15

Merged
neilgfoster merged 2 commits into
mainfrom
008-runtime-and-scope
Jun 23, 2026
Merged

fix: bounded IPv4-first connect, observable refresh, honest scope model#15
neilgfoster merged 2 commits into
mainfrom
008-runtime-and-scope

Conversation

@neilgfoster

Copy link
Copy Markdown
Owner

Resolves docs/HANDOVER-runtime-and-scope.md (Parts A & B), minus the explicitly-deferred Issue 4 (two-phase agent sign-in — LOW severity, partly an environment constraint; can be filed separately if still needed after the connect-path fix).

Issue 1/2 — dual-stack connect hang (HIGH)

Every request flowed through a bare urllib.request.urlopen. On a host where DNS returns both A and AAAA records but the IPv6 route is blackholed, Python tried the dead IPv6 address with the full operation timeout and hung indefinitely — the device code never printed, reads never returned, and the silent refresh hung too (the likely root cause of the "re-auth every session" pain).

runtime._http now connects through a bounded, IPv4-first path (happy-eyeballs-lite over stdlib socket/http.client): addresses are resolved ourselves, ordered IPv4-first, and each is tried with a short connect timeout (MSGRAPH_CONNECT_TIMEOUT, default 5s) so a dead address fails fast to a reachable one. MSGRAPH_FORCE_IPV4=1 restricts to IPv4. The 30s read timeout and the single _http seam are unchanged. A successful silent refresh now prints msgraph: renewed access token silently to stderr so renewal is observable.

Issue 3 — scope honesty (MEDIUM)

Microsoft (AAD) consent is sticky/cumulative, so a --mode read sign-in on an account that previously consented to a write tier returns a write-capable token. auth-login now warns on stderr when the granted scopes are a write-capable superset of the requested mode (runtime._extra_write_scopes), so the token's true capability is never hidden.

Part B — honest scope model (ADR)

docs/adr/0001-scope-isolation-one-app-vs-per-tier.md records the decision to keep one app registration and frame --mode as consent-shaping ergonomics + a _require_scopes guardrail (rejecting per-tier app registrations as overkill for a personal-mailbox tool, kept on record if strict isolation ever becomes load-bearing). The auth-login skill doc and README are reconciled: the unqualified "structurally cannot write" claim is replaced with the true, qualified guarantee — structural read-only holds only before any write mode has ever been consented.

Tests

  • Connect ordering (IPv4 promoted first) + bounded fallback via a fake socket — does not mock the _http seam (that seam was the blind spot that hid the hang).
  • Post-expiry silent refresh issues the refresh grant and no devicecode request.
  • Scope-diff helper (read→read empty; read→read+writes flags both; rules→rules empty).
  • Full suite green (85 passed); ruff clean; stdlib-only guard green.

Developed via the repo's local SDD flow (specs/008-runtime-and-scope, gitignored). Removes docs/HANDOVER-runtime-and-scope.md.

🤖 Generated with Claude Code

neilgfoster and others added 2 commits June 23, 2026 22:26
Resolves docs/HANDOVER-runtime-and-scope.md (Parts A & B), minus the
explicitly-deferred Issue 4 (two-phase agent sign-in).

Issue 1/2 — dual-stack connect hang. Every request went through a bare
urllib.request.urlopen, which on a host with a blackholed IPv6 route
tried the dead address with the full operation timeout and hung — no
device code, reads never return, and the silent refresh hung too (the
likely cause of "re-auth every session"). runtime._http now connects via
a bounded, IPv4-first path (happy-eyeballs-lite over socket/http.client):
each address tried with a short connect timeout (MSGRAPH_CONNECT_TIMEOUT,
default 5s) so a dead address fails fast to a reachable one.
MSGRAPH_FORCE_IPV4=1 restricts to IPv4. Stdlib only; the 30s read timeout
and the single _http seam are unchanged. A successful silent refresh now
prints a stderr note so renewal is observable.

Issue 3 — scope honesty. AAD consent is sticky/cumulative, so a read-mode
sign-in on a write-consented account returns a write-capable token.
auth-login now warns on stderr when granted scopes are a write-capable
superset of the requested mode (runtime._extra_write_scopes).

Part B — ADR-0001 records the decision to keep one app registration and
frame --mode as consent-shaping + guardrail (rejecting per-tier apps).
auth-login SKILL.md and README reconciled: the unqualified "structurally
cannot write" claim is replaced with the true, qualified guarantee.

Tests: connect ordering + bounded fallback via a fake socket (NOT mocking
the _http seam — that seam was the blind spot that hid the hang), a
post-expiry refresh that issues no devicecode, and the scope-diff helper.
85 passed; ruff clean.

Spec: specs/008-runtime-and-scope (gitignored, local SDD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@neilgfoster
neilgfoster force-pushed the 008-runtime-and-scope branch from bd14c78 to 222b503 Compare June 23, 2026 21:27
@neilgfoster
neilgfoster merged commit 95fa886 into main Jun 23, 2026
1 check passed
@neilgfoster
neilgfoster deleted the 008-runtime-and-scope branch June 23, 2026 21:31
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