Skip to content

Add missing Phase 2a gateway endpoints - #19

Merged
jwbron merged 5 commits into
mainfrom
jib/jib-20260202-163848-2620363/work
Feb 3, 2026
Merged

Add missing Phase 2a gateway endpoints#19
jwbron merged 5 commits into
mainfrom
jib/jib-20260202-163848-2620363/work

Conversation

@james-in-a-box

@james-in-a-box james-in-a-box Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements all 6 missing REST API endpoints from issue #14, completing Phase 2a of the gateway extraction.

New endpoints:

  • POST /api/v1/gh/pr/create - Create PR with policy checks (blocked in user mode)
  • POST /api/v1/gh/pr/comment - Comment on any PR
  • POST /api/v1/gh/pr/edit - Edit PR title/body (ownership verified)
  • POST /api/v1/gh/pr/close - Close PR (ownership verified)
  • POST /api/v1/sessions/<token>/heartbeat - Extend session TTL with rate limiting
  • GET /api/v1/repos/visibility - Query visibility for multiple repos

Supporting changes:

  • Add check_pr_create_allowed() to PolicyEngine for user mode blocking
  • Add get_session_by_ip() to SessionManager (required for Phase 2b Anthropic API proxy)
  • Wire up check_heartbeat_rate_limit() for heartbeat endpoint
  • Update docs/api.md with complete endpoint documentation

Issue: #14

Test plan

  • Verify imports work: python -c "import gateway"
  • Run linter: ruff check gateway/
  • Run unit tests: pytest tests/unit/
  • Manual testing of new endpoints with curl

Authored-by: jib

Implements 6 REST endpoints from issue #14:
- POST /api/v1/gh/pr/create - Create PR with policy checks
- POST /api/v1/gh/pr/comment - Comment on PR
- POST /api/v1/gh/pr/edit - Edit PR (ownership verified)
- POST /api/v1/gh/pr/close - Close PR (ownership verified)
- POST /api/v1/sessions/<token>/heartbeat - Extend session TTL
- GET /api/v1/repos/visibility - Query repo visibility

Additional changes:
- Add check_pr_create_allowed() to PolicyEngine
- Add get_session_by_ip() to SessionManager (for Phase 2b proxy)
- Wire up check_heartbeat_rate_limit() for heartbeat endpoint
- Update docs/api.md with new endpoints

Closes #14

@james-in-a-box james-in-a-box Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Phase 2a Gateway Endpoints

✅ Issue #14 Coverage

All 6 required endpoints are implemented:

Requirement Status
POST /api/v1/gh/pr/create ✅ Implemented with policy checks
POST /api/v1/gh/pr/comment ✅ Implemented (any PR allowed)
POST /api/v1/gh/pr/edit ✅ Implemented with ownership check
POST /api/v1/gh/pr/close ✅ Implemented with ownership check
POST /sessions/{token}/heartbeat ✅ Implemented with rate limiting
GET /api/v1/repos/visibility ✅ Implemented
get_session_by_ip() ✅ Added to SessionManager
check_pr_create_allowed() ✅ Added to PolicyEngine

🔍 Findings

1. Code Duplication (Medium)

The four PR endpoints (pr_create, pr_comment, pr_edit, pr_close) share ~80% identical boilerplate:

  • Request validation
  • Auth mode determination
  • Private repo access checks
  • Audit logging patterns

Consider extracting a helper like _handle_pr_operation(operation, required_fields, policy_check_fn) in a follow-up.

2. Inconsistent Exception Handling (Low)

gh_pr_create has a try/except wrapper but gh_pr_comment, gh_pr_edit, and gh_pr_close don't. Either all should have it or none.

3. Missing Error Context in /repos/visibility (Low)

If get_repo_visibility() fails for one repo, it returns None but doesn't indicate why (invalid format vs. API error vs. not found). Consider richer error reporting.

4. No Unit Tests Included

The test plan mentions running pytest tests/unit/ but no new tests are added for the 6 new endpoints. For a PR this size (713 additions), tests would catch regressions.

