Skip to content

feat: make triage agent multi-forge (GitHub + GitLab) - #686

Open
ggallen wants to merge 1 commit into
fullsend-ai:mainfrom
ggallen:worktree-gitlab-triage-agent
Open

feat: make triage agent multi-forge (GitHub + GitLab)#686
ggallen wants to merge 1 commit into
fullsend-ai:mainfrom
ggallen:worktree-gitlab-triage-agent

Conversation

@ggallen

@ggallen ggallen commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Extract forge-specific operations from pre/post-triage scripts into sourced ops files (scripts/github/triage-ops.sh, scripts/gitlab/triage-ops.sh) with a shared forge_* function interface
  • Reorganize forge-specific files by platform: policies/{github,gitlab}/, env/{github,gitlab}/, skills/{github,gitlab}/, skills/issue-labels/{github,gitlab}/
  • Make agent prompt forge-neutral (ISSUE_URL, generic terminology, delegates CLI commands to forge skills)
  • Update schema to accept both GitHub and GitLab URL patterns
  • Add forge sections to harness config with per-forge policy, skills, host_files, and env vars

Dependencies

Test plan

  • All 112 tests pass (bash scripts/post-triage-test.sh) — 101 GitHub + 11 GitLab
  • Schema validates both GitHub and GitLab URL patterns
  • Existing GitHub behavior preserved (including auto-code config)
  • End-to-end validation on GitLab runner (pending openshell gateway setup)

🤖 Generated with Claude Code

Extract forge-specific operations from pre/post-triage scripts into
sourced ops files (scripts/github/triage-ops.sh, scripts/gitlab/triage-ops.sh)
with a shared forge_* function interface. The orchestrator scripts source the
correct ops file based on FULLSEND_FORGE at runtime.

Reorganize forge-specific files by platform:
- policies/github/, policies/gitlab/ for sandbox policies
- env/github/, env/gitlab/ for sandbox env files
- skills/github/, skills/gitlab/ for general forge CLI skills
- skills/issue-labels/github/, skills/issue-labels/gitlab/ for label skills

Make agent prompt forge-neutral (ISSUE_URL, generic terminology, delegates
CLI commands to forge skills). Update schema to accept both GitHub and GitLab
URL patterns. Add forge sections to harness config with per-forge policy,
skills, and env vars.

All 95 tests pass (84 GitHub + 11 new GitLab).

Signed-off-by: Greg Allen <gallen@redhat.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ggallen
ggallen requested a review from a team as a code owner August 6, 2026 01:07
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 6, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:08 AM UTC · Completed 1:26 AM UTC
Commit: e8850dd · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Make triage agent multi-forge (GitHub + GitLab)

✨ Enhancement ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add forge abstraction for triage mutations via FULLSEND_FORGE-selected ops functions.
• Split harness policy/env/skills configuration into per-forge sections for GitHub and GitLab.
• Extend schema and tests to validate GitLab issue/MR URL support and behavior parity.
Diagram

