Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
08a672a
Initialize SDLC contract for issue #1556
Apr 23, 2026
ba164f7
Refine #1556: analysis for Jira gateway support with credential injec…
Apr 23, 2026
5a3f35e
Persist statefiles after refine phase
Apr 23, 2026
b54fcc9
Persist HITL resolution after refine phase gate
Apr 23, 2026
56612ec
Plan #1556: decompose Jira gateway work into 6-phase single-PR plan
Apr 23, 2026
0934858
Add plan-phase risk assessment for issue #1556 (Jira gateway)
Apr 23, 2026
f604ebb
Plan #1556: architect analysis for Jira gateway support
Apr 23, 2026
f7e4d68
Plan #1556 (rev 2): address reviewer_plan NACK — close 8 blocking gap…
Apr 24, 2026
7076710
Persist statefiles after plan phase
Apr 24, 2026
5383552
docs: document Jira gateway wrapper for issue #1556
Apr 24, 2026
3ce40a7
docs: address reviewer_code non-blocking feedback on Jira wrapper
Apr 24, 2026
0ac77cf
Issue #1556 (Phase 1+2): add Jira gateway foundation + REST routes
Apr 24, 2026
02dfb30
Issue #1556 (Phase 3+5): env injection, session plumbing, sandbox wra…
Apr 24, 2026
1d13464
Issue #1556: make Jira flat-import fallback self-sufficient in gatewa…
Apr 24, 2026
985c166
Issue #1556: keep Jira sandbox wrapper at artefact path until exempti…
Apr 24, 2026
7895474
Issue #1556 (cycle 2 NACK): close /search/jql hole + symlink jira in …
Apr 24, 2026
9d5703e
Issue #1556: tests for Jira gateway wrapper (Phase 4)
Apr 24, 2026
d481777
Issue #1556: lock down /search/jql execute-bypass removal in tests
Apr 24, 2026
67969fe
Issue #1556 (cycle 3 tester NACK): fix SIGHUP crash + ruff lint/format
Apr 24, 2026
1736410
Persist statefiles after implement phase
Apr 24, 2026
bf2a045
Remove ephemeral agent-output handoff artifacts (#1731)
Apr 24, 2026
24d3867
Fix mypy errors in Jira gateway modules
james-in-a-box[bot] Apr 24, 2026
d750a20
Fix mypy type: ignore comments for jira_client and jira_policy
james-in-a-box[bot] Apr 24, 2026
67323eb
Fix pytest.skip at module level: add allow_module_level=True
james-in-a-box[bot] Apr 24, 2026
b7b3846
Address review feedback on Jira gateway wrapper PR
james-in-a-box[bot] Apr 24, 2026
1510e90
Use RETURN trap in jira wrapper call_gateway() for function-scoped cl…
james-in-a-box[bot] Apr 24, 2026
3c2a9ec
Add 401/429 user-friendly messages to jira wrapper response parser
egg-reviewer[bot] Apr 24, 2026
c3808da
Add test coverage for 401/429 user-friendly messages in jira wrapper
james-in-a-box[bot] Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,138 changes: 1,138 additions & 0 deletions .egg-state/brc-history/1556-implement.json

Large diffs are not rendered by default.

3,031 changes: 3,031 additions & 0 deletions .egg-state/brc-history/1556-implement.md

Large diffs are not rendered by default.

375 changes: 375 additions & 0 deletions .egg-state/brc-history/1556-plan.json

Large diffs are not rendered by default.

1,309 changes: 1,309 additions & 0 deletions .egg-state/brc-history/1556-plan.md

Large diffs are not rendered by default.

169 changes: 169 additions & 0 deletions .egg-state/brc-history/1556-refine.json

Large diffs are not rendered by default.

382 changes: 382 additions & 0 deletions .egg-state/brc-history/1556-refine.md

Large diffs are not rendered by default.

909 changes: 909 additions & 0 deletions .egg-state/contracts/issue-1556.json

Large diffs are not rendered by default.

319 changes: 319 additions & 0 deletions .egg-state/drafts/1556-analysis.md

Large diffs are not rendered by default.

893 changes: 893 additions & 0 deletions .egg-state/drafts/1556-plan.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,21 @@ Controls which Confluence spaces, JIRA projects, and repositories are synced.
**Phase 3 (Target)**: LOW risk - DLP scanning + output monitoring

See file for detailed allowlists and blocked patterns.

### `jira.projects` — Jira Project Allowlist

The `jira` section controls which Jira projects are accessible through the gateway's
`/api/v1/jira/*` routes. Agents can only read tickets, comments, and search results
from allowlisted projects. Fail-closed: missing file, missing section, or malformed
YAML results in an empty allowlist (no project accessible).

```yaml
jira:
projects:
- ENG # Engineering project
- DEVOPS # DevOps project
```

- **Keys must match Atlassian format**: uppercase letter followed by uppercase letters, digits, or underscores (`[A-Z][A-Z0-9_]*`). Invalid entries are logged and ignored.
- **Empty list** (`projects: []`): No Jira projects accessible — all ticket/search/execute requests return 403.
- **Hot-reloadable**: Changes are picked up via `POST /api/v1/config/reload` or SIGHUP without restarting the gateway.
24 changes: 24 additions & 0 deletions config/context-filters.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Context filters for external integrations.
#
# This file is read by the gateway sidecar to enforce allowlists on external
# services (Jira, Confluence, etc.). It lives in the same repo as the
# gateway — edit it and either hot-reload with `POST /api/v1/config/reload`
# or roll the gateway pod.
#
# SAFETY: every section fails closed. Leaving a list empty (or this file
# missing entirely) blocks every call until an operator adds entries.

jira:
# Atlassian Jira project keys agents are allowed to read through the
# /api/v1/jira/* endpoints. Project keys must match the Atlassian shape:
# uppercase letter followed by letters / digits / underscore (e.g. ENG,
# DEVOPS, MOBILE_V2). Case-sensitive.
#
# Example:
# projects: ["ENG", "DEVOPS"]
#
# Any ticket whose project is not on this list returns HTTP 403 with
# `jira_*_denied` in the gateway audit log. JQL searches must be
# statically provable as scoped to the listed projects — see
# gateway/jira_search.py for the exact acceptance rules.
projects: []
14 changes: 10 additions & 4 deletions config/secrets.template.env
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,15 @@ CONFLUENCE_SPACE_KEYS="" # Comma-separated list of space keys to sync
# =============================================================================
# JIRA Integration (Optional)
# =============================================================================
# Same API token as Confluence if using Atlassian Cloud
# Same API token as Confluence if using Atlassian Cloud.
# Consumed by gateway/jira_credentials.py — sandboxed agents reach Jira
# through the gateway's /api/v1/jira/* endpoints; these credentials are
# never exported to the sandbox.
#
# Project allowlist lives in config/context-filters.yaml under jira.projects.
# Editing either file picks up via `POST /api/v1/config/reload` — no gateway
# restart required.

JIRA_BASE_URL="" # e.g., https://yourcompany.atlassian.net
JIRA_USERNAME="" # Your email
JIRA_BASE_URL="" # e.g., https://yourcompany.atlassian.net (no trailing slash)
JIRA_USERNAME="" # Atlassian account email
JIRA_API_TOKEN=""
JIRA_JQL_QUERY="" # e.g., project = ENG AND status != Done
41 changes: 35 additions & 6 deletions docs/architecture/credential-injection.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Credential Injection

The gateway sidecar injects credentials at the proxy layer, ensuring the sandbox container has zero credential access. This covers both GitHub (via git wrappers) and Anthropic API (via `ANTHROPIC_BASE_URL`) credentials.
The gateway sidecar injects credentials at the proxy layer, ensuring the sandbox container has zero credential access. This covers GitHub (via git wrappers), Anthropic API (via `ANTHROPIC_BASE_URL`), and Atlassian/Jira (via the `/api/v1/jira/*` REST endpoints) credentials.

**Key properties:**
- **Zero credential exposure**: Container never sees API keys, OAuth tokens, or GitHub tokens
Expand Down Expand Up @@ -147,22 +147,51 @@ Gateway enforcement cannot be bypassed because:
|------|--------|-----------------|
| API Key | `ANTHROPIC_API_KEY` in secrets.env | `x-api-key: <key>` |
| OAuth Token | `ANTHROPIC_OAUTH_TOKEN` in secrets.env | `Authorization: Bearer <token>` |
| Atlassian / Jira | `JIRA_BASE_URL` + `JIRA_USERNAME` + `JIRA_API_TOKEN` in secrets.env | `Authorization: Basic <base64(username:token)>` |

OAuth takes precedence if both are configured. OAuth tokens may expire; the user runs `claude auth status` to generate a new token, and the gateway hot-reloads via mtime-based cache refresh.
OAuth takes precedence over API key if both are configured. OAuth tokens may expire; the user runs `claude auth status` to generate a new token, and the gateway hot-reloads via mtime-based cache refresh.

### Atlassian / Jira

Sandboxed agents reach Jira exclusively through the gateway's `/api/v1/jira/*` REST endpoints. Atlassian credentials are held by the gateway and injected per-request; they never enter the sandbox.

**Credential storage:**
```bash
# ~/.config/egg/secrets.env
JIRA_BASE_URL="https://your-site.atlassian.net"
JIRA_USERNAME="bot@example.com" # Atlassian account email
JIRA_API_TOKEN="ATATT3x..." # Atlassian Cloud API token
```

**Loader:** `gateway/jira_credentials.py` mirrors `gateway/anthropic_credentials.py` — mtime-based cache refresh of `~/.config/egg/secrets.env` (override with `EGG_SECRETS_PATH`). `get_jira_credentials()` returns a `JiraCredentials` dataclass with `base_url`, `username`, `api_token`, and a `basic_auth_header()` helper that emits the base64-encoded `Basic` header. Missing values raise `JiraCredentialsUnavailable`, which the route layer translates to HTTP 503. `reload_jira_credentials()` is wired into the gateway's `_reload_all_config()` hook, so `POST /api/v1/config/reload` picks up rotated tokens without a process restart.

**Zero-credential invariant:** The orchestrator's sandbox-launch env builder (`orchestrator/routes/pipelines.py`) is forbidden from exporting `JIRA_BASE_URL`, `JIRA_USERNAME`, or `JIRA_API_TOKEN` to the agent container. A regression test in `orchestrator/tests/test_start_pipeline.py` iterates the sandbox env and asserts those three keys are absent. The only Jira-related variables the agent sees are `EGG_JIRA_TICKET` (the ticket the pipeline is scoped to) and `EGG_JIRA_PROJECT` (optional, advisory) — neither is a credential.

**Private-mode only + project allowlist:** Every `/api/v1/jira/*` route is decorated with `@require_private_mode` (`gateway/mode_gate.py`). In public mode, the decorator returns 403 and emits a `private_mode_required` audit entry **before** any credential is loaded or any upstream request is issued. After the mode gate, each route checks the extracted project key against the allowlist in `config/context-filters.yaml` (`jira.projects`) — see [Jira wrapper reference](../reference/jira-wrapper.md) for the full policy semantics.

**Squid allowlist excludes Atlassian domains:** `*.atlassian.net`, `*.atlassian.com`, `api.atlassian.com`, and `jira.atlassian.com` are intentionally **not** in `gateway/allowed_domains.txt`. All Jira traffic flows through the gateway REST endpoints, never through the Squid proxy, so the private-mode gate and the project allowlist cannot be bypassed by a direct `CONNECT` to Atlassian through the proxy. A regression test (`gateway/tests/test_allowed_domains.py`) enforces this invariant.

## Files

| File | Purpose |
|------|---------|
| `gateway/gateway.py` | Anthropic proxy endpoints, credential injection, tool filtering |
| `gateway/anthropic_credentials.py` | Credential loading from secrets.env |
| `gateway/allowed_domains.txt` | Domain allowlist (api.anthropic.com intentionally absent) |
| `gateway/gateway.py` | Anthropic proxy endpoints, `/api/v1/jira/*` routes, credential injection, tool filtering, `_reload_all_config()` hot-reload hook |
| `gateway/anthropic_credentials.py` | Anthropic credential loading from secrets.env |
| `gateway/jira_credentials.py` | Atlassian credential loading from secrets.env (mtime refresh, basic-auth header helper) |
| `gateway/jira_client.py` | Jira REST client + `validate_jira_api_path` regex allowlist + 429 retry + 404 envelope |
| `gateway/jira_policy.py` | Project allowlist loader for `config/context-filters.yaml` (`jira.projects`) |
| `gateway/mode_gate.py` | `@require_private_mode` decorator (fails closed in public mode, marks view for regression test) |
| `gateway/session_manager.py` | `Session.jira_ticket` audit field (observational; project allowlist is the only hard boundary) |
| `gateway/allowed_domains.txt` | Domain allowlist (api.anthropic.com and all Atlassian domains intentionally absent) |
| `sandbox/entrypoint.py` | Set ANTHROPIC_BASE_URL, remove creds from env, set disallowedTools in private mode |
| `sandbox/scripts/jira` | Sandbox CLI wrapper — POSTs to `/api/v1/jira/*` with `EGG_SESSION_TOKEN` |
| `shared/egg_agent/client.py` | Pass `disallowed_tools` via SDK options for headless agents in private mode |
| `config/secrets.template.env` | Template for Anthropic credentials |
| `config/secrets.template.env` | Template for Anthropic and Atlassian credentials |
| `config/context-filters.yaml` | Operator-facing Jira project allowlist (`jira.projects:`) |

## Related Documentation

- [Git Isolation Architecture](git-isolation.md) — Worktree isolation via gateway
- [Network Isolation](network-isolation.md) — Full network lockdown design
- [Jira Wrapper Reference](../reference/jira-wrapper.md) — `/api/v1/jira/*` endpoint surface, JQL scope extractor, not-found envelope, future-verb extension points
- [Architecture Overview](README.md) — System design
13 changes: 12 additions & 1 deletion docs/architecture/network-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ The gateway exposes a controlled API for git/gh operations:
- `POST /api/gh/pr/close` — close PR (only egg's own PRs)
- **No merge endpoint** — human must merge via GitHub UI

**Jira read endpoints (`/api/v1/jira/*`) — private-mode only, fail closed in public mode:**

- `POST /api/v1/jira/ticket/get` — read a single ticket (returns ADF-rendered body via `expand=renderedBody,renderedFields`)
- `POST /api/v1/jira/search` — JQL search against `/rest/api/3/search/jql` with a conservative static project-scope extractor (deny-on-ambiguity)
- `POST /api/v1/jira/ticket/comments` — read comments for a ticket
- `POST /api/v1/jira/execute` — GET-only passthrough, regex-allowlisted paths; write verbs (`transitions`, `worklog`, `attachments`, `watchers`, `DELETE`, `PUT`, `PATCH`) are permanently denied

All four routes compose `@require_session_auth` → `@require_private_mode` → project-allowlist check → fields/JQL validation → `JiraClient` call → structured audit log. In **public mode**, `@require_private_mode` short-circuits every call with a 403 and a `private_mode_required` audit entry **before** any upstream request is issued — no Atlassian traffic ever leaves the gateway in public mode. See [Jira wrapper reference](../reference/jira-wrapper.md).

### CLI Wrappers

The egg container uses `git` and `gh` CLI wrappers that:
Expand Down Expand Up @@ -310,7 +319,9 @@ The gateway maintains a strict allowlist of permitted domains:
- **Enforced at proxy** — Squid validates destination before forwarding
- **SNI-based validation** — for HTTPS, the proxy inspects the Server Name Indication (SNI) in the TLS ClientHello to determine the destination domain. This does **not** require MITM CA certificates or decrypting traffic — the proxy reads the plaintext hostname from the CONNECT request and SNI extension, then either tunnels or rejects.

**Explicitly excluded:** `*.actions.githubusercontent.com`, `ghcr.io`, `*.github.io`, `copilot-*.githubusercontent.com`
**Explicitly excluded:** `*.actions.githubusercontent.com`, `ghcr.io`, `*.github.io`, `copilot-*.githubusercontent.com`, **`*.atlassian.net` / `*.atlassian.com` / `api.atlassian.com` / `jira.atlassian.com`**

**Why Atlassian domains are excluded from the Squid allowlist:** all Jira traffic **must** flow through the gateway REST endpoints (`/api/v1/jira/*`). Adding `*.atlassian.net` to Squid would let a compromised sandbox reach Jira directly through the proxy, bypassing the private-mode gate, the project allowlist, the verb allowlist, and the audit log. A dedicated regression test (`gateway/tests/test_allowed_domains.py`) asserts that none of `atlassian.net`, `atlassian.com`, `api.atlassian.com`, or `jira.atlassian.com` appear in `gateway/allowed_domains.txt`.

### What Gets Blocked

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ This index helps both humans and LLMs navigate the documentation efficiently.
| [MCP Deployment Tools](reference/mcp-deployment-tools.md) | Six k8s-facing MCP tools: `get_deployment_context`, `validate_deployment_manifests`, `prune_stale_worktrees`, `validate_network_isolation`, `rebuild_and_rollout`, `get_service_logs` |
| [Agent MCP Tools](reference/agent-tools.md) | In-process SDK MCP tools sandbox agents call on the `tool_use` stream (15 iteration-1 verbs: `mcp__sdlc__*`, `mcp__brc__*`, `mcp__phase__*`, `mcp__progress__*`, `mcp__task__*`); on by default — set `EGG_MCP_TOOLS=false` to opt out |
| [Agent Wait Patterns](reference/agent-wait-patterns.md) | Canonical `egg-orch message wait-loop` idiom for BRC STAY ALIVE, the four anti-patterns to avoid, the `egg-orch message wait` exit-code contract, the `HEARTBEAT` metadata schema, and the `EGG_MESSAGE_POLL_MAX_WAIT` / `EGG_ORCH_WAITRESS_THREADS` env-var couplings |
| [Jira Wrapper](reference/jira-wrapper.md) | `/api/v1/jira/*` read-only gateway endpoints (ticket read, JQL search with static project-scope extraction, ticket comments, GET-only execute passthrough); private-mode only; project allowlist via `config/context-filters.yaml`; `not_found` envelope; future write-verb extension points |

### SDLC Pipeline Templates

Expand Down
Loading
Loading