Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions LOCAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ require `ISSUE_NUMBER`. It derives the work-item key from
external key as a GitHub or GitLab issue number.

```bash
# Jira-source env vars (runner-only — never enter sandbox)
# Jira-source env vars (JIRA_USER_EMAIL and JIRA_BASE_URL enter the code
Comment thread
ralphbean marked this conversation as resolved.
# sandbox as non-secret config; JIRA_TOKEN is read by the jira-ro
# provider on the host — it never enters the code agent's runner or
# sandbox environment)
export FULLSEND_WORK_ITEM_URL="https://your-site.atlassian.net/browse/TESTPROJ-42"
export JIRA_USER_EMAIL="you@example.com"
export JIRA_TOKEN="your-jira-api-token"
Expand All @@ -175,10 +178,10 @@ export GH_TOKEN="$(gh auth token)"

Run `fullsend run code` the same way as step 3 above. `--target-repo`
should point at a local checkout of the repo where the PR will be
created. The Jira pre-script fetches the issue via the `fullsend` CLI
and writes the context to `/tmp/jira-issue-context.json`, which
`host_files` copies into the sandbox as
`/sandbox/workspace/.issue-context.json`.
created. The Jira pre-script validates the issue URL and installs
pre-commit tool dependencies. The sandbox reads the Jira work item
directly through provider-backed API access (the `jira-ro` provider
handles credential injection at the network layer).

## Testing a new configuration option

Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ script-test:
$(call run-timed,bash scripts/post-prioritize-test.sh)
$(call run-timed,bash scripts/pre-code-test.sh)
$(call run-timed,bash scripts/pre-code-jira-test.sh)
$(call run-timed,bash scripts/harness-jira-test.sh)
$(call run-timed,bash scripts/sandbox-credential-boundary-test.sh)
$(call run-timed,bash scripts/post-code-test.sh)
$(call run-timed,bash scripts/pre-review-test.sh)
$(call run-timed,bash scripts/post-review-test.sh)
Expand Down
17 changes: 11 additions & 6 deletions docs/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ See [Customizing with AGENTS.md](https://fullsend.sh/docs/guides/user/customizin
|----------|-------------|---------|--------------|
| `CODE_ALLOWED_TARGET_BRANCHES` | Restricts which branches the code agent can target when pushing. The post-code script reads it from the runner when present and validates the agent's chosen target branch before pushing. It is never injected into the sandbox. | Repo default branch (auto-detected via forge API; falls back to `main`) | Comma-separated branch names (e.g. `main,develop`) or `*` for any branch |
| `FULLSEND_FORGE` | Forge platform. Set automatically by the harness overlay `env` section (matched via `when: 'runtime.forge == "<platform>"'`). | (set by harness) | `"github"`, `"gitlab"` |
| `FULLSEND_TRACKER` | Source tracker for the work item (matches triage convention). When set to `"jira"`, the code agent requires issue context at `/sandbox/workspace/.issue-context.json` (prepared by the Jira pre-script) instead of calling forge APIs. Set by the Jira-source overlay in `harness/code.yaml`. | (unset — forge-native) | `"jira"` |
| `FULLSEND_TRACKER` | Source tracker for the work item (matches triage convention). When set to `"jira"`, the code agent reads the Jira work item directly via provider-backed Jira API access (the `jira-ro` provider handles credential injection at the network layer). Set by the Jira-source overlay in `harness/code.yaml`. | (unset — forge-native) | `"jira"` |
| `ISSUE_NUMBER` | Numeric source issue identifier used when the source tracker is the target forge. It is optional for external-tracker runs because that work-item key is not a target-forge issue number. | (set by forge-native workflows) | Positive integer |
| `CODE_AUTO_MERGE` | Set to `"true"` to enable auto-merge on PRs/MRs created by the code agent. On GitHub, uses `gh pr merge --auto`; on GitLab, uses `merge_when_pipeline_succeeds`. Requires branch protection with required reviews or status checks on the target branch. Read directly from the runner environment (not declared in `env.runner`). | `""` (disabled) | `"true"` to enable |
| `CODE_AUTO_MERGE_METHOD` | Merge method for auto-merge: `"squash"`, `"rebase"`, or `"merge"`. When unset, auto-detected from the repo's allowed merge methods (prefers squash). Omitted automatically when the target branch uses a merge queue. Ignored unless `CODE_AUTO_MERGE` is `"true"`. | Auto-detected (prefers squash) | `"squash"`, `"rebase"`, `"merge"` |
Expand Down Expand Up @@ -182,11 +182,16 @@ vars. Key differences from single-forge setup:
per-forge env file (`env/github/code.env` or `env/gitlab/code.env`)
maps the platform-specific variable to `ISSUE_URL`.
- **Jira-source overlay** — when the work item originates from Jira
(`event.source.system == "jira"`), a dedicated overlay fetches the
issue via `fullsend issues get --tracker jira` on the runner and
copies the context into the sandbox. Jira credentials stay on the
runner. The Jira overlay composes with the target-forge overlay
(GitHub or GitLab) via merge-all-matching.
(`event.source.system == "jira"`), a dedicated overlay attaches the
`jira-ro` provider and `fullsend-jira-ro` OpenShell profile so the
sandbox can read the Jira work item via the REST API. Sandbox curl
commands use `--user "${JIRA_USER_EMAIL}:${JIRA_TOKEN}"` for Basic
auth, but `JIRA_TOKEN` inside the sandbox is the provider's opaque
placeholder — the real API token is never expanded into sandbox
config or env files. OpenShell replaces the placeholder in the
Basic Authorization header at the proxy boundary. The Jira overlay
composes with the target-forge overlay (GitHub or GitLab) via
merge-all-matching.
- **External work-item identity** — when the source tracker differs from the
target forge, the code agent derives the key from `ISSUE_URL`.
Branch names and PR text use that key and link the source URL; they do not
Expand Down
16 changes: 15 additions & 1 deletion docs/triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ GitHub/GitLab auth vars:
|----------|-------------|
| `FULLSEND_WORK_ITEM_URL` | The `https://<site>.atlassian.net/browse/<KEY>-<n>` URL of the issue to triage. |
| `JIRA_USER_EMAIL` | Email address of the Jira Cloud account used for Basic auth. |
| `JIRA_TOKEN` | API token for that account. |
| `JIRA_TOKEN` | API token for that account. Available to the runner for post-script mutations; the sandbox receives the `jira-ro` provider's opaque placeholder instead of the real token. |
| `JIRA_BASE_URL` | Base URL of the Jira Cloud site (e.g. `https://<site>.atlassian.net`). |

