Truthful tier detection: real headless/subagent/auto signals - #15
Merged
Conversation
… permission_mode proxy The permission_mode heuristic misreported every case that matters (gatewaystack-connect#692, docs/tier-semantics-auto-vs-interactive.md): headless/cron `claude -p` runs claimed "interactive" — the loosest tier for the most unattended shape — Task-spawned subagents were undetectable (they inherit permission_mode), and auto mode, a human at the terminal, claimed "subagent". Detection now reads real signals, most-restrictive-wins: - bypassPermissions -> background (unchanged) - headless/programmatic entrypoints (CLAUDE_CODE_ENTRYPOINT sdk-cli / sdk-ts / sdk-py / mcp / claude-code-github-action) or CI env -> background - Task-spawned subagent (agent_id on the hook input) -> subagent, unless the session itself is unattended - everything else, including auto mode -> interactive Detection failure never bricks a call: fall back to "interactive" loudly — lapse.log line plus a detectError flag in the new tier_signals body field the gateway records for audit. The gateway now flags client-reported vs enforced tier divergence with a non-blocking notice; relay it once per session as a systemMessage (deduped like the uncredentialed-session banner). retry-before-fail-closed.test.mjs induced "unattended" via the old auto->subagent mapping and spread the runner's env (CI=true would flip every case to background on Actions); it now sets entrypoints explicitly. Consequence worth naming: headless runs now fail CLOSED on gateway outages (the #385 posture for unattended tiers) where they previously failed open as fake-interactive sessions. Refs gatewaystack-connect#692
Collaborator
Author
|
Fail-closed-on-outage posture for background-labeled sessions confirmed by David 2026-08-16 ("fail closed is the right way to go" — uptime record on GCP acceptable risk). This is a deliberate refinement of the July fail-open-default policy: attended tiers keep fail-open+loud; unattended fails closed and must leave an audited lapse/coverage-gap record, never silent zero rows. Safe to merge on this point. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client half of gatewaystack-connect#692 (see
gatewaystack-connect/docs/tier-semantics-auto-vs-interactive.md, §4 and rec 1). Server half: davidcrowe/gatewaystack-connect PR forfix/truthful-tier-labels.What was wrong
resolveAgentTier()guessed the tier frompermission_modeand misreported every case that matters:cli/ IDE, no unattended signalclaude -p/ cronCLAUDE_CODE_ENTRYPOINT=sdk-climcp serve/ GitHub Actionsdk-ts/sdk-py/mcp/claude-code-github-action, orCI=trueagent_idon the hook inputdetectErrorintier_signals— never brick a client on detection uncertaintyAlso in this PR
tier_signals(entrypoint, agentId, ci, detectError) ride along on both hook bodies so the gateway can audit what drove the claim.systemMessage, deduped once per session like the uncredentialed banner. Allow-path exits funnel through one writer so the hook still emits exactly one stdout JSON object.retry-before-fail-closed.test.mjsinduced "unattended" via the oldauto→subagentmapping and spread the runner's env (CI=trueon Actions would have flipped every interactive case to background); it now sets entrypoints explicitly.Behavior change worth reviewing
Headless/CI runs now carry an unattended tier, so per #385 they fail closed on gateway outages where they previously failed open as fake-interactive sessions. That is the designed posture finally reaching the sessions it was designed for — but it is a real change for anyone running
claude -punder a wildcard key.Tests
node --test: 61 pass (12 new intest/tier-detection.test.mjs— wire-level, spawning the real hook against a capture server: headless detection, subagent detection, auto-mode regression, CI env, signals payload, notice relay + dedupe, single-JSON-object discipline, deny passthrough).