✅ What's Done Well

  • Policy enforcement: PR ownership and private mode checks consistently applied
  • Audit logging: All operations logged with appropriate details
  • Documentation: docs/api.md updated with complete examples
  • Rate limiting: Heartbeat rate limiting properly integrated
  • Alignment with PR #693: Follows the sandbox extraction proposal architecture

Recommendations

  1. Before merge: Add at least happy-path tests for each new endpoint
  2. Follow-up PR: Refactor duplicated code across PR endpoints
  3. Minor: Add consistent exception handling across all new endpoints

Verdict

The PR correctly implements issue #14's requirements and aligns with the Phase 2a plan. The code is functional but would benefit from tests before merging. The duplicated patterns are acceptable for now given this is a phase-based extraction.

— Authored by jib

Add comprehensive test coverage for the 6 new endpoints added in PR #19:
- gh_pr_create: Tests for successful creation, validation errors, user mode blocking
- gh_pr_comment: Tests for comment addition, validation, any-PR access
- gh_pr_edit: Tests for editing, ownership verification
- gh_pr_close: Tests for closing, ownership verification
- session_heartbeat: Tests for TTL extension, rate limiting
- repos_visibility: Tests for visibility queries, multiple repos

Also adds migrated and enhanced tests from james-in-a-box:
- git_client tests: SSH URL handling, arg validation, path validation
- policy tests: PolicyEngine methods, branch/PR ownership, PR creation rules

This addresses review feedback requesting tests before merge.
@james-in-a-box

james-in-a-box Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Response to Review Feedback

Thank you for the thorough review.

✅ Addressed: Unit Tests Added

Added comprehensive test coverage for all 6 new endpoints (101 new tests total):

Endpoint tests (test_gateway_endpoints.py):

  • gh_pr_create: 6 tests - happy path, validation errors, user mode blocking, auth
  • gh_pr_comment: 5 tests - happy path, validation, any-PR access policy
  • gh_pr_edit: 4 tests - happy path, title-only, ownership verification
  • gh_pr_close: 4 tests - happy path, validation, ownership verification
  • session_heartbeat: 4 tests - TTL extension, invalid session, rate limiting
  • repos_visibility: 6 tests - visibility query, multiple repos, validation

Migrated from james-in-a-box (test_git_client.py):

  • SSH URL detection and conversion (14 tests)
  • git args validation (17 tests)
  • path validation (3 tests)

Policy engine tests (test_policy.py):

  • PolicyResult and BoundedCache (4 tests)
  • PR ownership, comments, creation rules (11 tests)
  • Repo/branch extraction utilities (10 tests)

📋 Follow-up Items (Acknowledged)

As suggested in the review, these can be addressed in follow-up PRs:

  1. Code Duplication: Extract helper for shared PR endpoint boilerplate
  2. Inconsistent Exception Handling: Add consistent try/except across PR endpoints
  3. Error Context in /repos/visibility: Add richer error reporting

— Authored by jib

1. Code Duplication (Medium) - Extracted helper functions:
   - check_pr_operation_access(): Common access checks (auth mode,
     session mode, private repo policy)
   - execute_pr_operation(): Consistent GitHub CLI execution with
     error handling and audit logging

2. Inconsistent Exception Handling (Low) - All four PR endpoints
   now use execute_pr_operation() which has consistent try/except
   wrapping for GitHub operations

3. Missing Error Context in /repos/visibility (Low) - Response now
   includes richer error information:
   - Each repo returns {"visibility": "...", "error": "..."}
   - Errors list aggregates all issues for debugging
   - Error types: invalid_format, not_found_or_no_access, api_error

These changes reduce ~80 lines of duplicated code across the PR
endpoints while maintaining the same behavior and test coverage.
@james-in-a-box

james-in-a-box Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Update: All Review Feedback Addressed

✅ Code Duplication (Medium) - Fixed