Closing an issue (`duplicate`, `not-planned`, `split` actions) performs a
Expand Down Expand Up @@ -276,6 +276,20 @@ If you use `base:` composition to override `harness/triage.yaml`:
`prerequisites.existing[].url`) are interpolated verbatim and are
schema-constrained only. The prompt includes examples of the relevant URL
shape to guide the agent toward the correct format.
- **Jira credentials use provider-backed delivery**: The `jira-ro`
provider and `fullsend-jira-ro` OpenShell profile handle Jira API
token injection at the network layer. Sandbox curl commands use
`--user "${JIRA_USER_EMAIL}:${JIRA_TOKEN}"` for Basic auth, but
`JIRA_TOKEN` inside the sandbox is the provider's opaque placeholder
— the real API token is never expanded into sandbox config or env
files. OpenShell replaces the placeholder in the Basic Authorization
header at the proxy boundary. `JIRA_USER_EMAIL` and `JIRA_BASE_URL`
remain in `env.sandbox` as non-secret configuration. Runner-side
post-scripts retain the real `JIRA_TOKEN` via `env.runner` for
trusted mutations (label writes, transitions, comment posting).
Do not switch to bearer auth — the Jira Cloud tenant URL
(`*.atlassian.net/rest/api/3/...`) requires Basic auth with
`email:api_token`.
- **GitLab and Jira functional eval coverage is deferred**: The eval cases
under `eval/triage/cases/` currently cover GitHub only. GitLab and Jira
behavior is covered by unit-level bash tests in
Expand Down
1 change: 0 additions & 1 deletion env/jira/triage.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export ISSUE_URL="${FULLSEND_WORK_ITEM_URL}"
export JIRA_USER_EMAIL="${JIRA_USER_EMAIL}"
export JIRA_TOKEN="${JIRA_TOKEN}"
export JIRA_BASE_URL="${JIRA_BASE_URL}"
22 changes: 12 additions & 10 deletions harness/code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,22 @@ overlays:
ISSUE_URL: "${GITLAB_ISSUE_URL}"
GITLAB_TOKEN: "${GITLAB_TOKEN}"
FULLSEND_FORGE: gitlab
# Jira-source overlay — fetches the work item from Jira and prepares
# issue context for the sandbox. Ordered AFTER forge overlays so its
# env vars (especially ISSUE_URL) win under last-writer-wins merge
# semantics when both the Jira overlay and a forge overlay match.
# Jira-source overlay — provides provider-backed Jira API access so
# the sandbox can read work items directly through the credential
# boundary. Ordered AFTER forge overlays so its env vars (especially
# ISSUE_URL) win under last-writer-wins merge semantics when both the
# Jira overlay and a forge overlay match.
# pre_script overrides the top-level default. Post-script selection
# stays with the target forge (GitHub/GitLab).
- when: 'event.source.system == "jira"'
providers:
- providers/jira-ro.yaml
openshell:
profiles:
- profiles/fullsend-jira-ro.yaml
pre_script: scripts/pre-code-jira.sh
host_files:
- src: ${RUNNER_TEMP}/jira-issue-context.json
dest: /sandbox/workspace/.issue-context.json
env:
runner:
JIRA_ISSUE_CONTEXT_FILE: "${RUNNER_TEMP}/jira-issue-context.json"
JIRA_USER_EMAIL: "${JIRA_USER_EMAIL}"
JIRA_TOKEN: "${JIRA_TOKEN}"
JIRA_BASE_URL: "${JIRA_BASE_URL}"
ISSUE_URL: "${FULLSEND_WORK_ITEM_URL}"
# A Jira source work item has a key, not a target-forge issue number.
Expand All @@ -156,3 +156,5 @@ overlays:
FULLSEND_TRACKER: jira
ISSUE_NUMBER: ""
Comment thread
ralphbean marked this conversation as resolved.
ISSUE_URL: "${FULLSEND_WORK_ITEM_URL}"
JIRA_USER_EMAIL: "${JIRA_USER_EMAIL}"
JIRA_BASE_URL: "${JIRA_BASE_URL}"
1 change: 0 additions & 1 deletion harness/triage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,5 @@ overlays:
sandbox:
ISSUE_URL: "${FULLSEND_WORK_ITEM_URL}"
Comment thread
ralphbean marked this conversation as resolved.
JIRA_USER_EMAIL: "${JIRA_USER_EMAIL}"
JIRA_TOKEN: "${JIRA_TOKEN}"
JIRA_BASE_URL: "${JIRA_BASE_URL}"
Comment thread
ralphbean marked this conversation as resolved.
FULLSEND_FORGE: jira
229 changes: 229 additions & 0 deletions scripts/harness-jira-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
# harness-jira-test.sh — Verify Jira provider/profile configuration and
# credential boundary in triage and code harness files.
#
# Run from the repo root: bash scripts/harness-jira-test.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
FAILURES=0

