Skip to content

Commit b343369

Browse files
authored
ci: improve workflow validation parity (#145)
1 parent e7f832f commit b343369

16 files changed

Lines changed: 687 additions & 108 deletions

.github/workflows/build_registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
from pathlib import Path
1313

1414
from registry_utils import (
15-
SKIP_DIRS,
1615
extract_npm_package_name,
1716
extract_npm_package_version,
1817
extract_pypi_package_name,
1918
normalize_version,
19+
should_skip_dir,
2020
)
2121

2222
try:
@@ -535,7 +535,7 @@ def build_registry(dry_run: bool = False):
535535
print(" Install with: pip install jsonschema")
536536

537537
for entry_dir in sorted(registry_dir.iterdir()):
538-
if not entry_dir.is_dir() or entry_dir.name in SKIP_DIRS:
538+
if not entry_dir.is_dir() or should_skip_dir(entry_dir.name):
539539
continue
540540

541541
agent_json_path = entry_dir / "agent.json"

.github/workflows/daily-protocol-matrix.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
XDG_CONFIG_HOME: ${{ runner.temp }}/xdg-config
7171
run: |
7272
AGENTS_INPUT=""
73-
SKIP_INPUT="crow-cli"
73+
SKIP_INPUT=""
7474
TABLE_MODE="capabilities"
7575
CHANGED_ONLY="false"
7676
@@ -86,7 +86,7 @@ jobs:
8686
ARGS=(
8787
--sandbox-dir .matrix-sandbox
8888
--output-dir .protocol-matrix
89-
--init-timeout 45
89+
--init-timeout 120
9090
--rpc-timeout 5
9191
--table-mode "$TABLE_MODE"
9292
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
find_nss_wrapper() {
5+
for candidate in /usr/lib/*/libnss_wrapper.so /lib/*/libnss_wrapper.so; do
6+
if [ -f "$candidate" ]; then
7+
printf '%s\n' "$candidate"
8+
return 0
9+
fi
10+
done
11+
return 1
12+
}
13+
14+
setup_user() {
15+
uid="$(id -u)"
16+
gid="$(id -g)"
17+
home_dir="${HOME:-/tmp}"
18+
passwd_file="$home_dir/.nss-passwd"
19+
group_file="$home_dir/.nss-group"
20+
21+
mkdir -p "$home_dir"
22+
export USER="${USER:-codex}"
23+
export LOGNAME="${LOGNAME:-$USER}"
24+
25+
if grep -Eq "^[^:]*:[^:]*:${uid}:" /etc/passwd; then
26+
return 0
27+
fi
28+
29+
cp /etc/passwd "$passwd_file"
30+
cp /etc/group "$group_file"
31+
32+
if ! grep -Eq "^[^:]*:[^:]*:${gid}:" "$group_file"; then
33+
printf '%s:x:%s:\n' "$USER" "$gid" >> "$group_file"
34+
fi
35+
36+
printf '%s:x:%s:%s:Codex Container User:%s:/bin/sh\n' "$USER" "$uid" "$gid" "$home_dir" >> "$passwd_file"
37+
38+
nss_wrapper_lib="$(find_nss_wrapper || true)"
39+
if [ -n "$nss_wrapper_lib" ]; then
40+
export NSS_WRAPPER_PASSWD="$passwd_file"
41+
export NSS_WRAPPER_GROUP="$group_file"
42+
if [ -n "${LD_PRELOAD:-}" ]; then
43+
export LD_PRELOAD="$nss_wrapper_lib:$LD_PRELOAD"
44+
else
45+
export LD_PRELOAD="$nss_wrapper_lib"
46+
fi
47+
fi
48+
}
49+
50+
setup_user
51+
exec "$@"

.github/workflows/docker/registry-tools.Dockerfile

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
FROM node:22-bookworm-slim
1+
FROM node:22-bookworm-slim AS node-runtime
2+
3+
FROM ubuntu:24.04
24

35
ENV DEBIAN_FRONTEND=noninteractive \
46
PIP_DISABLE_PIP_VERSION_CHECK=1 \
@@ -12,15 +14,26 @@ RUN apt-get update \
1214
&& apt-get install -y --no-install-recommends \
1315
ca-certificates \
1416
git \
17+
libgomp1 \
18+
libnss-wrapper \
1519
python3 \
1620
python3-pip \
1721
python3-venv \
1822
&& rm -rf /var/lib/apt/lists/*
1923

20-
RUN python3 -m venv /opt/uv \
24+
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
25+
COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules
26+
COPY docker/registry-entrypoint.sh /usr/local/bin/registry-entrypoint.sh
27+
28+
RUN rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
29+
&& ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
30+
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
31+
&& ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack \
32+
&& python3 -m venv /opt/uv \
2133
&& /opt/uv/bin/pip install --no-cache-dir uv \
2234
&& ln -s /opt/uv/bin/uv /usr/local/bin/uv \
2335
&& printf '#!/bin/sh\nexec /usr/local/bin/uv tool run "$@"\n' > /usr/local/bin/uvx \
24-
&& chmod +x /usr/local/bin/uvx
36+
&& chmod +x /usr/local/bin/uvx /usr/local/bin/registry-entrypoint.sh
2537

2638
WORKDIR /workspace
39+
ENTRYPOINT ["/usr/local/bin/registry-entrypoint.sh"]

.github/workflows/protocol_matrix.py

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@
3434

3535
from verify_agents import build_agent_command, load_registry, prepare_binary
3636

37-
DEFAULT_INIT_TIMEOUT = 45.0
37+
DEFAULT_INIT_TIMEOUT = 120.0
3838
DEFAULT_RPC_TIMEOUT = 5.0
39+
DEFAULT_EXIT_GRACE = 0.25
40+
EXIT_GRACE_POLL_INTERVAL = 0.05
41+
EXIT_GRACE_REAP_SLACK = 0.05
3942
DEFAULT_SANDBOX_DIR = ".matrix-sandbox"
4043
DEFAULT_OUTPUT_DIR = ".protocol-matrix"
4144
DEFAULT_TABLE_MODE = "full"
@@ -330,23 +333,79 @@ def send_jsonrpc_request(
330333
proc.stdin.flush()
331334

332335

336+
def process_exit_outcome(
337+
exit_code: int,
338+
method: str,
339+
) -> tuple[ProbeOutcome, dict[str, Any] | None]:
340+
"""Build a normalized process-exit outcome for a pending request."""
341+
return (
342+
ProbeOutcome(
343+
status="process_error",
344+
message=f"process exited with code {exit_code} before responding to {method}",
345+
),
346+
None,
347+
)
348+
349+
350+
def reconcile_timed_out_request(
351+
proc: subprocess.Popen,
352+
request_id: int,
353+
method: str,
354+
exit_grace: float,
355+
) -> tuple[ProbeOutcome, dict[str, Any] | None] | None:
356+
"""Reconcile a timed-out request with a near-immediate exit or late response."""
357+
if exit_grace <= 0:
358+
return None
359+
360+
deadline = time.monotonic() + exit_grace
361+
while True:
362+
remaining = deadline - time.monotonic()
363+
if remaining <= 0:
364+
break
365+
366+
message = read_jsonrpc_line(proc, min(remaining, EXIT_GRACE_POLL_INTERVAL))
367+
if message is None:
368+
exit_code = proc.poll()
369+
if exit_code is not None:
370+
return process_exit_outcome(exit_code, method)
371+
continue
372+
373+
if "_decode_error" in message:
374+
return (
375+
ProbeOutcome(status="decode_error", message=message["_decode_error"]),
376+
None,
377+
)
378+
379+
if message.get("id") == request_id and ("result" in message or "error" in message):
380+
return classify_rpc_response(message), message
381+
382+
exit_code = proc.poll()
383+
if exit_code is not None:
384+
return process_exit_outcome(exit_code, method)
385+
386+
if EXIT_GRACE_REAP_SLACK > 0:
387+
try:
388+
exit_code = proc.wait(timeout=EXIT_GRACE_REAP_SLACK)
389+
except subprocess.TimeoutExpired:
390+
exit_code = None
391+
if exit_code is not None:
392+
return process_exit_outcome(exit_code, method)
393+
394+
return None
395+
396+
333397
def request_with_timeout(
334398
proc: subprocess.Popen,
335399
request_id: int,
336400
method: str,
337401
params: dict[str, Any],
338402
timeout: float,
403+
exit_grace: float = DEFAULT_EXIT_GRACE,
339404
) -> tuple[ProbeOutcome, dict[str, Any] | None]:
340405
"""Send request and wait for the response with matching id."""
341406
exit_code = proc.poll()
342407
if exit_code is not None:
343-
return (
344-
ProbeOutcome(
345-
status="process_error",
346-
message=f"process exited with code {exit_code} before {method}",
347-
),
348-
None,
349-
)
408+
return process_exit_outcome(exit_code, method)
350409

351410
try:
352411
send_jsonrpc_request(proc, request_id, method, params)
@@ -372,15 +431,7 @@ def request_with_timeout(
372431
if message is None:
373432
exit_code = proc.poll()
374433
if exit_code is not None:
375-
return (
376-
ProbeOutcome(
377-
status="process_error",
378-
message=(
379-
f"process exited with code {exit_code} before responding to {method}"
380-
),
381-
),
382-
None,
383-
)
434+
return process_exit_outcome(exit_code, method)
384435
break
385436

386437
if "_decode_error" in message:
@@ -392,6 +443,10 @@ def request_with_timeout(
392443
if message.get("id") == request_id and ("result" in message or "error" in message):
393444
return classify_rpc_response(message), message
394445

446+
reconciled = reconcile_timed_out_request(proc, request_id, method, exit_grace)
447+
if reconciled is not None:
448+
return reconciled
449+
395450
return (
396451
ProbeOutcome(status="no_response", message=f"timeout after {timeout:.1f}s"),
397452
None,
@@ -737,11 +792,22 @@ def ensure_binary_executable(cmd: list[str], dist_type: str) -> ProbeOutcome | N
737792
return None
738793

739794
exe_path = Path(cmd[0])
740-
if not exe_path.exists() or os.access(exe_path, os.X_OK):
795+
if not exe_path.exists():
796+
return None
797+
798+
try:
799+
current_mode = exe_path.stat().st_mode
800+
except OSError as exc:
801+
return ProbeOutcome(
802+
status="process_error",
803+
message=short_message(f"Failed to inspect executable {exe_path}: {exc}"),
804+
)
805+
806+
if current_mode & 0o111:
741807
return None
742808

743809
try:
744-
exe_path.chmod(exe_path.stat().st_mode | 0o755)
810+
exe_path.chmod(current_mode | 0o755)
745811
except OSError as exc:
746812
return ProbeOutcome(
747813
status="process_error",

.github/workflows/registry_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
}
1919

2020

21+
def should_skip_dir(name: str) -> bool:
22+
"""Return whether a top-level directory should be skipped during registry scans."""
23+
return name in SKIP_DIRS or name.startswith(".")
24+
25+
2126
def extract_npm_package_name(package_spec: str) -> str:
2227
"""Extract npm package name from spec like @scope/name@version."""
2328
if package_spec.startswith("@"):

.github/workflows/scripts/run-protocol-matrix.sh

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,64 @@ set -euo pipefail
33

44
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
TABLE_MODE="${ACP_PROTOCOL_MATRIX_TABLE_MODE:-capabilities}"
7+
SKIP_AGENTS="${ACP_PROTOCOL_MATRIX_SKIP_AGENTS:-}"
8+
KEEP_STATE="${ACP_PROTOCOL_MATRIX_KEEP_STATE:-0}"
9+
SANDBOX_DIR="${ACP_PROTOCOL_MATRIX_SANDBOX_DIR:-}"
10+
TEMP_STATE_DIR=""
11+
TEMP_SANDBOX_DIR=""
12+
TEMP_SANDBOX_ROOT="$ROOT/.matrix-sandbox/.tmp"
13+
TEMP_STATE_ROOT="$ROOT/.docker-state/.tmp"
14+
15+
cleanup() {
16+
if [[ -n "$TEMP_SANDBOX_DIR" ]]; then
17+
rm -rf "$TEMP_SANDBOX_DIR"
18+
fi
19+
if [[ -n "$TEMP_STATE_DIR" ]]; then
20+
rm -rf "$TEMP_STATE_DIR"
21+
fi
22+
}
23+
24+
if [[ -z "$SANDBOX_DIR" ]]; then
25+
if [[ "$KEEP_STATE" == "1" ]]; then
26+
SANDBOX_DIR=".matrix-sandbox"
27+
else
28+
mkdir -p "$TEMP_SANDBOX_ROOT"
29+
TEMP_SANDBOX_DIR="$(mktemp -d "$TEMP_SANDBOX_ROOT/protocol-matrix-sandbox.XXXXXX")"
30+
SANDBOX_DIR="${TEMP_SANDBOX_DIR#"$ROOT"/}"
31+
fi
32+
fi
33+
634
ARGS=(
735
python3
836
.github/workflows/protocol_matrix.py
937
--sandbox-dir
10-
.matrix-sandbox
38+
"$SANDBOX_DIR"
1139
--output-dir
1240
.protocol-matrix
41+
--init-timeout
42+
120
43+
--rpc-timeout
44+
5
45+
--table-mode
46+
"$TABLE_MODE"
1347
)
1448

15-
if [[ -n "${ACP_PROTOCOL_MATRIX_SKIP_AGENTS:-}" ]]; then
16-
ARGS+=(--skip-agent "$ACP_PROTOCOL_MATRIX_SKIP_AGENTS")
49+
if [[ -n "$SKIP_AGENTS" ]]; then
50+
ARGS+=(--skip-agent "$SKIP_AGENTS")
51+
fi
52+
53+
if [[ -z "${ACP_REGISTRY_STATE_DIR:-}" && "$KEEP_STATE" != "1" ]]; then
54+
mkdir -p "$TEMP_STATE_ROOT"
55+
TEMP_STATE_DIR="$(mktemp -d "$TEMP_STATE_ROOT/protocol-matrix-state.XXXXXX")"
56+
fi
57+
58+
if [[ -n "$TEMP_SANDBOX_DIR" || -n "$TEMP_STATE_DIR" ]]; then
59+
trap cleanup EXIT
60+
fi
61+
62+
if [[ -n "$TEMP_STATE_DIR" ]]; then
63+
export ACP_REGISTRY_STATE_DIR="${TEMP_STATE_DIR#"$ROOT"/}"
1764
fi
1865

19-
exec "$SCRIPT_DIR/run-registry-docker.sh" "${ARGS[@]}" "$@"
66+
"$SCRIPT_DIR/run-registry-docker.sh" "${ARGS[@]}" "$@"

0 commit comments

Comments
 (0)