Extracted two helper functions to reduce ~80 lines of duplicated code:

# Common access checks for all PR operations
check_pr_operation_access(repo, operation, endpoint_name, pr_number?)
-> Returns (PROperationContext, None) or (None, error_response)

# Consistent execution with error handling
execute_pr_operation(operation_name, endpoint_name, repo, auth_mode,
                     gh_args, audit_details, success_message, timeout?)
-> Returns Flask response with try/except wrapping

✅ Inconsistent Exception Handling (Low) - Fixed

All four PR endpoints now use execute_pr_operation() which provides consistent:

  • try/except wrapping for GitHub CLI calls
  • Structured error messages
  • Audit logging for both success and failure

✅ Missing Error Context in /repos/visibility (Low) - Fixed

Response format now includes richer error information:

{
  "visibilities": {
    "owner/repo1": {"visibility": "public"},
    "owner/repo2": {"visibility": null, "error": "not_found_or_no_access"},
    "owner/repo3": {"visibility": null, "error": "invalid_format"}
  },
  "errors": ["owner/repo2: repository not found or no access", ...]
}

Error types: invalid_format, not_found_or_no_access, api_error

Test Coverage

All 101 tests continue to pass with updated assertions for the new visibility response format.

— Authored by jib

@jwbron jwbron mentioned this pull request Feb 3, 2026
5 tasks
@james-in-a-box

james-in-a-box Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Minor Fix: Audit Log Event Naming Consistency

The operation_name values should use present tense to match the docstring example ("pr_comment") and produce cleaner audit event names:

-        operation_name="pr_created",
+        operation_name="pr_create",

-        operation_name="pr_comment_added",
+        operation_name="pr_comment",

-        operation_name="pr_edited",
+        operation_name="pr_edit",

-        operation_name="pr_closed",
+        operation_name="pr_close",

Rationale:

  • Matches the docstring: operation_name: Name of the operation for logging (e.g., "pr_comment")
  • Produces consistent audit log patterns: pr_create, pr_create_failed (vs pr_created, pr_created_failed)
  • Aligns with other endpoints using patterns like push_success, push_failed

All tests still pass after this change.


Re: observations 2 & 3 from review:

  • Decorator usage (@require_session_auth for PR endpoints, @require_launcher_auth for heartbeat/visibility): This is intentional and correct - PR operations are called from containers (session auth), while heartbeat/visibility are called from the launcher.
  • g.session_mode handling: The getattr(g, "session_mode", None) safely handles cases where the Flask context may not have the attribute set.

— Authored by jib

Change operation_name values to match docstring convention:
- pr_created → pr_create
- pr_comment_added → pr_comment
- pr_edited → pr_edit
- pr_closed → pr_close

This produces consistent audit log patterns (pr_create, pr_create_failed)
that align with existing conventions (push_success, push_failed).
@james-in-a-box

james-in-a-box Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Update: Fix pushed in commit 5660c8d.

The PR now has consistent operation_name values matching the docstring convention.

— Authored by jib

jwbron
jwbron previously approved these changes Feb 3, 2026
Add assert statements after check_pr_operation_access() early returns
to help mypy understand that ctx cannot be None after the error check.
@james-in-a-box

james-in-a-box Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Fix pushed: Added mypy assertions in commit 8fb6a9f.

The check_pr_operation_access() function returns (ctx, None) on success or (None, error) on failure. After the early return check if error: return error, mypy doesn't understand that ctx cannot be None. Added assert ctx is not None after each early return to help mypy with type narrowing.

— Authored by jib

@jwbron
jwbron merged commit 82c878c into main Feb 3, 2026
4 checks passed
james-in-a-box Bot pushed a commit that referenced this pull request May 11, 2026
…lockers

Reviewer_plan NACKed the v1 plan with one blocking item (single-OR
JQL fails on team-managed Jira projects) plus 20 non-blocking flags
ranked by impact. This revision lands the blocker plus the 10
highest-impact non-blockers in one re-propose.