assert_pass() {
local test_name="$1"
echo "PASS: ${test_name}"
}

assert_fail() {
local test_name="$1"
local detail="$2"
echo "FAIL: ${test_name} — ${detail}"
FAILURES=$((FAILURES + 1))
}

# ---------------------------------------------------------------------------
# Helper: extract a field from the Jira overlay in a harness YAML file.
# Uses yq, which the script-test workflow installs.
# $1 = harness YAML path
# $2 = yq expression evaluated on the Jira overlay.
# ---------------------------------------------------------------------------
jira_overlay_field() {
local harness_file="$1"
local yq_expr="$2"
yq -r ".overlays[] | select(.when | contains(\"jira\")) | ${yq_expr}" "${harness_file}"
}

# ---------------------------------------------------------------------------
# Triage harness tests
# ---------------------------------------------------------------------------
TRIAGE_HARNESS="${REPO_ROOT}/harness/triage.yaml"

# Provider present
if jira_overlay_field "${TRIAGE_HARNESS}" ".providers[]" | grep -qF "providers/jira-ro.yaml"; then
assert_pass "triage-jira-provider-present"
else
assert_fail "triage-jira-provider-present" "providers/jira-ro.yaml not in Jira overlay"
fi

# Profile present
if jira_overlay_field "${TRIAGE_HARNESS}" ".openshell.profiles[]" | grep -qF "profiles/fullsend-jira-ro.yaml"; then
assert_pass "triage-jira-profile-present"
else
assert_fail "triage-jira-profile-present" "profiles/fullsend-jira-ro.yaml not in Jira overlay"
fi

