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
53 changes: 50 additions & 3 deletions .claude/skills/test-e2e-openshell/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Verify:
- Output shows `Created sandbox: ci`
- Output shows `Running Claude Code (claude-haiku-4-5) via openshell backend`
- Claude's response contains `A1_OK`
- Token metrics show non-zero counts, cost around `$0.04`
- Token metrics show non-zero counts and cost is non-zero (e.g. `$0.04`)
- `Agent exit code: 0`
- `Sandbox deleted` and `Gateway stopped` at the end

Expand Down Expand Up @@ -174,7 +174,7 @@ Verify:
- Output shows `Auth: API key`
- Output shows `Creating Anthropic API key provider`
- Claude's response contains `B1_OK`
- Token metrics show non-zero counts and cost
- Token metrics show non-zero counts and cost is non-zero
- `Agent exit code: 0`

---
Expand Down Expand Up @@ -311,6 +311,53 @@ podman exec openshell-e2e rm -rf /tmp/workdir-test

---

## Section F: OTEL telemetry collection

Verifies that the sandbox-local OTEL collector receives metrics from the
agent and prints a token/cost summary. Uses Vertex AI auth and Claude Code
(the only harness that supports OTEL).

The OpenShell sandbox network isolation prevents reaching an external OTEL
collector, so agentic-ci embeds a lightweight OTLP receiver inside the
sandbox on localhost. After the run, the OTEL log is downloaded from the
sandbox and the summary is printed on the host.

Requires `OPENSHELL_SUPERVISOR_IMAGE` (see "Before you start").

Run cleanup first.

### F1. Run with OTEL enabled

```bash
podman exec \
-e ANTHROPIC_VERTEX_PROJECT_ID=<your-project-id> \
-e CLOUD_ML_REGION=global \
-e OPENSHELL_SUPERVISOR_IMAGE=quay.io/mprpic/openshell-supervisor:pr1763 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if this supervisor image reference exists in other files or documentation
rg -n "openshell-supervisor:pr1763" --type md

Repository: opendatahub-io/agentic-ci

Length of output: 265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".claude/skills/test-e2e-openshell/SKILL.md"
echo "== Context around /tmp/e2e-workdir in $file =="
sed -n '320,420p' "$file" | nl -ba | sed -n '1,120p'

echo
echo "== Search for /tmp/e2e-workdir creation/usage =="
rg -n "/tmp/e2e-workdir" "$file"

echo
echo "== Search for mkdir -p /tmp/e2e-workdir =="
rg -n "mkdir\s+-p\s+/tmp/e2e-workdir" "$file" || true

echo
echo "== Search for PR 1763 image refs =="
rg -n "openshell-supervisor:pr1763" -S --hidden .

echo
echo "== Inspect docs/backends/openshell.md around the PR merge note =="
target="docs/backends/openshell.md"
sed -n '1,120p' "$target" | nl -ba | sed -n '1,120p'
sed -n '180,260p' "$target" | nl -ba | sed -n '1,120p'

Repository: opendatahub-io/agentic-ci

Length of output: 214


🌐 Web query:

quay.io mprpic openshell-supervisor pr1763

💡 Result:

The query "quay.io mprpic openshell-supervisor pr1763" appears to conflate several distinct projects and references within the NVIDIA OpenShell ecosystem [1][2]. There is no evidence of a pull request #1763 in the NVIDIA/OpenShell repository specifically authored by "mprpic" (Martin Prpič) related to an "openshell-supervisor" image on quay.io [1][3][4]. Here is the context regarding the components mentioned: 1. OpenShell Supervisor: NVIDIA's OpenShell project utilizes an openshell-supervisor component, which is a specialized OCI image used for sandboxing [1][5]. While the project maintains its own container images (e.g., hosted at github.com/NVIDIA/OpenShell/pkgs/container/openshell/supervisor), it is distinct from the Quay.io container registry service [1][6]. 2. Martin Prpič (mprpic): A software developer known for contributions to Red Hat, Opendatahub, and other open-source projects [3][4]. There is no record of him being a primary contributor to the NVIDIA OpenShell supervisor image infrastructure [1][3][4]. 3. Quay.io: This is a container image registry service provided by Red Hat, not a component of the NVIDIA OpenShell software [6][7]. It is possible that "PR 1763" refers to a recent, ongoing, or closed pull request within the NVIDIA/OpenShell repository, but it does not involve the entities or registry paths identified in your query [2]. Other related PRs in the OpenShell repository, such as #1767, have recently addressed Kubernetes and AppArmor configurations for the supervisor [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

SKILL=".claude/skills/test-e2e-openshell/SKILL.md"
BACKEND="docs/backends/openshell.md"

python3 - <<'PY'
from pathlib import Path
path = Path(".claude/skills/test-e2e-openshell/SKILL.md")
lines = path.read_text().splitlines()
def show(start,end):
    for i in range(start-1,end):
        if 0 <= i < len(lines):
            print(f"{i+1}:{lines[i]}")
print("== SKILL.md: lines 320-360 ==")
show(320,360)
print("\n== SKILL.md: lines containing /tmp/e2e-workdir / e2e-workdir ==")
for i,l in enumerate(lines, start=1):
    if "/tmp/e2e-workdir" in l or "e2e-workdir" in l:
        print(f"{i}:{l}")
print("\n== SKILL.md: lines containing openshell-supervisor:pr1763 ==")
for i,l in enumerate(lines, start=1):
    if "openshell-supervisor:pr1763" in l:
        print(f"{i}:{l}")
PY

python3 - <<'PY'
from pathlib import Path
path = Path("docs/backends/openshell.md")
lines = path.read_text().splitlines()
def show(start,end):
    for i in range(start-1,end):
        if 0 <= i < len(lines):
            print(f"{i+1}:{lines[i]}")
print("== openshell.md: lines 40-90 (supervisor_image var) ==")
show(40,90)
print("\n== openshell.md: lines 200-240 (PR merge note / tag usage) ==")
show(200,240)
PY

echo
echo "== Quick grep for /tmp/e2e-workdir creation =="

rg -n "mkdir\s+-p\s+/tmp/e2e-workdir|/tmp/e2e-workdir.*(mkdir|create|rm|cleanup)" "$SKILL" || true

echo
echo "== Quick grep for PR tag mention =="

rg -n "pr1763|`#1763`|PR\s*#?1763" "$SKILL" "$BACKEND" || true

