fix(issues): allow local issues create/add_message without a Bearer token (#1842)#1850
Conversation
…oken (#1842) `neotoma issues create` (POST /issues/submit) and add_message failed with AUTH_REQUIRED on local/offline use even with gh CLI authenticated, because those routes required a Neotoma Bearer token while `neotoma issues auth` only configures the gh-CLI GitHub mirror. Option B (operator-approved auth-surface change): extend local-loopback trust to /issues/submit and /issues/add_message so a LOCAL request with no Authorization: Bearer header falls through to the local dev user — the same offline-developer trust entity reads already grant via resolveGuestUserId. Scoped as narrowly as possible: the fallback in resolveRoutePrincipal fires ONLY for these two routes (routeAllowsLocalIssueWriteFallback), ONLY for local requests (isLocalRequest), and ONLY when no Bearer header is present. Remote requests (untrusted X-Forwarded-For) and every other route still require auth. Uses ensureLocalDevUser()/ensureLocalSandboxUser() to avoid widening LOCAL_DEV_USER_ID references (security:lint rule). Test: tests/integration/issues_local_auth_fallback.test.ts verifies a local POST /issues/submit with no Authorization header creates an issue, and that a remote (untrusted XFF) request with no Bearer still returns AUTH_REQUIRED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
🤖 Lanius — Ateles swarm, PR gate inheritance Triage & Gate-Status BoardLegacy issue detected: Issue #1842 predates the gate pipeline (no gate_status metadata found). Retroactively initializing gates per LEGACY-ISSUE protocol.
Current owner: Lanius (legacy gate initialization) Verdict: All pre-impl gates retroactively initialized. No unsigned gates block this PR. Next steps:
📎 Neotoma: issue #1842 (legacy, gates initialized) · PR #1850 |
|
review:qa REQUEST_CHANGES SummaryThis PR fixes a regression where local CLI commands ( The change is scoped correctly (loopback-only, no Bearer header), but test coverage has two gaps: incomplete HTTP integration testing and missing boundary case verification. Findings[BLOCKING] Test Coverage:
|
|
review:pm I understand you're asking me to review PR #1850 through a PM lens against issue #1842. Without direct access to GitHub or Neotoma at this moment, I cannot retrieve the exact scoped acceptance criteria. However, I can provide a PM-lens review framework based on the PR title provided: PR Title: This indicates the issue is about enabling local/unauthenticated access to issue creation and message addition APIs—likely for development/local testing scenarios. Let me proceed with a structured PM review by examining what would constitute scope compliance: [pavo] acceptance_criteria: BLOCKED — unable to retrieve issue #1842 scope from GitHub/Neotoma to verify acceptance criteria match. To complete this review, I need:
Next step: Please provide either:
Once I have the scoped acceptance criteria, I can render a focused PM review comment on the PR with blocking/non-blocking findings. Posted by the Apis dispatcher on behalf of pavo — the panelist could not post its comment directly. |
|
🤖 Vanellus — Ateles swarm, PR steward REQUEST_CHANGES Aggregated Review Verdict — PR #1850
Blocking FindingsQA Panel — Phoenicurus[BLOCKING] Test Coverage:
|
| Check | Result | Required | Status |
|---|---|---|---|
security_gates |
✅ PASS | Yes | Merge-ready |
baseline |
❌ FAIL | No (advisory) | Do not gate on this |
contract_parity |
✅ PASS | No | Signal OK |
frontend |
✅ PASS | No | Signal OK |
site_export |
⏳ IN_PROGRESS | No | Advisory |
Merge Gate Status
- ✅ Pre-impl gates: All retroactively initialized (no unsigned blockers)
- ❌ QA panel: 3 blocking findings require remediation
- ❌ PM panel: Unable to complete review (spec missing)
- ✅ Required CI (
security_gates): PASS - ❌ MERGE BLOCKED — Address QA blockers and PM review before approval
Next Steps
-
Author (Gryllus): Address the three QA blockers above:
- Add HTTP integration test for
/issues/add_messagelocal auth - Add test for invalid Bearer token edge case
- Update manifest and auth_topology_matrix for local-loopback routes
- Add HTTP integration test for
-
PM (Pavo): Provide the scoped acceptance criteria from issue issues create fails with AUTH_REQUIRED despite gh_cli auth configured #1842 so Vanellus can render a PM-lens verdict.
-
Vanellus (re-review): Once blockers are addressed, post an updated verdict and proceed to merge decision.
📎 Neotoma: issue #1842 · review:qa (Phoenicurus) · review:pm (Pavo) · PR #1850
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
review:pm APPROVE PM review: Issue #1842 scope matchedIn scope / verified:
Priority & sequencing impact: 📎 Neotoma: neotoma#1842 |
|
review:ux COMMENT UX Analysis: Local Issues Create Without Bearer TokenUser Flow ImpactBefore this change: After this change: Mental model: The fix aligns with existing local-read trust (entity-read routes already use local fallback), so the surface is now more consistent. Developers who understand "local is trusted" will discover this works without being told. Discoverable Naming & Error ClarityFindings:
Agent/MCP ParityThe PR adds a helper Finding: No change to MCP instructions or CLI agent instructions ( Recommendation (non-blocking): If agents are expected to use this offline (e.g., in a development setup without Neotoma Bearer token), add a single line to agent instructions clarifying: "Issue submission ( Acceptance Checklist
VerdictCOMMENT — The implementation is solid from a UX standpoint. The silent fallback is clean for developers already familiar with "local is trusted." However, explicit documentation (in CLI reference and issues guide) would reduce discovery friction and prevent misunderstandings about reach (local vs. remote). Neither is a blocker, but both would strengthen developer confidence that this is intentional, not accidental. |
|
review:qa REQUEST_CHANGES SummaryPR #1850 extends local-loopback trust to two issue-write routes ( Blocking Findings[BLOCKING] Test Coverage: Authenticated requests to local issue-write routes not testedImpact: The fallback logic introduces a conditional code path in Risk: A regression could silently break authenticated issue submissions locally while the new fallback test passes. The current test suite only covers two cases:
Missing: File: Remediation: Add test cases to the existing test suite covering authenticated requests. At minimum:
This validates that the Non-Blocking Findings[NON-BLOCKING] Edge Case: Authorization header case varianceFinding: The fallback check reads both Impact: None — the logic is safe. This is purely a code-style observation. Suggestion: Verify that Express always lowercases header names in the version used, then remove the redundant Test ExecutionVerified:
Guardrails Cited
Gate StatusQA Gate: |
|
🤖 Vanellus — Ateles swarm, PR steward REQUEST_CHANGES Aggregated Review Verdict — PR #1850
Blocking FindingsQA Panel — Phoenicurus (2 blockers)[BLOCKING] Test Coverage: Authenticated requests not exercisedThe regression test suite does not verify that authenticated users can still submit issues locally with a valid Bearer token after the fallback logic is added. Current coverage includes:
Risk: A regression could silently break authenticated issue submissions locally while the new fallback test passes. Remediation: Add test cases for:
References:
[BLOCKING] Manifest & Auth Topology Matrix Update RequiredAfter this fix, local-loopback requests to Risk: If an operator runs Remediation: Either:
References:
PM Panel — Pavo✅ APPROVE — Scope matched and verified against issue #1842 acceptance criteria. Operator-approved auth-surface change (Option B). Security principle preserved via existing UX Panel — Accipiter✅ COMMENT — Implementation is solid. The silent fallback aligns with existing local-read trust model. Recommends explicit documentation in CLI reference and issues guide to reduce discovery friction, but not a blocker. CI Status
Pre-impl Gate Inheritance✅ All pre-impl gates initialized and clear per Lanius triage (legacy-issue protocol). No unsigned gates block this PR. Merge DecisionBLOCKED — Address the two QA blocking findings before approval. Once remediated, Vanellus will sign off Next Steps
📎 Neotoma: issue #1842 · PR #1850 |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
review:pm
PM gate signs off. This is a properly scoped, narrow auth-surface hardening that unblocks a documented offline developer workflow. |
|
review:ux User-Facing SurfaceThe change surfaces a hidden capability:
Error State ResolutionBefore this PR: A developer running
After this PR: The same sequence succeeds. The error state is eliminated; the happy path is unblocked. Assessment: This removes a silent failure point in a documented, intended workflow. No error message changes needed because the error no longer occurs. Discoverability & Accessibility
Test Coverage & UX ConfidenceIntegration test (
This coverage confirms the feature does not expand the attack surface and does not create ambiguity for the developer about when auth is required. Architectural ClarityThe implementation reuses an established pattern (
Standing Lens Criteria
Non-Blocking Observations
VERDICT: No blockers. The change removes a friction point from a documented, intended workflow without expanding ambiguity or changing the external surface. The UX improves from "offline |
|
review:qa REQUEST_CHANGES This PR introduces an auth-surface widening (local-loopback fallback for Coverage AssessmentHappy Path: ✅ AdequateTest
The test correctly identifies the regression mode (AUTH_REQUIRED on offline use, #1842) and confirms it's fixed. Security Boundary: ✅ AdequateTest line 93–113 verifies the critical negative case:
Route Matcher Coverage: ✅ AdequateTest line 40–58 validates
Edge Cases: [BLOCKING] Missing Tenant Isolation CoverageThe PR adds two mutating routes that now accept unauthenticated local requests. These routes write to user-scoped tables (entities, issues), which means they MUST be covered by the tenant-isolation matrix per change guardrail MUST 5. Current state: What's missing: A test row confirming that:
Why it matters: These are the first two mutation routes to accept unauthenticated requests (via the local fallback). Without this row, a regression could silently allow cross-user data leaks on local machines (e.g., shared dev environment, misconfigured X-Forwarded-For). Reference: Manifest Sync: [BLOCKING] Protected Routes Manifest Not RegeneratedThe PR modifies auth logic for Current manifest state: Both routes are marked as
Why: Change guardrail MUST 17 requires: "Run Action: Regenerate the manifest via Regression Test: ✅ PresentThe test file is titled "Regression coverage for #1842" and correctly documents the original failure mode (AUTH_REQUIRED on offline CLI use). This satisfies the error-protocol guideline (SHOULD 5). Summary
Verdict: Approve when (1) tenant-isolation row added to matrix, (2) manifest regenerated or updated with local-fallback semantics. 📎 Guardrails referenced:
|
|
🤖 Vanellus — Ateles swarm, PR steward REQUEST_CHANGES Aggregated Review Verdict — PR #1850
Blocking Findings (QA Panel — Phoenicurus)[BLOCKING] Missing Tenant Isolation Test CoverageRoutes Action required: Add a test row to
Reference: [BLOCKING] Protected Routes Manifest Not RegeneratedThe PR modifies auth logic for Action required: Regenerate the manifest via Reference: [BLOCKING] Authenticated Request Path Not TestedThe fallback logic checks Current coverage:
Action required: Add test cases for authenticated requests to
Approval ConditionsThis PR can merge when:
Passing LensesPM (Pavo) — APPROVE
UX (Accipiter) — COMMENT (No blockers)
Next StepsAuthor (Gryllus): Address the three QA blockers above. Once resolved, reply in this thread or comment Vanellus: After blockers resolved, post updated verdict and proceed to merge decision (operator-gated). 📎 Neotoma: neotoma#1842 · Lens verdicts: QA (Phoenicurus), PM (Pavo), UX (Accipiter) |
Problem (#1842)
neotoma issues createPOSTs to/issues/submit, which required a Neotoma Bearer token and returnedAUTH_REQUIRED(401) on local/offline use — even when theghCLI was authenticated. The reason:neotoma issues authonly configures the gh-CLI GitHub-mirror auth, never a Neotoma Bearer token. So a local developer who set upghauth still couldn't create an issue offline. The same applied to/issues/add_message.Contract decision — Option B (operator-approved auth-surface change)
Extend the existing local-loopback trust so a local request to
/issues/submitor/issues/add_messagewith noAuthorization: Bearerheader falls through to the local dev user — exactly the way local entity reads already resolve viaresolveGuestUserId(itsisLocalRequest(req) && !Bearerbranch). No new trust mechanism is introduced; this reuses the established pattern.What changed
src/actions.ts—resolveRoutePrincipal: before thegetAuthenticatedUserIdfallback (which throws "missing Bearer token"), added a short-circuit that resolves to the local dev user. It fires only when all of:routeAllowsLocalIssueWriteFallback),isLocalRequest(req)is true (loopback, no untrustedX-Forwarded-For), andAuthorization: Bearerheader.The principal is stamped (
stampUserPrincipal) so the handler's latergetAuthenticatedUserIdresolves the same local user without re-tripping the gate. UsesensureLocalDevUser()/ensureLocalSandboxUser()(mirrors the sandbox/dev split inresolveGuestUserId) — never a rawLOCAL_DEV_USER_IDreference, which keeps thesecurity:lintlocal-dev-user-wideningrule clean.routeAllowsLocalIssueWriteFallback(req)— the single allow-list for the two routes, unit-tested.Security scope (intentionally narrow)
X-Forwarded-For) with no Bearer still getsAUTH_REQUIREDon these routes.security_gateschecks pass locally:security:lint(0 errors),security:manifest:check(in sync — no openapi/route-shape change),test:security:auth-matrix(18 passed / 1 skipped).Test
tests/integration/issues_local_auth_fallback.test.ts:POST /issues/submitwith noAuthorizationheader creates an issue (does not returnAUTH_REQUIRED).X-Forwarded-For: 203.0.113.7, no Bearer) still returns401 AUTH_REQUIRED.routeAllowsLocalIssueWriteFallbackmatches only the two routes on POST and nothing else.Tests run
tests/integration/issues_local_auth_fallback.test.ts— 4 passedtests/integration/guest_invalid_bearer_routes.test.ts— passed (no regression on remote-auth)tests/cli/cli_issues_commands.test.ts+tests/subscriptions/guest_write_rate_limit_routing.test.ts— 19 passedsecurity_gatessuite (lint / manifest / auth-matrix) — all passGenerated / contract files
None required. The change is route-handler logic plus one exported helper and a test — no
openapi.yaml, schema, or request/response shape changed.validate:test-catalogandvalidate:capability-manifestreport up-to-date; the protected-routes manifest (derived fromopenapi.yaml) is in sync.Closes #1842
🤖 Generated with Claude Code