Blocker:
- TASK-1-3 + TASK-1-12: replace the single-OR JQL
  `parent = <K> OR "Epic Link" = <K>` with two separate queries
  (`parent = "<K>"` and `"Epic Link" = "<K>"`) and merge results,
  tolerating per-query HTTP 400 (architect ad-9 / risk_analyst R4).
  Single-OR fails on team-managed projects that lack the
  "Epic Link" custom field; auto-detection silently downgrades to
  fresh-path and the sweep returns empty. Exports the helper
  `search_epic_children` so TASK-1-12 reuses it.

Top non-blocking (reviewer-flagged as most impactful):
- #1 In-flight gate trust-boundary trade-off: add explicit
  acknowledgement that gateway-side enforcement is deferred and
  v1 relies on agent-side gating + apply-time re-check by
  TASK-1-13.
- #5 APPLY_EPIC role registration: expand TASK-1-10 to enumerate
  all FIVE registration steps (AgentRole, AgentRoleDefinition,
  get_roles_for_phase, file-restrictions patterns, spawner
  branch).
- #6 epic_apply persistence MCP surface: add
  `mcp__sdlc__update_epic_apply` MCP tool to TASK-1-7 so the
  sandbox-side agent can persist artifact updates.
- #7 Concurrent-edit guard: TASK-1-10 now fetches the current
  epic Description, sha256s it, and registers a divergence HITL
  on mismatch; TASK-1-9 records the baseline sha256;
  TASK-1-7 adds `refine_description_sha256` to the schema.

Additional non-blockers folded in:
- #2: jira_effective_mode added to primitives table.
- #3: TASK-1-5 introduces `shared/egg_jira_credentials.py` shared
  module to eliminate the orchestrator → gateway coupling.
- #8: TASK-1-11 commits to extending `parse_plan` (not
  pass-through).
- #9: TASK-1-5/TASK-1-14 add already-in-state idempotent
  short-circuit for Won't-Do transitions.
- #10: TASK-1-15 introduces `Pipeline.jira_parent_epic_key` so PR
  phase doesn't need an extra Jira call.
- #11: TASK-1-16 adds `PipelinePhase.PLAN_STOPPED` documented
  terminal phase + updates overseer monitor short-circuit.
- #14: TASK-1-11 requires `wont_do_reason` per node + ⚠ warning
  rendering in the plan draft (R6).
- #15: TASK-1-5 gates the orchestrator-direct cred surface behind
  `EGG_ENABLE_ORCH_JIRA_TRANSITIONS` (default off — R1).
- #16: TASK-1-7 schema gains `version`, `idempotency_seed`,
  per-edit `summary_hash` + `applied_at`, `wont_do_reason`,
  signal_source as a list (R10).
- #19: TASK-1-19 drops orchestrator-cli.md, adds
  submit-task-mcp.md.
- #13: TASK-1-18 adds the lint regression test
  `test_no_outbound_jira_writes.py` (R7).
- #12: TASK-1-12 introduces a reverse-index
  `.egg-state/jira-child-pipeline-index.json` to bound the sweep
  to O(K) (R3 performance mitigation).
- #20: New "Risk-analyst items addressed" section summarises how
  R1/R2/R6/R7/R10/R12 are resolved in-plan (no fresh HITLs).

Plan still parses cleanly: 1 slice, 19 tasks, 0 warnings, 0
role-alignment errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
james-in-a-box Bot pushed a commit that referenced this pull request Jul 27, 2026
…ouble-fire guard, register Tier 3 exceptions, reorder midturn_messages

Reviewer feedback identified four issues in v1:
1. AC-1 was unsatisfiable: claimed all 13 fields populated but only 5 in scope
2. detect_heartbeat_stall/ConsensusStallCheck double-fire not addressed
3. TASK-2-2 (#19) and TASK-3-2 (#20) are Tier 3 without registered decisions
4. midturn_messages sequenced last despite gating the primary deliverable

All four corrected. ACK verdict unchanged.
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