Repository: opendatahub-io/agentic-ci

Length of output: 6848


🏁 Script executed:

#!/bin/bash
set -euo pipefail
SKILL=".claude/skills/test-e2e-openshell/SKILL.md"

python3 - <<'PY'
from pathlib import Path
path = Path(".claude/skills/test-e2e-openshell/SKILL.md")
lines = path.read_text().splitlines()
def show(a,b):
    for i in range(a-1,b):
        if 0 <= i < len(lines):
            print(f"{i+1}:{lines[i]}")
print("== SKILL.md: occurrences of workdir/workdir-test ==")
import re
for i,l in enumerate(lines, start=1):
    if re.search(r'workdir', l, re.IGNORECASE):
        print(f"{i}:{l}")
PY

echo
echo "== SKILL.md: around 'Before you start' (first 260 lines search) =="
python3 - <<'PY'
from pathlib import Path
path=Path(".claude/skills/test-e2e-openshell/SKILL.md")
lines=path.read_text().splitlines()
for idx,l in enumerate(lines, start=1):
    if "Before you start" in l:
        start=max(1, idx-20); end=idx+60
        for i in range(start-1,end):
            if i < len(lines):
                print(f"{i+1}:{lines[i]}")
        break
else:
    print("No 'Before you start' heading found in first scan.")
PY