# JIRA_TOKEN not in sandbox env
if jira_overlay_field "${TRIAGE_HARNESS}" "(.env.sandbox // {}) | keys | .[]" | grep -qF "JIRA_TOKEN"; then
assert_fail "triage-jira-token-not-in-sandbox" "JIRA_TOKEN found in sandbox env"
else
assert_pass "triage-jira-token-not-in-sandbox"
fi

# JIRA_TOKEN still in runner env (needed for post-script mutations)
if jira_overlay_field "${TRIAGE_HARNESS}" "(.env.runner // {}) | keys | .[]" | grep -qF "JIRA_TOKEN"; then
assert_pass "triage-jira-token-in-runner"
else
assert_fail "triage-jira-token-in-runner" "JIRA_TOKEN missing from runner env (needed for post-script)"
fi

# JIRA_USER_EMAIL in sandbox env (non-secret, needed for Basic auth)
if jira_overlay_field "${TRIAGE_HARNESS}" "(.env.sandbox // {}) | keys | .[]" | grep -qF "JIRA_USER_EMAIL"; then
assert_pass "triage-jira-email-in-sandbox"
else
assert_fail "triage-jira-email-in-sandbox" "JIRA_USER_EMAIL missing from sandbox env"
fi

# JIRA_BASE_URL in sandbox env (non-secret, needed for API URLs)
if jira_overlay_field "${TRIAGE_HARNESS}" "(.env.sandbox // {}) | keys | .[]" | grep -qF "JIRA_BASE_URL"; then
assert_pass "triage-jira-base-url-in-sandbox"
else
assert_fail "triage-jira-base-url-in-sandbox" "JIRA_BASE_URL missing from sandbox env"
fi

# env/jira/triage.env does not contain JIRA_TOKEN
TRIAGE_ENV="${REPO_ROOT}/env/jira/triage.env"
if [ -f "${TRIAGE_ENV}" ]; then
if grep -qF "JIRA_TOKEN" "${TRIAGE_ENV}"; then
assert_fail "triage-env-file-no-token" "JIRA_TOKEN found in ${TRIAGE_ENV}"
else
assert_pass "triage-env-file-no-token"
fi
else
assert_fail "triage-env-file-no-token" "${TRIAGE_ENV} not found"
fi

# ---------------------------------------------------------------------------
# Code harness tests
# ---------------------------------------------------------------------------
CODE_HARNESS="${REPO_ROOT}/harness/code.yaml"

# Provider present
if jira_overlay_field "${CODE_HARNESS}" ".providers[]" | grep -qF "providers/jira-ro.yaml"; then
assert_pass "code-jira-provider-present"
else
assert_fail "code-jira-provider-present" "providers/jira-ro.yaml not in Jira overlay"
fi

# Profile present
if jira_overlay_field "${CODE_HARNESS}" ".openshell.profiles[]" | grep -qF "profiles/fullsend-jira-ro.yaml"; then
assert_pass "code-jira-profile-present"
else
assert_fail "code-jira-profile-present" "profiles/fullsend-jira-ro.yaml not in Jira overlay"
fi

# JIRA_TOKEN not in sandbox env
if jira_overlay_field "${CODE_HARNESS}" "(.env.sandbox // {}) | keys | .[]" | grep -qF "JIRA_TOKEN"; then
assert_fail "code-jira-token-not-in-sandbox" "JIRA_TOKEN found in sandbox env"
else
assert_pass "code-jira-token-not-in-sandbox"
fi

# JIRA_TOKEN not in runner env (code agent runner doesn't need Jira creds)
if jira_overlay_field "${CODE_HARNESS}" "(.env.runner // {}) | keys | .[]" | grep -qF "JIRA_TOKEN"; then
assert_fail "code-jira-token-not-in-runner" "JIRA_TOKEN found in runner env (no longer needed)"
else
assert_pass "code-jira-token-not-in-runner"
fi

# JIRA_USER_EMAIL in sandbox env (non-secret, needed for Basic auth)
if jira_overlay_field "${CODE_HARNESS}" "(.env.sandbox // {}) | keys | .[]" | grep -qF "JIRA_USER_EMAIL"; then
assert_pass "code-jira-email-in-sandbox"
else
assert_fail "code-jira-email-in-sandbox" "JIRA_USER_EMAIL missing from sandbox env"
fi