graph TD
  H["harness/triage.yaml"] --> Pre["pre-triage.sh"] --> Ops["forge triage-ops.sh"] --> API{{"GitHub/GitLab API"}}
  H --> Agent["agents/triage.md"] --> Skills["forge skills (gh/curl)"] --> API
  Agent --> Result[("agent-result.json")] --> Post["post-triage.sh"] --> Ops
  subgraph Legend
    direction LR
    _cfg["Config"] ~~~ _scr["Script"] ~~~ _data[("Data")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Single script with per-forge case statements
  • ➕ Keeps all behavior in one file; easier to diff GitHub vs GitLab handling
  • ➕ Avoids duplicated function definitions across ops files
  • ➖ Harder to test/maintain as forge surface area grows
  • ➖ Riskier changes: touching one forge can accidentally regress the other
2. Shared shell library + minimal forge adapters
  • ➕ Reduces duplication between GitHub and GitLab ops (common validation, logging, retries)
  • ➕ Provides a clearer contract for future forges (e.g., Bitbucket)
  • ➖ Requires more upfront factoring and careful boundary decisions
  • ➖ May complicate simple per-forge differences (e.g., GitLab label semantics)

Recommendation: The current approach (forge-specific ops files implementing a shared forge_* interface, sourced by pre/post scripts) is the best tradeoff for maintainability and risk isolation. If duplication between GitHub/GitLab ops grows, consider extracting a small shared helper library (logging/retry/JSON helpers) while keeping API semantics in per-forge adapters.

Files changed (16) +981 / -187 · 3 not counted

Enhancement (3) +323 / -3
triage-result.schema.jsonAccept GitLab URL formats and nested project paths in triage results +9/-3

Accept GitLab URL formats and nested project paths in triage results

• Expands URL validation to allow GitLab MR and issue URL patterns in pull_requests and prerequisites. Updates repo pattern to support GitLab subgroup paths (multiple segments).

schemas/triage-result.schema.json

triage-ops.shIntroduce GitHub forge ops (gh-based label/comment/issue actions) +119/-0

Introduce GitHub forge ops (gh-based label/comment/issue actions)

• Implements forge_validate_issue_url/forge_parse_issue_url and a forge_* API for labels, comments, sticky comments, closing, and issue creation using gh/fullsend.

scripts/github/triage-ops.sh

triage-ops.shIntroduce GitLab forge ops (curl-based label/comment/issue actions) +195/-0

Introduce GitLab forge ops (curl-based label/comment/issue actions)

• Implements the same forge_* interface for GitLab using curl against the REST API, including URL parsing, label updates, note posting with marker-based stickiness, closing, and issue creation.

scripts/gitlab/triage-ops.sh

Refactor (2) +70 / -117
post-triage.shRefactor post-triage mutations through forge ops abstraction +57/-91

Refactor post-triage mutations through forge ops abstraction

• Replaces GitHub-specific URL parsing and gh calls with forge_validate_issue_url/forge_parse_issue_url and forge_* operations. Ensures comments, labels, close, and create actions work consistently across GitHub and GitLab.

scripts/post-triage.sh

pre-triage.shRefactor pre-triage label reset through forge ops abstraction +13/-26

Refactor pre-triage label reset through forge ops abstraction

• Switches to ISSUE_URL/FULLSEND_FORGE and delegates label stripping/verification to forge_* functions so the same script can run on GitHub and GitLab.

scripts/pre-triage.sh

Tests (1) +240 / -1
post-triage-test.shAdd GitLab forge test coverage and switch tests to ISSUE_URL +240/-1

Add GitLab forge test coverage and switch tests to ISSUE_URL

• Updates GitHub tests to use ISSUE_URL/FULLSEND_FORGE and adds a new GitLab test suite with a mock curl to verify correct API calls and ensure gh is not invoked for GitLab runs.

scripts/post-triage-test.sh

Documentation (5) +262 / -57
triage.mdMake triage prompt forge-neutral (ISSUE_URL, PR/MR, CI paths) +29/-57

Make triage prompt forge-neutral (ISSUE_URL, PR/MR, CI paths)

• Replaces GitHub-specific language and inputs with a forge-neutral ISSUE_URL and generic terminology. Delegates data fetching/listing/searching commands to forge-specific skills, and broadens workflow-file guidance to include GitLab CI.

agents/triage.md

SKILL.mdAdd GitHub skill with canonical gh commands for triage +63/-0

Add GitHub skill with canonical gh commands for triage

• Documents standard gh commands for issue/PR retrieval, searches, and repo file reads, and shows how to derive REPO/ISSUE_NUMBER from ISSUE_URL.

skills/github/SKILL.md

SKILL.mdAdd GitLab skill with canonical curl API commands for triage +93/-0

Add GitLab skill with canonical curl API commands for triage

• Documents GitLab REST API usage via curl, including parsing ISSUE_URL into host/project/IID and commands for issues, notes, merge requests, and repo contents.

skills/gitlab/SKILL.md

SKILL.mdRelocate GitHub issue-labels skill under forge-specific path not counted

Relocate GitHub issue-labels skill under forge-specific path

• Keeps the GitHub issue-labels skill content available under skills/issue-labels/github/ for per-forge skill selection.

skills/issue-labels/github/SKILL.md

SKILL.mdAdd GitLab variant of issue-labels skill +77/-0

Add GitLab variant of issue-labels skill

• Adds GitLab-specific guidance for discovering labels and recommending label_actions using the GitLab API (projects/*/labels, issues listing).

skills/issue-labels/gitlab/SKILL.md

Other (5) +86 / -9
triage.envPlace GitHub triage env in forge-specific location not counted

Place GitHub triage env in forge-specific location

• Defines the GitHub env exports in env/github/triage.env for use by the GitHub forge harness section.

env/github/triage.env

triage.envAdd GitLab triage env file (issue URL + token) +2/-0

Add GitLab triage env file (issue URL + token)

• Introduces env/gitlab/triage.env exporting GITLAB_ISSUE_URL and GITLAB_TOKEN for GitLab runs.

env/gitlab/triage.env

triage.yamlAdd per-forge triage configuration for GitHub and GitLab +32/-9

Add per-forge triage configuration for GitHub and GitLab

• Moves policy/skills/host_files/env wiring under forge.github and forge.gitlab. Sets ISSUE_URL and FULLSEND_FORGE appropriately and attaches forge-specific policies and skills.

harness/triage.yaml

triage.yamlRelocate GitHub triage sandbox policy under policies/github/ not counted

Relocate GitHub triage sandbox policy under policies/github/

• Keeps the GitHub-specific sandbox policy in a forge-namespaced path to match the new harness layout.

policies/github/triage.yaml

triage.yamlAdd GitLab triage sandbox policy (curl + GitLab endpoints) +52/-0

Add GitLab triage sandbox policy (curl + GitLab endpoints)

• Adds a GitLab-specific sandbox policy that allows curl/node and GitLab API network access, while preserving Vertex AI access for inference.

policies/gitlab/triage.yaml

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (3)

Grey Divider


Action required

1. ISSUE_URL unsanitized in ::notice:: 📜 Skill insight ⛨ Security
Description
scripts/pre-triage.sh emits a GitHub Actions workflow command with an interpolated ISSUE_URL
that is not sanitized, enabling workflow-command injection via :: sequences, encoded newlines, or
control characters. This violates the requirement that every interpolated value in workflow commands
be sanitized individually.
Code

scripts/pre-triage.sh[22]

+echo "::notice::🔗 Triage target: ${ISSUE_URL}"
Relevance

●●● Strong

Strong precedent accepting sanitization of interpolated values in GitHub Actions workflow commands
to prevent injection.

PR-#592
PR-#573

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538382 requires sanitizing all interpolated variables in GitHub Actions workflow
commands. The updated pre-triage.sh emits ::notice::... while directly interpolating ISSUE_URL
with no sanitization step.

scripts/pre-triage.sh[18-24]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scripts/pre-triage.sh` prints a GitHub Actions workflow command (`::notice::...`) while interpolating `ISSUE_URL` without sanitization. This can allow workflow command injection if `ISSUE_URL` contains `::`, `%0A/%0D`, ANSI escapes, or other control characters.

## Issue Context
The repository already contains a `gha_echo` helper that sanitizes workflow command output in other scripts; `pre-triage.sh` should use the same (or equivalent) sanitization behavior.

## Fix Focus Areas
- scripts/pre-triage.sh[18-25]
- scripts/lib/post-failure-report.lib.sh[25-70]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Protected paths modified 📜 Skill insight § Compliance
Description
This PR modifies protected governance/infrastructure paths (including agents/, harness/,
policies/, scripts/, skills/, and schemas/), which must not be auto-approved and require
explicit human review. A protected-path finding is mandatory when these directories are changed.
Code

harness/triage.yaml[R35-43]

forge:
  github:
+    policy: policies/github/triage.yaml
    pre_script: scripts/pre-triage.sh
    post_script: scripts/post-triage.sh
+    skills:
+      - skills/github
+      - skills/issue-labels/github
+    host_files:
Relevance

●●● Strong

Protected-path governance findings are expected on harness/infra edits; similar governance notes
appear historically.

PR-#631
PR-#476

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538392 requires raising a finding whenever protected governance/infrastructure
paths are modified. The diff includes changes to protected configuration (harness/triage.yaml) and
related pipeline scripts/policies.

harness/triage.yaml[35-75]
scripts/pre-triage.sh[1-34]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Protected governance/infrastructure paths are modified in this PR. Per compliance, such PRs must not be auto-approved and should have explicit authorization context.

## Issue Context
Protected paths include agent definitions, harness configs, policies, scripts, skills, and schemas—changes here impact pipeline behavior and security boundaries.

## Fix Focus Areas
- harness/triage.yaml[35-75]
- scripts/pre-triage.sh[1-34]
- scripts/post-triage.sh[1-30]
- policies/gitlab/triage.yaml[1-52]
- schemas/triage-result.schema.json[39-81]
- agents/triage.md[1-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. GitLab policy lacks justification 📜 Skill insight ⛨ Security
Description
The new policies/gitlab/triage.yaml expands sandbox/network permissions (e.g., *.googleapis.com,
gitlab.com, gitlab.cee.redhat.com, allowed binaries) without an explicit linked issue/ADR
justification in the policy itself. Permission expansions without explicit authorization and
least-privilege justification violate the compliance requirement.
Code

policies/gitlab/triage.yaml[R28-32]

+      - host: "*.googleapis.com"
+        port: 443
+        protocol: rest
+        enforcement: enforce
+        access: read-write
Relevance

●● Moderate

Permission expansion is security-sensitive, but no clear precedent requiring in-file issue/ADR
justification for policies.

PR-#78

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538316 requires least-privilege permission changes with explicit justification.
The new GitLab policy declares broad network endpoints (including wildcard *.googleapis.com) and
allowed binaries, but contains no linked issue/ADR authorizing these permissions.

policies/gitlab/triage.yaml[19-52]
Skill: code-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new permission-declaring policy file is introduced for the GitLab triage forge, granting network access and binary allowances. The change lacks an explicit linked issue/ADR justification and least-privilege rationale alongside the permissions.

## Issue Context
Compliance requires permission expansions (policy manifests, RBAC, workflow permissions, etc.) to be least-privilege and explicitly justified with an authorizing issue/ADR.

## Fix Focus Areas
- policies/gitlab/triage.yaml[1-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. GitLab sticky comment duplicates 🐞 Bug ☼ Reliability
Description
GitLab forge_post_sticky_comment only fetches the first 100 notes sorted ascending when searching
for an existing marker, so on issues with >100 notes it can miss the marker and POST a new note
instead of updating. This can repeatedly spam issues with duplicate “sticky” triage comments on
re-triage.
Code

scripts/gitlab/triage-ops.sh[R158-161]

+  notes=$(_gitlab_api GET "/projects/${REPO}/issues/${ISSUE_NUMBER}/notes?per_page=100&sort=asc" 2>/dev/null) || notes="[]"
+
+  local note_id
+  note_id=$(echo "${notes}" | jq -r --arg marker "${marker}" \
Relevance

●●● Strong

Concrete reliability bug causing duplicate sticky comments; team has accepted fixes preventing
repeated/looping issue mutations.

PR-#326

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The function only requests one page (per_page=100) and uses sort=asc; if the marker note isn’t
in that first page, it will always execute the fallback POST branch, producing duplicates.

scripts/gitlab/triage-ops.sh[150-170]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`forge_post_sticky_comment` queries `notes?per_page=100&sort=asc` once and searches only that response for the marker. If the marker note isn’t in the first page, the script falls back to POSTing a new note, creating duplicates.

## Issue Context
Because results are sorted ascending, page 1 is the oldest notes. A marker note created later (or after many existing notes) may fall outside the first 100 and will never be found.

## Fix Focus Areas
- scripts/gitlab/triage-ops.sh[150-170]

## What to change
- Either:
 - Fetch notes sorted by newest first (`sort=desc`) so the marker is likely on page 1, and still paginate if not found.
- Or:
 - Implement pagination over notes (`page=1..N`) until the marker is found or pages are exhausted.
- Keep the update-in-place behavior by PUTing the found note ID; only POST when you’re sure no marker exists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. GitLab label discovery incomplete 🐞 Bug ≡ Correctness
Description
forge_list_repo_labels on GitLab fetches only /labels?per_page=100 with no pagination, so projects
with more than 100 labels will have missing entries. post-triage.sh then incorrectly treats some
real labels as nonexistent and refuses valid label_actions.
Code

scripts/gitlab/triage-ops.sh[R128-130]

+forge_list_repo_labels() {
+  _gitlab_api GET "/projects/${REPO}/labels?per_page=100" 2>/dev/null | jq -r '.[].name' || true
+}
Relevance

●●● Strong

Concrete pagination correctness bug; similar script correctness/reliability fixes are usually
accepted.

PR-#567

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
GitLab label listing is a single request capped at 100, while post-triage relies on the returned
list to decide whether to apply or refuse label_actions.

scripts/gitlab/triage-ops.sh[128-130]
scripts/post-triage.sh[512-545]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On GitLab, `forge_list_repo_labels` fetches only the first page of labels (100 max). The post-triage label_actions gate uses this as the authoritative label set, so labels beyond page 1 are incorrectly rejected.

## Issue Context
GitHub uses `--paginate` for labels; GitLab needs equivalent pagination (via `page=` and response headers).

## Fix Focus Areas
- scripts/gitlab/triage-ops.sh[128-130]
- scripts/post-triage.sh[512-545]

## What to change
- Implement a pagination loop in `forge_list_repo_labels`, e.g.:
 - loop `page=1..` until an empty array is returned
 - concatenate names across pages
- Alternatively, query label existence by name per action (slower, but avoids full pagination).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Repo schema mismatches GitHub 🐞 Bug ≡ Correctness
Description
The schema now allows prerequisites.create.repo values with 3+ path segments, but GitHub
forge_create_issue passes repo directly to gh issue create --repo, which expects OWNER/REPO.
This allows schema-valid outputs that deterministically fail prerequisite issue creation on GitHub
if a multi-segment repo is produced.
Code

schemas/triage-result.schema.json[R80-81]

+                "pattern": "^[a-zA-Z0-9._-]+(/[a-zA-Z0-9._-]+)+$"
              },
Relevance

●●● Strong

Deterministic schema/runtime mismatch; repo has accepted schema consistency/correctness fixes
previously.

PR-#622

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The schema explicitly permits nested repo paths, while the GitHub forge implementation forwards the
repo string directly to gh’s --repo flag and post-triage uses it without GitHub-specific
validation.

schemas/triage-result.schema.json[71-82]
scripts/post-triage.sh[199-201]
scripts/github/triage-ops.sh[114-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`prerequisites.create[].repo` is validated by a shared schema that now permits nested paths (needed for GitLab), but the GitHub implementation expects exactly one slash for `--repo OWNER/REPO`.

## Issue Context
This is a contract mismatch between validation (schema) and execution (GitHub ops). The post-triage script calls `forge_create_issue` with `TARGET_REPO` without additional GitHub-specific validation.

## Fix Focus Areas
- schemas/triage-result.schema.json[71-82]
- scripts/github/triage-ops.sh[114-119]
- scripts/post-triage.sh[199-201]

## What to change
- Option A (recommended): keep the schema broad for cross-forge, but add GitHub-side validation in `scripts/github/triage-ops.sh::forge_create_issue`:
 - reject repo values that don’t match `^[^/]+/[^/]+$` and return a clear error.
- Option B: change the schema field to `anyOf`:
 - GitHub repo pattern (`owner/repo`)
 - GitLab project path pattern (`group(/subgroup)+/project`)
 - and additionally validate at runtime based on `FULLSEND_FORGE`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
7. Unvalidated forge ops sourcing 🐞 Bug ⛨ Security
Description
pre-triage.sh and post-triage.sh source the ops script via an unvalidated FULLSEND_FORGE path, so a
traversal value (e.g., "../..") could cause unintended code execution on the runner if
FULLSEND_FORGE is ever influenced outside the trusted harness. This is a runner trust-boundary
weakness because sourcing executes arbitrary bash from the resolved path.
Code

scripts/post-triage.sh[R22-24]

+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck source=/dev/null
+source "${SCRIPT_DIR}/${FULLSEND_FORGE}/triage-ops.sh"
Relevance

●● Moderate

Traversal risk depends on trust boundary for FULLSEND_FORGE; no close allowlist/validation precedent
found.

PR-#415

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both host scripts construct the sourced file path using ${FULLSEND_FORGE} with no allowlist
validation, so traversal values could resolve outside the intended scripts/{github,gitlab}/
directories if the env var were ever set unexpectedly.

scripts/post-triage.sh[20-25]
scripts/pre-triage.sh[16-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`post-triage.sh` and `pre-triage.sh` build a `source` path from `$FULLSEND_FORGE` without validating it. Although the variable is quoted (preventing shell metacharacter injection), path traversal (e.g., `../..`) can still resolve to an unintended file.

## Issue Context
This code runs on the host/runner (not the sandbox). Today the harness sets `FULLSEND_FORGE` to fixed values, but the scripts should defensively enforce the trust boundary.

## Fix Focus Areas
- scripts/post-triage.sh[20-25]
- scripts/pre-triage.sh[16-21]

## What to change
- Add an explicit allowlist:
 - `case "${FULLSEND_FORGE}" in github|gitlab) ;; *) echo "ERROR: invalid FULLSEND_FORGE"; exit 1 ;; esac`
- Optionally map forge->path without interpolation (e.g., set `OPS_FILE` in the `case` and `source "$OPS_FILE"`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread scripts/pre-triage.sh
echo "ERROR: GITHUB_ISSUE_URL does not match expected pattern: ${GITHUB_ISSUE_URL}"
exit 1
fi
echo "::notice::🔗 Triage target: ${ISSUE_URL}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. issue_url unsanitized in ::notice:: 📜 Skill insight ⛨ Security

scripts/pre-triage.sh emits a GitHub Actions workflow command with an interpolated ISSUE_URL
that is not sanitized, enabling workflow-command injection via :: sequences, encoded newlines, or
control characters. This violates the requirement that every interpolated value in workflow commands
be sanitized individually.
Agent Prompt
## Issue description
`scripts/pre-triage.sh` prints a GitHub Actions workflow command (`::notice::...`) while interpolating `ISSUE_URL` without sanitization. This can allow workflow command injection if `ISSUE_URL` contains `::`, `%0A/%0D`, ANSI escapes, or other control characters.

## Issue Context
The repository already contains a `gha_echo` helper that sanitizes workflow command output in other scripts; `pre-triage.sh` should use the same (or equivalent) sanitization behavior.

## Fix Focus Areas
- scripts/pre-triage.sh[18-25]
- scripts/lib/post-failure-report.lib.sh[25-70]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +28 to +32
- host: "*.googleapis.com"
port: 443
protocol: rest
enforcement: enforce
access: read-write

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Gitlab policy lacks justification 📜 Skill insight ⛨ Security

The new policies/gitlab/triage.yaml expands sandbox/network permissions (e.g., *.googleapis.com,
gitlab.com, gitlab.cee.redhat.com, allowed binaries) without an explicit linked issue/ADR
justification in the policy itself. Permission expansions without explicit authorization and
least-privilege justification violate the compliance requirement.
Agent Prompt
## Issue description
A new permission-declaring policy file is introduced for the GitLab triage forge, granting network access and binary allowances. The change lacks an explicit linked issue/ADR justification and least-privilege rationale alongside the permissions.

## Issue Context
Compliance requires permission expansions (policy manifests, RBAC, workflow permissions, etc.) to be least-privilege and explicitly justified with an authorizing issue/ADR.

## Fix Focus Areas
- policies/gitlab/triage.yaml[1-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/triage.yaml
Comment on lines 35 to +43
forge:
github:
policy: policies/github/triage.yaml
pre_script: scripts/pre-triage.sh
post_script: scripts/post-triage.sh
skills:
- skills/github
- skills/issue-labels/github
host_files:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Protected paths modified 📜 Skill insight § Compliance

This PR modifies protected governance/infrastructure paths (including agents/, harness/,
policies/, scripts/, skills/, and schemas/), which must not be auto-approved and require
explicit human review. A protected-path finding is mandatory when these directories are changed.
Agent Prompt
## Issue description
Protected governance/infrastructure paths are modified in this PR. Per compliance, such PRs must not be auto-approved and should have explicit authorization context.

## Issue Context
Protected paths include agent definitions, harness configs, policies, scripts, skills, and schemas—changes here impact pipeline behavior and security boundaries.

## Fix Focus Areas
- harness/triage.yaml[35-75]
- scripts/pre-triage.sh[1-34]
- scripts/post-triage.sh[1-30]
- policies/gitlab/triage.yaml[1-52]
- schemas/triage-result.schema.json[39-81]
- agents/triage.md[1-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread scripts/post-triage.sh
Comment on lines +22 to +24
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/${FULLSEND_FORGE}/triage-ops.sh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Unvalidated forge ops sourcing 🐞 Bug ⛨ Security

pre-triage.sh and post-triage.sh source the ops script via an unvalidated FULLSEND_FORGE path, so a
traversal value (e.g., "../..") could cause unintended code execution on the runner if
FULLSEND_FORGE is ever influenced outside the trusted harness. This is a runner trust-boundary
weakness because sourcing executes arbitrary bash from the resolved path.
Agent Prompt
## Issue description
`post-triage.sh` and `pre-triage.sh` build a `source` path from `$FULLSEND_FORGE` without validating it. Although the variable is quoted (preventing shell metacharacter injection), path traversal (e.g., `../..`) can still resolve to an unintended file.

## Issue Context
This code runs on the host/runner (not the sandbox). Today the harness sets `FULLSEND_FORGE` to fixed values, but the scripts should defensively enforce the trust boundary.

## Fix Focus Areas
- scripts/post-triage.sh[20-25]
- scripts/pre-triage.sh[16-21]

## What to change
- Add an explicit allowlist:
  - `case "${FULLSEND_FORGE}" in github|gitlab) ;; *) echo "ERROR: invalid FULLSEND_FORGE"; exit 1 ;; esac`
- Optionally map forge->path without interpolation (e.g., set `OPS_FILE` in the `case` and `source "$OPS_FILE"`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +158 to +161
notes=$(_gitlab_api GET "/projects/${REPO}/issues/${ISSUE_NUMBER}/notes?per_page=100&sort=asc" 2>/dev/null) || notes="[]"

local note_id
note_id=$(echo "${notes}" | jq -r --arg marker "${marker}" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Gitlab sticky comment duplicates 🐞 Bug ☼ Reliability

GitLab forge_post_sticky_comment only fetches the first 100 notes sorted ascending when searching
for an existing marker, so on issues with >100 notes it can miss the marker and POST a new note
instead of updating. This can repeatedly spam issues with duplicate “sticky” triage comments on
re-triage.
Agent Prompt
## Issue description
`forge_post_sticky_comment` queries `notes?per_page=100&sort=asc` once and searches only that response for the marker. If the marker note isn’t in the first page, the script falls back to POSTing a new note, creating duplicates.

## Issue Context
Because results are sorted ascending, page 1 is the oldest notes. A marker note created later (or after many existing notes) may fall outside the first 100 and will never be found.

## Fix Focus Areas
- scripts/gitlab/triage-ops.sh[150-170]

## What to change
- Either:
  - Fetch notes sorted by newest first (`sort=desc`) so the marker is likely on page 1, and still paginate if not found.
- Or:
  - Implement pagination over notes (`page=1..N`) until the marker is found or pages are exhausted.
- Keep the update-in-place behavior by PUTing the found note ID; only POST when you’re sure no marker exists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +128 to +130
forge_list_repo_labels() {
_gitlab_api GET "/projects/${REPO}/labels?per_page=100" 2>/dev/null | jq -r '.[].name' || true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Gitlab label discovery incomplete 🐞 Bug ≡ Correctness

forge_list_repo_labels on GitLab fetches only /labels?per_page=100 with no pagination, so projects
with more than 100 labels will have missing entries. post-triage.sh then incorrectly treats some
real labels as nonexistent and refuses valid label_actions.
Agent Prompt
## Issue description
On GitLab, `forge_list_repo_labels` fetches only the first page of labels (100 max). The post-triage label_actions gate uses this as the authoritative label set, so labels beyond page 1 are incorrectly rejected.

## Issue Context
GitHub uses `--paginate` for labels; GitLab needs equivalent pagination (via `page=` and response headers).

## Fix Focus Areas
- scripts/gitlab/triage-ops.sh[128-130]
- scripts/post-triage.sh[512-545]

## What to change
- Implement a pagination loop in `forge_list_repo_labels`, e.g.:
  - loop `page=1..` until an empty array is returned
  - concatenate names across pages
- Alternatively, query label existence by name per action (slower, but avoids full pagination).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +80 to 81
"pattern": "^[a-zA-Z0-9._-]+(/[a-zA-Z0-9._-]+)+$"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Repo schema mismatches github 🐞 Bug ≡ Correctness

The schema now allows prerequisites.create.repo values with 3+ path segments, but GitHub
forge_create_issue passes repo directly to gh issue create --repo, which expects OWNER/REPO.
This allows schema-valid outputs that deterministically fail prerequisite issue creation on GitHub
if a multi-segment repo is produced.
Agent Prompt
## Issue description
`prerequisites.create[].repo` is validated by a shared schema that now permits nested paths (needed for GitLab), but the GitHub implementation expects exactly one slash for `--repo OWNER/REPO`.

## Issue Context
This is a contract mismatch between validation (schema) and execution (GitHub ops). The post-triage script calls `forge_create_issue` with `TARGET_REPO` without additional GitHub-specific validation.

## Fix Focus Areas
- schemas/triage-result.schema.json[71-82]
- scripts/github/triage-ops.sh[114-119]
- scripts/post-triage.sh[199-201]

## What to change
- Option A (recommended): keep the schema broad for cross-forge, but add GitHub-side validation in `scripts/github/triage-ops.sh::forge_create_issue`:
  - reject repo values that don’t match `^[^/]+/[^/]+$` and return a clear error.
- Option B: change the schema field to `anyOf`:
  - GitHub repo pattern (`owner/repo`)
  - GitLab project path pattern (`group(/subgroup)+/project`)
  - and additionally validate at runtime based on `FULLSEND_FORGE`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] Multiple protected paths — 13 of 16 changed files are under protected paths (agents/, harness/, policies/, scripts/, skills/). This PR has no linked issue providing authorization for modifying governance and infrastructure files. Human approval is always required for protected-path changes.
    Affected files: agents/triage.md, harness/triage.yaml, policies/github/triage.yaml, policies/gitlab/triage.yaml, scripts/github/triage-ops.sh, scripts/gitlab/triage-ops.sh, scripts/post-triage-test.sh, scripts/post-triage.sh, scripts/pre-triage.sh, skills/github/SKILL.md, skills/gitlab/SKILL.md, skills/issue-labels/github/SKILL.md, skills/issue-labels/gitlab/SKILL.md

  • [stale-doc] README.md:3 — README describes the project as automating the lifecycle "on GitHub," but the triage agent now supports both GitHub and GitLab.
    Remediation: Update to mention both forges or use "supported forges."

Medium

  • [command-injection-path-traversal] scripts/pre-triage.sh:8, scripts/post-triage.shFULLSEND_FORGE is used as a path component in source "${SCRIPT_DIR}/${FULLSEND_FORGE}/triage-ops.sh" without validating it is one of the expected values ("github" or "gitlab"). Defense-in-depth: add an allowlist check before the source line in both scripts.

  • [CI-coverage-regression] .github/scripts/select-eval-agents.sh:36extract_refs reads top-level policy, skills, and host_files from harness YAML but this PR moved those into forge-specific sections. Changes to policies/github/, skills/github/, env/github/, and all GitLab equivalents will no longer trigger triage eval tests in CI.
    Remediation: Update extract_refs to extract forge-specific paths (.forge[][].policy, .forge[][].skills[], etc.) and update the test fixture.

  • [harness-config-structure] harness/triage.yaml — Structural change from flat top-level fields (policy, skills, host_files) to forge-specific nested sections. The fullsend CLI was updated in coordinated PRs (#5858, #5918), but the CI path-extraction script and test fixtures were not updated.

  • [stale-doc] docs/triage.md:5 — Still describes the triage agent as "Inspects a GitHub issue." Should be forge-neutral.
    Remediation: Change to "Inspects an issue."

Low

  • [jq-filter-injection] scripts/github/triage-ops.sh:72forge_verify_labels_stripped interpolates label names directly into a jq filter. Currently hardcoded inputs only, but not defensive against future callers.
  • [jq-filter-injection] scripts/gitlab/triage-ops.sh:89forge_remove_label splices label names via single-quote boundary. Caller-side regex validation mitigates.
  • [skill-name-mismatch] skills/issue-labels/gitlab/SKILL.md:2 — GitLab variant has name: issue-labels-gitlab while GitHub variant has name: issue-labels. Harness resolves by path so not a runtime bug, but naming is asymmetric.
  • [design-coherence] harness/triage.yaml — Top-level pre_script/post_script are redundant with identical entries in each forge section.
  • [network-policy-scope] policies/gitlab/triage.yaml:39 — Hardcodes gitlab.cee.redhat.com alongside gitlab.com without documented justification.
  • [test-fixture-staleness] .github/scripts/select-eval-agents-test.sh:34 — Test fixture still uses old harness structure that no longer matches triage.yaml.
  • [race-condition] scripts/gitlab/triage-ops.sh:41 — GitLab label ops use read-modify-write (GET→compute→PUT), unlike GitHub's per-label POST/DELETE. Currently serial, but fragile.
  • [stale-doc] eval/triage/eval.yaml — References "GitHub issue" specifically; should be updated for multi-forge.
  • [schema-permissiveness] schemas/triage-result.schema.jsonrepo pattern now accepts multi-segment paths beyond what either platform requires. Intentional but unbounded.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread scripts/pre-triage.sh
@@ -6,42 +6,29 @@
# mutual-exclusion violations (Story 2, #125).
#
# Required env vars:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] command-injection-path-traversal

FULLSEND_FORGE env var is used as a path component in source "${SCRIPT_DIR}/${FULLSEND_FORGE}/triage-ops.sh" without validating it is one of the expected values ('github' or 'gitlab'). Same pattern in post-triage.sh. While harness-controlled, lacks defense-in-depth validation.

Suggested fix: Add allowlist check before the source line: if FULLSEND_FORGE is not 'github' or 'gitlab', exit with error. Apply to both pre-triage.sh and post-triage.sh.

remaining=$(gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/labels" --jq "${jq_filter}" 2>/dev/null || echo "VERIFY_FAILED")

if [[ "${remaining}" == "VERIFY_FAILED" ]]; then
echo "ERROR: cannot verify label state — API call failed" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] jq-filter-injection

In forge_verify_labels_stripped, label names are interpolated directly into a jq filter string via bash string concatenation. Currently called only with hardcoded label constants, but the function is not defensive against future callers with untrusted input.

Suggested fix: Use jq --arg or --argjson to pass label names safely.

if [[ -n "${filtered}" ]]; then
filtered="${filtered},${current}"
else
filtered="${current}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] jq-filter-injection

In forge_remove_label, label name is spliced into a jq expression via single-quote boundary. Currently mitigated by caller-side regex validation in post-triage.sh, but the function itself is not defensive.

Suggested fix: Use jq --arg to safely parameterize the label value.

@@ -0,0 +1,77 @@
---
name: issue-labels-gitlab

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] skill-name-mismatch

GitLab variant has name: issue-labels-gitlab in frontmatter while GitHub variant has name: issue-labels. The harness injects skills by directory path (not by name), so this is not a runtime bug, but the naming asymmetry is inconsistent and could confuse future contributors.

Suggested fix: Standardize both variants to use the same name (issue-labels) or both to use forge-suffixed names.


gitlab_api:
name: gitlab-api
endpoints:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] network-policy-scope

The GitLab network policy hardcodes gitlab.cee.redhat.com alongside gitlab.com without documented justification or linked issue. This embeds a customer-specific endpoint in a general-purpose policy.

Suggested fix: Consider moving to a customer-specific policy overlay, or add a comment documenting the justification.

# Extract host, project path, and issue IID from URL.
# e.g., https://gitlab.com/group/subgroup/project/-/issues/42
GITLAB_HOST=$(echo "${ISSUE_URL}" | sed -E 's|^https://([^/]+)/.*|\1|')
local project_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] race-condition

GitLab forge_add_label and forge_remove_label use read-modify-write patterns (GET current labels, compute new set, PUT). Concurrent mutations could silently overwrite each other. GitHub implementation avoids this with dedicated POST/DELETE endpoints per label. Currently mitigated by serial execution in post-triage.sh.

Comment thread env/gitlab/triage.env
@@ -0,0 +1,2 @@
export GITLAB_ISSUE_URL="${GITLAB_ISSUE_URL}"
export GITLAB_TOKEN="${GITLAB_TOKEN}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The gitlab token shouldn't be in an env var, it should be part of an openshell provider that hosts the gitlab token so the credential doesn't enter the openshell sandbox but it's filled by the provider when the sandbox queries gitlab

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Additional finding not anchorable to a line in this diff (file untouched by this PR):

CRITICAL: skill reorg leaves the unrelated review agent's issue-labels skill pointing at a directory with no SKILL.md

harness/review.yaml:16 — This PR moves skills/issue-labels/SKILL.md to skills/issue-labels/github/SKILL.md and adds a sibling skills/issue-labels/gitlab/SKILL.md; skills/issue-labels/ now contains only the github/ and gitlab/ subdirectories, with no SKILL.md directly under it. harness/triage.yaml was correctly updated to the new per-forge paths, but harness/review.yaml (not touched by this PR) still lists the old top-level skills/issue-labels path, and agents/review.md's frontmatter still declares skills: - issue-labels unchanged. The review agent is single-forge/GitHub-only and unrelated to this PR's triage work but shares this skill; after merge, its issue-labels skill mount resolves to a directory with no SKILL.md, silently breaking (or failing to load) its label recommendations.

Suggestion: update harness/review.yaml to reference skills/issue-labels/github (and update agents/review.md's frontmatter to match), or restore a top-level skills/issue-labels/SKILL.md that re-exports/dispatches to the forge-specific variant for backward compatibility. Grep the repo for every consumer of a renamed skill path before merging a skill reorg.

exit 0
CURLMOCK
# Fix the log path placeholder.
sed -i "s|CURL_LOG_PLACEHOLDER|${CURL_LOG}|" "${MOCK_BIN}/curl"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

CRITICAL: GNU-only sed -i here aborts the whole test run on macOS/BSD sed

This line is sed -i "s|CURL_LOG_PLACEHOLDER|${CURL_LOG}|" "${MOCK_BIN}/curl". GNU sed accepts a bare -i, but BSD/macOS sed requires an explicit (possibly empty) backup-suffix argument. Combined with set -euo pipefail at the top of this script, this line errors out on macOS and kills the entire test run right after the last GitHub test — before any of the new run_gitlab_test* invocations execute. That directly contradicts the PR description's claim that all 112 tests (101 GitHub + 11 GitLab) pass.

Suggestion: avoid sed -i for this single-word substitution — write the mock curl script with ${CURL_LOG} already expanded via a heredoc (dropping the placeholder+sed step), or use perl -pi -e, or the portable sed -i.bak ... && rm -f file.bak form that works on both GNU and BSD sed.

GITLAB_HOST=$(echo "${ISSUE_URL}" | sed -E 's|^https://([^/]+)/.*|\1|')
local project_path
project_path=$(echo "${ISSUE_URL}" | sed -E 's|^https://[^/]+/(.+)/-/issues/[0-9]+$|\1|')
REPO=$(printf '%s' "${project_path}" | jq -sRr @uri)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

HIGH: REPO is URL-encoded here, but post-triage.sh's same-repo allowlist check compares it against a plain path

forge_parse_issue_url sets REPO=$(printf '%s' "${project_path}" | jq -sRr @uri) — URL-encoded, e.g. group%2Fproject. In scripts/post-triage.sh, is_target_allowed() has a "source repo is always allowed" fallback: if [[ "${target_repo}" == "${REPO}" ]]; then return 0; fi. target_repo there comes from the agent's JSON as a plain path like group/project. "group/project" == "group%2Fproject" is always false, so on GitLab a triage recommendation to create a prerequisite issue in the very repo being triaged is always rejected as "not in create_issues.allow_targets" unless separately whitelisted. Note that forge_create_issue below (line ~186) correctly re-encodes its own local encoded_target before use — showing URL-encoding is the deliberate convention that the plain string-equality check in post-triage.sh violates. No GitLab test exercises the prerequisites/create-issue path, so this is untested.

Suggestion: keep REPO in a forge-neutral, human-readable form (e.g. group/project) for comparison/logging on both forges, and URL-encode only at the point of building the GitLab API URL (as forge_create_issue's local encoded_target already does). Add a GitLab test for the prerequisites/create action targeting the source repo.

Comment thread harness/triage.yaml
doc: docs/triage.md
model: opus
image: ghcr.io/fullsend-ai/fullsend-sandbox:latest
policy: policies/triage.yaml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

HIGH: removing all top-level fallbacks means any invocation that doesn't resolve a forge silently loses policy/skills/env, then crashes on an unbound var

This PR removes this top-level policy: line (and, further down, the top-level skills:/host_files:/issue-token env: entries), moving everything exclusively under forge.github/forge.gitlab. The fullsend CLI's detectForgePlatform returns an empty forge with no error when neither --forge nor GITHUB_ACTIONS/GITLAB_CI env vars are set, and Harness.ResolveForge("") is a documented no-op that leaves Policy/Skills/HostFiles/Env at their top-level values — which are now all empty except the unrelated TRIAGE_AUTO_CODE* vars. Harness.Validate() doesn't require a non-empty Policy, so fullsend run proceeds anyway, and pre-triage.sh (set -euo pipefail) then dies with an unbound-variable error on source "${SCRIPT_DIR}/${FULLSEND_FORGE}/triage-ops.sh".

This is a real, distinct failure mode from the existing CI-path-extraction comments (select-eval-agents.sh) — this is about a runtime crash/silent config loss when the forge can't be resolved at all, e.g. from eval/scripts/run-fullsend.sh, which invokes fullsend run with no --forge flag and doesn't export GITHUB_ACTIONS (it only "works" today by accident when actually run inside a real GH Actions job).

Suggestion: either pass --forge github explicitly from the eval runner (it's GitHub-only today) or export GITHUB_ACTIONS=true for that invocation, and more broadly, have ResolveForge (or the pre-script) fail with a clear, actionable error when no forge resolves, instead of silently proceeding with an empty policy/env.

Comment thread agents/triage.md
skills:
- issue-labels
tools: Bash(gh,jq)
tools: Bash(gh,curl,jq)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

HIGH: shared tools: Bash(gh,curl,jq) grant now lets the GitHub-forge agent invoke curl, contradicting the documented sandbox hardening rationale

This frontmatter line applies to both forges — there's no per-forge tool-scoping mechanism in this format. policies/github/triage.yaml (unchanged by this PR) carries the explicit comment "curl excluded from the binary allowlist to prevent raw HTTP access with the injected GH_TOKEN", and its github_api network_policy binaries list still only permits **/gh and **/node. So the network-layer control is technically still in place, but the tool-level allowlist that previously made curl uninvocable by the agent at all on GitHub now permits it, relying entirely on the (best-effort, per landlock: compatibility: best_effort) network-policy layer as the sole remaining control. This is a real narrowing of defense-in-depth introduced solely to accommodate GitLab's curl need.

Suggestion: if the harness format supports it, scope curl to the GitLab forge only (per-forge tool override). If not, at minimum add a comment next to this tools: line and in policies/github/triage.yaml acknowledging curl is now permitted at the tool layer and confirming the network-policy layer is verified to still block it for the GH_TOKEN-bearing GitHub sandbox.

local response
response=$(_gitlab_api POST "/projects/${encoded_target}/issues" \
--data-urlencode "title=${title}" \
--data-urlencode "description=${body}") || {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: forge_create_issue swallows curl's actual error detail on failure, producing an empty warning message

_gitlab_api runs curl --fail --silent --show-error .... With --fail, curl discards the HTTP response body from stdout on a >=400 status, and --show-error only prints to stderr, which the command substitution response=$(_gitlab_api POST ...) here does not capture. On failure, response is empty, so echo "${response}"; return 1 propagates nothing useful. The caller in scripts/post-triage.sh (CREATED_URL=$(forge_create_issue ...) || { echo "::warning::Failed to create issue in '${TARGET_REPO}': ${CREATED_URL}"; ...}) ends up posting a warning with an empty error suffix, unlike the GitHub path which captures gh issue create ... 2>&1 and preserves the real CLI error text in the same user-facing message.

Suggestion: capture stderr too (e.g. _gitlab_api POST ... 2>&1 and drop --fail, checking HTTP status separately, or use curl -w '\n%{http_code}') so failures surface an actionable error message instead of an empty string.

"pattern": "^https://github\\.com/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+/pull/[0-9]+$"
"anyOf": [
{ "pattern": "^https://github\\.com/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+/pull/[0-9]+$" },
{ "pattern": "^https://[a-zA-Z0-9._-]+/.+/-/merge_requests/[0-9]+$" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: GitLab issue/MR URL schema pattern is far looser than its GitHub sibling and the runtime validator, and gets echoed verbatim into public comments

This GitLab anyOf pattern (and the sibling one for prerequisites.existing[].url) uses an unconstrained .+ for the project-path segment: ^https://[a-zA-Z0-9._-]+/.+/-/(merge_requests|issues)/[0-9]+$. This is looser than both the GitHub sibling pattern on the same lines ([a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+) and the hand-written runtime validator forge_validate_issue_url in scripts/gitlab/triage-ops.sh, which requires (/[a-zA-Z0-9._-]+)+. This is a separate field/issue from the existing comment about the prerequisites.create.repo pattern below — that one concerns the repo-only field and its mismatch with GitHub's gh issue create --repo, not this URL field's looseness. A schema-valid-but-validator-rejecting URL (containing spaces, brackets, extra segments) would still be interpolated verbatim into the public issue/MR comment body in post-triage.sh (**Addressed by:**\n- ${url}), since the schema is the only guardrail on shape before posting.

Suggestion: tighten the GitLab URL patterns to mirror the character restrictions already used in forge_validate_issue_url, e.g. ^https://[a-zA-Z0-9._-]+(/[a-zA-Z0-9._-]+)+/-/(issues|merge_requests)/[0-9]+$.

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.

3 participants