echo
echo "== SKILL.md: around section E and F commands (270-380) =="
python3 - <<'PY'
from pathlib import Path
p=Path(".claude/skills/test-e2e-openshell/SKILL.md')
lines=p.read_text().splitlines()
for i in range(250-1, 390):
    if 0<=i<len(lines):
        print(f"{i+1}:{lines[i]}")
PY

Repository: opendatahub-io/agentic-ci

Length of output: 4392


🏁 Script executed:

#!/bin/bash
set -euo pipefail
SKILL=".claude/skills/test-e2e-openshell/SKILL.md"

python3 - <<'PY'
from pathlib import Path
path = Path(".claude/skills/test-e2e-openshell/SKILL.md")
lines = path.read_text().splitlines()
import re
print("== SKILL.md: occurrences of workdir/workdir-test ==")
for i,l in enumerate(lines, start=1):
    if re.search(r'workdir', l, re.IGNORECASE) or "/tmp/" in l and "workdir" in l:
        print(f"{i}:{l}")
PY

echo
echo "== SKILL.md: around 'Before you start' heading =="
python3 - <<'PY'
from pathlib import Path
p=Path(".claude/skills/test-e2e-openshell/SKILL.md")
lines=p.read_text().splitlines()
for idx,l in enumerate(lines, start=1):
    if "Before you start" in l:
        start=max(1, idx-20); end=idx+70
        for i in range(start-1, min(end, len(lines))):
            print(f"{i+1}:{lines[i]}")
        break
else:
    print("No 'Before you start' heading found.")
PY

echo
echo "== SKILL.md: around section E and F commands (250-380) =="
python3 - <<'PY'
from pathlib import Path
p=Path(".claude/skills/test-e2e-openshell/SKILL.md")
lines=p.read_text().splitlines()
for i in range(250-1, min(390, len(lines))):
    print(f"{i+1}:{lines[i]}")
PY

Repository: opendatahub-io/agentic-ci

Length of output: 8395


Harden OPENSHELL_SUPERVISOR_IMAGE supply-chain input + fix invalid cd in Section F

  • .claude/skills/test-e2e-openshell/SKILL.md (Section F1): cd /tmp/e2e-workdir is used, but the skill only creates /tmp/workdir-test; add mkdir -p /tmp/e2e-workdir (or remove the cd) to prevent runtime failure (CWE-703).
  • The quay.io/mprpic/openshell-supervisor:pr1763 tag is intentional per docs/backends/openshell.md (it’s tied to NVIDIA/OpenShell PR #1763 for the GCE metadata emulator), but it’s still an unpinned external OCI tag (CWE-494, CWE-829). Pin the image by digest and require provenance/approval for the Quay source for CI/E2E.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/test-e2e-openshell/SKILL.md at line 335, In SKILL.md Section
F1 fix the failing directory change and harden the external image input: ensure
the script creates the expected working directory before cd by adding mkdir -p
/tmp/e2e-workdir (or remove the cd to /tmp/e2e-workdir) so that the step using
/tmp/e2e-workdir does not fail; also replace the unpinned
OPENSHELL_SUPERVISOR_IMAGE value
(OPENSHELL_SUPERVISOR_IMAGE=quay.io/mprpic/openshell-supervisor:pr1763) with a
digest-pinned reference and add a note requiring provenance/approval for the
Quay source for CI/E2E to prevent using an untrusted tag.

-e SANDBOX_IMAGE="$CLAUDE_SANDBOX_IMAGE" \
openshell-e2e bash -c '
cd /tmp/e2e-workdir && \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Command references uncreated directory.

cd /tmp/e2e-workdir attempts to change to a directory that is never created in the skill instructions. Section E uses /tmp/workdir-test (different path). Since Section F doesn't specify --workdir, this cd is likely unnecessary and will cause the command to fail.

🐛 Proposed fix
   openshell-e2e bash -c '
-    cd /tmp/e2e-workdir && \
     agentic-ci run \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cd /tmp/e2e-workdir && \
openshell-e2e bash -c '
agentic-ci run \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/test-e2e-openshell/SKILL.md at line 338, The command `cd
/tmp/e2e-workdir` references a directory that is never created (Section E uses
`/tmp/workdir-test`) and Section F omits `--workdir`, so either remove the `cd
/tmp/e2e-workdir` step, change it to `cd /tmp/workdir-test` to match Section E,
or explicitly create `/tmp/e2e-workdir` and/or add `--workdir /tmp/e2e-workdir`
to the command in Section F; update the `cd /tmp/e2e-workdir` occurrence
accordingly so the working directory used by the skill matches the created path
or is not required.

agentic-ci run \
--backend openshell \
--harness claude-code \
--image "$SANDBOX_IMAGE" \
--model claude-haiku-4-5 \
"Respond with exactly: F1_OK"
'
```

Note: no `--no-otel` flag.

Verify:
- Output shows `Running Claude Code (claude-haiku-4-5) via openshell backend`
- Agent runs and completes with `F1_OK` in the response
- Output shows `Token/Cost Summary (OpenTelemetry)` section
- Token counts are non-zero (input tokens, output tokens, cache)
- Cost is non-zero (e.g. `$0.04`)
- `Agent exit code: 0`
- `Sandbox deleted` and `Gateway stopped` at the end

---

## Final cleanup

```bash
Expand All @@ -319,7 +366,7 @@ podman rm -f openshell-e2e

## Running the full suite

Execute sections in order (A through E), running the cleanup step before each
Execute sections in order (A through F), running the cleanup step before each
section. Skip sections whose prerequisites are not met. If any step fails,
check the gateway log inside the container:

Expand Down
2 changes: 1 addition & 1 deletion src/agentic_ci/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(self, workdir=".", image=None, *, harness: Harness):
self.verdict_path: Path | None = None

@abstractmethod
def setup(self):
def setup(self, otel_port: int | None = None):
"""Prepare the backend. Idempotent."""

@abstractmethod
Expand Down
26 changes: 22 additions & 4 deletions src/agentic_ci/backends/openshell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
if TYPE_CHECKING:
from agentic_ci.harness import Harness

_OPENSHELL_HOST = "host.openshell.internal"


class OpenShellBackend(Backend):
"""Runs an AI agent inside an OpenShell sandbox.
Expand All @@ -38,7 +40,7 @@ def __init__(self, workdir=".", image=None, policy=None, extra_env=None, *, harn
self.policy_path = policy
self._extra_env = extra_env or {}

def setup(self):
def setup(self, otel_port=None):
if not gateway.is_running():
log.section("Starting OpenShell gateway")
gateway.start()
Expand All @@ -55,7 +57,7 @@ def setup(self):
image_info = f", image: {self.image}" if self.image else ""
log.section(f"Creating sandbox ({image_info.lstrip(', ') or 'default image'})")

sandbox.create(image=self.image, policy_path=self.policy_path)
sandbox.create(image=self.image, policy_path=self.policy_path, otel_port=otel_port)

log.section("Uploading workdir")
sandbox.upload(self.workdir)
Expand Down Expand Up @@ -107,9 +109,25 @@ def _write_env_script(self, model, otel_port=None, otel_rate_file=None):

Uses the harness's native env script (Vertex AI vars or API key)
since the google-cloud provider injects GCP credentials directly.

For OTEL, uses ``host.openshell.internal`` to reach the host-side
collector through the gateway proxy instead of the harness default
(which uses an IP unreachable from the sandbox).
"""
lines = self.harness.build_env_script_lines(otel_port, otel_rate_file)
lines.append("export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1")
lines = self.harness.build_env_script_lines()
if otel_port:
lines.extend(
[
"export CLAUDE_CODE_ENABLE_TELEMETRY=1",
"export OTEL_METRICS_EXPORTER=otlp",
"export OTEL_LOGS_EXPORTER=otlp",
"export OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
f"export OTEL_EXPORTER_OTLP_ENDPOINT=http://{_OPENSHELL_HOST}:{otel_port}",
"export OTEL_METRIC_EXPORT_INTERVAL=5000",
]
)
else:
lines.append("export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1")

for key, val in self._extra_env.items():
lines.append(f"export {key}={shlex.quote(val)}")
Expand Down
8 changes: 5 additions & 3 deletions src/agentic_ci/backends/openshell/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def exists():
return result.returncode == 0


def create(image=None, policy_path=None):
def create(image=None, policy_path=None, otel_port=None):
"""Create a persistent sandbox with the CI provider attached.

The sandbox is created first, then the network policy is applied
Expand All @@ -47,16 +47,18 @@ def create(image=None, policy_path=None):
args.extend(["--", "true"])
_run(args, check=True)

_apply_policy(policy_path)
_apply_policy(policy_path, otel_port=otel_port)


def _apply_policy(policy_path):
def _apply_policy(policy_path, otel_port=None):
"""Apply network policy endpoints and wait for activation.

Uses ``openshell policy update --wait`` which blocks until the
supervisor has compiled and loaded the new policy revision.
"""
endpoints = resolve_endpoints(policy_path)
if otel_port:
endpoints.append(f"host.openshell.internal:{otel_port}:read-write")
if not endpoints:
return

Expand Down
2 changes: 1 addition & 1 deletion src/agentic_ci/backends/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(
self._config_dir = None
self._extra_env = extra_env or {}

def setup(self):
def setup(self, otel_port=None):
self._resolve_image()
if self.harness.auth_mode == "vertex":
self._resolve_credentials()
Expand Down
11 changes: 8 additions & 3 deletions src/agentic_ci/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ def cmd_run(args, backend, harness):
sys.exit(0)
log.info(f"{gate.name}: passed")

backend.setup()

model_env = harness.model_env_var()
if args.model:
model = args.model
Expand All @@ -73,10 +71,16 @@ def cmd_run(args, backend, harness):
try:
if not args.no_otel and harness.supports_otel:
log.section("Starting OTEL collector")
otel_proc, otel_port, otel_log, otel_rate = otel.start_collector(run_dir)
bind_addr = "0.0.0.0" if args.backend == "openshell" else "127.0.0.1"
otel_proc, otel_port, otel_log, otel_rate = otel.start_collector(
run_dir, bind_addr=bind_addr
)
Comment thread
EmilienM marked this conversation as resolved.
os.environ["OTEL_RATE_FILE"] = otel_rate
log.detail("pid", str(otel_proc.pid))
log.detail("port", str(otel_port))

backend.setup(otel_port=otel_port)

log.section(f"Running {harness.name} ({model}) via {args.backend} backend")
rc = backend.run(
prompt=args.prompt,
Expand Down Expand Up @@ -128,6 +132,7 @@ def cmd_run(args, backend, harness):
finally:
if otel_proc:
otel.stop_collector(otel_proc)
os.environ.pop("OTEL_RATE_FILE", None)
if not args.keep:
backend.stop()

Expand Down
6 changes: 3 additions & 3 deletions src/agentic_ci/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from abc import ABC, abstractmethod
from typing import Any

_OPENSHELL_GATEWAY_HOST = "10.200.0.1"


class Harness(ABC):
"""Base class for agent CLI harnesses."""
Expand Down Expand Up @@ -148,12 +150,10 @@ def build_env_script_lines(self, otel_port=None, otel_rate_file=None):
"export OTEL_METRICS_EXPORTER=otlp",
"export OTEL_LOGS_EXPORTER=otlp",
"export OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
f"export OTEL_EXPORTER_OTLP_ENDPOINT=http://10.200.0.1:{otel_port}",
f"export OTEL_EXPORTER_OTLP_ENDPOINT=http://{_OPENSHELL_GATEWAY_HOST}:{otel_port}",
"export OTEL_METRIC_EXPORT_INTERVAL=10000",
]
)
if otel_rate_file:
lines.append(f"export OTEL_RATE_FILE={shlex.quote(otel_rate_file)}")
return lines

def build_otel_exec_env(self, otel_port=None):
Expand Down
8 changes: 5 additions & 3 deletions src/agentic_ci/otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _update_token_rate(payload):
os.replace(tmp, rate_file)


def start_collector(run_dir):
def start_collector(run_dir, bind_addr="127.0.0.1"):
"""Start the OTEL collector as a subprocess. Returns (proc, port)."""
otel_log = os.path.join(run_dir, "claude-otel.jsonl")
otel_rate = os.path.join(run_dir, "claude-otel-rate.json")
Expand All @@ -108,6 +108,7 @@ def start_collector(run_dir):
"OTEL_RATE_FILE": otel_rate,
"OTEL_COLLECTOR_PORT": "0",
"OTEL_PORT_FILE": port_file,
"OTEL_BIND_ADDR": bind_addr,
}
proc = subprocess.Popen(
[sys.executable, "-m", "agentic_ci.otel"],
Expand Down Expand Up @@ -258,7 +259,8 @@ def print_summary(log_file):
def main():
"""Run the OTEL collector server."""
port = int(os.environ.get("OTEL_COLLECTOR_PORT", "4318"))
server = HTTPServer(("127.0.0.1", port), OTLPHandler)
bind_addr = os.environ.get("OTEL_BIND_ADDR", "127.0.0.1")
server = HTTPServer((bind_addr, port), OTLPHandler)
actual_port = server.server_address[1]
port_file = os.environ.get("OTEL_PORT_FILE")
if port_file:
Expand All @@ -267,7 +269,7 @@ def main():
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
log_file = os.environ.get("OTEL_LOG_FILE", "/tmp/claude-otel.jsonl")
print(
f"OTLP collector listening on 127.0.0.1:{actual_port}, writing to {log_file}",
f"OTLP collector listening on {bind_addr}:{actual_port}, writing to {log_file}",
file=sys.stderr,
)
try:
Expand Down
8 changes: 4 additions & 4 deletions tests/test_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ def test_build_env_script_lines_api_key(self, monkeypatch):
assert not any("CLAUDE_CODE_USE_VERTEX" in line for line in lines)
assert not any("GOOGLE_APPLICATION_CREDENTIALS" in line for line in lines)

def test_build_env_script_lines_with_otel(self, monkeypatch, tmp_path):
def test_build_env_script_lines_with_otel(self, monkeypatch):
monkeypatch.setenv("ANTHROPIC_VERTEX_PROJECT_ID", "proj")
harness = ClaudeCodeHarness()
rate_file = str(tmp_path / "rate.json")
lines = harness.build_env_script_lines(otel_port=4318, otel_rate_file=rate_file)
lines = harness.build_env_script_lines(otel_port=4318)
assert any("CLAUDE_CODE_ENABLE_TELEMETRY=1" in line for line in lines)
assert any(f"OTEL_RATE_FILE={rate_file}" in line for line in lines)
assert any("OTEL_EXPORTER_OTLP_ENDPOINT=http://10.200.0.1:4318" in line for line in lines)
assert not any("OTEL_RATE_FILE" in line for line in lines)

def test_credential_mount_target(self):
assert ClaudeCodeHarness().credential_mount_target() == "/home/agent-ci"
Expand Down
Loading