# JIRA_BASE_URL in sandbox env (non-secret, needed for API URLs)
if jira_overlay_field "${CODE_HARNESS}" "(.env.sandbox // {}) | keys | .[]" | grep -qF "JIRA_BASE_URL"; then
assert_pass "code-jira-base-url-in-sandbox"
else
assert_fail "code-jira-base-url-in-sandbox" "JIRA_BASE_URL missing from sandbox env"
fi

# No .issue-context.json host_file (prefetch removed)
if jira_overlay_field "${CODE_HARNESS}" ".host_files[]?.dest" | grep -qF ".issue-context.json"; then
assert_fail "code-no-issue-context-host-file" ".issue-context.json still in host_files"
else
assert_pass "code-no-issue-context-host-file"
fi

# JIRA_ISSUE_CONTEXT_FILE not in runner env (prefetch removed)
if jira_overlay_field "${CODE_HARNESS}" "(.env.runner // {}) | keys | .[]" | grep -qF "JIRA_ISSUE_CONTEXT_FILE"; then
assert_fail "code-no-issue-context-file-env" "JIRA_ISSUE_CONTEXT_FILE still in runner env"
else
assert_pass "code-no-issue-context-file-env"
fi

# ---------------------------------------------------------------------------
# Skill-level tests: sandbox curl commands must use Basic auth with the
# opaque provider placeholder (--user "${JIRA_USER_EMAIL}:${JIRA_TOKEN}").
# The real token never enters the sandbox; JIRA_TOKEN in these commands is
# the provider-supplied placeholder that OpenShell replaces at the proxy
# boundary.
# ---------------------------------------------------------------------------
SKILL_FILES=(
"${REPO_ROOT}/skills/jira-forge/SKILL.md"
"${REPO_ROOT}/skills/issue-labels/jira/SKILL.md"
"${REPO_ROOT}/skills/jira-components/SKILL.md"
"${REPO_ROOT}/skills/code-implementation/SKILL.md"
)

for skill_file in "${SKILL_FILES[@]}"; do
skill_name="$(basename "$(dirname "${skill_file}")")"
test_name="skill-${skill_name}-basic-auth-placeholder"

if [ ! -f "${skill_file}" ]; then
assert_fail "${test_name}" "${skill_file} not found"
continue
fi

# Every Jira curl command in the skill must use --user for Basic auth.
# Check that at least one --user flag exists alongside JIRA_TOKEN.
if grep -qF -- '--user "${JIRA_USER_EMAIL}:${JIRA_TOKEN}"' "${skill_file}"; then
assert_pass "${test_name}"
else
assert_fail "${test_name}" "missing --user \"\${JIRA_USER_EMAIL}:\${JIRA_TOKEN}\" in ${skill_file}"
fi
done

# Verify no Jira skill curl command uses bearer auth (would require OAuth
# token and the api.atlassian.com endpoint model, not the tenant URL).
for skill_file in "${SKILL_FILES[@]}"; do
skill_name="$(basename "$(dirname "${skill_file}")")"
test_name="skill-${skill_name}-no-bearer-auth"

if [ ! -f "${skill_file}" ]; then
continue # already reported above
fi

if grep -qi 'Authorization.*Bearer' "${skill_file}"; then
assert_fail "${test_name}" "bearer auth found in ${skill_file} (use Basic auth for tenant URL)"
else
assert_pass "${test_name}"
fi
done

# ---------------------------------------------------------------------------
# Provider and profile file existence
# ---------------------------------------------------------------------------
if [ -f "${REPO_ROOT}/providers/jira-ro.yaml" ]; then
assert_pass "provider-file-exists"
else
assert_fail "provider-file-exists" "providers/jira-ro.yaml not found"
fi

if [ -f "${REPO_ROOT}/profiles/fullsend-jira-ro.yaml" ]; then
assert_pass "profile-file-exists"
else
assert_fail "profile-file-exists" "profiles/fullsend-jira-ro.yaml not found"
fi

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
if [[ ${FAILURES} -gt 0 ]]; then
echo "${FAILURES} test(s) failed"
exit 1
fi
echo "All harness Jira tests passed"
Loading
Loading